| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | import { createSessionLifecycleFences, sessionLifecycleFences } from "../lib/sessionLifecycleFences"; |
| 4 | import { createProjectTreeRuntimeProjection } from "../lib/projectTreeRuntime"; |
| 5 | import { projectSessionIdentity } from "../lib/projectSessionIdentity"; |
| 6 | import type { ProjectNode, ProjectRuntimeTopic } from "../lib/types"; |
| 7 | |
| 8 | function session(id: string, lifecycleGeneration = 10): ProjectNode { |
| 9 | return { |
| 10 | key: id, kind: "topic", topicId: "shared-topic", label: id, root: "/repo", |
| 11 | session: { hostId: "local", sessionId: id }, lifecycleGeneration, children: [], |
| 12 | }; |
| 13 | } |
| 14 | function catalog(...rows: ProjectNode[]): ProjectNode[] { |
| 15 | return [{ key: "project", kind: "project", root: "/repo", label: "Project", children: rows }]; |
| 16 | } |
| 17 | function runtime(node: ProjectNode): ProjectRuntimeTopic[] { |
| 18 | return [{ scope: "project", workspaceRoot: "/repo", node: { ...node, running: true, runtimeOnly: true } }]; |
| 19 | } |
| 20 | |
| 21 | test("the application fence survives sidebar projection disposal and remount", () => { |
| 22 | const archived = session("remount-archived"); |
| 23 | const sibling = session("remount-sibling"); |
| 24 | sessionLifecycleFences.archive(archived, { lifecycleGeneration: 10, operationId: "archive-remount" }); |
| 25 | try { |
| 26 | const firstMount = createProjectTreeRuntimeProjection().apply(catalog(sibling, archived), runtime(archived), sessionLifecycleFences.keys()); |
| 27 | assert.deepEqual(firstMount[0]?.children?.map(node => node.session?.sessionId), ["remount-sibling"]); |
| 28 | const secondMount = createProjectTreeRuntimeProjection().apply(catalog(sibling, archived), runtime(archived), sessionLifecycleFences.keys()); |
| 29 | assert.deepEqual(secondMount[0]?.children?.map(node => node.session?.sessionId), ["remount-sibling"]); |
| 30 | assert.equal(sessionLifecycleFences.keys().has(projectSessionIdentity(archived)), true); |
| 31 | } finally { |
| 32 | sessionLifecycleFences.observeDirectory([session("remount-archived", 11)]); |
| 33 | } |
| 34 | }); |
| 35 | |
| 36 | test("old directory rows and runtime-only rows cannot release a committed archive fence", () => { |
| 37 | const fences = createSessionLifecycleFences(); |
| 38 | const archived = session("b", 20); |
| 39 | const alias = "source\x00local\x00legacy-b"; |
| 40 | fences.archive(archived, { lifecycleGeneration: 20, operationId: "archive-b", identityAliases: [alias] }); |
| 41 | fences.observeDirectory(catalog(session("b", 19))); |
| 42 | fences.observeDirectory(catalog(session("b", 20))); |
| 43 | fences.observeDirectory(catalog({ ...session("b", 999), runtimeOnly: true })); |
| 44 | assert.equal(fences.keys().has(projectSessionIdentity(archived)), true); |
| 45 | assert.equal(fences.keys().has(alias), true); |
| 46 | const projected = createProjectTreeRuntimeProjection().apply(catalog(), runtime(session("b", 999)), fences.keys()); |
| 47 | assert.deepEqual(projected[0]?.children, []); |
| 48 | }); |
| 49 | |
| 50 | test("a newer restored directory row releases only its exact session and source aliases", () => { |
| 51 | const fences = createSessionLifecycleFences(); |
| 52 | const a = session("a"); |
| 53 | const b = session("b"); |
| 54 | const aliasA = "source\x00local\x00legacy-a"; |
| 55 | const aliasB = "source\x00local\x00legacy-b"; |
| 56 | fences.archive(a, { lifecycleGeneration: 10, operationId: "archive-a", identityAliases: [aliasA] }); |
| 57 | fences.archive(b, { lifecycleGeneration: 10, operationId: "archive-b", identityAliases: [aliasB] }); |
| 58 | const restored = session("a", 11); |
| 59 | fences.observeDirectory(catalog(restored, b)); |
| 60 | assert.equal(fences.keys().has(projectSessionIdentity(a)), false); |
| 61 | assert.equal(fences.keys().has(aliasA), false); |
| 62 | assert.equal(fences.keys().has(projectSessionIdentity(b)), true); |
| 63 | assert.equal(fences.keys().has(aliasB), true); |
| 64 | const projected = createProjectTreeRuntimeProjection().apply(catalog(restored, b), runtime(b), fences.keys()); |
| 65 | assert.deepEqual(projected[0]?.children?.map(node => node.session?.sessionId), ["a"]); |
| 66 | }); |
| 67 | |
| 68 | test("a late archive receipt cannot weaken a newer fence for the same session", () => { |
| 69 | const fences = createSessionLifecycleFences(); |
| 70 | const b = session("b"); |
| 71 | fences.archive(b, { lifecycleGeneration: 30, operationId: "new-archive" }); |
| 72 | fences.archive(b, { lifecycleGeneration: 10, operationId: "old-archive" }); |
| 73 | fences.observeDirectory([session("b", 20)]); |
| 74 | assert.equal(fences.keys().has(projectSessionIdentity(b)), true); |
| 75 | fences.observeDirectory([session("b", 31)]); |
| 76 | assert.equal(fences.keys().has(projectSessionIdentity(b)), false); |
| 77 | }); |
| 78 |