




























@@ -1,51 +1,10 @@
1-import path from "node:path";
2-import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
1+import { describe, expect, it } from "vitest";
32import { FILE_LOCK_TIMEOUT_ERROR_CODE, type FileLockTimeoutError } from "../../infra/file-lock.js";
4-import { captureEnv } from "../../test-utils/env.js";
5-import { getOAuthProviderRuntimeMocks } from "./oauth-common-mocks.test-support.js";
6-import "./oauth-external-auth-passthrough.test-support.js";
73import {
8-OAUTH_AGENT_ENV_KEYS,
9-createOAuthMainAgentDir,
10-createOAuthTestTempRoot,
11-createExpiredOauthStore,
12-removeOAuthTestTempRoot,
13-resolveApiKeyForProfileInTest,
14-resetOAuthProviderRuntimeMocks,
15-} from "./oauth-test-utils.js";
4+buildRefreshContentionError,
5+isGlobalRefreshLockTimeoutError,
6+} from "./oauth-refresh-lock-errors.js";
167import { resolveAuthStorePath, resolveOAuthRefreshLockPath } from "./paths.js";
17-import { clearRuntimeAuthProfileStoreSnapshots, saveAuthProfileStore } from "./store.js";
18-19-const {
20- refreshProviderOAuthCredentialWithPluginMock,
21- formatProviderAuthProfileApiKeyWithPluginMock,
22-} = getOAuthProviderRuntimeMocks();
23-24-let resolveApiKeyForProfile: typeof import("./oauth.js").resolveApiKeyForProfile;
25-let resetOAuthRefreshQueuesForTest: typeof import("./oauth.js").resetOAuthRefreshQueuesForTest;
26-27-const { withFileLockMock } = vi.hoisted(() => ({
28-withFileLockMock: vi.fn(
29-async <T>(_filePath: string, _options: unknown, run: () => Promise<T>) => await run(),
30-),
31-}));
32-33-vi.mock("@mariozechner/pi-ai/oauth", () => ({
34-getOAuthApiKey: vi.fn(async () => null),
35-getOAuthProviders: () => [{ id: "openai-codex" }],
36-}));
37-38-vi.mock("../../infra/file-lock.js", () => ({
39-FILE_LOCK_TIMEOUT_ERROR_CODE: "file_lock_timeout",
40-resetFileLockStateForTest: () => undefined,
41-withFileLock: withFileLockMock,
42-}));
43-44-vi.mock("../../plugin-sdk/file-lock.js", () => ({
45-FILE_LOCK_TIMEOUT_ERROR_CODE: "file_lock_timeout",
46-resetFileLockStateForTest: () => undefined,
47-withFileLock: withFileLockMock,
48-}));
498509function createLockTimeoutError(lockPath: string): FileLockTimeoutError {
5110return Object.assign(new Error(`file lock timeout for ${lockPath.slice(0, -5)}`), {
@@ -55,105 +14,42 @@ function createLockTimeoutError(lockPath: string): FileLockTimeoutError {
5514}
56155716describe("OAuth refresh lock timeout classification", () => {
58-const envSnapshot = captureEnv(OAUTH_AGENT_ENV_KEYS);
59-let tempRoot = "";
60-let agentDir = "";
61-let caseIndex = 0;
62-63-beforeAll(async () => {
64-tempRoot = await createOAuthTestTempRoot("openclaw-oauth-lock-timeout-");
65-({ resolveApiKeyForProfile, resetOAuthRefreshQueuesForTest } = await import("./oauth.js"));
66-});
67-68-beforeEach(async () => {
69-resetOAuthProviderRuntimeMocks({
70- refreshProviderOAuthCredentialWithPluginMock,
71- formatProviderAuthProfileApiKeyWithPluginMock,
72-});
73-withFileLockMock.mockReset();
74-withFileLockMock.mockImplementation(
75-async <T>(_filePath: string, _options: unknown, run: () => Promise<T>) => await run(),
76-);
77-clearRuntimeAuthProfileStoreSnapshots();
78-const caseRoot = path.join(tempRoot, `case-${++caseIndex}`);
79-agentDir = await createOAuthMainAgentDir(caseRoot);
80-resetOAuthRefreshQueuesForTest();
81-});
82-83-afterEach(async () => {
84-envSnapshot.restore();
85-clearRuntimeAuthProfileStoreSnapshots();
86-resetOAuthRefreshQueuesForTest();
87-});
88-89-afterAll(async () => {
90-await removeOAuthTestTempRoot(tempRoot);
91-});
92-93-it("maps only global refresh lock timeouts to refresh_contention", async () => {
17+it("matches only the global refresh lock path", () => {
9418const profileId = "openai-codex:default";
9519const provider = "openai-codex";
96-const store = createExpiredOauthStore({ profileId, provider });
97-saveAuthProfileStore(store, agentDir);
98-99-const refreshLockPath = `${resolveOAuthRefreshLockPath(provider, profileId)}.lock`;
100-withFileLockMock.mockImplementationOnce(async () => {
101-throw createLockTimeoutError(refreshLockPath);
102-});
103-104-try {
105-await resolveApiKeyForProfileInTest(resolveApiKeyForProfile, {
106- store,
107- profileId,
108- agentDir,
109-});
110-throw new Error("expected refresh contention error");
111-} catch (error) {
112-expect((error as Error).message).toContain("another process is already refreshing");
113-expect((error as Error).message).toContain(
114-"Please wait for the in-flight refresh to finish and retry.",
115-);
116-expect((error as Error & { cause?: unknown }).cause).toMatchObject({
117-code: "refresh_contention",
118-});
119-expect(
120-((error as Error & { cause?: { cause?: unknown } }).cause as { cause?: unknown }).cause,
121-).toMatchObject({
122-code: FILE_LOCK_TIMEOUT_ERROR_CODE,
123-lockPath: refreshLockPath,
124-});
125-}
20+const refreshLockPath = resolveOAuthRefreshLockPath(provider, profileId);
21+const authStoreLockPath = resolveAuthStorePath("/tmp/openclaw-oauth-lock-timeout/agent");
22+23+expect(
24+isGlobalRefreshLockTimeoutError(
25+createLockTimeoutError(`${refreshLockPath}.lock`),
26+refreshLockPath,
27+),
28+).toBe(true);
29+expect(
30+isGlobalRefreshLockTimeoutError(
31+createLockTimeoutError(`${authStoreLockPath}.lock`),
32+refreshLockPath,
33+),
34+).toBe(false);
12635});
12736128-it("preserves auth-store lock timeouts instead of remapping them to refresh_contention", async () => {
37+it("builds refresh_contention errors that preserve the file-lock cause", () => {
12938const profileId = "openai-codex:default";
13039const provider = "openai-codex";
131-const store = createExpiredOauthStore({ profileId, provider });
132-saveAuthProfileStore(store, agentDir);
40+const refreshLockPath = resolveOAuthRefreshLockPath(provider, profileId);
41+const cause = createLockTimeoutError(`${refreshLockPath}.lock`);
13342134-const authStoreLockPath = `${resolveAuthStorePath(agentDir)}.lock`;
135-withFileLockMock
136-.mockImplementationOnce(
137-async <T>(_filePath: string, _options: unknown, run: () => Promise<T>) => await run(),
138-)
139-.mockImplementationOnce(async () => {
140-throw createLockTimeoutError(authStoreLockPath);
141-});
43+const error = buildRefreshContentionError({ provider, profileId, cause });
14244143-try {
144-await resolveApiKeyForProfileInTest(resolveApiKeyForProfile, {
145- store,
146- profileId,
147- agentDir,
148-});
149-throw new Error("expected auth-store lock timeout");
150-} catch (error) {
151-expect((error as Error).message).toContain("file lock timeout");
152-expect((error as Error).message).toContain("Please try again or re-authenticate.");
153-expect((error as Error & { cause?: unknown }).cause).toMatchObject({
45+expect(error).toMatchObject({
46+code: "refresh_contention",
47+cause: {
15448code: FILE_LOCK_TIMEOUT_ERROR_CODE,
155-lockPath: authStoreLockPath,
156-});
157-}
49+lockPath: `${refreshLockPath}.lock`,
50+},
51+});
52+expect(error.message).toContain("another process is already refreshing");
53+expect(error.message).toContain("Please wait for the in-flight refresh to finish and retry.");
15854});
15955});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。