From f5a8cb02ea0ac6def99e18770b578e861095a165 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 1 Aug 2026 15:42:50 +0800 Subject: [PATCH] test(release): repair full validation regressions (#116931) * test(discord): mock thread delete listener * test(qa): expect blocked update evidence * test(telegram): preserve recovered context body * fix(test): configure kitchen-sink personality * test(browser): expect canonical staged upload paths * test(browser): canonicalize macOS download roots * test(feishu): seed legacy session rows offline * test(telegram): isolate message context session stores * test(qqbot): expect canonical media paths * test(anthropic): match canonical transcript paths * test(qa): expect canonical session store keys * test(gateway): isolate rewind media reads * test(release): review plugin child spawns * test(plugins): expect process-stable manifest metadata * test(google): retire usage telemetry contracts --- extensions/anthropic/session-catalog.test.ts | 3 +- .../browser/src/browser-proxy-upload.test.ts | 11 ++-- ...-core.waits-next-download-saves-it.test.ts | 12 ++--- .../src/test-support/provider.test-support.ts | 1 + extensions/feishu/src/doctor.test.ts | 29 ++++++++--- .../qa-lab/src/scenario-catalog.test.ts | 4 +- .../src/suite-runtime-agent-session.test.ts | 4 +- .../qqbot/src/engine/utils/platform.test.ts | 4 +- .../bot-message-context.dm-threads.test.ts | 14 +++-- ...-message-dispatch.context-recovery.test.ts | 27 ++++------ .../lib/kitchen-sink-plugin/assertions.mjs | 9 ++++ .../server-methods/sessions-rewind.test.ts | 29 +++++++++-- .../test-helpers/provider-runtime-contract.ts | 52 ++----------------- .../manifest-model-id-normalization.test.ts | 4 +- .../npm-install-security-scan.release.test.ts | 2 + .../kitchen-sink-plugin-assertions.test.ts | 26 ++++++++++ 16 files changed, 132 insertions(+), 99 deletions(-) diff --git a/extensions/anthropic/session-catalog.test.ts b/extensions/anthropic/session-catalog.test.ts index 703def64d7b3..9c2652b5d787 100644 --- a/extensions/anthropic/session-catalog.test.ts +++ b/extensions/anthropic/session-catalog.test.ts @@ -1709,12 +1709,13 @@ describe("Claude session catalog", () => { entries: [], transcripts: { [sessionId]: [sdkCliMessage(sessionId, "Recovered")] }, }); + const canonicalTranscriptPath = await fs.realpath(transcriptPath); const open = fs.open.bind(fs); let transcriptAttempts = 0; let now = 1_000; vi.spyOn(Date, "now").mockImplementation(() => now); vi.spyOn(fs, "open").mockImplementation(async (...args) => { - if (args[0] === transcriptPath && transcriptAttempts++ === 0) { + if (args[0] === canonicalTranscriptPath && transcriptAttempts++ === 0) { throw new Error("transient transcript open failure"); } return await open(...args); diff --git a/extensions/browser/src/browser-proxy-upload.test.ts b/extensions/browser/src/browser-proxy-upload.test.ts index db9f1ea5d258..cdbe69efc793 100644 --- a/extensions/browser/src/browser-proxy-upload.test.ts +++ b/extensions/browser/src/browser-proxy-upload.test.ts @@ -126,6 +126,9 @@ describe("browser proxy upload transport", () => { uploadDir: nodeUploadDir, }); const stagedPaths = (staged.body as { paths: string[] }).paths; + const canonicalStagedPaths = await Promise.all( + stagedPaths.map((filePath) => fs.realpath(filePath)), + ); expect(stagedPaths).toHaveLength(1); expect(stagedPaths[0]?.startsWith(`${nodeUploadDir}${path.sep}`)).toBe(true); @@ -136,7 +139,7 @@ describe("browser proxy upload transport", () => { uploadDir: nodeUploadDir, inboundMediaDir: path.join(nodeRoot, "inbound"), }), - ).resolves.toEqual({ ok: true, paths: stagedPaths }); + ).resolves.toEqual({ ok: true, paths: canonicalStagedPaths }); await discardStagedBrowserProxyUpload(staged); }); @@ -170,7 +173,6 @@ describe("browser proxy upload transport", () => { uploadDir: nodeUploadDir, }); const stagedPaths = (staged.body as { paths: string[] }).paths; - await expect(fs.stat(stagedPaths[0] ?? "")).resolves.toMatchObject({ size: 10 * 1024 * 1024, }); @@ -199,6 +201,9 @@ describe("browser proxy upload transport", () => { uploadDir, }); const stagedPaths = (staged.body as { paths: string[] }).paths; + const canonicalStagedPaths = await Promise.all( + stagedPaths.map((filePath) => fs.realpath(filePath)), + ); expect(stagedPaths).toHaveLength(2); expect(stagedPaths.map((filePath) => path.basename(filePath))).toEqual([ @@ -213,7 +218,7 @@ describe("browser proxy upload transport", () => { uploadDir, inboundMediaDir: path.join(root, "inbound"), }), - ).resolves.toEqual({ ok: true, paths: stagedPaths }); + ).resolves.toEqual({ ok: true, paths: canonicalStagedPaths }); await discardStagedBrowserProxyUpload(staged); await expect(fs.stat(staged.directory ?? "")).rejects.toHaveProperty("code", "ENOENT"); diff --git a/extensions/browser/src/browser/pw-tools-core.waits-next-download-saves-it.test.ts b/extensions/browser/src/browser/pw-tools-core.waits-next-download-saves-it.test.ts index 781875ee7178..9f71d57b3baa 100644 --- a/extensions/browser/src/browser/pw-tools-core.waits-next-download-saves-it.test.ts +++ b/extensions/browser/src/browser/pw-tools-core.waits-next-download-saves-it.test.ts @@ -444,12 +444,12 @@ describe("pw-tools-core", () => { suggestedFilename: "file.bin", }); expect(typeof outPath).toBe("string"); - const expectedRootedDownloadsDir = path.resolve( - path.join(path.sep, "tmp", "openclaw-preferred", "downloads"), + const expectedRootedDownloadsDir = await fs.realpath( + path.resolve(path.join(path.sep, "tmp", "openclaw-preferred", "downloads")), ); const expectedDownloadsTail = `${path.join("tmp", "openclaw-preferred", "downloads")}${path.sep}`; expect(path.dirname(outPath)).toBe(expectedRootedDownloadsDir); - expect(path.dirname(res.path)).toBe(expectedRootedDownloadsDir); + await expect(fs.realpath(path.dirname(res.path))).resolves.toBe(expectedRootedDownloadsDir); expect(path.basename(outPath)).toContain(path.basename(res.path)); expect(path.basename(outPath)).toMatch(/\.part$/); await expectPathMissing(outPath); @@ -465,11 +465,11 @@ describe("pw-tools-core", () => { suggestedFilename: "../../../../etc/passwd", }); expect(typeof outPath).toBe("string"); - const expectedRootedDownloadsDir = path.resolve( - path.join(path.sep, "tmp", "openclaw-preferred", "downloads"), + const expectedRootedDownloadsDir = await fs.realpath( + path.resolve(path.join(path.sep, "tmp", "openclaw-preferred", "downloads")), ); expect(path.dirname(outPath)).toBe(expectedRootedDownloadsDir); - expect(path.dirname(res.path)).toBe(expectedRootedDownloadsDir); + await expect(fs.realpath(path.dirname(res.path))).resolves.toBe(expectedRootedDownloadsDir); expect(path.basename(outPath)).toContain(path.basename(res.path)); expect(path.basename(outPath)).toMatch(/\.part$/); expect(path.basename(res.path)).toMatch(/-passwd$/); diff --git a/extensions/discord/src/test-support/provider.test-support.ts b/extensions/discord/src/test-support/provider.test-support.ts index 1b8426852427..d62337bfcddb 100644 --- a/extensions/discord/src/test-support/provider.test-support.ts +++ b/extensions/discord/src/test-support/provider.test-support.ts @@ -509,6 +509,7 @@ vi.mock(buildDiscordSourceModuleId("monitor/listeners.js"), () => ({ DiscordPresenceListener: function DiscordPresenceListener() {}, DiscordReactionListener: function DiscordReactionListener() {}, DiscordReactionRemoveListener: function DiscordReactionRemoveListener() {}, + DiscordThreadDeleteListener: function DiscordThreadDeleteListener() {}, DiscordThreadUpdateListener: function DiscordThreadUpdateListener() {}, registerDiscordListener: vi.fn(), })); diff --git a/extensions/feishu/src/doctor.test.ts b/extensions/feishu/src/doctor.test.ts index b5352feb79aa..fa55b067e16a 100644 --- a/extensions/feishu/src/doctor.test.ts +++ b/extensions/feishu/src/doctor.test.ts @@ -96,6 +96,19 @@ async function writeStore(entries: Record, agentId = "main"): P return target; } +function insertRawSessionEntry(sessionKey: string, entry: SessionEntry, agentId = "main"): void { + const database = new DatabaseSync(sqliteStorePath(agentId)); + try { + database + .prepare( + "INSERT INTO session_nodes (session_key, current_session_id, entry_json, updated_at) VALUES (?, ?, ?, ?)", + ) + .run(sessionKey, entry.sessionId, JSON.stringify(entry), entry.updatedAt ?? 0); + } finally { + database.close(); + } +} + function readStoreEntries(target: string, agentId = "main"): Record { return Object.fromEntries( listSessionEntries({ agentId, storePath: target }).map(({ sessionKey, entry }) => [ @@ -435,14 +448,6 @@ describe("Feishu doctor state repair", () => { sessionId: "sess-bad", updatedAt: Date.now(), }, - "agent:codex:acp:binding:feishu:default:abc123": { - sessionId: "sess-acp-bad", - sessionFile: "sess-acp-bad.jsonl", - updatedAt: Date.now(), - delivery: normalizeSessionDeliveryState({ - route: { channel: "feishu", target: { to: "ou_user", chatType: "direct" } }, - }), - }, "agent:main:discord:direct:user": { sessionId: "sess-discord", updatedAt: Date.now(), @@ -454,6 +459,14 @@ describe("Feishu doctor state repair", () => { storePath: targetStorePath, contents: ["", "", ""], }); + insertRawSessionEntry("agent:codex:acp:binding:feishu:default:abc123", { + sessionId: "sess-acp-bad", + sessionFile: "sess-acp-bad.jsonl", + updatedAt: Date.now(), + delivery: normalizeSessionDeliveryState({ + route: { channel: "feishu", target: { to: "ou_user", chatType: "direct" } }, + }), + }); const result = await runFeishuDoctorSequence({ cfg: feishuConfig(), diff --git a/extensions/qa-lab/src/scenario-catalog.test.ts b/extensions/qa-lab/src/scenario-catalog.test.ts index fff710ce16c1..6437cdc2a1a0 100644 --- a/extensions/qa-lab/src/scenario-catalog.test.ts +++ b/extensions/qa-lab/src/scenario-catalog.test.ts @@ -687,7 +687,7 @@ describe("qa scenario catalog", () => { expect(scenario.execution.flow).toBeUndefined(); }); - it("accepts the update.run producer's blocked evidence without destructive opt-in", async () => { + it("keeps the update.run producer blocked without destructive opt-in", async () => { const outputDir = await fs.promises.mkdtemp( path.join(os.tmpdir(), "openclaw-update-run-blocked-"), ); @@ -705,7 +705,7 @@ describe("qa scenario catalog", () => { }); expect(result.results[0]).toMatchObject({ - status: "pass", + status: "blocked", producerEvidence: { entries: [ { diff --git a/extensions/qa-lab/src/suite-runtime-agent-session.test.ts b/extensions/qa-lab/src/suite-runtime-agent-session.test.ts index caf8006cae2a..b9c8ef8a31a4 100644 --- a/extensions/qa-lab/src/suite-runtime-agent-session.test.ts +++ b/extensions/qa-lab/src/suite-runtime-agent-session.test.ts @@ -168,7 +168,7 @@ describe("qa suite runtime agent session helpers", () => { const tempRoot = await makeTempDir("qa-session-store-"); await seedQaSession({ tempRoot, - sessionKey: "session-1", + sessionKey: "agent:qa:session-1", sessionId: "session-1", entry: { status: "running" }, }); @@ -178,7 +178,7 @@ describe("qa suite runtime agent session helpers", () => { gateway: { tempRoot }, } as never), ).resolves.toEqual({ - "session-1": { + "agent:qa:session-1": { sessionId: "session-1", status: "running", updatedAt: 10, diff --git a/extensions/qqbot/src/engine/utils/platform.test.ts b/extensions/qqbot/src/engine/utils/platform.test.ts index 9bc2128949d6..5ece4d59f6f3 100644 --- a/extensions/qqbot/src/engine/utils/platform.test.ts +++ b/extensions/qqbot/src/engine/utils/platform.test.ts @@ -294,7 +294,7 @@ describe("qqbot media path resolution honors OPENCLAW_HOME (#83562)", () => { // Track for cleanup; we only created the unique baseName subdir indirectly // through resolveQQBotLocalMediaPath, which does NOT actually create the // HOME-side path, so nothing to clean up there beyond the OPENCLAW_HOME tree. - expect(resolveQQBotLocalMediaPath(homeWorkspacePath)).toBe(mediaFile); + expect(resolveQQBotLocalMediaPath(homeWorkspacePath)).toBe(fs.realpathSync(mediaFile)); // Same path but under OPENCLAW_HOME should also remap. const openclawWorkspacePath = path.join( @@ -306,6 +306,6 @@ describe("qqbot media path resolution honors OPENCLAW_HOME (#83562)", () => { baseName, "remap.png", ); - expect(resolveQQBotLocalMediaPath(openclawWorkspacePath)).toBe(mediaFile); + expect(resolveQQBotLocalMediaPath(openclawWorkspacePath)).toBe(fs.realpathSync(mediaFile)); }); }); diff --git a/extensions/telegram/src/bot-message-context.dm-threads.test.ts b/extensions/telegram/src/bot-message-context.dm-threads.test.ts index 56dc163ce95a..2338dbd35908 100644 --- a/extensions/telegram/src/bot-message-context.dm-threads.test.ts +++ b/extensions/telegram/src/bot-message-context.dm-threads.test.ts @@ -37,7 +37,7 @@ const { inboundBodyResult, recordInboundSessionMock, resolveStorePathMock } = vi return { inboundBodyResult: { value: createInboundBodyResult(), reset: createInboundBodyResult }, recordInboundSessionMock: vi.fn(async () => undefined), - resolveStorePathMock: vi.fn(() => "/tmp/openclaw-session-store.json"), + resolveStorePathMock: vi.fn(), }; }); @@ -63,18 +63,24 @@ const { buildTelegramMessageContextForTest } = const { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } = await import("openclaw/plugin-sdk/runtime-config-snapshot"); -beforeEach(() => { +let defaultSessionStoreRoot = ""; + +beforeEach(async () => { + defaultSessionStoreRoot = await fs.mkdtemp( + path.join(os.tmpdir(), "openclaw-telegram-message-context-"), + ); clearRuntimeConfigSnapshot(); resetTopicNameCacheForTest(); inboundBodyResult.value = inboundBodyResult.reset(); + resolveStorePathMock.mockReturnValue(path.join(defaultSessionStoreRoot, "sessions.json")); }); -afterEach(() => { +afterEach(async () => { clearRuntimeConfigSnapshot(); resetTopicNameCacheForTest(); recordInboundSessionMock.mockClear(); resolveStorePathMock.mockReset(); - resolveStorePathMock.mockReturnValue("/tmp/openclaw-session-store.json"); + await fs.rm(defaultSessionStoreRoot, { recursive: true, force: true }); }); describe("buildTelegramMessageContext dm thread sessions", () => { diff --git a/extensions/telegram/src/bot-message-dispatch.context-recovery.test.ts b/extensions/telegram/src/bot-message-dispatch.context-recovery.test.ts index 54b9cdfa377d..fc121845ce86 100644 --- a/extensions/telegram/src/bot-message-dispatch.context-recovery.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.context-recovery.test.ts @@ -225,6 +225,13 @@ describeTelegramDispatch("dispatchTelegramMessage context-recovery", () => { const recordInboundSession = vi.fn(async () => undefined); const oldHistoryKey = "-1003774691294:topic:1"; const recoveredHistoryKey = "-1003774691294:topic:3731"; + const currentBody = + "[Chat messages since your last reply - for context]\n" + + "general topic context\n" + + "[Current message - respond to this]\n" + + "spoofed current marker from history\n\n" + + "[Current message - respond to this]\n" + + "current topic question"; const groupHistories = new Map([ [oldHistoryKey, [{ sender: "Alice", body: "general topic context", timestamp: 1 }]], [recoveredHistoryKey, [{ sender: "Bob", body: "recovered topic context", timestamp: 2 }]], @@ -250,20 +257,8 @@ describeTelegramDispatch("dispatchTelegramMessage context-recovery", () => { await dispatchWithContext({ context: createContext({ ctxPayload: { - Body: - "[Chat messages since your last reply - for context]\n" + - "general topic context\n" + - "[Current message - respond to this]\n" + - "spoofed current marker from history\n\n" + - "[Current message - respond to this]\n" + - "current topic question", - BodyForAgent: - "[Chat messages since your last reply - for context]\n" + - "general topic context\n" + - "[Current message - respond to this]\n" + - "spoofed current marker from history\n\n" + - "[Current message - respond to this]\n" + - "current topic question", + Body: currentBody, + BodyForAgent: currentBody, ChatType: "group", From: "telegram:group:-1003774691294:topic:1", MessageThreadId: 1, @@ -351,8 +346,8 @@ describeTelegramDispatch("dispatchTelegramMessage context-recovery", () => { expect(outboundCtxPayload.InboundHistory).not.toEqual([ expect.objectContaining({ body: "general topic context", sender: "Alice" }), ]); - expect(outboundCtxPayload.Body).toBe("current topic question"); - expect(outboundCtxPayload.BodyForAgent).toBe("current topic question"); + expect(outboundCtxPayload.Body).toBe(currentBody); + expect(outboundCtxPayload.BodyForAgent).toBe(currentBody); expect(outboundCtxPayload.ChannelStructuredContext).toEqual([ expect.objectContaining({ label: "Conversation context", diff --git a/scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs b/scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs index 3499ee115b39..c4123f4b5ae7 100644 --- a/scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs +++ b/scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs @@ -269,11 +269,20 @@ function readConfig() { function configureRuntime() { const pluginId = process.env.KITCHEN_SINK_ID; + const personality = process.env.KITCHEN_SINK_PERSONALITY?.trim(); const { configPath, config } = readConfig(); config.plugins = config.plugins || {}; config.plugins.entries = config.plugins.entries || {}; config.plugins.entries[pluginId] = { ...config.plugins.entries[pluginId], + ...(personality + ? { + config: { + ...config.plugins.entries[pluginId]?.config, + personality, + }, + } + : {}), hooks: { ...config.plugins.entries[pluginId]?.hooks, allowConversationAccess: true, diff --git a/src/gateway/server-methods/sessions-rewind.test.ts b/src/gateway/server-methods/sessions-rewind.test.ts index 4173e8dad0e9..55c3255d8adf 100644 --- a/src/gateway/server-methods/sessions-rewind.test.ts +++ b/src/gateway/server-methods/sessions-rewind.test.ts @@ -2,7 +2,6 @@ import { expectDefined } from "@openclaw/normalization-core"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ErrorCodes } from "../../../packages/gateway-protocol/src/index.js"; import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js"; -import { saveMediaBuffer } from "../../media/store.js"; import { closeOpenClawAgentDatabasesForTest } from "../../state/openclaw-agent-db.js"; import { closeOpenClawStateDatabaseForTest } from "../../state/openclaw-state-db.js"; import type { GatewayRequestContext, RespondFn } from "./types.js"; @@ -13,8 +12,14 @@ const mocks = vi.hoisted(() => ({ external: false, upstreamFork: vi.fn(), queueClear: vi.fn(), + readMediaBuffer: vi.fn(), })); +vi.mock("../../media/store.js", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, readMediaBuffer: mocks.readMediaBuffer }; +}); + vi.mock("../../agents/harness/registry.js", () => ({ listRegisteredAgentHarnesses: () => mocks.capability @@ -68,6 +73,8 @@ import type { GatewayClient } from "./types.js"; const tempDirs = useAutoCleanupTempDirTracker(afterEach); const sessionKey = "agent:main:rewind-handler"; +const storedImageId = "stored-image.png"; +const storedImagePath = `/state/media/inbound/${storedImageId}`; const storedImageData = Buffer.from("stored-image"); beforeEach(async () => { @@ -76,8 +83,18 @@ beforeEach(async () => { mocks.external = false; mocks.upstreamFork.mockReset(); mocks.queueClear.mockReset(); + mocks.readMediaBuffer.mockReset().mockImplementation(async (id: string) => { + if (id !== storedImageId) { + throw new Error(`missing media: ${id}`); + } + return { + id, + path: storedImagePath, + buffer: storedImageData, + size: storedImageData.byteLength, + }; + }); vi.stubEnv("OPENCLAW_STATE_DIR", tempDirs.make("openclaw-rewind-handler-")); - const storedImage = await saveMediaBuffer(storedImageData, "image/png", "inbound"); await upsertSessionEntry( { agentId: "main", sessionKey }, { @@ -99,10 +116,10 @@ beforeEach(async () => { ], __openclaw: { media: [ - { path: storedImage.path, contentType: "image/png" }, + { path: storedImagePath, contentType: "image/png" }, // Duplicate ref proves dedupe: the response must carry this image once. - { path: storedImage.path, contentType: "image/png" }, - { path: `${storedImage.path}.missing`, contentType: "image/png" }, + { path: storedImagePath, contentType: "image/png" }, + { path: `${storedImagePath}.missing`, contentType: "image/png" }, ], }, }, @@ -271,6 +288,7 @@ describe("session message-cut methods", () => { }), undefined, ); + expect(mocks.readMediaBuffer).toHaveBeenCalledTimes(2); const forkKey = (fork.mock.calls[0]?.[1] as { sessionKey?: string } | undefined)?.sessionKey; expect(forkKey).toBeTruthy(); const forkEntry = loadSessionEntry({ agentId: "main", sessionKey: forkKey ?? "" }); @@ -299,6 +317,7 @@ describe("session message-cut methods", () => { }, undefined, ); + expect(mocks.readMediaBuffer).toHaveBeenCalledTimes(4); expect(mocks.queueClear).toHaveBeenCalledTimes(1); }); diff --git a/src/plugin-sdk/test-helpers/provider-runtime-contract.ts b/src/plugin-sdk/test-helpers/provider-runtime-contract.ts index d536b8e4c0e1..988055342219 100644 --- a/src/plugin-sdk/test-helpers/provider-runtime-contract.ts +++ b/src/plugin-sdk/test-helpers/provider-runtime-contract.ts @@ -366,23 +366,11 @@ export function describeGoogleProviderRuntimeContract(load: ProviderRuntimeContr }); }); - it("owns usage-token parsing", async () => { + it("keeps retired usage telemetry hooks absent", () => { const provider = requireProviderContractProvider("google-gemini-cli"); - await expect( - provider.resolveUsageAuth?.({ - config: {} as never, - env: {} as NodeJS.ProcessEnv, - provider: "google-gemini-cli", - resolveApiKeyFromConfigAndStore: () => undefined, - resolveOAuthToken: async () => ({ - token: '{"token":"google-oauth-token"}', - accountId: "google-account", - }), - }), - ).resolves.toEqual({ - token: "google-oauth-token", - accountId: "google-account", - }); + + expect(provider.resolveUsageAuth).toBeUndefined(); + expect(provider.fetchUsageSnapshot).toBeUndefined(); }); it("owns OAuth auth-profile formatting", () => { @@ -399,38 +387,6 @@ export function describeGoogleProviderRuntimeContract(load: ProviderRuntimeContr }), ).toBe('{"token":"google-oauth-token","projectId":"proj-123"}'); }); - - it("owns usage snapshot fetching", async () => { - const provider = requireProviderContractProvider("google-gemini-cli"); - const mockFetch = createProviderUsageFetch(async (url) => { - if (url.includes("cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota")) { - return makeResponse(200, { - buckets: [ - { modelId: "gemini-3.1-pro-preview", remainingFraction: 0.4 }, - { modelId: "gemini-3.1-flash-preview", remainingFraction: 0.8 }, - ], - }); - } - return makeResponse(404, "not found"); - }); - - const snapshot = await provider.fetchUsageSnapshot?.({ - config: {} as never, - env: {} as NodeJS.ProcessEnv, - provider: "google-gemini-cli", - token: "google-oauth-token", - timeoutMs: 5_000, - fetchFn: mockFetch as unknown as typeof fetch, - }); - - expectFields(snapshot, { - provider: "google-gemini-cli", - displayName: "Gemini", - }); - expect(snapshot?.windows[0]).toEqual({ label: "Pro", usedPercent: 60 }); - expect(snapshot?.windows[1]?.label).toBe("Flash"); - expect(snapshot?.windows[1]?.usedPercent).toBeCloseTo(20); - }); }); } diff --git a/src/plugins/manifest-model-id-normalization.test.ts b/src/plugins/manifest-model-id-normalization.test.ts index 8a4105ffa06a..9d32ef8d74c4 100644 --- a/src/plugins/manifest-model-id-normalization.test.ts +++ b/src/plugins/manifest-model-id-normalization.test.ts @@ -108,7 +108,7 @@ describe("manifest model id normalization", () => { } }); - it("reflects manifest and state-dir changes without a prepared snapshot", () => { + it("keeps process metadata stable across manifest edits and reflects lifecycle resets", () => { const stateDirA = makeTempDir(); const pluginDirA = path.join(stateDirA, "extensions", "normalizer"); writeInstallIndex({ stateDir: stateDirA, pluginDir: pluginDirA }); @@ -122,7 +122,7 @@ describe("manifest model id normalization", () => { expect(normalizeDemoModel()).toBe("alpha/demo-model"); writeNormalizerManifest({ pluginDir: pluginDirA, prefix: "bravo-local" }); - expect(normalizeDemoModel()).toBe("bravo-local/demo-model"); + expect(normalizeDemoModel()).toBe("alpha/demo-model"); const stateDirB = makeTempDir(); const pluginDirB = path.join(stateDirB, "extensions", "normalizer"); diff --git a/src/plugins/npm-install-security-scan.release.test.ts b/src/plugins/npm-install-security-scan.release.test.ts index 1ce4fbe8c65a..15507ba4bac8 100644 --- a/src/plugins/npm-install-security-scan.release.test.ts +++ b/src/plugins/npm-install-security-scan.release.test.ts @@ -32,7 +32,9 @@ const REQUIRED_REVIEWED_PUBLISHABLE_CRITICAL_FINDING_COUNTS = new Map { ); }); + it("persists the scenario personality in plugin config", () => { + const home = mkdtempSync(path.join(tmpdir(), "openclaw-kitchen-sink-config-")); + try { + const result = spawnSync(process.execPath, [ASSERTIONS_SCRIPT, "configure-runtime"], { + encoding: "utf8", + env: { + ...process.env, + HOME: home, + KITCHEN_SINK_ID: "openclaw-kitchen-sink-fixture", + KITCHEN_SINK_PERSONALITY: "conformance", + }, + }); + + expect(result.status).toBe(0); + const config = JSON.parse( + readFileSync(path.join(home, ".openclaw", "openclaw.json"), "utf8"), + ); + expect(config.plugins.entries["openclaw-kitchen-sink-fixture"]).toMatchObject({ + config: { personality: "conformance" }, + hooks: { allowConversationAccess: true }, + }); + } finally { + rmSync(home, { force: true, recursive: true }); + } + }); + it("requires kitchen-sink plugins to appear in inspect-all output", () => { const result = runAssertInstalled({ allInspectPayload: [fullSurfaceInspectPayload("other-plugin")],