| 1 | import assert from "node:assert/strict"; |
| 2 | import { createProjectTreeRuntimeProjection, projectTreeApplyRuntimeTopics } from "../lib/projectTreeRuntime"; |
| 3 | import type { ProjectNode } from "../lib/types"; |
| 4 | |
| 5 | const catalog: ProjectNode[] = [ |
| 6 | { key: "project-a", kind: "project", label: "A", root: "/a", children: [ |
| 7 | { key: "known", kind: "topic", label: "Known", root: "/a", topicId: "known", children: [] }, |
| 8 | ] }, |
| 9 | { key: "project-b", kind: "project", label: "B", root: "/b", children: [] }, |
| 10 | ]; |
| 11 | const overlaid = projectTreeApplyRuntimeTopics(catalog, [ |
| 12 | { scope: "project", workspaceRoot: "/a", node: { |
| 13 | key: "known", kind: "topic", label: "Known live", root: "/a", topicId: "known", running: true, status: "thinking", children: [ |
| 14 | { key: "known-a", kind: "session", label: "Session A", topicId: "known", sessionPath: "/a/a.jsonl" }, |
| 15 | { key: "known-b", kind: "session", label: "Session B", topicId: "known", sessionPath: "/a/b.jsonl" }, |
| 16 | ], |
| 17 | } }, |
| 18 | { scope: "project", workspaceRoot: "/b", node: { |
| 19 | key: "new", kind: "topic", label: "New live", root: "/b", topicId: "new", running: true, status: "streaming", children: [], |
| 20 | } }, |
| 21 | ]); |
| 22 | const shape = (tree: ProjectNode[]) => tree.map((project) => project.children?.map((topic) => [ |
| 23 | topic.topicId, topic.running, topic.runtimeOnly, |
| 24 | ])); |
| 25 | assert.deepEqual(shape(overlaid), [[['known', true, undefined]], [['new', true, true]]]); |
| 26 | assert.equal(overlaid[0]?.children?.[0]?.children?.length, 2); |
| 27 | assert.deepEqual(shape(projectTreeApplyRuntimeTopics(overlaid, [])), [[['known', undefined, undefined]], []]); |
| 28 | assert.equal(projectTreeApplyRuntimeTopics(overlaid, [ |
| 29 | { scope: "project", workspaceRoot: "/a", node: { |
| 30 | key: "known", kind: "topic", label: "Known live", root: "/a", topicId: "known", running: true, children: [], |
| 31 | } }, |
| 32 | ], new Set(["known"]))[0]?.children?.length, 0); |
| 33 | const settled = projectTreeApplyRuntimeTopics(overlaid, []); |
| 34 | assert.equal(settled[0]?.children?.[0]?.children?.length, 2, |
| 35 | "clearing runtime state preserves the topic's durable session children"); |
| 36 | assert.equal(settled[0]?.children?.[0]?.children?.some((session) => session.open || session.running), false, |
| 37 | "clearing runtime state removes only transient session status"); |
| 38 | |
| 39 | const projects: ProjectNode[] = Array.from({ length: 100 }, (_, index) => ({ |
| 40 | key: `p-${index}`, kind: "project", label: `P ${index}`, root: `/p/${index}`, children: [], |
| 41 | })); |
| 42 | const topics = projects.map((project, index) => ({ |
| 43 | scope: "project", workspaceRoot: project.root, node: { |
| 44 | key: `t-${index}`, kind: "topic" as const, label: `T ${index}`, root: project.root, |
| 45 | topicId: `t-${index}`, running: true, status: "thinking" as const, children: [], |
| 46 | }, |
| 47 | })); |
| 48 | const hundred = projectTreeApplyRuntimeTopics(projects, topics); |
| 49 | assert.equal(hundred.reduce((count, project) => count + (project.children?.filter((topic) => topic.running).length ?? 0), 0), 100); |
| 50 | |
| 51 | const colliding = projectTreeApplyRuntimeTopics([ |
| 52 | { key: "collision-a", kind: "project", label: "A", root: "/collision/a", children: [] }, |
| 53 | { key: "collision-b", kind: "project", label: "B", root: "/collision/b", children: [] }, |
| 54 | ], [ |
| 55 | { scope: "project", workspaceRoot: "/collision/a", node: { |
| 56 | key: "shared-a", kind: "topic", label: "Shared A", root: "/collision/a", topicId: "shared", |
| 57 | sessionPath: "/collision/a/a.jsonl", running: true, children: [], |
| 58 | } }, |
| 59 | { scope: "project", workspaceRoot: "/collision/b", node: { |
| 60 | key: "shared-b", kind: "topic", label: "Shared B", root: "/collision/b", topicId: "shared", |
| 61 | sessionPath: "/collision/b/b.jsonl", open: true, children: [], |
| 62 | } }, |
| 63 | ]); |
| 64 | assert.deepEqual( |
| 65 | colliding.map((project) => project.children?.map((topic) => [topic.label, topic.sessionPath, topic.running, topic.open])), |
| 66 | [[['Shared A', '/collision/a/a.jsonl', true, undefined]], [['Shared B', '/collision/b/b.jsonl', undefined, true]]], |
| 67 | "runtime rows with the same topic id stay inside their project scope", |
| 68 | ); |
| 69 | |
| 70 | const stableProjection = createProjectTreeRuntimeProjection(); |
| 71 | const stableCatalog: ProjectNode[] = [{ |
| 72 | key: "stable-project", kind: "project", label: "Stable", root: "/stable", children: [ |
| 73 | { key: "topic-a", kind: "topic", label: "A", root: "/stable", topicId: "a", createdAt: 300, lastActivityAt: 600, children: [] }, |
| 74 | { key: "topic-b", kind: "topic", label: "B", root: "/stable", topicId: "b", createdAt: 200, lastActivityAt: 500, children: [] }, |
| 75 | { key: "topic-c", kind: "topic", label: "C", root: "/stable", topicId: "c", createdAt: 100, lastActivityAt: 400, children: [] }, |
| 76 | ], |
| 77 | }]; |
| 78 | const runtimeB = [{ |
| 79 | scope: "project", workspaceRoot: "/stable", node: { |
| 80 | key: "topic-b", kind: "topic" as const, label: "B", root: "/stable", topicId: "b", |
| 81 | open: true, running: false, children: [], |
| 82 | }, |
| 83 | }]; |
| 84 | const projected = stableProjection.apply(stableCatalog, runtimeB); |
| 85 | assert.strictEqual(projected[0]?.children?.[0], stableCatalog[0]?.children?.[0], "an unrelated row keeps its object identity"); |
| 86 | assert.strictEqual(projected[0]?.children?.[2], stableCatalog[0]?.children?.[2], "every unrelated row keeps its object identity"); |
| 87 | const repeated = stableProjection.apply(projected, structuredClone(runtimeB)); |
| 88 | assert.strictEqual(repeated, projected, "an equivalent runtime snapshot is a tree-level no-op"); |
| 89 | |
| 90 | const transientCatalog: ProjectNode[] = [{ |
| 91 | ...stableCatalog[0], |
| 92 | children: [stableCatalog[0]!.children![0]!, stableCatalog[0]!.children![2]!], |
| 93 | }]; |
| 94 | const transient = stableProjection.apply(transientCatalog, structuredClone(runtimeB)); |
| 95 | const restoredRuntimeB = transient[0]?.children?.find((topic) => topic.topicId === "b"); |
| 96 | assert.equal(restoredRuntimeB?.runtimeOnly, true, "a catalog-lag row remains marked as runtime-only"); |
| 97 | assert.equal(restoredRuntimeB?.createdAt, 200, "a catalog-lag row retains its resident creation time"); |
| 98 | assert.equal(restoredRuntimeB?.lastActivityAt, 500, "a catalog-lag row retains its resident activity time"); |
| 99 | assert.deepEqual( |
| 100 | [...(transient[0]?.children ?? [])] |
| 101 | .sort((a, b) => (b.lastActivityAt || b.createdAt || 0) - (a.lastActivityAt || a.createdAt || 0)) |
| 102 | .map((topic) => topic.topicId), |
| 103 | ["a", "b", "c"], |
| 104 | "catalog lag cannot move the active row to a zero-time sort position", |
| 105 | ); |
| 106 | |
| 107 | const pruningProjection = createProjectTreeRuntimeProjection(); |
| 108 | const pruningCatalog: ProjectNode[] = [{ |
| 109 | key: "pruning-project", kind: "project", label: "Pruning", root: "/pruning", children: [ |
| 110 | { key: "stale", kind: "topic", label: "Stale metadata", root: "/pruning", topicId: "stale", createdAt: 10, children: [] }, |
| 111 | ], |
| 112 | }]; |
| 113 | pruningProjection.apply(pruningCatalog, []); |
| 114 | const emptyPruningCatalog: ProjectNode[] = [{ ...pruningCatalog[0]!, children: [] }]; |
| 115 | pruningProjection.apply(emptyPruningCatalog, []); |
| 116 | const afterPrune = pruningProjection.apply(emptyPruningCatalog, [{ |
| 117 | scope: "project", workspaceRoot: "/pruning", node: { |
| 118 | key: "fresh", kind: "topic", label: "Fresh runtime", root: "/pruning", topicId: "stale", running: true, children: [], |
| 119 | }, |
| 120 | }]); |
| 121 | assert.equal(afterPrune[0]?.children?.[0]?.label, "Fresh runtime", "deleted resident metadata is not resurrected"); |
| 122 | assert.equal(afterPrune[0]?.children?.[0]?.createdAt, undefined, "deleted resident timestamps are released"); |
| 123 | console.log(" PASS project tree runtime projection"); |
| 124 |