diff --git a/extensions/migrate-hermes/secrets.test.ts b/extensions/migrate-hermes/secrets.test.ts index 42f573ea13e9..f025355225d5 100644 --- a/extensions/migrate-hermes/secrets.test.ts +++ b/extensions/migrate-hermes/secrets.test.ts @@ -658,6 +658,7 @@ describe("Hermes migration secret items", () => { reportDir, }); const plan = await provider.plan(ctx); + const plannedTarget = authProfileTarget(agentDir, "openai:hermes-import"); writeAuthProfileStore(agentDir, { version: 1, profiles: { @@ -677,7 +678,7 @@ describe("Hermes migration secret items", () => { kind: "secret", action: "create", source: path.join(source, ".env"), - target: authProfileTarget(agentDir, "openai:hermes-import"), + target: plannedTarget, status: "conflict", sensitive: true, reason: HERMES_REASON_AUTH_PROFILE_EXISTS, diff --git a/src/agents/auth-profiles/path-resolve.shared-store.test.ts b/src/agents/auth-profiles/path-resolve.shared-store.test.ts index 5895b80418e7..79e57e269d27 100644 --- a/src/agents/auth-profiles/path-resolve.shared-store.test.ts +++ b/src/agents/auth-profiles/path-resolve.shared-store.test.ts @@ -1,9 +1,13 @@ +import { existsSync } from "node:fs"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js"; import { writeConfigMachineState } from "../../state/config-machine-state.js"; import { closeOpenClawStateDatabaseForTest } from "../../state/openclaw-state-db.js"; import { resolveOpenClawStateSqlitePath } from "../../state/openclaw-state-db.paths.js"; +import { withEnv } from "../../test-utils/env.js"; +import { resolveAuthStatePathForDisplay, resolveAuthStorePathForDisplay } from "./paths.js"; +import { writePersistedAuthProfileStoreRaw } from "./sqlite.js"; const tempDirs = useAutoCleanupTempDirTracker(afterEach); @@ -38,6 +42,14 @@ describe("shared auth store path resolution", () => { expect(resolveSharedAuthStorePath(aliasEnv)).toBe( path.join(legacyDir, "openclaw-agent.sqlite"), ); + + withEnv({ OPENCLAW_STATE_DIR: env.OPENCLAW_STATE_DIR, OPENCLAW_AGENT_DIR: undefined }, () => { + writePersistedAuthProfileStoreRaw({ version: 1, profiles: {} }, legacyDir); + const expectedPath = path.join(legacyDir, "openclaw-agent.sqlite"); + expect(resolveAuthStorePathForDisplay(legacyDir)).toBe(expectedPath); + expect(resolveAuthStatePathForDisplay(legacyDir)).toBe(expectedPath); + expect(existsSync(expectedPath)).toBe(true); + }); }); it("resolves the relocated store to the canonical shared state database", async () => { @@ -48,6 +60,29 @@ describe("shared auth store path resolution", () => { expect(resolveSharedAuthStoreOwnership(env)).toEqual({ location: "state-db" }); expect(resolveSharedAuthStorePath(env)).toBe(resolveOpenClawStateSqlitePath(env)); + + withEnv({ OPENCLAW_STATE_DIR: env.OPENCLAW_STATE_DIR, OPENCLAW_AGENT_DIR: undefined }, () => { + writePersistedAuthProfileStoreRaw({ version: 1, profiles: {} }); + const agentDir = path.join(env.OPENCLAW_STATE_DIR ?? "", "agents", "helper", "agent"); + const expectedPath = resolveOpenClawStateSqlitePath(env); + expect(resolveAuthStorePathForDisplay(agentDir)).toBe(expectedPath); + expect(resolveAuthStatePathForDisplay(agentDir)).toBe(expectedPath); + expect(existsSync(expectedPath)).toBe(true); + }); + }); + + it("keeps an agent-local store local under shared-state ownership", async () => { + const env = makeStateEnv(); + writeConfigMachineState("auth.sharedStore", { location: "state-db" }, { env }); + const agentDir = path.join(env.OPENCLAW_STATE_DIR ?? "", "agents", "helper", "agent"); + + withEnv({ OPENCLAW_STATE_DIR: env.OPENCLAW_STATE_DIR, OPENCLAW_AGENT_DIR: undefined }, () => { + writePersistedAuthProfileStoreRaw({ version: 1, profiles: {} }, agentDir); + const expectedPath = path.join(agentDir, "openclaw-agent.sqlite"); + expect(resolveAuthStorePathForDisplay(agentDir)).toBe(expectedPath); + expect(resolveAuthStatePathForDisplay(agentDir)).toBe(expectedPath); + expect(existsSync(expectedPath)).toBe(true); + }); }); it("caches ownership independently for each canonical state root", async () => { diff --git a/src/agents/auth-profiles/path-resolve.ts b/src/agents/auth-profiles/path-resolve.ts index 3d1261d91aab..5aff42b2ffe4 100644 --- a/src/agents/auth-profiles/path-resolve.ts +++ b/src/agents/auth-profiles/path-resolve.ts @@ -1,13 +1,12 @@ /** * Auth profile path resolution. - * Centralizes canonical SQLite display paths and cross-agent OAuth refresh lock paths. + * Centralizes canonical shared SQLite and cross-agent OAuth refresh lock paths. */ import path from "node:path"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { resolveStateDir } from "../../config/paths.js"; import { readConfigMachineState } from "../../state/config-machine-state.js"; import { resolveOpenClawStateSqlitePath } from "../../state/openclaw-state-db.paths.js"; -import { resolveUserPath } from "../../utils.js"; import { resolveSharedMainAuthAgentDir } from "./shared-main-dir.js"; export const SHARED_AUTH_STORE_STATE_KEY = "auth.sharedStore"; @@ -84,22 +83,6 @@ export function resolveSharedAuthStorePath(env: NodeJS.ProcessEnv = process.env) return path.join(resolveSharedMainAuthAgentDir(env), "openclaw-agent.sqlite"); } -/** Resolve the user-facing auth profile database path. */ -export function resolveAuthStorePathForDisplay(agentDir?: string): string { - const pathname = agentDir - ? path.join(resolveUserPath(agentDir), "openclaw-agent.sqlite") - : resolveSharedAuthStorePath(); - return pathname.startsWith("~") ? pathname : resolveUserPath(pathname); -} - -/** Resolve the user-facing auth state database path. */ -export function resolveAuthStatePathForDisplay(agentDir?: string): string { - const pathname = agentDir - ? path.join(resolveUserPath(agentDir), "openclaw-agent.sqlite") - : resolveSharedAuthStorePath(); - return pathname.startsWith("~") ? pathname : resolveUserPath(pathname); -} - /** * Resolve the path of the cross-agent, per-profile OAuth refresh coordination * lock. The filename digests a JSON tuple of `[provider, profileId]` so it is diff --git a/src/agents/auth-profiles/paths-direct-import.test.ts b/src/agents/auth-profiles/paths-direct-import.test.ts index 6c14bd0a2a89..bdb0d5bcd9a2 100644 --- a/src/agents/auth-profiles/paths-direct-import.test.ts +++ b/src/agents/auth-profiles/paths-direct-import.test.ts @@ -1,29 +1,27 @@ /** * Direct-import tests for auth profile path helpers. - * Calls path-resolve exports directly so coverage attribution stays honest - * despite the public paths.ts re-export barrel. + * Calls the owning modules directly so coverage attribution stays honest. */ -import fs from "node:fs/promises"; -import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.js"; import { resolveLegacyAuthProfilesPath as resolveAuthStorePath, resolveLegacyAuthStatePath as resolveAuthStatePath, resolveLegacyFlatAuthPath as resolveLegacyAuthStorePath, } from "../../commands/doctor-auth-legacy-paths.js"; import { withEnv } from "../../test-utils/env.js"; -import { resolveAuthStatePathForDisplay, resolveAuthStorePathForDisplay } from "./path-resolve.js"; +import { resolveSharedAuthStorePath } from "./path-resolve.js"; +import { resolveAuthStatePathForDisplay, resolveAuthStorePathForDisplay } from "./paths.js"; +import { writePersistedAuthProfileStoreRaw } from "./sqlite.js"; -describe("path-resolve helpers (direct-import coverage attribution)", () => { +const tempDirs = useAutoCleanupTempDirTracker(afterEach); + +describe("auth profile path helpers (direct-import coverage attribution)", () => { let stateDir = ""; - beforeEach(async () => { - stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-path-direct-")); - }); - - afterEach(async () => { - await fs.rm(stateDir, { recursive: true, force: true }); + beforeEach(() => { + stateDir = tempDirs.make("openclaw-path-direct-"); }); it("resolveAuthStorePath joins agentDir with the auth-profiles filename", () => { @@ -80,22 +78,23 @@ describe("path-resolve helpers (direct-import coverage attribution)", () => { }); }); - it("resolveAuthStorePathForDisplay returns the resolved path for a non-tilde input", () => { + it("uses one database path for an agent-local auth store and its runtime state", () => { const agentDir = path.join(stateDir, "agents", "main", "agent"); - const resolved = resolveAuthStorePathForDisplay(agentDir); - expect(resolved.startsWith(stateDir)).toBe(true); - expect(path.basename(resolved)).toBe("openclaw-agent.sqlite"); + withEnv({ OPENCLAW_STATE_DIR: stateDir }, () => { + writePersistedAuthProfileStoreRaw({ version: 1, profiles: {} }, agentDir); + const expectedPath = path.join(agentDir, "openclaw-agent.sqlite"); + expect(resolveAuthStorePathForDisplay(agentDir)).toBe(expectedPath); + expect(resolveAuthStatePathForDisplay(agentDir)).toBe(expectedPath); + }); }); - it("resolveAuthStorePathForDisplay expands a tilde-rooted agent dir to the sqlite store", () => { - const tildeAgentDir = "~fake-openclaw-no-expand"; - const resolved = resolveAuthStorePathForDisplay(tildeAgentDir); - expect(resolved).toBe(path.resolve(tildeAgentDir, "openclaw-agent.sqlite")); - }); - - it("resolveAuthStatePathForDisplay returns the sqlite auth state store", () => { - const agentDir = path.join(stateDir, "agents", "main", "agent"); - const resolved = resolveAuthStatePathForDisplay(agentDir); - expect(resolved).toBe(path.join(agentDir, "openclaw-agent.sqlite")); + it("falls back to the shared owner for an agent dir that has no local store", () => { + withEnv({ OPENCLAW_STATE_DIR: stateDir }, () => { + // A tilde-rooted dir resolveUserPath cannot expand still must not be reported as the owner: + // without a local store the loader reads the shared database, so display must name that. + const resolved = resolveAuthStorePathForDisplay("~fake-openclaw-no-expand"); + expect(resolved).toBe(resolveSharedAuthStorePath()); + expect(resolved.startsWith("~")).toBe(false); + }); }); }); diff --git a/src/agents/auth-profiles/paths.ts b/src/agents/auth-profiles/paths.ts index d96307cff2db..6ffa13599f34 100644 --- a/src/agents/auth-profiles/paths.ts +++ b/src/agents/auth-profiles/paths.ts @@ -2,8 +2,23 @@ * Public path barrel for auth-profile stores. * Import through this file for canonical SQLite display and lock paths. */ -export { - resolveAuthStatePathForDisplay, - resolveAuthStorePathForDisplay, - resolveOAuthRefreshLockPath, -} from "./path-resolve.js"; +import path from "node:path"; +import { resolveUserPath } from "../../utils.js"; +import { resolveOAuthRefreshLockPath, resolveSharedAuthStorePath } from "./path-resolve.js"; +import { hasLocalAuthProfileStoreSource } from "./source-check.js"; + +export { resolveOAuthRefreshLockPath }; + +/** Resolve the user-facing path for the database selected by the auth store loader. */ +export function resolveAuthStorePathForDisplay(agentDir?: string): string { + const pathname = + agentDir && hasLocalAuthProfileStoreSource(agentDir) + ? path.join(resolveUserPath(agentDir), "openclaw-agent.sqlite") + : resolveSharedAuthStorePath(); + return pathname.startsWith("~") ? pathname : resolveUserPath(pathname); +} + +/** Retained name for callers that present auth runtime state from the same selected store. */ +export function resolveAuthStatePathForDisplay(agentDir?: string): string { + return resolveAuthStorePathForDisplay(agentDir); +} diff --git a/src/agents/model-auth-provider.ts b/src/agents/model-auth-provider.ts index eb1943e20233..9302c15493ce 100644 --- a/src/agents/model-auth-provider.ts +++ b/src/agents/model-auth-provider.ts @@ -1,7 +1,6 @@ /** * Ordered credential resolution for one provider request. */ -import path from "node:path"; import { formatCliCommand } from "../cli/command-format.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { formatErrorMessage } from "../infra/errors.js"; @@ -12,6 +11,7 @@ import { } from "../plugins/provider-runtime.js"; import { resolveOwningPluginIdsForProviderRef } from "../plugins/providers.js"; import { SecretSurfaceUnavailableError } from "../secrets/runtime-degraded-state.js"; +import { resolveUserPath } from "../utils.js"; import { resolveDefaultAgentDir } from "./agent-scope-config.js"; import { type AuthProfileStore, @@ -603,13 +603,13 @@ export async function resolveApiKeyForProviderCore(params: { } const authStorePath = resolveAuthStorePathForDisplay(agentDir); - const resolvedAgentDir = path.dirname(authStorePath); + const agentDirContext = agentDir ? ` (agentDir: ${resolveUserPath(agentDir)})` : ""; throw new ProviderAuthError( "missing-provider-auth", provider, [ `No API key found for provider "${provider}".`, - `Auth store: ${authStorePath} (agentDir: ${resolvedAgentDir}).`, + `Auth store: ${authStorePath}${agentDirContext}.`, `Configure auth for this agent (${formatCliCommand("openclaw agents add ")}) or copy only portable static auth profiles from the main agentDir.`, ].join(" "), ); diff --git a/src/agents/model-auth.profiles.test.ts b/src/agents/model-auth.profiles.test.ts index 68219fef6d1a..41285ff0dc12 100644 --- a/src/agents/model-auth.profiles.test.ts +++ b/src/agents/model-auth.profiles.test.ts @@ -5,6 +5,8 @@ import path from "node:path"; import type { Model } from "openclaw/plugin-sdk/llm"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { writeConfigMachineState } from "../state/config-machine-state.js"; +import { resolveOpenClawStateSqlitePath } from "../state/openclaw-state-db.paths.js"; import { withEnvAsync } from "../test-utils/env.js"; import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js"; import { clearRuntimeAuthProfileStoreSnapshots } from "./auth-profiles/runtime-snapshots.js"; @@ -730,12 +732,20 @@ describe("getApiKeyForModelCore", () => { OPENAI_API_KEY: undefined, }, }, - async () => { - await expect(resolveApiKeyForProviderCore({ provider: "openai" })).rejects.toMatchObject({ + async (state) => { + writeConfigMachineState("auth.sharedStore", { location: "state-db" }, { env: state.env }); + const error = await resolveApiKeyForProviderCore({ + provider: "openai", + agentDir: state.agentDir(), + }).catch((caught: unknown) => caught); + expect(error).toMatchObject({ code: "missing-provider-auth", - message: expect.stringContaining('No API key found for provider "openai".'), provider: "openai", }); + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toContain( + `Auth store: ${resolveOpenClawStateSqlitePath(state.env)} (agentDir: ${state.agentDir()}).`, + ); }, ); diff --git a/src/commands/doctor-auth.profile-health.test.ts b/src/commands/doctor-auth.profile-health.test.ts index d1f4e81abeb9..08606424423e 100644 --- a/src/commands/doctor-auth.profile-health.test.ts +++ b/src/commands/doctor-auth.profile-health.test.ts @@ -1,10 +1,14 @@ // Doctor auth profile-health tests cover stale profile detection, repair notes, and store health. import fs from "node:fs"; -import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js"; +import { writePersistedAuthProfileStoreRaw } from "../agents/auth-profiles/sqlite.js"; import type { AuthProfileFailureReason, AuthProfileStore } from "../agents/auth-profiles/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { writeConfigMachineState } from "../state/config-machine-state.js"; +import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js"; +import { resolveOpenClawStateSqlitePath } from "../state/openclaw-state-db.paths.js"; import type { DoctorPrompter } from "./doctor-prompter.js"; const authProfileMocks = vi.hoisted(() => ({ @@ -40,12 +44,14 @@ import { note } from "../../packages/terminal-core/src/note.js"; import { collectAuthProfileHealthFindings, noteAuthProfileHealth } from "./doctor-auth.js"; const noteMock = vi.mocked(note); +const tempDirs = useAutoCleanupTempDirTracker(afterEach); describe("noteAuthProfileHealth", () => { let tempDir: string; beforeEach(() => { - tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-doctor-auth-")); + tempDir = tempDirs.make("openclaw-doctor-auth-"); + vi.stubEnv("OPENCLAW_STATE_DIR", tempDir); authProfileMocks.ensureAuthProfileStore.mockReset(); authProfileMocks.hasAnyAuthProfileStoreSource.mockReset(); authProfileMocks.hasAnyAuthProfileStoreSource.mockReturnValue(false); @@ -57,13 +63,14 @@ describe("noteAuthProfileHealth", () => { }); afterEach(() => { + closeOpenClawStateDatabaseForTest(); + vi.unstubAllEnvs(); vi.restoreAllMocks(); - fs.rmSync(tempDir, { recursive: true, force: true }); }); function writeAuthStore(agentDir: string): void { fs.mkdirSync(agentDir, { recursive: true }); - fs.writeFileSync(path.join(agentDir, "auth-profiles.json"), "{}\n", "utf8"); + writePersistedAuthProfileStoreRaw({ version: 1, profiles: {} }, agentDir); } function expectedAuthStorePath(agentDir: string): string { @@ -89,6 +96,7 @@ describe("noteAuthProfileHealth", () => { const now = 1_700_000_000_000; vi.spyOn(Date, "now").mockReturnValue(now); const mainDir = path.join(tempDir, "main-agent"); + writeAuthStore(mainDir); authProfileMocks.hasAnyAuthProfileStoreSource.mockReturnValue(true); authProfileMocks.ensureAuthProfileStore.mockReturnValue( expiredStore("openai:default", now - 60_000), @@ -114,6 +122,30 @@ describe("noteAuthProfileHealth", () => { ]); }); + it("points shared-store findings at the existing shared state database", async () => { + const now = 1_700_000_000_000; + vi.spyOn(Date, "now").mockReturnValue(now); + const mainDir = path.join(tempDir, "main-agent"); + writeConfigMachineState("auth.sharedStore", { location: "state-db" }); + writePersistedAuthProfileStoreRaw({ version: 1, profiles: {} }); + authProfileMocks.hasAnyAuthProfileStoreSource.mockReturnValue(true); + authProfileMocks.ensureAuthProfileStore.mockReturnValue( + expiredStore("openai:default", now - 60_000), + ); + + const findings = await collectAuthProfileHealthFindings({ + cfg: { + agents: { + list: [{ id: "main", default: true, agentDir: mainDir }], + }, + } as OpenClawConfig, + }); + const sharedPath = resolveOpenClawStateSqlitePath(); + + expect(findings).toEqual([expect.objectContaining({ path: sharedPath })]); + expect(fs.existsSync(sharedPath)).toBe(true); + }); + it("does not warn while Claude CLI owns refresh of an expiring access token", async () => { const now = 1_700_000_000_000; vi.spyOn(Date, "now").mockReturnValue(now); @@ -213,6 +245,7 @@ describe("noteAuthProfileHealth", () => { const now = 1_700_000_000_000; vi.spyOn(Date, "now").mockReturnValue(now); const mainDir = path.join(tempDir, "main-agent"); + writeAuthStore(mainDir); authProfileMocks.hasAnyAuthProfileStoreSource.mockReturnValue(true); authProfileMocks.resolveProfileUnusableUntilForDisplay.mockReturnValue(now + 5 * 60_000); authProfileMocks.ensureAuthProfileStore.mockReturnValue({ @@ -427,6 +460,7 @@ describe("noteAuthProfileHealth", () => { it("maps malformed API-key auth profiles to structured findings", async () => { const mainDir = path.join(tempDir, "main-agent"); + writeAuthStore(mainDir); authProfileMocks.hasAnyAuthProfileStoreSource.mockReturnValue(true); authProfileMocks.ensureAuthProfileStore.mockReturnValue({ version: 1, diff --git a/src/commands/doctor-auth.ts b/src/commands/doctor-auth.ts index ff64ac728c55..d70290b0e600 100644 --- a/src/commands/doctor-auth.ts +++ b/src/commands/doctor-auth.ts @@ -30,10 +30,8 @@ import { formatOAuthRefreshFailureLoginCommandMarkdown, type OAuthRefreshFailureReason, } from "../agents/auth-profiles/oauth-refresh-failure.js"; -import { - resolveAuthStorePathForDisplay, - resolveSharedAuthStoreOwnership, -} from "../agents/auth-profiles/path-resolve.js"; +import { resolveSharedAuthStoreOwnership } from "../agents/auth-profiles/path-resolve.js"; +import { resolveAuthStorePathForDisplay } from "../agents/auth-profiles/paths.js"; import { inspectPersistedSharedAuthProfileStoreRaw } from "../agents/auth-profiles/sqlite.js"; import { buildProviderAuthRecoveryHint } from "../agents/provider-auth-recovery-hint.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; diff --git a/src/commands/models/auth-list.test.ts b/src/commands/models/auth-list.test.ts index c6ee99e1bc7b..2e6570915bb7 100644 --- a/src/commands/models/auth-list.test.ts +++ b/src/commands/models/auth-list.test.ts @@ -10,6 +10,7 @@ const mocks = vi.hoisted(() => ({ externalCliDiscoveryForProviderAuth: vi.fn(() => ({ kind: "none" })), loadModelsConfig: vi.fn(), resolveAuthProfileDisplayLabel: vi.fn(({ profileId }: { profileId: string }) => profileId), + resolveAuthStatePathForDisplay: vi.fn((agentDir: string) => `${agentDir}/openclaw-agent.sqlite`), resolveModelsTargetAgent: vi.fn((_cfg: OpenClawConfig, rawAgentId?: string) => { const agentId = rawAgentId ?? "main"; return { agentDir: `/tmp/openclaw/agents/${agentId}`, agentId }; @@ -25,7 +26,7 @@ vi.mock("../../agents/auth-profiles.js", () => ({ ensureAuthProfileStore: mocks.ensureAuthProfileStore, externalCliDiscoveryForProviderAuth: mocks.externalCliDiscoveryForProviderAuth, resolveAuthProfileDisplayLabel: mocks.resolveAuthProfileDisplayLabel, - resolveAuthStatePathForDisplay: (agentDir: string) => `${agentDir}/openclaw-agent.sqlite`, + resolveAuthStatePathForDisplay: mocks.resolveAuthStatePathForDisplay, })); vi.mock("./load-config.js", () => ({ @@ -62,6 +63,9 @@ describe("modelsAuthListCommand", () => { mocks.ensureAuthProfileStore.mockReset(); mocks.externalCliDiscoveryForProviderAuth.mockClear(); mocks.resolveAuthProfileDisplayLabel.mockClear(); + mocks.resolveAuthStatePathForDisplay + .mockReset() + .mockImplementation((agentDir: string) => `${agentDir}/openclaw-agent.sqlite`); mocks.resolveModelsTargetAgent.mockClear(); }); @@ -296,15 +300,19 @@ describe("modelsAuthListCommand", () => { expect(JSON.stringify(runtime.jsonPayloads[0])).not.toContain("secret"); }); - it("prints an empty profile list without failing", async () => { + it.each([ + ["agent-local", "/tmp/openclaw/agents/main/openclaw-agent.sqlite"], + ["shared", "/tmp/openclaw/state/openclaw.sqlite"], + ])("prints an empty profile list with the %s auth path", async (_shape, authStatePath) => { mocks.ensureAuthProfileStore.mockReturnValue({ version: 1, profiles: {} }); + mocks.resolveAuthStatePathForDisplay.mockReturnValue(authStatePath); const runtime = createRuntime(); await modelsAuthListCommand({}, runtime); expect(runtime.logs).toEqual([ "Agent: main", - "Auth state store: /tmp/openclaw/agents/main/openclaw-agent.sqlite", + `Auth state store: ${authStatePath}`, "Profiles: (none)", ]); });