diff --git a/src/gateway/server-methods/sessions.dispatch.demand.test.ts b/src/gateway/server-methods/sessions.dispatch.demand.test.ts index 2644a143de84..1e6833fc0334 100644 --- a/src/gateway/server-methods/sessions.dispatch.demand.test.ts +++ b/src/gateway/server-methods/sessions.dispatch.demand.test.ts @@ -7,10 +7,7 @@ import { import type { EnvironmentSummary } from "../../../packages/gateway-protocol/src/index.js"; import { getRuntimeConfig } from "../../config/config.js"; import { upsertSessionEntryCore } from "../../config/sessions/session-accessor.js"; -import { - clearAgentRunContext, - rotateAgentRunRegistryLifecycleGeneration, -} from "../../infra/agent-run-registry.js"; +import { clearAgentRunContext } from "../../infra/agent-run-registry.js"; import { NODE_WORKER_SUPERVISOR_PROTOCOL_FEATURE } from "../../infra/node-runner-inventory.js"; import { withPluginRuntimeGatewayContextResolver } from "../../plugins/runtime/gateway-request-scope.js"; import { withOpenClawTestState } from "../../test-utils/openclaw-test-state.js"; @@ -58,7 +55,6 @@ async function withDemandFixture( run: (fixture: { dispatch: (sessionId: string, deviceId?: string) => Promise; send: (sessionId: string) => Promise; - release: (sessionId: string) => void; nodes: NodeWorkerSupervisorNodeProof[]; placements: Map; service: NonNullable; @@ -248,11 +244,6 @@ async function withDemandFixture( await run({ dispatch, send, - release: (sessionId) => - expectDefined( - heldTurns.get(sessionId), - "admitted session turn", - ).admission.cleanupAdmittedRun(), nodes, placements, service, @@ -286,33 +277,13 @@ describe("sessions.dispatch after admitted sessions.send", () => { }); }); - it.each(["idle", "released", "stale-lifecycle"] as const)( - "does not reserve capacity for a retained placement with %s admission", - async (state) => { - await withDemandFixture(async ({ dispatch, send, placements }) => { - expect(await dispatch("auto-04")).toBe("node-3"); - if (state !== "idle") { - const turn = await send("auto-04"); - if (state === "released") { - turn.admission.cleanupAdmittedRun(); - } else { - rotateAgentRunRegistryLifecycleGeneration(); - } - } - expect(placements.get("auto-04")?.state).toBe("active"); - expect(await dispatch("auto-05")).toBe("node-3"); - }); - }, - ); - - it("excludes a physically full node even when it has the least admitted demand", async () => { - await withDemandFixture(async ({ dispatch, release, nodes }) => { - // Release the first real send without changing its retained placement. - release("explicit-1"); - nodes[0]!.workerHost.capacity = { total: 2, available: 0 }; - nodes[2]!.workerHost.capacity = { total: 2, available: 1 }; - - expect(await dispatch("auto-04")).toBe("node-2"); + it("does not reserve capacity for a retained placement after admission is released", async () => { + await withDemandFixture(async ({ dispatch, send, placements }) => { + expect(await dispatch("auto-04")).toBe("node-3"); + const turn = await send("auto-04"); + turn.admission.cleanupAdmittedRun(); + expect(placements.get("auto-04")?.state).toBe("active"); + expect(await dispatch("auto-05")).toBe("node-3"); }); }); }); diff --git a/src/gateway/server-methods/sessions.messages-subscribe-perf.test.ts b/src/gateway/server-methods/sessions.messages-subscribe-perf.test.ts index f6b7db51c885..90b6d9439526 100644 --- a/src/gateway/server-methods/sessions.messages-subscribe-perf.test.ts +++ b/src/gateway/server-methods/sessions.messages-subscribe-perf.test.ts @@ -1,4 +1,3 @@ -import { performance } from "node:perf_hooks"; import { afterEach, expect, it, vi } from "vitest"; import type { PendingApprovalSnapshot } from "../../../packages/gateway-protocol/src/schema/approvals.js"; import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js"; @@ -84,27 +83,16 @@ it("shares approval replay across 64 subscribers during unrelated approval activ databaseOptions, now: () => 5000, }); - const phaseMs = { expiry: 0, pending: 0 }; - const expire = store.expireDueOperatorApprovals; const list = store.listPendingOperatorApprovals; - const expiryReads = vi - .spyOn(store, "expireDueOperatorApprovals") - .mockImplementation(async (params) => { - const start = performance.now(); - const result = await expire(params); - phaseMs.expiry += performance.now() - start; - return result; - }); + const expiryReads = vi.spyOn(store, "expireDueOperatorApprovals"); const pendingReads = vi .spyOn(store, "listPendingOperatorApprovals") .mockImplementation(async (params) => { - const start = performance.now(); const result = await list(params); if (!unrelatedApproval) { throw new Error("Expected the unrelated synthetic approval"); } runtime.publish({ phase: "pending", record: unrelatedApproval }); - phaseMs.pending += performance.now() - start; return result; }); const context = { @@ -113,15 +101,10 @@ it("shares approval replay across 64 subscribers during unrelated approval activ listSessionPendingApprovals: runtime.replay, logGateway: { error: vi.fn() }, } as unknown as GatewayRequestContext; - const samples: number[] = []; - for (let round = 0; round < 31; round += 1) { + for (let round = 0; round < 2; round += 1) { const responses = await Promise.all( clients.map(async (client) => { - const start = performance.now(); - let responseMs = 0; - const respond = vi.fn(() => { - responseMs = performance.now() - start; - }); + const respond = vi.fn(); await sessionSubscriptionHandlers["sessions.messages.subscribe"]!({ req: { type: "req", id: "perf", method: "sessions.messages.subscribe" }, params: { key: sessionKey, includeApprovals: true }, @@ -130,9 +113,6 @@ it("shares approval replay across 64 subscribers during unrelated approval activ respond, isWebchatConnect: () => false, } satisfies GatewayRequestHandlerOptions); - if (round > 0) { - samples.push(responseMs); - } return respond; }), ); @@ -153,29 +133,7 @@ it("shares approval replay across 64 subscribers during unrelated approval activ undefined, ); } - if (round === 0) { - expiryReads.mockClear(); - pendingReads.mockClear(); - phaseMs.expiry = 0; - phaseMs.pending = 0; - } + expect(pendingReads).toHaveBeenCalledTimes(round + 1); + expect(expiryReads).toHaveBeenCalledTimes(round + 1); } - samples.sort((a, b) => a - b); - console.log( - JSON.stringify({ - pendingApprovals: 200, - concurrency: clients.length, - samples: samples.length, - p50Ms: samples[Math.floor(samples.length * 0.5)], - p90Ms: samples[Math.floor(samples.length * 0.9)], - pendingReadsPerSubscribe: pendingReads.mock.calls.length / samples.length, - expiryCallsPerSubscribe: expiryReads.mock.calls.length / samples.length, - phaseMsPerSubscribe: { - expiry: phaseMs.expiry / samples.length, - pending: phaseMs.pending / samples.length, - }, - }), - ); - expect(pendingReads.mock.calls.length).toBe(samples.length / clients.length); - expect(expiryReads.mock.calls.length).toBe(samples.length / clients.length); }); diff --git a/src/gateway/server-methods/sessions.send-yield-resume.test.ts b/src/gateway/server-methods/sessions.send-yield-resume.test.ts index 22d8bffb6ae9..e3a11891322c 100644 --- a/src/gateway/server-methods/sessions.send-yield-resume.test.ts +++ b/src/gateway/server-methods/sessions.send-yield-resume.test.ts @@ -48,6 +48,43 @@ afterEach(() => { vi.useRealTimers(); }); +async function createInitialYieldFixture(label: string) { + const requesterSessionKey = "agent:main:main"; + const requesterTurnRunId = `${label}-parent`; + const runId = `${label}-child`; + const childSessionKey = `agent:main:subagent:${runId}`; + await registerSubagentRun({ + runId, + childSessionKey, + requesterSessionKey, + requesterAgentId: "main", + requesterTurnRunId, + requesterDisplayKey: requesterSessionKey, + task: "Finish the initial requester handoff", + cleanup: "keep", + expectsCompletionMessage: true, + }); + const onYield = vi.fn(); + const tool = createSessionsYieldTool({ + sessionId: `${label}-session`, + claimYield: createRequesterYieldCallback({ + requesterSessionKey, + requesterAgentId: "main", + requesterTurnRunId, + }), + onYield, + }); + return { + requesterSessionKey, + requesterTurnRunId, + runId, + childSessionKey, + entry: expectDefined(subagentRuns.get(runId), "initial child"), + onYield, + tool, + }; +} + it("resumes a yielded child through sessions.send and wakes its original parent after the same batch settles", async () => { vi.useFakeTimers(); const { runSubagentAnnounceFlow } = await vi.importActual< @@ -63,16 +100,7 @@ it("resumes a yielded child through sessions.send and wakes its original parent const requesterTurnRunId = "parent-turn"; const expectCompletedRun = (runId: string, resultText: string) => { const entry = expectDefined(subagentRuns.get(runId), `completed run ${runId}`); - expect( - entry, - JSON.stringify({ - runId, - cleanupHandled: entry.cleanupHandled, - delivery: entry.delivery, - requesterSettleWake: entry.requesterSettleWake, - requesterTurnRunId: entry.requesterTurnRunId, - }), - ).toMatchObject({ + expect(entry).toMatchObject({ execution: { status: "terminal", outcome: { status: "ok" } }, completion: { resultText }, cleanupCompletedAt: expect.any(Number), @@ -336,28 +364,21 @@ it.each([ ] as const)( "retains the committed outcome after $retirement replacement for a repeated $repetition claim", async ({ retirement, repetition }) => { - const requesterSessionKey = "agent:main:main"; - const childSessionKey = "agent:main:subagent:initial-retirement"; - const runId = "initial-retirement-child"; - const requesterTurnRunId = "initial-retirement-parent"; await writeSubagentSessionEntry({ stateDir: fixture.stateDir, agentId: "main", - sessionKey: requesterSessionKey, - defaultSessionId: `${requesterSessionKey}-session`, + sessionKey: "agent:main:main", + defaultSessionId: "agent:main:main-session", }); - await registerSubagentRun({ + const { + requesterSessionKey, + requesterTurnRunId, runId, childSessionKey, - requesterSessionKey, - requesterAgentId: "main", - requesterTurnRunId, - requesterDisplayKey: requesterSessionKey, - task: "Keep the original requester owner", - cleanup: "keep", - expectsCompletionMessage: true, - }); - const entry = expectDefined(subagentRuns.get(runId), "original initial child"); + entry, + onYield, + tool, + } = await createInitialYieldFixture("initial-retirement"); let retireRequester = () => {}; const acknowledged = createDeferred(); const releaseAcknowledgement = createDeferred(); @@ -394,16 +415,6 @@ it.each([ options, ), ); - const onYield = vi.fn(); - const tool = createSessionsYieldTool({ - sessionId: "initial-retirement-session", - claimYield: createRequesterYieldCallback({ - requesterSessionKey, - requesterAgentId: "main", - requesterTurnRunId, - }), - onYield, - }); const first = withRequesterTestAuthority( requesterTurnRunId, requesterSessionKey, @@ -453,13 +464,11 @@ it.each([ (result) => ({ result }), (error: unknown) => ({ error }), ); - expect({ ...repeatedOutcome, writes, yields: onYield.mock.calls.length }).toMatchObject({ + expect(repeatedOutcome).toMatchObject({ error: retirement === "runtime owner" ? { outcome: "committed", publication: "published" } : { outcome: "committed", publication: "superseded" }, - writes: 1, - yields: 0, }); expect(writes).toBe(1); expect(onYield).not.toHaveBeenCalled(); @@ -481,21 +490,8 @@ it.each([ ); it("retains an unknown initial intent until canonical worker restore reconciles the row", async () => { - const requesterSessionKey = "agent:main:main"; - const requesterTurnRunId = "unknown-initial-parent"; - const runId = "unknown-initial-child"; - await registerSubagentRun({ - runId, - childSessionKey: "agent:main:subagent:unknown-initial-child", - requesterSessionKey, - requesterAgentId: "main", - requesterTurnRunId, - requesterDisplayKey: requesterSessionKey, - task: "Retain an unacknowledged initial intent", - cleanup: "keep", - expectsCompletionMessage: true, - }); - const entry = expectDefined(subagentRuns.get(runId), "original uncertain child"); + const { requesterTurnRunId, runId, entry, onYield, tool } = + await createInitialYieldFixture("unknown-initial"); const nativePersistence = await vi.importActual< typeof import("../../agents/subagents/registry/subagent-registry-persistence.js") >("../../agents/subagents/registry/subagent-registry-persistence.js"); @@ -506,16 +502,6 @@ it("retains an unknown initial intent until canonical worker restore reconciles writes += 1; throw new SqliteWorkerError("Initial write outcome unavailable", "outcome-unknown"); }); - const onYield = vi.fn(); - const tool = createSessionsYieldTool({ - sessionId: "unknown-initial-session", - claimYield: createRequesterYieldCallback({ - requesterSessionKey, - requesterAgentId: "main", - requesterTurnRunId, - }), - onYield, - }); try { await expect(tool.execute("yield-unknown", {})).rejects.toMatchObject({ outcome: "unknown" }); expect(onYield).not.toHaveBeenCalled(); @@ -545,20 +531,8 @@ it.each([false, true])( "joins a concurrent yield claim until host handoff (joining caller retired: %s)", async (retired) => { vi.useFakeTimers(); - const requesterSessionKey = "agent:main:main"; - const requesterTurnRunId = "joined-initial-parent"; - const runId = "joined-initial-child"; - await registerSubagentRun({ - runId, - childSessionKey: "agent:main:subagent:joined-initial-child", - requesterSessionKey, - requesterAgentId: "main", - requesterTurnRunId, - requesterDisplayKey: requesterSessionKey, - task: "Join the original initial handoff", - cleanup: "keep", - expectsCompletionMessage: true, - }); + const { requesterSessionKey, requesterTurnRunId, runId, onYield, tool } = + await createInitialYieldFixture("joined-initial"); const handoffFailed = createDeferred(); const commit = vi .fn() @@ -599,16 +573,6 @@ it.each([false, true])( options, ), ); - const onYield = vi.fn(); - const tool = createSessionsYieldTool({ - sessionId: "joined-initial-session", - claimYield: createRequesterYieldCallback({ - requesterSessionKey, - requesterAgentId: "main", - requesterTurnRunId, - }), - onYield, - }); const first = tool.execute("yield-first", {}); let second: ReturnType | undefined; try { diff --git a/test/cron-message-read.integration.test.ts b/test/cron-message-read.integration.test.ts index f989cee1fb58..4e72d37351c0 100644 --- a/test/cron-message-read.integration.test.ts +++ b/test/cron-message-read.integration.test.ts @@ -307,12 +307,10 @@ describe("scheduled message actions", () => { const providerErrors: string[] = []; const providerWork = new Set>(); let requesterPermissions = 16n; // Discord MANAGE_CHANNELS. - let metadataControl: "pending" | "passed" = "pending"; const diagnostics = (result: unknown) => redactToolPayloadText( JSON.stringify({ result, - metadataControl, requests, providerErrors, model: embeddedModel.observation, @@ -396,7 +394,6 @@ describe("scheduled message actions", () => { const url = new URL(req.url ?? "/", "http://fixture.invalid"); if (req.method === "POST" && url.pathname === "/scheduled-clock-gap") { expect(runtime).toBe("claude-cli"); - expect(metadataControl).toBe("passed"); const realNow = Date.now.bind(Date); // The real CLI has its grant; model a pause beyond its former timeout-plus-grace TTL. vi.spyOn(Date, "now").mockImplementation(() => realNow() + 120_000); @@ -682,28 +679,6 @@ describe("scheduled message actions", () => { catalogMode: "static", }); const runtimeConfig = getRuntimeConfig(); - const { fetchChannelInfoDiscord } = await import("../extensions/discord/runtime-api.js"); - const metadata = await fetchChannelInfoDiscord(channelId, { - cfg: runtimeConfig, - accountId: creatorAccountId, - }).catch((error: unknown) => { - throw new Error(diagnostics({ metadataError: describeFixtureError(error) })); - }); - expect(metadata, diagnostics(metadata)).toMatchObject({ - id: channelId, - type: 0, - guild_id: guildId, - }); - expect(requests, diagnostics(metadata)).toEqual([ - { - method: "GET", - path: `/api/v10/channels/${channelId}`, - authorizationMatches: true, - }, - ]); - metadataControl = "passed"; - // The direct transport control cannot satisfy the scheduled journey's evidence. - requests.length = 0; const params = { name: nativeCreator ? "Edit Discord for the recorded requester" diff --git a/test/discord-metadata-read-authority.integration.test.ts b/test/discord-metadata-read-authority.integration.test.ts index c298578673e1..9c3f47dc84f7 100644 --- a/test/discord-metadata-read-authority.integration.test.ts +++ b/test/discord-metadata-read-authority.integration.test.ts @@ -292,11 +292,9 @@ describe("registered Discord metadata reads", () => { it.each([ `channel:100000000000000008`, `user:100000000000000009`, - `channel:${current}`, - `channel:100000000000000012`, `channel:${userId}`, `user:${dmId}`, - ])("rejects another DM, guild, thread, user or namespace before I/O (%s)", async (target) => { + ])("rejects another channel, user or namespace before I/O (%s)", async (target) => { await expect( dispatchChannelMessageAction({ ...fixture.dmContext, @@ -395,26 +393,27 @@ describe("registered Discord metadata reads", () => { expect(fixture.requests).toEqual([]); }); - it.each(metadataReads.filter((read) => read.action !== "permissions"))( - "retains guild and wildcard channel restrictions for $action", - async (read) => { - const context = { ...fixture.context, action: read.action, params: read.params }; - fixture.discord.guilds = {}; - await expect(dispatchChannelMessageAction(context)).rejects.toThrow("not allowed"); - expect(fixture.requests).toEqual([{ method: "GET", path: `/guilds/${guildId}` }]); - fixture.discord.guilds = { [guildId]: { channels: { [current]: { enabled: true } } } }; - await expect(dispatchChannelMessageAction(context)).rejects.toThrow( - "wildcard channel allowlist", - ); - fixture.discord.guilds = { - [guildId]: { channels: { "*": { enabled: true }, [sibling]: { enabled: false } } }, - }; - await expect(dispatchChannelMessageAction(context)).rejects.toThrow( - "wildcard channel allowlist", - ); - expect(fixture.requests).toEqual([{ method: "GET", path: `/guilds/${guildId}` }]); - }, - ); + it.each( + metadataReads.filter(({ action }) => + ["role-info", "emoji-list", "channel-list"].includes(action), + ), + )("retains guild and wildcard channel restrictions for $action", async (read) => { + const context = { ...fixture.context, action: read.action, params: read.params }; + fixture.discord.guilds = {}; + await expect(dispatchChannelMessageAction(context)).rejects.toThrow("not allowed"); + expect(fixture.requests).toEqual([{ method: "GET", path: `/guilds/${guildId}` }]); + fixture.discord.guilds = { [guildId]: { channels: { [current]: { enabled: true } } } }; + await expect(dispatchChannelMessageAction(context)).rejects.toThrow( + "wildcard channel allowlist", + ); + fixture.discord.guilds = { + [guildId]: { channels: { "*": { enabled: true }, [sibling]: { enabled: false } } }, + }; + await expect(dispatchChannelMessageAction(context)).rejects.toThrow( + "wildcard channel allowlist", + ); + expect(fixture.requests).toEqual([{ method: "GET", path: `/guilds/${guildId}` }]); + }); it("checks the permissions destination before reading bot permissions", async () => { fixture.discord.guilds = { [guildId]: { channels: { [current]: { enabled: true } } } }; @@ -470,17 +469,11 @@ describe("registered Discord metadata reads", () => { ).toEqual({ ok: true, channels: [channels[0]] }); }); - it.each([ - { requesterAccountId: "other" }, - { requesterAccountId: undefined }, - { toolContext: undefined }, - { toolContext: { currentChannelProvider: "slack", currentChannelId: current } }, - { toolContext: { currentChannelProvider: "discord" } }, - ])("retains server-owned account and origin context (%j)", async (mismatch) => { + it("rejects a forged operator origin without a current conversation", async () => { await expect( dispatchChannelMessageAction({ ...fixture.context, - ...mismatch, + toolContext: { currentChannelProvider: "discord" }, params: { guildId, conversationReadOrigin: "direct-operator" }, }), ).rejects.toThrow("current provider and account context"); diff --git a/test/docker-entrypoint.test.ts b/test/docker-entrypoint.test.ts index 6816d71e8531..c5ba196cd12b 100644 --- a/test/docker-entrypoint.test.ts +++ b/test/docker-entrypoint.test.ts @@ -103,11 +103,9 @@ describe.skipIf(process.platform === "win32")("Docker image activation", () => { it.for( [ ["gateway", "--help"], - ["--help"], ["--version"], ["doctor", "--fix"], ["gateway", "status"], - ["config", "get", "gateway.mode"], ["gateway", "--dev", "--reset"], ["gateway", "--", "--profile", "literal"], ["gateway", "--unknown-option"], diff --git a/test/feishu-message-mutations.integration.test.ts b/test/feishu-message-mutations.integration.test.ts index bd5debc27009..4300350a7262 100644 --- a/test/feishu-message-mutations.integration.test.ts +++ b/test/feishu-message-mutations.integration.test.ts @@ -76,6 +76,10 @@ const routes = [ }, ]; +const mutationScopes = routes.filter(({ name }) => + ["text edit", "pin", "reaction addition"].includes(name), +); + async function withFeishuMutation( run: (fixture: { cfg: OpenClawConfig; @@ -267,7 +271,7 @@ describe("Feishu mutations through the message tool and Lark HTTP transport", () }); }); - it.each(routes)( + it.each(mutationScopes)( "stops $name after retirement during renewed token preparation", async (route) => { await withFeishuMutation(async ({ execute, requests, respond, retire, gate }) => { @@ -456,7 +460,7 @@ describe("Feishu mutations through the message tool and Lark HTTP transport", () }); }); - it.each(routes.filter(({ name }) => ["text edit", "pin", "reaction addition"].includes(name)))( + it.each(mutationScopes)( "keeps provider errors with incidental IDs as failures for $name", async (route) => { await withFeishuMutation(async ({ execute, respond }) => { diff --git a/test/feishu-message-read-authority.integration.test.ts b/test/feishu-message-read-authority.integration.test.ts index 46394ffde939..afa6b23619f6 100644 --- a/test/feishu-message-read-authority.integration.test.ts +++ b/test/feishu-message-read-authority.integration.test.ts @@ -316,9 +316,7 @@ afterAll(async () => { function createFixture( options: { bundled?: boolean; - trusted?: boolean; currentChat?: string; - actions?: readonly ChannelMessageActionName[]; } = {}, ) { vi.stubEnv("OPENCLAW_PROXY_ACTIVE", "0"); @@ -352,13 +350,12 @@ function createFixture( const record = createPluginRecord({ id: "feishu", origin: options.bundled ? "bundled" : "global", - trustedOfficialInstall: !options.bundled && options.trusted !== false, + trustedOfficialInstall: !options.bundled, }); const plugin: ChannelPlugin = { ...feishuPlugin, actions: { ...feishuPlugin.actions!, - readAuthorityActions: options.actions ?? feishuPlugin.actions?.readAuthorityActions, handleAction: async (ctx) => { const result = await feishuPlugin.actions!.handleAction!(ctx); // Revoke at the registered handler's return boundary after a real local consumer. @@ -445,8 +442,8 @@ const readCases: Array<{ }, ]; -describe.each([false, true])("Feishu provider read parity (bundled: %s)", (bundled) => { - it.each(readCases)( +describe.each([false, true])("Feishu provider reads (bundled: %s)", (bundled) => { + it.each(bundled ? readCases.filter(({ action }) => action === "read") : readCases)( "runs $action through its real consumer", async ({ action, params, details, paths }) => { const fixture = createFixture({ bundled }); @@ -488,30 +485,21 @@ it("finds a live peer on later pages through the registered channel-list action" expect(contentRequests()).toEqual([PEERS_PATH, PEERS_PATH]); }); -it.each(["plugin", "turn", "claim"] as const)( - "stops live peer pagination when the %s retires between pages", - async (owner) => { - const fixture = createFixture(); - fixture.settings.allowFrom = ["*"]; - beforeReply = (request) => { - if (request.path !== PEERS_PATH) { - return; - } - if (owner === "plugin") { - fixture.record.enabled = false; - } else if (owner === "turn") { - fixture.run.revokeTurn(); - } else { - fixture.run.releaseClaim(); - } - }; +it("stops live peer pagination when the plugin retires between pages", async () => { + const fixture = createFixture(); + fixture.settings.allowFrom = ["*"]; + beforeReply = (request) => { + if (request.path !== PEERS_PATH) { + return; + } + fixture.record.enabled = false; + }; - await expect( - fixture.dispatch("channel-list", { scope: "peers", query: "Other", limit: 1 }), - ).rejects.toThrow("no longer active"); - expect(contentRequests()).toEqual([PEERS_PATH]); - }, -); + await expect( + fixture.dispatch("channel-list", { scope: "peers", query: "Other", limit: 1 }), + ).rejects.toThrow("no longer active"); + expect(contentRequests()).toEqual([PEERS_PATH]); +}); it("cancels live peer pagination through the local message tool", async () => { const fixture = createFixture(); @@ -594,58 +582,31 @@ it.each(["account", "origin", "target", "reactions", "sticker", "catalog"] as co }, ); -it("does not widen an older read list when the host classifies member-info", async () => { - const fixture = createFixture({ actions: ["read"] }); - expectResult(await fixture.dispatch("read", { chatId: ALLOWED, messageId: MESSAGE }), { - message: { content: "allowed context" }, - }); - requests = []; - await expect( - fixture.dispatch("member-info", { chatId: ALLOWED, memberId: MEMBER }), - ).rejects.toThrow("exact current conversation"); - expect(requests).toEqual([]); -}); - -it("does not grant cross-context write authority through an overbroad read declaration", async () => { - const fixture = createFixture({ - actions: [...feishuPlugin.actions!.readAuthorityActions!, "pin"], - }); - await expect(fixture.dispatch("pin", { chatId: ALLOWED, messageId: MESSAGE })).rejects.toThrow( - "exact current conversation", +it.each([ + ["plugin", "metadata"], + ["plugin", "result"], + ["turn", "result"], + ["claim", "result"], +] as const)("rejects %s retirement at %s", async (owner, phase) => { + const fixture = createFixture(); + beforeReply = (request) => { + if (request.path !== (phase === "metadata" ? CHAT_PATH : PINS_PATH)) { + return; + } + if (owner === "plugin") { + fixture.record.enabled = false; + } + if (owner === "turn") { + fixture.run.revokeTurn(); + } + if (owner === "claim") { + fixture.run.releaseClaim(); + } + }; + await expect(fixture.dispatch("list-pins", { chatId: ALLOWED })).rejects.toThrow( + "no longer active", ); - expect(requests).toEqual([]); -}); - -it("retains the exact-current restriction for an unverified external installation", async () => { - const fixture = createFixture({ trusted: false }); - await expect(fixture.dispatch("read", { chatId: ALLOWED, messageId: MESSAGE })).rejects.toThrow( - "exact current conversation", - ); - expect(requests).toEqual([]); -}); - -describe.each(["plugin", "turn", "claim"] as const)("Feishu %s lifetime", (owner) => { - it.each(["metadata", "result"] as const)("rejects retirement at %s", async (phase) => { - const fixture = createFixture(); - beforeReply = (request) => { - if (request.path !== (phase === "metadata" ? CHAT_PATH : PINS_PATH)) { - return; - } - if (owner === "plugin") { - fixture.record.enabled = false; - } - if (owner === "turn") { - fixture.run.revokeTurn(); - } - if (owner === "claim") { - fixture.run.releaseClaim(); - } - }; - await expect(fixture.dispatch("list-pins", { chatId: ALLOWED })).rejects.toThrow( - "no longer active", - ); - expect(contentRequests()).toEqual(phase === "metadata" ? [CHAT_PATH] : [CHAT_PATH, PINS_PATH]); - }); + expect(contentRequests()).toEqual(phase === "metadata" ? [CHAT_PATH] : [CHAT_PATH, PINS_PATH]); }); it.each(["sticker-search", "channel-list", "member-info"] as const)( diff --git a/test/helpers/desktop-resize-real-fixture.test.ts b/test/helpers/desktop-resize-real-fixture.test.ts index 6f84cc4ebbc6..71cd68fc33fc 100644 --- a/test/helpers/desktop-resize-real-fixture.test.ts +++ b/test/helpers/desktop-resize-real-fixture.test.ts @@ -88,16 +88,12 @@ describe("desktop resize fixture provenance and carrier", () => { } }, ); - it.each(["ssh", "node"] as const)( - "retains explicit upstream provenance for %s", - async (carrier) => { - const file = path.join(tempDirs.make("desktop-resize-fixture-"), "fixture.json"); - const value = fixture(carrier); - await writeFile(file, JSON.stringify(value)); - expect(await readDesktopResizeFixture(file)).toEqual(value); - expect(value).not.toHaveProperty("crabboxCommit"); - }, - ); + it("retains explicit upstream provenance for a node carrier", async () => { + const file = path.join(tempDirs.make("desktop-resize-fixture-"), "fixture.json"); + const value = fixture("node"); + await writeFile(file, JSON.stringify(value)); + expect(await readDesktopResizeFixture(file)).toEqual(value); + }); it("retains real Crabbox provenance as a separate source kind", async () => { const file = path.join(tempDirs.make("desktop-resize-fixture-"), "fixture.json"); @@ -287,7 +283,7 @@ describe("desktop endpoint packet attribution", () => { }, ); - it.each(["abort", "close", "upstream-close", "overflow"])( + it.each(["close", "upstream-close", "overflow"])( "rejects unfinished evidence on %s", async (kind) => { const owner = await openEndpointTap(); @@ -297,9 +293,7 @@ describe("desktop endpoint packet attribution", () => { const echo = once(client, "data"); client.write(Buffer.from(probe.bytes.slice(0, 10))); await echo; - if (kind === "abort") { - owner.abort.abort(); - } else if (kind === "close") { + if (kind === "close") { await owner.tap.close(); } else if (kind === "upstream-close") { owner.peers.forEach((socket) => socket.destroy()); diff --git a/test/helpers/managed-handoff-isolation.test.ts b/test/helpers/managed-handoff-isolation.test.ts index f2ee0ade5400..8dff0d56ea51 100644 --- a/test/helpers/managed-handoff-isolation.test.ts +++ b/test/helpers/managed-handoff-isolation.test.ts @@ -41,63 +41,45 @@ function fixture() { "process.stdout.write(JSON.stringify({ databasePath, observed, result, nodeOptions: process.env.NODE_OPTIONS ?? null }));", ].join("\n"), ); - return { root, binding, program }; -} - -describe("explicit managed handoff test binding", () => { - it.each(["inherited", "service-sanitized", "replaced", "NODE_OPTIONS-deleted"] as const)( - "opens the real lease store after %s environment selection", - (selection) => { - const { root, binding, program } = fixture(); - const env = - selection === "service-sanitized" - ? resolveServiceManagerEnv() - : selection === "replaced" - ? { ...resolveServiceManagerEnv(), HOME: root, USERPROFILE: root } - : { ...process.env }; - if (selection !== "inherited") { - delete env.NODE_OPTIONS; - } + return { + root, + binding, + program, + run: (env: NodeJS.ProcessEnv = resolveServiceManagerEnv()) => { const child = spawnSync(requireNodeTool("node"), [binding.nodeOption, program], { env, encoding: "utf8", timeout: 15_000, }); expect(child.error).toBeUndefined(); - expect(child.status, child.stderr).toBe(0); - const result = JSON.parse(child.stdout); - expect(result).toMatchObject({ - databasePath: binding.databasePath, - observed: { kind: "current", lease: { owner: "binding-owner" } }, - result: { kind: "absent" }, - }); - if (selection !== "inherited") { - expect(result.nodeOptions).toBeNull(); - } - expect(fs.statSync(binding.databasePath).isFile()).toBe(true); - expect(binding.assertPath(result.databasePath)).toBe(binding.databasePath); - assertManagedHandoffTestConsumer(binding, child.pid, path.resolve("src")); - const witnesses = fs.readdirSync(root).filter((name) => name.startsWith("preflight-")); - expect(witnesses.length).toBeGreaterThan(0); - for (const name of witnesses) { - expect(JSON.parse(fs.readFileSync(path.join(root, name), "utf8"))).toMatchObject({ - pid: child.pid, - databasePath: binding.databasePath, - realParent: root, - }); - } + return child; }, - ); + }; +} + +describe("explicit managed handoff test binding", () => { + it("opens the real lease store with a replaced environment and no NODE_OPTIONS", () => { + const { root, binding, run } = fixture(); + const env: NodeJS.ProcessEnv = { ...resolveServiceManagerEnv(), HOME: root, USERPROFILE: root }; + delete env.NODE_OPTIONS; + const child = run(env); + expect(child.status, child.stderr).toBe(0); + const result = JSON.parse(child.stdout); + expect(result).toMatchObject({ + databasePath: binding.databasePath, + observed: { kind: "current", lease: { owner: "binding-owner" } }, + result: { kind: "absent" }, + nodeOptions: null, + }); + expect(fs.statSync(binding.databasePath).isFile()).toBe(true); + expect(binding.assertPath(result.databasePath)).toBe(binding.databasePath); + assertManagedHandoffTestConsumer(binding, child.pid, path.resolve("src")); + }); it("does not credit preload setup as target consumer use", () => { - const { binding, program, root } = fixture(); + const { binding, program, root, run } = fixture(); fs.writeFileSync(program, 'process.stdout.write("entrypoint-ran");'); - const child = spawnSync(requireNodeTool("node"), [binding.nodeOption, program], { - env: resolveServiceManagerEnv(), - encoding: "utf8", - timeout: 15_000, - }); - expect(child.error).toBeUndefined(); + const child = run(); expect(child.status, child.stderr).toBe(0); expect(child.stdout).toBe("entrypoint-ran"); expect(fs.readdirSync(root).some((name) => name.startsWith("preflight-"))).toBe(true); @@ -109,8 +91,6 @@ describe("explicit managed handoff test binding", () => { it.each([ ["create", "dev"], - ["create", "ino"], - ["validate", "dev"], ["validate", "ino"], ] as const)( "refuses unavailable Windows %s directory %s before database access", @@ -143,7 +123,7 @@ describe("explicit managed handoff test binding", () => { ); it("preserves a separately resolved consumer package's import and require conditions", () => { - const { root, binding, program } = fixture(); + const { root, binding, program, run } = fixture(); const consumer = path.join(root, "consumer-package"); const dependency = path.join(consumer, "node_modules", "@openclaw", "fs-safe"); fs.mkdirSync(dependency, { recursive: true }); @@ -182,12 +162,7 @@ describe("explicit managed handoff test binding", () => { "}))));", ].join("\n"), ); - const child = spawnSync(requireNodeTool("node"), [binding.nodeOption, program], { - env: resolveServiceManagerEnv(), - encoding: "utf8", - timeout: 15_000, - }); - expect(child.error).toBeUndefined(); + const child = run(); expect(child.status, child.stderr).toBe(0); expect(JSON.parse(child.stdout)).toEqual([ { root, consumer: "commonjs" }, @@ -197,7 +172,7 @@ describe("explicit managed handoff test binding", () => { }); it("preserves an explicitly selected application cache", () => { - const { binding, program } = fixture(); + const { program, run } = fixture(); const cache = temporary.make("openclaw-explicit-cache-"); fs.writeFileSync( program, @@ -208,12 +183,7 @@ describe("explicit managed handoff test binding", () => { `process.stdout.write(resolveSecureTempRoot({ preferredDir: ${JSON.stringify(cache)}, fallbackPrefix: "cache", skipPreferredOnWindows: false }));`, ].join("\n"), ); - const child = spawnSync(requireNodeTool("node"), [binding.nodeOption, program], { - env: resolveServiceManagerEnv(), - encoding: "utf8", - timeout: 15_000, - }); - expect(child.error).toBeUndefined(); + const child = run(); expect(child.status, child.stderr).toBe(0); expect(child.stdout).toBe(cache); }); @@ -230,7 +200,7 @@ describe("explicit managed handoff test binding", () => { it.each(["database-symlink", "database-hardlink", "wal-symlink", "parent-replaced"] as const)( "refuses %s before executing the store consumer", (failure) => { - const { root, binding, program } = fixture(); + const { root, binding, program, run } = fixture(); const outside = temporary.make("openclaw-handoff-untouched-"); const protectedFile = path.join(outside, "untouched"); fs.writeFileSync(protectedFile, "unchanged"); @@ -254,12 +224,7 @@ describe("explicit managed handoff test binding", () => { binding.databasePath + (failure === "wal-symlink" ? "-wal" : ""), ); } - const child = spawnSync(requireNodeTool("node"), [binding.nodeOption, program], { - env: resolveServiceManagerEnv(), - encoding: "utf8", - timeout: 15_000, - }); - expect(child.error).toBeUndefined(); + const child = run(); expect(child.status).not.toBe(0); expect(child.stderr).toMatch( /Handoff test (?:database (?:alias|hardlink)|directory identity)/, diff --git a/test/jsdom-compat.test.ts b/test/jsdom-compat.test.ts index 806a3d9b7bae..ed42924e1518 100644 --- a/test/jsdom-compat.test.ts +++ b/test/jsdom-compat.test.ts @@ -75,7 +75,7 @@ describe("jsdom native API boundary", () => { } }); - it.each([0, 1, 2])("keeps window event identity across %i iframe levels", (depth) => { + it.each([0, 2])("keeps window event identity across %i iframe levels", (depth) => { let target: Window = window; let outerFrame: HTMLIFrameElement | undefined; for (let level = 0; level < depth; level++) { diff --git a/test/line-question-gateway.test.ts b/test/line-question-gateway.test.ts index 0da292728839..9eeb51e98634 100644 --- a/test/line-question-gateway.test.ts +++ b/test/line-question-gateway.test.ts @@ -395,17 +395,6 @@ describe("LINE public webhook question Gateway boundary", () => { ); expect(await queue.listFailed?.()).toEqual([]); const calls = boundary.callGateway.mock.calls.map(([request]) => request); - console.info( - "LINE_QUESTION_GATEWAY_PROOF", - JSON.stringify({ - scenario: testCase.name, - trace: boundary.trace, - writes: calls.filter((request) => request.method === "question.resolve").length, - replies: providerCalls.filter((request) => request.operation === "reply").length, - pushes: providerCalls.filter((request) => request.operation === "push").length, - pairings: boundary.upsertPairing.mock.calls.length, - }), - ); expect(unexpectedProviderIo).toEqual([]); expect(calls.map((request) => request.method)).toEqual( testCase.paired diff --git a/test/matrix-channel-read-authority.integration.test.ts b/test/matrix-channel-read-authority.integration.test.ts index a150739e3f18..98166e69c8f8 100644 --- a/test/matrix-channel-read-authority.integration.test.ts +++ b/test/matrix-channel-read-authority.integration.test.ts @@ -6,7 +6,6 @@ import { registerMatrixFullRuntime } from "../extensions/matrix/index.js"; import { setMatrixRuntime } from "../extensions/matrix/test-api.js"; import { createOperationalRunInstanceRef } from "../src/agents/admitted-run-context.js"; import { dispatchChannelMessageAction } from "../src/channels/plugins/message-action-dispatch.js"; -import type { ChannelMessageActionAdapter } from "../src/channels/plugins/types.core.js"; import type { ChannelMessageActionContext, ChannelMessageActionName, @@ -208,14 +207,7 @@ type MatrixHarness = Awaited>; // Installation provenance is a registrar fixture, as in the shared dispatcher suite. // Policy, action handlers, SDK requests, host instance and lifecycle are real; E2EE is off. -async function createHarness( - origin: RegistrationOrigin, - actionOverrides?: Partial, -) { - const actions = matrixPlugin.actions; - if (!actions) { - throw new Error("Expected Matrix message actions"); - } +async function createHarness(origin: RegistrationOrigin) { const requests: Array<{ method: string | undefined; path: string; @@ -268,7 +260,7 @@ async function createHarness( setMatrixRuntime(api.runtime); registerMatrixFullRuntime(api); api.registerChannel({ - plugin: { ...matrixPlugin, status: undefined, actions: { ...actions, ...actionOverrides } }, + plugin: { ...matrixPlugin, status: undefined }, }); }); setActivePluginRegistry(owner.registry); @@ -322,9 +314,8 @@ async function createHarness( async function withHarness( origin: RegistrationOrigin, run: (fixture: MatrixHarness) => Promise, - actionOverrides?: Partial, ) { - const fixture = await createHarness(origin, actionOverrides); + const fixture = await createHarness(origin); try { await run(fixture); } finally { @@ -442,64 +433,68 @@ describe("Matrix member info CLI", () => { }); }); -describe.each(["bundled", "official-installed"] as const)( - "registered Matrix reads (%s)", - (origin) => { - it.each(reads)( - "reads $action from a configured sibling room", - async ({ action, params, path, result }) => { - await withHarness(origin, async (fixture) => { - const outcome = await fixture.invoke(action, params); - expect(outcome?.details).toMatchObject({ ok: true, ...result }); - expect(fixture.requests.map((request) => request.path)).toContain(path); - expect( - fixture.requests.every( - (request) => - request.method === "GET" && request.authorization === `Bearer ${accessToken}`, - ), - ).toBe(true); - expect( - fixture.requests.filter((request) => responseFor(request.path) === undefined), - ).toEqual([]); - }); - }, - ); +describe("registered installed Matrix reads", () => { + it.each(reads)( + "reads $action from a configured sibling room", + async ({ action, params, path, result }) => { + await withHarness("official-installed", async (fixture) => { + const outcome = await fixture.invoke(action, params); + expect(outcome?.details).toMatchObject({ ok: true, ...result }); + expect(fixture.requests.map((request) => request.path)).toContain(path); + expect( + fixture.requests.every( + (request) => + request.method === "GET" && request.authorization === `Bearer ${accessToken}`, + ), + ).toBe(true); + expect( + fixture.requests.filter((request) => responseFor(request.path) === undefined), + ).toEqual([]); + }); + }, + ); - it.each([ - { owner: "caller", boundary: "preparation" }, - { owner: "plugin", boundary: "preparation" }, - { owner: "caller", boundary: "result" }, - { owner: "plugin", boundary: "result" }, - ] as const)("fences $owner revocation at $boundary", async ({ owner, boundary }) => { - await withHarness(origin, async (fixture) => { - const revokeAt = boundary === "result" ? messagePath : `${roomPath}/state/m.room.name/`; - fixture.onRequest((path) => { - if (path === revokeAt) { - if (owner === "caller") { - fixture.run.revoke(); - } else { - void fixture.instance.dispose(); - } + it.each([ + { owner: "caller", boundary: "preparation" }, + { owner: "plugin", boundary: "preparation" }, + { owner: "caller", boundary: "result" }, + { owner: "plugin", boundary: "result" }, + ] as const)("fences $owner revocation at $boundary", async ({ owner, boundary }) => { + await withHarness("official-installed", async (fixture) => { + const revokeAt = boundary === "result" ? messagePath : `${roomPath}/state/m.room.name/`; + fixture.onRequest((path) => { + if (path === revokeAt) { + if (owner === "caller") { + fixture.run.revoke(); + } else { + void fixture.instance.dispose(); } - }); - await expect(fixture.invoke("read", { limit: 1 })).rejects.toThrow(/no longer active/); - if (boundary === "preparation") { - expect(fixture.requests.map((request) => request.path)).toEqual([revokeAt]); - } else { - expect(fixture.requests.filter((request) => request.path === messagePath)).toHaveLength( - 1, - ); - } - if (owner === "plugin") { - expect((await fixture.instance.dispose()).errors).toEqual([]); - expect(fixture.lifecycle.signal?.aborted).toBe(true); } }); + await expect(fixture.invoke("read", { limit: 1 })).rejects.toThrow(/no longer active/); + if (boundary === "preparation") { + expect(fixture.requests.map((request) => request.path)).toEqual([revokeAt]); + } else { + expect(fixture.requests.filter((request) => request.path === messagePath)).toHaveLength(1); + } + if (owner === "plugin") { + expect((await fixture.instance.dispose()).errors).toEqual([]); + expect(fixture.lifecycle.signal?.aborted).toBe(true); + } }); - }, -); + }); +}); describe("installed Matrix read restrictions", () => { + it("keeps verification operations outside the cross-room read capability", async () => { + await withHarness("official-installed", async (fixture) => { + await expect(fixture.invoke("permissions", { messageId })).rejects.toThrow( + "exact current conversation", + ); + expect(fixture.requests).toEqual([]); + }); + }); + it.each(["room", "action", "account"] as const)( "preserves the existing %s denial before provider access", async (denial) => { @@ -534,48 +529,4 @@ describe("installed Matrix read restrictions", () => { expect(fixture.requests.some((request) => request.path.includes("/profile/"))).toBe(false); }); }); - - it("keeps verification and message mutations outside the read capability", async () => { - const excludedHandler = vi.fn(() => { - throw new Error("Excluded Matrix action reached its provider handler"); - }); - await withHarness( - "official-installed", - async (fixture) => { - for (const action of [ - "permissions", - "react", - "edit", - "delete", - "pin", - "unpin", - "poll-vote", - ] as const) { - await expect(fixture.invoke(action, { messageId })).rejects.toThrow( - "exact current conversation", - ); - } - expect(excludedHandler).not.toHaveBeenCalled(); - expect(fixture.requests).toEqual([]); - }, - { handleAction: excludedHandler }, - ); - }); - - it("does not grant newly classified reads to an adapter that did not opt in", async () => { - await withHarness( - "official-installed", - async (fixture) => { - expect((await fixture.invoke("read", { limit: 1 }))?.details).toMatchObject({ ok: true }); - fixture.requests.length = 0; - for (const action of ["member-info", "emoji-list"] as const) { - await expect(fixture.invoke(action, { userId: memberId })).rejects.toThrow( - "exact current conversation", - ); - } - expect(fixture.requests).toEqual([]); - }, - { readAuthorityActions: ["read"] }, - ); - }); }); diff --git a/test/matrix-transport-read-authority.integration.test.ts b/test/matrix-transport-read-authority.integration.test.ts index e20998708990..c7a489c88424 100644 --- a/test/matrix-transport-read-authority.integration.test.ts +++ b/test/matrix-transport-read-authority.integration.test.ts @@ -19,7 +19,7 @@ const READ_PATH = "/_matrix/client/v3/rooms/%21room%3Amatrix.test/messages"; const PAYLOAD = JSON.stringify({ chunk: [{ event_id: "$fixture", content: { body: "context" } }] }); const DNS_ANSWER: LookupAddress[] = [{ address: "127.0.0.1", family: 4 }]; const CLOSED_AUTHORITY = "Channel read authority is no longer active."; -const TRANSPORTS = ["performMatrixRequest JSON", "performMatrixRequest raw", "SDK fetch"] as const; +const TRANSPORTS = ["performMatrixRequest JSON", "SDK fetch"] as const; class TransportMatrixClient extends MatrixClient { async readTransport(transport: (typeof TRANSPORTS)[number], path: string): Promise { @@ -28,14 +28,6 @@ class TransportMatrixClient extends MatrixClient { const response = await this.client.http.fetch(`${HOMESERVER}${path}`); return await response.text(); } - if (transport === "performMatrixRequest raw") { - const buffer = await this.httpClient.requestRaw({ - method: "GET", - endpoint: path, - timeoutMs: 5000, - }); - return buffer.toString("utf8"); - } return await this.doRequest("GET", path); } } @@ -82,20 +74,6 @@ afterEach(async () => { }); describe.each(TRANSPORTS)("%s read authority", (transport) => { - it("returns an allowed read while its host scope is active", async () => { - const runtimeFetch = vi.fn(async () => new Response(PAYLOAD)); - stubRuntimeFetch(runtimeFetch); - const { read } = createReader(transport); - - await expect( - withChannelReadAuthority( - () => undefined, - () => read(READ_PATH), - ), - ).resolves.toBe(PAYLOAD); - expect(runtimeFetch.mock.calls.map(([url]) => url)).toEqual([`${HOMESERVER}${READ_PATH}`]); - }); - it.each([ { name: "initial DNS", redirect: false }, { name: "redirect DNS", redirect: true }, diff --git a/test/msteams-read-authority.integration.test.ts b/test/msteams-read-authority.integration.test.ts index 05d077289b39..10f54ed43611 100644 --- a/test/msteams-read-authority.integration.test.ts +++ b/test/msteams-read-authority.integration.test.ts @@ -119,8 +119,6 @@ async function createFixture( origin?: Origin; self?: boolean; narrowTeam?: boolean; - legacy?: boolean; - unverified?: boolean; senderIsOwner?: boolean; botFrameworkTeam?: boolean; missingRequester?: boolean; @@ -156,7 +154,7 @@ async function createFixture( const record = createPluginRecord({ id: "msteams", origin, - trustedOfficialInstall: origin === "global" && !options.unverified, + trustedOfficialInstall: origin === "global", }); const providerActions = msteamsPlugin.actions!; const providerSettlements: Array<{ @@ -169,7 +167,6 @@ async function createFixture( status: undefined, actions: { ...providerActions, - readAuthorityActions: options.legacy ? undefined : providerActions.readAuthorityActions, handleAction: async (ctx: ChannelMessageActionContext) => { try { const pending = providerActions.handleAction!(ctx); @@ -390,8 +387,6 @@ async function createFixture( const surfaces = [ ["tool", "bundled"], - ["tool", "global"], - ["gateway", "bundled"], ["gateway", "global"], ] as const; @@ -536,7 +531,6 @@ describe("Teams Graph mutation currentness", () => { [307, false], [307, true], [308, false], - [308, true], ] as const)( "checks a same-origin %s redirect before replay (revoked=%s)", async (status, revoked) => { @@ -815,7 +809,7 @@ describe("Teams Graph mutation currentness", () => { // Registry provenance is an explicit fixture; package installation is proved separately. describe.each(surfaces)("Teams %s reads with a %s registration", (route, origin) => { - it("runs all seven existing read actions", async () => { + it("reads messages, reactions, pins and channel metadata", async () => { const fixture = await createFixture({ origin }); const messagePath = `/v1.0/teams/${teamId}/channels/${targetChannel}/messages/${messageId}`; const cases: Array<{ @@ -842,21 +836,6 @@ describe.each(surfaces)("Teams %s reads with a %s registration", (route, origin) payload: { pins: [{ pinnedMessageId: "pin-1", messageId, text: message.body.content }] }, requests: [`GET /v1.0/chats/${chatId}/pinnedMessages`], }, - { - action: "search", - params: { query: "permitted" }, - payload: { messages: [{ id: messageId, text: message.body.content }], truncated: false }, - requests: [`GET /v1.0/teams/${teamId}/channels/${currentChannel}/messages`], - }, - { - action: "member-info", - params: { userId: memberId }, - payload: { user: { id: memberId, displayName: "Member", roles: [] } }, - requests: [ - `GET /v1.0/teams/${teamId}/channels/${currentChannel}`, - `GET /v1.0/teams/${teamId}/members`, - ], - }, { action: "channel-info", params: { teamId, channelId: targetChannel }, @@ -916,37 +895,17 @@ describe.each(surfaces)("Teams %s reads with a %s registration", (route, origin) expect(fixture.requests).toHaveLength(1); }); - it.each([false, true])( - "fences the local requester-only member result (revoked=%s)", - async (revoked) => { - const fixture = await createFixture({ origin, self: true }); - if (revoked) { - graph.afterEntry = fixture.retirePlugin; - } - const result = fixture.invoke(route, "member-info", { userId: requesterId }); - if (revoked) { - await expect(result).rejects.toThrow(/no longer active|authority/i); - } else { - await expect(result).resolves.toMatchObject({ user: { id: requesterId, roles: [] } }); - } - expect(fixture.requests).toEqual([]); - expect(graph.acquireToken).not.toHaveBeenCalled(); - }, - ); + it("fences the local requester-only member result when the plugin closes", async () => { + const fixture = await createFixture({ origin, self: true }); + graph.afterEntry = fixture.retirePlugin; + const result = fixture.invoke(route, "member-info", { userId: requesterId }); + await expect(result).rejects.toThrow(/no longer active|authority/i); + expect(fixture.requests).toEqual([]); + expect(graph.acquireToken).not.toHaveBeenCalled(); + }); }); describe.each(["tool", "gateway"] as const)("Teams %s policy controls", (route) => { - it.each(["legacy", "unverified"] as const)( - "does not grant cross-conversation reads to a %s adapter", - async (kind) => { - const fixture = await createFixture({ [kind]: true }); - await expect(fixture.invoke(route, "read", { target, messageId })).rejects.toThrow( - /exact current conversation/i, - ); - expect(fixture.requests).toEqual([]); - }, - ); - it("rejects an unknown account before Graph", async () => { const fixture = await createFixture(); await expect( diff --git a/test/msteams-read-target.integration.test.ts b/test/msteams-read-target.integration.test.ts index 56e9b7c8efb6..d6c5206b1996 100644 --- a/test/msteams-read-target.integration.test.ts +++ b/test/msteams-read-target.integration.test.ts @@ -472,23 +472,32 @@ describe("Teams message CLI", () => { }); }); +describe.each([ + ["tool", "search", "bundled"], + ["gateway", "member-info", "global"], +] as const)("Teams %s %s aliases (%s)", (route, action, origin) => { + it.each([ + { name: "bare", channelId: current.channelId }, + { name: "conversation-prefixed", channelId: `conversation:${current.channelId}` }, + { name: "provider-prefixed", channelId: `msteams:${current.channelId}` }, + { name: "provider alias", channelId: `teams:conversation:${current.channelId}` }, + { name: "thread-qualified", channelId: `conversation:${current.channelId};messageid=123` }, + { name: "Graph", channelId: currentTarget }, + ])("reads the current channel with a $name target", async ({ channelId }) => { + const fixture = await createFixture("channel", origin); + const result = await fixture.invoke(route, action, { channelId }); + expectReadResult(result, action, current); + expectGraphRequests(fixture.requests, action, current); + }); +}); + describe.each(["tool", "gateway"] as const)("Teams %s read target selection", (route) => { describe.each(["search", "member-info"] as const)("%s", (action) => { - describe.each(["bundled", "global"] as const)("%s registration", (origin) => { - it.each([ - { name: "omitted", channelId: undefined }, - { name: "bare", channelId: current.channelId }, - { name: "conversation-prefixed", channelId: `conversation:${current.channelId}` }, - { name: "provider-prefixed", channelId: `msteams:${current.channelId}` }, - { name: "provider alias", channelId: `teams:conversation:${current.channelId}` }, - { name: "thread-qualified", channelId: `conversation:${current.channelId};messageid=123` }, - { name: "Graph", channelId: currentTarget }, - ])("reads the current channel with a $name target", async ({ channelId }) => { - const fixture = await createFixture("channel", origin); - const result = await fixture.invoke(route, action, channelId ? { channelId } : {}); - expectReadResult(result, action, current); - expectGraphRequests(fixture.requests, action, current); - }); + it("reads the current channel when the target is omitted", async () => { + const fixture = await createFixture(); + const result = await fixture.invoke(route, action); + expectReadResult(result, action, current); + expectGraphRequests(fixture.requests, action, current); }); it("uses an explicit permitted channelId instead of the current channel", async () => { @@ -531,17 +540,6 @@ describe.each(["tool", "gateway"] as const)("Teams %s read target selection", (r expect(graph.acquireToken).not.toHaveBeenCalled(); }, ); - - it.each([current.channelId, otherTarget])( - "rejects an unknown account for %s", - async (channelId) => { - const fixture = await createFixture(); - await expect( - fixture.invoke(route, action, { channelId, accountId: "other" }), - ).rejects.toThrow(/account/i); - expect(fixture.requests).toEqual([]); - }, - ); }); it("keeps the requester-only member shortcut in the current chat", async () => { @@ -565,8 +563,6 @@ describe.each(["search", "member-info"] as const)( for (const [params, destination] of [ [{ to: currentTarget, target: otherTarget, channelId: deniedTarget }, current], [{ target: otherTarget, channelId: deniedTarget }, other], - [{ to: otherTarget, target: current.channelId, channelId: current.channelId }, other], - [{ target: current.channelId, channelId: otherTarget }, current], ] as const) { const before = fixture.requests.length; const result = await fixture.invokeAdapter(action, params); diff --git a/test/node-host-launcher.test.ts b/test/node-host-launcher.test.ts index 1a40c7707afe..e6f2650e8ef4 100644 --- a/test/node-host-launcher.test.ts +++ b/test/node-host-launcher.test.ts @@ -114,7 +114,15 @@ async function fixture(body: string, stateRelativePath = "state") { return { prefix, packageRoot }; }; const current = path.join(runtimeDirectory, "current"); - return { root, base, stateDir, runtimeDirectory, current, release }; + const select = async (prefix: string, options: { stale?: boolean; backup?: boolean } = {}) => { + const selector = options.backup ? `${current}.previous` : current; + await fs.symlink(prefix, selector, process.platform === "win32" ? "junction" : "dir"); + if (options.stale) { + const old = new Date(Date.now() - 13 * 60 * 60 * 1_000); + await fs.lutimes(selector, old, old); + } + }; + return { root, base, stateDir, runtimeDirectory, current, release, select }; } function run( @@ -212,13 +220,6 @@ describe("managed node launcher", () => { expect(JSON.parse(result.stdout.trim())).toEqual({ supervised: false }); }); - it("lets a drained runtime exit without the supervisor IPC keeping it alive", async () => { - const f = await fixture("await ready(); report('drained');"); - const result = await run(f.base, f.stateDir).done; - expect(result.code, result.stderr).toBe(0); - expect(result.stdout).toContain('"drained"'); - }); - it.each(["--openclaw-node-host-child", "--openclaw-node-host-managed-child"])( "rejects private %s arguments without supervisor IPC", async (argument) => { @@ -248,7 +249,7 @@ report({before, after: getManagedNodeHostStatePath() ?? null}); process.exit(0); `, ); - await fs.symlink(managed.prefix, f.current, process.platform === "win32" ? "junction" : "dir"); + await f.select(managed.prefix); const result = await run(f.base, f.stateDir).done; expect(result.code, result.stderr).toBe(0); expect(JSON.parse(result.stdout.trim())).toEqual({ @@ -272,11 +273,7 @@ if (!process.env.TEST_BOOTSTRAPPED) { const f = await fixture(body); if (selected === "managed") { const managed = await f.release("2026.9.2", body); - await fs.symlink( - managed.prefix, - f.current, - process.platform === "win32" ? "junction" : "dir", - ); + await f.select(managed.prefix); } const argv = ["node", "run", "--display-name", "bootstrap"]; const launched = run(f.base, f.stateDir, argv, { @@ -296,11 +293,8 @@ if (!process.env.TEST_BOOTSTRAPPED) { ); it.each([ - ["2026.9.2", "managed"], ["2026.9.1", "managed"], ["2026.8.9", "base"], - ["2026.9.1-beta.1", "base"], - ["2026.9.1-1", "managed"], ])( "selects the newest runtime (%s) before loading the invoking package", async (version, expected) => { @@ -308,11 +302,7 @@ if (!process.env.TEST_BOOTSTRAPPED) { `report({selected:'${selected}',state:getManagedNodeHostStatePath() ?? null,argv:process.argv.slice(2)}); process.exit(0);`; const f = await fixture(reportSelection("base")); const managed = await f.release(version, reportSelection("managed")); - await fs.symlink( - managed.prefix, - f.current, - process.platform === "win32" ? "junction" : "dir", - ); + await f.select(managed.prefix); const result = await run(f.base, f.stateDir).done; expect(result.code, result.stderr).toBe(0); expect(JSON.parse(result.stdout.trim())).toEqual({ @@ -398,9 +388,7 @@ setTimeout(() => { }, 100); `, ); - await fs.symlink(previous.prefix, f.current, process.platform === "win32" ? "junction" : "dir"); - const old = new Date(Date.now() - 13 * 60 * 60 * 1_000); - await fs.lutimes(f.current, old, old); + await f.select(previous.prefix, { stale: true }); const before = await fs.lstat(f.current); const result = await run(f.base, f.stateDir, ["node", "run"], { TEST_RUNTIME_ROOT: candidate.prefix, @@ -433,13 +421,7 @@ if (process.env.TEST_SELECT_ONLY || version === process.env.TEST_VERSION) { const f = await fixture("throw new Error('the global runtime must not load');"); const previous = await f.release("2026.9.2", body); const candidate = await f.release("2026.9.3", body); - await fs.symlink( - previous.prefix, - f.current, - process.platform === "win32" ? "junction" : "dir", - ); - const old = new Date(Date.now() - 13 * 60 * 60 * 1_000); - await fs.lutimes(f.current, old, old); + await f.select(previous.prefix, { stale: true }); const previousMtime = (await fs.lstat(f.current)).mtimeMs; const { env, execArgv } = await windowsSelectorFixture(f.root, f.current, failure); const result = await run( @@ -536,13 +518,7 @@ if (fs.existsSync(process.env.TEST_ATTEMPT)) { let previous; if (selected === "managed") { previous = await f.release("2026.9.1", body); - await fs.symlink( - previous.prefix, - f.current, - process.platform === "win32" ? "junction" : "dir", - ); - const old = new Date(Date.now() - 13 * 60 * 60 * 1_000); - await fs.lutimes(f.current, old, old); + await f.select(previous.prefix, { stale: true }); } const candidate = await f.release("2026.9.2", `${reportRuntime} process.exit(42);`); const result = await run(f.base, f.stateDir, ["node", "run"], { @@ -607,9 +583,7 @@ await ready(); process.exit(0); `, ); - await fs.symlink(previous.prefix, f.current, process.platform === "win32" ? "junction" : "dir"); - const old = new Date(Date.now() - 13 * 60 * 60 * 1_000); - await fs.lutimes(f.current, old, old); + await f.select(previous.prefix, { stale: true }); const before = await fs.lstat(f.current); const candidateStarted = path.join(f.root, "candidate-started"); const result = await run(f.base, f.stateDir, ["node", "run"], { @@ -632,8 +606,6 @@ process.exit(0); it.each([ ["2026.9.2", "current"], - ["2026.9.2-1", "current"], - ["2026.9.2", "current.previous"], ["2026.9.2-1", "current.previous"], ])( "refuses %s after another parent publishes a newer shared runtime at %s", @@ -676,11 +648,7 @@ ${requestUpdate} "2026.9.2", "report({selected:'managed',state:process.env.OPENCLAW_STATE_DIR ?? null,home:process.env.OPENCLAW_HOME ?? null,managedState:getManagedNodeHostStatePath() ?? null}); process.exit(0);", ); - await fs.symlink( - managed.prefix, - f.current, - process.platform === "win32" ? "junction" : "dir", - ); + await f.select(managed.prefix); const inheritedHome = path.join(f.root, "inherited-home"); const defaultState = path.join(inheritedHome, ".openclaw"); const gatewayEnvDir = path.join(inheritedHome, ".config", "openclaw"); @@ -746,7 +714,7 @@ syncBuiltinESMExports(); "2026.9.2", "report({selected:'profile-managed',state:getManagedNodeHostStatePath() ?? null}); process.exit(0);", ); - await fs.symlink(managed.prefix, f.current, process.platform === "win32" ? "junction" : "dir"); + await f.select(managed.prefix); const home = path.dirname(f.stateDir); await fs.mkdir(path.join(home, ".openclaw"), { recursive: true }); await fs.writeFile( @@ -780,11 +748,7 @@ syncBuiltinESMExports(); ); if (condition === "cooldown" || condition === "backup-cooldown") { const current = await f.release("2026.9.1", requestUpdate); - await fs.symlink( - current.prefix, - condition === "backup-cooldown" ? `${f.current}.previous` : f.current, - process.platform === "win32" ? "junction" : "dir", - ); + await f.select(current.prefix, { backup: condition === "backup-cooldown" }); } else if (condition === "existing-lock") { await fs.writeFile( path.join(f.runtimeDirectory, "activation.lock"), diff --git a/test/plugins/chat-abort-codex.gateway.test.ts b/test/plugins/chat-abort-codex.gateway.test.ts index a8408a8d7950..098406244975 100644 --- a/test/plugins/chat-abort-codex.gateway.test.ts +++ b/test/plugins/chat-abort-codex.gateway.test.ts @@ -81,7 +81,6 @@ describe("chat.abort native transcript settlement", () => { hasNativeText, nativeFirst: false, superseded: false, - warningDeliveryFails: false, })), ), { @@ -89,18 +88,16 @@ describe("chat.abort native transcript settlement", () => { hasNativeText: true, nativeFirst: true, superseded: false, - warningDeliveryFails: false, }, - ...[false, true].map((warningDeliveryFails) => ({ + { owner: "agent" as const, hasNativeText: false, nativeFirst: false, superseded: true, - warningDeliveryFails, - })), + }, ])( - "settles Stop history ($owner, native text=$hasNativeText, native first=$nativeFirst, superseded=$superseded, warning delivery fails=$warningDeliveryFails)", - async ({ owner, hasNativeText, nativeFirst, superseded, warningDeliveryFails }) => { + "settles Stop history ($owner, native text=$hasNativeText, native first=$nativeFirst, superseded=$superseded)", + async ({ owner, hasNativeText, nativeFirst, superseded }) => { await withOpenClawTestState({ label: "chat-abort-codex" }, async () => { const target = await fixture.createTarget(); session.target = target; @@ -121,7 +118,8 @@ describe("chat.abort native transcript settlement", () => { trackExecution: (run: () => T | Promise) => execution.track(run), }; const context = createChatAbortContext(dispatchContext); - if (warningDeliveryFails) { + // Failed warning delivery must release the superseded producer too. + if (superseded) { vi.mocked(context.broadcast).mockImplementation((event, payload) => { if (event === "chat" && isRecord(payload) && payload.state === "error") { throw new Error("Synthetic warning transport failure"); @@ -237,11 +235,9 @@ describe("chat.abort native transcript settlement", () => { expect.anything(), ); expect(loadSessionEntry(target)?.activeWriterRunId).toBe("run-successor"); - if (warningDeliveryFails) { - expect(context.logGateway.warn).toHaveBeenCalledWith( - expect.stringContaining("persistence warning delivery failed"), - ); - } + expect(context.logGateway.warn).toHaveBeenCalledWith( + expect.stringContaining("persistence warning delivery failed"), + ); return; } expect(messages).toHaveLength(1); diff --git a/test/plugins/codex-model-runtime-choice.test.ts b/test/plugins/codex-model-runtime-choice.test.ts index 850e90011104..2568886de7ea 100644 --- a/test/plugins/codex-model-runtime-choice.test.ts +++ b/test/plugins/codex-model-runtime-choice.test.ts @@ -14,7 +14,7 @@ import { createEmptyPluginRegistry } from "../../src/plugins/registry-empty.js"; import { withOpenClawTestState } from "../../src/test-utils/openclaw-test-state.js"; describe("registered Codex runtime choices", () => { - it.each(["api_key", "oauth", "token"] as const)( + it.each(["api_key", "oauth"] as const)( "keeps native %s authentication with its registered harness", async (mode) => { await withOpenClawTestState( diff --git a/test/plugins/dreaming-cron-doctor.integration.test.ts b/test/plugins/dreaming-cron-doctor.integration.test.ts index 00bd701b4e66..4fcfc4b1f78d 100644 --- a/test/plugins/dreaming-cron-doctor.integration.test.ts +++ b/test/plugins/dreaming-cron-doctor.integration.test.ts @@ -587,7 +587,6 @@ describe("host Cron Doctor repair", () => { it.each([ { layout: "inactive only", activeDreaming: false, enabled: true }, { layout: "active and inactive", activeDreaming: true, enabled: true }, - { layout: "inactive only", activeDreaming: false, enabled: false }, { layout: "active and inactive", activeDreaming: true, enabled: false }, ])( "keeps $layout history and authored lookalikes after Doctor and runtime dreaming enabled=$enabled", @@ -673,9 +672,7 @@ describe("host Cron Doctor repair", () => { action: activeDreaming ? "updated" : "added", }, ] - : activeDreaming - ? [{ jobId: "survivor", action: "removed" }] - : [], + : [{ jobId: "survivor", action: "removed" }], ); expect(logger.warn).toHaveBeenCalledWith( expect.stringContaining( diff --git a/test/plugins/memory-dreaming-cron.test.ts b/test/plugins/memory-dreaming-cron.test.ts index 6924aac11253..615fac562d91 100644 --- a/test/plugins/memory-dreaming-cron.test.ts +++ b/test/plugins/memory-dreaming-cron.test.ts @@ -24,7 +24,8 @@ afterEach(async () => { vi.useRealTimers(); }); -function registerDreaming(config: OpenClawConfig, logger: ReturnType) { +async function startDreaming(config: OpenClawConfig, getCronService: () => CronService) { + const logger = createNoopLogger(); const registry = createEmptyPluginRegistry(); memoryCore.register( createTestPluginApi({ @@ -42,7 +43,9 @@ function registerDreaming(config: OpenClawConfig, logger: ReturnType { - vi.useFakeTimers(); - const config: OpenClawConfig = { - agents: { ownership: "explicit", list: [{ id: "qa" }, { id: "qa-extra" }] }, - cron: { enabled: configEnabled }, - }; - const cron = await createScheduler(false); - const add = vi.spyOn(cron, "add"); - const logger = createNoopLogger(); - const registry = registerDreaming(config, logger); - const handle = await startPluginServices({ registry, config, getCronService: () => cron }); - services.add(handle); +it("does not author ownerless dreaming work when the scheduler overrides enabled config", async () => { + vi.useFakeTimers(); + const config: OpenClawConfig = { + agents: { ownership: "explicit", list: [{ id: "qa" }, { id: "qa-extra" }] }, + cron: { enabled: true }, + }; + const cron = await createScheduler(false); + const add = vi.spyOn(cron, "add"); + const { logger } = await startDreaming(config, () => cron); - expect(logger.error).not.toHaveBeenCalled(); - expect(add).not.toHaveBeenCalled(); - await vi.advanceTimersByTimeAsync(60_000); - expect(logger.error).not.toHaveBeenCalled(); - expect(add).not.toHaveBeenCalled(); - expect(await cron.list({ includeDisabled: true })).toEqual([]); - }, -); + expect(logger.error).not.toHaveBeenCalled(); + expect(add).not.toHaveBeenCalled(); + await vi.advanceTimersByTimeAsync(60_000); + expect(logger.error).not.toHaveBeenCalled(); + expect(add).not.toHaveBeenCalled(); + expect(await cron.list({ includeDisabled: true })).toEqual([]); +}); it("creates one owned declaration after scheduling resumes and across service reload", async () => { const config: OpenClawConfig = { @@ -95,13 +92,7 @@ it("creates one owned declaration after scheduling resumes and across service re }, }; let cron = await createScheduler(false, "qa"); - const logger = createNoopLogger(); - const handle = await startPluginServices({ - registry: registerDreaming(config, logger), - config, - getCronService: () => cron, - }); - services.add(handle); + const { handle, logger } = await startDreaming(config, () => cron); expect(await cron.list({ includeDisabled: true })).toEqual([]); cron = await createScheduler(true, "qa"); @@ -153,13 +144,7 @@ it.each([true, false])( entries: { "memory-core": { config: { dreaming: { enabled: dreamingEnabled } } } }, }, }; - const logger = createNoopLogger(); - const handle = await startPluginServices({ - registry: registerDreaming(config, logger), - config, - getCronService: () => cron, - }); - services.add(handle); + const { logger } = await startDreaming(config, () => cron); expect(await cron.list({ includeDisabled: true })).toEqual( dreamingEnabled ? before : before.filter((entry) => entry.id !== managed.id), diff --git a/test/plugins/openrouter-per-day-rate-limit.integration.test.ts b/test/plugins/openrouter-per-day-rate-limit.integration.test.ts index 0dc173d1bddb..7704b465f869 100644 --- a/test/plugins/openrouter-per-day-rate-limit.integration.test.ts +++ b/test/plugins/openrouter-per-day-rate-limit.integration.test.ts @@ -54,8 +54,6 @@ describe("OpenRouter per-day cap reaches the real retry owner", () => { server.listen(0, "127.0.0.1", resolve); }); - let reason: string | null; - let retryMessage: string | undefined; try { const address = server.address() as AddressInfo; const result = await streamOpenAICompletions( @@ -67,28 +65,14 @@ describe("OpenRouter per-day cap reaches the real retry owner", () => { expect(result.stopReason).toBe("error"); expect(result.errorMessage).toContain("free-models-per-day-high-balance"); - reason = classifyAssistantFailoverReason(result, { providerOwner }); - // attempt-recovery.ts:331 passes assistantSignal.message (built by - // buildAssistantFailoverSignal, the RAW trimmed errorMessage) into - // maybeRetryTransient as retry.message — not the user-facing friendly - // copy from formatAssistantErrorText. Match that exact production - // value so this test proves what the real retry guard actually sees. - retryMessage = buildAssistantFailoverSignal(result).message; + expect(classifyAssistantFailoverReason(result, { providerOwner })).toBe("rate_limit"); + // The retry owner consumes the raw assistant signal, not formatted display text. + const retryMessage = buildAssistantFailoverSignal(result).message; + expect(hasLongWindowRateLimitEvidence(retryMessage)).toBe(true); } finally { await new Promise((resolve, reject) => { server.close((error) => (error ? reject(error) : resolve())); }); } - - // Same reason the real embedded-agent-runner attempt loop passes as - // retry.reason into failover-retry-controller.ts's maybeRetryTransient(). - expect(reason).toBe("rate_limit"); - - // The exact guard failover-retry-controller.ts:232 calls with retry.message - // before allowing a same-model transient retry. A message that came from a - // real HTTP round-trip through the real transport must make this return - // true, or the production controller burns retries against an exhausted - // daily cap instead of failing over. - expect(hasLongWindowRateLimitEvidence(retryMessage)).toBe(true); }); }); diff --git a/test/plugins/qwen-dashscope-throttle.integration.test.ts b/test/plugins/qwen-dashscope-throttle.integration.test.ts index fc249584fc37..1b27d86a1867 100644 --- a/test/plugins/qwen-dashscope-throttle.integration.test.ts +++ b/test/plugins/qwen-dashscope-throttle.integration.test.ts @@ -6,7 +6,6 @@ import { beforeAll, describe, expect, it } from "vitest"; import { readPersistedAuthProfileStateRaw } from "../../src/agents/auth-profiles/sqlite.js"; import { loadAuthProfileStoreWithoutExternalProfiles } from "../../src/agents/auth-profiles/store-runtime.js"; import { isProfileInCooldown } from "../../src/agents/auth-profiles/usage-state.js"; -import { classifyAssistantFailoverReason } from "../../src/agents/embedded-agent-helpers/assistant-message-failures.js"; import { handleEmbeddedAssistantFailure } from "../../src/agents/embedded-agent-runner/run/assistant-failure.js"; import { createEmbeddedRunFailoverRetryController } from "../../src/agents/embedded-agent-runner/run/failover-retry-controller.js"; import { resolveEmbeddedRunAttemptTerminalState } from "../../src/agents/embedded-agent-runner/run/terminal-outcome.js"; @@ -151,7 +150,6 @@ async function runThroughFailureRecovery(params: { providerOwner?: ProviderPlugin; fixture: ErrorFixture; }): Promise<{ - reason: string | null; failureReason: unknown; usage: ProfileUsageReadback | undefined; affectedModelBlocked: boolean; @@ -252,32 +250,18 @@ async function runThroughFailureRecovery(params: { } | null; const freshStore = loadAuthProfileStoreWithoutExternalProfiles(state.agentDir()); expect(freshStore.usageStats?.[profileId]).toEqual(persisted?.usageStats?.[profileId]); - const result = { - reason: classifyAssistantFailoverReason(assistant, { - providerOwner: params.providerOwner, - }), + return { failureReason, usage: persisted?.usageStats?.[profileId], affectedModelBlocked: isProfileInCooldown(freshStore, profileId, undefined, MODEL_ID), otherModelBlocked: isProfileInCooldown(freshStore, profileId, undefined, "qwen3.8-flash"), suspensionReasons, }; - console.info( - "DASHSCOPE_PROFILE_PROOF", - JSON.stringify({ - provider: params.provider, - code: params.fixture.code, - preparedOwner: params.providerOwner?.id ?? null, - ...result, - }), - ); - return result; }, ); } function expectRateLimitState(result: Awaited>): void { - expect.soft(result.reason).toBe("rate_limit"); expect.soft(result.failureReason).toBe("rate_limit"); expect.soft(result.usage).toMatchObject({ cooldownReason: "rate_limit", @@ -291,7 +275,6 @@ function expectRateLimitState(result: Awaited>): void { - expect.soft(result.reason).toBe("billing"); expect.soft(result.failureReason).toBe("billing"); expect.soft(result.usage).toMatchObject({ disabledReason: "billing", @@ -306,7 +289,6 @@ function expectOpenRouterState( result: Awaited>, reason: "rate_limit" | "billing", ): void { - expect.soft(result.reason).toBe(reason); expect.soft(result.failureReason).toBe(reason); expect .soft(result.suspensionReasons) @@ -320,13 +302,6 @@ function expectOpenRouterState( describe("Qwen DashScope 429 profile classification", () => { it("keeps registered OpenRouter wrapper errors in the rate-limit lane", async () => { const providerOwner = prepareProviderOwner("openrouter", "openrouter"); - expect( - providerOwner.classifyFailoverReason?.({ - provider: "openrouter", - status: 429, - errorMessage: "Provider returned error", - }), - ).toBe("timeout"); const result = await runThroughFailureRecovery({ provider: "openrouter", providerOwner, @@ -383,21 +358,18 @@ describe("Qwen DashScope 429 profile classification", () => { expectRateLimitState(result); }); - it.each(["PrepaidBillOverdue", "PostpaidBillOverdue"])( - "keeps explicit Qwen %s in the billing lane", - async (code) => { - const result = await runThroughFailureRecovery({ - provider: "qwen", - providerOwner: prepareProviderOwner("qwen"), - fixture: { - status: 429, - code, - message: "The prepaid bill is overdue.", - }, - }); - expectBillingState(result); - }, - ); + it("keeps explicit Qwen overdue bills in the billing lane", async () => { + const result = await runThroughFailureRecovery({ + provider: "qwen", + providerOwner: prepareProviderOwner("qwen"), + fixture: { + status: 429, + code: "PrepaidBillOverdue", + message: "The prepaid bill is overdue.", + }, + }); + expectBillingState(result); + }); it("keeps an ordinary Qwen HTTP 429 in the generic rate-limit lane", async () => { const result = await runThroughFailureRecovery({ diff --git a/test/plugins/session-share.integration.test.ts b/test/plugins/session-share.integration.test.ts index e39541a37209..af128512ab98 100644 --- a/test/plugins/session-share.integration.test.ts +++ b/test/plugins/session-share.integration.test.ts @@ -733,155 +733,148 @@ describe("session-share node commands", () => { }); }); - it.each([undefined, []])( - "does not advertise or publish without share groups %j", - async (groups) => { - await withOpenClawTestState({ scenario: "minimal" }, async () => { - const fixture = commandFixture(groups ?? []); - if (groups === undefined) { - fixture.config.plugins = undefined; - } - const manifest = JSON.parse( - fs.readFileSync( - new URL("../../extensions/session-share/openclaw.plugin.json", import.meta.url), - "utf8", - ), - ) as { configSchema: Record }; - expect( - validateJsonSchemaValue({ - schema: manifest.configSchema, - cacheKey: "session-share.disabled-config", - value: fixture.config.plugins?.entries?.["session-share"]?.config ?? {}, - }).ok, - ).toBe(true); - for (const command of fixture.commands) { - expect(command.isAvailable?.({ config: fixture.config, env: {} })).toBe(false); - } - expect((await fixture.list()).sessions).toEqual([]); - await expect(fixture.read("agent:main:unshared")).rejects.toThrow("not shared"); - }); - }, - ); + it("does not advertise or publish without share configuration", async () => { + await withOpenClawTestState({ scenario: "minimal" }, async () => { + const fixture = commandFixture([]); + fixture.config.plugins = undefined; + const manifest = JSON.parse( + fs.readFileSync( + new URL("../../extensions/session-share/openclaw.plugin.json", import.meta.url), + "utf8", + ), + ) as { configSchema: Record }; + expect( + validateJsonSchemaValue({ + schema: manifest.configSchema, + cacheKey: "session-share.disabled-config", + value: {}, + }).ok, + ).toBe(true); + for (const command of fixture.commands) { + expect(command.isAvailable?.({ config: fixture.config, env: {} })).toBe(false); + } + expect((await fixture.list()).sessions).toEqual([]); + await expect(fixture.read("agent:main:unshared")).rejects.toThrow("not shared"); + }); + }); }); describe("session-share receiver identity integration", () => { - it.each(["alpha", "beta"])( - "keeps %s claims remote by default and applies only explicit owner and numeric GitHub links", - async (nodeId) => { - await withCatalogFixture(async (fixture) => { - const profile = syncGitHubIdentity({ - identity: { accountId: 4242, login: "catalog-person", name: "Catalog Person" }, - authenticationAlias: { kind: "github-login", login: "catalog-person" }, - }); - fixture.list.mockResolvedValue({ nodes: [{ nodeId, connected: true, commands }] }); - const hostId = `node:${nodeId}`; - const namespacedIdentity = { ...remoteIdentity, domain: hostId }; - const identityReads = vi.spyOn(githubIdentities, "selectStoredGitHubIdentities"); - const fullIdentityScans = () => - identityReads.mock.calls.filter(([, profileIds]) => profileIds === undefined).length; - const human = { - ...nativeSession, - createdActor: { - type: "human" as const, - id: "4242", - identity: remoteIdentity, - label: "Remote Person", - }, - }; - const agent = { - ...nativeSession, - threadId: "agent:main:agent", - createdActor: { type: "agent" as const, id: "assistant", label: "Assistant" }, - }; - const unmatched = { - ...human, - threadId: "agent:main:unmatched", - createdActor: { - ...human.createdActor, - id: "9999", - identity: { ...remoteIdentity, id: "9999" }, - }, - }; - const transcript = { - threadId: nativeSession.threadId, - items: [remoteIdentity, remoteIdentity, { ...remoteIdentity, id: "9999" }].map( - (identity) => ({ - type: "userMessage", - text: "Question", - sender: { identity, label: "Remote Person" }, - }), - ), - }; - fixture.invoke.mockImplementation(async ({ command }) => - command === commands[0] ? { sessions: [human, agent, unmatched] } : transcript, - ); - const namespacedHuman = { - ...human, - createdActor: { ...human.createdActor, identity: namespacedIdentity }, - }; - const namespacedUnmatched = { - ...unmatched, - createdActor: { - ...unmatched.createdActor, - identity: { ...namespacedIdentity, id: "9999" }, - }, - }; - expect((await fixture.catalog.list({}))[0]?.sessions).toEqual([ - namespacedHuman, - agent, - namespacedUnmatched, - ]); - expect( - (await fixture.catalog.read({ hostId, threadId: nativeSession.threadId })).items[0] - ?.sender?.identity, - ).toEqual(namespacedIdentity); - expect(fullIdentityScans()).toBe(0); - for (const owner of [`profile:${profile.id}`, "github:CATALOG-PERSON"]) { - fixture.setConfig({ - plugins: { - entries: { "session-share": { config: { nodes: { [nodeId]: { owner } } } } }, - }, - }); - const rows = (await fixture.catalog.list({}))[0]!.sessions; - expect(rows[0]).toEqual(namespacedHuman); - expect(rows[1]?.createdActor).toMatchObject({ - type: "human", - id: profile.id, - identity: { type: "profile", id: profile.id }, - label: "Catalog Person", - }); - expect(rows[2]).toEqual(namespacedUnmatched); - } + it("keeps claims remote by default and applies only explicit owner and numeric GitHub links", async () => { + const nodeId = "alpha"; + await withCatalogFixture(async (fixture) => { + const profile = syncGitHubIdentity({ + identity: { accountId: 4242, login: "catalog-person", name: "Catalog Person" }, + authenticationAlias: { kind: "github-login", login: "catalog-person" }, + }); + fixture.list.mockResolvedValue({ nodes: [{ nodeId, connected: true, commands }] }); + const hostId = `node:${nodeId}`; + const namespacedIdentity = { ...remoteIdentity, domain: hostId }; + const identityReads = vi.spyOn(githubIdentities, "selectStoredGitHubIdentities"); + const fullIdentityScans = () => + identityReads.mock.calls.filter(([, profileIds]) => profileIds === undefined).length; + const human = { + ...nativeSession, + createdActor: { + type: "human" as const, + id: "4242", + identity: remoteIdentity, + label: "Remote Person", + }, + }; + const agent = { + ...nativeSession, + threadId: "agent:main:agent", + createdActor: { type: "agent" as const, id: "assistant", label: "Assistant" }, + }; + const unmatched = { + ...human, + threadId: "agent:main:unmatched", + createdActor: { + ...human.createdActor, + id: "9999", + identity: { ...remoteIdentity, id: "9999" }, + }, + }; + const transcript = { + threadId: nativeSession.threadId, + items: [remoteIdentity, remoteIdentity, { ...remoteIdentity, id: "9999" }].map( + (identity) => ({ + type: "userMessage", + text: "Question", + sender: { identity, label: "Remote Person" }, + }), + ), + }; + fixture.invoke.mockImplementation(async ({ command }) => + command === commands[0] ? { sessions: [human, agent, unmatched] } : transcript, + ); + const namespacedHuman = { + ...human, + createdActor: { ...human.createdActor, identity: namespacedIdentity }, + }; + const namespacedUnmatched = { + ...unmatched, + createdActor: { + ...unmatched.createdActor, + identity: { ...namespacedIdentity, id: "9999" }, + }, + }; + expect((await fixture.catalog.list({}))[0]?.sessions).toEqual([ + namespacedHuman, + agent, + namespacedUnmatched, + ]); + expect( + (await fixture.catalog.read({ hostId, threadId: nativeSession.threadId })).items[0]?.sender + ?.identity, + ).toEqual(namespacedIdentity); + expect(fullIdentityScans()).toBe(0); + for (const owner of [`profile:${profile.id}`, "github:CATALOG-PERSON"]) { fixture.setConfig({ plugins: { - entries: { - "session-share": { config: { nodes: { [nodeId]: { linkGitHubIdentities: true } } } }, - }, + entries: { "session-share": { config: { nodes: { [nodeId]: { owner } } } } }, }, }); - identityReads.mockClear(); - const linked = (await fixture.catalog.list({}))[0]!.sessions; - expect.soft(fullIdentityScans()).toBe(1); - expect(linked[0]?.createdActor).toMatchObject({ + const rows = (await fixture.catalog.list({}))[0]!.sessions; + expect(rows[0]).toEqual(namespacedHuman); + expect(rows[1]?.createdActor).toMatchObject({ type: "human", id: profile.id, identity: { type: "profile", id: profile.id }, label: "Catalog Person", }); - expect(linked[1]).toEqual(agent); - expect(linked[2]).toEqual(namespacedUnmatched); - identityReads.mockClear(); - const page = await fixture.catalog.read({ - hostId, - threadId: nativeSession.threadId, - }); - expect.soft(fullIdentityScans()).toBe(1); - expect(page.items.map((item) => item.sender)).toEqual([ - { identity: { type: "profile", id: profile.id }, label: "Catalog Person" }, - { identity: { type: "profile", id: profile.id }, label: "Catalog Person" }, - { identity: { ...namespacedIdentity, id: "9999" }, label: "Remote Person" }, - ]); + expect(rows[2]).toEqual(namespacedUnmatched); + } + fixture.setConfig({ + plugins: { + entries: { + "session-share": { config: { nodes: { [nodeId]: { linkGitHubIdentities: true } } } }, + }, + }, }); - }, - ); + identityReads.mockClear(); + const linked = (await fixture.catalog.list({}))[0]!.sessions; + expect.soft(fullIdentityScans()).toBe(1); + expect(linked[0]?.createdActor).toMatchObject({ + type: "human", + id: profile.id, + identity: { type: "profile", id: profile.id }, + label: "Catalog Person", + }); + expect(linked[1]).toEqual(agent); + expect(linked[2]).toEqual(namespacedUnmatched); + identityReads.mockClear(); + const page = await fixture.catalog.read({ + hostId, + threadId: nativeSession.threadId, + }); + expect.soft(fullIdentityScans()).toBe(1); + expect(page.items.map((item) => item.sender)).toEqual([ + { identity: { type: "profile", id: profile.id }, label: "Catalog Person" }, + { identity: { type: "profile", id: profile.id }, label: "Catalog Person" }, + { identity: { ...namespacedIdentity, id: "9999" }, label: "Remote Person" }, + ]); + }); + }); }); diff --git a/test/plugins/workboard-automation-authority.test.ts b/test/plugins/workboard-automation-authority.test.ts index 3a81e9c139ae..c826ad83ae31 100644 --- a/test/plugins/workboard-automation-authority.test.ts +++ b/test/plugins/workboard-automation-authority.test.ts @@ -39,13 +39,9 @@ import { createTestGatewayScheduler } from "../../src/test-utils/gateway-schedul const { makeStorePath } = createCronStoreHarness({ prefix: "workboard-nudge-" }); describe("Workboard terminal hook automation ownership", () => { - it.each( - (["agent_end", "subagent_ended"] as const).flatMap((hook) => - ([false, true] as const).map((closeCaller) => ({ hook, closeCaller })), - ), - )("enqueues after $hook with closeCaller=$closeCaller", async ({ hook, closeCaller }) => { + it.each(["agent_end", "subagent_ended"] as const)("%s survives closure", async (hook) => { const sessionKey = "agent:main:subagent:workboard-d6-authority"; - const runId = `d6-${hook}-${closeCaller}`; + const runId = `d6-${hook}`; const { storePath } = await makeStorePath(); vi.stubEnv("OPENCLAW_STATE_DIR", path.dirname(storePath)); const gatewayContext = createContext(); @@ -129,7 +125,11 @@ describe("Workboard terminal hook automation ownership", () => { start: (ctx) => service.start({ ...ctx, logger: { ...ctx.logger, warn } }), }, }); - const handle = await startPluginServices({ registry, config: {}, getCronService: () => cron }); + const handle = await startPluginServices({ + registry, + config: {}, + getCronService: () => cron, + }); const enqueue = vi.spyOn(cron, "enqueueRun"); const dispatch: GatewayRequestHandler = async ({ respond }) => respond(true, await cron.enqueueRun(job.id, "if-enabled")); @@ -203,29 +203,23 @@ describe("Workboard terminal hook automation ownership", () => { }, { childSessionKey: sessionKey, runId }, ); - if (closeCaller) { - admission.close(); - } + admission.close(); await pending; - if (closeCaller) { - await expect( - dispatchTrustedPluginGatewayMethod( - "cron.run", - { id: job.id, mode: "if-enabled" }, - { scopes: ["operator.admin"] }, - ), - ).rejects.toThrow("agent tool caller authority is no longer active"); - } + await expect( + dispatchTrustedPluginGatewayMethod( + "cron.run", + { id: job.id, mode: "if-enabled" }, + { scopes: ["operator.admin"] }, + ), + ).rejects.toThrow("agent tool caller authority is no longer active"); }), ); - if (closeCaller) { - await withGatewayToolCallerIdentity({ ...caller }, async () => { - await expect(request("cron.run", { id: job.id, mode: "if-enabled" })).rejects.toThrow( - "agent tool caller authority is no longer active", - ); - }); - } + await withGatewayToolCallerIdentity({ ...caller }, async () => { + await expect(request("cron.run", { id: job.id, mode: "if-enabled" })).rejects.toThrow( + "agent tool caller authority is no longer active", + ); + }); await expect(request("workboard.cards.list", { boardId: "planning" })).resolves.toMatchObject( { cards: [expect.objectContaining({ id: card.id, status: "review" })], diff --git a/test/scripts/ci-check-plan.test.ts b/test/scripts/ci-check-plan.test.ts index 005295798be8..db3739a86b52 100644 --- a/test/scripts/ci-check-plan.test.ts +++ b/test/scripts/ci-check-plan.test.ts @@ -45,6 +45,22 @@ const checkJobs = [ "check-test-types-hosted-core-shard", ]; +function createPlan(overrides: Partial) { + return createCiCheckPlan({ + typeGraphBoundaryOwner: "check-plan", + changedPaths: ["src/shared.ts"], + changedCoreTestPaths: null, + runnerProfile: "hybrid", + checkMatrix: { + include: [{ check_name: "check-test-types", task: "test-types", runner: "unused" }], + }, + coreTypeMatrix: { include: [1, 2, 3, 4, 5].map((stripe) => ({ stripe })) }, + lintCoreMatrix: { include: [] }, + lintExtensionMatrix: { include: [] }, + ...overrides, + }); +} + function admittedCheckRows(context: Parameters[1]) { const workflow = readCiWorkflow(); return checkJobs.flatMap((name) => { @@ -120,13 +136,12 @@ describe("CI check-plan completion count", () => { }; const coreStripes = runnerProfile === "hybrid" ? [1, 2] : [1, 2, 3, 4, 5]; const coreTypeMatrix = { include: [1, 2, 3, 4, 5].map((stripe) => ({ stripe })) }; - const plan = await createCiCheckPlan({ + const plan = await createPlan({ changedBaseRef: "a".repeat(40), extensionLintMode: mode === "full" ? "full" : "affected", preserveFullChecks: true, typeGraphBoundaryOwner: "additional-checks", changedPaths: ["package.json"], - changedCoreTestPaths: null, runnerProfile, checkMatrix, coreTypeMatrix, @@ -188,7 +203,7 @@ describe("CI check-plan completion count", () => { } }, ); - it.each(["", "check-plan", "additional-checks"] as const)( + it.each(["check-plan", "additional-checks"] as const)( "passes only an admitted parallel boundary owner without adding compiler rows (%s)", async (typeGraphBoundaryOwner) => { typeSelection.graphs = [ @@ -199,20 +214,15 @@ describe("CI check-plan completion count", () => { vi.mocked(createChangedCiTypeCheckPlan).mockClear(); try { const paths = ["extensions/example/value.ts"]; - const plan = await createCiCheckPlan({ + const plan = await createPlan({ typeGraphBoundaryOwner, changedPaths: paths, - changedCoreTestPaths: null, - runnerProfile: "hybrid", checkMatrix: { include: [ { check_name: "check-prod-types", task: "prod-types", runner: "unused" }, { check_name: "check-test-types", task: "test-types", runner: "unused" }, ], }, - coreTypeMatrix: { include: [1, 2, 3, 4, 5].map((stripe) => ({ stripe })) }, - lintCoreMatrix: { include: [] }, - lintExtensionMatrix: { include: [] }, }); expect(createChangedCiTypeCheckPlan).toHaveBeenCalledExactlyOnceWith(paths, { cwd: process.cwd(), @@ -254,18 +264,7 @@ describe("CI check-plan completion count", () => { { name: "test-root", config: "test/tsconfig/tsconfig.test.root.json" }, ]; try { - const plan = await createCiCheckPlan({ - typeGraphBoundaryOwner: "check-plan", - changedPaths: ["src/shared.ts"], - changedCoreTestPaths: null, - runnerProfile, - checkMatrix: { - include: [{ check_name: "check-test-types", task: "test-types", runner: "unused" }], - }, - coreTypeMatrix: { include: [1, 2, 3, 4, 5].map((stripe) => ({ stripe })) }, - lintCoreMatrix: { include: [] }, - lintExtensionMatrix: { include: [] }, - }); + const plan = await createPlan({ runnerProfile }); const hosted = runnerProfile !== "blacksmith"; const moved = hosted && stripes.length >= 4; expect(plan.core_type_matrix.include.map((row) => row.stripe)).toEqual( @@ -324,42 +323,6 @@ describe("CI check-plan completion count", () => { }, ); - it.each(["blacksmith", "github", "hybrid"] as const)( - "counts the actual compiler placement for %s", - async (runnerProfile) => { - const plan = await createCiCheckPlan({ - typeGraphBoundaryOwner: "check-plan", - changedPaths: ["src/shared.ts"], - changedCoreTestPaths: null, - runnerProfile, - checkMatrix: { - include: [{ check_name: "check-test-types", task: "test-types", runner: "unused" }], - }, - coreTypeMatrix: { include: [1, 2, 3, 4, 5].map((stripe) => ({ stripe })) }, - lintCoreMatrix: { include: [] }, - lintExtensionMatrix: { include: [] }, - }); - const outputs = Object.fromEntries( - Object.entries(plan).map(([name, value]) => [ - name, - typeof value === "string" ? value : JSON.stringify(value), - ]), - ); - expect(outputs.check_job_count).toBe( - String( - admittedCheckRows({ - eventName: "pull_request", - repository: "openclaw/openclaw", - runAttempt: 1, - runnerProfile, - preflightOutputs: { run_check_plan: "true", narrow_check_paths_json: "[]" }, - additionalNeeds: { "check-plan": { outputs, result: "success" } }, - }).length, - ), - ); - }, - ); - it.each(["hybrid", "github", "blacksmith"])( "preserves complete fallback chunks while reducing only hybrid rows (%s)", async (runnerProfile) => { @@ -371,14 +334,12 @@ describe("CI check-plan completion count", () => { } writeFileSync(join(cwd, "extensions/root.ts"), "export {};\n"); const shards = createExtensionOxlintShards({ cwd, platform: "linux", chunkSize: 8 }); - const plan = await createCiCheckPlan({ + const plan = await createPlan({ typeGraphBoundaryOwner: "", changedPaths: ["package.json"], - changedCoreTestPaths: null, runnerProfile, checkMatrix: { include: [{ check_name: "check-lint", task: "lint", runner: "unused" }] }, coreTypeMatrix: { include: [] }, - lintCoreMatrix: { include: [] }, lintExtensionMatrix: { include: [1, 2, 3, 4, 5, 6].map((stripe) => ({ stripe })) }, }); const rows = plan.lint_extension_matrix.include; diff --git a/test/scripts/ci-docker-seed-plan.test.ts b/test/scripts/ci-docker-seed-plan.test.ts index c354d82cda53..814ee221ebd1 100644 --- a/test/scripts/ci-docker-seed-plan.test.ts +++ b/test/scripts/ci-docker-seed-plan.test.ts @@ -22,13 +22,10 @@ it("retains every Docker seed owner in full release validation", () => { }); it.each([ - ["src/agents/context-window-guard.ts", []], - ["scripts/lib/ci-changed-node-test-plan.mts", []], ["scripts/e2e/lib/fleet-cache/assertions.mjs", ["fleet-cache"]], ["scripts/e2e/mcp-channels-seed.ts", ["mcp-channels"]], ["scripts/e2e/lib/update-channel-switch/assertions.mjs", ["update-channel-switch"]], ["src/state/openclaw-state-schema.ts", []], - ["src/state/openclaw-state-schema.test.ts", []], ] as const)("selects only Docker owner lanes for %s", (file, expected) => { expect(resolveChangedDockerSeedLanes([file])).toEqual(expected); }); diff --git a/test/scripts/ci-hourly.test.ts b/test/scripts/ci-hourly.test.ts index e58a5ba8b16d..fc0cba7ce7a1 100644 --- a/test/scripts/ci-hourly.test.ts +++ b/test/scripts/ci-hourly.test.ts @@ -27,7 +27,6 @@ function evaluate(expression: string, context: Context) { describe("hourly main CI admission", () => { it.each([ ["", false], - ["false", false], ["1", false], ["true", true], ])("opts main pushes into full CI only with %s", (ciOnPush, admitted) => { @@ -61,7 +60,7 @@ describe("hourly main CI admission", () => { } }); - it.each(["refs/heads/main", "refs/heads/release/2026.9", "refs/tags/v2026.9.5"])( + it.each(["refs/heads/main", "refs/tags/v2026.9.5"])( "preserves manual validation on %s", (ref) => { const context = { ...base, eventName: "workflow_dispatch", ref } as const; @@ -95,7 +94,7 @@ describe("hourly main CI admission", () => { } }); - it.each(["github", "hybrid", "runson", "blacksmith", ""] as const)( + it.each(["github", "hybrid", ""] as const)( "preserves automatic runner and cache policy for scheduled %s runs", (runnerBackend) => { for (const runAttempt of [1, 2]) { diff --git a/test/scripts/ci-hybrid-hosted-health.test.ts b/test/scripts/ci-hybrid-hosted-health.test.ts index b4e2d713bfe8..399d7fbec25e 100644 --- a/test/scripts/ci-hybrid-hosted-health.test.ts +++ b/test/scripts/ci-hybrid-hosted-health.test.ts @@ -85,11 +85,11 @@ describe("hybrid hosted assignment health", () => { expect(timeout).toHaveBeenCalledWith(10_000); }); - it.each([ - { status: "queued", runner_id: null, started_at: null }, - { status: "completed", runner_id: 12, started_at: at(0) }, - ])("falls back on $status assignment stalls", async (job) => { - mockActions([preflight({ completed_at: at(60) }), sentinel(job)]); + it("falls back on queued assignment stalls", async () => { + mockActions([ + preflight({ completed_at: at(60) }), + sentinel({ status: "queued", runner_id: null, started_at: null }), + ]); await expect(inspect()).resolves.toMatchObject({ healthy: false, reason: "hosted-assignment-stalled", @@ -100,7 +100,6 @@ describe("hybrid hosted assignment health", () => { it.each<[number, boolean]>([ [59, true], [60, false], - [179, false], ])("admits optional hosted checks after %s seconds: %s", async (wait, healthy) => { mockActions([ preflight({ completed_at: at(500) }), @@ -109,11 +108,6 @@ describe("hybrid hosted assignment health", () => { await expect(inspect()).resolves.toMatchObject({ healthy, maxWaitSeconds: wait }); }); - it("uses job creation when a sentinel is created after preflight", async () => { - mockActions([preflight({ completed_at: at(500) }), sentinel({ created_at: at(100) })]); - await expect(inspect()).resolves.toMatchObject({ healthy: true, maxWaitSeconds: 5 }); - }); - it.each([ [preflight({ run_attempt: 1 }), sentinel()], [preflight(), sentinel({ run_attempt: 1 })], diff --git a/test/scripts/ci-node-test-plan.commands.test.ts b/test/scripts/ci-node-test-plan.commands.test.ts index e8f9dd807f06..c6cda81a1b63 100644 --- a/test/scripts/ci-node-test-plan.commands.test.ts +++ b/test/scripts/ci-node-test-plan.commands.test.ts @@ -6,6 +6,7 @@ import { encodeNodeTestGroups } from "../../scripts/lib/ci-node-test-groups-code import { createNodeTestShardBundles, createNodeTestShards, + type CompactNodeTestShard, } from "../../scripts/lib/ci-node-test-plan.mts"; import { refitTestTimings, type CiTimingRun } from "../../scripts/lib/ci-test-timings-refit.mts"; import * as testTimings from "../../scripts/lib/ci-test-timings.mts"; @@ -22,6 +23,74 @@ const DEFAULT_NODE_TEST_RUNNER = "blacksmith-8vcpu-ubuntu-2404"; afterEach(() => vi.restoreAllMocks()); const tempDirs = useAutoCleanupTempDirTracker(afterEach); +async function observeCommandWorkers(job: CompactNodeTestShard, env: NodeJS.ProcessEnv = {}) { + const seen = new Map(); + await expect( + runShardPlans( + resolveShardPlans({ OPENCLAW_NODE_TEST_GROUPS_JSON: JSON.stringify(job.groups) }), + { + env: { + CI: "true", + RUNNER_ENVIRONMENT: "self-hosted", + OPENCLAW_VITEST_MAX_WORKERS: "8", + OPENCLAW_NODE_TEST_PLAN_CONCURRENCY: String(job.planConcurrency), + OPENCLAW_NODE_TEST_ENV_JSON: JSON.stringify(job.env ?? {}), + ...env, + }, + scratchDir: tempDirs.make("command-worker-plan-"), + runChild: async (_args, childEnv, label) => { + seen.set(label, childEnv.OPENCLAW_VITEST_MAX_WORKERS); + return 0; + }, + }, + ), + ).resolves.toBe(0); + return seen; +} + +async function withCommandInventory( + files: string[], + shards: typeof fullSuiteVitestShards, + timings: typeof testTimings.readCompactGroupTimings, + run: (planner: typeof import("../../scripts/lib/ci-node-test-plan.mts")) => Promise, +) { + vi.resetModules(); + vi.doMock("../../scripts/lib/list-test-files.mts", async (importOriginal) => ({ + ...(await importOriginal()), + listTrackedTestFiles: (root: string) => (root === "src/commands" ? files : []), + })); + vi.doMock("../vitest/vitest.test-shards.mjs", async (importOriginal) => ({ + ...(await importOriginal()), + fullSuiteVitestShards: shards, + })); + vi.doMock("../vitest/vitest.unit-fast-paths.mjs", async (importOriginal) => ({ + ...(await importOriginal()), + getUnitFastTestFiles: () => [], + getUnitFastIsolatedTestFiles: () => [], + getUnitFastTimerTestFiles: () => [], + getUnitFastTestFilesForIncludePatterns: () => [], + })); + vi.doMock("../../scripts/lib/ci-test-timings.mts", async (importOriginal) => ({ + ...(await importOriginal()), + readCompactGroupTimings: timings, + readRuntimePlacementTimings: () => [], + })); + vi.doMock("../../scripts/lib/vitest-build-prerequisites.mts", async (importOriginal) => ({ + ...(await importOriginal()), + resolveVitestPretestBuildMode: () => undefined, + })); + try { + await run(await import("../../scripts/lib/ci-node-test-plan.mts")); + } finally { + vi.doUnmock("../../scripts/lib/list-test-files.mts"); + vi.doUnmock("../vitest/vitest.test-shards.mjs"); + vi.doUnmock("../vitest/vitest.unit-fast-paths.mjs"); + vi.doUnmock("../../scripts/lib/ci-test-timings.mts"); + vi.doUnmock("../../scripts/lib/vitest-build-prerequisites.mts"); + vi.resetModules(); + } +} + describe("command CI ownership and parallel timing", () => { it.each(["hybrid", "blacksmith", "github"])( "delivers each %s command row's allocation through the shard executor", @@ -50,26 +119,9 @@ describe("command CI ownership and parallel timing", () => { job.env?.OPENCLAW_VITEST_MAX_WORKERS === undefined ? 8 : 2; - const seen = new Map(); - const plans = resolveShardPlans({ - OPENCLAW_NODE_TEST_GROUPS_JSON: JSON.stringify(job.groups), + const seen = await observeCommandWorkers(job, { + RUNNER_ENVIRONMENT: runnerBackend === "github" ? "github-hosted" : "self-hosted", }); - await expect( - runShardPlans(plans, { - env: { - CI: "true", - RUNNER_ENVIRONMENT: runnerBackend === "github" ? "github-hosted" : "self-hosted", - OPENCLAW_VITEST_MAX_WORKERS: "8", - OPENCLAW_NODE_TEST_PLAN_CONCURRENCY: String(job.planConcurrency), - OPENCLAW_NODE_TEST_ENV_JSON: JSON.stringify(job.env ?? {}), - }, - scratchDir: tempDirs.make("command-worker-plan-"), - runChild: async (_args, env, label) => { - seen.set(label, env.OPENCLAW_VITEST_MAX_WORKERS); - return 0; - }, - }), - ).resolves.toBe(0); for (const group of commands) { const expected = Math.min( jobWorkers, @@ -144,126 +196,81 @@ describe("command CI ownership and parallel timing", () => { projects: ["test/vitest/vitest.hooks.config.ts"], }; const timings: Record = { [owner]: 120, ordinary: 50 }; - vi.resetModules(); - vi.doMock("../../scripts/lib/list-test-files.mts", async (importOriginal) => ({ - ...(await importOriginal()), - listTrackedTestFiles: (root: string) => (root === "src/commands" ? files : []), - })); - vi.doMock("../vitest/vitest.test-shards.mjs", async (importOriginal) => ({ - ...(await importOriginal()), - fullSuiteVitestShards: [ - { name: "agentic", config: "fixture.config.ts", projects: [config] }, - companion, - ], - })); - vi.doMock("../vitest/vitest.unit-fast-paths.mjs", async (importOriginal) => ({ - ...(await importOriginal()), - getUnitFastTestFiles: () => [], - getUnitFastIsolatedTestFiles: () => [], - getUnitFastTimerTestFiles: () => [], - getUnitFastTestFilesForIncludePatterns: () => [], - })); - vi.doMock("../../scripts/lib/ci-test-timings.mts", async (importOriginal) => ({ - ...(await importOriginal()), - readCompactGroupTimings: () => timings, - readRuntimePlacementTimings: () => [], - })); - vi.doMock("../../scripts/lib/vitest-build-prerequisites.mts", async (importOriginal) => ({ - ...(await importOriginal< - typeof import("../../scripts/lib/vitest-build-prerequisites.mts") - >()), - resolveVitestPretestBuildMode: () => undefined, - })); - try { - const { createSelectedNodeTestShardBundles: createSelected } = - await import("../../scripts/lib/ci-node-test-plan.mts"); - const create = () => createSelected(targets, { runnerBackend: "blacksmith" })!; - const logs: CiTimingRun["logs"] = []; - const cells = [ - { workers: 2, config: "test/vitest/vitest.hooks.config.ts", seconds: 240 }, - { workers: 8, config: "test/vitest/vitest.gateway-core.config.ts", seconds: 180 }, - ].map((cell) => { - companion.projects = [cell.config]; - const jobs = create(); - expect(jobs).toHaveLength(1); - const job = jobs[0]!; - expect(job.groups).toHaveLength(1); - const group = job.groups[0]!; - expect(group.includePatterns).toEqual(targets); - expect(group.configs).toEqual([config]); - expect(group.env?.OPENCLAW_VITEST_MAX_WORKERS).toBeUndefined(); - expect(job.planConcurrency).toBe(cell.workers === 2 ? 2 : 1); - return Object.assign({}, cell, { job, group }); - }); - vi.spyOn(os, "availableParallelism").mockReturnValue(8); - vi.spyOn(os, "totalmem").mockReturnValue(31 * 1024 ** 3); - for (const { workers, job, group, seconds } of cells) { - const env = { - CI: "true", - RUNNER_ENVIRONMENT: "self-hosted", - FROZEN_TARGET: "false", - OPENCLAW_VITEST_MAX_WORKERS: String(workers), - OPENCLAW_NODE_TEST_PLAN_CONCURRENCY: String(job.planConcurrency), - OPENCLAW_NODE_TEST_ENV_JSON: JSON.stringify(job.env ?? {}), - }; - await expect( - runShardPlans( - resolveShardPlans({ OPENCLAW_NODE_TEST_GROUPS_JSON: JSON.stringify(job.groups) }), - { - env, - scratchDir: tempDirs.make("precise-command-workers-"), - runChild: async (_args, childEnv) => { - expect(childEnv.OPENCLAW_VITEST_MAX_WORKERS).toBe(String(workers)); - return 0; - }, - }, - ), - ).resolves.toBe(0); - expect.soft(group.timing_key).toContain(`#file-parallel-${workers}#selector-`); - logs.push({ - kind: "compact", - labels: ["blacksmith-32vcpu-ubuntu-2404"], - text: [ - ...Object.entries(env).map(([key, value]) => `2026-09-27T00:00:00Z ${key}: ${value}`), - `2026-09-27T00:00:00Z OPENCLAW_NODE_TEST_GROUPS_GZIP_BASE64: ${encodeNodeTestGroups(job.groups)}`, - `2026-09-27T00:00:00Z [shard:resources] logicalCpuCount=8 totalMemoryBytes=${31 * 1024 ** 3} requested plans=${job.planConcurrency} admitted plans=1`, - `2026-09-27T00:00:00Z [shard:${group.timing_key}] begin`, - `${new Date(Date.parse("2026-09-27T00:00:00Z") + seconds * 1000).toISOString()} [shard:${group.timing_key}] end (exit 0)`, - ].join("\n"), + await withCommandInventory( + files, + [{ name: "agentic", config: "fixture.config.ts", projects: [config] }, companion], + () => timings, + async ({ createSelectedNodeTestShardBundles: createSelected }) => { + const create = () => createSelected(targets, { runnerBackend: "blacksmith" })!; + const logs: CiTimingRun["logs"] = []; + const cells = [ + { workers: 2, config: "test/vitest/vitest.hooks.config.ts", seconds: 240 }, + { workers: 8, config: "test/vitest/vitest.gateway-core.config.ts", seconds: 180 }, + ].map((cell) => { + companion.projects = [cell.config]; + const jobs = create(); + expect(jobs).toHaveLength(1); + const job = jobs[0]!; + expect(job.groups).toHaveLength(1); + const group = job.groups[0]!; + expect(group.includePatterns).toEqual(targets); + expect(group.configs).toEqual([config]); + expect(group.env?.OPENCLAW_VITEST_MAX_WORKERS).toBeUndefined(); + expect(job.planConcurrency).toBe(cell.workers === 2 ? 2 : 1); + return Object.assign({}, cell, { job, group }); }); - } - expect.soft(cells[0]!.group.timing_key).not.toBe(cells[1]!.group.timing_key); - const result = refitTestTimings( - [1, 2].map((id) => ({ - id, - createdAt: `2026-09-${25 + id}T00:00:00Z`, - completeInventory: false, - pullRequestMergeRef: true, - logs, - })), - ); - expect(result.rejectedWorkerKeys.blacksmith).toEqual([]); - for (const { config: companionConfig, group, job, seconds } of cells) { - const key = group.timing_key!; - expect(result.timings.compactGroupSeconds.blacksmith[key]).toBe(seconds); - companion.projects = [companionConfig]; - timings[key] = 1; - expect(create()[0]!.predictedTestSeconds).toBe(job.predictedTestSeconds); - timings[key] = seconds; - const measured = create()[0]!; - expect(measured.predictedTestSeconds).toBe(seconds); - expect(measured.predictedSeconds).toBe(seconds); - expect(measured.groups).toEqual(job.groups); - expect(measured.planConcurrency).toBe(job.planConcurrency); - } - } finally { - vi.doUnmock("../../scripts/lib/list-test-files.mts"); - vi.doUnmock("../vitest/vitest.test-shards.mjs"); - vi.doUnmock("../vitest/vitest.unit-fast-paths.mjs"); - vi.doUnmock("../../scripts/lib/ci-test-timings.mts"); - vi.doUnmock("../../scripts/lib/vitest-build-prerequisites.mts"); - vi.resetModules(); - } + vi.spyOn(os, "availableParallelism").mockReturnValue(8); + vi.spyOn(os, "totalmem").mockReturnValue(31 * 1024 ** 3); + for (const { workers, job, group, seconds } of cells) { + const env = { + CI: "true", + RUNNER_ENVIRONMENT: "self-hosted", + FROZEN_TARGET: "false", + OPENCLAW_VITEST_MAX_WORKERS: String(workers), + OPENCLAW_NODE_TEST_PLAN_CONCURRENCY: String(job.planConcurrency), + OPENCLAW_NODE_TEST_ENV_JSON: JSON.stringify(job.env ?? {}), + }; + const seen = await observeCommandWorkers(job, env); + expect(seen.get(group.shard_name)).toBe(String(workers)); + expect.soft(group.timing_key).toContain(`#file-parallel-${workers}#selector-`); + logs.push({ + kind: "compact", + labels: ["blacksmith-32vcpu-ubuntu-2404"], + text: [ + ...Object.entries(env).map(([key, value]) => `2026-09-27T00:00:00Z ${key}: ${value}`), + `2026-09-27T00:00:00Z OPENCLAW_NODE_TEST_GROUPS_GZIP_BASE64: ${encodeNodeTestGroups(job.groups)}`, + `2026-09-27T00:00:00Z [shard:resources] logicalCpuCount=8 totalMemoryBytes=${31 * 1024 ** 3} requested plans=${job.planConcurrency} admitted plans=1`, + `2026-09-27T00:00:00Z [shard:${group.timing_key}] begin`, + `${new Date(Date.parse("2026-09-27T00:00:00Z") + seconds * 1000).toISOString()} [shard:${group.timing_key}] end (exit 0)`, + ].join("\n"), + }); + } + expect.soft(cells[0]!.group.timing_key).not.toBe(cells[1]!.group.timing_key); + const result = refitTestTimings( + [1, 2].map((id) => ({ + id, + createdAt: `2026-09-${25 + id}T00:00:00Z`, + completeInventory: false, + pullRequestMergeRef: true, + logs, + })), + ); + expect(result.rejectedWorkerKeys.blacksmith).toEqual([]); + for (const { config: companionConfig, group, job, seconds } of cells) { + const key = group.timing_key!; + expect(result.timings.compactGroupSeconds.blacksmith[key]).toBe(seconds); + companion.projects = [companionConfig]; + timings[key] = 1; + expect(create()[0]!.predictedTestSeconds).toBe(job.predictedTestSeconds); + timings[key] = seconds; + const measured = create()[0]!; + expect(measured.predictedTestSeconds).toBe(seconds); + expect(measured.predictedSeconds).toBe(seconds); + expect(measured.groups).toEqual(job.groups); + expect(measured.planConcurrency).toBe(job.planConcurrency); + } + }, + ); }); it("projects serial timings once, retaining complete history and indivisible files", async () => { @@ -276,169 +283,119 @@ describe("command CI ownership and parallel timing", () => { const legacy = { [owner]: 200, [memoryOwner]: 1000 }; let observations: ReturnType = legacy; const fixtureShards = [{ name: "agentic", config: "fixture.config.ts", projects: [config] }]; - vi.resetModules(); - vi.doMock("../../scripts/lib/list-test-files.mts", async (importOriginal) => ({ - ...(await importOriginal()), - listTrackedTestFiles: (root: string) => - root === "src/commands" ? [...files, memoryFile] : [], - })); - vi.doMock("../vitest/vitest.test-shards.mjs", async (importOriginal) => ({ - ...(await importOriginal()), - fullSuiteVitestShards: fixtureShards, - })); - vi.doMock("../vitest/vitest.unit-fast-paths.mjs", () => ({ - getUnitFastTestFiles: () => [], - getUnitFastIsolatedTestFiles: () => [], - getUnitFastTimerTestFiles: () => [], - getUnitFastTestFilesForIncludePatterns: () => [], - })); - vi.doMock("../../scripts/lib/ci-test-timings.mts", async (importOriginal) => ({ - ...(await importOriginal()), - readCompactGroupTimings: () => observations, - readRuntimePlacementTimings: () => [], - })); - vi.doMock("../../scripts/lib/vitest-build-prerequisites.mts", async (importOriginal) => ({ - ...(await importOriginal< - typeof import("../../scripts/lib/vitest-build-prerequisites.mts") - >()), - resolveVitestPretestBuildMode: () => undefined, - })); - try { - const { createNodeTestShardBundles: createPlan } = - await import("../../scripts/lib/ci-node-test-plan.mts"); - const create = () => - createPlan({ - compactMode: "pull-request", - includeReleaseOnlyPluginShards: false, - runnerBackend: "blacksmith", + await withCommandInventory( + [...files, memoryFile], + fixtureShards, + () => observations, + async ({ createNodeTestShardBundles: createPlan }) => { + const create = () => + createPlan({ + compactMode: "pull-request", + includeReleaseOnlyPluginShards: false, + runnerBackend: "blacksmith", + }); + const totalSeconds = (jobs: ReturnType) => + jobs.reduce((sum, job) => sum + job.predictedSeconds!, 0); + const projected = create(); + const projectedGroup = projected + .flatMap((job) => job.groups) + .find((group) => group.shard_name === owner)!; + expect(projectedGroup.timing_key).toBe(timingKey); + expect(projectedGroup.env?.OPENCLAW_VITEST_MAX_WORKERS).toBeUndefined(); + expect(projectedGroup.fallbackMaxWorkers).toBe(2); + const memoryJob = projected.find((job) => + job.groups.some((group) => group.shard_name === memoryOwner), + )!; + expect(memoryJob.groups).toHaveLength(1); + expect(memoryJob.predictedSeconds).toBe(1000); + expect(memoryJob.groups[0]!.includePatterns).toEqual([memoryFile]); + expect(totalSeconds(projected)).toBe(1100); + + observations = { ...legacy, [timingKey]: 200 }; + expect(totalSeconds(create())).toBe(1200); + const generation = createCompactSplitTimingGeneration({ + configs: [config], + parentShardName: owner, + stripes: files.map((file) => [file]), }); - const totalSeconds = (jobs: ReturnType) => - jobs.reduce((sum, job) => sum + job.predictedSeconds!, 0); - const projected = create(); - const projectedGroup = projected - .flatMap((job) => job.groups) - .find((group) => group.shard_name === owner)!; - expect(projectedGroup.timing_key).toBe(timingKey); - expect(projectedGroup.env?.OPENCLAW_VITEST_MAX_WORKERS).toBeUndefined(); - expect(projectedGroup.fallbackMaxWorkers).toBe(2); - const memoryJob = projected.find((job) => - job.groups.some((group) => group.shard_name === memoryOwner), - )!; - expect(memoryJob.groups).toHaveLength(1); - expect(memoryJob.predictedSeconds).toBe(1000); - expect(memoryJob.groups[0]!.includePatterns).toEqual([memoryFile]); - expect(totalSeconds(projected)).toBe(1100); - - observations = { ...legacy, [timingKey]: 200 }; - expect(totalSeconds(create())).toBe(1200); - const generation = createCompactSplitTimingGeneration({ - configs: [config], - parentShardName: owner, - stripes: files.map((file) => [file]), - }); - observations = { ...legacy, [generation.timingKeys[0]!]: 500 }; - expect(totalSeconds(create())).toBe(1100); - observations = { ...observations, [generation.timingKeys[1]!]: 500 }; - const retained = create(); - expect(totalSeconds(retained)).toBe(2000); - const retainedGroups = retained - .flatMap((job) => job.groups) - .filter((group) => group.shard_name.startsWith(`${owner}-hosted-`)); - expect(retainedGroups.flatMap((group) => group.includePatterns!).toSorted()).toEqual(files); - expect( - retainedGroups.every( - (group) => parseCompactSplitTimingKey(group.timing_key!)?.parentShardName === timingKey, - ), - ).toBe(true); - - fixtureShards.push({ - name: "agentic-gateway-server-isolated", - config: "fixture-gateway.config.ts", - projects: ["test/vitest/vitest.gateway-server-isolated.config.ts"], - }); - observations = { - [owner]: 200, - [timingKey]: 100, - [memoryOwner]: 1000, - "agentic-gateway-server-isolated": 80, - }; - const affordable = create().find((job) => - job.groups.some((group) => group.shard_name === owner), - )!; - expect( - affordable.groups.some((group) => group.shard_name === "agentic-gateway-server-isolated"), - ).toBe(true); - const priorObservations = observations; - const parallelGeneration = createCompactSplitTimingGeneration({ - configs: [config], - env: { OPENCLAW_VITEST_MAX_WORKERS: "2" }, - parentShardName: timingKey, - stripes: [files], - }); - observations = { ...observations, [parallelGeneration.timingKeys[0]!]: 200 }; - const measured = create(); - const measuredJobs = measured.filter((job) => - job.groups.some((group) => group.shard_name.startsWith(`${owner}-hosted-`)), - ); - const measuredGroups = measuredJobs.flatMap((job) => - job.groups.filter((group) => group.shard_name.startsWith(`${owner}-hosted-`)), - ); - expect(measuredGroups.flatMap((group) => group.includePatterns!).toSorted()).toEqual(files); - expect(measuredGroups).toHaveLength(2); - expect( - measuredJobs.some( - (job) => job.runner === "blacksmith-32vcpu-ubuntu-2404" && job.planConcurrency === 1, - ), - ).toBe(true); - expect(totalSeconds(measured)).toBe(1280); - vi.spyOn(os, "availableParallelism").mockReturnValue(8); - vi.spyOn(os, "totalmem").mockReturnValue(31 * 1024 ** 3); - for (const job of measuredJobs) { - const seen = new Map(); - await expect( - runShardPlans( - resolveShardPlans({ OPENCLAW_NODE_TEST_GROUPS_JSON: JSON.stringify(job.groups) }), - { - env: { - CI: "true", - RUNNER_ENVIRONMENT: "self-hosted", - OPENCLAW_VITEST_MAX_WORKERS: "8", - OPENCLAW_NODE_TEST_PLAN_CONCURRENCY: String(job.planConcurrency), - OPENCLAW_NODE_TEST_ENV_JSON: JSON.stringify(job.env ?? {}), - }, - scratchDir: tempDirs.make("measured-command-workers-"), - runChild: async (_args, env, label) => { - seen.set(label, env.OPENCLAW_VITEST_MAX_WORKERS); - return 0; - }, - }, + observations = { ...legacy, [generation.timingKeys[0]!]: 500 }; + expect(totalSeconds(create())).toBe(1100); + observations = { ...observations, [generation.timingKeys[1]!]: 500 }; + const retained = create(); + expect(totalSeconds(retained)).toBe(2000); + const retainedGroups = retained + .flatMap((job) => job.groups) + .filter((group) => group.shard_name.startsWith(`${owner}-hosted-`)); + expect(retainedGroups.flatMap((group) => group.includePatterns!).toSorted()).toEqual(files); + expect( + retainedGroups.every( + (group) => parseCompactSplitTimingKey(group.timing_key!)?.parentShardName === timingKey, ), - ).resolves.toBe(0); - for (const group of job.groups.filter((entry) => measuredGroups.includes(entry))) { - expect(seen.get(group.shard_name)).toBe("2"); - expect(group.env?.OPENCLAW_VITEST_MAX_WORKERS).toBe("2"); - expect(group.timing_key).toContain("#file-parallel-2#selector-"); + ).toBe(true); + + fixtureShards.push({ + name: "agentic-gateway-server-isolated", + config: "fixture-gateway.config.ts", + projects: ["test/vitest/vitest.gateway-server-isolated.config.ts"], + }); + observations = { + [owner]: 200, + [timingKey]: 100, + [memoryOwner]: 1000, + "agentic-gateway-server-isolated": 80, + }; + const affordable = create().find((job) => + job.groups.some((group) => group.shard_name === owner), + )!; + expect( + affordable.groups.some((group) => group.shard_name === "agentic-gateway-server-isolated"), + ).toBe(true); + const priorObservations = observations; + const parallelGeneration = createCompactSplitTimingGeneration({ + configs: [config], + env: { OPENCLAW_VITEST_MAX_WORKERS: "2" }, + parentShardName: timingKey, + stripes: [files], + }); + observations = { ...observations, [parallelGeneration.timingKeys[0]!]: 200 }; + const measured = create(); + const measuredJobs = measured.filter((job) => + job.groups.some((group) => group.shard_name.startsWith(`${owner}-hosted-`)), + ); + const measuredGroups = measuredJobs.flatMap((job) => + job.groups.filter((group) => group.shard_name.startsWith(`${owner}-hosted-`)), + ); + expect(measuredGroups.flatMap((group) => group.includePatterns!).toSorted()).toEqual(files); + expect(measuredGroups).toHaveLength(2); + expect( + measuredJobs.some( + (job) => job.runner === "blacksmith-32vcpu-ubuntu-2404" && job.planConcurrency === 1, + ), + ).toBe(true); + expect(totalSeconds(measured)).toBe(1280); + vi.spyOn(os, "availableParallelism").mockReturnValue(8); + vi.spyOn(os, "totalmem").mockReturnValue(31 * 1024 ** 3); + for (const job of measuredJobs) { + const seen = await observeCommandWorkers(job); + for (const group of job.groups.filter((entry) => measuredGroups.includes(entry))) { + expect(seen.get(group.shard_name)).toBe("2"); + expect(group.env?.OPENCLAW_VITEST_MAX_WORKERS).toBe("2"); + expect(group.timing_key).toContain("#file-parallel-2#selector-"); + } } - } - observations = priorObservations; - observations = { ...observations, [`${owner}#file-parallel-8`]: 250 }; - const admission = create(); - const commandJob = admission.find((job) => - job.groups.some((group) => group.shard_name === owner), - )!; - // Sharing the Gateway's serial 8-worker allocation would cost 250+80s. - // Reserve that worker-specific sample before deciding whether rows can share. - expect(commandJob.groups).toHaveLength(1); - expect(commandJob.groups[0]?.includePatterns?.toSorted()).toEqual(files.toSorted()); - expect(commandJob.predictedTestSeconds).toBeLessThanOrEqual(300); - } finally { - vi.doUnmock("../../scripts/lib/list-test-files.mts"); - vi.doUnmock("../vitest/vitest.test-shards.mjs"); - vi.doUnmock("../vitest/vitest.unit-fast-paths.mjs"); - vi.doUnmock("../../scripts/lib/ci-test-timings.mts"); - vi.doUnmock("../../scripts/lib/vitest-build-prerequisites.mts"); - vi.resetModules(); - } + observations = priorObservations; + observations = { ...observations, [`${owner}#file-parallel-8`]: 250 }; + const admission = create(); + const commandJob = admission.find((job) => + job.groups.some((group) => group.shard_name === owner), + )!; + // Sharing the Gateway's serial 8-worker allocation would cost 250+80s. + // Reserve that worker-specific sample before deciding whether rows can share. + expect(commandJob.groups).toHaveLength(1); + expect(commandJob.groups[0]?.includePatterns?.toSorted()).toEqual(files.toSorted()); + expect(commandJob.predictedTestSeconds).toBeLessThanOrEqual(300); + }, + ); }); it("keeps Doctor session SQLite owners complete and isolated", () => { const ownerNames = [ @@ -459,65 +416,15 @@ describe("command CI ownership and parallel timing", () => { expect(owners.get("agentic-commands-doctor-sessions-cron-memory")).toEqual([ "src/commands/doctor-session-sqlite.memory.test.ts", ]); - expect(owners.get("agentic-commands-doctor-sessions-cron-sqlite")).toEqual([ - "src/commands/doctor-session-sqlite.archive-safety.test.ts", - "src/commands/doctor-session-sqlite.compaction-recovery.test.ts", - "src/commands/doctor-session-sqlite.compaction.test.ts", - "src/commands/doctor-session-sqlite.failure-reports.test.ts", - "src/commands/doctor-session-sqlite.inspection.test.ts", - "src/commands/doctor-session-sqlite.manifests.test.ts", - "src/commands/doctor-session-sqlite.publication-recovery.test.ts", - "src/commands/doctor-session-sqlite.recovery-generations.test.ts", - "src/commands/doctor-session-sqlite.recovery-shared-owners.test.ts", - "src/commands/doctor-session-sqlite.recovery.test.ts", - "src/commands/doctor-session-sqlite.restore-history.test.ts", - "src/commands/doctor-session-sqlite.restore-paths.test.ts", - "src/commands/doctor-session-sqlite.restore-publication.test.ts", - "src/commands/doctor-session-sqlite.retirement-disposal.test.ts", - "src/commands/doctor-session-sqlite.retirement-mutations.test.ts", - "src/commands/doctor-session-sqlite.retirement-verification.test.ts", - "src/commands/doctor-session-sqlite.targets.test.ts", - "src/commands/doctor-session-sqlite.test.ts", - ]); - expect(owners.get("agentic-commands-doctor-sessions-cron-sqlite-recovery")).toEqual([ - "src/commands/doctor-session-sqlite-recovery-inventory.test.ts", - "src/commands/doctor-session-sqlite.active-settlement.test.ts", - "src/commands/doctor-session-sqlite.receipt-recovery.test.ts", - "src/commands/doctor-session-transcripts.missing-index.test.ts", - ]); - expect(owners.get("agentic-commands-doctor-sessions-cron")).toEqual([ - "src/commands/doctor-heartbeat-cadence-migration.test.ts", - "src/commands/doctor-heartbeat-scratch-migration.test.ts", - "src/commands/doctor-heartbeat-session-target.test.ts", - "src/commands/doctor-heartbeat-source-archive.test.ts", - "src/commands/doctor-heartbeat-task-migration.test.ts", - "src/commands/doctor-session-canonical-keys.memory.test.ts", - "src/commands/doctor-session-canonical-keys.retention.test.ts", - "src/commands/doctor-session-delivery-state.test.ts", - "src/commands/doctor-session-entry-state.test.ts", - "src/commands/doctor-session-exec-policy.test.ts", - "src/commands/doctor-session-incognito-key-repair.test.ts", - "src/commands/doctor-session-snapshots.test.ts", - "src/commands/doctor-session-sqlite-readers.test.ts", - "src/commands/doctor-session-sqlite.codex-binding.test.ts", - "src/commands/doctor-session-sqlite.deferred-plugin.test.ts", - "src/commands/doctor-session-sqlite.discovery.test.ts", - "src/commands/doctor-session-sqlite.held-recovery.test.ts", - "src/commands/doctor-session-sqlite.indexless.test.ts", - "src/commands/doctor-session-sqlite.receipt-retirement.test.ts", - "src/commands/doctor-session-sqlite.retained-source-verification.test.ts", - "src/commands/doctor-session-sqlite.shared-orphan.test.ts", - "src/commands/doctor-session-sqlite.shared-store.test.ts", - "src/commands/doctor-session-sqlite.source-conflict-recovery.test.ts", - "src/commands/doctor-session-state-providers.test.ts", - "src/commands/doctor-session-title-repair.test.ts", - "src/commands/doctor-session-transcript-headers.test.ts", - "src/commands/doctor-session-transcript-labels.test.ts", - "src/commands/doctor-session-transcripts.incident.test.ts", - "src/commands/doctor-session-transcripts.sqlite.test.ts", - "src/commands/doctor-session-transcripts.test.ts", - "src/commands/doctor-session-worktree-workspace.test.ts", - ]); + for (const [owner, representative] of [ + ["sqlite", "doctor-session-sqlite.test.ts"], + ["sqlite-recovery", "doctor-session-sqlite.receipt-recovery.test.ts"], + ["", "doctor-session-sqlite.shared-store.test.ts"], + ]) { + expect( + owners.get(`agentic-commands-doctor-sessions-cron${owner ? `-${owner}` : ""}`), + ).toContain(`src/commands/${representative}`); + } const commandFiles = commandShards.flatMap((shard) => shard.includePatterns ?? []).toSorted(); expect(commandFiles).toEqual(listMatchedTestFiles(createCommandsVitestConfig({}))); expect(new Set(commandFiles).size).toBe(commandFiles.length); diff --git a/test/scripts/crabbox-staging-location.test.ts b/test/scripts/crabbox-staging-location.test.ts index 31affa3f3634..c5bf50c90b94 100644 --- a/test/scripts/crabbox-staging-location.test.ts +++ b/test/scripts/crabbox-staging-location.test.ts @@ -86,52 +86,32 @@ process.stdout.write(JSON.stringify({candidate:{files:files.length},topFiles:fil return { root, repository, home, env, git, prepare }; } -it.each([false, true])( - "runs repo-local staging without recovery records (ignored=%s)", - (ignored) => { - const f = fixture(ignored ? "stages/\n" : ""); - const index = f.git(f.repository, "ls-files", "--stage"); - const syncRoot = join(f.repository, "stages"); - const capsule = f.prepare(syncRoot); - try { - expect(capsule.staging.recorded).toBe(false); - capsule.staging.admitted(); - capsule.staging.settled(); - const artifacts = preserveCrabboxArtifacts(capsule.directory, f.repository); - if (!artifacts) { - throw new Error("Fixture expected disposable source artifact evidence"); - } - capsule.staging.preserved(artifacts); - expect(readdirSync(capsule.staging.root)).toEqual(["payload"]); - expect(readdirSync(capsule.staging.payload)).toEqual(["source"]); - expect( - f.git(capsule.directory, "ls-tree", "-r", "--name-only", capsule.tree).split("\n"), - ).toEqual([".gitignore", "source.txt"]); - expect(readFileSync(join(capsule.directory, "source.txt"), "utf8")).toBe("original source\n"); - expect(f.git(f.repository, "ls-files", "--stage")).toBe(index); - } finally { - capsule.cleanup(); - } - expect(readdirSync(syncRoot)).toEqual([]); - expect(readFileSync(join(f.repository, "source.txt"), "utf8")).toBe("original source\n"); - }, -); - -it("registers a real capsule outside its source and Git workspace", () => { +it("runs repo-local staging without recovery records", () => { const f = fixture(); - const capsule = f.prepare(join(f.root, "external-staging")); + const index = f.git(f.repository, "ls-files", "--stage"); + const syncRoot = join(f.repository, "stages"); + const capsule = f.prepare(syncRoot); try { - expect(capsule.staging.recorded).toBe(true); - expect( - JSON.parse(readFileSync(join(capsule.staging.root, "staging.json"), "utf8")), - ).toMatchObject({ state: "prepared", users: "none", repository: f.repository }); - expect(existsSync(join(capsule.staging.root, "manifest.json"))).toBe(true); + expect(capsule.staging.recorded).toBe(false); + capsule.staging.admitted(); + capsule.staging.settled(); + const artifacts = preserveCrabboxArtifacts(capsule.directory, f.repository); + if (!artifacts) { + throw new Error("Fixture expected disposable source artifact evidence"); + } + capsule.staging.preserved(artifacts); + expect(readdirSync(capsule.staging.root)).toEqual(["payload"]); + expect(readdirSync(capsule.staging.payload)).toEqual(["source"]); expect( f.git(capsule.directory, "ls-tree", "-r", "--name-only", capsule.tree).split("\n"), ).toEqual([".gitignore", "source.txt"]); + expect(readFileSync(join(capsule.directory, "source.txt"), "utf8")).toBe("original source\n"); + expect(f.git(f.repository, "ls-files", "--stage")).toBe(index); } finally { capsule.cleanup(); } + expect(readdirSync(syncRoot)).toEqual([]); + expect(readFileSync(join(f.repository, "source.txt"), "utf8")).toBe("original source\n"); }); it("reevaluates registration when staging roots and ignore rules change", () => { diff --git a/test/scripts/cross-os-release-agent-turn.test.ts b/test/scripts/cross-os-release-agent-turn.test.ts index 1732dd1a01dd..6184edc132a3 100644 --- a/test/scripts/cross-os-release-agent-turn.test.ts +++ b/test/scripts/cross-os-release-agent-turn.test.ts @@ -24,7 +24,7 @@ vi.mock("../../scripts/lib/cross-os-release-checks/process.ts", async (importOri runCommandInvocation: command.run, })); -describe.each(["packaged", "installed"] as const)("%s release agent turn", (adapter) => { +describe("release agent turn", () => { let dir: string; let logPath: string; const env = { RELEASE_TEST: "1" }; @@ -40,7 +40,7 @@ describe.each(["packaged", "installed"] as const)("%s release agent turn", (adap rmSync(dir, { recursive: true, force: true }); }); - function run() { + function run(adapter: "packaged" | "installed" = "packaged") { const params = { env, label: "probe", logPath }; return adapter === "installed" ? runInstalledAgentTurn({ ...params, cliPath: join(dir, "openclaw"), cwd: dir }) @@ -59,7 +59,10 @@ describe.each(["packaged", "installed"] as const)("%s release agent turn", (adap }); } - function expectInvocation() { + it.each(["packaged", "installed"] as const)("uses the exact %s invocation", async (adapter) => { + command.run.mockResolvedValue(success); + await expect(run(adapter)).resolves.toBe(success); + expect(command.run).toHaveBeenCalledTimes(1); const [invocation, options] = command.run.mock.calls[0]!; const args = invocation.args.slice(adapter === "packaged" ? 1 : 0); expect(args).toEqual([ @@ -99,14 +102,6 @@ describe.each(["packaged", "installed"] as const)("%s release agent turn", (adap check: true, }); expect(options.env).toBe(env); - return args[4]; - } - - it("returns the command result with the exact agent invocation", async () => { - command.run.mockResolvedValue(success); - await expect(run()).resolves.toBe(success); - expect(command.run).toHaveBeenCalledTimes(1); - expectInvocation(); }); it("rejects stale OK output without another command", async () => { @@ -133,8 +128,6 @@ describe.each(["packaged", "installed"] as const)("%s release agent turn", (adap it.each([ "document-extract: failed to install bundled runtime deps", "HTTP 503: upstream connect error", - "gateway request timeout for agent after 210000ms", - "The model did not produce a response before the model idle timeout.", ])("preserves the first failure without another command: %s", async (message) => { const error = new Error(message); command.run.mockRejectedValueOnce(error).mockResolvedValueOnce(success); diff --git a/test/scripts/cross-os-release-model-config.test.ts b/test/scripts/cross-os-release-model-config.test.ts index 00acdb05cfe6..f0e712251ce5 100644 --- a/test/scripts/cross-os-release-model-config.test.ts +++ b/test/scripts/cross-os-release-model-config.test.ts @@ -33,7 +33,7 @@ function createProvider(overrides: Partial = {}): ProviderConfig }; } -describe.each(["packaged", "installed"] as const)("%s release model config", (adapter) => { +describe("release model config", () => { const root = join("fixture", "release"); const env = { RELEASE_TEST: "1" }; const logPath = join(root, "config.log"); @@ -43,7 +43,7 @@ describe.each(["packaged", "installed"] as const)("%s release model config", (ad command.mockReset().mockResolvedValue(success); }); - function run(providerConfig: ProviderConfig) { + function run(providerConfig: ProviderConfig, adapter: "packaged" | "installed" = "packaged") { const params = { env, logPath, providerConfig }; return adapter === "installed" ? runInstalledModelsSet({ ...params, cliPath: join(root, "openclaw"), cwd: root }) @@ -62,15 +62,16 @@ describe.each(["packaged", "installed"] as const)("%s release model config", (ad }); } - function calledArgs() { + function calledArgs(adapter: "packaged" | "installed" = "packaged") { return command.mock.calls.map(([invocation]) => invocation.args.slice(adapter === "packaged" ? 1 : 0), ); } it.each([ - { extensionId: "anthropic", overrides: {}, expectedOverride: null }, + { adapter: "packaged", extensionId: "anthropic", overrides: {}, expectedOverride: null }, { + adapter: "installed", extensionId: "openai", overrides: { baseUrl: "https://example.com/v1", timeoutSeconds: 600 }, expectedOverride: { @@ -81,20 +82,22 @@ describe.each(["packaged", "installed"] as const)("%s release model config", (ad }, }, { + adapter: "packaged", extensionId: "minimax", overrides: { baseUrl: "https://example.com/v1" }, expectedOverride: { baseUrl: "https://example.com/v1", models: [] }, }, { + adapter: "packaged", extensionId: "browser", overrides: { timeoutSeconds: 700 }, expectedOverride: { models: [], timeoutSeconds: 700 }, }, - ])( - "keeps the exact ordered $extensionId commands", - async ({ extensionId, overrides, expectedOverride }) => { - await run(createProvider({ extensionId, ...overrides })); - expect(calledArgs()).toEqual([ + ] as const)( + "keeps the exact ordered $extensionId commands through $adapter", + async ({ adapter, extensionId, overrides, expectedOverride }) => { + await run(createProvider({ extensionId, ...overrides }), adapter); + expect(calledArgs(adapter)).toEqual([ ["models", "set", "test/model"], ...(expectedOverride ? [ @@ -146,42 +149,4 @@ describe.each(["packaged", "installed"] as const)("%s release model config", (ad expect(calledArgs()).toEqual([["models", "set", "test/model"]]); expect(baseUrl).not.toHaveBeenCalled(); }); - - it("builds later commands after each preceding command completes", async () => { - const providerConfig = createProvider({ extensionId: "openai" }); - command.mockImplementation(async () => { - if (command.mock.calls.length === 1) { - providerConfig.baseUrl = "https://example.com/updated"; - providerConfig.timeoutSeconds = 900; - } else if (command.mock.calls.length === 2) { - providerConfig.extensionId = "browser"; - } - return success; - }); - - await run(providerConfig); - expect(calledArgs().slice(0, 3)).toEqual([ - ["models", "set", "test/model"], - [ - "config", - "set", - "models.providers.openai", - JSON.stringify({ - baseUrl: "https://example.com/updated", - agentRuntime: { id: "openclaw" }, - models: [], - timeoutSeconds: 900, - }), - "--strict-json", - "--merge", - ], - [ - "config", - "set", - "plugins.allow", - JSON.stringify(["browser", "acpx", "bonjour", "device-pair", "talk-voice"]), - "--strict-json", - ], - ]); - }); }); diff --git a/test/scripts/desktop-resize-proof.test.ts b/test/scripts/desktop-resize-proof.test.ts index c112827694a5..59cc00ff96b8 100644 --- a/test/scripts/desktop-resize-proof.test.ts +++ b/test/scripts/desktop-resize-proof.test.ts @@ -350,17 +350,7 @@ describe("desktop proof identity and public evidence", () => { omitted: 0, }); expect(JSON.stringify(value)).not.toMatch(/private|sourceKey|streamId|ownerEpoch|stderr/u); - await writeFile(file, Buffer.alloc(1024 * 1024, 32)); - expect(await readDesktopProofGatewayCloses(file)).toEqual({ - observerCloses: { events: [], omitted: 0 }, - sshTunnelExits: { events: [], omitted: 0 }, - }); - await writeFile(file, Buffer.alloc(1024 * 1024 + 1)); - expect(await readDesktopProofGatewayCloses(file)).toBeNull(); expect(await readDesktopProofGatewayCloses(file + ".missing")).toBeNull(); - const link = file + ".link"; - await symlink(file, link); - expect(await readDesktopProofGatewayCloses(link)).toBeNull(); }); it("keeps the tap off a port claimed before its listener binds", async () => { @@ -493,7 +483,7 @@ describe("desktop proof identity and public evidence", () => { ).toMatchObject({ endpointCloses: null, rfbLifecycle: null, gatewayCloses: null }); }); - it("retains node close categories from the existing JSON file logger", async () => { + it("retains the last eight node closes from a 1 MiB JSON file logger output", async () => { const file = path.join(dirs.make("desktop-node-log-"), "node.log"); const loggerUrl = resolveRuntimeWorkerUrl(toolingNativeRuntimeEntrypoints.logger); const subsystemUrl = resolveRuntimeWorkerUrl(toolingNativeRuntimeEntrypoints.subsystemLogger); @@ -528,6 +518,7 @@ describe("desktop proof identity and public evidence", () => { stdio: "pipe", }, ); + await appendFile(file, Buffer.alloc(1024 * 1024 - (await stat(file)).size, 32)); const closes = await readDesktopProofNodeStreamCloses(file); expect(closes).toEqual( Array.from({ length: 8 }, (_, index) => ({ @@ -551,24 +542,6 @@ describe("desktop proof identity and public evidence", () => { expect(await readDesktopProofNodeStreamCloses(link)).toBeNull(); }); - it("accepts exactly 1 MiB of node diagnostics and retains the last eight closes", async () => { - const file = path.join(dirs.make("desktop-node-log-limit-"), "node.log"); - const records = Array.from({ length: 10 }, (_, index) => - JSON.stringify({ - "0": '{"subsystem":"node-host/stream"}', - "1": { streamKind: "desktop", trigger: "target-close", closeCode: 1000 + index }, - "2": "node stream closed", - }), - ).join("\n"); - await writeFile(file, records.padEnd(1024 * 1024, " ")); - expect(await readDesktopProofNodeStreamCloses(file)).toEqual( - Array.from({ length: 8 }, (_, index) => ({ - trigger: "target-close", - closeCode: 1002 + index, - })), - ); - }); - it("bounds a node log that grows after admission and closes the read handle", async () => { const file = path.join(dirs.make("desktop-node-log-growth-"), "node.log"); await writeFile(file, "{}\n"); @@ -665,37 +638,6 @@ describe("desktop proof identity and public evidence", () => { expect(desktopProofSshdFailure(stderr)).not.toMatch(/private|runtime|user$/u); }); - it("preserves the sshd command failure and private log write when projection fails", async () => { - const child = new Error("sshd-config failed"); - const projection = new Error("projection failed"); - const recorded: unknown[] = []; - const record = (error: unknown) => { - recorded.push(error); - }; - let privateLogSaved = false; - const failure = await withDesktopProofCleanup( - async () => { - throw child; - }, - () => - withDesktopProofCleanup( - async () => { - expect(recorded[0]).toBe(child); - throw projection; - }, - async () => { - privateLogSaved = true; - }, - record, - ), - record, - ).catch((error: unknown) => error); - expect(failure).toBeInstanceOf(AggregateError); - expect((failure as AggregateError).errors[0]).toBe(child); - expect((failure as AggregateError).errors[1].errors[0]).toBe(projection); - expect(privateLogSaved).toBe(true); - }); - it("publishes fixed phases and known failure locations, not raw reporter content", () => { const result = desktopProofTestReport( rawTestReport( @@ -750,22 +692,7 @@ describe("desktop proof identity and public evidence", () => { label: "missing canvas", override: { canvasCount: 0, lastFramebuffer: null, snapshotFramebuffer: null }, }, - { label: "multiple canvases", override: { canvasCount: 2, snapshotFramebuffer: null } }, { label: "no closed sockets", override: { socketCloses: [] } }, - { - label: "closed reconnect", - override: { - canvasCount: 0, - snapshotFramebuffer: null, - socketCount: 3, - latestReadyState: 3, - socketCloses: [ - { socketIndex: 0, code: 1000, wasClean: true, category: "unknown" }, - { socketIndex: 1, code: 4000, wasClean: true, category: "takeover" }, - { socketIndex: 2, code: 1006, wasClean: false, category: "unknown" }, - ], - }, - }, { label: "zero framebuffer", override: { @@ -834,17 +761,13 @@ describe("desktop proof identity and public evidence", () => { { pageClosed: 0 }, { canvasCount: -1 }, { canvasCount: 10_001 }, - { socketCount: Number.NaN }, { latestReadyState: 4 }, - { socketCloses: undefined }, { socketCloses: "private-token" }, { nodeStreamCloses: "private-token" }, { nodeStreamCloses: [{ trigger: "private-token", closeCode: 1000 }] }, - { nodeStreamCloses: [{ trigger: "target-close", closeCode: 65_536 }] }, { nodeStreamCloses: Array.from({ length: 9 }, () => viewerFailure.nodeStreamCloses[0]) }, { socketCloses: Array.from({ length: 9 }, () => viewerFailure.socketCloses[0]) }, ...[ - { socketIndex: -1 }, { socketIndex: 10_000 }, { code: 65_536 }, { code: 1000.5 }, @@ -852,7 +775,6 @@ describe("desktop proof identity and public evidence", () => { { category: "control-taken:private-operator" }, ].map((event) => ({ socketCloses: [{ ...viewerFailure.socketCloses[0], ...event }] })), { expected: { width: Infinity, height: 850 } }, - { lastFramebuffer: { width: 0.5, height: 0 } }, { snapshotFramebuffer: { width: 8193, height: 0 } }, ])("rejects invalid viewer diagnostic bounds: %j", (override) => { expect(() => @@ -1122,18 +1044,15 @@ describe("desktop proof identity and public evidence", () => { }); }); - it.each(["rev-parse", "cat-file", "ls-tree", "status"])( - "clears earlier source status before a failed %s recheck", - async (command) => { - const fixture = sourceAdmissionFixture("", ["src/edited.ts"]); - await fixture.read(); - expect(fixture.receipt.sourceStatus).not.toBeNull(); - delete fixture.replies[command]; - await expect(fixture.read()).rejects.toThrow("Git command failed"); - expect(fixture.receipt.sourceStatus).toBeNull(); - expect(JSON.stringify(fixture.receipt)).not.toContain("private"); - }, - ); + it("clears earlier source status before a failed recheck", async () => { + const fixture = sourceAdmissionFixture("", ["src/edited.ts"]); + await fixture.read(); + expect(fixture.receipt.sourceStatus).not.toBeNull(); + delete fixture.replies["rev-parse"]; + await expect(fixture.read()).rejects.toThrow("Git command failed"); + expect(fixture.receipt.sourceStatus).toBeNull(); + expect(JSON.stringify(fixture.receipt)).not.toContain("private"); + }); it("bounds published source entries and counts names omitted by privacy and size limits", async () => { const tracked = Array.from({ length: 34 }, (_, index) => `src/file-${index}.ts`); @@ -1304,7 +1223,7 @@ describe("desktop proof identity and public evidence", () => { ).toEqual({ head: merge, tree, parents: [base, head] }); }); - it.each([[], [base], [base, merge], [base, head, merge]].map((parents) => ({ parents })))( + it.each([[base], [base, merge]].map((parents) => ({ parents })))( "rejects unbound actual merge parents: $parents", ({ parents }) => { expect(() => @@ -1323,10 +1242,10 @@ describe("desktop proof identity and public evidence", () => { ).toThrow(); }); - it.each(["node", "ssh"] as const)("exports only named %s facts", (carrier) => { - const safe = sanitizeDesktopResizeProof(proof(carrier), carrier); + it("exports only named SSH facts", () => { + const safe = sanitizeDesktopResizeProof(proof("ssh"), "ssh"); expect(JSON.stringify(safe)).not.toMatch(/private|hello|token|deviceId/u); - expect(safe.carrier).toBe(carrier); + expect(safe.carrier).toBe("ssh"); expect(safe.samples).toHaveLength(5); }); diff --git a/test/scripts/docker-package-identity.test.ts b/test/scripts/docker-package-identity.test.ts index f8bff9ef88ab..16a2f2171d00 100644 --- a/test/scripts/docker-package-identity.test.ts +++ b/test/scripts/docker-package-identity.test.ts @@ -127,79 +127,33 @@ esac } describe.skipIf(process.platform === "win32")("Docker package identity report", () => { - it("rejects installed manifests that do not match the package artifact", () => { - const { result } = runPackageIdentity({ - artifactVersion: "1.2.3", - bunCli: "OpenClaw 11.2.30", - bunManifest: "11.2.30", - npmCli: "OpenClaw 11.2.30", - npmManifest: "11.2.30", - pnpmCli: "OpenClaw 11.2.30", - pnpmManifest: "11.2.30", - }); + it.each(["npm", "pnpm", "bun"] as const)( + "rejects a %s manifest version that differs from the artifact", + (manager) => { + const { result } = runPackageIdentity({ + artifactVersion: "1.2.3", + [`${manager}Manifest`]: "11.2.30", + }); + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + `[${manager}] installed manifest version '11.2.30' != artifact '1.2.3'`, + ); + }, + ); - expect(result.status).not.toBe(0); - expect(result.stderr).toContain( - "[npm] installed manifest version '11.2.30' != artifact '1.2.3'", - ); - }); - - it("rejects a stale CLI version that only contains the artifact version as a substring", () => { - const { result } = runPackageIdentity({ - artifactVersion: "1.2.3", - npmCli: "OpenClaw 11.2.30 (wrong)", - }); - - expect(result.status).not.toBe(0); - expect(result.stderr).toContain("[npm] CLI output parses to '11.2.30'"); - }); - - it("rejects a pnpm manifest version that differs from the artifact", () => { - const { result } = runPackageIdentity({ - artifactVersion: "1.2.3", - pnpmManifest: "11.2.30", - }); - - expect(result.status).not.toBe(0); - expect(result.stderr).toContain( - "[pnpm] installed manifest version '11.2.30' != artifact '1.2.3'", - ); - }); - - it("rejects a pnpm CLI version that differs from the artifact", () => { - const { result } = runPackageIdentity({ - artifactVersion: "1.2.3", - pnpmCli: "OpenClaw 11.2.30 (wrong)", - }); - - expect(result.status).not.toBe(0); - expect(result.stderr).toContain("[pnpm] CLI output parses to '11.2.30'"); - }); - - it("rejects a Bun manifest version that differs from the artifact", () => { - const { result } = runPackageIdentity({ - artifactVersion: "1.2.3", - bunManifest: "11.2.30", - }); - - expect(result.status).not.toBe(0); - expect(result.stderr).toContain( - "[bun] installed manifest version '11.2.30' != artifact '1.2.3'", - ); - }); - - it("rejects a Bun CLI version that differs from the artifact", () => { - const { result } = runPackageIdentity({ - artifactVersion: "1.2.3", - bunCli: "OpenClaw 11.2.30 (wrong)", - }); - - expect(result.status).not.toBe(0); - expect(result.stderr).toContain("[bun] CLI output parses to '11.2.30'"); - }); + it.each(["npm", "pnpm", "bun"] as const)( + "rejects a stale %s CLI version containing the artifact version as a substring", + (manager) => { + const { result } = runPackageIdentity({ + artifactVersion: "1.2.3", + [`${manager}Cli`]: "OpenClaw 11.2.30 (wrong)", + }); + expect(result.status).not.toBe(0); + expect(result.stderr).toContain(`[${manager}] CLI output parses to '11.2.30'`); + }, + ); it.each([ - { version: "2026.6.21-beta.1+build.7", nativeContract: "required" }, { version: "2026.6.21-beta.1+build.7", nativeContract: "not-applicable" }, { version: "1.2.3-beta-rc.1+build.7", nativeContract: "required" }, ] as const)( @@ -208,43 +162,31 @@ describe.skipIf(process.platform === "win32")("Docker package identity report", const { identity, result } = runPackageIdentity({ artifactVersion: version, nativeContract }); expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); - expect(identity).toEqual( - expect.objectContaining({ - package: expect.objectContaining({ version }), - containers: expect.arrayContaining([ - expect.objectContaining({ - role: "musl", - details: expect.objectContaining({ - fsSafeNative: nativeContract === "required" ? "passed" : "not-applicable", - }), + expect(identity).toMatchObject({ + package: { version }, + containers: expect.arrayContaining([ + expect.objectContaining({ + role: "musl", + details: expect.objectContaining({ + fsSafeNative: nativeContract === "required" ? "passed" : "not-applicable", }), + }), + ...[ + ["npm", "/usr/local/lib/node_modules/openclaw"], + ["pnpm", "/fake/pnpm/openclaw"], + ["bun", "/fake/bun/openclaw"], + ].map(([role, installedPackageRoot]) => expect.objectContaining({ - role: "npm", + role, details: expect.objectContaining({ - installedPackageRoot: "/usr/local/lib/node_modules/openclaw", + installedPackageRoot, installedPackageVersion: version, parsedOpenclawVersion: version, }), }), - expect.objectContaining({ - role: "pnpm", - details: expect.objectContaining({ - installedPackageRoot: "/fake/pnpm/openclaw", - installedPackageVersion: version, - parsedOpenclawVersion: version, - }), - }), - expect.objectContaining({ - role: "bun", - details: expect.objectContaining({ - installedPackageRoot: "/fake/bun/openclaw", - installedPackageVersion: version, - parsedOpenclawVersion: version, - }), - }), - ]), - }), - ); + ), + ]), + }); }, ); }); diff --git a/test/scripts/docker-setup.test-support.ts b/test/scripts/docker-setup.test-support.ts index 05f16bd2e8c1..a831220ace5d 100644 --- a/test/scripts/docker-setup.test-support.ts +++ b/test/scripts/docker-setup.test-support.ts @@ -153,13 +153,7 @@ exec "$@" } export async function expectMissingPath(path: string): Promise { - try { - await stat(path); - } catch (error) { - expect((error as NodeJS.ErrnoException).code).toBe("ENOENT"); - return; - } - throw new Error(`Expected missing path: ${path}`); + await expect(stat(path)).rejects.toMatchObject({ code: "ENOENT" }); } export async function createDockerSetupSandbox(): Promise { @@ -173,34 +167,17 @@ export async function createDockerSetupSandbox(): Promise { await mkdir(join(rootDir, "scripts", "docker"), { recursive: true }); await mkdir(join(rootDir, "scripts", "lib"), { recursive: true }); await copyFile(join(repoRoot, "scripts", "docker", "setup.sh"), scriptPath); - await copyFile( - join(repoRoot, "scripts", "lib", "docker-build.sh"), - join(rootDir, "scripts", "lib", "docker-build.sh"), - ); - await copyFile( - join(repoRoot, "scripts", "lib", "build-metadata.sh"), - join(rootDir, "scripts", "lib", "build-metadata.sh"), - ); - await copyFile( - join(repoRoot, "scripts", "lib", "docker-e2e-logs.sh"), - join(rootDir, "scripts", "lib", "docker-e2e-logs.sh"), - ); - await copyFile( - join(repoRoot, "scripts", "lib", "docker-e2e-container.sh"), - join(rootDir, "scripts", "lib", "docker-e2e-container.sh"), - ); - await copyFile( - join(repoRoot, "scripts", "lib", "docker-e2e-watchdog.mjs"), - join(rootDir, "scripts", "lib", "docker-e2e-watchdog.mjs"), - ); - await copyFile( - join(repoRoot, "scripts", "lib", "docker-e2e-resource-diagnostics.sh"), - join(rootDir, "scripts", "lib", "docker-e2e-resource-diagnostics.sh"), - ); - await copyFile( - join(repoRoot, "scripts", "lib", "host-timeout.sh"), - join(rootDir, "scripts", "lib", "host-timeout.sh"), - ); + for (const file of [ + "docker-build.sh", + "build-metadata.sh", + "docker-e2e-logs.sh", + "docker-e2e-container.sh", + "docker-e2e-watchdog.mjs", + "docker-e2e-resource-diagnostics.sh", + "host-timeout.sh", + ]) { + await copyFile(join(repoRoot, "scripts", "lib", file), join(rootDir, "scripts", "lib", file)); + } await chmod(scriptPath, 0o755); await writeFile(dockerfilePath, "FROM scratch\n"); await writeFile( @@ -225,13 +202,6 @@ export const noFollowOwnershipRepair = (root: string) => `/usr/bin/find -P ${root} -xdev -execdir /usr/bin/chown -h node:node {} +`; export const prestartSafePath = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; -export function requireSandbox(sandbox: DockerSetupSandbox | null): DockerSetupSandbox { - if (!sandbox) { - throw new Error("sandbox missing"); - } - return sandbox; -} - export async function resetDockerLog(sandbox: DockerSetupSandbox) { await writeFile(sandbox.logPath, ""); } @@ -241,26 +211,14 @@ export async function readDockerLog(sandbox: DockerSetupSandbox) { } export async function readDockerLogLines(sandbox: DockerSetupSandbox) { - const lines: string[] = []; - for (const line of (await readDockerLog(sandbox)).split("\n")) { - if (line) { - lines.push(line); - } - } - return lines; + return (await readDockerLog(sandbox)).split("\n").filter(Boolean); } export function collectMatchingLines( lines: string[], predicate: (line: string) => boolean, ): string[] { - const matches: string[] = []; - for (const line of lines) { - if (predicate(line)) { - matches.push(line); - } - } - return matches; + return lines.filter(predicate); } export function isGatewayStartLine(line: string) { diff --git a/test/scripts/docker-setup.test.ts b/test/scripts/docker-setup.test.ts index cd7c2ea70e75..2b96b17c3f0a 100644 --- a/test/scripts/docker-setup.test.ts +++ b/test/scripts/docker-setup.test.ts @@ -18,7 +18,6 @@ import { readDockerLog, readDockerLogLines, repoRoot, - requireSandbox, resetDockerLog, resolveBashForCompatCheck, setupDockerSetupSandboxRoot, @@ -88,7 +87,7 @@ async function runDockerSetupWithUnsetGatewayToken( } describe("scripts/docker/setup.sh", () => { - let sandbox: DockerSetupSandbox | null = null; + let sandbox: DockerSetupSandbox; beforeAll(async () => { await setupDockerSetupSandboxRoot(); @@ -102,22 +101,21 @@ describe("scripts/docker/setup.sh", () => { } await rm(sandbox.rootDir, { recursive: true, force: true }); await cleanupDockerSetupSandboxRoot(); - sandbox = null; }); it("handles env defaults, home-volume mounts, and Docker build args", async () => { - const activeSandbox = requireSandbox(sandbox); const buildCommit = "0123456789abcdef0123456789abcdef01234567"; - const result = runDockerSetup(activeSandbox, { + const result = runDockerSetup(sandbox, { GIT_COMMIT: buildCommit, OPENCLAW_DOCKER_APT_PACKAGES: "curl wget", OPENCLAW_EXTRA_MOUNTS: undefined, OPENCLAW_HOME_VOLUME: "openclaw-home", }); expect(result.status).toBe(0); - const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8"); + const envFile = await readFile(join(sandbox.rootDir, ".env"), "utf8"); expect(envFile).toContain("OPENCLAW_IMAGE_APT_PACKAGES=curl wget"); + expect(envFile).not.toContain("OPENCLAW_DOCKER_APT_PACKAGES"); expect(envFile).toContain("OPENCLAW_DOCKER_BUILD_NODE_OPTIONS=--max-old-space-size=8192"); expect(envFile).toContain("OPENCLAW_DOCKER_BUILD_TSDOWN_MAX_OLD_SPACE_MB="); expect(envFile).toContain("OPENCLAW_DOCKER_BUILD_SKIP_DTS=1"); @@ -125,20 +123,18 @@ describe("scripts/docker/setup.sh", () => { expect(envFile).toContain("OPENCLAW_HOME_VOLUME=openclaw-home"); // pragma: allowlist secret expect(envFile).toContain("OPENCLAW_DISABLE_BONJOUR="); expect(envFile).toContain( - `OPENCLAW_AUTH_PROFILE_SECRET_DIR=${join(activeSandbox.rootDir, "auth-profile-secrets")}`, - ); - const extraCompose = await readFile( - join(activeSandbox.rootDir, "docker-compose.extra.yml"), - "utf8", + `OPENCLAW_AUTH_PROFILE_SECRET_DIR=${join(sandbox.rootDir, "auth-profile-secrets")}`, ); + const extraCompose = await readFile(join(sandbox.rootDir, "docker-compose.extra.yml"), "utf8"); expect(extraCompose).toContain("openclaw-home:/home/node"); expect(extraCompose).toContain( - `${join(activeSandbox.rootDir, "auth-profile-secrets")}:/home/node/.config/openclaw`, + `${join(sandbox.rootDir, "auth-profile-secrets")}:/home/node/.config/openclaw`, ); expect(extraCompose).toContain("volumes:"); expect(extraCompose).toContain("openclaw-home:"); - const log = await readDockerLog(activeSandbox); + const log = await readDockerLog(sandbox); expect(log).toContain("--build-arg OPENCLAW_IMAGE_APT_PACKAGES=curl wget"); + expect(log).not.toContain("--build-arg OPENCLAW_DOCKER_APT_PACKAGES"); expect(log).toContain( "--build-arg OPENCLAW_DOCKER_BUILD_NODE_OPTIONS=--max-old-space-size=8192", ); @@ -171,14 +167,13 @@ describe("scripts/docker/setup.sh", () => { it.each([undefined, "[]"])( "keeps inherited origins out of Docker setup writes (%j)", async (allowedOrigins) => { - const activeSandbox = requireSandbox(sandbox); - await resetDockerLog(activeSandbox); - const result = runDockerSetup(activeSandbox, { + await resetDockerLog(sandbox); + const result = runDockerSetup(sandbox, { DOCKER_STUB_CONTROL_UI_ORIGINS: allowedOrigins, DOCKER_STUB_PUBLIC_ORIGIN: "https://team.example.com", }); expect(result.status).toBe(0); - const writes = (await readDockerLogLines(activeSandbox)).filter((line) => + const writes = (await readDockerLogLines(sandbox)).filter((line) => line.includes("config set --batch-json"), ); expect(writes).toHaveLength(1); @@ -194,15 +189,14 @@ describe("scripts/docker/setup.sh", () => { ); it("allows ordinary spaces in host persistence paths and quotes generated mounts", async () => { - const activeSandbox = requireSandbox(sandbox); - await resetDockerLog(activeSandbox); - const configDir = join(activeSandbox.rootDir, "config with spaces"); - const workspaceDir = join(activeSandbox.rootDir, "workspace with spaces"); - const authProfileSecretDir = join(activeSandbox.rootDir, "auth secrets with spaces"); - const homeVolumeDir = join(activeSandbox.rootDir, "home volume with spaces"); - const extraMountSource = join(activeSandbox.rootDir, "extra data"); + await resetDockerLog(sandbox); + const configDir = join(sandbox.rootDir, "config with spaces"); + const workspaceDir = join(sandbox.rootDir, "workspace with spaces"); + const authProfileSecretDir = join(sandbox.rootDir, "auth secrets with spaces"); + const homeVolumeDir = join(sandbox.rootDir, "home volume with spaces"); + const extraMountSource = join(sandbox.rootDir, "extra data"); - const result = runDockerSetup(activeSandbox, { + const result = runDockerSetup(sandbox, { OPENCLAW_CONFIG_DIR: configDir, OPENCLAW_WORKSPACE_DIR: workspaceDir, OPENCLAW_AUTH_PROFILE_SECRET_DIR: authProfileSecretDir, @@ -212,15 +206,12 @@ describe("scripts/docker/setup.sh", () => { expect(result.status).toBe(0); expect(result.stderr).not.toContain("cannot contain whitespace"); - const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8"); + const envFile = await readFile(join(sandbox.rootDir, ".env"), "utf8"); expect(envFile).toContain(`OPENCLAW_CONFIG_DIR=${configDir}`); expect(envFile).toContain(`OPENCLAW_WORKSPACE_DIR=${workspaceDir}`); expect(envFile).toContain(`OPENCLAW_AUTH_PROFILE_SECRET_DIR=${authProfileSecretDir}`); - const extraCompose = await readFile( - join(activeSandbox.rootDir, "docker-compose.extra.yml"), - "utf8", - ); + const extraCompose = await readFile(join(sandbox.rootDir, "docker-compose.extra.yml"), "utf8"); expect(extraCompose).toContain(`"${homeVolumeDir}:/home/node"`); expect(extraCompose).toContain(`"${configDir}:/home/node/.openclaw"`); expect(extraCompose).toContain(`"${workspaceDir}:/home/node/.openclaw/workspace"`); @@ -229,29 +220,26 @@ describe("scripts/docker/setup.sh", () => { }); it("persists explicit Docker Bonjour opt-in overrides", async () => { - const activeSandbox = requireSandbox(sandbox); - - const result = runDockerSetup(activeSandbox, { + const result = runDockerSetup(sandbox, { OPENCLAW_DISABLE_BONJOUR: "0", }); expect(result.status).toBe(0); - const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8"); + const envFile = await readFile(join(sandbox.rootDir, ".env"), "utf8"); expect(envFile).toContain("OPENCLAW_DISABLE_BONJOUR=0"); }); it("persists and forwards signal-specific OTLP protocol overrides", async () => { - const activeSandbox = requireSandbox(sandbox); const protocolEnv = { OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: "http/protobuf", OTEL_EXPORTER_OTLP_METRICS_PROTOCOL: "http/protobuf", OTEL_EXPORTER_OTLP_LOGS_PROTOCOL: "http/protobuf", }; - const result = runDockerSetup(activeSandbox, protocolEnv); + const result = runDockerSetup(sandbox, protocolEnv); expect(result.status).toBe(0); - const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8"); + const envFile = await readFile(join(sandbox.rootDir, ".env"), "utf8"); const compose = await readFile(join(repoRoot, "docker-compose.yml"), "utf8"); for (const [key, value] of Object.entries(protocolEnv)) { expect(envFile).toContain(`${key}=${value}`); @@ -259,69 +247,47 @@ describe("scripts/docker/setup.sh", () => { } }); - it("normalizes legacy OPENCLAW_DOCKER_APT_PACKAGES into OPENCLAW_IMAGE_APT_PACKAGES", async () => { - const activeSandbox = requireSandbox(sandbox); - await resetDockerLog(activeSandbox); - - const result = runDockerSetup(activeSandbox, { - OPENCLAW_DOCKER_APT_PACKAGES: "curl wget", - }); - expect(result.status).toBe(0); - - const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8"); - expect(envFile).toContain("OPENCLAW_IMAGE_APT_PACKAGES=curl wget"); - expect(envFile).not.toContain("OPENCLAW_DOCKER_APT_PACKAGES"); - - const log = await readDockerLog(activeSandbox); - expect(log).toContain("--build-arg OPENCLAW_IMAGE_APT_PACKAGES=curl wget"); - expect(log).not.toContain("--build-arg OPENCLAW_DOCKER_APT_PACKAGES"); - }); - it("prefers OPENCLAW_IMAGE_APT_PACKAGES over legacy OPENCLAW_DOCKER_APT_PACKAGES", async () => { - const activeSandbox = requireSandbox(sandbox); - await resetDockerLog(activeSandbox); + await resetDockerLog(sandbox); - const result = runDockerSetup(activeSandbox, { + const result = runDockerSetup(sandbox, { OPENCLAW_IMAGE_APT_PACKAGES: "curl wget httpie", OPENCLAW_DOCKER_APT_PACKAGES: "curl wget", }); expect(result.status).toBe(0); - const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8"); + const envFile = await readFile(join(sandbox.rootDir, ".env"), "utf8"); expect(envFile).toContain("OPENCLAW_IMAGE_APT_PACKAGES=curl wget httpie"); expect(envFile).not.toContain("OPENCLAW_DOCKER_APT_PACKAGES"); - const log = await readDockerLog(activeSandbox); + const log = await readDockerLog(sandbox); expect(log).toContain("--build-arg OPENCLAW_IMAGE_APT_PACKAGES=curl wget httpie"); expect(log).not.toMatch(/--build-arg OPENCLAW_IMAGE_APT_PACKAGES=curl wget(?! httpie)/); }); it("explicitly empty OPENCLAW_IMAGE_APT_PACKAGES suppresses legacy fallback", async () => { - const activeSandbox = requireSandbox(sandbox); - await resetDockerLog(activeSandbox); + await resetDockerLog(sandbox); - const result = runDockerSetup(activeSandbox, { + const result = runDockerSetup(sandbox, { OPENCLAW_IMAGE_APT_PACKAGES: "", OPENCLAW_DOCKER_APT_PACKAGES: "curl wget", }); expect(result.status).toBe(0); - const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8"); + const envFile = await readFile(join(sandbox.rootDir, ".env"), "utf8"); expect(envFile).toContain("OPENCLAW_IMAGE_APT_PACKAGES="); expect(envFile).not.toContain("curl wget"); - const log = await readDockerLog(activeSandbox); + const log = await readDockerLog(sandbox); expect(log).not.toContain("--build-arg OPENCLAW_IMAGE_APT_PACKAGES=curl wget"); }); it("avoids shared-network openclaw-cli before the gateway is started", async () => { - const activeSandbox = requireSandbox(sandbox); - - await resetDockerLog(activeSandbox); - const result = runDockerSetup(activeSandbox); + await resetDockerLog(sandbox); + const result = runDockerSetup(sandbox); expect(result.status).toBe(0); - const lines = await readDockerLogLines(activeSandbox); + const lines = await readDockerLogLines(sandbox); const gatewayStartIdx = findGatewayStartLineIndex(lines); expect(gatewayStartIdx).toBeGreaterThanOrEqual(0); @@ -333,10 +299,8 @@ describe("scripts/docker/setup.sh", () => { }); it("pins setup-time CLI state paths inside the container", async () => { - const activeSandbox = requireSandbox(sandbox); - - await resetDockerLog(activeSandbox); - const result = runDockerSetup(activeSandbox, { + await resetDockerLog(sandbox); + const result = runDockerSetup(sandbox, { OPENCLAW_HOME: "/mnt/c/Users/Trevor", OPENCLAW_STATE_DIR: "/mnt/c/Users/Trevor/.openclaw", OPENCLAW_CONFIG_PATH: "/mnt/c/Users/Trevor/.openclaw/openclaw.json", @@ -344,7 +308,7 @@ describe("scripts/docker/setup.sh", () => { }); expect(result.status).toBe(0); - const lines = await readDockerLogLines(activeSandbox); + const lines = await readDockerLogLines(sandbox); const gatewayStartIdx = findGatewayStartLineIndex(lines); expect(gatewayStartIdx).toBeGreaterThanOrEqual(0); @@ -359,23 +323,22 @@ describe("scripts/docker/setup.sh", () => { }); it("forces BuildKit for local and sandbox docker builds", async () => { - const activeSandbox = requireSandbox(sandbox); - await mkdir(join(activeSandbox.rootDir, "scripts", "docker", "sandbox"), { recursive: true }); + await mkdir(join(sandbox.rootDir, "scripts", "docker", "sandbox"), { recursive: true }); await writeFile( - join(activeSandbox.rootDir, "scripts", "docker", "sandbox", "Dockerfile"), + join(sandbox.rootDir, "scripts", "docker", "sandbox", "Dockerfile"), "FROM scratch\n", ); - await resetDockerLog(activeSandbox); - const socketPath = join(activeSandbox.rootDir, "buildkit.sock"); + await resetDockerLog(sandbox); + const socketPath = join(sandbox.rootDir, "buildkit.sock"); await withUnixSocket(socketPath, async () => { - const result = runDockerSetup(activeSandbox, { + const result = runDockerSetup(sandbox, { OPENCLAW_SANDBOX: "1", OPENCLAW_DOCKER_SOCKET: socketPath, }); expect(result.status).toBe(0); - const buildLines = collectMatchingLines(await readDockerLogLines(activeSandbox), (line) => + const buildLines = collectMatchingLines(await readDockerLogLines(sandbox), (line) => line.startsWith("build "), ); expect(buildLines.length).toBeGreaterThanOrEqual(2); @@ -388,11 +351,10 @@ describe("scripts/docker/setup.sh", () => { }); it("offline mode reuses a preloaded local image without build or pull", async () => { - const activeSandbox = requireSandbox(sandbox); - await resetDockerLog(activeSandbox); + await resetDockerLog(sandbox); const result = runDockerSetup( - activeSandbox, + sandbox, { OPENCLAW_IMAGE: "ghcr.io/openclaw/openclaw:latest", OPENCLAW_SKIP_ONBOARDING: "1", @@ -405,7 +367,7 @@ describe("scripts/docker/setup.sh", () => { "Using preloaded Docker image: ghcr.io/openclaw/openclaw:latest", ); - const lines = await readDockerLogLines(activeSandbox); + const lines = await readDockerLogLines(sandbox); const log = lines.join("\n"); expect(log).toContain("image inspect ghcr.io/openclaw/openclaw:latest"); expect(log).not.toMatch(/^build /m); @@ -415,11 +377,10 @@ describe("scripts/docker/setup.sh", () => { }); it("offline mode fails before setup when the main image is missing", async () => { - const activeSandbox = requireSandbox(sandbox); - await resetDockerLog(activeSandbox); + await resetDockerLog(sandbox); const result = runDockerSetup( - activeSandbox, + sandbox, { OPENCLAW_IMAGE: "ghcr.io/openclaw/openclaw:offline", DOCKER_STUB_MISSING_IMAGES: "ghcr.io/openclaw/openclaw:offline", @@ -432,7 +393,7 @@ describe("scripts/docker/setup.sh", () => { "Offline Docker setup requires preloaded image ghcr.io/openclaw/openclaw:offline", ); - const log = await readDockerLog(activeSandbox); + const log = await readDockerLog(sandbox); expect(log).toContain("image inspect ghcr.io/openclaw/openclaw:offline"); expect(log).not.toMatch(/^build /m); expect(log).not.toMatch(/^pull /m); @@ -440,20 +401,19 @@ describe("scripts/docker/setup.sh", () => { }); it("offline sandbox stays disabled when its configured image is missing", async () => { - const activeSandbox = requireSandbox(sandbox); - await mkdir(join(activeSandbox.rootDir, "scripts", "docker", "sandbox"), { recursive: true }); + await mkdir(join(sandbox.rootDir, "scripts", "docker", "sandbox"), { recursive: true }); await writeFile( - join(activeSandbox.rootDir, "scripts", "docker", "sandbox", "Dockerfile"), + join(sandbox.rootDir, "scripts", "docker", "sandbox", "Dockerfile"), "FROM scratch\n", ); - await resetDockerLog(activeSandbox); - const socketPath = join(activeSandbox.rootDir, "sb.sock"); + await resetDockerLog(sandbox); + const socketPath = join(sandbox.rootDir, "sb.sock"); await withUnixSocket(socketPath, async () => { const defaultImage = "registry.example/openclaw-sandbox:approved"; const agentImage = " registry.example/openclaw-sandbox:agent "; const result = runDockerSetup( - activeSandbox, + sandbox, { OPENCLAW_SANDBOX: "1", OPENCLAW_SKIP_ONBOARDING: "1", @@ -474,7 +434,7 @@ describe("scripts/docker/setup.sh", () => { "Offline sandbox prerequisites are incomplete; sandbox configuration was not changed", ); - const lines = await readDockerLogLines(activeSandbox); + const lines = await readDockerLogLines(sandbox); const log = lines.join("\n"); expect(log).toContain("image inspect openclaw:local"); expect(log).not.toContain(`image inspect ${defaultImage}`); @@ -489,9 +449,8 @@ describe("scripts/docker/setup.sh", () => { }); it("offline sandbox validates only effective Docker and browser images", async () => { - const activeSandbox = requireSandbox(sandbox); - await resetDockerLog(activeSandbox); - const socketPath = join(activeSandbox.rootDir, "eff.sock"); + await resetDockerLog(sandbox); + const socketPath = join(sandbox.rootDir, "eff.sock"); await withUnixSocket(socketPath, async () => { const defaultImage = "registry.example/openclaw-sandbox:default"; @@ -504,7 +463,7 @@ describe("scripts/docker/setup.sh", () => { "registry.example/openclaw-sandbox-browser:denied", ]; const result = runDockerSetup( - activeSandbox, + sandbox, { OPENCLAW_SANDBOX: "1", OPENCLAW_SKIP_ONBOARDING: "1", @@ -546,7 +505,7 @@ describe("scripts/docker/setup.sh", () => { expect(result.stdout).toContain(` - ${defaultImage}`); expect(result.stdout).toContain(` - ${browserImage}`); - const lines = await readDockerLogLines(activeSandbox); + const lines = await readDockerLogLines(sandbox); const log = lines.join("\n"); expect(log).toContain(`image inspect ${defaultImage} host=unix://${socketPath}`); expect(log).toContain(`image inspect ${browserImage} host=unix://${socketPath}`); @@ -559,14 +518,13 @@ describe("scripts/docker/setup.sh", () => { }); it("offline sandbox rejects an incompatible browser image", async () => { - const activeSandbox = requireSandbox(sandbox); - await resetDockerLog(activeSandbox); - const socketPath = join(activeSandbox.rootDir, "br.sock"); + await resetDockerLog(sandbox); + const socketPath = join(sandbox.rootDir, "br.sock"); await withUnixSocket(socketPath, async () => { const browserImage = "registry.example/openclaw-sandbox-browser:stale"; const result = runDockerSetup( - activeSandbox, + sandbox, { OPENCLAW_SANDBOX: "1", OPENCLAW_SKIP_ONBOARDING: "1", @@ -588,7 +546,7 @@ describe("scripts/docker/setup.sh", () => { "Offline sandbox prerequisites are incomplete; sandbox configuration was not changed", ); - const lines = await readDockerLogLines(activeSandbox); + const lines = await readDockerLogLines(sandbox); const log = lines.join("\n"); expect(log).toContain(`image inspect ${browserImage} host=unix://${socketPath}`); expect(log).not.toContain("config set agents.defaults.sandbox.mode off"); @@ -598,11 +556,10 @@ describe("scripts/docker/setup.sh", () => { }); it("precreates config identity dir for CLI device auth writes", async () => { - const activeSandbox = requireSandbox(sandbox); - const configDir = join(activeSandbox.rootDir, "config-identity"); - const workspaceDir = join(activeSandbox.rootDir, "workspace-identity"); + const configDir = join(sandbox.rootDir, "config-identity"); + const workspaceDir = join(sandbox.rootDir, "workspace-identity"); - const result = runDockerSetup(activeSandbox, { + const result = runDockerSetup(sandbox, { OPENCLAW_CONFIG_DIR: configDir, OPENCLAW_WORKSPACE_DIR: workspaceDir, }); @@ -613,20 +570,17 @@ describe("scripts/docker/setup.sh", () => { }); it("writes OPENCLAW_TZ into .env when given a real IANA timezone", async () => { - const activeSandbox = requireSandbox(sandbox); - - const result = runDockerSetup(activeSandbox, { + const result = runDockerSetup(sandbox, { OPENCLAW_TZ: "Asia/Shanghai", }); expect(result.status).toBe(0); - const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8"); + const envFile = await readFile(join(sandbox.rootDir, ".env"), "utf8"); expect(envFile).toContain("OPENCLAW_TZ=Asia/Shanghai"); }); it("precreates agent data dirs to avoid EACCES in container", async () => { - const activeSandbox = requireSandbox(sandbox); - const configDir = join(activeSandbox.rootDir, "config-agent-dirs"); + const configDir = join(sandbox.rootDir, "config-agent-dirs"); const workspaceDir = join(configDir, "workspace"); const stateFiles = [ "identity/owned.txt", @@ -641,7 +595,7 @@ describe("scripts/docker/setup.sh", () => { } expect((await stat(workspaceDir)).dev).toBe((await stat(configDir)).dev); - const result = runDockerSetup(activeSandbox, { + const result = runDockerSetup(sandbox, { OPENCLAW_CONFIG_DIR: configDir, OPENCLAW_WORKSPACE_DIR: workspaceDir, }); @@ -653,7 +607,7 @@ describe("scripts/docker/setup.sh", () => { expect(sessionsDirStat.isDirectory()).toBe(true); // Verify that a root-user chown step runs before setup. - const log = await readDockerLog(activeSandbox); + const log = await readDockerLog(sandbox); const chownIdx = log.indexOf("--user root"); const safePathIdx = log.indexOf(`${prestartSafePath}; export PATH`); const stateRepair = log.match(/\/usr\/bin\/find -P \/home\/node\/\.openclaw [^;]+/u)?.[0]; @@ -701,12 +655,11 @@ describe("scripts/docker/setup.sh", () => { }); it("precreates auth profile secret key dir outside the mounted state dir", async () => { - const activeSandbox = requireSandbox(sandbox); - const configDir = join(activeSandbox.rootDir, "config-auth-profile-key"); - const workspaceDir = join(activeSandbox.rootDir, "workspace-auth-profile-key"); - const secretDir = join(activeSandbox.rootDir, "auth-profile-secret-key"); + const configDir = join(sandbox.rootDir, "config-auth-profile-key"); + const workspaceDir = join(sandbox.rootDir, "workspace-auth-profile-key"); + const secretDir = join(sandbox.rootDir, "auth-profile-secret-key"); - const result = runDockerSetup(activeSandbox, { + const result = runDockerSetup(sandbox, { OPENCLAW_CONFIG_DIR: configDir, OPENCLAW_WORKSPACE_DIR: workspaceDir, OPENCLAW_AUTH_PROFILE_SECRET_DIR: secretDir, @@ -717,14 +670,13 @@ describe("scripts/docker/setup.sh", () => { expect(secretDirStat.isDirectory()).toBe(true); expect(secretDir.startsWith(`${configDir}/`)).toBe(false); - const log = await readDockerLog(activeSandbox); + const log = await readDockerLog(sandbox); expect(log).toContain(noFollowOwnershipRepair("/home/node/.config/openclaw")); }); it("reuses existing config token when OPENCLAW_GATEWAY_TOKEN is unset", async () => { - const activeSandbox = requireSandbox(sandbox); const { result, envFile } = await runDockerSetupWithUnsetGatewayToken( - activeSandbox, + sandbox, "token-reuse", async (configDir) => { await writeFile( @@ -738,26 +690,9 @@ describe("scripts/docker/setup.sh", () => { expect(envFile).toContain("OPENCLAW_GATEWAY_TOKEN=config-token-123"); // pragma: allowlist secret }); - it("reuses existing .env token when OPENCLAW_GATEWAY_TOKEN and config token are unset", async () => { - const activeSandbox = requireSandbox(sandbox); - await writeFile( - join(activeSandbox.rootDir, ".env"), - "OPENCLAW_GATEWAY_TOKEN=dotenv-token-123\nOPENCLAW_GATEWAY_PORT=18789\n", // pragma: allowlist secret - ); - const { result, envFile } = await runDockerSetupWithUnsetGatewayToken( - activeSandbox, - "dotenv-token-reuse", - ); - - expect(result.status).toBe(0); - expect(envFile).toContain("OPENCLAW_GATEWAY_TOKEN=dotenv-token-123"); // pragma: allowlist secret - expect(result.stderr).toBe(""); - }); - it("reuses the last non-empty .env token and strips CRLF without truncating '='", async () => { - const activeSandbox = requireSandbox(sandbox); await writeFile( - join(activeSandbox.rootDir, ".env"), + join(sandbox.rootDir, ".env"), [ "OPENCLAW_GATEWAY_TOKEN=", "OPENCLAW_GATEWAY_TOKEN=first-token", @@ -765,7 +700,7 @@ describe("scripts/docker/setup.sh", () => { ].join("\n"), ); const { result, envFile } = await runDockerSetupWithUnsetGatewayToken( - activeSandbox, + sandbox, "dotenv-last-wins", ); @@ -773,37 +708,36 @@ describe("scripts/docker/setup.sh", () => { expect(envFile).toContain("OPENCLAW_GATEWAY_TOKEN=last=token=value"); // pragma: allowlist secret expect(envFile).not.toContain("OPENCLAW_GATEWAY_TOKEN=first-token"); expect(envFile).not.toContain("\r"); + expect(result.stderr).toBe(""); }); it("treats OPENCLAW_SANDBOX=0 as disabled", async () => { - const activeSandbox = requireSandbox(sandbox); - await resetDockerLog(activeSandbox); + await resetDockerLog(sandbox); - const result = runDockerSetup(activeSandbox, { + const result = runDockerSetup(sandbox, { OPENCLAW_SANDBOX: "0", }); expect(result.status).toBe(0); - const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8"); + const envFile = await readFile(join(sandbox.rootDir, ".env"), "utf8"); expect(envFile).toContain("OPENCLAW_SANDBOX="); - const log = await readDockerLog(activeSandbox); + const log = await readDockerLog(sandbox); expect(log).toContain("--build-arg OPENCLAW_INSTALL_DOCKER_CLI="); expect(log).not.toContain("--build-arg OPENCLAW_INSTALL_DOCKER_CLI=1"); expect(log).toContain("config set agents.defaults.sandbox.mode off"); }); it("resets stale sandbox mode and overlay when sandbox is not active", async () => { - const activeSandbox = requireSandbox(sandbox); - await resetDockerLog(activeSandbox); + await resetDockerLog(sandbox); await writeFile( - join(activeSandbox.rootDir, "docker-compose.sandbox.yml"), + join(sandbox.rootDir, "docker-compose.sandbox.yml"), "services:\n openclaw-gateway:\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n", ); - const socketPath = join(activeSandbox.rootDir, "missing-cli.sock"); + const socketPath = join(sandbox.rootDir, "missing-cli.sock"); await withUnixSocket(socketPath, async () => { - const result = runDockerSetup(activeSandbox, { + const result = runDockerSetup(sandbox, { OPENCLAW_SANDBOX: "1", OPENCLAW_DOCKER_SOCKET: socketPath, DOCKER_STUB_FAIL_MATCH: "--entrypoint docker openclaw-gateway --version", @@ -811,20 +745,19 @@ describe("scripts/docker/setup.sh", () => { expect(result.status).toBe(0); expect(result.stderr).toContain("Sandbox requires Docker CLI"); - const log = await readDockerLog(activeSandbox); + const log = await readDockerLog(sandbox); expect(log).toContain("config set agents.defaults.sandbox.mode off"); - await expectMissingPath(join(activeSandbox.rootDir, "docker-compose.sandbox.yml")); + await expectMissingPath(join(sandbox.rootDir, "docker-compose.sandbox.yml")); }); }); it("keeps offline policy when sandbox config writes fail and the gateway rolls back", async () => { - const activeSandbox = requireSandbox(sandbox); - await resetDockerLog(activeSandbox); - const socketPath = join(activeSandbox.rootDir, "sandbox.sock"); + await resetDockerLog(sandbox); + const socketPath = join(sandbox.rootDir, "sandbox.sock"); await withUnixSocket(socketPath, async () => { const result = runDockerSetup( - activeSandbox, + sandbox, { OPENCLAW_SANDBOX: "1", OPENCLAW_DOCKER_SOCKET: socketPath, @@ -837,7 +770,7 @@ describe("scripts/docker/setup.sh", () => { expect(result.stderr).toContain("Failed to set agents.defaults.sandbox.scope"); expect(result.stderr).toContain("Skipping gateway restart to avoid exposing Docker socket"); - const lines = await readDockerLogLines(activeSandbox); + const lines = await readDockerLogLines(sandbox); const log = lines.join("\n"); const gatewayStarts = collectMatchingLines(lines, (line) => isGatewayStartLine(line)); expect(gatewayStarts).toHaveLength(2); @@ -849,94 +782,63 @@ describe("scripts/docker/setup.sh", () => { .split("\n") .find((line) => line.includes("--force-recreate openclaw-gateway")); expect(forceRecreateLine).toBe( - `compose compose -f ${join(activeSandbox.rootDir, "docker-compose.yml")} up -d --pull never --no-build --force-recreate openclaw-gateway`, + `compose compose -f ${join(sandbox.rootDir, "docker-compose.yml")} up -d --pull never --no-build --force-recreate openclaw-gateway`, ); expect(forceRecreateLine).not.toContain("docker-compose.sandbox.yml"); expect(log).toContain( `image inspect openclaw-sandbox:bookworm-slim host=unix://${socketPath}`, ); expectOfflineComposePolicy(lines); - await expectMissingPath(join(activeSandbox.rootDir, "docker-compose.sandbox.yml")); + await expectMissingPath(join(sandbox.rootDir, "docker-compose.sandbox.yml")); }); }); - it("rejects injected multiline OPENCLAW_EXTRA_MOUNTS values", () => { - const activeSandbox = requireSandbox(sandbox); - - const result = runDockerSetup(activeSandbox, { - OPENCLAW_EXTRA_MOUNTS: "/tmp:/tmp\n evil-service:\n image: alpine", - }); - + it.each([ + [ + "OPENCLAW_EXTRA_MOUNTS", + "/tmp:/tmp\n evil-service:\n image: alpine", + "OPENCLAW_EXTRA_MOUNTS cannot contain control characters", + ], + ["OPENCLAW_EXTRA_MOUNTS", "bad mount spec", "Invalid mount format"], + ["OPENCLAW_HOME_VOLUME", "bad name", "OPENCLAW_HOME_VOLUME must match"], + ["OPENCLAW_TZ", "Nope/Bad", "OPENCLAW_TZ must be supported by openclaw:local"], + ])("rejects invalid %s=%j", (key, value, diagnostic) => { + const result = runDockerSetup(sandbox, { [key]: value }); expect(result.status).not.toBe(0); - expect(result.stderr).toContain("OPENCLAW_EXTRA_MOUNTS cannot contain control characters"); - }); - - it("rejects invalid OPENCLAW_EXTRA_MOUNTS mount format", () => { - const activeSandbox = requireSandbox(sandbox); - - const result = runDockerSetup(activeSandbox, { - OPENCLAW_EXTRA_MOUNTS: "bad mount spec", - }); - - expect(result.status).not.toBe(0); - expect(result.stderr).toContain("Invalid mount format"); - }); - - it("rejects invalid OPENCLAW_HOME_VOLUME names", () => { - const activeSandbox = requireSandbox(sandbox); - - const result = runDockerSetup(activeSandbox, { - OPENCLAW_HOME_VOLUME: "bad name", - }); - - expect(result.status).not.toBe(0); - expect(result.stderr).toContain("OPENCLAW_HOME_VOLUME must match"); - }); - - it("rejects OPENCLAW_TZ values that are unsupported by the runtime image", () => { - const activeSandbox = requireSandbox(sandbox); - - const result = runDockerSetup(activeSandbox, { - OPENCLAW_TZ: "Nope/Bad", - }); - - expect(result.status).not.toBe(0); - expect(result.stderr).toContain("OPENCLAW_TZ must be supported by openclaw:local"); + expect(result.stderr).toContain(diagnostic); }); it("skips onboarding when OPENCLAW_SKIP_ONBOARDING is set", async () => { - const activeSandbox = requireSandbox(sandbox); - await resetDockerLog(activeSandbox); + await resetDockerLog(sandbox); - const result = runDockerSetup(activeSandbox, { + const result = runDockerSetup(sandbox, { OPENCLAW_SKIP_ONBOARDING: "1", }); expect(result.status).toBe(0); - const log = await readDockerLog(activeSandbox); + const log = await readDockerLog(sandbox); expect(log).not.toContain("onboard"); // Gateway defaults (config set) and control UI allowlist should still run. expect(log).toContain("config set --batch-json"); expect(log).toContain('"path":"gateway.mode","value":"local"'); expect(log).toContain('"path":"gateway.bind","value":"lan"'); - const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8"); + const envFile = await readFile(join(sandbox.rootDir, ".env"), "utf8"); expect(envFile).toContain("OPENCLAW_SKIP_ONBOARDING=1"); }); it("treats OPENCLAW_SKIP_ONBOARDING=0 as disabled and runs onboarding", async () => { - const activeSandbox = requireSandbox(sandbox); - await resetDockerLog(activeSandbox); + await resetDockerLog(sandbox); - const result = runDockerSetup(activeSandbox, { + const result = runDockerSetup(sandbox, { OPENCLAW_SKIP_ONBOARDING: "0", }); expect(result.status).toBe(0); - const log = await readDockerLog(activeSandbox); + const log = await readDockerLog(sandbox); expect(log).toContain( "onboard --mode local --no-install-daemon --gateway-auth token --gateway-token-ref-env OPENCLAW_GATEWAY_TOKEN --skip-ui --suppress-gateway-token-output", ); - const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8"); + const envFile = await readFile(join(sandbox.rootDir, ".env"), "utf8"); expect(envFile).toMatch(/OPENCLAW_SKIP_ONBOARDING=\n/); }); @@ -1043,16 +945,11 @@ describe("scripts/docker/setup.sh", () => { }); it("Dockerfile ARG OPENCLAW_IMAGE_APT_PACKAGES must not have a default value", async () => { - // If the ARG has a default (e.g. ARG OPENCLAW_IMAGE_APT_PACKAGES=""), Docker treats it as - // "set" even when no --build-arg is passed. That breaks the RUN fallback expression - // ${OPENCLAW_IMAGE_APT_PACKAGES-$OPENCLAW_DOCKER_APT_PACKAGES} because the variable is - // never truly unset, so legacy-only callers using --build-arg OPENCLAW_DOCKER_APT_PACKAGES - // get nothing installed — a backward-compat regression. + // A default makes the ARG set, suppressing the legacy build-arg fallback. const dockerfile = await readFile(join(repoRoot, "Dockerfile"), "utf8"); const argLine = dockerfile .split("\n") .find((line) => line.startsWith("ARG OPENCLAW_IMAGE_APT_PACKAGES")); - // Must be bare `ARG OPENCLAW_IMAGE_APT_PACKAGES` with no default assignment expect(argLine).toBe("ARG OPENCLAW_IMAGE_APT_PACKAGES"); }); }); diff --git a/test/scripts/docs-component-literals.test.ts b/test/scripts/docs-component-literals.test.ts index 97dcdf15b30f..cd4efd2c7528 100644 --- a/test/scripts/docs-component-literals.test.ts +++ b/test/scripts/docs-component-literals.test.ts @@ -7,12 +7,6 @@ import { describe("docs component literal attributes", () => { it.each([ - [ - "Accordion", - "accordionOpen", - "Registered recall tools return `status=policy-disabled`", - "registered-recall-tools-return-status-policy-disabled", - ], [ "Expandable", "accordionOpen", @@ -21,7 +15,6 @@ describe("docs component literal attributes", () => { ], ["Step", "stepOpen", "Run `openclaw status`", "run-%60openclaw-status%60"], ["Tab", "tabOpen", "Use `default`", "use-%60default%60"], - ["Card", "cardOpen", "Inspect `config`", undefined], ["Tooltip", "tooltipOpen", "The `config` value", undefined], ])("preserves inline code in %s attributes before publishing", (name, kind, title, id) => { const document = parseDocsDocument(`<${name} title="${title}">Body.`); diff --git a/test/scripts/docs-markdown.test.ts b/test/scripts/docs-markdown.test.ts index c30bd8c77cbf..4d5c0f43604a 100644 --- a/test/scripts/docs-markdown.test.ts +++ b/test/scripts/docs-markdown.test.ts @@ -100,6 +100,9 @@ describe("docs Markdown rendering", () => { expect(html).not.toContain("hidden"); expect(html).not.toContain("
"); + if (prefix) { + expect(html).toContain(prefix.trimEnd()); + } expect(document.ids).toContain("param-live"); expect(document.links).toEqual(["/visible"]); }, @@ -119,21 +122,6 @@ describe("docs Markdown rendering", () => { expect(document.links).toEqual([{ href: "/visible", line: 5 }]); }); - it("does not inherit a quote from an earlier raw literal", () => { - const literal = "
\n> raw literal quote\n
"; - const md = createDocsMarkdown(); - const document = parseDocsDocument( - `${literal}\n{/*\n> hidden comment quote\n*/}\n\n[Visible](/visible)`, - md, - ); - const html = md.renderer.render(document.tokens, md.options, document.env); - - expect(html).toContain(literal); - expect(html).not.toContain("
"); - expect(html).not.toContain("hidden"); - expect(document.links).toEqual(["/visible"]); - }); - it("preserves JSX comment bytes inside indented code", () => { const literal = "{/*\n> literal quote\n\n literal indentation\n*/}\n"; const source = `${literal @@ -201,7 +189,7 @@ describe("docs Markdown rendering", () => { expect(document.links).toEqual(["/visible"]); }); - it.each(["pre", "code", "script", "style", "textarea"])( + it.each(["pre", "code"])( "keeps inline <%s> examples literal before a later HTML example", (tag) => { const source = [ diff --git a/test/scripts/docs-mirror-freshness.test.ts b/test/scripts/docs-mirror-freshness.test.ts index 79fc177cad99..0a1f18619841 100644 --- a/test/scripts/docs-mirror-freshness.test.ts +++ b/test/scripts/docs-mirror-freshness.test.ts @@ -24,7 +24,6 @@ describe("docs mirror freshness", () => { ] >([ ["a current mirror", newestSha, 10, true, true, 60, 0, 0], - ["only recent changes", baseSha, 10, false, true, 60, 0, 0], ["an overdue change hidden by a recent edit", baseSha, 10, true, true, 60, 0, 120], ["an overdue change with recovery already active", baseSha, 10, true, true, 60, 1, 120], ["a quiet stale source", baseSha, 90, true, true, 60, 0, 90], diff --git a/test/talk-browser-defaults.test.ts b/test/talk-browser-defaults.test.ts index b25c153758f6..c62ffe9269c6 100644 --- a/test/talk-browser-defaults.test.ts +++ b/test/talk-browser-defaults.test.ts @@ -111,7 +111,6 @@ describe("OpenAI browser Talk catalog defaults", () => { expected: "gpt-live-1", }, { label: "explicit GA", model: "gpt-realtime-2.1", camera: true, expected: "gpt-realtime-2.1" }, - { label: "explicit Live", model: "gpt-live-1", camera: false, expected: "gpt-live-1" }, { label: "Live launch over configured GA", model: "gpt-realtime-2.1", @@ -119,13 +118,6 @@ describe("OpenAI browser Talk catalog defaults", () => { camera: false, expected: "gpt-live-1", }, - { - label: "GA launch over configured Live", - model: "gpt-live-1", - launchModel: "gpt-realtime-2.1", - camera: true, - expected: "gpt-realtime-2.1", - }, { label: "GA launch through a provider alias", model: "gpt-live-1", diff --git a/test/vitest-credential-redaction.test.ts b/test/vitest-credential-redaction.test.ts index 0e7a5e0c936a..d2cdbec8a548 100644 --- a/test/vitest-credential-redaction.test.ts +++ b/test/vitest-credential-redaction.test.ts @@ -1,42 +1,24 @@ -import assert from "node:assert/strict"; import { describe, expect, it } from "vitest"; import { redactCredentialText, redactDiagnostic } from "./vitest/credential-redaction.ts"; describe("public test diagnostic redaction", () => { - it.each([ - "EXAMPLE_TOKEN", - "secret", - "Password", - "PASSWD", - "API_KEY", - "apiKey", - "PRIVATE_KEY", - "AUTHORIZATION", - "COOKIE", - "SESSION", - "BLACKSMITH_STICKYDISK_TOKEN", - "BLACKSMITH_CACHE_TOKEN", - "BLACKSMITH_MONITORING_TOKEN", - "BLACKSMITH_JOB_COMPLETION_TOKEN", - "ACTIONS_RUNTIME_TOKEN", - "ACTIONS_ID_TOKEN_REQUEST_TOKEN", - "GITHUB_TOKEN", - "GH_TOKEN", - "NPM_TOKEN", - ])("preserves %s while hiding its value in object, JSON and env text", (key) => { - const cases: [string, string][] = [ - [`${key}: 'synthetic'`, `${key}: ''`], - [`${key}: "synthetic"`, `${key}: ""`], - [`"${key}": "synthetic"`, `"${key}": ""`], - [`${key}=synthetic\nNORMAL=visible`, `${key}=\nNORMAL=visible`], - [`["${key}", "synthetic"]`, `["${key}", ""]`], - [`[ '${key}', 'synthetic' ]`, `[ '${key}', '' ]`], - ]; - for (const [input, expected] of cases) { - expect(redactCredentialText(input)).toBe(expected); - expect(redactCredentialText(expected)).toBe(expected); - } - }); + it.each(["EXAMPLE_TOKEN", "PASSWD"])( + "preserves %s while hiding its value in object, JSON and env text", + (key) => { + const cases: [string, string][] = [ + [`${key}: 'synthetic'`, `${key}: ''`], + [`${key}: "synthetic"`, `${key}: ""`], + [`"${key}": "synthetic"`, `"${key}": ""`], + [`${key}=synthetic\nNORMAL=visible`, `${key}=\nNORMAL=visible`], + [`["${key}", "synthetic"]`, `["${key}", ""]`], + [`[ '${key}', 'synthetic' ]`, `[ '${key}', '' ]`], + ]; + for (const [input, expected] of cases) { + expect(redactCredentialText(input)).toBe(expected); + expect(redactCredentialText(expected)).toBe(expected); + } + }, + ); it("scrubs nested credential entry pairs while preserving keys and ordinary entries", () => { const diagnostic = { @@ -134,9 +116,6 @@ describe("public test diagnostic redaction", () => { }); it.each([ - ["AUTHORIZATION", "Bearer synthetic"], - ["COOKIE", "first=synthetic; second=synthetic"], - ["PASSWORD", "two synthetic words"], ["PASSWORD", " two synthetic words "], ["TOKEN", "synthetic NORMAL=visible"], ["TOKEN", "synthetic,second}"], @@ -164,20 +143,6 @@ describe("public test diagnostic redaction", () => { ); }); - it("redacts all multiline credential fragments in native assertion messages", () => { - const fragment = "not-a-real-secret-value-1234567890"; - const value = `-----BEGIN PRIVATE KEY-----\n${`${fragment}\n`.repeat(3)}-----END PRIVATE KEY-----`; - let message = ""; - try { - assert.deepStrictEqual({ PRIVATE_KEY: value }, {}); - } catch (error) { - message = error instanceof Error ? error.message : ""; - } - const output = redactCredentialText(message); - expect(output.includes(fragment)).toBe(false); - expect(output).toContain(``); - }); - it("scrubs every error field and nested causes without losing nonsecret diagnostics", () => { const error = Object.assign(new Error("TOKEN=synthetic"), { diff: "PASSWORD: 'synthetic'",