test(ci,gateway,integration): remove low-value tests (batch d151) (#163532)

* test(ci): deslop s1016 tests

* test(gateway): deslop s842 tests

Port s842 commits 77c589897919272a876fe407c3b567a26ee4df11 and
9cccf18820d7eb027e39ed7f22f94a99a29d647f onto the campaign lane.
The abort fixture fixes are already present on the lane.

Consolidate yield setup while preserving HEAD's eight yield/resume cases,
transactional pause, requester retirement, acknowledgement recovery, and
unknown-write reconciliation assertions. Apply the shard's dispatch and
approval replay cleanup to files unchanged since its parent.

Validation: Testbox on origin/main baa26a6ad5
plus all five resolved shard files: 5 files, 25 tests passed, no skips,
82.86s Vitest duration. No timeouts raised. oxlint, oxfmt --check,
git diff --check, and independent review passed.

* test(tooling): deslop s1009 tests

* test(integration): deslop s1007 tests

* test(helpers): deslop s1010 tests

* test(plugins): deslop s1011 tests

* test(integration): deslop s1006 tests

* test(scripts): deslop s1022 tests

* test(scripts): deslop s1021 tests

* test(plugins): deslop s1012 tests
This commit is contained in:
Peter Steinberger 2026-10-02 08:03:08 -05:00 • committed by GitHub
parent 5af3a429a4
commit 94cbbecd46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 1033 additions and 2043 deletions

View file

@ -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<string>;
send: (sessionId: string) => Promise<HeldTurn>;
release: (sessionId: string) => void;
nodes: NodeWorkerSupervisorNodeProof[];
placements: Map<string, WorkerSessionPlacementRecord>;
service: NonNullable<GatewayRequestContext["workerPlacementDispatchService"]>;
@ -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");
});
});
});

View file

@ -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);
});

View file

@ -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<typeof tool.execute> | undefined;
try {

View file

@ -307,12 +307,10 @@ describe("scheduled message actions", () => {
const providerErrors: string[] = [];
const providerWork = new Set<Promise<void>>();
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"

View file

@ -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");

View file

@ -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"],

View file

@ -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 }) => {

View file

@ -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)(

View file

@ -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());

View file

@ -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)/,

View file

@ -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++) {

View file

@ -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

View file

@ -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<ReturnType<typeof createHarness>>;
// 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<ChannelMessageActionAdapter>,
) {
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<void>,
actionOverrides?: Partial<ChannelMessageActionAdapter>,
) {
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"] },
);
});
});

View file

@ -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<unknown> {
@ -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<typeof fetch>(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 },

View file

@ -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(

View file

@ -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);

View file

@ -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"),

View file

@ -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: <T>(run: () => T | Promise<T>) => 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);

View file

@ -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(

View file

@ -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(

View file

@ -24,7 +24,8 @@ afterEach(async () => {
vi.useRealTimers();
});
function registerDreaming(config: OpenClawConfig, logger: ReturnType<typeof createNoopLogger>) {
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<typeof crea
},
}),
);
return registry;
const handle = await startPluginServices({ registry, config, getCronService });
services.add(handle);
return { handle, logger };
}
async function createScheduler(cronEnabled: boolean, owner?: string) {
@ -62,29 +65,23 @@ async function createScheduler(cronEnabled: boolean, owner?: string) {
return cron;
}
it.each([false, true])(
"does not author ownerless dreaming work when effective scheduling is disabled (config enabled=%s)",
async (configEnabled) => {
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),

View file

@ -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<void>((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);
});
});

View file

@ -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<ReturnType<typeof runThroughFailureRecovery>>): 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<ReturnType<typeof runThroughFailur
}
function expectBillingState(result: Awaited<ReturnType<typeof runThroughFailureRecovery>>): 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<ReturnType<typeof runThroughFailureRecovery>>,
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({

View file

@ -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<string, unknown> };
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<string, unknown> };
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" },
]);
});
});
});

View file

@ -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" })],

View file

@ -45,6 +45,22 @@ const checkJobs = [
"check-test-types-hosted-core-shard",
];
function createPlan(overrides: Partial<CiCheckPlanInput>) {
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<typeof evaluateWorkflowExpression>[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;

View file

@ -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);
});

View file

@ -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]) {

View file

@ -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 })],

View file

@ -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<string, string | undefined>();
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<void>,
) {
vi.resetModules();
vi.doMock("../../scripts/lib/list-test-files.mts", async (importOriginal) => ({
...(await importOriginal<typeof import("../../scripts/lib/list-test-files.mts")>()),
listTrackedTestFiles: (root: string) => (root === "src/commands" ? files : []),
}));
vi.doMock("../vitest/vitest.test-shards.mjs", async (importOriginal) => ({
...(await importOriginal<typeof import("../vitest/vitest.test-shards.mjs")>()),
fullSuiteVitestShards: shards,
}));
vi.doMock("../vitest/vitest.unit-fast-paths.mjs", async (importOriginal) => ({
...(await importOriginal<typeof import("../vitest/vitest.unit-fast-paths.mjs")>()),
getUnitFastTestFiles: () => [],
getUnitFastIsolatedTestFiles: () => [],
getUnitFastTimerTestFiles: () => [],
getUnitFastTestFilesForIncludePatterns: () => [],
}));
vi.doMock("../../scripts/lib/ci-test-timings.mts", async (importOriginal) => ({
...(await importOriginal<typeof import("../../scripts/lib/ci-test-timings.mts")>()),
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 {
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<string, string | undefined>();
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<string, number> = { [owner]: 120, ordinary: 50 };
vi.resetModules();
vi.doMock("../../scripts/lib/list-test-files.mts", async (importOriginal) => ({
...(await importOriginal<typeof import("../../scripts/lib/list-test-files.mts")>()),
listTrackedTestFiles: (root: string) => (root === "src/commands" ? files : []),
}));
vi.doMock("../vitest/vitest.test-shards.mjs", async (importOriginal) => ({
...(await importOriginal<typeof import("../vitest/vitest.test-shards.mjs")>()),
fullSuiteVitestShards: [
{ name: "agentic", config: "fixture.config.ts", projects: [config] },
companion,
],
}));
vi.doMock("../vitest/vitest.unit-fast-paths.mjs", async (importOriginal) => ({
...(await importOriginal<typeof import("../vitest/vitest.unit-fast-paths.mjs")>()),
getUnitFastTestFiles: () => [],
getUnitFastIsolatedTestFiles: () => [],
getUnitFastTimerTestFiles: () => [],
getUnitFastTestFilesForIncludePatterns: () => [],
}));
vi.doMock("../../scripts/lib/ci-test-timings.mts", async (importOriginal) => ({
...(await importOriginal<typeof import("../../scripts/lib/ci-test-timings.mts")>()),
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<typeof testTimings.readCompactGroupTimings> = 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<typeof import("../../scripts/lib/list-test-files.mts")>()),
listTrackedTestFiles: (root: string) =>
root === "src/commands" ? [...files, memoryFile] : [],
}));
vi.doMock("../vitest/vitest.test-shards.mjs", async (importOriginal) => ({
...(await importOriginal<typeof import("../vitest/vitest.test-shards.mjs")>()),
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<typeof import("../../scripts/lib/ci-test-timings.mts")>()),
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<typeof create>) =>
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<typeof create>) =>
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<string, string | undefined>();
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);

View file

@ -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", () => {

View file

@ -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);

View file

@ -33,7 +33,7 @@ function createProvider(overrides: Partial<ProviderConfig> = {}): 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",
],
]);
});
});

View file

@ -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);
});

View file

@ -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,
}),
}),
]),
}),
);
),
]),
});
},
);
});

View file

@ -153,13 +153,7 @@ exec "$@"
}
export async function expectMissingPath(path: string): Promise<void> {
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<DockerSetupSandbox> {
@ -173,34 +167,17 @@ export async function createDockerSetupSandbox(): Promise<DockerSetupSandbox> {
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) {

View file

@ -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");
});
});

View file

@ -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.</${name}>`);

View file

@ -100,6 +100,9 @@ describe("docs Markdown rendering", () => {
expect(html).not.toContain("hidden");
expect(html).not.toContain("<blockquote>");
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 = "<pre>\n> raw literal quote\n</pre>";
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("<blockquote>");
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 = [

View file

@ -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],

View file

@ -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",

View file

@ -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}: '<redacted len=9>'`],
[`${key}: "synthetic"`, `${key}: "<redacted len=9>"`],
[`"${key}": "synthetic"`, `"${key}": "<redacted len=9>"`],
[`${key}=synthetic\nNORMAL=visible`, `${key}=<redacted len=9>\nNORMAL=visible`],
[`["${key}", "synthetic"]`, `["${key}", "<redacted len=9>"]`],
[`[ '${key}', 'synthetic' ]`, `[ '${key}', '<redacted len=9>' ]`],
];
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}: '<redacted len=9>'`],
[`${key}: "synthetic"`, `${key}: "<redacted len=9>"`],
[`"${key}": "synthetic"`, `"${key}": "<redacted len=9>"`],
[`${key}=synthetic\nNORMAL=visible`, `${key}=<redacted len=9>\nNORMAL=visible`],
[`["${key}", "synthetic"]`, `["${key}", "<redacted len=9>"]`],
[`[ '${key}', 'synthetic' ]`, `[ '${key}', '<redacted len=9>' ]`],
];
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(`<redacted len=${value.length}>`);
});
it("scrubs every error field and nested causes without losing nonsecret diagnostics", () => {
const error = Object.assign(new Error("TOKEN=synthetic"), {
diff: "PASSWORD: 'synthetic'",