Skip to content

Commit 3bff76d

Browse files
test(workspace): fail any test that leaves a skill sync running
Tests that recorded a link without `awaitBackfill`, or used the TUI create path, left syncs running into later files; some never settled and stalled every later `flushPendingSyncs()`. Await them, fix the one test that relied on the leak, and check for leaks in the test preload. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174HUYiaceApqHP7EKMNgnb
1 parent c89f457 commit 3bff76d

9 files changed

Lines changed: 95 additions & 42 deletions

File tree

‎packages/opencode/test/altimate/plugin/workspace.test.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ describe("workspace binding cache", () => {
156156
repoRemote: "git@github.com:acme/proj-a.git",
157157
projectPath: "/work/proj-a",
158158
linkedAt: 1_700_000_000_000,
159-
})
159+
}, { awaitBackfill: true })
160160

161161
const read = await readLocalBinding("/work/proj-a")
162162
expect(read).not.toBeNull()
@@ -470,7 +470,7 @@ describe("workspace binding cache", () => {
470470
repoRemote: "git@github.com:acme/x.git",
471471
projectPath: "/work/proj-a",
472472
linkedAt: 1,
473-
})
473+
}, { awaitBackfill: true })
474474
expect(existsSync(cachePath())).toBe(true)
475475
const mode = statSync(cachePath()).mode & 0o777
476476
expect(mode).toBe(0o600)
@@ -483,7 +483,7 @@ describe("workspace binding cache", () => {
483483
repoRemote: "git@github.com:acme/proj-a.git",
484484
projectPath: "/work/proj-a",
485485
linkedAt: 1,
486-
})
486+
}, { awaitBackfill: true })
487487

488488
// Switch account → the cached binding must not be surfaced.
489489
unstubCreds()
@@ -500,7 +500,7 @@ describe("workspace binding cache", () => {
500500
repoRemote: "git@github.com:acme/proj-a.git",
501501
projectPath: "/work/proj-a",
502502
linkedAt: 1,
503-
})
503+
}, { awaitBackfill: true })
504504

505505
unstubCreds()
506506
stubCreds("acme", "https://different-host.example.com")
@@ -516,7 +516,7 @@ describe("workspace binding cache", () => {
516516
repoRemote: "git@github.com:acme/proj-a.git",
517517
projectPath: "/work/proj-a",
518518
linkedAt: 1,
519-
})
519+
}, { awaitBackfill: true })
520520
const read = await readLocalBinding("/work/proj-b")
521521
expect(read).toBeNull()
522522
})

‎packages/opencode/test/altimate/workspace/create-then-rebind.test.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const { createAndBindInline, bindOrRebindInline, runWorkspaceManage } = await im
4848
"@/plugin/tui/altimate/workspace"
4949
)
5050
const { recordApprovedBinding } = await import("@/altimate/workspace/state")
51+
const { flushPendingSyncs } = await import("@/altimate/workspace/skill-sync")
5152
const { HIDDEN_BINDING_MESSAGE, QUICK_WORKSPACE_PRIVATE_NOTE } = await import(
5253
"@/altimate/workspace/api-client"
5354
)
@@ -116,7 +117,10 @@ beforeEach(() => {
116117
opened.length = 0
117118
stubFetch()
118119
})
119-
afterEach(() => {
120+
afterEach(async () => {
121+
// The TUI create leaves its skill sync detached so the dialog can close at once. Settled here,
122+
// against this file's stub, so it cannot run on into the next test.
123+
await flushPendingSyncs()
120124
globalThis.fetch = ORIGINAL_FETCH
121125
process.exitCode = 0 // Bun ignores `= undefined`; a leaked 1 fails later files
122126
})
@@ -420,7 +424,7 @@ describe("TUI: /workspace menu options", () => {
420424
repoRemote: BINDING.repo_remote,
421425
projectPath: null,
422426
linkedAt: Date.now(),
423-
})
427+
}, { awaitBackfill: true })
424428
const m = menuApi()
425429
await runWorkspaceManage(m.api, "/tmp/proj")
426430
const values = m.cap.options.map((o) => o.value)

‎packages/opencode/test/altimate/workspace/identity-section.test.ts‎

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ describe("systemSection", () => {
123123
repoRemote: null,
124124
projectPath: projectDir,
125125
linkedAt: Date.now(),
126-
})
126+
}, { awaitBackfill: true })
127127
resetEnablementMemoForTests()
128128
memoryEnabledCache.set(77, { checkedAt: Date.now() })
129129
try {
@@ -244,7 +244,7 @@ describe("systemSection", () => {
244244
repoRemote: null,
245245
projectPath: projectDir,
246246
linkedAt: Date.now(),
247-
})
247+
}, { awaitBackfill: true })
248248
expect(await inProject(systemSection)).toContain('is "analytics"')
249249
await clearLocalBinding(projectDir, { scope: ACME_SCOPE })
250250
globalThis.fetch = (async () =>
@@ -283,7 +283,7 @@ describe("systemSection", () => {
283283
repoRemote: null,
284284
projectPath: projectDir,
285285
linkedAt: Date.now() - 10 * 60 * 1000,
286-
})
286+
}, { awaitBackfill: true })
287287
const { expireValidationForTests } = await import("../../../src/altimate/workspace/state")
288288
expireValidationForTests?.(projectDir)
289289
let unlinkedMidFlight = false
@@ -329,7 +329,7 @@ describe("systemSection", () => {
329329
repoRemote: null,
330330
projectPath: projectDir,
331331
linkedAt: Date.now() - 10 * 60 * 1000,
332-
})
332+
}, { awaitBackfill: true })
333333
const { expireValidationForTests } = await import("../../../src/altimate/workspace/state")
334334
expireValidationForTests(projectDir)
335335
globalThis.fetch = (async () => {
@@ -399,7 +399,7 @@ describe("systemSection", () => {
399399
repoRemote: null,
400400
projectPath: projectDir,
401401
linkedAt: Date.now() - 10 * 60 * 1000,
402-
})
402+
}, { awaitBackfill: true })
403403
const { expireValidationForTests } = await import("../../../src/altimate/workspace/state")
404404
expireValidationForTests(projectDir)
405405
// First step: bound (served from cache while the server is asked).
@@ -427,7 +427,7 @@ describe("systemSection", () => {
427427
repoRemote: null,
428428
projectPath: other,
429429
linkedAt: Date.now(),
430-
})
430+
}, { awaitBackfill: true })
431431
globalThis.fetch = (async () =>
432432
new Response(JSON.stringify({ detail: "not found" }), {
433433
status: 404,
@@ -469,7 +469,7 @@ describe("systemSection", () => {
469469
repoRemote: null,
470470
projectPath: projectDir,
471471
linkedAt: Date.now(),
472-
})
472+
}, { awaitBackfill: true })
473473
expect(await inProject(systemSection)).toContain('is "Linked"')
474474
})
475475

@@ -484,15 +484,13 @@ describe("systemSection", () => {
484484
repoRemote: null,
485485
projectPath: projectDir,
486486
linkedAt: Date.now() - 10 * 60 * 1000,
487-
})
487+
}, { awaitBackfill: true })
488488
const { expireValidationForTests } = await import("../../../src/altimate/workspace/state")
489489
expireValidationForTests(projectDir)
490490
let release!: () => void
491491
const gate = new Promise<void>((r) => (release = r))
492492
let firstStarted!: () => void
493493
const firstOnWire = new Promise<void>((r) => (firstStarted = r))
494-
let secondStarted!: () => void
495-
const secondOnWire = new Promise<void>((r) => (secondStarted = r))
496494
let calls = 0
497495
globalThis.fetch = (async () => {
498496
calls++
@@ -507,7 +505,6 @@ describe("systemSection", () => {
507505
{ status: 200, headers: { "content-type": "application/json" } },
508506
)
509507
}
510-
secondStarted()
511508
return new Response(JSON.stringify({ detail: "not found" }), {
512509
status: 404,
513510
headers: { "content-type": "application/json" },
@@ -516,11 +513,12 @@ describe("systemSection", () => {
516513
const first = inProject(systemSection)
517514
await firstOnWire // step 1's request is out and parked on the gate
518515
await clearLocalBinding(projectDir, { scope: ACME_SCOPE })
519-
const second = inProject(systemSection)
520-
await secondOnWire // step 2 made its OWN request while step 1 was still pending
516+
// Step 2 answers while step 1 is still parked. The unlink records this project as unbound, so
517+
// step 2 needs no request of its own; joined to step 1's resolve, it could only render what
518+
// step 1 knew.
519+
const next = await inProject(systemSection)
521520
release()
522-
const [, next] = await Promise.all([first, second])
523-
expect(calls).toBe(2)
521+
await first
524522
expect(next).not.toContain('is "old"')
525523
expect(next).toContain("No Altimate Workspace")
526524
})
@@ -599,7 +597,7 @@ describe("systemSection", () => {
599597
repoRemote: null,
600598
projectPath: projectDir,
601599
linkedAt: Date.now(),
602-
})
600+
}, { awaitBackfill: true })
603601
setCreds("acme")
604602
let switched = false
605603
globalThis.fetch = (() =>
@@ -634,7 +632,7 @@ describe("systemSection", () => {
634632
repoRemote: null,
635633
projectPath: projectDir,
636634
linkedAt: Date.now(),
637-
})
635+
}, { awaitBackfill: true })
638636
let t = 5_000_000
639637
setClockForTests(() => t)
640638
expect(await inProject(systemSection)).toContain('is "mine"') // memo filled for A
@@ -746,7 +744,7 @@ describe("systemSection", () => {
746744
repoRemote: null,
747745
projectPath: projectDir,
748746
linkedAt: Date.now(),
749-
})
747+
}, { awaitBackfill: true })
750748
const saved: Record<string, string | undefined> = {}
751749
const pinEnv: Record<string, string> = {
752750
ALTIMATE_CODE_SERVE: "1",
@@ -787,7 +785,7 @@ describe("systemSection", () => {
787785
repoRemote: null,
788786
projectPath: projectDir,
789787
linkedAt: Date.now(),
790-
})
788+
}, { awaitBackfill: true })
791789
const saved: Record<string, string | undefined> = {}
792790
const pinEnv: Record<string, string> = {
793791
ALTIMATE_CODE_SERVE: "1",

‎packages/opencode/test/altimate/workspace/launch-resolve.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ describe("resolveWorkspaceForLaunch", () => {
120120
repoRemote: null,
121121
projectPath: DIRECTORY,
122122
linkedAt: 0,
123-
})
123+
}, { awaitBackfill: true })
124124
})
125125

126126
test("no --workspace arg → no env var set, no-op", async () => {

‎packages/opencode/test/altimate/workspace/manage.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ describe("renames", () => {
371371
repoRemote: "git@github.com:acme/app.git",
372372
projectPath: projectDir,
373373
linkedAt: Date.now(),
374-
} as any)
374+
} as any, { awaitBackfill: true })
375375
expect(fired).toBeGreaterThan(0)
376376
} finally {
377377
stop()

‎packages/opencode/test/altimate/workspace/memory-sync.test.ts‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,7 +1199,7 @@ describe("reaping blocks archived elsewhere", () => {
11991199
await recordApprovedBinding(
12001200
here,
12011201
{ datamateId: 99, datamateName: "other", repoRemote: null, projectPath: here, linkedAt: Date.now() },
1202-
{ seed: false },
1202+
{ awaitBackfill: true, seed: false },
12031203
)
12041204
}
12051205
return inner(scope, id, directory)
@@ -1705,7 +1705,7 @@ describe("binding changes", () => {
17051705
const b = { ...BINDING, datamateId: 43, datamateName: "beta", projectPath: dir, linkedAt: 2 }
17061706
workspaces = [...workspaces, { id: 43, name: "beta", memory_enabled: true }]
17071707
syncInternals.resolveBinding = async () => b as any
1708-
await recordApprovedBinding(dir, b)
1708+
await recordApprovedBinding(dir, b, { awaitBackfill: true })
17091709
await hydrate(SES)
17101710
expect(overlayBlocks(SES).map((x) => x.id)).toEqual(["from-b"])
17111711
})
@@ -1721,7 +1721,7 @@ describe("binding changes", () => {
17211721
// The first lookup adopts (and notifies); later ones read the cache, as in production.
17221722
if (!adopted) {
17231723
adopted = true
1724-
await recordApprovedBinding(dir, { ...BINDING, projectPath: dir, linkedAt: 3 }, { seed: false })
1724+
await recordApprovedBinding(dir, { ...BINDING, projectPath: dir, linkedAt: 3 }, { awaitBackfill: true, seed: false })
17251725
}
17261726
return BINDING as any
17271727
}
@@ -1752,7 +1752,7 @@ describe("refresh racing a relink", () => {
17521752
await reachedList
17531753
const { recordApprovedBinding } = await import("../../../src/altimate/workspace/state")
17541754
const dir = mkdtempSync(path.join(SANDBOX, "race-"))
1755-
await recordApprovedBinding(dir, { ...BINDING, datamateId: 44, projectPath: dir, linkedAt: 4 }, { seed: false })
1755+
await recordApprovedBinding(dir, { ...BINDING, datamateId: 44, projectPath: dir, linkedAt: 4 }, { awaitBackfill: true, seed: false })
17561756
release?.()
17571757
const result = await pending
17581758
// Superseded: neither its read nor the prior overlay belongs to the new binding.
@@ -1776,7 +1776,7 @@ describe("overlay invalidation", () => {
17761776
await hydrate(SES)
17771777
expect(overlayBlocks(SES).length).toBe(1)
17781778
const dir = mkdtempSync(path.join(SANDBOX, "hide-"))
1779-
await recordApprovedBinding(dir, { ...BINDING, datamateId: 45, projectPath: dir, linkedAt: 5 }, { seed: false })
1779+
await recordApprovedBinding(dir, { ...BINDING, datamateId: 45, projectPath: dir, linkedAt: 5 }, { awaitBackfill: true, seed: false })
17801780
expect(overlayBlocks(SES)).toEqual([])
17811781
})
17821782

@@ -1819,7 +1819,7 @@ describe("epoch bracketing and scope", () => {
18191819
calls++
18201820
// First lookup returns A, but the relink to B lands before it returns.
18211821
if (calls === 1) {
1822-
await recordApprovedBinding(dir, { ...BINDING, datamateId: 46, projectPath: dir, linkedAt: 6 }, { seed: false })
1822+
await recordApprovedBinding(dir, { ...BINDING, datamateId: 46, projectPath: dir, linkedAt: 6 }, { awaitBackfill: true, seed: false })
18231823
return BINDING as any
18241824
}
18251825
return { ...BINDING, datamateId: 46 } as any
@@ -1841,10 +1841,10 @@ describe("epoch bracketing and scope", () => {
18411841
const other = mkdtempSync(path.join(SANDBOX, "other-"))
18421842
await refresh(SES, mine)
18431843
expect(overlayBlocks(SES).map((x) => x.id)).toEqual(["mine"])
1844-
await recordApprovedBinding(other, { ...BINDING, datamateId: 47, projectPath: other, linkedAt: 7 }, { seed: false })
1844+
await recordApprovedBinding(other, { ...BINDING, datamateId: 47, projectPath: other, linkedAt: 7 }, { awaitBackfill: true, seed: false })
18451845
expect(overlayBlocks(SES).map((x) => x.id)).toEqual(["mine"])
18461846
// A change to this project's own binding still hides it.
1847-
await recordApprovedBinding(mine, { ...BINDING, datamateId: 48, projectPath: mine, linkedAt: 8 }, { seed: false })
1847+
await recordApprovedBinding(mine, { ...BINDING, datamateId: 48, projectPath: mine, linkedAt: 8 }, { awaitBackfill: true, seed: false })
18481848
expect(overlayBlocks(SES)).toEqual([])
18491849
})
18501850
})
@@ -1887,7 +1887,7 @@ describe("superseded failures", () => {
18871887
await reachedList
18881888
const dir = mkdtempSync(path.join(SANDBOX, "supersede-"))
18891889
syncInternals.resolveBinding = async () => b as any
1890-
await recordApprovedBinding(dir, { ...b, projectPath: dir, linkedAt: 9 }, { seed: false })
1890+
await recordApprovedBinding(dir, { ...b, projectPath: dir, linkedAt: 9 }, { awaitBackfill: true, seed: false })
18911891
release?.()
18921892
await first
18931893
failList = false

‎packages/opencode/test/altimate/workspace/routing-pin.test.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,27 @@ function stubCreds() {
3939
({ altimateInstanceName: "acme", altimateUrl: "https://api.test", altimateApiKey: "k" }) as Creds
4040
}
4141

42+
const originalFetch = globalThis.fetch
43+
44+
/** Seeding a link runs a skill sync (awaited, see `seedLocalLink`) that lists the workspace's
45+
* skills from `api.test`. Answered here with an empty page, so that sync finishes offline instead
46+
* of waiting on however long a resolver takes to fail a name that cannot exist. */
47+
function stubFetch() {
48+
globalThis.fetch = (async (input: string | URL, init?: RequestInit) => {
49+
const url = new URL(String(input))
50+
if (url.host !== "api.test") return originalFetch(input, init)
51+
const skills = url.pathname.endsWith("/skills")
52+
// The page is echoed back: the skill-list parser rejects a page that does not match its request.
53+
const body = skills
54+
? { items: [], page: Number(url.searchParams.get("page") ?? "1"), pages: 1, total: 0 }
55+
: { detail: "not found" }
56+
return new Response(JSON.stringify(body), {
57+
status: skills ? 200 : 404,
58+
headers: { "content-type": "application/json" },
59+
})
60+
}) as typeof fetch
61+
}
62+
4263
let listCalls = 0
4364

4465
function stubList(rows: { id: number; name: string }[]) {
@@ -105,6 +126,7 @@ beforeEach(() => {
105126
listCalls = 0
106127
__resetPinValidation()
107128
stubCreds()
129+
stubFetch()
108130
stubList([
109131
{ id: 42, name: "pinned-workspace" },
110132
{ id: 7, name: "project-link" },
@@ -133,6 +155,7 @@ afterAll(() => {
133155
;(AltimateApi as unknown as { isConfigured: unknown }).isConfigured = originalIsConfigured
134156
;(AltimateApi as unknown as { getCredentials: unknown }).getCredentials = originalGetCreds
135157
;(WorkspaceApi as unknown as { listDatamates: unknown }).listDatamates = originalList
158+
globalThis.fetch = originalFetch
136159
if (ORIGINAL_XDG_STATE_HOME === undefined) delete process.env.XDG_STATE_HOME
137160
else process.env.XDG_STATE_HOME = ORIGINAL_XDG_STATE_HOME
138161
rmSync(SANDBOX, { recursive: true, force: true })

‎packages/opencode/test/altimate/workspace/state-account-scope.test.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ describe("binding cache is scoped to the account, not the tenant", () => {
8080
// A's cached binding — and loaded A's private workspace's skills — without
8181
// any visibility check of its own.
8282
asAccount("key-A")
83-
await recordApprovedBinding(ROOT, binding(7, "A's private workspace"), { seed: false })
83+
await recordApprovedBinding(ROOT, binding(7, "A's private workspace"), { awaitBackfill: true, seed: false })
8484
expect((await readLocalBinding(ROOT))?.datamateId).toBe(7)
8585

8686
asAccount("key-B")
@@ -92,7 +92,7 @@ describe("binding cache is scoped to the account, not the tenant", () => {
9292
// Guards the one above: rejecting every read would satisfy it while making
9393
// the cache useless.
9494
asAccount("key-A")
95-
await recordApprovedBinding(ROOT, binding(7, "A's private workspace"), { seed: false })
95+
await recordApprovedBinding(ROOT, binding(7, "A's private workspace"), { awaitBackfill: true, seed: false })
9696
asAccount("key-B")
9797
expect(await readLocalBinding(ROOT)).toBeNull()
9898

@@ -102,9 +102,9 @@ describe("binding cache is scoped to the account, not the tenant", () => {
102102

103103
test("a write by the other account evicts this one's rows entirely", async () => {
104104
asAccount("key-A")
105-
await recordApprovedBinding(ROOT, binding(7, "A's workspace"), { seed: false })
105+
await recordApprovedBinding(ROOT, binding(7, "A's workspace"), { awaitBackfill: true, seed: false })
106106
asAccount("key-B")
107-
await recordApprovedBinding(ROOT, binding(9, "B's workspace"), { seed: false })
107+
await recordApprovedBinding(ROOT, binding(9, "B's workspace"), { awaitBackfill: true, seed: false })
108108

109109
expect((await readLocalBinding(ROOT))?.datamateId).toBe(9)
110110
asAccount("key-A")
@@ -140,7 +140,7 @@ describe("binding cache is scoped to the account, not the tenant", () => {
140140
// their row — the cache is per credential now, so it is not theirs to
141141
// touch.
142142
asAccount("key-B")
143-
await recordApprovedBinding(ROOT, binding(9, "B's workspace"), { seed: false })
143+
await recordApprovedBinding(ROOT, binding(9, "B's workspace"), { awaitBackfill: true, seed: false })
144144
expect((await readLocalBinding(ROOT))?.datamateId).toBe(9)
145145

146146
asAccount("key-A")

0 commit comments

Comments
 (0)