From 3c67fcc45d83bb8cd7eb9d7b076ae33e347bece5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Jul 2026 03:42:00 -0400 Subject: [PATCH] fix(release): stabilize beta validation and completion (#114396) * test(release): fix rebased validation gates * test(migrate-hermes): use canonical auth store fixture * test(secrets): reset runtime state before migration isolation * test(secrets): reload singleton graph for migration isolation * test(secrets): isolate auth migration state * fix(release): return completed validation run * docs: refresh docs map after forward-port --- extensions/migrate-hermes/secrets.test.ts | 21 ++- .../openai/video-generation-provider.test.ts | 14 +- extensions/xai/stream.test.ts | 9 +- scripts/full-release-validation-at-sha.mjs | 2 +- .../runtime.auth-migration-isolation.test.ts | 157 ++++++++++-------- .../full-release-validation-at-sha.test.ts | 1 + 6 files changed, 117 insertions(+), 87 deletions(-) diff --git a/extensions/migrate-hermes/secrets.test.ts b/extensions/migrate-hermes/secrets.test.ts index f7813724dbb7..f6ed1f89b4a5 100644 --- a/extensions/migrate-hermes/secrets.test.ts +++ b/extensions/migrate-hermes/secrets.test.ts @@ -707,17 +707,24 @@ describe("Hermes migration secret items", () => { const stateDir = path.join(root, "state"); const agentDir = path.join(stateDir, "agents", "main", "agent"); await writeFile(path.join(source, ".env"), "OPENAI_API_KEY=sk-hermes\n"); - await writeFile( - path.join(agentDir, "auth.json"), - JSON.stringify({ - openai: { type: "api_key", provider: "openai", key: "legacy-main-key" }, - }), - ); + const existingStore: AuthProfileStore = { + version: 1, + profiles: { + "openai:existing": { + type: "api_key", + provider: "openai", + key: "existing-main-key", + }, + }, + }; + writeAuthProfileStore(agentDir, existingStore); + const beforePlanStore = readAuthProfileStore(agentDir); const provider = buildHermesMigrationProvider(); await provider.plan(makeContext({ source, stateDir, workspaceDir, includeSecrets: true })); - await expect(fs.access(path.join(agentDir, "auth.json"))).resolves.toBeUndefined(); + expect(readAuthProfileStore(agentDir)).toEqual(beforePlanStore); + // Canonical profiles stay in SQLite; planning must not recreate the retired JSON sidecar. await expectMissingPath(path.join(agentDir, "auth-profiles.json")); }); diff --git a/extensions/openai/video-generation-provider.test.ts b/extensions/openai/video-generation-provider.test.ts index c9eedea0662d..bf8be0fcdf25 100644 --- a/extensions/openai/video-generation-provider.test.ts +++ b/extensions/openai/video-generation-provider.test.ts @@ -2,6 +2,10 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { + clearRuntimeAuthProfileStoreSnapshots, + saveAuthProfileStore, +} from "openclaw/plugin-sdk/agent-runtime"; import { getProviderHttpMocks, installProviderHttpMockCleanup, @@ -130,9 +134,8 @@ describe("openai video generation provider", () => { const previousOpenAIKey = process.env.OPENAI_API_KEY; delete process.env.OPENAI_API_KEY; try { - fs.writeFileSync( - path.join(agentDir, "auth-profiles.json"), - JSON.stringify({ + saveAuthProfileStore( + { version: 1, profiles: { "openai:chatgpt": { @@ -143,11 +146,14 @@ describe("openai video generation provider", () => { expires: Date.now() + 60_000, }, }, - }), + }, + agentDir, + { filterExternalAuthProfiles: false, syncExternalCli: false }, ); expect(buildOpenAIVideoGenerationProvider().isConfigured?.({ agentDir })).toBe(false); } finally { + clearRuntimeAuthProfileStoreSnapshots(); if (previousOpenAIKey === undefined) { delete process.env.OPENAI_API_KEY; } else { diff --git a/extensions/xai/stream.test.ts b/extensions/xai/stream.test.ts index e9ff011a7b0f..64fb6afdee10 100644 --- a/extensions/xai/stream.test.ts +++ b/extensions/xai/stream.test.ts @@ -17,6 +17,7 @@ import { } from "./test-helpers.js"; type XaiStreamApi = Extract; type StreamEvent = Record & { type?: string }; +const PAYLOAD_CAPTURE_TIMEOUT_MS = 5_000; async function collectEvents(stream: ReturnType): Promise { const events: StreamEvent[] = []; @@ -124,7 +125,7 @@ async function captureXaiResponsesPayloadWithThinking( const payloadPromise = new Promise>((resolve, reject) => { const timeout = setTimeout( () => reject(new Error("provider payload callback was not invoked")), - 1_000, + PAYLOAD_CAPTURE_TIMEOUT_MS, ); const stream = streamSimple( model, @@ -478,19 +479,19 @@ describe("xai stream wrappers", () => { expect(payload.reasoning).toEqual({ effort: "low", summary: "auto" }); expect(payload.include).toEqual(["reasoning.encrypted_content"]); - }); + }, 10_000); it("clamps unsupported Grok 4.5 off reasoning to low", async () => { const payload = await captureXaiResponsesPayloadWithThinking("off"); expect(payload.reasoning).toEqual({ effort: "low", summary: "auto" }); - }); + }, 10_000); it("maps Grok 4.3 off reasoning to xAI none", async () => { const payload = await captureXaiResponsesPayloadWithThinking("off", "grok-4.3"); expect(payload.reasoning).toEqual({ effort: "none" }); - }); + }, 10_000); it("moves image-bearing tool results out of function_call_output payloads", () => { const payload: Record = { diff --git a/scripts/full-release-validation-at-sha.mjs b/scripts/full-release-validation-at-sha.mjs index 71f14f00b5a8..57105c14adf6 100755 --- a/scripts/full-release-validation-at-sha.mjs +++ b/scripts/full-release-validation-at-sha.mjs @@ -299,7 +299,7 @@ function waitForWorkflowRun(parentRunId, workflowSha) { } if (suite?.status === "completed") { if (suite.conclusion === "success") { - return; + return suite; } throw new Error( `Full Release Validation concluded ${String(suite.conclusion).toLowerCase()}: https://github.com/openclaw/openclaw/actions/runs/${parentRunId}`, diff --git a/src/secrets/runtime.auth-migration-isolation.test.ts b/src/secrets/runtime.auth-migration-isolation.test.ts index 57f0d50a7471..e8a48661dc10 100644 --- a/src/secrets/runtime.auth-migration-isolation.test.ts +++ b/src/secrets/runtime.auth-migration-isolation.test.ts @@ -1,85 +1,100 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { writePersistedAuthProfileStoreRaw } from "../agents/auth-profiles/sqlite.js"; -import { resolveApiKeyForProvider } from "../agents/model-auth.js"; -import { closeOpenClawAgentDatabasesForTest } from "../state/openclaw-agent-db.js"; -import { - activateSecretsRuntimeSnapshot, - clearSecretsRuntimeSnapshot, - prepareSecretsRuntimeSnapshot, -} from "./runtime.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +let resolveApiKeyForProvider: typeof import("../agents/model-auth.js").resolveApiKeyForProvider; +let closeOpenClawAgentDatabasesForTest: typeof import("../state/openclaw-agent-db.js").closeOpenClawAgentDatabasesForTest; +let withOpenClawTestState: typeof import("../test-utils/openclaw-test-state.js").withOpenClawTestState; +let activateSecretsRuntimeSnapshot: typeof import("./runtime.js").activateSecretsRuntimeSnapshot; +let clearSecretsRuntimeSnapshot: typeof import("./runtime.js").clearSecretsRuntimeSnapshot; +let prepareSecretsRuntimeSnapshot: typeof import("./runtime.js").prepareSecretsRuntimeSnapshot; describe("auth profile migration isolation", () => { - const roots: string[] = []; - - afterEach(async () => { + beforeEach(async () => { + // This shard is non-isolated, so load the singleton-backed runtime and auth helpers + // from one fresh graph after neighboring tests reset the module cache. + vi.resetModules(); + ({ + activateSecretsRuntimeSnapshot, + clearSecretsRuntimeSnapshot, + prepareSecretsRuntimeSnapshot, + } = await import("./runtime.js")); + ({ resolveApiKeyForProvider } = await import("../agents/model-auth.js")); + ({ closeOpenClawAgentDatabasesForTest } = await import("../state/openclaw-agent-db.js")); + ({ withOpenClawTestState } = await import("../test-utils/openclaw-test-state.js")); + clearSecretsRuntimeSnapshot(); + closeOpenClawAgentDatabasesForTest(); + vi.unstubAllEnvs(); + }); + + afterEach(() => { clearSecretsRuntimeSnapshot(); closeOpenClawAgentDatabasesForTest(); vi.unstubAllEnvs(); - for (const root of roots.splice(0)) { - await fs.rm(root, { recursive: true, force: true }); - } }); it("isolates one legacy owner and blocks env fallback without affecting a healthy agent", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-migration-isolation-")); - roots.push(root); - const legacyAgentDir = path.join(root, "agents", "legacy", "agent"); - const healthyAgentDir = path.join(root, "agents", "healthy", "agent"); - await fs.mkdir(legacyAgentDir, { recursive: true }); - await fs.mkdir(healthyAgentDir, { recursive: true }); - await fs.writeFile( - path.join(legacyAgentDir, "auth.json"), - `${JSON.stringify({ openai: { type: "api_key", key: "fake-legacy-key" } })}\n`, - ); - writePersistedAuthProfileStoreRaw( + await withOpenClawTestState( { - version: 1, - profiles: { - "openai:default": { - type: "api_key", - provider: "openai", - key: "fake-healthy-key", - }, + layout: "state-only", + label: "auth-migration-isolation", + env: { + OPENAI_API_KEY: "fake-env-fallback-key", }, }, - healthyAgentDir, + async (state) => { + const legacyAgentDir = state.agentDir("legacy"); + const healthyAgentDir = state.agentDir("healthy"); + await state.writeText( + "agents/legacy/agent/auth.json", + `${JSON.stringify({ openai: { type: "api_key", key: "fake-legacy-key" } })}\n`, + ); + await state.writeAuthProfiles( + { + version: 1, + profiles: { + "openai:default": { + type: "api_key", + provider: "openai", + key: "fake-healthy-key", + }, + }, + }, + "healthy", + ); + + const snapshot = await prepareSecretsRuntimeSnapshot({ + config: {}, + env: state.env, + agentDirs: [legacyAgentDir, healthyAgentDir], + includeConfigRefs: false, + allowUnavailableSecretOwners: true, + loadablePluginOrigins: new Map(), + }); + expect(snapshot.degradedOwners).toEqual([ + expect.objectContaining({ + ownerKind: "route", + reason: "auth profile migration required", + paths: ["auth-profile-legacy:legacy-auth"], + }), + ]); + activateSecretsRuntimeSnapshot(snapshot); + + await expect( + resolveApiKeyForProvider({ provider: "openai", agentDir: legacyAgentDir }), + ).rejects.toMatchObject({ + code: "AUTH_PROFILE_MIGRATION_REQUIRED", + action: "openclaw doctor --fix", + sourceKinds: ["legacy-auth"], + }); + await expect( + resolveApiKeyForProvider({ + provider: "openai", + agentDir: healthyAgentDir, + profileId: "openai:default", + lockedProfile: true, + store: snapshot.authStores.find((entry) => entry.agentDir === healthyAgentDir)?.store, + }), + ).resolves.toMatchObject({ apiKey: "fake-healthy-key" }); + }, ); - vi.stubEnv("OPENAI_API_KEY", "fake-env-fallback-key"); - - const snapshot = await prepareSecretsRuntimeSnapshot({ - config: {}, - agentDirs: [legacyAgentDir, healthyAgentDir], - includeConfigRefs: false, - allowUnavailableSecretOwners: true, - loadablePluginOrigins: new Map(), - }); - expect(snapshot.degradedOwners).toEqual([ - expect.objectContaining({ - ownerKind: "route", - reason: "auth profile migration required", - paths: ["auth-profile-legacy:legacy-auth"], - }), - ]); - activateSecretsRuntimeSnapshot(snapshot); - - await expect( - resolveApiKeyForProvider({ provider: "openai", agentDir: legacyAgentDir }), - ).rejects.toMatchObject({ - code: "AUTH_PROFILE_MIGRATION_REQUIRED", - action: "openclaw doctor --fix", - sourceKinds: ["legacy-auth"], - }); - await expect( - resolveApiKeyForProvider({ - provider: "openai", - agentDir: healthyAgentDir, - profileId: "openai:default", - lockedProfile: true, - store: snapshot.authStores.find((entry) => entry.agentDir === healthyAgentDir)?.store, - }), - ).resolves.toMatchObject({ apiKey: "fake-healthy-key" }); }); }); diff --git a/test/scripts/full-release-validation-at-sha.test.ts b/test/scripts/full-release-validation-at-sha.test.ts index 9cae4cc5476f..50ee351104b9 100644 --- a/test/scripts/full-release-validation-at-sha.test.ts +++ b/test/scripts/full-release-validation-at-sha.test.ts @@ -152,6 +152,7 @@ describe("full-release-validation-at-sha", () => { const source = readFileSync("scripts/full-release-validation-at-sha.mjs", "utf8"); expect(source).toContain("actions/runs/${parentRunId}"); expect(source).toContain("workflowRun.head_sha !== workflowSha"); + expect(source).toContain("return suite;"); expect(source).not.toContain('"graphql"'); expect(source).not.toContain('["run", "watch"'); });