From b1c09f1088e0fba7b91c661d3b20b945c96ab141 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 2 Oct 2026 08:15:14 -0700 Subject: [PATCH] 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 --- scripts/release-approval-receipt.mjs | 2 +- .../tts-request.preparation-resources.test.ts | 414 ++++++++---------- src/tts/tts-streaming.resources.test.ts | 399 ++++++++--------- src/tts/tts-summary.resources.test.ts | 43 +- src/tts/tts-summary.selection.test.ts | 84 ++-- .../full-release-child-evidence.test.ts | 81 ++-- test/scripts/full-release-child-reuse.test.ts | 9 +- ...full-release-publication-admission.test.ts | 122 +----- test/scripts/gateway-bench-child.test.ts | 205 +++------ test/scripts/gateway-bench-profile.test.ts | 2 - test/scripts/gateway-resource-host.test.ts | 28 +- test/scripts/package-bundled-mcp.test.ts | 103 +++-- .../plugin-npm-prepared-release.test.ts | 92 +--- ...ase-pretag-pack-check.process-tree.test.ts | 31 +- .../scripts/pr-wrapper-source-closure.test.ts | 95 +--- .../preflight-frozen-target-contracts.test.ts | 393 +++++++---------- test/scripts/qa-profile-run-status.test.ts | 34 +- test/scripts/release-approval-receipt.test.ts | 43 +- .../release-approval-workflows.test.ts | 39 -- test/scripts/release-channel-waiver.test.ts | 2 +- test/scripts/release-clawhub-children.test.ts | 73 +-- test/scripts/release-evidence-retry.test.ts | 5 +- ...elease-publish-preflight-interface.test.ts | 3 +- test/scripts/release-publish-state.test.ts | 78 ++-- test/scripts/release-stable-post.test.ts | 1 - test/scripts/release-stable.test.ts | 4 +- test/scripts/release-verify-publish.test.ts | 22 +- test/scripts/security-review-event.test.ts | 100 ++--- .../upgrade-survivor-backup-rollback.test.ts | 72 ++- .../upgrade-survivor-baseline-check.test.ts | 134 +++--- .../upgrade-survivor-cron-reader.test.ts | 4 +- .../upgrade-survivor-live-models.test.ts | 12 +- ...ade-survivor-migration-diagnostics.test.ts | 79 ++-- test/scripts/vitest-isolated.test.ts | 30 +- .../vitest-worker-artifacts.batch.test.ts | 30 +- ui/src/components/github-link-target.test.ts | 16 - .../components/image-lightbox-gallery.test.ts | 67 ++- ...link-reader-hovercard-interactions.test.ts | 178 +------- ui/src/components/provider-icon.test.ts | 48 +- .../session-channel-identity.test.ts | 74 +--- .../components/session-people-search.test.ts | 34 +- 41 files changed, 1072 insertions(+), 2213 deletions(-) diff --git a/scripts/release-approval-receipt.mjs b/scripts/release-approval-receipt.mjs index 9b939859c5d9..21b99291710d 100644 --- a/scripts/release-approval-receipt.mjs +++ b/scripts/release-approval-receipt.mjs @@ -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}`; diff --git a/src/tts/tts-request.preparation-resources.test.ts b/src/tts/tts-request.preparation-resources.test.ts index dff4a2ee9561..e436e1e5009c 100644 --- a/src/tts/tts-request.preparation-resources.test.ts +++ b/src/tts/tts-request.preparation-resources.test.ts @@ -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) => - 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) { 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 | 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 | 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 | 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((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 | 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 | 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((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((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 | 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((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 | 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); + }); }); }); diff --git a/src/tts/tts-streaming.resources.test.ts b/src/tts/tts-streaming.resources.test.ts index b13cafbd2231..c828dda61540 100644 --- a/src/tts/tts-streaming.resources.test.ts +++ b/src/tts/tts-streaming.resources.test.ts @@ -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) => - withEnvAsync( - { - OPENCLAW_HOME: dir, - OPENCLAW_STATE_DIR: dir, - OPENCLAW_CONFIG_PATH: path.join(dir, "config.json"), - }, - async () => { - useNoBundledPlugins(); - await operation(); - }, - ), + async runTest(operation: () => Promise) { + 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?.(); + } + }); }, ); }); diff --git a/src/tts/tts-summary.resources.test.ts b/src/tts/tts-summary.resources.test.ts index 5b203fcc8777..50d4a137884b 100644 --- a/src/tts/tts-summary.resources.test.ts +++ b/src/tts/tts-summary.resources.test.ts @@ -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(); diff --git a/src/tts/tts-summary.selection.test.ts b/src/tts/tts-summary.selection.test.ts index e9d2bba84687..a4a7a6685cc2 100644 --- a/src/tts/tts-summary.selection.test.ts +++ b/src/tts/tts-summary.selection.test.ts @@ -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)( + "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]); - }, - ); - }, - ); }, ); diff --git a/test/scripts/full-release-child-evidence.test.ts b/test/scripts/full-release-child-evidence.test.ts index 8202537149da..94941be9e59c 100644 --- a/test/scripts/full-release-child-evidence.test.ts +++ b/test/scripts/full-release-child-evidence.test.ts @@ -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, diff --git a/test/scripts/full-release-child-reuse.test.ts b/test/scripts/full-release-child-reuse.test.ts index b002e185ccae..03875e25a36b 100644 --- a/test/scripts/full-release-child-reuse.test.ts +++ b/test/scripts/full-release-child-reuse.test.ts @@ -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"); diff --git a/test/scripts/full-release-publication-admission.test.ts b/test/scripts/full-release-publication-admission.test.ts index 1358ae103656..918995179517 100644 --- a/test/scripts/full-release-publication-admission.test.ts +++ b/test/scripts/full-release-publication-admission.test.ts @@ -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", () => { diff --git a/test/scripts/gateway-bench-child.test.ts b/test/scripts/gateway-bench-child.test.ts index 7ef12806fa1a..b4e86bd374bd 100644 --- a/test/scripts/gateway-bench-child.test.ts +++ b/test/scripts/gateway-bench-child.test.ts @@ -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; - 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; - 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 }; - exitCode: number | null; - kill: ReturnType; - signalCode: NodeJS.Signals | null; - stderr: { destroy: ReturnType }; - stdin: { destroy: ReturnType }; - stdout: { destroy: ReturnType }; - unref: ReturnType; - }; - 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; - pid: number; - signalCode: NodeJS.Signals | null; - stderr: { destroy: ReturnType }; - stdin: { destroy: ReturnType }; - stdout: { destroy: ReturnType }; - unref: ReturnType; + 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; - pid: number; - signalCode: NodeJS.Signals | null; - stderr: { destroy: ReturnType }; - stdin: { destroy: ReturnType }; - stdout: { destroy: ReturnType }; - unref: ReturnType; - }; - 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(); diff --git a/test/scripts/gateway-bench-profile.test.ts b/test/scripts/gateway-bench-profile.test.ts index 04ab50edceb4..f991354d9aea 100644 --- a/test/scripts/gateway-bench-profile.test.ts +++ b/test/scripts/gateway-bench-profile.test.ts @@ -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", diff --git a/test/scripts/gateway-resource-host.test.ts b/test/scripts/gateway-resource-host.test.ts index 91afb81851e3..edcbdc02f48f 100644 --- a/test/scripts/gateway-resource-host.test.ts +++ b/test/scripts/gateway-resource-host.test.ts @@ -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); }); }, ); diff --git a/test/scripts/package-bundled-mcp.test.ts b/test/scripts/package-bundled-mcp.test.ts index becf5152134e..56848fc8a6a1 100644 --- a/test/scripts/package-bundled-mcp.test.ts +++ b/test/scripts/package-bundled-mcp.test.ts @@ -73,7 +73,7 @@ const ASSETS = [ "build/src/third_party/issue-descriptions/example.md", ]; -function fixture(version: string, hashes: Record) { +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( diff --git a/test/scripts/plugin-npm-prepared-release.test.ts b/test/scripts/plugin-npm-prepared-release.test.ts index d34f1c343fe1..b6f296f8dd0a 100644 --- a/test/scripts/plugin-npm-prepared-release.test.ts +++ b/test/scripts/plugin-npm-prepared-release.test.ts @@ -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( diff --git a/test/scripts/plugin-release-pretag-pack-check.process-tree.test.ts b/test/scripts/plugin-release-pretag-pack-check.process-tree.test.ts index dbb9a41b1374..4b2a46582298 100644 --- a/test/scripts/plugin-release-pretag-pack-check.process-tree.test.ts +++ b/test/scripts/plugin-release-pretag-pack-check.process-tree.test.ts @@ -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); diff --git a/test/scripts/pr-wrapper-source-closure.test.ts b/test/scripts/pr-wrapper-source-closure.test.ts index fcf092713980..b61f61841a6e 100644 --- a/test/scripts/pr-wrapper-source-closure.test.ts +++ b/test/scripts/pr-wrapper-source-closure.test.ts @@ -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(); - const missing = new Set(); - 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([]); }, ); diff --git a/test/scripts/preflight-frozen-target-contracts.test.ts b/test/scripts/preflight-frozen-target-contracts.test.ts index 70160386ba16..7e3a7965bbca 100644 --- a/test/scripts/preflight-frozen-target-contracts.test.ts +++ b/test/scripts/preflight-frozen-target-contracts.test.ts @@ -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, 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) { + 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, error?: string) { + expect(result.status, result.stderr).toBe(1); + if (error) { + expect(result.stderr).toContain(error); + } + expect(result.stdout).toBe(""); +} + function fixture( files: Record = {}, 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 = { - "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 = { + "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); }); }); diff --git a/test/scripts/qa-profile-run-status.test.ts b/test/scripts/qa-profile-run-status.test.ts index 4a9820236077..dd9888acd9be 100644 --- a/test/scripts/qa-profile-run-status.test.ts +++ b/test/scripts/qa-profile-run-status.test.ts @@ -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); diff --git a/test/scripts/release-approval-receipt.test.ts b/test/scripts/release-approval-receipt.test.ts index f57b27ad4574..0c5a82393860 100644 --- a/test/scripts/release-approval-receipt.test.ts +++ b/test/scripts/release-approval-receipt.test.ts @@ -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", diff --git a/test/scripts/release-approval-workflows.test.ts b/test/scripts/release-approval-workflows.test.ts index e24ef38c3d91..9e4c118bb22f 100644 --- a/test/scripts/release-approval-workflows.test.ts +++ b/test/scripts/release-approval-workflows.test.ts @@ -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" diff --git a/test/scripts/release-channel-waiver.test.ts b/test/scripts/release-channel-waiver.test.ts index c139effa641f..2552c3a37c8d 100644 --- a/test/scripts/release-channel-waiver.test.ts +++ b/test/scripts/release-channel-waiver.test.ts @@ -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 }; diff --git a/test/scripts/release-clawhub-children.test.ts b/test/scripts/release-clawhub-children.test.ts index fde02fda74c8..ae92ccc83f5c 100644 --- a/test/scripts/release-clawhub-children.test.ts +++ b/test/scripts/release-clawhub-children.test.ts @@ -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'); diff --git a/test/scripts/release-evidence-retry.test.ts b/test/scripts/release-evidence-retry.test.ts index 601d6450af5e..0e4160096071 100644 --- a/test/scripts/release-evidence-retry.test.ts +++ b/test/scripts/release-evidence-retry.test.ts @@ -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({ diff --git a/test/scripts/release-publish-preflight-interface.test.ts b/test/scripts/release-publish-preflight-interface.test.ts index 83efae64bfdb..c47b3fbe3e5e 100644 --- a/test/scripts/release-publish-preflight-interface.test.ts +++ b/test/scripts/release-publish-preflight-interface.test.ts @@ -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(() => diff --git a/test/scripts/release-publish-state.test.ts b/test/scripts/release-publish-state.test.ts index 02dcf308e838..5d8d0ea3935d 100644 --- a/test/scripts/release-publish-state.test.ts +++ b/test/scripts/release-publish-state.test.ts @@ -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[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"); diff --git a/test/scripts/release-stable-post.test.ts b/test/scripts/release-stable-post.test.ts index 789c8c38718e..0cbe2e0a933b 100644 --- a/test/scripts/release-stable-post.test.ts +++ b/test/scripts/release-stable-post.test.ts @@ -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", diff --git a/test/scripts/release-stable.test.ts b/test/scripts/release-stable.test.ts index 3f43b4d23962..5009649a373a 100644 --- a/test/scripts/release-stable.test.ts +++ b/test/scripts/release-stable.test.ts @@ -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"); diff --git a/test/scripts/release-verify-publish.test.ts b/test/scripts/release-verify-publish.test.ts index 438d1af0a50f..17b11ef0def4 100644 --- a/test/scripts/release-verify-publish.test.ts +++ b/test/scripts/release-verify-publish.test.ts @@ -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", ); } }); diff --git a/test/scripts/security-review-event.test.ts b/test/scripts/security-review-event.test.ts index ee2a7eb27080..0ecfb9f60c50 100644 --- a/test/scripts/security-review-event.test.ts +++ b/test/scripts/security-review-event.test.ts @@ -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, diff --git a/test/scripts/upgrade-survivor-backup-rollback.test.ts b/test/scripts/upgrade-survivor-backup-rollback.test.ts index c0366e0d9753..ef327c98cefe 100644 --- a/test/scripts/upgrade-survivor-backup-rollback.test.ts +++ b/test/scripts/upgrade-survivor-backup-rollback.test.ts @@ -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", () => { diff --git a/test/scripts/upgrade-survivor-baseline-check.test.ts b/test/scripts/upgrade-survivor-baseline-check.test.ts index 004965e55371..0c708aadb9ff 100644 --- a/test/scripts/upgrade-survivor-baseline-check.test.ts +++ b/test/scripts/upgrade-survivor-baseline-check.test.ts @@ -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), + }); }, ); }); diff --git a/test/scripts/upgrade-survivor-cron-reader.test.ts b/test/scripts/upgrade-survivor-cron-reader.test.ts index 0a05ce1a6bca..47f6dc0ebddd 100644 --- a/test/scripts/upgrade-survivor-cron-reader.test.ts +++ b/test/scripts/upgrade-survivor-cron-reader.test.ts @@ -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"), diff --git a/test/scripts/upgrade-survivor-live-models.test.ts b/test/scripts/upgrade-survivor-live-models.test.ts index fe08bc5cf95b..607b3cca349c 100644 --- a/test/scripts/upgrade-survivor-live-models.test.ts +++ b/test/scripts/upgrade-survivor-live-models.test.ts @@ -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"])( diff --git a/test/scripts/upgrade-survivor-migration-diagnostics.test.ts b/test/scripts/upgrade-survivor-migration-diagnostics.test.ts index 5dc093861b9f..abf3ea0155cb 100644 --- a/test/scripts/upgrade-survivor-migration-diagnostics.test.ts +++ b/test/scripts/upgrade-survivor-migration-diagnostics.test.ts @@ -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, telemetry: Record) { +function integrityFixture( + identity: Record = {}, + telemetry: Record = {}, +) { + 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") { diff --git a/test/scripts/vitest-isolated.test.ts b/test/scripts/vitest-isolated.test.ts index 86d256090b15..ed5d16b51746 100644 --- a/test/scripts/vitest-isolated.test.ts +++ b/test/scripts/vitest-isolated.test.ts @@ -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(); }); diff --git a/test/scripts/vitest-worker-artifacts.batch.test.ts b/test/scripts/vitest-worker-artifacts.batch.test.ts index e1d8df2b167a..c81979f20c22 100644 --- a/test/scripts/vitest-worker-artifacts.batch.test.ts +++ b/test/scripts/vitest-worker-artifacts.batch.test.ts @@ -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", diff --git a/ui/src/components/github-link-target.test.ts b/ui/src/components/github-link-target.test.ts index 8b650989f24f..1d1d73add65c 100644 --- a/ui/src/components/github-link-target.test.ts +++ b/ui/src/components/github-link-target.test.ts @@ -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(); }); }); diff --git a/ui/src/components/image-lightbox-gallery.test.ts b/ui/src/components/image-lightbox-gallery.test.ts index 440ba81c9627..8f69486d15d3 100644 --- a/ui/src/components/image-lightbox-gallery.test.ts +++ b/ui/src/components/image-lightbox-gallery.test.ts @@ -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(); - 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(); + 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"); diff --git a/ui/src/components/link-reader-hovercard-interactions.test.ts b/ui/src/components/link-reader-hovercard-interactions.test.ts index 2e6d4eb2dcee..936a3486c9ba 100644 --- a/ui/src/components/link-reader-hovercard-interactions.test.ts +++ b/ui/src/components/link-reader-hovercard-interactions.test.ts @@ -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 = {}) { ? { 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>(); @@ -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>(); - 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(); 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>(); 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(); @@ -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); diff --git a/ui/src/components/provider-icon.test.ts b/ui/src/components/provider-icon.test.ts index 117dba1acadc..2430f13e9286 100644 --- a/ui/src/components/provider-icon.test.ts +++ b/ui/src/components/provider-icon.test.ts @@ -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"); diff --git a/ui/src/components/session-channel-identity.test.ts b/ui/src/components/session-channel-identity.test.ts index cd2ddb93536b..4cdf2ec5fa7c 100644 --- a/ui/src/components/session-channel-identity.test.ts +++ b/ui/src/components/session-channel-identity.test.ts @@ -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(); + }); }); diff --git a/ui/src/components/session-people-search.test.ts b/ui/src/components/session-people-search.test.ts index 586ed9261878..ab9620da2992 100644 --- a/ui/src/components/session-people-search.test.ts +++ b/ui/src/components/session-people-search.test.ts @@ -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('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('[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");