|
| 1 | +import { describe, expect, it } from "vitest"; |
| 2 | +import { |
| 3 | + buildSubagentRunReadIndexFromRuns, |
| 4 | + countActiveDescendantRunsFromRuns, |
| 5 | + getSubagentRunByChildSessionKeyFromRuns, |
| 6 | + listRunsForControllerFromRuns, |
| 7 | + type SubagentRunReadIndex, |
| 8 | +} from "./subagent-registry-queries.js"; |
| 9 | +import type { SubagentRunRecord } from "./subagent-registry.types.js"; |
| 10 | + |
| 11 | +function makeRun(overrides: Partial<SubagentRunRecord>): SubagentRunRecord { |
| 12 | + const runId = overrides.runId ?? "run-default"; |
| 13 | + const childSessionKey = overrides.childSessionKey ?? `agent:main:subagent:${runId}`; |
| 14 | + const requesterSessionKey = overrides.requesterSessionKey ?? "agent:main:main"; |
| 15 | + return { |
| 16 | + runId, |
| 17 | + childSessionKey, |
| 18 | + controllerSessionKey: overrides.controllerSessionKey, |
| 19 | + requesterSessionKey, |
| 20 | + requesterDisplayKey: requesterSessionKey, |
| 21 | + task: "test task", |
| 22 | + cleanup: "keep", |
| 23 | + createdAt: overrides.createdAt ?? Date.now(), |
| 24 | + ...overrides, |
| 25 | + }; |
| 26 | +} |
| 27 | + |
| 28 | +function toRunMap(runs: SubagentRunRecord[]): Map<string, SubagentRunRecord> { |
| 29 | + return new Map(runs.map((run) => [run.runId, run])); |
| 30 | +} |
| 31 | + |
| 32 | +function listRunsForController( |
| 33 | + index: SubagentRunReadIndex, |
| 34 | + controllerSessionKey: string, |
| 35 | +): readonly SubagentRunRecord[] { |
| 36 | + return index.runsByControllerSessionKey.get(controllerSessionKey.trim()) ?? []; |
| 37 | +} |
| 38 | + |
| 39 | +describe("subagent registry read index", () => { |
| 40 | + it("matches existing query helpers while reusing one indexed snapshot", () => { |
| 41 | + const now = Date.now(); |
| 42 | + const root = "agent:main:main"; |
| 43 | + const parent = "agent:main:subagent:parent"; |
| 44 | + const liveChild = "agent:main:subagent:parent:subagent:live-child"; |
| 45 | + const movedChild = "agent:main:subagent:moved-child"; |
| 46 | + const runs = toRunMap([ |
| 47 | + makeRun({ |
| 48 | + runId: "run-parent", |
| 49 | + childSessionKey: parent, |
| 50 | + controllerSessionKey: root, |
| 51 | + requesterSessionKey: root, |
| 52 | + createdAt: now - 5_000, |
| 53 | + startedAt: now - 4_500, |
| 54 | + endedAt: now - 2_500, |
| 55 | + }), |
| 56 | + makeRun({ |
| 57 | + runId: "run-live-child", |
| 58 | + childSessionKey: liveChild, |
| 59 | + controllerSessionKey: parent, |
| 60 | + requesterSessionKey: parent, |
| 61 | + createdAt: now - 2_000, |
| 62 | + startedAt: now - 1_500, |
| 63 | + }), |
| 64 | + makeRun({ |
| 65 | + runId: "run-moved-old", |
| 66 | + childSessionKey: movedChild, |
| 67 | + controllerSessionKey: root, |
| 68 | + requesterSessionKey: root, |
| 69 | + createdAt: now - 4_000, |
| 70 | + startedAt: now - 3_500, |
| 71 | + }), |
| 72 | + makeRun({ |
| 73 | + runId: "run-moved-new", |
| 74 | + childSessionKey: movedChild, |
| 75 | + controllerSessionKey: "agent:main:other-controller", |
| 76 | + requesterSessionKey: "agent:main:other-controller", |
| 77 | + createdAt: now - 1_000, |
| 78 | + startedAt: now - 900, |
| 79 | + }), |
| 80 | + ]); |
| 81 | + |
| 82 | + const index = buildSubagentRunReadIndexFromRuns({ runs, now }); |
| 83 | + |
| 84 | + expect(listRunsForController(index, root)).toEqual(listRunsForControllerFromRuns(runs, root)); |
| 85 | + expect(index.getDisplaySubagentRun(parent)).toEqual( |
| 86 | + getSubagentRunByChildSessionKeyFromRuns(runs, parent), |
| 87 | + ); |
| 88 | + expect(index.countActiveDescendantRuns(root)).toBe( |
| 89 | + countActiveDescendantRunsFromRuns(runs, root), |
| 90 | + ); |
| 91 | + expect(index.countActiveDescendantRuns(root)).toBe(1); |
| 92 | + }); |
| 93 | + |
| 94 | + it("handles empty registry snapshots", () => { |
| 95 | + const runs = new Map<string, SubagentRunRecord>(); |
| 96 | + const index = buildSubagentRunReadIndexFromRuns({ runs }); |
| 97 | + |
| 98 | + expect(listRunsForController(index, "agent:main:main")).toEqual([]); |
| 99 | + expect(index.getDisplaySubagentRun("agent:main:subagent:missing")).toBeNull(); |
| 100 | + expect(index.countActiveDescendantRuns("agent:main:main")).toBe(0); |
| 101 | + }); |
| 102 | + |
| 103 | + it("uses requesterSessionKey when controllerSessionKey is missing", () => { |
| 104 | + const root = "agent:main:main"; |
| 105 | + const run = makeRun({ |
| 106 | + runId: "run-controller-fallback", |
| 107 | + childSessionKey: "agent:main:subagent:fallback-child", |
| 108 | + requesterSessionKey: root, |
| 109 | + controllerSessionKey: undefined, |
| 110 | + }); |
| 111 | + const runs = toRunMap([run]); |
| 112 | + const index = buildSubagentRunReadIndexFromRuns({ runs }); |
| 113 | + |
| 114 | + expect(listRunsForController(index, root)).toEqual(listRunsForControllerFromRuns(runs, root)); |
| 115 | + expect(listRunsForController(index, root)).toEqual([run]); |
| 116 | + }); |
| 117 | + |
| 118 | + it("keeps moved middle descendants under the latest requester", () => { |
| 119 | + const now = Date.now(); |
| 120 | + const root = "agent:main:root"; |
| 121 | + const otherRoot = "agent:main:other-root"; |
| 122 | + const middle = "agent:main:subagent:middle"; |
| 123 | + const grandchild = "agent:main:subagent:grandchild"; |
| 124 | + const runs = toRunMap([ |
| 125 | + makeRun({ |
| 126 | + runId: "run-middle-old", |
| 127 | + childSessionKey: middle, |
| 128 | + controllerSessionKey: root, |
| 129 | + requesterSessionKey: root, |
| 130 | + createdAt: now - 3_000, |
| 131 | + startedAt: now - 2_900, |
| 132 | + }), |
| 133 | + makeRun({ |
| 134 | + runId: "run-grandchild", |
| 135 | + childSessionKey: grandchild, |
| 136 | + controllerSessionKey: middle, |
| 137 | + requesterSessionKey: middle, |
| 138 | + createdAt: now - 2_000, |
| 139 | + startedAt: now - 1_900, |
| 140 | + }), |
| 141 | + makeRun({ |
| 142 | + runId: "run-middle-moved", |
| 143 | + childSessionKey: middle, |
| 144 | + controllerSessionKey: otherRoot, |
| 145 | + requesterSessionKey: otherRoot, |
| 146 | + createdAt: now - 1_000, |
| 147 | + startedAt: now - 900, |
| 148 | + }), |
| 149 | + ]); |
| 150 | + const index = buildSubagentRunReadIndexFromRuns({ runs, now }); |
| 151 | + |
| 152 | + expect(index.countActiveDescendantRuns(root)).toBe( |
| 153 | + countActiveDescendantRunsFromRuns(runs, root), |
| 154 | + ); |
| 155 | + expect(index.countActiveDescendantRuns(root)).toBe(0); |
| 156 | + expect(index.countActiveDescendantRuns(otherRoot)).toBe( |
| 157 | + countActiveDescendantRunsFromRuns(runs, otherRoot), |
| 158 | + ); |
| 159 | + expect(index.countActiveDescendantRuns(otherRoot)).toBe(2); |
| 160 | + }); |
| 161 | + |
| 162 | + it("keeps one snapshot stable for the lifetime of the context", () => { |
| 163 | + const root = "agent:main:main"; |
| 164 | + const runs = toRunMap([ |
| 165 | + makeRun({ |
| 166 | + runId: "run-original", |
| 167 | + childSessionKey: "agent:main:subagent:original", |
| 168 | + requesterSessionKey: root, |
| 169 | + controllerSessionKey: root, |
| 170 | + }), |
| 171 | + ]); |
| 172 | + const index = buildSubagentRunReadIndexFromRuns({ runs }); |
| 173 | + |
| 174 | + runs.set( |
| 175 | + "run-added-after-context", |
| 176 | + makeRun({ |
| 177 | + runId: "run-added-after-context", |
| 178 | + childSessionKey: "agent:main:subagent:added", |
| 179 | + requesterSessionKey: root, |
| 180 | + controllerSessionKey: root, |
| 181 | + }), |
| 182 | + ); |
| 183 | + |
| 184 | + expect(listRunsForController(index, root).map((run) => run.runId)).toEqual(["run-original"]); |
| 185 | + expect( |
| 186 | + listRunsForController(buildSubagentRunReadIndexFromRuns({ runs }), root).map( |
| 187 | + (run) => run.runId, |
| 188 | + ), |
| 189 | + ).toEqual(["run-original", "run-added-after-context"]); |
| 190 | + }); |
| 191 | + |
| 192 | + it("normalizes display lookup keys for whitespace-padded child session keys", () => { |
| 193 | + const normalizedChildSessionKey = "agent:main:subagent:whitespace-child"; |
| 194 | + const run = makeRun({ |
| 195 | + runId: "run-whitespace-child", |
| 196 | + childSessionKey: ` ${normalizedChildSessionKey} `, |
| 197 | + requesterSessionKey: "agent:main:main", |
| 198 | + }); |
| 199 | + const runs = toRunMap([run]); |
| 200 | + const index = buildSubagentRunReadIndexFromRuns({ runs }); |
| 201 | + |
| 202 | + expect(index.getDisplaySubagentRun(normalizedChildSessionKey)).toBe(run); |
| 203 | + }); |
| 204 | + |
| 205 | + it("keeps the display-row preference for in-memory records over persisted snapshots", () => { |
| 206 | + const childSessionKey = "agent:main:subagent:display-child"; |
| 207 | + const persistedRuns = toRunMap([ |
| 208 | + makeRun({ |
| 209 | + runId: "run-persisted-newer", |
| 210 | + childSessionKey, |
| 211 | + requesterSessionKey: "agent:main:main", |
| 212 | + createdAt: 200, |
| 213 | + startedAt: 200, |
| 214 | + }), |
| 215 | + ]); |
| 216 | + const inMemoryRuns = toRunMap([ |
| 217 | + makeRun({ |
| 218 | + runId: "run-memory-older-ended", |
| 219 | + childSessionKey, |
| 220 | + requesterSessionKey: "agent:main:main", |
| 221 | + createdAt: 100, |
| 222 | + startedAt: 100, |
| 223 | + endedAt: 150, |
| 224 | + }), |
| 225 | + ]); |
| 226 | + |
| 227 | + const index = buildSubagentRunReadIndexFromRuns({ |
| 228 | + runs: persistedRuns, |
| 229 | + inMemoryRuns: inMemoryRuns.values(), |
| 230 | + }); |
| 231 | + |
| 232 | + expect(index.getDisplaySubagentRun(childSessionKey)?.runId).toBe("run-memory-older-ended"); |
| 233 | + }); |
| 234 | +}); |
0 commit comments