

























@@ -21,7 +21,9 @@ const { getOAuthApiKeyMock } = vi.hoisted(() => ({
2121}));
22222323const { readCodexCliCredentialsCachedMock } = vi.hoisted(() => ({
24-readCodexCliCredentialsCachedMock: vi.fn<() => OAuthCredential | null>(() => null),
24+readCodexCliCredentialsCachedMock: vi.fn<(_options?: unknown) => OAuthCredential | null>(
25+() => null,
26+),
2527}));
26282729const {
@@ -462,6 +464,237 @@ describe("resolveApiKeyForProfile openai-codex refresh fallback", () => {
462464expect(persisted.profiles[profileId]).not.toHaveProperty("refresh");
463465});
464466467+it("uses same-account Codex CLI credentials after forced local refresh fails", async () => {
468+const profileId = "openai-codex:default";
469+saveAuthProfileStore(
470+{
471+version: 1,
472+profiles: {
473+[profileId]: {
474+type: "oauth",
475+provider: "openai-codex",
476+access: "local-access-token",
477+refresh: "local-refresh-token",
478+expires: Date.now() + 86_400_000,
479+accountId: "acct-shared",
480+},
481+},
482+},
483+agentDir,
484+);
485+readCodexCliCredentialsCachedMock.mockReturnValue({
486+type: "oauth",
487+provider: "openai-codex",
488+access: "codex-cli-access-token",
489+refresh: "codex-cli-refresh-token",
490+expires: Date.now() + 86_400_000,
491+accountId: "acct-shared",
492+});
493+refreshProviderOAuthCredentialWithPluginMock.mockImplementationOnce(async () => {
494+throw new Error(
495+'401 {"error":{"message":"Your refresh token is expired.","code":"refresh_token_expired"}}',
496+);
497+});
498+499+await expect(
500+resolveApiKeyForProfile({
501+store: ensureAuthProfileStore(agentDir),
502+ profileId,
503+ agentDir,
504+forceRefresh: true,
505+}),
506+).resolves.toEqual({
507+apiKey: "codex-cli-access-token",
508+provider: "openai-codex",
509+email: undefined,
510+});
511+512+expect(readCodexCliCredentialsCachedMock).toHaveBeenCalledWith({
513+ttlMs: expect.any(Number),
514+allowKeychainPrompt: false,
515+});
516+const persisted = await readPersistedStore(agentDir);
517+const persistedProfile = requireOAuthProfile(persisted, profileId);
518+expect(persistedProfile.accountId).toBe("acct-shared");
519+expect(persistedProfile).not.toHaveProperty("access");
520+expect(persistedProfile).not.toHaveProperty("refresh");
521+expect(JSON.stringify(persisted)).not.toContain("codex-cli-access-token");
522+expect(JSON.stringify(persisted)).not.toContain("codex-cli-refresh-token");
523+});
524+525+it("uses same-account Codex CLI credentials for named Codex profiles after forced local refresh fails", async () => {
526+const profileId = "openai-codex:user@example.com";
527+saveAuthProfileStore(
528+{
529+version: 1,
530+profiles: {
531+[profileId]: {
532+type: "oauth",
533+provider: "openai-codex",
534+access: "local-access-token",
535+refresh: "local-refresh-token",
536+expires: Date.now() + 86_400_000,
537+accountId: "acct-shared",
538+email: "user@example.com",
539+},
540+},
541+},
542+agentDir,
543+);
544+readCodexCliCredentialsCachedMock.mockReturnValue({
545+type: "oauth",
546+provider: "openai-codex",
547+access: "codex-cli-access-token",
548+refresh: "codex-cli-refresh-token",
549+expires: Date.now() + 86_400_000,
550+accountId: "acct-shared",
551+});
552+refreshProviderOAuthCredentialWithPluginMock.mockImplementationOnce(async () => {
553+throw new Error(
554+'401 {"error":{"message":"Your refresh token is expired.","code":"refresh_token_expired"}}',
555+);
556+});
557+558+await expect(
559+resolveApiKeyForProfile({
560+store: ensureAuthProfileStore(agentDir),
561+ profileId,
562+ agentDir,
563+forceRefresh: true,
564+}),
565+).resolves.toEqual({
566+apiKey: "codex-cli-access-token",
567+provider: "openai-codex",
568+email: "user@example.com",
569+});
570+571+const persisted = await readPersistedStore(agentDir);
572+const persistedProfile = requireOAuthProfile(persisted, profileId);
573+expect(persistedProfile.accountId).toBe("acct-shared");
574+expect(persistedProfile.email).toBe("user@example.com");
575+expect(JSON.stringify(persisted)).not.toContain("codex-cli-access-token");
576+expect(JSON.stringify(persisted)).not.toContain("codex-cli-refresh-token");
577+});
578+579+it("rejects mismatched Codex CLI fallback after forced local refresh fails", async () => {
580+const profileId = "openai-codex:default";
581+saveAuthProfileStore(
582+{
583+version: 1,
584+profiles: {
585+[profileId]: {
586+type: "oauth",
587+provider: "openai-codex",
588+access: "local-access-token",
589+refresh: "local-refresh-token",
590+expires: Date.now() + 86_400_000,
591+accountId: "acct-local",
592+},
593+},
594+},
595+agentDir,
596+);
597+readCodexCliCredentialsCachedMock.mockReturnValue({
598+type: "oauth",
599+provider: "openai-codex",
600+access: "codex-cli-access-token",
601+refresh: "codex-cli-refresh-token",
602+expires: Date.now() + 86_400_000,
603+accountId: "acct-other",
604+});
605+refreshProviderOAuthCredentialWithPluginMock.mockImplementationOnce(async () => {
606+throw new Error(
607+'401 {"error":{"message":"Your refresh token is expired.","code":"refresh_token_expired"}}',
608+);
609+});
610+611+await expect(
612+resolveApiKeyForProfile({
613+store: ensureAuthProfileStore(agentDir),
614+ profileId,
615+ agentDir,
616+forceRefresh: true,
617+}),
618+).rejects.toThrow(/OAuth token refresh failed for openai-codex/);
619+});
620+621+it("rejects identity-less Codex CLI fallback after forced local refresh fails", async () => {
622+const profileId = "openai-codex:default";
623+saveAuthProfileStore(
624+{
625+version: 1,
626+profiles: {
627+[profileId]: {
628+type: "oauth",
629+provider: "openai-codex",
630+access: "local-access-token",
631+refresh: "local-refresh-token",
632+expires: Date.now() + 86_400_000,
633+},
634+},
635+},
636+agentDir,
637+);
638+readCodexCliCredentialsCachedMock.mockReturnValue({
639+type: "oauth",
640+provider: "openai-codex",
641+access: "codex-cli-access-token",
642+refresh: "codex-cli-refresh-token",
643+expires: Date.now() + 86_400_000,
644+accountId: "acct-cli",
645+});
646+refreshProviderOAuthCredentialWithPluginMock.mockImplementationOnce(async () => {
647+throw new Error(
648+'401 {"error":{"message":"Your refresh token is expired.","code":"refresh_token_expired"}}',
649+);
650+});
651+652+await expect(
653+resolveApiKeyForProfile({
654+store: ensureAuthProfileStore(agentDir),
655+ profileId,
656+ agentDir,
657+forceRefresh: true,
658+}),
659+).rejects.toThrow(/OAuth token refresh failed for openai-codex/);
660+});
661+662+it("rejects unchanged Codex CLI fallback during forced refresh", async () => {
663+const profileId = "openai-codex:default";
664+const credential: OAuthCredential = {
665+type: "oauth",
666+provider: "openai-codex",
667+access: "shared-access-token",
668+refresh: "shared-refresh-token",
669+expires: Date.now() + 86_400_000,
670+accountId: "acct-shared",
671+};
672+saveAuthProfileStore(
673+{
674+version: 1,
675+profiles: {
676+[profileId]: credential,
677+},
678+},
679+agentDir,
680+);
681+readCodexCliCredentialsCachedMock.mockReturnValue({ ...credential });
682+refreshProviderOAuthCredentialWithPluginMock.mockImplementationOnce(async () => {
683+throw new Error(
684+'401 {"error":{"message":"Your refresh token is expired.","code":"refresh_token_expired"}}',
685+);
686+});
687+688+await expect(
689+resolveApiKeyForProfile({
690+store: ensureAuthProfileStore(agentDir),
691+ profileId,
692+ agentDir,
693+forceRefresh: true,
694+}),
695+).rejects.toThrow(/OAuth token refresh failed for openai-codex/);
696+});
697+465698it("adopts fresher stored credentials after refresh_token_reused", async () => {
466699const profileId = "openai-codex:default";
467700saveAuthProfileStore(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。