























1+import { afterEach, describe, expect, test, vi } from "vitest";
2+import { resetConfigRuntimeState, setRuntimeConfigSnapshot } from "../config/config.js";
3+import type { OpenClawConfig } from "../config/config.js";
4+import {
5+resolveStorePath,
6+saveSessionStore,
7+updateSessionStore,
8+type SessionEntry,
9+} from "../config/sessions.js";
10+import { resetPluginRuntimeStateForTest } from "../plugins/runtime.js";
11+import { withStateDirEnv } from "../test-helpers/state-dir-env.js";
12+13+const subagentRegistryReadMock = vi.hoisted(() => {
14+let runsByChildSessionKey = new Map<string, Record<string, unknown>>();
15+const buildSubagentRunReadIndex = vi.fn(() => {
16+const runsByControllerSessionKey = new Map<string, Record<string, unknown>[]>();
17+for (const entry of runsByChildSessionKey.values()) {
18+const controllerSessionKey =
19+typeof entry.controllerSessionKey === "string"
20+ ? entry.controllerSessionKey
21+ : typeof entry.requesterSessionKey === "string"
22+ ? entry.requesterSessionKey
23+ : undefined;
24+if (!controllerSessionKey) {
25+continue;
26+}
27+const runs = runsByControllerSessionKey.get(controllerSessionKey) ?? [];
28+runs.push(entry);
29+runsByControllerSessionKey.set(controllerSessionKey, runs);
30+}
31+return {
32+ runsByControllerSessionKey,
33+getDisplaySubagentRun: vi.fn(
34+(childSessionKey: string) => runsByChildSessionKey.get(childSessionKey) ?? null,
35+),
36+countActiveDescendantRuns: vi.fn(() => 0),
37+};
38+});
39+return {
40+ buildSubagentRunReadIndex,
41+countActiveDescendantRuns: vi.fn(() => 0),
42+getSessionDisplaySubagentRunByChildSessionKey: vi.fn(
43+(childSessionKey: string) => runsByChildSessionKey.get(childSessionKey) ?? null,
44+),
45+getSubagentSessionRuntimeMs: vi.fn(() => undefined),
46+getSubagentSessionStartedAt: vi.fn(() => undefined),
47+isSubagentRunLive: vi.fn(() => false),
48+listSubagentRunsForController: vi.fn((controllerSessionKey: string) =>
49+[...runsByChildSessionKey.values()].filter((entry) => {
50+const controller =
51+typeof entry.controllerSessionKey === "string"
52+ ? entry.controllerSessionKey
53+ : typeof entry.requesterSessionKey === "string"
54+ ? entry.requesterSessionKey
55+ : undefined;
56+return controller === controllerSessionKey;
57+}),
58+),
59+resolveSubagentSessionStatus: vi.fn(() => undefined),
60+setSubagentRunsForTest: (runs: Record<string, unknown>[]) => {
61+runsByChildSessionKey = new Map(
62+runs
63+.filter((entry) => typeof entry.childSessionKey === "string")
64+.map((entry) => [entry.childSessionKey as string, entry]),
65+);
66+},
67+};
68+});
69+70+vi.mock("../agents/subagent-registry-read.js", () => subagentRegistryReadMock);
71+72+import { loadGatewaySessionRow } from "./session-utils.js";
73+74+describe("single gateway session row child-session cache", () => {
75+afterEach(() => {
76+resetConfigRuntimeState();
77+resetPluginRuntimeStateForTest();
78+subagentRegistryReadMock.setSubagentRunsForTest([]);
79+vi.clearAllMocks();
80+});
81+82+test("shares the child-session index across repeated single-row loads for the same store", async () => {
83+await withStateDirEnv("openclaw-single-row-cache-", async () => {
84+const cfg: OpenClawConfig = {
85+agents: {
86+list: [
87+{
88+id: "main",
89+default: true,
90+workspace: "/tmp/openclaw-single-row-cache",
91+},
92+],
93+defaults: { model: { primary: "openai/gpt-5.4" } },
94+},
95+} as OpenClawConfig;
96+setRuntimeConfigSnapshot(cfg, cfg);
97+const now = Math.floor(Date.now() / 1_000) * 1_000 + 100;
98+const store: Record<string, SessionEntry> = {
99+"agent:main:subagent:parent-a": {
100+sessionId: "parent-a",
101+updatedAt: now,
102+},
103+"agent:main:subagent:child-a": {
104+sessionId: "child-a",
105+parentSessionKey: "agent:main:subagent:parent-a",
106+updatedAt: now,
107+status: "running",
108+},
109+"agent:main:subagent:parent-b": {
110+sessionId: "parent-b",
111+updatedAt: now,
112+},
113+"agent:main:subagent:child-b": {
114+sessionId: "child-b",
115+parentSessionKey: "agent:main:subagent:parent-b",
116+updatedAt: now,
117+status: "running",
118+},
119+};
120+await saveSessionStore(resolveStorePath(cfg.session?.store, { agentId: "main" }), store);
121+122+const rowA = loadGatewaySessionRow("agent:main:subagent:parent-a", { now });
123+const rowB = loadGatewaySessionRow("agent:main:subagent:parent-b", { now: now + 50 });
124+const rowAAfterWindow = loadGatewaySessionRow("agent:main:subagent:parent-a", {
125+now: now + 1_500,
126+});
127+128+expect(rowA?.childSessions).toEqual(["agent:main:subagent:child-a"]);
129+expect(rowB?.childSessions).toEqual(["agent:main:subagent:child-b"]);
130+expect(rowAAfterWindow?.childSessions).toEqual(["agent:main:subagent:child-a"]);
131+expect(subagentRegistryReadMock.buildSubagentRunReadIndex).not.toHaveBeenCalled();
132+});
133+});
134+135+test("refreshes subagent registry state while reusing store child candidates", async () => {
136+await withStateDirEnv("openclaw-single-row-cache-fresh-registry-", async () => {
137+const cfg: OpenClawConfig = {
138+agents: {
139+list: [
140+{
141+id: "main",
142+default: true,
143+workspace: "/tmp/openclaw-single-row-cache-fresh-registry",
144+},
145+],
146+defaults: { model: { primary: "openai/gpt-5.4" } },
147+},
148+} as OpenClawConfig;
149+setRuntimeConfigSnapshot(cfg, cfg);
150+const now = Math.floor(Date.now() / 1_000) * 1_000 + 100;
151+const oldParent = "agent:main:subagent:parent-old";
152+const newParent = "agent:main:subagent:parent-new";
153+const child = "agent:main:subagent:child";
154+const store: Record<string, SessionEntry> = {
155+[oldParent]: {
156+sessionId: "parent-old",
157+updatedAt: now,
158+},
159+[newParent]: {
160+sessionId: "parent-new",
161+updatedAt: now,
162+},
163+[child]: {
164+sessionId: "child",
165+parentSessionKey: oldParent,
166+updatedAt: now,
167+status: "running",
168+},
169+};
170+await saveSessionStore(resolveStorePath(cfg.session?.store, { agentId: "main" }), store);
171+172+subagentRegistryReadMock.setSubagentRunsForTest([
173+{
174+childSessionKey: child,
175+controllerSessionKey: oldParent,
176+requesterSessionKey: oldParent,
177+createdAt: now,
178+},
179+]);
180+expect(loadGatewaySessionRow(oldParent, { now })?.childSessions).toEqual([child]);
181+182+subagentRegistryReadMock.setSubagentRunsForTest([
183+{
184+childSessionKey: child,
185+controllerSessionKey: newParent,
186+requesterSessionKey: newParent,
187+createdAt: now + 25,
188+},
189+]);
190+expect(loadGatewaySessionRow(oldParent, { now: now + 50 })?.childSessions).toBeUndefined();
191+expect(loadGatewaySessionRow(newParent, { now: now + 50 })?.childSessions).toEqual([child]);
192+expect(subagentRegistryReadMock.buildSubagentRunReadIndex).not.toHaveBeenCalled();
193+});
194+});
195+196+test("rebuilds store child candidates after same-object session store writes", async () => {
197+await withStateDirEnv("openclaw-single-row-cache-write-version-", async () => {
198+const cfg: OpenClawConfig = {
199+agents: {
200+list: [
201+{
202+id: "main",
203+default: true,
204+workspace: "/tmp/openclaw-single-row-cache-write-version",
205+},
206+],
207+defaults: { model: { primary: "openai/gpt-5.4" } },
208+},
209+} as OpenClawConfig;
210+setRuntimeConfigSnapshot(cfg, cfg);
211+const now = Math.floor(Date.now() / 1_000) * 1_000 + 100;
212+const oldParent = "agent:main:subagent:parent-old";
213+const newParent = "agent:main:subagent:parent-new";
214+const child = "agent:main:subagent:child";
215+const storePath = resolveStorePath(cfg.session?.store, { agentId: "main" });
216+const store: Record<string, SessionEntry> = {
217+[oldParent]: {
218+sessionId: "parent-old",
219+updatedAt: now,
220+},
221+[newParent]: {
222+sessionId: "parent-new",
223+updatedAt: now,
224+},
225+[child]: {
226+sessionId: "child",
227+parentSessionKey: oldParent,
228+updatedAt: now,
229+status: "running",
230+},
231+};
232+await saveSessionStore(storePath, store);
233+234+expect(loadGatewaySessionRow(oldParent, { now })?.childSessions).toEqual([child]);
235+await updateSessionStore(
236+storePath,
237+(cachedStore) => {
238+const childEntry = cachedStore[child];
239+if (childEntry) {
240+childEntry.parentSessionKey = newParent;
241+childEntry.updatedAt = now + 25;
242+}
243+},
244+{ skipMaintenance: true, takeCacheOwnership: true },
245+);
246+247+expect(loadGatewaySessionRow(oldParent, { now: now + 50 })?.childSessions).toBeUndefined();
248+expect(loadGatewaySessionRow(newParent, { now: now + 50 })?.childSessions).toEqual([child]);
249+expect(subagentRegistryReadMock.buildSubagentRunReadIndex).not.toHaveBeenCalled();
250+});
251+});
252+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。