test(scripts,tts,ui): remove low-value tests (batch d153) (#163563)

* test(scripts): deslop s1024 tests

* test(upgrade): deslop s1039 tests

* test(scripts): deslop s1036 tests

* test(scripts): deslop s1029 tests

* test(release): deslop s1034 tests

* test(tooling): deslop s1033 tests

* test(tooling): deslop s1041 tests

* test(tts): deslop s1000 tests

* test(ui): deslop s1053 tests

* test(ui): deslop s1056 tests
This commit is contained in:
Peter Steinberger 2026-10-02 08:15:14 -07:00 • committed by GitHub
parent 64e97dac1f
commit b1c09f1088
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 1072 additions and 2213 deletions

View file

@ -63,7 +63,7 @@ function npmApprovals(approvals) {
);
}
export function releaseApprovalArtifactName({ parentRunId, parentRunAttempt }) {
function releaseApprovalArtifactName({ parentRunId, parentRunAttempt }) {
pattern(parentRunId, ID, "Parent run id");
pattern(parentRunAttempt, ID, "Parent run attempt");
return `openclaw-release-approval-v1-${parentRunId}-${parentRunAttempt}`;

View file

@ -1,6 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import { DatabaseSync } from "node:sqlite";
import { setImmediate as nextTurn } from "node:timers/promises";
import { afterAll, afterEach, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/types.js";
import { LegacyPluginSdkResourceHost } from "../plugins/legacy-sdk-resource-host.js";
@ -109,31 +110,37 @@ module.exports = { id: ${JSON.stringify(id)}, register(api) {
runtime,
state,
prepare: () => runtime.tts.prepareTtsRequest({ cfg, text }),
withEnvironment: (run: () => Promise<void>) =>
withEnvAsync(
{
OPENCLAW_HOME: dir,
OPENCLAW_STATE_DIR: dir,
OPENCLAW_CONFIG_PATH: path.join(dir, "config.json"),
OPENCLAW_TTS_PREFS: path.join(dir, "prefs.json"),
},
() => host.run(run),
),
async cleanup() {
tailResume.resolve();
await Promise.allSettled(tails);
await host.close();
clearPluginMetadataLifecycleCaches();
async runTest(run: () => Promise<void>) {
try {
const cleanup = await waitForPluginCacheRetirement();
expect(cleanup.failures).toEqual([]);
await withEnvAsync(
{
OPENCLAW_HOME: dir,
OPENCLAW_STATE_DIR: dir,
OPENCLAW_CONFIG_PATH: path.join(dir, "config.json"),
OPENCLAW_TTS_PREFS: path.join(dir, "prefs.json"),
},
() =>
host.run(async () => {
useNoBundledPlugins();
await run();
}),
);
} finally {
for (const { database } of connections) {
if (database.isOpen) {
database.close();
tailResume.resolve();
await Promise.allSettled(tails);
await host.close();
clearPluginMetadataLifecycleCaches();
try {
const cleanup = await waitForPluginCacheRetirement();
expect(cleanup.failures).toEqual([]);
} finally {
for (const { database } of connections) {
if (database.isOpen) {
database.close();
}
}
Reflect.deleteProperty(globalThis, key);
}
Reflect.deleteProperty(globalThis, key);
}
},
};
@ -148,156 +155,137 @@ afterAll(cleanupPluginLoaderFixturesForTest);
describe("async speech preparation resources", () => {
it("reuses registrations and preserves opaque overrides for synthesis after inspection retirement", async () => {
const fixture = createPreparationFixture();
try {
await fixture.withEnvironment(async () => {
useNoBundledPlugins();
const inspection = await acquirePluginRegistryForInspection({ config: fixture.cfg });
try {
const prepared = await withPluginRuntimeRegistryScope(inspection.registry, async () => {
const first = await fixture.prepare();
for (let index = 0; index < 3; index++) {
const next = await fixture.prepare();
expect(
next.directives.overrides.providerOverrides?.[fixture.id]?.voiceSettings ===
first.directives.overrides.providerOverrides?.[fixture.id]?.voiceSettings,
).toBe(true);
}
const synthesis = await fixture.runtime.tts.textToSpeechTelephony({
cfg: first.cfg,
text: first.directives.cleanedText,
overrides: first.directives.overrides,
});
expect(synthesis.success).toBe(true);
// Ordinary dispatch must restore the handle inside its exact owning plugin.
await fixture.runTest(async () => {
const inspection = await acquirePluginRegistryForInspection({ config: fixture.cfg });
try {
const prepared = await withPluginRuntimeRegistryScope(inspection.registry, async () => {
const first = await fixture.prepare();
for (let index = 0; index < 3; index++) {
const next = await fixture.prepare();
expect(
fixture.state.receivedOverrides[0] === fixture.state.connections[0]?.opaque,
next.directives.overrides.providerOverrides?.[fixture.id]?.voiceSettings ===
first.directives.overrides.providerOverrides?.[fixture.id]?.voiceSettings,
).toBe(true);
return first;
}
const synthesis = await fixture.runtime.tts.textToSpeechTelephony({
cfg: first.cfg,
text: first.directives.cleanedText,
overrides: first.directives.overrides,
});
expect(fixture.state.connections.length).toBe(1);
await inspection.release();
expect(fixture.state.connections[0]?.database.isOpen).toBe(true);
expect(
prepared.directives.overrides.providerOverrides?.[fixture.id]?.voiceSettings,
).toHaveProperty("voiceId", "voice-42");
const result = await fixture.runtime.tts.textToSpeechTelephony({
cfg: prepared.cfg,
text: prepared.directives.cleanedText,
overrides: prepared.directives.overrides,
});
expect(result.success).toBe(true);
expect(fixture.state.consumedVoices).toEqual(["voice-42", "voice-42"]);
await fixture.host.close();
expect(fixture.state.connections.every((entry) => !entry.database.isOpen)).toBe(true);
expect(fixture.state.connections.every((entry) => entry.disposals === 1)).toBe(true);
} finally {
await inspection.release();
}
});
} finally {
await fixture.cleanup();
}
expect(synthesis.success).toBe(true);
// Ordinary dispatch must restore the handle inside its exact owning plugin.
expect(fixture.state.receivedOverrides[0] === fixture.state.connections[0]?.opaque).toBe(
true,
);
return first;
});
expect(fixture.state.connections.length).toBe(1);
await inspection.release();
expect(fixture.state.connections[0]?.database.isOpen).toBe(true);
expect(
prepared.directives.overrides.providerOverrides?.[fixture.id]?.voiceSettings,
).toHaveProperty("voiceId", "voice-42");
const result = await fixture.runtime.tts.textToSpeechTelephony({
cfg: prepared.cfg,
text: prepared.directives.cleanedText,
overrides: prepared.directives.overrides,
});
expect(result.success).toBe(true);
expect(fixture.state.consumedVoices).toEqual(["voice-42", "voice-42"]);
await fixture.host.close();
expect(fixture.state.connections.every((entry) => !entry.database.isOpen)).toBe(true);
expect(fixture.state.connections.every((entry) => entry.disposals === 1)).toBe(true);
} finally {
await inspection.release();
}
});
});
it("retains the selected source before provider metadata can retire its inspection", async () => {
const fixture = createPreparationFixture();
try {
await fixture.withEnvironment(async () => {
useNoBundledPlugins();
const inspection = await acquirePluginRegistryForInspection({ config: fixture.cfg });
let retirement: Promise<void> | undefined;
fixture.state.callbacks.onMetadata = () => {
retirement ??= inspection.release();
};
try {
const prepared = await withPluginRuntimeRegistryScope(
inspection.registry,
fixture.prepare,
);
await retirement;
expect(prepared.directives.hasDirective).toBe(true);
expect(fixture.state.connections[0]?.opaque.voiceId).toBe("voice-42");
await fixture.host.close();
expect(fixture.state.connections[0]?.disposals).toBe(1);
} finally {
await inspection.release();
}
});
} finally {
await fixture.cleanup();
}
await fixture.runTest(async () => {
const inspection = await acquirePluginRegistryForInspection({ config: fixture.cfg });
let retirement: Promise<void> | undefined;
fixture.state.callbacks.onMetadata = () => {
retirement ??= inspection.release();
};
try {
const prepared = await withPluginRuntimeRegistryScope(inspection.registry, fixture.prepare);
await retirement;
expect(prepared.directives.hasDirective).toBe(true);
expect(fixture.state.connections[0]?.opaque.voiceId).toBe("voice-42");
await fixture.host.close();
expect(fixture.state.connections[0]?.disposals).toBe(1);
} finally {
await inspection.release();
}
});
});
it("owns failed projection cleanup until asynchronous disposal finishes", async () => {
const fixture = createPreparationFixture();
const disposeResume = createDeferredCore();
try {
await fixture.withEnvironment(async () => {
useNoBundledPlugins();
const inspection = await acquirePluginRegistryForInspection({ config: fixture.cfg });
let retirement: Promise<void> | undefined;
fixture.state.callbacks.onDispose = () => disposeResume.promise;
fixture.state.callbacks.onProjection = () => {
retirement ??= inspection.release();
throw new Error("speech directive projection failed");
};
try {
await expect(
withPluginRuntimeRegistryScope(inspection.registry, fixture.prepare),
).rejects.toThrow("speech directive projection failed");
let closed = false;
const closing = fixture.host.close().then(() => {
closed = true;
});
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
expect(closed).toBe(false);
disposeResume.resolve();
await closing;
expect(fixture.state.connections[0]?.disposals).toBe(1);
} finally {
disposeResume.resolve();
await retirement;
await inspection.release();
}
});
} finally {
disposeResume.resolve();
await fixture.cleanup();
}
await fixture.runTest(async () => {
const inspection = await acquirePluginRegistryForInspection({ config: fixture.cfg });
const disposeResume = createDeferredCore();
let retirement: Promise<void> | undefined;
fixture.state.callbacks.onDispose = () => disposeResume.promise;
fixture.state.callbacks.onProjection = () => {
retirement ??= inspection.release();
throw new Error("speech directive projection failed");
};
try {
await expect(
withPluginRuntimeRegistryScope(inspection.registry, fixture.prepare),
).rejects.toThrow("speech directive projection failed");
let closed = false;
const closing = fixture.host.close().then(() => {
closed = true;
});
await nextTurn();
expect(closed).toBe(false);
disposeResume.resolve();
await closing;
expect(fixture.state.connections[0]?.disposals).toBe(1);
} finally {
disposeResume.resolve();
await retirement;
await inspection.release();
}
});
});
it("keeps failed projection resources alive through its tracked directive tail", async () => {
const fixture = createPreparationFixture();
try {
await fixture.withEnvironment(async () => {
useNoBundledPlugins();
it.each([false, true])(
"joins directive tails before closing resources (projection fails: %s)",
async (fails) => {
const fixture = createPreparationFixture();
await fixture.runTest(async () => {
const inspection = await acquirePluginRegistryForInspection({ config: fixture.cfg });
let retirement: Promise<void> | undefined;
fixture.state.callbacks.trackTail = true;
fixture.state.callbacks.onProjection = () => {
retirement ??= inspection.release();
throw new Error("speech directive projection failed");
};
if (fails) {
fixture.state.callbacks.onProjection = () => {
retirement ??= inspection.release();
throw new Error("speech directive projection failed");
};
}
try {
await expect(
withPluginRuntimeRegistryScope(inspection.registry, fixture.prepare).then(
() => undefined,
),
).rejects.toThrow("speech directive projection failed");
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
expect(fixture.state.connections[0]?.database.isOpen).toBe(true);
const preparation = withPluginRuntimeRegistryScope(inspection.registry, fixture.prepare);
if (fails) {
await expect(preparation).rejects.toThrow("speech directive projection failed");
await nextTurn();
expect(fixture.state.connections[0]?.database.isOpen).toBe(true);
} else {
await preparation;
expect(fixture.state.tails.length).toBe(1);
await inspection.release();
}
let closed = false;
const closing = fixture.host.close().then(() => {
closed = true;
});
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
await nextTurn();
expect(closed).toBe(false);
expect(fixture.state.connections[0]?.database.isOpen).toBe(true);
fixture.state.tailResume.resolve();
await Promise.all(fixture.state.tails);
await closing;
@ -309,110 +297,58 @@ describe("async speech preparation resources", () => {
await inspection.release();
}
});
} finally {
await fixture.cleanup();
}
});
},
);
it("rejects projection when its SDK host closes during preparation", async () => {
const fixture = createPreparationFixture();
try {
await fixture.withEnvironment(async () => {
useNoBundledPlugins();
const inspection = await acquirePluginRegistryForInspection({ config: fixture.cfg });
let closing: Promise<void> | undefined;
fixture.state.callbacks.onProjection = () => {
closing ??= fixture.host.close();
};
try {
await expect(
withPluginRuntimeRegistryScope(inspection.registry, fixture.prepare),
).rejects.toThrow("Plugin SDK resource host is closed");
await closing;
await inspection.release();
expect(fixture.state.connections[0]?.disposals).toBe(1);
} finally {
await inspection.release();
}
});
} finally {
await fixture.cleanup();
}
});
it("joins tracked directive work before closing borrowed SQLite resources", async () => {
const fixture = createPreparationFixture();
try {
await fixture.withEnvironment(async () => {
useNoBundledPlugins();
const inspection = await acquirePluginRegistryForInspection({ config: fixture.cfg });
fixture.state.callbacks.trackTail = true;
try {
await withPluginRuntimeRegistryScope(inspection.registry, fixture.prepare);
expect(fixture.state.tails.length).toBe(1);
await inspection.release();
let closed = false;
const closing = fixture.host.close().then(() => {
closed = true;
});
await new Promise<void>((resolve) => {
setImmediate(resolve);
});
expect(closed).toBe(false);
expect(fixture.state.connections[0]?.database.isOpen).toBe(true);
fixture.state.tailResume.resolve();
await Promise.all(fixture.state.tails);
await closing;
expect(fixture.state.connections[0]?.disposals).toBe(1);
} finally {
fixture.state.tailResume.resolve();
await Promise.allSettled(fixture.state.tails);
await inspection.release();
}
});
} finally {
await fixture.cleanup();
}
await fixture.runTest(async () => {
const inspection = await acquirePluginRegistryForInspection({ config: fixture.cfg });
let closing: Promise<void> | undefined;
fixture.state.callbacks.onProjection = () => {
closing ??= fixture.host.close();
};
try {
await expect(
withPluginRuntimeRegistryScope(inspection.registry, fixture.prepare),
).rejects.toThrow("Plugin SDK resource host is closed");
await closing;
await inspection.release();
expect(fixture.state.connections[0]?.disposals).toBe(1);
} finally {
await inspection.release();
}
});
});
it.each(["active", "cold"] as const)(
"preserves %s raw registration lifetime and cache reuse",
async (mode) => {
const fixture = createPreparationFixture();
try {
await fixture.withEnvironment(async () => {
useNoBundledPlugins();
const registry =
mode === "active" ? loadPluginRegistryHandle({ config: fixture.cfg }) : undefined;
for (let index = 0; index < 4; index++) {
const result = await withPluginRuntimeRegistryScope(registry, fixture.prepare);
expect(result.directives.hasDirective).toBe(true);
}
expect(fixture.state.connections.length).toBe(1);
await fixture.host.close();
expect(fixture.state.connections[0]?.database.isOpen).toBe(true);
expect(fixture.state.connections[0]?.disposals).toBe(0);
});
} finally {
await fixture.cleanup();
}
await fixture.runTest(async () => {
const registry =
mode === "active" ? loadPluginRegistryHandle({ config: fixture.cfg }) : undefined;
for (let index = 0; index < 4; index++) {
const result = await withPluginRuntimeRegistryScope(registry, fixture.prepare);
expect(result.directives.hasDirective).toBe(true);
}
expect(fixture.state.connections.length).toBe(1);
await fixture.host.close();
expect(fixture.state.connections[0]?.database.isOpen).toBe(true);
expect(fixture.state.connections[0]?.disposals).toBe(0);
});
expect(fixture.state.connections[0]?.cleanups).toBe(1);
},
);
it("rejects new preparation after the SDK host closes", async () => {
const fixture = createPreparationFixture();
try {
await fixture.withEnvironment(async () => {
useNoBundledPlugins();
await fixture.host.close();
await expect(fixture.prepare().then(() => undefined)).rejects.toThrow(
"Plugin SDK resource host is closed",
);
expect(fixture.state.connections.length).toBe(0);
});
} finally {
await fixture.cleanup();
}
await fixture.runTest(async () => {
await fixture.host.close();
await expect(fixture.prepare().then(() => undefined)).rejects.toThrow(
"Plugin SDK resource host is closed",
);
expect(fixture.state.connections.length).toBe(0);
});
});
});

View file

@ -28,7 +28,7 @@ function createFixture(
explicitRelease?: boolean;
ending?: "eof" | "error" | "blocked";
initialState?: "empty" | "error" | "chunk";
projectionFailure?: "result" | "config-model" | "config-voice" | "audioStream" | "release";
projectionFailure?: "result" | "config-model" | "audioStream" | "release";
holdRelease?: boolean;
invalidMetadata?: boolean;
releaseFailure?: boolean;
@ -108,8 +108,8 @@ module.exports = { id: ${JSON.stringify(id)}, register(api) {
record("release"); state.released.resolve(); state.aborted.resolve();
await state.releaseResume.promise; record("released"); if (state.options.releaseFailure) throw new Error("native cleanup failure");
})();
if (state.options.projectionFailure?.startsWith("config-")) {
Object.defineProperty(request.providerConfig, state.options.projectionFailure === "config-model" ? "modelId" : "speakerVoiceId", { get() { throw new Error("native config projection failure"); } });
if (state.options.projectionFailure === "config-model") {
Object.defineProperty(request.providerConfig, "modelId", { get() { throw new Error("native config projection failure"); } });
}
if (["audioStream", "release"].includes(state.options.projectionFailure)) {
state.work.push(state.trackAsyncWork(async () => { await state.aborted.promise; record("capture-tail"); }));
@ -148,18 +148,23 @@ module.exports = { id: ${JSON.stringify(id)}, register(api) {
plugin,
state,
run,
withEnvironment: (operation: () => Promise<void>) =>
withEnvAsync(
{
OPENCLAW_HOME: dir,
OPENCLAW_STATE_DIR: dir,
OPENCLAW_CONFIG_PATH: path.join(dir, "config.json"),
},
async () => {
useNoBundledPlugins();
await operation();
},
),
async runTest(operation: () => Promise<void>) {
try {
await withEnvAsync(
{
OPENCLAW_HOME: dir,
OPENCLAW_STATE_DIR: dir,
OPENCLAW_CONFIG_PATH: path.join(dir, "config.json"),
},
async () => {
useNoBundledPlugins();
await operation();
},
);
} finally {
await this.cleanup();
}
},
async cleanup() {
aborted.resolve();
tail.resolve();
@ -196,115 +201,77 @@ afterEach(() => {
afterAll(cleanupPluginLoaderFixturesForTest);
describe("streaming speech registration ownership", () => {
it.each(["eof", "error"] as const)(
"keeps explicit cleanup after %s and closes on release",
async (ending) => {
const fixture = createFixture({ ending });
it.each([
{ ending: "eof", explicitRelease: true },
{ ending: "error", explicitRelease: true },
{ ending: "eof", explicitRelease: false },
{ ending: "error", explicitRelease: false },
] as const)("closes after $ending with explicit release=$explicitRelease", async (options) => {
const fixture = createFixture(options);
await fixture.runTest(async () => {
const result = await fixture.run();
expect(result.success).toBe(true);
const reader = result.audioStream!.getReader();
try {
await fixture.withEnvironment(async () => {
const result = await fixture.run();
expect(result.success).toBe(true);
const reader = result.audioStream!.getReader();
try {
expect((await reader.read()).value).toEqual(pcm);
if (ending === "error") {
await expect(reader.read()).rejects.toThrow("native read failure");
} else {
expect((await reader.read()).done).toBe(true);
}
expect(fixture.state.events.some((event) => event.phase === "release")).toBe(false);
expect(fixture.state.connections.every((entry) => entry.database.isOpen)).toBe(true);
await result.release?.();
await result.release?.();
expect(fixture.state.events.filter((event) => event.phase === "release").length).toBe(
1,
);
expectClosed(fixture);
} finally {
reader.releaseLock();
await result.release?.();
}
});
expect((await reader.read()).value).toEqual(pcm);
if (options.ending === "error") {
await expect(reader.read()).rejects.toThrow("native read failure");
} else {
expect((await reader.read()).done).toBe(true);
}
if (options.explicitRelease) {
expect(fixture.state.events.some((event) => event.phase === "release")).toBe(false);
expect(fixture.state.connections.every((entry) => entry.database.isOpen)).toBe(true);
await result.release?.();
await result.release?.();
expect(fixture.state.events.filter((event) => event.phase === "release").length).toBe(1);
}
expectClosed(fixture);
} finally {
await fixture.cleanup();
reader.releaseLock();
await result.release?.();
}
},
);
it.each(["eof", "error"] as const)(
"releases a provider with no release callback after %s",
async (ending) => {
const fixture = createFixture({ ending, explicitRelease: false });
try {
await fixture.withEnvironment(async () => {
const result = await fixture.run();
const reader = result.audioStream!.getReader();
try {
expect((await reader.read()).value).toEqual(pcm);
if (ending === "error") {
await expect(reader.read()).rejects.toThrow("native read failure");
} else {
expect((await reader.read()).done).toBe(true);
}
expectClosed(fixture);
} finally {
reader.releaseLock();
await result.release?.();
}
});
} finally {
await fixture.cleanup();
}
},
);
});
});
it("releases an unopened stream without requiring a reader", async () => {
const fixture = createFixture();
try {
await fixture.withEnvironment(async () => {
const result = await fixture.run();
await result.release?.();
expect(fixture.state.events.some((event) => event.phase === "pull")).toBe(false);
expectClosed(fixture);
});
} finally {
await fixture.cleanup();
}
await fixture.runTest(async () => {
const result = await fixture.run();
await result.release?.();
expect(fixture.state.events.some((event) => event.phase === "pull")).toBe(false);
expectClosed(fixture);
});
});
it.each(["managed", "raw"] as const)(
"preserves %s registration custody after stream handoff",
async (kind) => {
const fixture = createFixture();
try {
await fixture.withEnvironment(async () => {
const inspection =
kind === "managed"
? await acquirePluginRegistryForInspection({ config: fixture.cfg })
: undefined;
const registry =
inspection?.registry ?? loadPluginRegistryHandle({ config: fixture.cfg });
const result = await withPluginRuntimeRegistryScope(registry, fixture.run);
await inspection?.release();
const reader = result.audioStream!.getReader();
try {
expect((await reader.read()).value).toEqual(pcm);
expect((await reader.read()).done).toBe(true);
await result.release?.();
if (kind === "managed") {
expectClosed(fixture);
} else {
expect(fixture.state.connections.every((entry) => entry.database.isOpen)).toBe(true);
expect(fixture.state.connections.every((entry) => entry.disposals === 0)).toBe(true);
}
} finally {
reader.releaseLock();
await result.release?.();
await fixture.runTest(async () => {
const inspection =
kind === "managed"
? await acquirePluginRegistryForInspection({ config: fixture.cfg })
: undefined;
const registry = inspection?.registry ?? loadPluginRegistryHandle({ config: fixture.cfg });
const result = await withPluginRuntimeRegistryScope(registry, fixture.run);
await inspection?.release();
const reader = result.audioStream!.getReader();
try {
expect((await reader.read()).value).toEqual(pcm);
expect((await reader.read()).done).toBe(true);
await result.release?.();
if (kind === "managed") {
expectClosed(fixture);
} else {
expect(fixture.state.connections.every((entry) => entry.database.isOpen)).toBe(true);
expect(fixture.state.connections.every((entry) => entry.disposals === 0)).toBe(true);
}
});
} finally {
await fixture.cleanup();
}
} finally {
reader.releaseLock();
await result.release?.();
}
});
},
);
@ -312,54 +279,50 @@ describe("streaming speech registration ownership", () => {
"%s aborts before joining the blocked pull and retains its actual tail/context",
async (method) => {
const fixture = createFixture({ ending: "blocked" });
try {
await fixture.withEnvironment(async () => {
const result = await fixture.run();
const reader = result.audioStream!.getReader();
const read = reader.read();
await fixture.state.started.promise;
let settled = false;
const closing = fixture.state.context
.run("foreign", () => (method === "cancel" ? reader.cancel("stop") : result.release!()))
.then(() => {
settled = true;
});
try {
await nextTurn();
expect(fixture.state.events.some((event) => event.phase === "release")).toBe(true);
expect(settled).toBe(false);
expect(fixture.state.connections.every((entry) => entry.database.isOpen)).toBe(true);
fixture.state.tail.resolve();
await closing;
await read;
expect(
fixture.state.events
.filter((event) => ["cancel", "release", "tail"].includes(event.phase))
.every((event) => event.context === "source"),
).toBe(true);
expectClosed(fixture);
} finally {
fixture.state.aborted.resolve();
fixture.state.tail.resolve();
await closing;
await read;
reader.releaseLock();
await result.release?.();
}
});
} finally {
await fixture.cleanup();
}
await fixture.runTest(async () => {
const result = await fixture.run();
const reader = result.audioStream!.getReader();
const read = reader.read();
await fixture.state.started.promise;
let settled = false;
const closing = fixture.state.context
.run("foreign", () => (method === "cancel" ? reader.cancel("stop") : result.release!()))
.then(() => {
settled = true;
});
try {
await nextTurn();
expect(fixture.state.events.some((event) => event.phase === "release")).toBe(true);
expect(settled).toBe(false);
expect(fixture.state.connections.every((entry) => entry.database.isOpen)).toBe(true);
fixture.state.tail.resolve();
await closing;
await read;
expect(
fixture.state.events
.filter((event) => ["cancel", "release", "tail"].includes(event.phase))
.every((event) => event.context === "source"),
).toBe(true);
expectClosed(fixture);
} finally {
fixture.state.aborted.resolve();
fixture.state.tail.resolve();
await closing;
await read;
reader.releaseLock();
await result.release?.();
}
});
},
);
it.each(["result", "config-model", "config-voice"] as const)(
it.each(["result", "config-model"] as const)(
"closes a stream when %s metadata projection fails before fallback",
async (projectionFailure) => {
const primary = createFixture({ id: "primary-stream", projectionFailure, holdRelease: true });
const fallback = createFixture({ id: "fallback-stream" });
try {
await primary.withEnvironment(async () => {
await primary.runTest(async () => {
const cfg: OpenClawConfig = {
...primary.cfg,
plugins: {
@ -397,31 +360,26 @@ describe("streaming speech registration ownership", () => {
}
});
} finally {
await primary.cleanup();
await fallback.cleanup();
}
},
);
it("releases a successful provider result rejected by the public metadata guard", async () => {
const fixture = createFixture({ invalidMetadata: true });
try {
await fixture.withEnvironment(async () => {
const result = await fixture.run();
expect(result.success).toBe(false);
expect(result.error).toBe("Streaming TTS conversion failed");
expect(fixture.state.events.filter((event) => event.phase === "release").length).toBe(1);
expectClosed(fixture);
});
} finally {
await fixture.cleanup();
}
await fixture.runTest(async () => {
const result = await fixture.run();
expect(result.success).toBe(false);
expect(result.error).toBe("Streaming TTS conversion failed");
expect(fixture.state.events.filter((event) => event.phase === "release").length).toBe(1);
expectClosed(fixture);
});
});
it("keeps a post-byte stream failure with the selected provider", async () => {
const primary = createFixture({ id: "primary-stream", ending: "error" });
const fallback = createFixture({ id: "fallback-stream" });
try {
await primary.withEnvironment(async () => {
await primary.runTest(async () => {
const cfg: OpenClawConfig = {
...primary.cfg,
plugins: {
@ -447,24 +405,19 @@ describe("streaming speech registration ownership", () => {
}
});
} finally {
await primary.cleanup();
await fallback.cleanup();
}
});
it("preserves the projection error when its stream cleanup also fails", async () => {
const fixture = createFixture({ projectionFailure: "result", releaseFailure: true });
try {
await fixture.withEnvironment(async () => {
const result = await fixture.run();
expect(result.success).toBe(false);
expect(result.error).toContain("native result projection failure");
expect(result.error).not.toContain("native cleanup failure");
expectClosed(fixture);
});
} finally {
await fixture.cleanup();
}
await fixture.runTest(async () => {
const result = await fixture.run();
expect(result.success).toBe(false);
expect(result.error).toContain("native result projection failure");
expect(result.error).not.toContain("native cleanup failure");
expectClosed(fixture);
});
});
it.each([
{ projectionFailure: "audioStream", releaseFailure: false },
@ -474,64 +427,56 @@ describe("streaming speech registration ownership", () => {
"uses the other cleanup field when $projectionFailure fails (cleanup rejects: $releaseFailure)",
async ({ projectionFailure, releaseFailure }) => {
const fixture = createFixture({ projectionFailure, releaseFailure });
try {
await fixture.withEnvironment(async () => {
const pending = fixture.run();
try {
await fixture.state.started.promise;
await nextTurn();
expect(
fixture.state.events.some(
(event) =>
event.phase === (projectionFailure === "audioStream" ? "release" : "cancel"),
),
).toBe(true);
const result = await pending;
expect(result.success).toBe(false);
expect(result.error).toContain(`native ${projectionFailure} projection failure`);
expect(fixture.state.events.some((event) => event.phase === "capture-tail")).toBe(true);
expectClosed(fixture);
} finally {
fixture.state.aborted.resolve();
await pending;
}
});
} finally {
await fixture.cleanup();
}
await fixture.runTest(async () => {
const pending = fixture.run();
try {
await fixture.state.started.promise;
await nextTurn();
expect(
fixture.state.events.some(
(event) =>
event.phase === (projectionFailure === "audioStream" ? "release" : "cancel"),
),
).toBe(true);
const result = await pending;
expect(result.success).toBe(false);
expect(result.error).toContain(`native ${projectionFailure} projection failure`);
expect(fixture.state.events.some((event) => event.phase === "capture-tail")).toBe(true);
expectClosed(fixture);
} finally {
fixture.state.aborted.resolve();
await pending;
}
});
},
);
it.each(["empty", "error", "chunk"] as const)(
"finishes a no-release source in initial state %s without an extra EOF read",
async (initialState) => {
const fixture = createFixture({ explicitRelease: false, initialState });
try {
await fixture.withEnvironment(async () => {
const result = await fixture.run();
const reader = result.audioStream!.getReader();
try {
if (initialState === "chunk") {
expect((await reader.read()).value).toEqual(pcm);
}
if (initialState === "error") {
await expect(reader.closed).rejects.toThrow("native initial failure");
} else {
await expect(reader.closed).resolves.toBeUndefined();
}
expectClosed(fixture);
if (initialState === "error") {
await expect(reader.read()).rejects.toThrow("native initial failure");
} else {
expect((await reader.read()).done).toBe(true);
}
} finally {
reader.releaseLock();
await result.release?.();
await fixture.runTest(async () => {
const result = await fixture.run();
const reader = result.audioStream!.getReader();
try {
if (initialState === "chunk") {
expect((await reader.read()).value).toEqual(pcm);
}
});
} finally {
await fixture.cleanup();
}
if (initialState === "error") {
await expect(reader.closed).rejects.toThrow("native initial failure");
} else {
await expect(reader.closed).resolves.toBeUndefined();
}
expectClosed(fixture);
if (initialState === "error") {
await expect(reader.read()).rejects.toThrow("native initial failure");
} else {
expect((await reader.read()).done).toBe(true);
}
} finally {
reader.releaseLock();
await result.release?.();
}
});
},
);
});

View file

@ -342,31 +342,26 @@ afterEach(async () => {
});
afterAll(cleanupPluginLoaderFixturesForTest);
it.each([false, true])(
"owns the selected summary model through completion and cleanup (tail=%s)",
async (tail) => {
const fixture = nativeSummaryFixture();
await fixture.run(async () => {
fixture.state.completionTail = tail;
fixture.state.finish.resolve();
const outcome = await fixture.summarize();
expect(fixture.state.connections[0]?.openAtCompletion).toBe(true);
expect(outcome).toMatchObject({
result: { summary: "Spoken summary 42." },
});
expect(fixture.state.calls.map((call) => call.model)).toEqual(["summary-model"]);
if (tail) {
await setImmediate();
expect(fixture.state.connections[0]?.database.isOpen).toBe(true);
fixture.state.finishTail.resolve();
}
await fixture.assertClosed();
expect(fixture.state.connections[0]?.completionReads).toBe(1);
expect(fixture.state.connections[0]?.cleanupReads).toBe(tail ? 1 : 0);
expect(fixture.state.cancellation.failure).toBeUndefined();
it("owns the selected summary model through completion and its cleanup tail", async () => {
const fixture = nativeSummaryFixture();
await fixture.run(async () => {
fixture.state.completionTail = true;
fixture.state.finish.resolve();
const outcome = await fixture.summarize();
expect(fixture.state.connections[0]?.openAtCompletion).toBe(true);
expect(outcome).toMatchObject({
result: { summary: "Spoken summary 42." },
});
},
);
expect(fixture.state.calls.map((call) => call.model)).toEqual(["summary-model"]);
await setImmediate();
expect(fixture.state.connections[0]?.database.isOpen).toBe(true);
fixture.state.finishTail.resolve();
await fixture.assertClosed();
expect(fixture.state.connections[0]?.completionReads).toBe(1);
expect(fixture.state.connections[0]?.cleanupReads).toBe(1);
expect(fixture.state.cancellation.failure).toBeUndefined();
});
});
it("starts the summary deadline after preparation and keeps an uncooperative provider owned", async () => {
const fixture = nativeSummaryFixture();

View file

@ -1,5 +1,5 @@
import { createServer } from "node:http";
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, expect, it, vi } from "vitest";
import { requireApiKey } from "../agents/model-auth.js";
import { acquireAgentRunPreparedModelRuntime } from "../agents/prepared-model-runtime.js";
import { resetPreparedModelRuntimeSnapshotsForTest } from "../agents/prepared-model-runtime.test-support.js";
@ -203,62 +203,36 @@ function summaryRequest(cfg: OpenClawConfig) {
};
}
describe.each([undefined, "openai-completions"] as const)(
"unscoped summary selection with provider API %s",
(api) => {
it.each([
{ name: "explicit override", summaryModel: `${provider}/entry`, expected: "middle" },
{ name: "global bare alias", summaryModel: "fast", expected: "middle" },
{ name: "global qualified alias", summaryModel: `${provider}/fast`, expected: "middle" },
{ name: "bare literal", summaryModel: "entry", expected: "middle" },
{ name: "missing override", primary: `${provider}/entry`, expected: "middle" },
{ name: "bare global default", primary: "fast", expected: "middle" },
{ name: "invalid override", summaryModel: "/", primary: "fast", expected: "middle" },
{ name: "profile suffix", summaryModel: "fast@work", expected: "middle" },
{ name: "raw next alias", summaryModel: `${provider}/middle`, expected: "final" },
{ name: "ordinary model", summaryModel: `${provider}/plain`, expected: "plain" },
])("uses the global model for $name", async ({ expected, ...options }) => {
await withSummaryFixture({ ...options, api }, async (cfg, _state, requests) => {
expect(await summarizeText(summaryRequest(cfg))).toMatchObject({
summary: `materialized:${expected}`,
});
expect(requests).toEqual([expected]);
it.each([
{ name: "explicit override", summaryModel: `${provider}/entry` },
{ name: "qualified alias", summaryModel: `${provider}/fast`, api: "openai-completions" },
{ name: "bare literal", summaryModel: "entry", api: "openai-completions" },
{ name: "missing override", primary: `${provider}/entry` },
{ name: "invalid override", summaryModel: "/", primary: "fast", api: "openai-completions" },
{ name: "bare alias with profile suffix", summaryModel: "fast@work" },
{ name: "bare default without configured rows", primary: "entry", bareDefault: true },
{
name: "invalid override without configured rows",
primary: "entry",
summaryModel: "/",
bareDefault: true,
api: "openai-completions",
},
{
name: "bare override without configured rows",
primary: "plain",
summaryModel: "entry",
bareDefault: true,
},
] satisfies Array<FixtureOptions & { name: string }>)(
"normalizes the global summary selection once for $name",
async (options) => {
await withSummaryFixture(options, async (cfg, _state, requests) => {
expect(await summarizeText(summaryRequest(cfg))).toMatchObject({
summary: "materialized:middle",
});
expect(requests).toEqual(["middle"]);
});
it.each([
{ name: "missing override", primary: "entry", expected: "middle" },
{ name: "invalid override", primary: "entry", summaryModel: "/", expected: "middle" },
{
name: "explicit bare override",
primary: "plain",
summaryModel: "entry",
expected: "middle",
},
{
name: "qualified primary",
primary: "openai/entry",
expected: "middle",
},
{ name: "raw next alias", primary: "middle", expected: "final" },
{ name: "ordinary primary", primary: "plain", expected: "plain" },
])(
"uses the default provider without configured rows for $name",
async ({ expected, ...options }) => {
await withSummaryFixture(
{ ...options, api, bareDefault: true },
async (cfg, _state, requests) => {
expect(cfg.agents?.defaults?.models).toEqual({});
expect(cfg.agents?.entries?.main?.models).toEqual({});
expect(cfg.models?.providers?.openai?.models).toEqual([]);
expect(await summarizeText(summaryRequest(cfg))).toMatchObject({
summary: `materialized:${expected}`,
});
expect(requests).toEqual([expected]);
},
);
},
);
},
);

View file

@ -114,47 +114,44 @@ if (endpoint === "repos/openclaw/openclaw/actions/runs/101") {
}
describe("full release child evidence producer", () => {
it.each(["normalCi", "pluginPrereleaseIndependent"])(
"blocks failed selected metadata and workload jobs for %s",
(key) => {
const workload = {
name: "install_smoke",
status: "completed",
conclusion: "success",
};
const metadata = { name: PUBLISHER, status: "completed", conclusion: "failure" };
const snapshot = () =>
classifyReleaseSnapshot({
children: [
{
key,
selected: true,
required: true,
result: "success",
source: "fresh",
runId: "101",
runAttempt: 1,
status: "completed",
conclusion: "success",
jobs: [workload, metadata],
},
],
releaseProfile: "stable",
workflowRef: "main",
});
expect(snapshot()).toMatchObject({
state: "blocked_complete",
blockers: [expect.objectContaining({ job: PUBLISHER, kind: "job_failure" })],
it.each(["normalCi"])("blocks failed selected metadata and workload jobs for %s", (key) => {
const workload = {
name: "install_smoke",
status: "completed",
conclusion: "success",
};
const metadata = { name: PUBLISHER, status: "completed", conclusion: "failure" };
const snapshot = () =>
classifyReleaseSnapshot({
children: [
{
key,
selected: true,
required: true,
result: "success",
source: "fresh",
runId: "101",
runAttempt: 1,
status: "completed",
conclusion: "success",
jobs: [workload, metadata],
},
],
releaseProfile: "stable",
workflowRef: "main",
});
metadata.conclusion = "success";
expect(snapshot()).toMatchObject({ state: "passed", blockers: [] });
workload.conclusion = "failure";
expect(snapshot()).toMatchObject({
state: "blocked_complete",
blockers: [expect.objectContaining({ job: "install_smoke", kind: "job_failure" })],
});
},
);
expect(snapshot()).toMatchObject({
state: "blocked_complete",
blockers: [expect.objectContaining({ job: PUBLISHER, kind: "job_failure" })],
});
metadata.conclusion = "success";
expect(snapshot()).toMatchObject({ state: "passed", blockers: [] });
workload.conclusion = "failure";
expect(snapshot()).toMatchObject({
state: "blocked_complete",
blockers: [expect.objectContaining({ job: "install_smoke", kind: "job_failure" })],
});
});
it.each([
"ci.yml",
@ -263,7 +260,7 @@ describe("full release child evidence producer", () => {
]);
});
it.each([2, 3])(
it.each([3])(
"recovers publisher-only attempt %s while carrying earlier workload evidence",
(runAttempt) => {
const data = fixture();
@ -289,7 +286,7 @@ describe("full release child evidence producer", () => {
workloadConclusion: "success",
effectiveRunAttempt: runAttempt,
});
expect(evidence.observedRunAttempts).toEqual(runAttempt === 2 ? [1, 2] : [1, 2, 3]);
expect(evidence.observedRunAttempts).toEqual([1, 2, 3]);
expect(
evidence.jobs.map((job: { name: string; acceptedRunAttempt: number }) => [
job.name,

View file

@ -296,11 +296,7 @@ describe("independent release child reuse", () => {
expect.stringContaining('scanned 7 runs, evaluated 1; skipped {"target receipt absent":6}'),
);
});
it.each([
{ status: "completed", conclusion: "failure" },
{ status: "completed", conclusion: "cancelled" },
{ status: "in_progress", conclusion: null },
])(
it.each([{ status: "in_progress", conclusion: null }])(
"reuses green children from $status/$conclusion parents without a manifest",
async (parentState) => {
const data = await fixture();
@ -485,14 +481,13 @@ describe("independent release child reuse", () => {
it.each([
{ target_ref: "c".repeat(40), release_scope: "full" },
{ target_ref: TARGET, release_scope: "npm-beta" },
{ target_ref: TARGET, release_scope: "" },
])("rejects different exact dispatch inputs %#", async (inputs) => {
const data = await fixture();
expect(await discoverReusableReleaseChild({ ...data.request, inputs }, data.deps)).toBeNull();
});
it.each(['{ "id": "801" }', '{"id":"802"}', ""])(
it.each(['{ "id": "801" }', ""])(
"requires the exact candidate descriptor bytes %s",
async (candidate_artifact_json) => {
const data = await fixture("pluginPrereleaseCandidate");

View file

@ -248,17 +248,7 @@ describe("publication dispatch transport", () => {
value: { trustedWorkflow: identity, publicationSelection: selection },
error: "source-admission envelope requires identity, purpose and selection",
},
{
name: "missing identity",
value: { validationPurpose: "publish", publicationSelection: selection },
error: "source-admission envelope requires identity, purpose and selection",
},
{ name: "old flat identity", value: identity, error: "invalid source-admission envelope" },
{
name: "extra envelope field",
value: { ...envelope, extra: true },
error: "invalid source-admission envelope",
},
{
name: "extra identity field",
value: { ...envelope, trustedWorkflow: { ...identity, extra: true } },
@ -1567,16 +1557,6 @@ describe("FRV observation worker boundary", () => {
}),
);
publicationIt.concurrent.for([
[
"beta plugin",
"2026.9.9-beta.1",
"2026.9.9-beta.1",
"normal",
"beta",
"@openclaw/demo-plugin",
"npm-absent",
true,
],
["root package", "2026.9.9", "2026.9.9", "normal", "latest", "openclaw", "npm-absent", false],
[
"core package",
@ -1588,16 +1568,6 @@ describe("FRV observation worker boundary", () => {
"npm-absent",
false,
],
[
"stable plugin on beta",
"2026.9.9",
"2026.9.9",
"normal",
"beta",
"@openclaw/demo-plugin",
"npm-absent",
false,
],
[
"stable plugin on latest",
"2026.9.9",
@ -1638,16 +1608,6 @@ describe("FRV observation worker boundary", () => {
"npm-absent",
true,
],
[
"prepared beta plugin",
"2026.9.9-beta.1",
"2026.9.9-beta.1",
"prepared",
"beta",
"@openclaw/demo-plugin",
"npm-absent",
true,
],
[
"prepared npm-only beta plugin",
"2026.9.9-beta.1",
@ -1678,16 +1638,6 @@ describe("FRV observation worker boundary", () => {
"npm-empty-history",
false,
],
[
"stable empty history",
"2026.9.9",
"2026.9.9",
"normal",
"latest",
"@openclaw/demo-plugin",
"npm-empty-history",
false,
],
] as const)(
"matches npm bootstrap writer ownership for %s",
{ timeout: 30_000 },
@ -1822,8 +1772,6 @@ describe("FRV observation worker boundary", () => {
);
publicationIt.concurrent.for([
["npm-absent", "normal", true, "npm", "owner-preparation-and-access"],
["npm-absent", "prepared", true, "npm", "owner-preparation-and-access"],
["clawhub-absent", "normal", true, "clawhub", "bootstrap-and-owner-access"],
["clawhub-absent", "prepared", false, "clawhub", ""],
["missing-trust", "normal", true, "clawhub", "publisher-repair"],
@ -1958,23 +1906,6 @@ describe("FRV observation worker boundary", () => {
check(result.observations).toBeUndefined();
}),
);
publicationIt.concurrent(
"uses the existing parser-false runtime for a same-SHA worker",
async ({ command: processFixture, expect: check }) =>
processFixture.lifetime.run(async () => {
const result = await fixture(processFixture, check, { registry: "healthy", sameSha: true });
check(result.status, result.stderr).toBe(0);
check(result.targetSha).toBe(result.toolingSha);
check(result.registryCalls.filter((entry) => entry.kind === "request")).toHaveLength(6);
check(result.registryCalls).toContainEqual({
kind: "runtime",
worker: true,
inherited: [],
});
}),
30_000,
);
});
describe("FRV publication source admission", () => {
@ -2005,10 +1936,6 @@ describe("FRV publication source admission", () => {
);
publicationIt.concurrent.for([
["2026.9.9", "normal", false],
["2026.9.9-1", "normal", false],
["2026.9.9", "prepared", false],
["2026.9.9-1", "prepared", false],
["2026.9.9", "normal", true],
["2026.9.9-1", "prepared", true],
] as const)(
"preserves beta-first %s publication through %s with Windows=%s",
@ -2114,27 +2041,7 @@ describe("FRV publication source admission", () => {
const result = await fixture(processFixture, check, { fault: "readme" });
check(result.status, result.stderr).toBe(1);
check(result.stderr).toContain("README.md must exist");
for (const id of ["normal_ci", "prepare_npm_package", "prepare_docker_release"]) {
const job = expectDefined(workflow.jobs[id], `${id} job`);
check([job.needs].flat()).toContain("resolve_target");
check(
evaluate(job.if ?? "", {
github: { run_attempt: 1 },
inputs: { rerun_group: "all" },
needs: {
resolve_target: {
result: result.status === 0 ? "success" : "failure",
outputs: {
candidate_required: "true",
target_version: "2026.9.9",
},
},
plugin_compatibility_readiness: { result: "success" },
evidence_reuse: { result: "skipped", outputs: { reuse: "false" } },
},
}),
).toBe(false);
}
check(result.fact).toBeUndefined();
}),
30_000,
);
@ -2172,7 +2079,7 @@ describe("FRV publication source admission", () => {
}),
);
publicationIt.concurrent.for(["diagnostic", "main-qualification", "postpublish-confidence"])(
publicationIt.concurrent.for(["diagnostic"])(
"retains %s without publication bootstrap or added installation",
{ timeout: 30_000 },
async (purpose, { command: processFixture, expect: check }) =>
@ -2225,7 +2132,6 @@ describe("FRV publication source admission", () => {
publicationIt.concurrent.for([
["2026.9.9-beta.1", "normal", "beta", "release/2026.9.9", false],
["2026.9.9", "normal", "latest", "release/2026.9.9", true],
["2026.9.9", "normal", "beta", "v2026.9.9", true],
["2026.9.9-1", "normal", "beta", "v2026.9.9-1", true],
] as const)(
"matches actual Windows publication selection for %s through %s to %s",
@ -2352,7 +2258,6 @@ describe("FRV publication source admission", () => {
publicationIt.concurrent.for([
["2026.9.9-beta.1", "normal", "beta", "release/2026.9.9"],
["2026.9.9", "prepared", "latest", "release/2026.9.9"],
["2026.8.33", "extended-stable", "extended-stable", "extended-stable/2026.8.33"],
])(
"admits %s through the existing %s source policy",
@ -2371,14 +2276,7 @@ describe("FRV publication source admission", () => {
check(result.fact?.projection?.version).toBe(version);
check(result.fact?.targetContextRef).toBe(targetContextRef);
const platforms = expectDefined(result.fact?.projection?.platforms, "source platforms");
if (version === "2026.9.9") {
check(platforms).toContainEqual({
id: "linux",
source: ".github/workflows/linux-app-release-request.yml",
});
} else {
check(platforms).not.toContainEqual(expect.objectContaining({ id: "linux" }));
}
check(platforms).not.toContainEqual(expect.objectContaining({ id: "linux" }));
if (route === "extended-stable") {
check(result.fact?.projection?.packages).toEqual(
expect.arrayContaining([{ name: "@openclaw/demo-plugin", version, targets: ["npm"] }]),
@ -2558,10 +2456,7 @@ describe("publication source intent and durable binding", () => {
);
publicationIt.each([
["vbad", false],
["latest", false],
["v0.5", false],
["v0.5.0", true],
["v0.5.0-rc.1", true],
])("validates Windows source tag %s using its native version contract", (tag, valid) => {
const normalize = () =>
@ -2577,10 +2472,8 @@ describe("publication source intent and durable binding", () => {
});
publicationIt.each([
["", ""],
["unknown", ""],
["publish", ""],
["diagnostic", JSON.stringify(selection)],
["publish", JSON.stringify({ ...selection, extra: true })],
["publish", JSON.stringify({ ...selection, pluginPublishScope: "selected" })],
["publish", JSON.stringify({ ...selection, route: "prepared", publishOpenclawNpm: false })],
@ -2588,15 +2481,6 @@ describe("publication source intent and durable binding", () => {
expect(() => normalizePublicationIntent(purpose, value)).toThrow();
});
publicationIt("keeps canonical reusable intent free of per-parent identities", () => {
expect(
publicationIntentInputs(normalizePublicationIntent("publish", JSON.stringify(selection))),
).toEqual({
validationPurpose: "publish",
publicationSelectionJson: publicationSourceJson(selection),
});
});
publicationIt(
"requires exact workflow capability and rejects missing or relabeled new evidence",
() => {

View file

@ -3,50 +3,48 @@ import { EventEmitter } from "node:events";
import { describe, expect, it, vi } from "vitest";
import { stopChild } from "../../scripts/lib/gateway-bench-child.ts";
function childFixture() {
const state: { exitCode: number | null; signalCode: NodeJS.Signals | null } = {
exitCode: null,
signalCode: null,
};
return Object.assign(new EventEmitter(), state, { kill: vi.fn(() => true) });
}
function pipedChildFixture() {
return Object.assign(childFixture(), {
stderr: { destroy: vi.fn() },
stdin: { destroy: vi.fn() },
stdout: { destroy: vi.fn() },
unref: vi.fn(),
});
}
describe("gateway benchmark child teardown", () => {
it.each([0, 7])(
"classifies queued child exit %i before sending teardown signals",
async (exitCode) => {
const child = new EventEmitter() as EventEmitter & {
exitCode: number | null;
kill: ReturnType<typeof vi.fn>;
signalCode: NodeJS.Signals | null;
};
child.exitCode = null;
child.signalCode = null;
child.kill = vi.fn(() => true);
const stopped = stopChild(child as unknown as ChildProcess);
queueMicrotask(() => {
child.exitCode = exitCode;
child.emit("exit", exitCode, null);
});
await expect(stopped).resolves.toEqual({
exitedBeforeTeardown: true,
exitCode,
signal: null,
});
expect(child.kill).not.toHaveBeenCalled();
},
);
it("classifies a queued child failure before sending teardown signals", async () => {
const child = childFixture();
const stopped = stopChild(child as unknown as ChildProcess);
queueMicrotask(() => {
child.exitCode = 7;
child.emit("exit", 7, null);
});
await expect(stopped).resolves.toEqual({
exitedBeforeTeardown: true,
exitCode: 7,
signal: null,
});
expect(child.kill).not.toHaveBeenCalled();
});
it("classifies failed teardown signaling as a pre-teardown child exit", async () => {
const child = new EventEmitter() as EventEmitter & {
exitCode: number | null;
kill: ReturnType<typeof vi.fn>;
signalCode: NodeJS.Signals | null;
};
child.exitCode = null;
child.signalCode = null;
child.kill = vi.fn(() => {
const child = childFixture();
child.kill.mockImplementation(() => {
setImmediate(() => {
child.exitCode = 8;
child.emit("exit", 8, null);
});
return false;
});
await expect(stopChild(child as unknown as ChildProcess)).resolves.toEqual({
exitedBeforeTeardown: true,
exitCode: 8,
@ -56,30 +54,9 @@ describe("gateway benchmark child teardown", () => {
});
it("bounds teardown and releases IPC when the child ignores termination signals", async () => {
const child = new EventEmitter() as EventEmitter & {
channel: { unref: ReturnType<typeof vi.fn> };
exitCode: number | null;
kill: ReturnType<typeof vi.fn>;
signalCode: NodeJS.Signals | null;
stderr: { destroy: ReturnType<typeof vi.fn> };
stdin: { destroy: ReturnType<typeof vi.fn> };
stdout: { destroy: ReturnType<typeof vi.fn> };
unref: ReturnType<typeof vi.fn>;
};
child.exitCode = null;
child.signalCode = null;
child.channel = { unref: vi.fn() };
child.kill = vi.fn(() => true);
child.stderr = { destroy: vi.fn() };
child.stdin = { destroy: vi.fn() };
child.stdout = { destroy: vi.fn() };
child.unref = vi.fn();
const child = Object.assign(pipedChildFixture(), { channel: { unref: vi.fn() } });
await expect(
stopChild(child as unknown as ChildProcess, {
killGraceMs: 1,
teardownGraceMs: 1,
}),
stopChild(child as unknown as ChildProcess, { killGraceMs: 1, teardownGraceMs: 1 }),
).resolves.toEqual({
exitedBeforeTeardown: false,
exitCode: null,
@ -94,31 +71,24 @@ describe("gateway benchmark child teardown", () => {
expect(child.unref).toHaveBeenCalledOnce();
});
it.skipIf(process.platform === "win32")(
"preserves pre-teardown wrapper exits while cleaning the process group",
async () => {
const child = new EventEmitter() as EventEmitter & {
exitCode: number | null;
kill: ReturnType<typeof vi.fn>;
pid: number;
signalCode: NodeJS.Signals | null;
stderr: { destroy: ReturnType<typeof vi.fn> };
stdin: { destroy: ReturnType<typeof vi.fn> };
stdout: { destroy: ReturnType<typeof vi.fn> };
unref: ReturnType<typeof vi.fn>;
it.skipIf(process.platform === "win32").each([true, false])(
"joins the process group after wrapper exit with exitedBeforeTeardown=%s",
async (exitedBeforeTeardown) => {
const child = Object.assign(pipedChildFixture(), { pid: 4444 });
const queueExit = () => {
queueMicrotask(() => {
child.exitCode = 0;
child.emit("exit", 0, null);
});
};
child.exitCode = null;
child.kill = vi.fn(() => true);
child.pid = 4444;
child.signalCode = null;
child.stderr = { destroy: vi.fn() };
child.stdin = { destroy: vi.fn() };
child.stdout = { destroy: vi.fn() };
child.unref = vi.fn();
let emittedExit = false;
let processGroupAlive = true;
const processKill = vi.spyOn(process, "kill").mockImplementation((pid, signal) => {
expect(pid).toBe(-child.pid);
if (!exitedBeforeTeardown && signal === "SIGTERM" && !emittedExit) {
emittedExit = true;
queueExit();
}
if (signal === "SIGKILL") {
processGroupAlive = false;
return true;
@ -133,81 +103,10 @@ describe("gateway benchmark child teardown", () => {
killGraceMs: 50,
teardownGraceMs: 1,
});
queueMicrotask(() => {
child.exitCode = 0;
child.emit("exit", 0, null);
});
await expect(stopped).resolves.toEqual({
exitedBeforeTeardown: true,
exitCode: 0,
signal: null,
});
expect(processKill).toHaveBeenCalledWith(-child.pid, "SIGTERM");
expect(processKill).toHaveBeenCalledWith(-child.pid, "SIGKILL");
expect(child.kill).not.toHaveBeenCalled();
expect(child.stdin.destroy).not.toHaveBeenCalled();
expect(child.stdout.destroy).not.toHaveBeenCalled();
expect(child.stderr.destroy).not.toHaveBeenCalled();
expect(child.unref).not.toHaveBeenCalled();
} finally {
processKill.mockRestore();
}
},
);
it.skipIf(process.platform === "win32")(
"waits for the process group after a teardown-triggered wrapper exit",
async () => {
const child = new EventEmitter() as EventEmitter & {
exitCode: number | null;
kill: ReturnType<typeof vi.fn>;
pid: number;
signalCode: NodeJS.Signals | null;
stderr: { destroy: ReturnType<typeof vi.fn> };
stdin: { destroy: ReturnType<typeof vi.fn> };
stdout: { destroy: ReturnType<typeof vi.fn> };
unref: ReturnType<typeof vi.fn>;
};
child.exitCode = null;
child.kill = vi.fn(() => true);
child.pid = 4445;
child.signalCode = null;
child.stderr = { destroy: vi.fn() };
child.stdin = { destroy: vi.fn() };
child.stdout = { destroy: vi.fn() };
child.unref = vi.fn();
let emittedExit = false;
let processGroupAlive = true;
const processKill = vi.spyOn(process, "kill").mockImplementation((pid, signal) => {
expect(pid).toBe(-child.pid);
if (signal === "SIGTERM" && !emittedExit) {
emittedExit = true;
queueMicrotask(() => {
child.exitCode = 0;
child.emit("exit", 0, null);
});
if (exitedBeforeTeardown) {
queueExit();
}
if (signal === "SIGKILL") {
processGroupAlive = false;
return true;
}
if (signal === 0 && !processGroupAlive) {
throw Object.assign(new Error("gone"), { code: "ESRCH" });
}
return true;
});
try {
await expect(
stopChild(child as unknown as ChildProcess, {
killGraceMs: 50,
teardownGraceMs: 1,
}),
).resolves.toEqual({
exitedBeforeTeardown: false,
exitCode: 0,
signal: null,
});
await expect(stopped).resolves.toEqual({ exitedBeforeTeardown, exitCode: 0, signal: null });
expect(processKill).toHaveBeenCalledWith(-child.pid, "SIGTERM");
expect(processKill).toHaveBeenCalledWith(-child.pid, "SIGKILL");
expect(child.kill).not.toHaveBeenCalled();

View file

@ -252,8 +252,6 @@ async function waitMessage(child: ChildProcess, field: string) {
it.each([
["cpu", false],
["heap", false],
["cpu", true],
["heap", true],
] as const)(
"maps %s profiles to native workers with preexisting=%s and records retirement",

View file

@ -58,28 +58,6 @@ describe.skipIf(process.platform !== "linux" || typeof process.threadCpuUsage !=
expect(existsSync(root)).toBe(false);
});
it("retains preparation failure without starting a child or running work", async () => {
const receipt = result();
await runResourceGatewayCase({
result: receipt,
runtime,
prepare: async ({ root }) => {
roots.push(root);
throw new Error("fixture preparation failed");
},
run: async () => {
throw new Error("work must not run");
},
});
expect(receipt).toMatchObject({
status: "failed",
error: "fixture preparation failed",
phases: [],
});
expect(receipt.shutdown).toBeUndefined();
expect(existsSync(roots[0]!)).toBe(true);
});
it.each([
{ workloadFails: true, exitCode: "0", stopError: "0" },
{ workloadFails: false, exitCode: "1", stopError: "0" },
@ -137,11 +115,15 @@ describe.skipIf(process.platform !== "linux" || typeof process.threadCpuUsage !=
writeFileSync(archive, "mismatched fixture");
await installArchive(archive, "0".repeat(64));
},
run: async () => {},
run: async () => {
throw new Error("work must not run");
},
});
expect(receipt).toMatchObject({ status: "failed", fixtures: [], phases: [] });
expect(receipt.error).toContain("matching its SHA-256");
expect(existsSync(invocations)).toBe(false);
expect(receipt.shutdown).toBeUndefined();
expect(existsSync(roots[0]!)).toBe(true);
});
},
);

View file

@ -73,7 +73,7 @@ const ASSETS = [
"build/src/third_party/issue-descriptions/example.md",
];
function fixture(version: string, hashes: Record<string, string>) {
function fixture(version = "1.10.1", hashes = CONTRACT_HASHES["1.10.1"]) {
return {
declaredVersion: version,
manifest: { version, type: "module", bin: { "chrome-devtools-mcp": "./" + CLI } },
@ -87,63 +87,60 @@ describe.each(Object.entries(CONTRACT_HASHES))("patched MCP %s contract", (versi
expect(collectPatchedMcpArtifactErrors(fixture(version, hashes))).toEqual([]);
});
it("binds the bundled manifest to the exact declared pin", () => {
it("rejects changed bytes for every hashed runtime entry", () => {
const input = fixture(version, hashes);
input.manifest.version = version === "1.10.1" ? "1.9.0" : "1.10.1";
expect(collectPatchedMcpArtifactErrors(input)).toContain(
"bundled chrome-devtools-mcp must be ESM version " + version,
input.sha256 = () => "0".repeat(64);
expect(collectPatchedMcpArtifactErrors(input).toSorted()).toEqual(
Object.keys(hashes)
.map((file) => "bundled chrome-devtools-mcp has unpatched or changed runtime entry " + file)
.toSorted(),
);
});
it.each(Object.keys(hashes))("rejects changed or unpatched bytes: %s", (file) => {
const input = fixture(version, hashes);
input.sha256 = (entry) => (entry === file ? "0".repeat(64) : hashes[entry]);
expect(collectPatchedMcpArtifactErrors(input)).toContain(
"bundled chrome-devtools-mcp has unpatched or changed runtime entry " + file,
);
});
it.each([...Object.keys(hashes), ...ASSETS])("rejects missing runtime entries: %s", (file) => {
const input = fixture(version, hashes);
input.files.delete(file);
expect(collectPatchedMcpArtifactErrors(input)).toContain(
file.endsWith("example.md")
? "bundled chrome-devtools-mcp is missing third-party issue descriptions"
: "bundled chrome-devtools-mcp is missing required runtime entry " + file,
);
});
it("rejects mixed-version byte contracts", () => {
const other = Object.entries(CONTRACT_HASHES).find(([candidate]) => candidate !== version)?.[1];
expect(other).toBeDefined();
expect(collectPatchedMcpArtifactErrors(fixture(version, other ?? {}))).toContain(
"bundled chrome-devtools-mcp has unpatched or changed runtime entry build/src/bin/chrome-devtools-mcp-main.js",
);
});
it("does not trust artifact-supplied replacement hashes", () => {
const input = fixture(version, hashes);
expect(
collectPatchedMcpArtifactErrors({
...input,
manifest: { ...input.manifest, hashes: { [CLI]: "0".repeat(64) } },
sha256: (file) => (file === CLI ? "0".repeat(64) : hashes[file]),
}),
).toContain("bundled chrome-devtools-mcp has unpatched or changed runtime entry " + CLI);
});
it("retains the ESM and CLI requirements", () => {
const input = fixture(version, hashes);
input.manifest.type = "commonjs";
input.manifest.bin["chrome-devtools-mcp"] = "./other.js";
expect(collectPatchedMcpArtifactErrors(input)).toEqual([
"bundled chrome-devtools-mcp must be ESM version " + version,
"bundled chrome-devtools-mcp must expose CLI " + CLI,
]);
});
});
it.each([null, 1.8, "", "^1.8.0", "~1.9.0", "1.7.0", "1.10.0", "latest", "toString"])(
it("binds the bundled manifest to the exact declared pin", () => {
const input = fixture();
input.manifest.version = "1.9.0";
expect(collectPatchedMcpArtifactErrors(input)).toContain(
"bundled chrome-devtools-mcp must be ESM version 1.10.1",
);
});
it("reports every missing runtime entry", () => {
const input = fixture();
input.files.clear();
expect(collectPatchedMcpArtifactErrors(input).toSorted()).toEqual(
[
...[...ASSETS.slice(0, -1), ...Object.keys(CONTRACT_HASHES["1.10.1"])].map(
(file) => "bundled chrome-devtools-mcp is missing required runtime entry " + file,
),
"bundled chrome-devtools-mcp is missing third-party issue descriptions",
].toSorted(),
);
});
it("does not trust artifact-supplied replacement hashes", () => {
const input = fixture();
expect(
collectPatchedMcpArtifactErrors({
...input,
manifest: { ...input.manifest, hashes: { [CLI]: "0".repeat(64) } },
sha256: (file) => (file === CLI ? "0".repeat(64) : input.sha256(file)),
}),
).toContain("bundled chrome-devtools-mcp has unpatched or changed runtime entry " + CLI);
});
it("retains the ESM and CLI requirements", () => {
const input = fixture();
input.manifest.type = "commonjs";
input.manifest.bin["chrome-devtools-mcp"] = "./other.js";
expect(collectPatchedMcpArtifactErrors(input)).toEqual([
"bundled chrome-devtools-mcp must be ESM version 1.10.1",
"bundled chrome-devtools-mcp must expose CLI " + CLI,
]);
});
it.each([null, "^1.8.0", "1.10.0", "toString"])(
"rejects unpinned or unknown declarations: %s",
(declaredVersion) => {
expect(

View file

@ -228,30 +228,9 @@ async function packedPluginFixture(runtime = true) {
}
describe("prepared plugin npm publication", () => {
it("seals the full selection, including previously published packages", () => {
const manifest = createPreparedNpmRelease(preparation());
expect(manifest.packages.map((entry: { packageName: string }) => entry.packageName)).toEqual([
"@openclaw/demo",
"@openclaw/existing",
]);
expect(manifest.packages[1].artifact).toMatchObject({
runId: 101,
runAttempt: 2,
artifactId: 2,
});
});
it.each([
["beta bootstrap", version, "beta", "beta", "default", "npm-token-bootstrap"],
["regular stable bootstrap", "2026.9.32", "stable", "latest", "default", "npm-token-bootstrap"],
[
"regular stable correction bootstrap",
"2026.9.32-1",
"stable",
"latest",
"default",
"npm-token-bootstrap",
],
[
"extended-stable OIDC",
"2026.9.33",
@ -280,24 +259,16 @@ describe("prepared plugin npm publication", () => {
},
);
it.each(["npm-oidc", "npm-token-bootstrap"])(
"rejects retired alpha preparation on %s",
(route) => {
expect(() =>
createPreparedNpmRelease(
preparation(
{ version: "2026.9.3-alpha.1", channel: "alpha", publishTag: "alpha" },
route,
),
),
).toThrow("Alpha releases are retired");
},
);
it("rejects retired alpha preparation", () => {
expect(() =>
createPreparedNpmRelease(
preparation({ version: "2026.9.3-alpha.1", channel: "alpha", publishTag: "alpha" }),
),
).toThrow("Alpha releases are retired");
});
it.each([
["first extended-stable patch on latest", "2026.9.33", "stable", "latest", "default"],
["later extended-stable patch on latest", "2026.9.34", "stable", "latest", "default"],
["extended-stable correction on latest", "2026.9.33-1", "stable", "latest", "default"],
["explicit extended-stable", "2026.9.33", "stable", "extended-stable", "extended-stable"],
])(
"rejects bootstrap preparation for %s",
@ -521,51 +492,10 @@ describe("prepared npm registry readback", () => {
return { bytes, packument, params };
}
it("adopts an already-published version only after exact byte and selector readback", async () => {
const { bytes, packument, params } = registryFixture();
const requests: string[] = [];
const result = await verifyPreparedNpmRegistry({
...params,
fetchImpl: async (input: string) => {
requests.push(input);
return input.endsWith(".tgz")
? new Response(new Uint8Array(bytes))
: Response.json(packument);
},
});
expect(result).toEqual({ alreadyPublished: true });
expect(requests).toHaveLength(2);
});
async function publishedFixture(beta: string) {
it("refuses an incomparable selector on a version this run did not publish", async () => {
const fixture = registryFixture(readFileSync((await packedPluginFixture()).tarballPath));
fixture.packument["dist-tags"].beta = beta;
return fixture;
}
it("passes a version this run did not publish once a later release owns its selector", async () => {
const { bytes, packument, params } = await publishedFixture("2026.9.2-beta.2");
let tarballReads = 0;
await expect(
verifyPublishedNpmRegistry({
...params,
fetchImpl: async (input: string) => {
if (input.endsWith(".tgz")) {
tarballReads += 1;
return new Response(new Uint8Array(bytes));
}
return Response.json(packument);
},
}),
).resolves.toEqual({ alreadyPublished: true, supersededBy: "2026.9.2-beta.2" });
expect(tarballReads).toBe(1);
});
it.each([
["lagging", "2026.9.1-beta.1"],
["incomparable", "not-a-version"],
])("still refuses a %s selector on a version this run did not publish", async (_label, beta) => {
const { bytes, packument, params } = await publishedFixture(beta);
const { bytes, packument, params } = fixture;
packument["dist-tags"].beta = "not-a-version";
await expect(
verifyPublishedNpmRegistry({
...params,
@ -661,7 +591,7 @@ describe("prepared npm registry readback", () => {
);
});
it.each(["0", "-1", "NaN", "1.5"])("rejects invalid readback timeout %s", async (timeout) => {
it.each(["0", "NaN", "1.5"])("rejects invalid readback timeout %s", async (timeout) => {
vi.stubEnv("OPENCLAW_NPM_READBACK_TIMEOUT_MS", timeout);
const { params, packument } = registryFixture();
await expect(

View file

@ -171,43 +171,18 @@ describe("scripts/plugin-release-pretag-pack-check.ts process-tree proof", () =>
expect(isProcessAlive(directPid)).toBe(true);
expect(isProcessAlive(descendantPid)).toBe(true);
const readyAt = Date.now();
let thrown: unknown;
try {
await withinTest(releaseAndWait(), signal);
} catch (error) {
thrown = error;
}
const elapsedMs = Date.now() - startedAt;
const completionMs = Date.now() - readyAt;
expect(thrown).toMatchObject({
await expect(withinTest(releaseAndWait(), signal)).rejects.toMatchObject({
code: "ETIMEDOUT",
message:
"plugin runtime build for @openclaw/demo-plugin timed out after 100ms: node --import tsx scripts/check-plugin-npm-runtime-builds.mts --package extensions/demo-plugin",
});
const elapsedMs = Date.now() - startedAt;
const completionMs = Date.now() - readyAt;
expect(elapsedMs).toBeGreaterThanOrEqual(timeoutMs * 0.75);
expect(completionMs).toBeLessThan(7_500);
expect(Number.isInteger(directPid) && directPid > 1).toBe(true);
expect(Number.isInteger(descendantPid) && descendantPid > 1).toBe(true);
// requireProcessTreeExit joins the exact tree before this command rejects.
expect(isProcessAlive(directPid)).toBe(false);
expect(isProcessAlive(descendantPid)).toBe(false);
const proof = {
timeoutCode: (thrown as { code?: string }).code,
elapsedMs,
completionMs,
completionBounded: completionMs < 7_500,
directExited: !isProcessAlive(directPid),
descendantExited: !isProcessAlive(descendantPid),
};
console.log(`pretag-caller-process-tree-proof ${JSON.stringify(proof)}`);
expect(proof).toMatchObject({
timeoutCode: "ETIMEDOUT",
completionBounded: true,
directExited: true,
descendantExited: true,
});
} finally {
await releaseAndWait().catch(() => {});
directPid ||= readPid(directPidFile);

View file

@ -1,8 +1,6 @@
import { readFileSync, statSync } from "node:fs";
import { dirname, relative, resolve } from "node:path";
import * as ts from "typescript/unstable/ast";
import { readFileSync } from "node:fs";
import { expect, it } from "vitest";
import { createNativeTypeScriptParser } from "../../scripts/lib/native-typescript.mts";
import { collectRuntimeImportClosure } from "../../scripts/lib/runtime-import-closure.mts";
const components = [
"scripts/pr",
@ -17,85 +15,14 @@ it.each([
])(
"retains %s and its relative ESM runtime dependencies in the wrapper inventory",
(entrypoint) => {
const root = process.cwd();
const pending = [entrypoint];
const visited = new Set<string>();
const missing = new Set<string>();
const parser = createNativeTypeScriptParser({ cwd: root });
try {
while (pending.length > 0) {
const file = pending.pop();
if (!file || visited.has(file)) {
continue;
}
visited.add(file);
if (
!components.some((component) => file === component || file.startsWith(`${component}/`))
) {
missing.add(file);
}
const absolute = resolve(root, file);
const source = parser.parseSourceFile(file, readFileSync(absolute, "utf8"));
const enqueue = (specifier: string) => {
if (!specifier.startsWith(".")) {
return;
}
const literal = resolve(dirname(absolute), specifier);
// Explicit runtime files win over adjacent declarations selected by TypeScript.
const target = [literal, literal.replace(/\.([cm]?)js$/, ".$1ts")].find((candidate) =>
statSync(candidate, { throwIfNoEntry: false })?.isFile(),
);
if (!target || /\.d\.[cm]?ts$/.test(target)) {
throw new Error(`Cannot resolve wrapper runtime dependency ${file}: ${specifier}`);
}
pending.push(relative(root, target).replaceAll("\\", "/"));
};
const visit = (node: ts.Node): void => {
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
const clause = node.importClause;
const bindings = clause?.namedBindings;
if (
!clause ||
(clause.phaseModifier !== ts.SyntaxKind.TypeKeyword &&
(clause.name ||
!bindings ||
ts.isNamespaceImport(bindings) ||
bindings.elements.some((element) => !element.isTypeOnly)))
) {
enqueue(node.moduleSpecifier.text);
}
return;
}
if (ts.isExportDeclaration(node)) {
const clause = node.exportClause;
if (
node.moduleSpecifier &&
ts.isStringLiteral(node.moduleSpecifier) &&
!node.isTypeOnly &&
(!clause ||
ts.isNamespaceExport(clause) ||
clause.elements.some((element) => !element.isTypeOnly))
) {
enqueue(node.moduleSpecifier.text);
}
return;
}
if (ts.isImportTypeNode(node)) {
return;
}
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
const argument = node.arguments[0];
if (argument && ts.isStringLiteralLikeNode(argument)) {
enqueue(argument.text);
}
}
node.forEachChild(visit);
};
visit(source);
}
} finally {
parser.close();
}
expect([...missing].toSorted()).toEqual([]);
const closure = collectRuntimeImportClosure(process.cwd(), [entrypoint], {
includeDynamicImports: true,
});
expect(
closure.filter(
(file) =>
!components.some((component) => file === component || file.startsWith(`${component}/`)),
),
).toEqual([]);
},
);

View file

@ -1,4 +1,4 @@
import { execFileSync, spawnSync } from "node:child_process";
import { execFileSync, spawnSync, type SpawnSyncReturns } from "node:child_process";
import {
copyFileSync,
cpSync,
@ -68,6 +68,27 @@ function commit(root: string, excluded: string[] = []) {
return { root, sha: git("rev-parse", "HEAD"), git };
}
function removeBlob(source: ReturnType<typeof commit>, path: string) {
const oid = source.git("rev-parse", `${source.sha}:${path}`);
rmSync(join(source.root, ".git/objects", oid.slice(0, 2), oid.slice(2)));
return oid;
}
function configureUnavailablePromisor(source: ReturnType<typeof commit>) {
source.git("config", "remote.origin.url", "fixture::unavailable");
source.git("config", "remote.origin.promisor", "true");
source.git("config", "extensions.partialClone", "origin");
source.git("config", "protocol.fixture.allow", "always");
}
function expectRejected(result: SpawnSyncReturns<string>, error?: string) {
expect(result.status, result.stderr).toBe(1);
if (error) {
expect(result.stderr).toContain(error);
}
expect(result.stdout).toBe("");
}
function fixture(
files: Record<string, string> = {},
parser = false,
@ -226,27 +247,24 @@ describe("frozen admission Docker consumer aliases", () => {
'import { writeFileSync } from "node:fs";',
'writeFileSync(`${process.env.HOME}/selected-code-executed`, "executed");',
].join("\n");
const consumerAliases = aliases.filter(({ lane }) =>
["live-gateway", "mcp-channels"].includes(lane),
);
it.each(aliases)(
it.each(consumerAliases)(
"rejects a missing committed $lane contract before emitting admission",
({ lane, path, current }) => {
const source = `${executionSentinel}\n${current}\n`;
const f = fixture({ [path]: source });
const tree = f.selected.git("rev-parse", "HEAD^{tree}");
const oid = f.selected.git("rev-parse", `${f.selected.sha}:${path}`);
f.selected.git("config", "remote.origin.url", "fixture::unavailable");
f.selected.git("config", "remote.origin.promisor", "true");
f.selected.git("config", "extensions.partialClone", "origin");
f.selected.git("config", "protocol.fixture.allow", "always");
rmSync(join(f.selected.root, ".git/objects", oid.slice(0, 2), oid.slice(2)));
configureUnavailablePromisor(f.selected);
removeBlob(f.selected, path);
expect(f.selected.git("rev-parse", "HEAD^{tree}")).toBe(tree);
expect(readFileSync(join(f.selected.root, path), "utf8")).toBe(source);
const result = f.run({ docker: { lanes: [lane] } });
expect(existsSync(join(f.root, "selected-code-executed"))).toBe(false);
expect(result.status, result.stderr).toBe(1);
expect(result.stderr).toContain("unable to read selected source");
expect(result.stdout).toBe("");
expectRejected(result, "unable to read selected source");
const currentOnly = f.run(
{ docker: { lanes: [lane] } },
@ -258,11 +276,10 @@ describe("frozen admission Docker consumer aliases", () => {
},
);
it.each(
aliases.flatMap((alias) =>
(["current", "legacy"] as const).map((dialect) => Object.assign({}, alias, { dialect })),
),
)("admits $lane with the committed $dialect contract", (alias) => {
it.each([
...aliases.map((alias) => Object.assign({}, alias, { dialect: "current" as const })),
...consumerAliases.map((alias) => Object.assign({}, alias, { dialect: "legacy" as const })),
])("admits $lane with the committed $dialect contract", (alias) => {
const { lane, consumer, path, mode, dialect } = alias;
const f = fixture({ [path]: `${executionSentinel}\n${alias[dialect]}\n` });
const result = f.run({ docker: { lanes: [lane] } });
@ -290,7 +307,7 @@ describe("frozen admission Docker consumer aliases", () => {
expect(existsSync(join(f.tooling.root, "node_modules"))).toBe(false);
});
it.each(aliases)(
it.each(consumerAliases)(
"preserves the legitimate absent-file fallback for $lane",
({ lane, consumer, mode }) => {
const f = fixture();
@ -350,8 +367,6 @@ describe("frozen admission Docker consumer aliases", () => {
it.each([
{ lane: "live-gateway", removed: [pluginAssertions], consumer: "live-cli-backend" },
{ lane: "mcp-channels", removed: [cliMetadata], consumer: "plugins" },
{ lane: "kitchen-sink-rpc", removed: [cliMetadata], consumer: "plugins" },
{ lane: "plugins-offline", removed: [cliMetadata], consumer: "plugins" },
{ lane: "docker-package-install", removed: [cliMetadata, pluginAssertions], consumer: null },
])("keeps unreadable unrelated contracts inert for $lane", ({ lane, removed, consumer }) => {
const f = fixture({
@ -359,8 +374,7 @@ describe("frozen admission Docker consumer aliases", () => {
[pluginAssertions]: `${executionSentinel}\nexport function assertPluginUninstallConfigState() {}\n`,
});
for (const path of removed) {
const oid = f.selected.git("rev-parse", `${f.selected.sha}:${path}`);
rmSync(join(f.selected.root, ".git/objects", oid.slice(0, 2), oid.slice(2)));
removeBlob(f.selected, path);
}
const result = f.run({ docker: { lanes: [lane] } });
expect(result.status, result.stderr).toBe(0);
@ -380,32 +394,29 @@ describe("frozen admission upgrade Docker aliases", () => {
const companion = "scripts/e2e/lib/plugin-index-sqlite.mjs";
const pluginAssertions = "scripts/e2e/lib/plugins/assertions.mjs";
it.each(
lanes.flatMap((lane) =>
[
{
shape: "malformed version",
version: "invalid",
error: "selected upgrade target has an invalid release version",
},
{
shape: "unsupported correction",
version: "2026.7.33-1",
error: "unsupported extended-stable correction",
},
{
shape: "missing scenario",
version: "2026.7.33",
error: "selected extended-stable target lacks its scenario",
},
{
shape: "missing companion blob",
version: "2026.7.33",
error: "unable to read selected source",
},
].map((value) => Object.assign({}, value, { lane })),
),
)("rejects $lane with $shape before emitting admission", ({ lane, shape, version, error }) => {
it.each([
{
shape: "malformed version",
version: "invalid",
error: "selected upgrade target has an invalid release version",
},
{
shape: "unsupported correction",
version: "2026.7.33-1",
error: "unsupported extended-stable correction",
},
{
shape: "missing scenario",
version: "2026.7.33",
error: "selected extended-stable target lacks its scenario",
},
{
shape: "missing companion blob",
version: "2026.7.33",
error: "unable to read selected source",
},
])("rejects an upgrade alias with $shape before admission", ({ shape, version, error }) => {
const lane = "root-managed-vps-upgrade";
const files =
shape === "missing companion blob"
? survivorFiles(version)
@ -417,18 +428,13 @@ describe("frozen admission upgrade Docker aliases", () => {
for (const path of Object.keys(files).filter((file) => file !== companion)) {
expect(f.selected.git("rev-parse", `${f.selected.sha}:${path}`), path).not.toBe(oid);
}
f.selected.git("config", "remote.origin.url", "fixture::unavailable");
f.selected.git("config", "remote.origin.promisor", "true");
f.selected.git("config", "extensions.partialClone", "origin");
f.selected.git("config", "protocol.fixture.allow", "always");
rmSync(join(f.selected.root, ".git/objects", oid.slice(0, 2), oid.slice(2)));
configureUnavailablePromisor(f.selected);
removeBlob(f.selected, companion);
expect(f.selected.git("rev-parse", "HEAD^{tree}")).toBe(tree);
expect(readFileSync(join(f.selected.root, companion), "utf8")).toBe(files[companion]);
}
const result = f.run({ docker: { lanes: [lane] } });
expect(result.status, result.stderr).toBe(1);
expect(result.stderr).toContain(error);
expect(result.stdout).toBe("");
expectRejected(result, error);
for (const root of [f.root, f.selected.root, f.tooling.root]) {
expect(existsSync(join(root, "selected-code-executed"))).toBe(false);
}
@ -440,15 +446,14 @@ describe("frozen admission upgrade Docker aliases", () => {
expect(JSON.parse(currentOnly.stdout).sources.selected).toEqual([]);
});
it.each(
lanes.flatMap((lane) =>
[
{ version: "2026.6.35", recipe: "config-recipe.mjs", train: "extended-stable" },
{ version: "2026.7.33", recipe: "config-recipe.mts", train: "extended-stable" },
{ version: "2026.9.9", recipe: "", train: "stable" },
].map((value) => Object.assign({}, value, { lane })),
),
)("admits $lane with committed $version contracts", ({ lane, version, recipe, train }) => {
it.each([
...[
{ version: "2026.6.35", recipe: "config-recipe.mjs", train: "extended-stable" },
{ version: "2026.7.33", recipe: "config-recipe.mts", train: "extended-stable" },
{ version: "2026.9.9", recipe: "", train: "stable" },
].map((value) => Object.assign({}, value, { lane: "root-managed-vps-upgrade" })),
{ lane: "update-restart-auth", version: "2026.9.9", recipe: "", train: "stable" },
])("admits $lane with committed $version contracts", ({ lane, version, recipe, train }) => {
const files = recipe
? survivorFiles(version, recipe)
: { "package.json": JSON.stringify({ type: "module", version }) };
@ -456,8 +461,7 @@ describe("frozen admission upgrade Docker aliases", () => {
...files,
[pluginAssertions]: "throw new Error('unselected plugin code executed');",
});
const oid = f.selected.git("rev-parse", `${f.selected.sha}:${pluginAssertions}`);
rmSync(join(f.selected.root, ".git/objects", oid.slice(0, 2), oid.slice(2)));
removeBlob(f.selected, pluginAssertions);
const result = f.run({ docker: { lanes: [lane] } });
expect(result.status, result.stderr).toBe(0);
const record = JSON.parse(result.stdout);
@ -498,43 +502,38 @@ describe("frozen admission upgrade Docker aliases", () => {
]);
});
it.each([
"plugins-offline",
"docker-package-install",
"live-cli-backend-claude",
"live-cli-backend-gemini",
"update-first-hop-compat",
"release-user-journey",
"release-upgrade-user-journey",
])("keeps unselected upgrade contracts inert for %s", (lane) => {
const files: Record<string, string> = {
"package.json": '{"type":"module","version":"invalid"}',
"src/infra/clawhub-install-trust.ts": "throw new Error('unselected upgrade code executed');",
"scripts/print-cli-backend-live-metadata.ts":
"throw new Error('unselected CLI code executed');",
};
if (lane === "update-first-hop-compat") {
files["scripts/runtime-postbuild.mts"] =
`throw new Error("selected postbuild executed");\n${readFileSync("scripts/runtime-postbuild.mts", "utf8")}`;
}
const f = fixture(files);
for (const path of [
"src/infra/clawhub-install-trust.ts",
"scripts/print-cli-backend-live-metadata.ts",
]) {
const oid = f.selected.git("rev-parse", `${f.selected.sha}:${path}`);
rmSync(join(f.selected.root, ".git/objects", oid.slice(0, 2), oid.slice(2)));
}
const requestedLanes = expandUpdateFirstHopCompatLanes([lane]);
const result = f.run({ docker: { lanes: requestedLanes } });
expect(result.status, result.stderr).toBe(0);
const record = JSON.parse(result.stdout);
expect(record.docker).toEqual({ lanes: requestedLanes, omitted: [], status: "ADMITTED" });
expect(record.selection.consumers).toEqual(lane === "plugins-offline" ? ["plugins"] : []);
expect(record.contracts.map((contract: { consumer: string }) => contract.consumer)).toEqual(
record.selection.consumers,
);
});
it.each(["plugins-offline", "docker-package-install", "update-first-hop-compat"])(
"keeps unselected upgrade contracts inert for %s",
(lane) => {
const files: Record<string, string> = {
"package.json": '{"type":"module","version":"invalid"}',
"src/infra/clawhub-install-trust.ts":
"throw new Error('unselected upgrade code executed');",
"scripts/print-cli-backend-live-metadata.ts":
"throw new Error('unselected CLI code executed');",
};
if (lane === "update-first-hop-compat") {
files["scripts/runtime-postbuild.mts"] =
`throw new Error("selected postbuild executed");\n${readFileSync("scripts/runtime-postbuild.mts", "utf8")}`;
}
const f = fixture(files);
for (const path of [
"src/infra/clawhub-install-trust.ts",
"scripts/print-cli-backend-live-metadata.ts",
]) {
removeBlob(f.selected, path);
}
const requestedLanes = expandUpdateFirstHopCompatLanes([lane]);
const result = f.run({ docker: { lanes: requestedLanes } });
expect(result.status, result.stderr).toBe(0);
const record = JSON.parse(result.stdout);
expect(record.docker).toEqual({ lanes: requestedLanes, omitted: [], status: "ADMITTED" });
expect(record.selection.consumers).toEqual(lane === "plugins-offline" ? ["plugins"] : []);
expect(record.contracts.map((contract: { consumer: string }) => contract.consumer)).toEqual(
record.selection.consumers,
);
},
);
});
describe("frozen admission bootstrap repairs", () => {
@ -542,49 +541,35 @@ describe("frozen admission bootstrap repairs", () => {
const reader = "scripts/lib/frozen-target-source.mjs";
const shell = "scripts/lib/frozen-target-compat.sh";
it.each([
reader,
"scripts/lib/docker-e2e-scenarios.mts",
"scripts/lib/record-shared.mjs",
shell,
"scripts/lib/trusted-native-typescript.mjs",
"scripts/lib/native-typescript.mts",
])("rejects dirty executable %s before any dependent code runs at unchanged HEAD", (path) => {
const f = fixture({ "src/config/zod-schema.ts": "lastRunAt:" });
const sentinel = join(f.root, "dependent-code-executed");
const file = join(f.tooling.root, path);
const payload =
path === shell
? `\nprintf executed > '${sentinel}'\n`
: `\n(await import("node:fs")).writeFileSync(${JSON.stringify(sentinel)}, "executed");\n`;
writeFileSync(file, readFileSync(file, "utf8") + payload);
expect(f.tooling.git("rev-parse", "HEAD")).toBe(f.tooling.sha);
const result = f.run({ consumers: ["onboard"] });
expect(existsSync(sentinel), result.stderr).toBe(false);
expect(result.status, result.stderr).toBe(1);
expect(result.stderr).toContain(`tooling closure does not match committed source: ${path}`);
expect(result.stdout).toBe("");
});
it.each([reader, "scripts/lib/docker-e2e-scenarios.mts", shell])(
"rejects dirty executable %s before any dependent code runs at unchanged HEAD",
(path) => {
const f = fixture({ "src/config/zod-schema.ts": "lastRunAt:" });
const sentinel = join(f.root, "dependent-code-executed");
const file = join(f.tooling.root, path);
const payload =
path === shell
? `\nprintf executed > '${sentinel}'\n`
: `\n(await import("node:fs")).writeFileSync(${JSON.stringify(sentinel)}, "executed");\n`;
writeFileSync(file, readFileSync(file, "utf8") + payload);
expect(f.tooling.git("rev-parse", "HEAD")).toBe(f.tooling.sha);
const result = f.run({ consumers: ["onboard"] });
expect(existsSync(sentinel), result.stderr).toBe(false);
expectRejected(result, `tooling closure does not match committed source: ${path}`);
},
);
it.each([
entrypoint,
"scripts/lib/official-external-channel-catalog.json",
"scripts/lib/official-external-provider-catalog.json",
"scripts/lib/record-shared.mjs",
"scripts/lib/upgrade-survivor-scenarios.json",
`${recipeDirectory}/agents.json`,
"package.json",
"pnpm-lock.yaml",
])("rejects dirty closure data %s at unchanged HEAD", (path) => {
const f = fixture();
const file = join(f.tooling.root, path);
writeFileSync(file, readFileSync(file, "utf8") + "\n");
expect(f.tooling.git("rev-parse", "HEAD")).toBe(f.tooling.sha);
const result = f.run({});
expect(result.status, result.stderr).toBe(1);
expect(result.stderr).toContain(`tooling closure does not match committed source: ${path}`);
expect(result.stdout).toBe("");
});
it.each([entrypoint, `${recipeDirectory}/agents.json`])(
"rejects dirty closure data %s at unchanged HEAD",
(path) => {
const f = fixture();
const file = join(f.tooling.root, path);
writeFileSync(file, readFileSync(file, "utf8") + "\n");
expect(f.tooling.git("rev-parse", "HEAD")).toBe(f.tooling.sha);
const result = f.run({});
expectRejected(result, `tooling closure does not match committed source: ${path}`);
},
);
it.each(["file", "parent directory"] as const)(
"rejects a tooling %s symlink even when its bytes match",
@ -597,9 +582,7 @@ describe("frozen admission bootstrap repairs", () => {
rmSync(original, { recursive: true });
symlinkSync(outside, original);
const result = f.run({});
expect(result.status, result.stderr).toBe(1);
expect(result.stderr).toContain("tooling closure requires an owned regular file:");
expect(result.stdout).toBe("");
expectRejected(result, "tooling closure requires an owned regular file:");
},
);
@ -635,22 +618,18 @@ describe("frozen admission bootstrap repairs", () => {
const f = fixture();
f.tooling.git("commit", "--allow-empty", "-qm", "different HEAD");
const result = f.run({});
expect(result.status, result.stderr).toBe(1);
expect(result.stderr).toContain("checkout does not match OPENCLAW_SELECTED_SHA");
expect(result.stdout).toBe("");
expectRejected(result, "checkout does not match OPENCLAW_SELECTED_SHA");
});
it.each([entrypoint, reader, `${recipeDirectory}/agents.json`])(
it.each([entrypoint, `${recipeDirectory}/agents.json`])(
"rejects a missing committed tooling object %s without hydration",
(path) => {
const f = fixture();
const oid = f.tooling.git("rev-parse", `${f.tooling.sha}:${path}`);
f.tooling.git("config", "remote.origin.url", "fixture::unavailable");
f.tooling.git("config", "remote.origin.promisor", "true");
rmSync(join(f.tooling.root, ".git/objects", oid.slice(0, 2), oid.slice(2)));
removeBlob(f.tooling, path);
const result = f.run({});
expect(result.status, result.stderr).toBe(1);
expect(result.stdout).toBe("");
expectRejected(result);
},
);
@ -752,12 +731,6 @@ describe("frozen admission entry", () => {
allow: true,
mode: "unsupported",
},
{
name: "current authorized",
files: { "src/config/zod-schema.session-config.ts": "coldStorage: z.object({})" },
allow: true,
mode: "required",
},
{
name: "current declaration regression",
files: {
@ -857,9 +830,7 @@ describe("frozen admission entry", () => {
expect(existsSync(join(f.tooling.root, "node_modules"))).toBe(false);
expect(f.run(selection).stdout).toBe(first.stdout);
const invalid = f.run({ docker: { lanes: ["onboard", "session-runtime-context"] } });
expect(invalid.status).toBe(1);
expect(invalid.stderr).toContain("unable to resolve frozen runtime-context");
expect(invalid.stdout).toBe("");
expectRejected(invalid, "unable to resolve frozen runtime-context");
});
it.each(["June", "July"])(
@ -891,9 +862,7 @@ describe("frozen admission entry", () => {
});
rmSync(join(f.tooling.root, "node_modules"), { recursive: true });
const rejected = f.run({ consumers: ["agent-bundle-mcp-tools"] });
expect(rejected.status).toBe(1);
expect(rejected.stderr).toContain("trusted TypeScript parser");
expect(rejected.stdout).toBe("");
expectRejected(rejected, "trusted TypeScript parser");
expect(f.run({ consumers: ["onboard"] }).status).toBe(0);
},
);
@ -913,12 +882,8 @@ describe("frozen admission entry", () => {
},
);
if (shape === "deleted blob") {
const oid = f.selected.git("rev-parse", `${f.selected.sha}:${relative}`);
f.selected.git("config", "remote.origin.url", "fixture::unavailable");
f.selected.git("config", "remote.origin.promisor", "true");
f.selected.git("config", "extensions.partialClone", "origin");
f.selected.git("config", "protocol.fixture.allow", "always");
rmSync(join(f.selected.root, ".git/objects", oid.slice(0, 2), oid.slice(2)));
configureUnavailablePromisor(f.selected);
removeBlob(f.selected, relative);
} else if (shape === "dirty absent metadata") {
mkdirSync(dirname(join(f.selected.root, relative)), { recursive: true });
writeFileSync(join(f.selected.root, relative), "dirty supported decoy");
@ -939,11 +904,10 @@ describe("frozen admission entry", () => {
status: "NOT RUN",
});
} else {
expect(result.status).toBe(1);
expect(result.stderr).toContain(
expectRejected(
result,
shape === "unknown catalog" ? "inert scenario catalog" : "unable to read selected source",
);
expect(result.stdout).toBe("");
}
},
);
@ -951,7 +915,6 @@ describe("frozen admission entry", () => {
it.each([
{ scenarios: "acpx-openclaw-tools-bridge", allow: true, supported: false },
{ scenarios: "base acpx-openclaw-tools-bridge", allow: true, supported: true },
{ scenarios: "acpx-openclaw-tools-bridge", allow: false, supported: false },
{ scenarios: "base acpx-openclaw-tools-bridge", allow: false, supported: true },
])(
"preserves inert-only survivor coverage for $scenarios with omissions $allow",
@ -987,9 +950,7 @@ describe("frozen admission entry", () => {
{ allowFrozenTargetScenarioOmissions: allow },
);
if (!allow) {
expect(result.status, result.stderr).toBe(1);
expect(result.stderr).toContain("require authorized scenario omissions");
expect(result.stdout).toBe("");
expectRejected(result, "require authorized scenario omissions");
return;
}
expect(result.status, result.stderr).toBe(0);
@ -1061,12 +1022,9 @@ describe("frozen admission entry", () => {
{ consumer: "live-codex-harness-gpt56-sol-docker", status: "NOT RUN" },
{ consumer: "fs-safe-native", mode: "not-applicable" },
]);
const oid = f.selected.git("rev-parse", `${f.selected.sha}:${catalog}`);
rmSync(join(f.selected.root, ".git/objects", oid.slice(0, 2), oid.slice(2)));
removeBlob(f.selected, catalog);
const rejected = f.run(selection);
expect(rejected.status).toBe(1);
expect(rejected.stderr).toContain("unable to read selected source");
expect(rejected.stdout).toBe("");
expectRejected(rejected, "unable to read selected source");
expect(f.run(selection, { allowFrozenTargetScenarioOmissions: false }).status).toBe(0);
});
@ -1090,9 +1048,7 @@ describe("frozen admission entry", () => {
);
const result = f.run({ fsSafeNative: true });
if (requiresDefaults) {
expect(result.status).toBe(1);
expect(result.stderr).toContain("missing fs-safe defaults source");
expect(result.stdout).toBe("");
expectRejected(result, "missing fs-safe defaults source");
} else {
expect(result.status, result.stderr).toBe(0);
expect(JSON.parse(result.stdout).contracts).toEqual([
@ -1186,23 +1142,15 @@ describe("frozen admission entry", () => {
if (consumer === "kitchen-sink-plugin") {
expect(args).toContain("OPENCLAW_FROZEN_TARGET_PLUGIN_UNINSTALL_MODE=legacy");
expect(contract.modes.OPENCLAW_FROZEN_TARGET_PLUGIN_UNINSTALL_MODE).toBe("legacy");
const capabilityOid = f.selected.git("rev-parse", `${f.selected.sha}:${pluginAssertions}`);
rmSync(
join(f.selected.root, ".git/objects", capabilityOid.slice(0, 2), capabilityOid.slice(2)),
);
const capabilityOid = removeBlob(f.selected, pluginAssertions);
const unreadable = f.run({ consumers: [consumer] });
expect(unreadable.status).toBe(1);
expect(unreadable.stderr).toContain("unable to read selected source");
expect(unreadable.stdout).toBe("");
expectRejected(unreadable, "unable to read selected source");
expect(f.run({ consumers: ["onboard"] }).status).toBe(0);
expect(f.selected.git("hash-object", "-w", pluginAssertions)).toBe(capabilityOid);
}
const oid = f.selected.git("rev-parse", `${f.selected.sha}:${files[0]}`);
rmSync(join(f.selected.root, ".git/objects", oid.slice(0, 2), oid.slice(2)));
removeBlob(f.selected, files[0]!);
const rejected = f.run({ consumers: [consumer] });
expect(rejected.status).toBe(1);
expect(rejected.stderr).toContain("unable to read selected source");
expect(rejected.stdout).toBe("");
expectRejected(rejected, "unable to read selected source");
});
it.each(["absent", "missing object"])(
@ -1215,54 +1163,30 @@ describe("frozen admission entry", () => {
true,
);
if (shape === "missing object") {
const oid = f.selected.git("rev-parse", `${f.selected.sha}:${manifest}`);
rmSync(join(f.selected.root, ".git/objects", oid.slice(0, 2), oid.slice(2)));
removeBlob(f.selected, manifest);
}
for (const allow of [true, false]) {
const result = f.run(
{ consumers: ["codex-on-demand"] },
{ allowFrozenTargetScenarioOmissions: allow },
);
expect(result.status, result.stderr).toBe(1);
expect(result.stderr).toContain(
expectRejected(
result,
shape === "absent" ? "missing required contract file" : "unable to read selected source",
);
expect(result.stdout).toBe("");
}
expect(f.run({ consumers: ["onboard"] }).status).toBe(0);
},
);
it.each(["missing run", "missing imported data", "missing companion blob", "complete"])(
"checks the selected survivor directory closure: %s",
(shape) => {
const dir = "scripts/e2e/lib/upgrade-survivor";
it.each(["run.sh", "config-recipe/models-openai.json"])(
"rejects the selected survivor directory missing %s",
(path) => {
const files = survivorFiles();
const companion = "scripts/e2e/lib/plugin-index-sqlite.mjs";
if (shape === "missing run") {
delete files[`${dir}/run.sh`];
}
if (shape === "missing imported data") {
delete files[`${dir}/config-recipe/models-openai.json`];
}
delete files[`scripts/e2e/lib/upgrade-survivor/${path}`];
const f = fixture(files);
if (shape === "missing companion blob") {
const oid = f.selected.git("rev-parse", `${f.selected.sha}:${companion}`);
rmSync(join(f.selected.root, ".git/objects", oid.slice(0, 2), oid.slice(2)));
}
const result = f.run({ consumers: ["upgrade-survivor"] });
if (shape === "complete") {
expect(result.status, result.stderr).toBe(0);
expect(JSON.parse(result.stdout).contracts[0].modes.releaseTrain).toBe("extended-stable");
} else {
expect(result.status).toBe(1);
expect(result.stderr).toContain(
shape === "missing companion blob"
? "unable to read selected source"
: "missing required contract file",
);
expect(result.stdout).toBe("");
}
expectRejected(result, "missing required contract file");
},
);
@ -1276,7 +1200,6 @@ describe("frozen admission entry", () => {
])("rejects malformed or widened admission input %#", (overrides) => {
const f = fixture();
const result = f.run({}, overrides);
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
expectRejected(result);
});
});

View file

@ -101,10 +101,7 @@ function fixture(layout: "named" | "direct" = "named") {
}
describe("QA profile failure diagnostics", () => {
it.each([
{ label: "one of two planned shards", matrix: plan, missing: ["shard-01"] },
{ label: "one planned shard", matrix: { include: [plan.include[1]] }, missing: [] },
])("retains a directly extracted survivor with $label", ({ matrix, missing }) => {
it("retains a directly extracted survivor when another planned shard is missing", () => {
const f = fixture("direct");
const statusPath = f.writeShard(1, {
...f.status(1),
@ -129,10 +126,7 @@ describe("QA profile failure diagnostics", () => {
...payloadFiles.map(([relativePath]) => path.join(f.input, relativePath)),
];
const originalInputs = inputPaths.map((filePath) => readFileSync(filePath));
const { result } = f.collect({
PLAN_MATRIX_JSON: JSON.stringify(matrix),
SHARD_COUNT: String(matrix.include.length),
});
const { result } = f.collect();
expect(result.shards).toEqual([
{
id: "shard-02",
@ -148,8 +142,8 @@ describe("QA profile failure diagnostics", () => {
stages: { AGGREGATE_OUTCOME: "failure", FINALIZE_OUTCOME: "skipped" },
statusFiles: 1,
evidenceFiles: 1,
missingStatuses: missing,
missingEvidence: missing,
missingStatuses: ["shard-01"],
missingEvidence: ["shard-01"],
issues: [],
});
expect(inputPaths.map((filePath) => readFileSync(filePath))).toEqual(originalInputs);
@ -289,7 +283,7 @@ describe("QA profile failure diagnostics", () => {
},
);
it.skipIf(process.platform === "win32").each([null, {}, [], "invalid", 1, true])(
it.skipIf(process.platform === "win32").each([null, {}, [], "invalid"])(
"preserves the previous jq admission for shard shape %#",
(shard) => {
const f = fixture();
@ -331,7 +325,6 @@ describe("QA profile failure diagnostics", () => {
it.each([
[0, false, "none"],
[1, false, "none"],
[124, false, "none"],
[137, false, "none"],
[124, true, "term"],
@ -353,7 +346,7 @@ describe("QA profile failure diagnostics", () => {
},
);
it.skipIf(process.platform === "win32").each(["0", "1", "124", "137"])(
it.skipIf(process.platform === "win32").each(["0", "137"])(
"does not change the existing allow_failures decision for exit %s",
(code) => {
for (const allowFailures of ["false", "true"]) {
@ -370,14 +363,13 @@ describe("QA profile failure diagnostics", () => {
},
);
it.each(
(["named", "direct"] as const).flatMap((layout) =>
['{"untrusted-status-sentinel":', "null", "[]", "x".repeat(65 * 1024)].map((payload) => ({
layout,
payload,
})),
),
)("bounds malformed $layout status input %#", ({ layout, payload }) => {
it.each([
{ layout: "named", payload: '{"untrusted-status-sentinel":' },
{ layout: "named", payload: "null" },
{ layout: "named", payload: "[]" },
{ layout: "named", payload: "x".repeat(65 * 1024) },
{ layout: "direct", payload: '{"untrusted-status-sentinel":' },
] as const)("bounds malformed $layout status input %#", ({ layout, payload }) => {
const f = fixture(layout);
const source = f.writeShard(0, payload);
const original = readFileSync(source);

View file

@ -9,7 +9,6 @@ import {
awaitParentAuthorization,
createReleaseApprovalReceipt,
downloadReleaseApprovalReceipt,
releaseApprovalArtifactName,
validateReleaseApprovalReceipt,
verifyReleaseApprovalReceipt,
} from "../../scripts/release-approval-receipt.mjs";
@ -131,21 +130,13 @@ describe("release approval receipt", () => {
];
});
expect(receipt).toEqual(fixture().receipt);
expect(Object.keys(receipt)).toEqual(Object.keys(fixture().receipt));
expect(releaseApprovalArtifactName({ parentRunId: "10", parentRunAttempt: "2" })).toBe(
"openclaw-release-approval-v1-10-2",
);
expect(validateReleaseApprovalReceipt(receipt)).toBe(receipt);
});
it.each(["github-actions[bot]", "reviewer[BoT]", "", " "])(
"rejects an invalid last approver %j",
(login) => {
expect(() => createReleaseApprovalReceipt(env, () => [approved(), approved(login)])).toThrow(
/human login/u,
);
},
);
it.each(["reviewer[BoT]", " "])("rejects an invalid last approver %j", (login) => {
expect(() => createReleaseApprovalReceipt(env, () => [approved(), approved(login)])).toThrow(
/human login/u,
);
});
it("rejects approval histories without an approved npm-release entry", () => {
expect(() =>
@ -188,11 +179,6 @@ describe("release approval receipt", () => {
);
});
it("verifies approval while the parent publish job is still running", () => {
const input = fixture();
expect(verifyReleaseApprovalReceipt(input)).toBe(input.receipt);
});
it("rejects extra receipt keys", () => {
const input = fixture();
expect(() =>
@ -394,10 +380,8 @@ describe("release approval artifact download", () => {
);
});
describe.each([
["ClawHub", "openclaw-clawhub-parent-authorization-v2-10-2-30-1", false],
["npm", "openclaw-release-approval-v1-10-2", true],
])("%s parent authorization wait", (_target, name, requireInProgress) => {
describe("parent authorization wait", () => {
const name = "openclaw-release-approval-v1-10-2";
const listing = `actions/runs/10/artifacts?name=${name}&per_page=100`;
const authorization = {
name,
@ -408,7 +392,7 @@ describe.each([
parentRunId: "10",
parentRunAttempt: "2",
expectedArtifactName: name,
requireInProgress,
requireInProgress: true,
toolingSha: "a".repeat(40),
sleep: async () => {},
};
@ -437,11 +421,11 @@ describe.each([
);
});
it.each(["success", "cancelled", "failure"])(
"requires a live npm parent even when its artifact exists (%s)",
async (conclusion) => {
const runGhJson = api([[authorization]], { status: "completed", conclusion });
const result = awaitParentAuthorization({ ...params, runGhJson });
it.each([true, false])(
"accepts detached authorization only when a live parent is not required (%s)",
async (requireInProgress) => {
const runGhJson = api([[authorization]], { status: "completed", conclusion: "failure" });
const result = awaitParentAuthorization({ ...params, runGhJson, requireInProgress });
if (requireInProgress) {
await expect(result).rejects.toThrow(/completed\//u);
} else {
@ -485,7 +469,6 @@ describe("parent authorization CLI", () => {
it.each([
["wait-npm-authorization", "in_progress", null, 0, "openclaw-release-approval-v1-10-2"],
["wait-npm-authorization", "completed", "success", 1, "openclaw-release-approval-v1-10-2"],
["wait-npm-authorization", "completed", "cancelled", 1, "openclaw-release-approval-v1-10-2"],
[
"wait-clawhub-authorization",
"completed",

View file

@ -2,7 +2,6 @@ import { readFileSync } from "node:fs";
import { runInNewContext } from "node:vm";
import { describe, expect, it } from "vitest";
import { parse } from "yaml";
import { validateReleasePublishParentRun } from "../../scripts/release-tooling-identity.mjs";
type WorkflowStep = {
name: string;
@ -137,14 +136,6 @@ describe("release approval workflow contracts", () => {
}
});
it.each([
["plugin-npm-release", "publish_plugins_npm"],
["plugin-npm-release", "trusted_publisher_preflight"],
["openclaw-npm-release", "publish_openclaw_npm"],
])("%s %s uses the npm trusted-publisher environment", (workflow, jobId) => {
expect(requireJob(workflow, jobId).environment).toBe("npm-publish");
});
it.each([
[
"openclaw-npm-release",
@ -297,36 +288,6 @@ describe("release approval workflow contracts", () => {
expect(policyFor("", bot)).toBe("active-or-failure");
}
const sha = "a".repeat(40);
const ref = `release-publish/${sha.slice(0, 12)}-123`;
const verifyParent = (status: string, conclusion: string | null) =>
validateReleasePublishParentRun({
identity: { ref, fullRef: `refs/tags/${ref}`, sha },
releasePublishFullRef: `refs/tags/${ref}`,
releasePublishParentStatePolicy: receiptPolicy,
releasePublishRef: ref,
releasePublishRunAttempt: "2",
releasePublishRunId: "67890",
repository: "openclaw/openclaw",
run: {
id: 67890,
run_attempt: 2,
repository: { full_name: "openclaw/openclaw" },
path: `.github/workflows/openclaw-release-publish.yml@refs/tags/${ref}`,
event: "workflow_dispatch",
head_branch: ref,
head_sha: sha,
status,
conclusion,
},
});
// The parent failed after the child's authorization wait: refuse before npm I/O.
expect(() => verifyParent("completed", "failure")).toThrow(
"release publish parent run state is not allowed by active",
);
expect(() => verifyParent("completed", "success")).toThrow();
expect(() => verifyParent("in_progress", null)).not.toThrow();
const script = publish.run;
const mutation =
workflow === "openclaw-npm-release"

View file

@ -48,7 +48,7 @@ describe("reviewed release channel waiver", () => {
expect(releaseWaivedIntegrationChannels({ ...approved, telegramWaiver: "" })).toEqual([]);
});
it.each(["qa-live-matrix", "qa-matrix", "matrix", "MATRIX", "qa-live-matrix,qa-live-buzz"])(
it.each(["qa-matrix", "MATRIX", "qa-live-buzz,qa-live-matrix"])(
"rejects explicit Matrix selection %s under the combined 9.5 declaration",
(liveSuiteFilter) => {
const inputs = { ...approved, liveSuiteFilter };

View file

@ -1,20 +1,17 @@
import { spawnSync } from "node:child_process";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
const roots: string[] = [];
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
const workflowRef = "release-publish/aaaaaaaaaaaa-123";
const workflowSha = "a".repeat(40);
const releaseTag = "v2026.9.5";
const repository = "openclaw/openclaw";
afterEach(() => {
for (const root of roots.splice(0)) {
rmSync(root, { recursive: true, force: true });
}
});
const dispatchCommand =
'require_clawhub_dispatch_available "$WORKFLOW_REF" "$WORKFLOW"\ndispatch_workflow_at_ref "$WORKFLOW_REF" "$PARENT_WORKFLOW_SHA" "$WORKFLOW" -f release_tag="$RELEASE_TAG"';
function fixture({
workflow = "plugin-clawhub-release.yml",
@ -29,9 +26,8 @@ function fixture({
cancellationFails = false,
publisherRunning = false,
} = {}) {
const root = mkdtempSync(join(tmpdir(), "release-clawhub-children-"));
const root = tempDirs.make("release-clawhub-children-");
const currentWorkflowRef = workflow === "plugin-clawhub-new.yml" ? "main" : workflowRef;
roots.push(root);
mkdirSync(join(root, "bin"));
const child = {
id: 91,
@ -108,7 +104,7 @@ if (args[0] === 'run' && args[1] === 'list') {
);
return {
root,
run(command: string) {
run(command = dispatchCommand) {
const result = spawnSync("bash", ["-c", `source "$HELPER_SCRIPT"\n${command}`], {
encoding: "utf8",
env: {
@ -146,9 +142,7 @@ describe("ClawHub child lifecycle", () => {
"reclaims an older failed parent's waiting %s before dispatch",
(workflow) => {
const f = fixture({ workflow });
const result = f.run(
'require_clawhub_dispatch_available "$WORKFLOW_REF" "$WORKFLOW"\ndispatch_workflow_at_ref "$WORKFLOW_REF" "$PARENT_WORKFLOW_SHA" "$WORKFLOW" -f release_tag="$RELEASE_TAG"',
);
const result = f.run();
expect(result.status, result.stderr).toBe(0);
const cancellation = result.calls.findIndex((args) => args[1] === "cancel");
const dispatch = result.calls.findIndex((args) =>
@ -167,9 +161,7 @@ describe("ClawHub child lifecycle", () => {
{ publisherRunning: true, label: "active publisher" },
{ cancellationFails: true, label: "failed cancellation" },
])("does not dispatch past $label", (options) => {
const result = fixture(options).run(
'require_clawhub_dispatch_available "$WORKFLOW_REF" "$WORKFLOW"\ndispatch_workflow_at_ref "$WORKFLOW_REF" "$PARENT_WORKFLOW_SHA" "$WORKFLOW" -f release_tag="$RELEASE_TAG"',
);
const result = fixture(options).run();
expect(result.status).not.toBe(0);
expect(result.calls.some((args) => args.some((arg) => arg.endsWith("/dispatches")))).toBe(
false,
@ -179,55 +171,38 @@ describe("ClawHub child lifecycle", () => {
}
});
it.each([
["plugin-clawhub-release.yml", false],
["plugin-clawhub-release.yml", true],
["plugin-clawhub-new.yml", false],
["plugin-clawhub-new.yml", true],
] as const)(
"leaves another tag's waiting %s alone (same tooling: %s)",
(workflow, sameToolingRef) => {
const result = fixture({ workflow, titleTag: "v2026.9.4", sameToolingRef }).run(
'require_clawhub_dispatch_available "$WORKFLOW_REF" "$WORKFLOW"\ndispatch_workflow_at_ref "$WORKFLOW_REF" "$PARENT_WORKFLOW_SHA" "$WORKFLOW" -f release_tag="$RELEASE_TAG"',
);
it.each(["plugin-clawhub-release.yml", "plugin-clawhub-new.yml"])(
"leaves another tag's waiting %s alone on the same tooling ref",
(workflow) => {
const result = fixture({ workflow, titleTag: "v2026.9.4", sameToolingRef: true }).run();
expect(result.status, result.stderr).toBe(0);
expect(result.calls.some((args) => args[1] === "cancel")).toBe(false);
},
);
it.each(["plugin-clawhub-release.yml", "plugin-clawhub-new.yml"])(
"leaves same-tag validation on the same tooling ref independent in %s",
(workflow) => {
const result = fixture({
workflow,
sameToolingRef: true,
validation: true,
childStatus: "in_progress",
}).run(
'require_clawhub_dispatch_available "$WORKFLOW_REF" "$WORKFLOW"\ndispatch_workflow_at_ref "$WORKFLOW_REF" "$PARENT_WORKFLOW_SHA" "$WORKFLOW" -f release_tag="$RELEASE_TAG"',
);
expect(result.status, result.stderr).toBe(0);
expect(result.calls.some((args) => args[1] === "cancel")).toBe(false);
},
);
it("leaves same-tag validation on the same tooling ref independent", () => {
const result = fixture({
sameToolingRef: true,
validation: true,
childStatus: "in_progress",
}).run();
expect(result.status, result.stderr).toBe(0);
expect(result.calls.some((args) => args[1] === "cancel")).toBe(false);
});
it("preserves bootstrap's existing independent slots for unidentified main runs", () => {
const result = fixture({
workflow: "plugin-clawhub-new.yml",
sameToolingRef: true,
legacyTitle: true,
}).run(
'require_clawhub_dispatch_available "$WORKFLOW_REF" "$WORKFLOW"\ndispatch_workflow_at_ref "$WORKFLOW_REF" "$PARENT_WORKFLOW_SHA" "$WORKFLOW" -f release_tag="$RELEASE_TAG"',
);
}).run();
expect(result.status, result.stderr).toBe(0);
expect(result.calls.some((args) => args[1] === "cancel")).toBe(false);
});
it("cleans up immediately recorded children after a later dispatch step fails", () => {
const f = fixture({ titleTag: "v2026.9.4" });
const dispatched = f.run(
'require_clawhub_dispatch_available "$WORKFLOW_REF" "$WORKFLOW"\ndispatch_workflow_at_ref "$WORKFLOW_REF" "$PARENT_WORKFLOW_SHA" "$WORKFLOW" -f release_tag="$RELEASE_TAG"\nexit 1',
);
const dispatched = f.run(`${dispatchCommand}\nexit 1`);
expect(dispatched.status).toBe(1);
expect(dispatched.savedEnv).toContain("=92");
const cleanup = f.run('source "$GITHUB_ENV"\ncleanup_clawhub_children');

View file

@ -81,7 +81,6 @@ describe("release evidence API reads", () => {
["GraphQL", ["api", "graphql"], "HTTP 502"],
["forbidden", ["api", "repos/openclaw/openclaw/actions/runs/42"], "HTTP 403"],
["rate limited", ["api", "repos/openclaw/openclaw/actions/runs/42"], "HTTP 429"],
["not found", ["api", "repos/openclaw/openclaw/actions/runs/42"], "HTTP 404"],
])("does not retry %s", (_label, args, stderr) => {
const wait = vi.spyOn(Atomics, "wait").mockReturnValue("timed-out");
const failure = Object.assign(new Error("gh failed"), { stderr });
@ -93,10 +92,10 @@ describe("release evidence API reads", () => {
expect(wait).not.toHaveBeenCalled();
});
it.each(["ECONNRESET", "ETIMEDOUT", "unexpected EOF"])("recovers a network error: %s", (code) => {
it("recovers a network error", () => {
vi.spyOn(Atomics, "wait").mockReturnValue("timed-out");
transport.read.mockImplementationOnce(() => {
throw Object.assign(new Error(code), { code });
throw Object.assign(new Error("connection failed"), { code: "ECONNRESET" });
});
transport.read.mockReturnValue('{"id":42}');
expect(createReleaseEvidenceClient("openclaw/openclaw").getRunAttempt("42", 1)).toEqual({

View file

@ -81,7 +81,6 @@ describe("release publish preflight operator interface", () => {
);
it.each([
{ tag: "v2026.9.5-beta.1", npmDistTag: "beta" },
{ tag: "v2026.9.5", npmDistTag: "beta" },
{ tag: "v2026.8.33", npmDistTag: "extended-stable" },
])("keeps $tag on $npmDistTag behind Docker before GitHub activation", (input) => {
@ -116,7 +115,7 @@ describe("release publish preflight operator interface", () => {
},
);
it.each(["maybe", "TRUE", "0"])(
it.each(["TRUE", "0"])(
"rejects ambiguous publish-openclaw-npm=%s before observation",
(value) => {
expect(() =>

View file

@ -13,6 +13,27 @@ import { createNestedGitEnv, writeJsonFile } from "../helpers/temp-repo.js";
const temps = useAutoCleanupTempDirTracker(afterEach);
const version = "2026.9.5";
const sourceSha = "a".repeat(40);
const githubOptions = {
repository: "openclaw/openclaw",
releaseTag: `v${version}`,
sourceSha,
npmDistTag: "latest",
};
const pluginOptions = {
version,
npmDistTag: "latest",
publishOpenclawNpm: false,
plugins: [
{
extensionId: "example",
packageDir: "extensions/example",
packageName: "@openclaw/example",
version,
channel: "stable",
publishTag: "latest",
},
],
} satisfies Parameters<typeof observeReleaseNpmState>[0];
afterEach(() => vi.unstubAllGlobals());
describe("release publication state", () => {
@ -30,21 +51,7 @@ describe("release publication state", () => {
}),
),
);
const result = await observeReleaseNpmState({
version,
npmDistTag: "latest",
publishOpenclawNpm: false,
plugins: [
{
extensionId: "example",
packageDir: "extensions/example",
packageName: "@openclaw/example",
version,
channel: "stable",
publishTag: "latest",
},
],
});
const result = await observeReleaseNpmState(pluginOptions);
expect(result.gates).toContainEqual(
expect.objectContaining({
id: "npm.package.@openclaw/example",
@ -77,21 +84,7 @@ describe("release publication state", () => {
}),
),
);
const result = await observeReleaseNpmState({
version,
npmDistTag: "latest",
publishOpenclawNpm: false,
plugins: [
{
extensionId: "example",
packageDir: "extensions/example",
packageName: "@openclaw/example",
version,
channel: "stable",
publishTag: "latest",
},
],
});
const result = await observeReleaseNpmState(pluginOptions);
expect(result.gates).toContainEqual(
expect.objectContaining({
id: "npm.package.@openclaw/example",
@ -261,10 +254,7 @@ function observeRuns(params: {
onProgress?: (message: string) => void;
}) {
return observeReleaseGitHubState({
repository: "openclaw/openclaw",
releaseTag: `v${version}`,
sourceSha,
npmDistTag: "latest",
...githubOptions,
onProgress: params.onProgress,
runGh: (args) => {
const endpoint = args[1];
@ -325,10 +315,7 @@ describe("release concurrency observations", () => {
return "[]";
});
const result = observeReleaseGitHubState({
repository: "openclaw/openclaw",
releaseTag: `v${version}`,
sourceSha,
npmDistTag: "latest",
...githubOptions,
budgetMs: 100,
now: () => now,
runGh,
@ -357,10 +344,7 @@ describe("release concurrency observations", () => {
it("retains a gh stderr failure and exact unread command without hiding known blockers", () => {
const result = observeReleaseGitHubState({
repository: "openclaw/openclaw",
releaseTag: `v${version}`,
sourceSha,
npmDistTag: "latest",
...githubOptions,
runGh(args) {
if (args[1]?.includes("/plugin-npm-release.yml/") && args[1].includes("status=queued")) {
return JSON.stringify({
@ -413,10 +397,7 @@ describe("release concurrency observations", () => {
: JSON.stringify({ total_count: 0, workflow_runs: [] }),
);
const result = observeReleaseGitHubState({
repository: "openclaw/openclaw",
releaseTag: `v${version}`,
sourceSha,
npmDistTag: "latest",
...githubOptions,
runGh,
});
expect(result.release).toMatchObject(release);
@ -425,10 +406,7 @@ describe("release concurrency observations", () => {
it("replays a failed filtered release-inventory read with its exact --jq filter", () => {
const result = observeReleaseGitHubState({
repository: "openclaw/openclaw",
releaseTag: `v${version}`,
sourceSha,
npmDistTag: "latest",
...githubOptions,
runGh(args) {
if (args[1]?.includes("/releases/tags/")) {
throw new Error("HTTP 404: Not Found");

View file

@ -392,7 +392,6 @@ describe("release:stable post-publication CLI", () => {
});
it.each([
{ label: "neither asset", names: [] },
{ label: "only the evidence", names: [`openclaw-${RELEASE}-stable-main-closeout.json`] },
{
label: "only the checksum",

View file

@ -320,8 +320,8 @@ describe("release:stable CLI", () => {
},
);
it.each(["unknown", "stableSoakWaiver", "laneWaiver"])(
"refuses retired or unknown state field %s without overwriting recovery evidence",
it.each(["stableSoakWaiver", "laneWaiver"])(
"refuses retired state field %s without overwriting recovery evidence",
(field) => {
const release = fixture();
const state = phaseState("validate");

View file

@ -23,20 +23,12 @@ describe("full parent publication verifier entrypoint", () => {
it.each([
"none",
"no-publish",
// Detailed fault matrices belong to plugin-npm-publication-readback.test.ts.
"missing-tarball",
"conflicting-bytes",
"missing-receipt",
"missing-planned-job",
"prior-missing-tarball",
"prior-conflicting-bytes",
"prior-archive-identity",
])("gates release success on qualified plugin readback: %s", async (fault) => {
const root = tempDirs.make("parent-publish-cli-");
const fixture = await createNpmPublicationReadbackFixture(
root,
fault.startsWith("prior-") ? "prior-deferred" : "direct",
fault.replace(/^prior-/u, ""),
);
const fixture = await createNpmPublicationReadbackFixture(root, "direct", fault);
const readback = await createPluginNpmPublicationReadback(fixture.options).catch(
() => undefined,
);
@ -147,15 +139,7 @@ globalThis.fetch = async (url) => {
).toMatchObject([{ packageName, verification: "published-registry" }]);
} else {
expect(result.stderr).toContain(
fault === "missing-planned-job"
? "planned candidate"
: fault === "missing-receipt"
? "Expected one consumed"
: fault.endsWith("missing-tarball")
? "HTTP 404"
: fault === "prior-archive-identity"
? "archive package identity"
: "bytes differ",
fault === "missing-planned-job" ? "planned candidate" : "HTTP 404",
);
}
});

View file

@ -161,6 +161,15 @@ globalThis.fetch = async (url, options = {}) => {
};
}
function reconcileStatuses(statuses: unknown[]) {
return evaluate({
eventName: "schedule",
responses: {
[`${prefix}/commits/${head}/statuses?per_page=100&page=1`]: { body: statuses },
},
});
}
describe("automatic security review event resolution", () => {
it("automatically resolves the current PR after a rate-limited lookup", () => {
const result = evaluate({
@ -377,20 +386,13 @@ describe("automatic security review event resolution", () => {
});
it.each([
{ action: "created", body: "/allow-security-sensitive-change" },
{
action: "created",
body: " \r\n /allow-dependencies-change \r\n/allow-security-sensitive-change\n",
},
{ action: "edited", body: "Removed", previousBody: "/allow-dependencies-change" },
{
action: "edited",
body: "> /allow-security-sensitive-change",
previousBody: "/allow-security-sensitive-change",
},
{ action: "edited", body: "/allow-dependencies-change", previousBody: "Thanks" },
{ action: "deleted", body: "/allow-security-sensitive-change" },
{ action: "deleted", body: "/allow-dependencies-change\n/allow-security-sensitive-change" },
])("reevaluates approval comment activity: %j", ({ action, body, previousBody }) => {
expect(
evaluate({
@ -411,15 +413,10 @@ describe("automatic security review event resolution", () => {
it.each([
{ action: "created", body: "Thanks" },
{ action: "edited", body: "Thanks again", previousBody: "Thanks" },
{ action: "deleted", body: "Thanks" },
{ action: "created", body: "Please post /allow-dependencies-change" },
{ action: "created", body: "> /allow-security-sensitive-change" },
{ action: "created", body: "```\n/allow-dependencies-change\n```" },
{ action: "created", body: "/allow-dependencies-change-extra" },
{ action: "created", body: "/allow-dependencies-change\nThanks" },
{ action: "edited", body: "Removed", previousBody: "Please post /allow-dependencies-change" },
{ action: "deleted", body: "> /allow-security-sensitive-change" },
{ action: "created", body: "/ALLOW-DEPENDENCIES-CHANGE" },
{ action: "created", body: " \r\n " },
{ action: "deleted", body: null },
@ -454,7 +451,7 @@ describe("automatic security review event resolution", () => {
});
});
it.each(["success", "failure", "cancelled"])(
it.each(["success", "failure"])(
"reevaluates %s CI completion against live workflow and head",
(conclusion) => {
const result = evaluate({ run: { conclusion } });
@ -619,29 +616,22 @@ describe("scheduled reconciliation", () => {
{ state: "pending", created_at: "2026-01-01T23:41:00Z" },
{ state: "success", created_at: "2026-01-01T23:30:00Z" },
])("ignores newer foreign successes before the newest Actions status: %j", (owned) => {
const result = evaluate({
eventName: "schedule",
responses: {
[`${prefix}/commits/${head}/statuses?per_page=100&page=1`]: {
body: [
{
...recordedPullRequest(42),
state: "success",
created_at: "2026-01-01T23:45:00Z",
creator: { login: "foreign-bot[bot]", type: "Bot" },
},
{
...recordedPullRequest(42),
state: "success",
created_at: "2026-01-01T23:44:00Z",
creator: { login: "github-actions[bot]", type: "User" },
},
{ ...recordedPullRequest(42), ...owned },
{ ...recordedPullRequest(42), state: "success", created_at: "2026-01-01T23:20:00Z" },
],
},
const result = reconcileStatuses([
{
...recordedPullRequest(42),
state: "success",
created_at: "2026-01-01T23:45:00Z",
creator: { login: "foreign-bot[bot]", type: "Bot" },
},
});
{
...recordedPullRequest(42),
state: "success",
created_at: "2026-01-01T23:44:00Z",
creator: { login: "github-actions[bot]", type: "User" },
},
{ ...recordedPullRequest(42), ...owned },
{ ...recordedPullRequest(42), state: "success", created_at: "2026-01-01T23:20:00Z" },
]);
expect(result.status, result.error).toBe(0);
expect(result.matrix).toEqual({ include: [{ pr: 42, head }] });
expect(result.published).toHaveLength(1);
@ -649,31 +639,17 @@ describe("scheduled reconciliation", () => {
it.each([
{ state: "success", created_at: "2026-01-01T23:41:00Z" },
{ state: "failure", created_at: "2026-01-01T23:41:00Z" },
{ state: "error", created_at: "2026-01-01T23:41:00Z" },
{ state: "success", created_at: completedRun.updated_at },
])("does not reselect a result settled at or after CI completion: %j", (status) => {
const result = evaluate({
eventName: "schedule",
responses: {
[`${prefix}/commits/${head}/statuses?per_page=100&page=1`]: {
body: [{ ...recordedPullRequest(42), context: "OpenClaw/CI-Gate", ...status }],
},
},
});
const result = reconcileStatuses([
{ ...recordedPullRequest(42), context: "OpenClaw/CI-Gate", ...status },
]);
expect(result).toMatchObject({ status: 0, matrix: { include: [] }, published: [] });
expect(result.requests.some(({ path }) => path.includes("/pulls/"))).toBe(false);
});
it.each([
{ state: "success", created_at: "2026-01-01T23:30:00Z" },
{ state: "failure", created_at: "2026-01-01T23:30:00Z" },
{ state: "error", created_at: "2026-01-01T23:30:00Z" },
{
state: "pending",
created_at: "2026-01-01T23:41:00Z",
description: "PR #42: Waiting for CI; review updates automatically",
},
{
state: "pending",
created_at: "2026-01-01T23:41:00Z",
@ -685,14 +661,7 @@ describe("scheduled reconciliation", () => {
description: "PR #42: CI and security review have not completed",
},
])("reselects a result older than the latest rerun or any pending status: %j", (status) => {
const result = evaluate({
eventName: "schedule",
responses: {
[`${prefix}/commits/${head}/statuses?per_page=100&page=1`]: {
body: [{ ...recordedPullRequest(42), ...status }],
},
},
});
const result = reconcileStatuses([{ ...recordedPullRequest(42), ...status }]);
expect(result.status, result.error).toBe(0);
expect(result.matrix).toEqual({ include: [{ pr: 42, head }] });
expect(result.published).toHaveLength(1);
@ -719,14 +688,7 @@ describe("scheduled reconciliation", () => {
);
it("selects a head with no ci-gate status", () => {
const result = evaluate({
eventName: "schedule",
responses: {
[`${prefix}/commits/${head}/statuses?per_page=100&page=1`]: {
body: [{ context: "unrelated/status", state: "success" }],
},
},
});
const result = reconcileStatuses([{ context: "unrelated/status", state: "success" }]);
expect(result).toMatchObject({ status: 0, matrix: { include: [{ pr: 42, head }] } });
expect(result.published).toHaveLength(1);
});
@ -965,7 +927,7 @@ describe("scheduled reconciliation", () => {
},
);
it.each([99, 100, 101])("selects at most 100 of %s stale candidates, oldest first", (count) => {
it.each([100, 101])("selects at most 100 of %s stale candidates, oldest first", (count) => {
const candidates = Array.from({ length: count }, (_, index) => ({
...completedRun,
id: index + 1,

View file

@ -53,7 +53,6 @@ if (args[0] === "backup" && args[1] === "create") {
execFileSync("tar", ["-xzf", archive, "-C", target]);
if (controls.mode === "lost-transcript") fs.unlinkSync(path.join(state, controls.transcriptRelative));
const db = new DatabaseSync(path.join(state, controls.agentRelative));
if (controls.mode === "missing-event") db.exec("DELETE FROM transcript_events WHERE seq = 2");
if (controls.mode === "changed-session") db.exec("UPDATE session_nodes SET current_session_id = 'different-session'");
if (controls.mode === "changed-payload") db.exec("UPDATE transcript_events SET event_json = '{}' WHERE seq = 1");
if (controls.mode === "changed-schema") db.exec("PRAGMA user_version = 21");
@ -163,6 +162,13 @@ function fixture(withTranscript = false) {
const beforeFile = join(artifactRoot, "schema-before.json");
const afterFile = join(artifactRoot, "schema-after.json");
const resultFile = join(artifactRoot, "backup-rollback.json");
const fileEvidence = (kind: string, relative: string) => ({
kind,
relative,
sha256: createHash("sha256")
.update(readFileSync(join(state, relative)))
.digest("hex"),
});
const schema = {
baselineVersion: "2026.9.4",
candidateVersion: "2026.9.5",
@ -173,36 +179,18 @@ function fixture(withTranscript = false) {
{ kind: "agent", relative: agentRelative, userVersion: 19, contentVersion: 19 },
],
agents: [
{ agentId: "main", databaseRelative: agentRelative, files: [] },
{
agentId: "main",
databaseRelative: agentRelative,
files: withTranscript ? [fileEvidence("transcript", transcriptRelative)] : [],
},
{
agentId: "ops",
databaseRelative: "agents/ops/agent/openclaw-agent.sqlite",
files: [
{
kind: "legacy-store",
relative: legacyRelative,
sha256: createHash("sha256")
.update(readFileSync(join(state, legacyRelative)))
.digest("hex"),
},
],
files: [fileEvidence("legacy-store", legacyRelative)],
},
],
};
if (withTranscript) {
const files = schema.agents[0]!.files as Array<{
kind: string;
relative: string;
sha256: string;
}>;
files.push({
kind: "transcript",
relative: transcriptRelative,
sha256: createHash("sha256")
.update(readFileSync(join(state, transcriptRelative)))
.digest("hex"),
});
}
writeJson(beforeFile, schema);
writeJson(afterFile, {
...schema,
@ -311,7 +299,6 @@ describe("published backup rollback proof", () => {
it.each([
"zero-omission",
"missing-history",
"changed-history",
"unqualified-path",
"unqualified-kind",
@ -321,12 +308,10 @@ describe("published backup rollback proof", () => {
if (mode === "zero-omission") {
f.mode(mode);
}
if (mode === "missing-history" || mode === "changed-history") {
if (mode === "changed-history") {
const db = new DatabaseSync(join(f.state, "agents/main/agent/openclaw-agent.sqlite"));
db.exec(
mode === "missing-history"
? "DELETE FROM transcript_events WHERE session_id = 'upgrade-restored-index-history'"
: "UPDATE transcript_events SET event_json = '{}' WHERE session_id = 'upgrade-restored-index-history' AND seq = 1",
"UPDATE transcript_events SET event_json = '{}' WHERE session_id = 'upgrade-restored-index-history' AND seq = 1",
);
db.close();
}
@ -368,7 +353,6 @@ describe("published backup rollback proof", () => {
});
it.each([
["missing-event", "restored baseline inventory differs"],
["changed-session", "restored baseline inventory differs"],
["changed-payload", "restored baseline inventory differs"],
["changed-schema", "restored baseline inventory differs"],
@ -422,20 +406,18 @@ describe("published backup rollback proof", () => {
expect(result.stderr).toContain("inventory path escaped its root");
});
it.each(["2026.9.3", "2026.9.4-beta.1"])(
"records %s as not applicable without launching a baseline runtime",
(version) => {
const f = fixture();
const result = f.run("eligibility", version, f.resultFile);
expect(result.status, result.stderr).toBe(0);
expect(readJson(f.resultFile)).toMatchObject({
status: "not-applicable",
baselineVersion: version,
});
expect(f.verify().status).toBe(0);
expect(existsSync(f.events)).toBe(false);
},
);
it("records pre-9.4 baselines as not applicable without launching a baseline runtime", () => {
const version = "2026.9.4-beta.1";
const f = fixture();
const result = f.run("eligibility", version, f.resultFile);
expect(result.status, result.stderr).toBe(0);
expect(readJson(f.resultFile)).toMatchObject({
status: "not-applicable",
baselineVersion: version,
});
expect(f.verify().status).toBe(0);
expect(existsSync(f.events)).toBe(false);
});
});
describe.skipIf(process.platform === "win32")("survivor rollback ordering", () => {

View file

@ -1,14 +1,16 @@
import { spawnSync } from "node:child_process";
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it } from "vitest";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
function runFixture(
failure: "version" | "runtime" | "install" | "launch",
overrides: NodeJS.ProcessEnv = {},
) {
const root = mkdtempSync(path.join(tmpdir(), "survivor-precheck-"));
const root = tempDirs.make("survivor-precheck-");
const bin = path.join(root, "bin");
const evidence = path.join(root, "evidence");
const installs = path.join(root, "installs.jsonl");
@ -54,7 +56,12 @@ fs.writeFileSync(path.join(prefix, "bin", "openclaw"), '#!/usr/bin/env node\\n'
},
},
);
return { root, result, evidence, installs };
return {
result,
evidence,
installs,
report: JSON.parse(readFileSync(path.join(evidence, "summary.json"), "utf8")),
};
}
describe("published baseline startup admission", () => {
@ -62,50 +69,43 @@ describe("published baseline startup admission", () => {
"skips unusable %s baselines with evidence before scheduling scenarios",
(failure) => {
const fixture = runFixture(failure);
try {
expect(fixture.result.status, fixture.result.stderr).toBe(0);
const groups = JSON.parse(fixture.result.stdout);
expect(groups.map((group: { label: string }) => group.label)).toEqual([
"published-upgrade-survivor-2026.8.2",
"onboard",
expect(fixture.result.status, fixture.result.stderr).toBe(0);
const groups = JSON.parse(fixture.result.stdout);
expect(groups.map((group: { label: string }) => group.label)).toEqual([
"published-upgrade-survivor-2026.8.2",
"onboard",
]);
expect(fixture.report.baselines).toEqual([
expect.objectContaining({
baseline: "openclaw@2026.8.1",
status: "skipped",
reason: expect.stringContaining("unusable published baseline"),
error: expect.stringContaining("Cannot find package fixture-runtime"),
scenarios: ["legacy-operator-state", "base"],
}),
expect.objectContaining({ baseline: "openclaw@2026.8.2", status: "usable" }),
]);
const installs = readFileSync(fixture.installs, "utf8")
.trim()
.split("\n")
.map((line) => JSON.parse(line));
expect(installs).toHaveLength(2);
for (const install of installs) {
expect(install.args).toEqual([
"install",
"-g",
"--prefix",
install.prefix,
expect.stringMatching(/^openclaw@/),
"--no-fund",
"--no-audit",
]);
const report = JSON.parse(
readFileSync(path.join(fixture.evidence, "summary.json"), "utf8"),
);
expect(report.baselines).toEqual([
expect.objectContaining({
baseline: "openclaw@2026.8.1",
status: "skipped",
reason: expect.stringContaining("unusable published baseline"),
error: expect.stringContaining("Cannot find package fixture-runtime"),
scenarios: ["legacy-operator-state", "base"],
}),
expect.objectContaining({ baseline: "openclaw@2026.8.2", status: "usable" }),
]);
const installs = readFileSync(fixture.installs, "utf8")
.trim()
.split("\n")
.map((line) => JSON.parse(line));
expect(installs).toHaveLength(2);
for (const install of installs) {
expect(install.args).toEqual([
"install",
"-g",
"--prefix",
install.prefix,
expect.stringMatching(/^openclaw@/),
"--no-fund",
"--no-audit",
]);
expect(() => readFileSync(path.join(install.prefix, "bin", "openclaw"))).toThrow();
}
const summary = readFileSync(path.join(fixture.evidence, "summary.md"), "utf8");
expect(summary).toContain("skipped");
expect(summary).toContain("Cannot find package fixture-runtime");
expect(summary).not.toContain("passed");
} finally {
rmSync(fixture.root, { recursive: true, force: true });
expect(() => readFileSync(path.join(install.prefix, "bin", "openclaw"))).toThrow();
}
const summary = readFileSync(path.join(fixture.evidence, "summary.md"), "utf8");
expect(summary).toContain("skipped");
expect(summary).toContain("Cannot find package fixture-runtime");
expect(summary).not.toContain("passed");
},
);
@ -118,20 +118,15 @@ describe("published baseline startup admission", () => {
GROUP_SIZE: "2",
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: "",
});
try {
expect(fixture.result.status, fixture.result.stderr).toBe(0);
expect(
JSON.parse(fixture.result.stdout).map(
(group: { docker_lanes: string }) => group.docker_lanes,
),
).toEqual(expected);
const report = JSON.parse(readFileSync(path.join(fixture.evidence, "summary.json"), "utf8"));
expect(report.baselines).toEqual([
expect.objectContaining({ baseline: "openclaw@2026.8.1", status: "skipped" }),
]);
} finally {
rmSync(fixture.root, { recursive: true, force: true });
}
expect(fixture.result.status, fixture.result.stderr).toBe(0);
expect(
JSON.parse(fixture.result.stdout).map(
(group: { docker_lanes: string }) => group.docker_lanes,
),
).toEqual(expected);
expect(fixture.report.baselines).toEqual([
expect.objectContaining({ baseline: "openclaw@2026.8.1", status: "skipped" }),
]);
});
it.each([
@ -141,18 +136,11 @@ describe("published baseline startup admission", () => {
"fails closed on $failure errors, preserving diagnostics instead of skipping coverage",
({ failure, error }) => {
const fixture = runFixture(failure);
try {
expect(fixture.result.status).not.toBe(0);
const report = JSON.parse(
readFileSync(path.join(fixture.evidence, "summary.json"), "utf8"),
);
expect(report.baselines[0]).toMatchObject({
status: "failed",
error: expect.stringContaining(error),
});
} finally {
rmSync(fixture.root, { recursive: true, force: true });
}
expect(fixture.result.status).not.toBe(0);
expect(fixture.report.baselines[0]).toMatchObject({
status: "failed",
error: expect.stringContaining(error),
});
},
);
});

View file

@ -134,7 +134,6 @@ it.each(["2026.9.4", "2026.9.6"])(
it.each([
{ source: "native", damage: "none" },
{ source: "legacy", damage: "none" },
{ source: "updater", damage: "none" },
{ source: "native", damage: "content" },
{ source: "legacy", damage: "missing-filter" },
{ source: "native", damage: "offset" },
@ -155,8 +154,7 @@ it.each([
})),
}),
);
const contract =
source === "updater" ? "published-updater-import-preserved" : "candidate-doctor-import";
const contract = "candidate-doctor-import";
if (source !== "native") {
fs.writeFileSync(
path.join(root, "legacy-operator-cron-history.json"),

View file

@ -61,18 +61,14 @@ describe("upgrade survivor live model selection", () => {
).toMatchObject({ overridesLiveOpenai: true, models: [{ provider: "google" }] });
});
it.each([
["openai/gpt-5.5", "OPENAI_API_KEY"],
["anthropic/claude-opus-5", "ANTHROPIC_API_KEY"],
["google/gemini-3.1-pro-preview", "GEMINI_API_KEY"],
])("fails clearly when %s has no key", (model, keyEnv) => {
it("names the missing provider key", () => {
expect(() =>
resolveLiveModels({
...keys,
[keyEnv]: " ",
OPENCLAW_UPGRADE_SURVIVOR_LIVE_MODELS: model,
OPENAI_API_KEY: " ",
OPENCLAW_UPGRADE_SURVIVOR_LIVE_MODELS: "openai/gpt-5.5",
}),
).toThrow(`Live model ${model} requires ${keyEnv}`);
).toThrow("Live model openai/gpt-5.5 requires OPENAI_API_KEY");
});
it.each([" \t\n", "gpt-5.5", "unknown/model", "openai/gpt-5.5 openai/gpt-5.5"])(

View file

@ -293,7 +293,6 @@ it("keeps policy log limits and symlink protections on successful publication",
it.each([
{ candidateVersion: "2026.9.5" },
{ activePlugins: ["device-pair"] },
{ candidateEnabledPlugins: ["memory-core", "telegram", "unrelated"] },
{ candidateEnabledPlugins: ["telegram"] },
{ baselineEnabledPlugins: [] },
{ hookUnauthorizedStatus: 200 },
@ -351,18 +350,43 @@ it.each(["failed", "passed"] as const)(
},
);
function integrityLog(f: ReturnType<typeof fixture>, telemetry: Record<string, unknown>) {
function integrityFixture(
identity: Record<string, unknown> = {},
telemetry: Record<string, unknown> = {},
) {
const f = fixture();
write(path.join(f.artifacts, "diagnostics/process-42-started.json"), {
event: "started",
role: "update",
packageVersion: "2026.9.4",
pid: 42,
parentPid: 1,
timeOriginUnixMs: 100,
...identity,
});
const observation = {
readerId: "42:1",
timeOriginUnixMs: 100,
event: "reader-settled",
phase: "retained",
budgetMs: 30000,
elapsedMs: 30002,
outcome: "timed-out",
pendingIo: 0,
...telemetry,
};
fs.writeFileSync(
path.join(f.root, "openclaw-upgrade-survivor", "gateway.jsonl"),
JSON.stringify({
0: JSON.stringify({ subsystem: "update/package-integrity" }),
1: telemetry,
2: telemetry.event,
message: telemetry.event,
1: observation,
2: observation.event,
message: observation.event,
_meta: { logLevelName: "DEBUG", path: privateBody },
privateBody,
}) + "\n",
);
return f;
}
it("captures only the actual baseline updater's released integrity envelope and preserves failure", () => {
@ -439,35 +463,12 @@ process.exit(1);`,
it.each([
{ name: "different PID", identity: { pid: 43 } },
{ name: "different process origin", identity: { timeOriginUnixMs: 101 } },
{ name: "Doctor", identity: { role: "doctor" } },
{ name: "post-core", identity: { role: "post-core" } },
{ name: "different package", identity: { packageVersion: "2026.9.5" } },
{ name: "non-numeric budget", telemetry: { budgetMs: "30000" } },
{ name: "missing settled outcome", telemetry: { outcome: undefined } },
{ name: "negative pending I/O", telemetry: { pendingIo: -1 } },
])("omits integrity evidence from $name", ({ identity, telemetry }) => {
const f = fixture();
write(path.join(f.artifacts, "diagnostics/process-42-started.json"), {
event: "started",
role: "update",
packageVersion: "2026.9.4",
pid: 42,
parentPid: 1,
timeOriginUnixMs: 100,
...identity,
});
integrityLog(f, {
readerId: "42:1",
timeOriginUnixMs: 100,
event: "reader-settled",
phase: "retained",
budgetMs: 30000,
elapsedMs: 30002,
outcome: "timed-out",
pendingIo: 0,
...telemetry,
});
const f = integrityFixture(identity, telemetry);
const report = capture(f);
expect(report.exitStatus).toBe(1);
expect(report.packageIntegrity).toEqual({ availability: "unavailable" });
@ -477,25 +478,7 @@ it.each([
it.each(["input", "output", "entries", "symlink", "directory-symlink", "malformed"] as const)(
"omits the whole integrity diagnostic at the %s boundary",
(boundary) => {
const f = fixture();
write(path.join(f.artifacts, "diagnostics/process-42-started.json"), {
event: "started",
role: "update",
packageVersion: "2026.9.4",
pid: 42,
parentPid: 1,
timeOriginUnixMs: 100,
});
integrityLog(f, {
readerId: "42:1",
timeOriginUnixMs: 100,
event: "reader-settled",
phase: "transaction",
budgetMs: 30000,
elapsedMs: 30002,
outcome: "timed-out",
pendingIo: 1,
});
const f = integrityFixture({}, { phase: "transaction", pendingIo: 1 });
const log = path.join(f.root, "openclaw-upgrade-survivor", "gateway.jsonl");
const line = fs.readFileSync(log, "utf8");
if (boundary === "symlink") {

View file

@ -83,14 +83,11 @@ describe("isolated Vitest admission", () => {
args: ["run", "-t"],
});
});
it.each(["latest", "repo:tag", "sha256:1234", "", "--network=host"])(
"refuses ambiguous image %s",
(value) => {
expect(() => parseIsolatedVitestArgs(["--isolated-image", value, "run", file])).toThrow(
"full local sha256",
);
},
);
it.each(["repo:tag", "sha256:1234", ""])("refuses ambiguous image %s", (value) => {
expect(() => parseIsolatedVitestArgs(["--isolated-image", value, "run", file])).toThrow(
"full local sha256",
);
});
it("refuses duplicate image selectors", () => {
expect(() =>
parseIsolatedVitestArgs(["--isolated-image", image, "--isolated-image", image]),
@ -375,7 +372,7 @@ function lifecycle(
}
describe("isolated container lifecycle", () => {
it.each([0, 1, 7])(
it.each([0, 7])(
"preserves test exit %s only after wait, remove and confirmed absence",
async (exit) => {
const fixture = lifecycle({ exit });
@ -415,15 +412,12 @@ describe("isolated container lifecycle", () => {
expect(onAbsent).not.toHaveBeenCalled();
},
);
it("reconciles a failed create without starting or falling back", async () => {
const fixture = lifecycle({ failCreate: true });
await expect(fixture.run()).rejects.toThrow("create failed");
expect(fixture.calls.some((args) => args[0] === "start")).toBe(false);
expect(fixture.onAbsent).toHaveBeenCalledOnce();
});
it("does not start a container whose inspection fails", async () => {
const fixture = lifecycle({ failVerify: true });
await expect(fixture.run()).rejects.toThrow("unexpected mounts");
it.each([
{ options: { failCreate: true }, error: "create failed" },
{ options: { failVerify: true }, error: "unexpected mounts" },
])("reconciles $error without starting the container", async ({ options, error }) => {
const fixture = lifecycle(options);
await expect(fixture.run()).rejects.toThrow(error);
expect(fixture.calls.some((args) => args[0] === "start")).toBe(false);
expect(fixture.onAbsent).toHaveBeenCalledOnce();
});

View file

@ -86,15 +86,7 @@ const discordCapture = "test/e2e/gateway-transcripts-discord-capture.e2e.test.ts
const e2eConfig = "test/vitest/vitest.e2e.config.ts";
it.for([
{ name: "worker", args: [coreWorker], prepare: true },
{ name: "absolute worker", args: [path.resolve(coreWorker)], prepare: true },
{ name: "line selection", args: [`${coreWorker}:12`], prepare: true },
{ name: "ordinary infra", args: ["src/infra/node-sqlite.test.ts"], prepare: false },
{ name: "excluded worker", args: [coreWorker, "--exclude", coreWorker], prepare: false },
{ name: "excluded glob", args: [coreWorker, "--exclude=src/infra/**"], prepare: false },
{ name: "empty include", args: [], include: [], prepare: false },
{ name: "worker include", args: [], include: [coreWorker], prepare: true },
{ name: "nonmatching include", args: [coreWorker], include: ["test/**"], prepare: false },
{ name: "root config", config: "vitest.config.ts", args: [coreWorker], prepare: true },
{ name: "custom config", config: "custom.config.ts", args: [coreWorker], prepare: false },
{ name: "full channels", config: channelsConfig, args: [], prepare: true },
@ -105,12 +97,12 @@ it.for([
prepare: false,
},
{ name: "empty channels", config: channelsConfig, args: [], include: [], prepare: false },
...[agentVitestProjectOwners.core, agentVitestProjectOwners.all].map((owner) => ({
name: `code-mode ${owner.name}`,
config: owner.config,
{
name: "code-mode all agents",
config: agentVitestProjectOwners.all.config,
args: [codeModeWorker],
prepare: true,
})),
},
{
name: "code-mode full agentic config",
config: "test/vitest/vitest.full-agentic.config.ts",
@ -123,13 +115,6 @@ it.for([
args: [codeModeWorker, "--exclude", path.basename(codeModeWorker)],
prepare: false,
},
{
name: "code-mode include",
config: agentsCoreConfig,
args: [],
include: [codeModeWorker],
prepare: true,
},
{
name: "code-mode omitted by include",
config: agentsCoreConfig,
@ -137,12 +122,6 @@ it.for([
include: ["src/agents/code-mode.test.ts"],
prepare: false,
},
{
name: "code-mode non-owning config",
config: infraConfig,
args: [codeModeWorker],
prepare: false,
},
{
name: "code-mode selection leaves unrelated agents lazy",
config: agentsCoreConfig,
@ -163,7 +142,6 @@ it.runIf(process.platform !== "win32").for([
(route.startsWith("contracts-")
? ["ready", "excluded"]
: [
"ready",
"code-mode",
"capture",
"failure",

View file

@ -1,10 +1,8 @@
import { describe, expect, it } from "vitest";
import { matchGitHubItemUrl } from "./github-link-eligibility.ts";
import { parseGitHubLinkTarget } from "./github-link-target.ts";
describe("GitHub issue and pull-request preview targets", () => {
it.each([
["https://github.com/acme/project/issues/42", "issue"],
["https://github.com/acme/project/issues/42/#issuecomment-7", "issue"],
["https://github.com/acme/project/pull/42", "pull"],
["https://github.com/acme/project/pull/42/files#diff-example", "pull"],
@ -14,7 +12,6 @@ describe("GitHub issue and pull-request preview targets", () => {
["HTTPS://GITHUB.COM:443/acme/project/pull/42", "pull"],
["https://github.com/%61cme/project/pull/42", "pull"],
])("preserves resource identity and destination for %s", (href, kind) => {
expect(matchGitHubItemUrl(new URL(href))).not.toBeNull();
expect(parseGitHubLinkTarget(href)).toEqual({
kind,
owner: "acme",
@ -25,18 +22,9 @@ describe("GitHub issue and pull-request preview targets", () => {
});
it.each([
"https://github.com/login",
"https://github.com/login?return_to=https://github.com/acme/project/pull/42",
"https://github.com/login?return_to=%2Facme%2Fproject%2Fissues%2F42",
"https://github.com/login/oauth/authorize?client_id=example-client",
"https://github.com/session",
"https://github.com/settings/connections/applications/example",
"https://github.com/",
"https://github.com/acme",
"https://github.com/acme/project",
"https://github.com/acme/project/commit/abcdef0123456789",
"https://example.com/?redirect=https://github.com/acme/project/pull/42",
"https://example.com/https://github.com/acme/project/pull/42",
"https://github.com.example.com/acme/project/pull/42",
"https://example@github.com/acme/project/pull/42",
"http://github.com/acme/project/pull/42",
@ -48,15 +36,11 @@ describe("GitHub issue and pull-request preview targets", () => {
"https://github.com/acme/" + "r".repeat(101) + "/pull/42",
"https://github.com/acme/project.git/pull/42",
"https://github.com/acme/project.atom/pull/42",
"https://github.com/acme/project/pull/42/login",
"https://github.com/acme/project/issues/42/files",
"https://github.com/acme/project/pull/42/files/extra",
"https://github.com//acme/project/pull/42",
"https://github.com/acme%2Fother/project/pull/42",
"https://github.com/%20acme/project/pull/42",
"https://github.com/acme/project%2Fother/pull/42",
])("does not infer an issue or PR from %s", (href) => {
expect(matchGitHubItemUrl(new URL(href))).toBeNull();
expect(parseGitHubLinkTarget(href)).toBeNull();
});
});

View file

@ -28,7 +28,7 @@ afterEach(() => {
});
describe("image lightbox gallery resource lifecycle", () => {
it.each(["close", "reset", "evict"] as const)(
it.each(["reset", "evict"] as const)(
"releases a late full-resolution image after %s without replacing newer intent",
async (action) => {
const initial = imageItem("preview");
@ -49,17 +49,13 @@ describe("image lightbox gallery resource lifecycle", () => {
if (action === "reset") {
controller.reset(undefined, replacement);
} else if (action === "evict") {
expect(await controller.move(1)).toBe(true);
expect(await controller.move(1)).toBe(true);
} else {
controller.dispose();
expect(await controller.move(1)).toBe(true);
expect(await controller.move(1)).toBe(true);
}
pending.resolve(original);
await vi.waitFor(() => expect(original.release).toHaveBeenCalledOnce());
expect(controller.current).toBe(
action === "close" ? undefined : action === "evict" ? beyond : replacement,
);
expect(controller.current).toBe(action === "evict" ? beyond : replacement);
controller.dispose();
await Promise.resolve();
expect(original.release).toHaveBeenCalledOnce();
@ -117,39 +113,32 @@ describe("image lightbox gallery resource lifecycle", () => {
},
);
it.each(["close", "reset"] as const)(
"releases a late image once after %s without replacing the current selection",
async (action) => {
const initial = imageItem("initial");
const late = imageItem("late");
const replacement = imageItem("replacement");
const pending = createDeferred<ImageLightboxItem | null>();
const load = vi.fn(() => pending.promise);
controller.reset({ index: 0, items: [async () => initial, load] }, initial);
const moving = controller.move(1);
await vi.waitFor(() => expect(load).toHaveBeenCalledOnce());
expect(controller.current).toBe(initial);
expect(controller.busy).toBe(true);
it("releases a late image once after reset without replacing the current selection", async () => {
const initial = imageItem("initial");
const late = imageItem("late");
const replacement = imageItem("replacement");
const pending = createDeferred<ImageLightboxItem | null>();
const load = vi.fn(() => pending.promise);
controller.reset({ index: 0, items: [async () => initial, load] }, initial);
const moving = controller.move(1);
await vi.waitFor(() => expect(load).toHaveBeenCalledOnce());
expect(controller.current).toBe(initial);
expect(controller.busy).toBe(true);
if (action === "reset") {
controller.reset(undefined, replacement);
} else {
controller.dispose();
}
pending.resolve(late);
controller.reset(undefined, replacement);
pending.resolve(late);
expect(await moving).toBe(false);
await vi.waitFor(() => expect(late.release).toHaveBeenCalledOnce());
expect(controller.current).toBe(action === "reset" ? replacement : undefined);
expect(controller.busy).toBe(false);
expect(controller.failed).toBe(false);
controller.dispose();
await Promise.resolve();
expect(late.release).toHaveBeenCalledOnce();
expect(initial.release).not.toHaveBeenCalled();
expect(replacement.release).not.toHaveBeenCalled();
},
);
expect(await moving).toBe(false);
await vi.waitFor(() => expect(late.release).toHaveBeenCalledOnce());
expect(controller.current).toBe(replacement);
expect(controller.busy).toBe(false);
expect(controller.failed).toBe(false);
controller.dispose();
await Promise.resolve();
expect(late.release).toHaveBeenCalledOnce();
expect(initial.release).not.toHaveBeenCalled();
expect(replacement.release).not.toHaveBeenCalled();
});
it("keeps the current image after a failed neighbor load and retries on navigation", async () => {
const initial = imageItem("initial");

View file

@ -8,12 +8,12 @@ import { LinkReaderHovercardProvider } from "./link-reader-hovercard.ts";
const ELEMENT_NAME = "test-link-reader-interaction-" + crypto.randomUUID();
customElements.define(ELEMENT_NAME, class extends LinkReaderHovercardProvider {});
const ISSUE_HREF = "https://github.com/openclaw/openclaw/issues/99815";
function createLink(href: string, label = "Item") {
function createLink(href: string) {
const provider = document.createElement(ELEMENT_NAME) as LinkReaderHovercardProvider;
provider.readers = [TEST_LINK_READER];
const anchor = document.createElement("a");
anchor.href = href;
anchor.textContent = label;
anchor.textContent = "Item";
provider.append(anchor);
document.body.append(provider);
return { provider, anchor };
@ -25,14 +25,11 @@ function issuePreviewResponse(overrides: Record<string, unknown> = {}) {
? { url: "https://github.com/openclaw/openclaw/issues/" + overrides.number }
: {}),
...overrides,
...(typeof overrides.comments !== "number"
? {}
: { metadata: [{ label: "Comments", value: String(overrides.comments) }] }),
};
}
function createIssueLink(response = issuePreviewResponse()) {
function createIssueLink() {
const link = createLink(ISSUE_HREF);
const request = vi.fn().mockResolvedValue(response);
const request = vi.fn().mockResolvedValue(issuePreviewResponse());
link.provider.client = { request } as unknown as GatewayBrowserClient;
return { ...link, request };
}
@ -76,7 +73,7 @@ describe("generic preview portal lifecycle", () => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it.each(["immediate rejection", "late rejection", "late success"])(
it.each(["immediate rejection", "late rejection"])(
"reopens an abandoned request without poisoning its replacement cache: %s",
async (settlement) => {
const abandoned = createDeferred<ReturnType<typeof issuePreviewResponse>>();
@ -109,11 +106,7 @@ describe("generic preview portal lifecycle", () => {
expect(request).toHaveBeenCalledTimes(2);
expect(hovercard()?.textContent).toContain("Keep hover previews reachable");
if (settlement === "late success") {
abandoned.resolve(issuePreviewResponse({ title: "Abandoned preview" }));
} else {
abandoned.reject(new Error("gateway request aborted"));
}
abandoned.reject(new Error("gateway request aborted"));
await vi.advanceTimersByTimeAsync(0);
expect(hovercard()?.textContent).toContain("Keep hover previews reachable");
leave(anchor);
@ -124,40 +117,6 @@ describe("generic preview portal lifecycle", () => {
},
);
it("keeps genuine request failures cached for 30 seconds before retrying on hover", async () => {
const retry = createDeferred<ReturnType<typeof issuePreviewResponse>>();
const request = vi
.fn()
.mockRejectedValueOnce(
new GatewayRequestError({ code: "UNAVAILABLE", message: "GitHub preview unavailable" }),
)
.mockReturnValue(retry.promise);
const { anchor, provider } = createLink(ISSUE_HREF);
provider.client = { request } as unknown as GatewayBrowserClient;
await hover(anchor);
expect(hovercard()?.textContent).toContain("GitHub preview unavailable");
expect(anchor.getAttribute("aria-expanded")).toBe("true");
anchor.focus();
await vi.advanceTimersByTimeAsync(0);
expect(document.activeElement).toBe(anchor);
expect(anchor.getAttribute("aria-controls")).toBe(hovercard()?.id);
anchor.blur();
leave(anchor);
await vi.advanceTimersByTimeAsync(29_000);
await hover(anchor);
expect(request).toHaveBeenCalledTimes(1);
expect(hovercard()?.textContent).toContain("GitHub preview unavailable");
leave(anchor);
await vi.advanceTimersByTimeAsync(1_000);
await hover(anchor);
expect(request).toHaveBeenCalledTimes(2);
expect(hovercard()).toBeNull();
retry.resolve(issuePreviewResponse());
await vi.advanceTimersByTimeAsync(0);
expect(hovercard()?.textContent).toContain("Keep hover previews reachable");
});
it("shows a pending preview rejection without moving focus and keeps its original link reachable", async () => {
const pending = createDeferred<unknown>();
const { anchor, provider } = createLink(ISSUE_HREF);
@ -215,22 +174,18 @@ describe("generic preview portal lifecycle", () => {
},
);
it.each(
[
"pointer leave",
"focus leave",
"Escape",
"click",
"route replacement",
"href change",
"agent change",
"client change",
"disconnect",
].flatMap((dismissal) => [
{ dismissal, settlement: "success" },
{ dismissal, settlement: "failure" },
]),
)("does not mount a late $settlement after $dismissal", async ({ dismissal, settlement }) => {
it.each([
{ dismissal: "pointer leave", settlement: "success" },
{ dismissal: "pointer leave", settlement: "failure" },
{ dismissal: "focus leave", settlement: "success" },
{ dismissal: "Escape", settlement: "success" },
{ dismissal: "click", settlement: "success" },
{ dismissal: "route replacement", settlement: "success" },
{ dismissal: "href change", settlement: "success" },
{ dismissal: "agent change", settlement: "success" },
{ dismissal: "client change", settlement: "success" },
{ dismissal: "disconnect", settlement: "success" },
])("does not mount a late $settlement after $dismissal", async ({ dismissal, settlement }) => {
const mountedCards = observeHovercardMounts();
const pending = createDeferred<ReturnType<typeof issuePreviewResponse>>();
const { anchor, provider } = createLink(ISSUE_HREF);
@ -451,8 +406,6 @@ describe("generic preview portal lifecycle", () => {
it.each([
["agent", "agent-a"],
["client", "agent-a"],
["agent", "agent-b"],
["client", "agent-b"],
])(
"preserves the unchanged provider when a peer changes its %s from %s",
@ -587,20 +540,6 @@ describe("generic preview portal lifecycle", () => {
expect(hovercard()).toBeNull();
});
it("renders issue comments and supports focus plus Escape", async () => {
const { anchor } = createIssueLink(issuePreviewResponse({ comments: 4 }));
anchor.dispatchEvent(new FocusEvent("focusin", { bubbles: true, composed: true }));
await vi.advanceTimersByTimeAsync(0);
expect(hovercard()?.textContent).toContain("Comments: 4");
expect(hovercard()?.textContent).toContain("Open");
// Issues have no files-changed view, so their metric stays plain text.
expect(hovercard()?.querySelector(".link-reader-hovercard__metric--files")).toBeNull();
anchor.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
expect(hovercard()).toBeNull();
});
it.each([false, true])("hands focus back at Tab edges (shiftKey=%s)", async (shiftKey) => {
const { anchor } = createIssueLink();
@ -632,19 +571,6 @@ describe("generic preview portal lifecycle", () => {
expect(hovercard()).toBeNull();
});
it("closes on Escape from inside the card and returns focus to the link", async () => {
const { anchor } = createIssueLink();
anchor.focus();
await vi.advanceTimersByTimeAsync(0);
const title = titleLinkInCard();
title?.focus();
title?.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Escape" }));
expect(hovercard()).toBeNull();
expect(document.activeElement).toBe(anchor);
});
it("closes once focus leaves both the link and the card", async () => {
const { anchor } = createIssueLink();
const outside = document.createElement("button");
@ -661,54 +587,6 @@ describe("generic preview portal lifecycle", () => {
expect(anchor.hasAttribute("aria-expanded")).toBe(false);
});
it("ignores unsupported GitHub links and reports failed supported previews", async () => {
const request = vi
.fn()
.mockRejectedValue(new GatewayRequestError({ code: "UNAVAILABLE", message: "Not Found" }));
const unsupportedLink = createLink("https://github.com/openclaw/openclaw", "repository");
unsupportedLink.provider.client = { request } as unknown as GatewayBrowserClient;
await hover(unsupportedLink.anchor);
expect(request).not.toHaveBeenCalled();
expect(document.querySelector(".link-reader-hovercard")).toBeNull();
const missingLink = createLink("https://github.com/openclaw/openclaw/issues/999999", "missing");
missingLink.provider.client = { request } as unknown as GatewayBrowserClient;
await hover(missingLink.anchor);
expect(hovercard()?.textContent).toContain("Not Found");
expect(request).toHaveBeenCalledTimes(1);
});
it("discards cached and pending previews when the selected agent changes", async () => {
const { anchor, provider } = createIssueLink();
const request = vi.fn().mockResolvedValue(issuePreviewResponse());
provider.client = { request } as unknown as GatewayBrowserClient;
provider.agentId = "first-agent";
await hover(anchor);
expect(request.mock.calls[0]?.[1]).toMatchObject({ agentId: "first-agent" });
provider.agentId = "second-agent";
expect(hovercard()).toBeNull();
await hover(anchor);
expect(request).toHaveBeenCalledTimes(2);
expect(request.mock.calls[1]?.[1]).toMatchObject({ agentId: "second-agent" });
let resolvePending!: (value: unknown) => void;
const nextRequest = vi.fn().mockImplementation(
() =>
new Promise((resolve) => {
resolvePending = resolve;
}),
);
provider.client = { request: nextRequest } as unknown as GatewayBrowserClient;
expect(hovercard()).toBeNull();
await hover(anchor);
provider.agentId = "third-agent";
resolvePending(issuePreviewResponse());
await vi.advanceTimersByTimeAsync(0);
expect(hovercard()).toBeNull();
});
it("uses the latest dependencies assigned before its lazy definition finishes", async () => {
const tag = `test-github-lazy-upgrade-${crypto.randomUUID()}`;
const loaded = createDeferred<CustomElementConstructor>();
@ -745,13 +623,9 @@ describe("generic preview portal lifecycle", () => {
});
it.each([
"http://github.com/openclaw/openclaw/issues/99815",
"https://user:password@github.com/openclaw/openclaw/issues/99815",
"https://github.com:8443/openclaw/openclaw/issues/99815",
"https://github.com.example.com/openclaw/openclaw/issues/99815",
"blob:https://github.com/issues/99815",
"https://example.com/openclaw/openclaw/issues/99815",
"javascript:alert(1)",
])("does not preview an untrusted item URL: %s", async (href) => {
const request = vi.fn();
const { anchor, provider } = createLink(href);
@ -763,20 +637,6 @@ describe("generic preview portal lifecycle", () => {
expect(document.querySelector(".link-reader-hovercard")).toBeNull();
});
it("leaves no popup state on the link when hover ends before opening", async () => {
const request = vi.fn();
const { anchor, provider } = createLink("https://github.com/openclaw/openclaw/issues/99815");
provider.client = { request } as unknown as GatewayBrowserClient;
anchor.dispatchEvent(new MouseEvent("pointerover", { bubbles: true, composed: true }));
leave(anchor);
await vi.advanceTimersByTimeAsync(250);
expect(anchor.hasAttribute("aria-haspopup")).toBe(false);
expect(anchor.hasAttribute("aria-expanded")).toBe(false);
expect(request).not.toHaveBeenCalled();
});
it.each(["pending", "held"])(
"retires a %s GitHub preview when its pane becomes inert",
async (phase) => {
@ -801,7 +661,7 @@ describe("generic preview portal lifecycle", () => {
);
it("closes when route replacement removes its active link", async () => {
const { provider, anchor } = createIssueLink(issuePreviewResponse({ comments: 1 }));
const { provider, anchor } = createIssueLink();
const route = document.createElement("main");
route.append(anchor);
provider.append(route);

View file

@ -9,24 +9,13 @@ import {
} from "./provider-icon.ts";
describe("cloud provider presentation", () => {
it.each(["aws", "azure", "daytona", "gcp", "google", "google-cloud", "hetzner", "machine0"])(
it.each(["google", "machine0"])(
"orders backend %s before local/custom profiles regardless of their names",
(backend) => {
const cloud = { id: "z-local", providerId: "crabbox", providerDisplayId: backend };
for (const providerDisplayId of [
"incus",
"local-container",
"docker",
"local-docker",
"podman",
"local-podman",
"custom",
"constructor",
]) {
const infrastructure = { id: "a-aws", providerId: "aws", providerDisplayId };
expect(compareCloudProfiles(cloud, infrastructure)).toBeLessThan(0);
expect(compareCloudProfiles(infrastructure, cloud)).toBeGreaterThan(0);
}
const infrastructure = { id: "a-aws", providerId: "aws", providerDisplayId: "constructor" };
expect(compareCloudProfiles(cloud, infrastructure)).toBeLessThan(0);
expect(compareCloudProfiles(infrastructure, cloud)).toBeGreaterThan(0);
expect(
compareCloudProfiles(
{ id: "z-local", providerId: backend },
@ -46,30 +35,21 @@ describe("cloud provider presentation", () => {
expect(profiles.toSorted(compareCloudProfiles).map((p) => p.id)).toEqual(["y", "z", "a", "b"]);
expect(profiles.map((p) => p.id)).toEqual(["b", "z", "a", "y"]);
});
it.each(["gcp", "google", "google-cloud"])(
"keeps %s separate from model-provider brands",
(providerDisplayId) => {
const container = document.createElement("div");
render(resolveCloudProfileIcon({ providerId: "crabbox", providerDisplayId }).icon, container);
expect(container.querySelector('[data-provider-icon="gcp"]')).not.toBeNull();
render(renderProviderBrandIcon("google"), container);
expect(container.querySelector('[data-provider-icon="gemini"]')).not.toBeNull();
},
);
it("keeps Google Cloud separate from model-provider brands", () => {
const container = document.createElement("div");
render(
resolveCloudProfileIcon({ providerId: "crabbox", providerDisplayId: "google" }).icon,
container,
);
expect(container.querySelector('[data-provider-icon="gcp"]')).not.toBeNull();
render(renderProviderBrandIcon("google"), container);
expect(container.querySelector('[data-provider-icon="gemini"]')).not.toBeNull();
});
it.each([
["machine0", icons.server],
["incus", icons.server],
["local-container", icons.box],
["docker", icons.box],
["local-docker", icons.box],
["podman", icons.box],
["local-podman", icons.box],
["external", icons.cloud],
["unknown", icons.cloud],
["constructor", icons.cloud],
["__proto__", icons.cloud],
["toString", icons.cloud],
["crabbox", icons.cloud],
])("uses shared generic geometry for %s", (providerId, expectedIcon) => {
const actual = document.createElement("div"),
expected = document.createElement("div");

View file

@ -5,11 +5,6 @@ import type { SidebarRecentSession } from "./app-sidebar-session-types.ts";
describe("sidebar linked conversation identity", () => {
it.each([
{
name: "route-only WhatsApp direct chat",
row: { key: "agent:main:whatsapp:direct:15555550123" },
expected: { channelLabel: "WhatsApp", kind: "direct", address: "+15555550123" },
},
{
name: "route-only email direct chat",
row: { key: "agent:main:email:direct:alex@example.com" },
@ -24,9 +19,19 @@ describe("sidebar linked conversation identity", () => {
name: "current privacy identity instead of an older phone route",
row: {
key: "agent:main:whatsapp:direct:15555550123",
origin: { provider: "whatsapp", from: "100000000@lid", to: "15555550999@s.whatsapp.net" },
origin: {
provider: "whatsapp",
from: "100000000@lid",
to: "15555550999@s.whatsapp.net",
label: "100000000@lid",
},
},
expected: {
channelLabel: "WhatsApp",
kind: "direct",
address: undefined,
conversation: undefined,
},
expected: { channelLabel: "WhatsApp", kind: "direct", address: undefined },
},
{
name: "WhatsApp phone JID",
@ -55,20 +60,6 @@ describe("sidebar linked conversation identity", () => {
address: undefined,
},
},
{
name: "WhatsApp privacy identifier",
row: {
key: "agent:main:whatsapp:direct:100000000@lid",
channel: "whatsapp",
origin: {
provider: "whatsapp",
from: "100000000@lid",
to: "15555550999@s.whatsapp.net",
label: "100000000@lid",
},
},
expected: { channelLabel: "WhatsApp", address: undefined, conversation: undefined },
},
{
name: "Discord thread without redundant origin metadata",
row: {
@ -132,19 +123,6 @@ describe("sidebar linked conversation identity", () => {
},
expected: { channelLabel: "Discord", kind: "thread", conversation: "Example #releases" },
},
{
name: "Slack workspace and channel",
row: {
key: "agent:main:slack:channel:c123:thread:123.456",
channel: "slack",
chatType: "channel",
subject: "Example #releases",
groupChannel: "#releases",
space: "T123",
origin: { provider: "slack", threadId: "123.456" },
},
expected: { channelLabel: "Slack", kind: "thread", conversation: "Example #releases" },
},
{
name: "Telegram topic",
row: {
@ -193,11 +171,6 @@ describe("sidebar linked conversation identity", () => {
row: { key: "agent:main:constructor:direct:42" },
expected: { channelLabel: "Constructor", kind: "direct" },
},
{
name: "__proto__ channel from an explicit session key",
row: { key: "agent:main:__proto__:channel:example" },
expected: { channelLabel: "__proto__", kind: "channel" },
},
{
name: "canonical peer instead of conflicting last-delivery metadata",
row: {
@ -224,17 +197,14 @@ describe("sidebar linked conversation identity", () => {
expect(projectSidebarSession(row).channelPresentation).toMatchObject(expected);
});
it.each(["agent:main:main", "agent:main:dashboard:chat", "agent:main:work", "global"])(
"does not label %s as channel-linked because of its last delivery route",
(key) => {
expect(
projectSidebarSession({
key,
channel: "whatsapp",
chatType: "direct",
origin: { provider: "whatsapp", from: "+15555550123" },
}).channelPresentation,
).toBeUndefined();
},
);
it("does not label a dashboard session as channel-linked because of its last delivery route", () => {
expect(
projectSidebarSession({
key: "agent:main:dashboard:chat",
channel: "whatsapp",
chatType: "direct",
origin: { provider: "whatsapp", from: "+15555550123" },
}).channelPresentation,
).toBeUndefined();
});
});

View file

@ -3,7 +3,7 @@ import { render } from "lit";
import { describe, expect, it, vi } from "vitest";
import { createDeferred } from "../../../test/helpers/promise.js";
import { renderChatSessionSharing } from "../pages/chat/components/chat-session-sharing.ts";
import { containers, mountMenu, selectMenuValue } from "../test-helpers/session-menu.ts";
import { containers, mountMenu } from "../test-helpers/session-menu.ts";
import {
createSessionOwnerMenuHarness,
sessionOwnerProfiles,
@ -26,38 +26,6 @@ describe("searchable session people", () => {
expect(menu.querySelectorAll('[value^="assign-owner:"]')).toHaveLength(1);
});
it.each([21, 1000])(
"searches the whole %i-person directory without rendering it all",
async (count) => {
const names = Array.from(
{ length: count },
(_, index) => `Person ${String(index).padStart(4, "0")}`,
);
const { context, request } = createSessionOwnerMenuHarness(() =>
sessionOwnerProfiles(...names),
);
const onAction = vi.fn();
const menu = await mountMenu({ context, onAction });
await waitForFast(() => expect(request).toHaveBeenCalledWith("users.list", {}));
await waitForFast(() => expect(menu.textContent).toContain(names[0]));
expect(menu.querySelectorAll('[value^="assign-owner:"]').length).toBeLessThanOrEqual(20);
const search = menu.querySelector<HTMLInputElement>('input[type="search"]');
expect(search).not.toBeNull();
search!.value = names.at(-1)!;
search!.dispatchEvent(new InputEvent("input", { bubbles: true }));
await waitForFast(() =>
expect(menu.querySelectorAll('[value^="assign-owner:"]')).toHaveLength(1),
);
const row = menu.querySelector<HTMLElement>('[value^="assign-owner:"]')!;
expect(row.textContent).toContain(names.at(-1));
selectMenuValue(menu, row.getAttribute("value")!);
expect(onAction).toHaveBeenCalledWith({
kind: "assign-owner",
owner: { type: "human", id: `profile-person-${String(count - 1).padStart(4, "0")}` },
});
},
);
it("searches beyond the first member page and preserves remove-member selection", async () => {
const onMemberChange = vi.fn();
const root = document.createElement("div");