- Profile management: discoverProfiles, getEffectiveProfile precedence, setUIActiveProfile, resolveWebChatDir, workspace registry (32 tests) - Workspace init API: creation, bootstrap seeding, custom paths, validation, idempotency (13 tests) - Profile switch API: GET/POST profiles, validation, default reset (10 tests) - Chat isolation: profile-scoped chat dirs, session isolation (7 tests) - LLM context awareness: bootstrap loading, subagent filtering, resolveBootstrapContextForRun content isolation (15 unit + 5 live) - Subagent streaming: registerSubagent, event replay, persistence, ensureRegisteredFromDisk, fan-out (24 unit + 5 live) - deploy.sh: add --skip-tests flag, pnpm test + web:build pre-flight, auto git commit/push of version bump after publish - package.json: add test:workspace and test:workspace:live scripts Co-authored-by: Cursor <cursoragent@cursor.com>
192 lines
5.8 KiB
TypeScript
192 lines
5.8 KiB
TypeScript
/**
|
|
* Live E2E tests for subagent streaming.
|
|
*
|
|
* These tests verify that:
|
|
* - Subagent registration works with real disk persistence
|
|
* - Events can be persisted and reloaded from disk
|
|
* - The profile-scoped subagent index works end-to-end
|
|
*
|
|
* Requires: LIVE=1 or OPENCLAW_LIVE_TEST=1
|
|
* Does NOT require a running gateway — tests the subagent run manager directly.
|
|
*/
|
|
import fs from "node:fs/promises";
|
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { describe, expect, it, beforeEach, afterEach } from "vitest";
|
|
|
|
const LIVE =
|
|
process.env.LIVE === "1" ||
|
|
process.env.OPENCLAW_LIVE_TEST === "1" ||
|
|
process.env.CLAWDBOT_LIVE_TEST === "1";
|
|
|
|
const describeLive = LIVE ? describe : describe.skip;
|
|
|
|
describeLive("subagent streaming (live)", () => {
|
|
let tempDir: string;
|
|
const originalEnv = { ...process.env };
|
|
|
|
beforeEach(async () => {
|
|
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "subagent-live-"));
|
|
process.env.OPENCLAW_HOME = tempDir;
|
|
process.env.OPENCLAW_STATE_DIR = path.join(tempDir, ".openclaw");
|
|
mkdirSync(path.join(tempDir, ".openclaw"), { recursive: true });
|
|
// Reset subagent singleton
|
|
delete (globalThis as Record<string, unknown>)[
|
|
"__openclaw_subagentRuns"
|
|
];
|
|
});
|
|
|
|
afterEach(async () => {
|
|
process.env = { ...originalEnv };
|
|
delete (globalThis as Record<string, unknown>)[
|
|
"__openclaw_subagentRuns"
|
|
];
|
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
});
|
|
|
|
it("persists subagent index to disk on registration", async () => {
|
|
const webChatDir = path.join(tempDir, ".openclaw", "web-chat");
|
|
mkdirSync(webChatDir, { recursive: true });
|
|
|
|
const {
|
|
registerSubagent,
|
|
} = await import("./subagent-runs.js");
|
|
|
|
registerSubagent("parent-session", {
|
|
sessionKey: "sub:p:live1",
|
|
runId: "run-live-1",
|
|
task: "live test task",
|
|
label: "live label",
|
|
});
|
|
|
|
const indexPath = path.join(webChatDir, "subagent-index.json");
|
|
expect(existsSync(indexPath)).toBe(true);
|
|
|
|
const index = JSON.parse(readFileSync(indexPath, "utf-8"));
|
|
expect(index["sub:p:live1"]).toBeDefined();
|
|
expect(index["sub:p:live1"].task).toBe("live test task");
|
|
expect(index["sub:p:live1"].status).toBe("running");
|
|
}, 10_000);
|
|
|
|
it("persists user messages to event JSONL file", async () => {
|
|
const webChatDir = path.join(tempDir, ".openclaw", "web-chat");
|
|
mkdirSync(webChatDir, { recursive: true });
|
|
|
|
const {
|
|
registerSubagent,
|
|
persistUserMessage,
|
|
} = await import("./subagent-runs.js");
|
|
|
|
registerSubagent("parent-session", {
|
|
sessionKey: "sub:p:live2",
|
|
runId: "run-live-2",
|
|
task: "msg persistence test",
|
|
});
|
|
|
|
persistUserMessage("sub:p:live2", { text: "hello from live test" });
|
|
|
|
const eventsDir = path.join(webChatDir, "subagent-events");
|
|
expect(existsSync(eventsDir)).toBe(true);
|
|
|
|
const eventFile = path.join(eventsDir, "sub_p_live2.jsonl");
|
|
expect(existsSync(eventFile)).toBe(true);
|
|
|
|
const lines = readFileSync(eventFile, "utf-8")
|
|
.split("\n")
|
|
.filter(Boolean);
|
|
expect(lines.length).toBeGreaterThan(0);
|
|
|
|
const event = JSON.parse(lines[0]);
|
|
expect(event.type).toBe("user-message");
|
|
expect(event.text).toBe("hello from live test");
|
|
}, 10_000);
|
|
|
|
it("multiple subagents for same parent are tracked independently", async () => {
|
|
const webChatDir = path.join(tempDir, ".openclaw", "web-chat");
|
|
mkdirSync(webChatDir, { recursive: true });
|
|
|
|
const {
|
|
registerSubagent,
|
|
getSubagentsForSession,
|
|
} = await import("./subagent-runs.js");
|
|
|
|
registerSubagent("parent-multi", {
|
|
sessionKey: "sub:p:multi1",
|
|
runId: "r-m1",
|
|
task: "task 1",
|
|
});
|
|
registerSubagent("parent-multi", {
|
|
sessionKey: "sub:p:multi2",
|
|
runId: "r-m2",
|
|
task: "task 2",
|
|
});
|
|
|
|
const subs = getSubagentsForSession("parent-multi");
|
|
expect(subs).toHaveLength(2);
|
|
const keys = subs.map((s) => s.sessionKey);
|
|
expect(keys).toContain("sub:p:multi1");
|
|
expect(keys).toContain("sub:p:multi2");
|
|
}, 10_000);
|
|
|
|
it("subscriber receives events in real-time", async () => {
|
|
const webChatDir = path.join(tempDir, ".openclaw", "web-chat");
|
|
mkdirSync(webChatDir, { recursive: true });
|
|
|
|
const {
|
|
registerSubagent,
|
|
subscribeToSubagent,
|
|
persistUserMessage,
|
|
} = await import("./subagent-runs.js");
|
|
|
|
registerSubagent("parent-sub", {
|
|
sessionKey: "sub:p:realtime",
|
|
runId: "r-rt",
|
|
task: "realtime test",
|
|
});
|
|
|
|
const received: Array<Record<string, unknown>> = [];
|
|
subscribeToSubagent(
|
|
"sub:p:realtime",
|
|
(event) => {
|
|
if (event) {received.push(event as Record<string, unknown>);}
|
|
},
|
|
{ replay: false },
|
|
);
|
|
|
|
persistUserMessage("sub:p:realtime", { text: "msg 1" });
|
|
persistUserMessage("sub:p:realtime", { text: "msg 2" });
|
|
|
|
expect(received).toHaveLength(2);
|
|
expect(received[0].text).toBe("msg 1");
|
|
expect(received[1].text).toBe("msg 2");
|
|
}, 10_000);
|
|
|
|
it("replay delivers buffered events on subscribe", async () => {
|
|
const webChatDir = path.join(tempDir, ".openclaw", "web-chat");
|
|
mkdirSync(webChatDir, { recursive: true });
|
|
|
|
const {
|
|
registerSubagent,
|
|
persistUserMessage,
|
|
subscribeToSubagent,
|
|
} = await import("./subagent-runs.js");
|
|
|
|
registerSubagent("parent-replay", {
|
|
sessionKey: "sub:p:replay",
|
|
runId: "r-rp",
|
|
task: "replay test",
|
|
});
|
|
|
|
persistUserMessage("sub:p:replay", { text: "buffered 1" });
|
|
persistUserMessage("sub:p:replay", { text: "buffered 2" });
|
|
|
|
const received: Array<Record<string, unknown>> = [];
|
|
subscribeToSubagent("sub:p:replay", (event) => {
|
|
if (event) {received.push(event as Record<string, unknown>);}
|
|
});
|
|
|
|
expect(received.length).toBeGreaterThanOrEqual(2);
|
|
}, 10_000);
|
|
}, 60_000);
|