
















@@ -1,6 +1,10 @@
11import fs from "node:fs/promises";
22import os from "node:os";
33import path from "node:path";
4+import {
5+clearRuntimeAuthProfileStoreSnapshots,
6+loadAuthProfileStoreForSecretsRuntime,
7+} from "openclaw/plugin-sdk/agent-runtime";
48import { upsertAuthProfile } from "openclaw/plugin-sdk/provider-auth";
59import { afterEach, describe, expect, it, vi } from "vitest";
610import {
@@ -72,7 +76,7 @@ vi.mock("openclaw/plugin-sdk/agent-runtime", async (importOriginal) => {
7276if (refreshed?.access) {
7377oauthCredential = refreshed as typeof oauthCredential;
7478params.store.profiles[params.profileId] = oauthCredential;
75-if (params.agentDir) {
79+if (params.agentDir || process.env.OPENCLAW_STATE_DIR) {
7680actual.saveAuthProfileStore(params.store, params.agentDir);
7781}
7882}
@@ -92,6 +96,7 @@ vi.mock("openclaw/plugin-sdk/agent-runtime", async (importOriginal) => {
92969397afterEach(() => {
9498vi.unstubAllEnvs();
99+clearRuntimeAuthProfileStoreSnapshots();
95100oauthMocks.refreshOpenAICodexToken.mockReset();
96101providerRuntimeMocks.formatProviderAuthProfileApiKeyWithPlugin.mockReset();
97102providerRuntimeMocks.refreshProviderOAuthCredentialWithPlugin.mockClear();
@@ -635,6 +640,132 @@ describe("bridgeCodexAppServerStartOptions", () => {
635640}
636641});
637642643+it("refreshes inherited main Codex OAuth without cloning it into the child store", async () => {
644+const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
645+const stateDir = path.join(root, "state");
646+const childAgentDir = path.join(stateDir, "agents", "worker", "agent");
647+const childAuthPath = path.join(childAgentDir, "auth-profiles.json");
648+vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
649+vi.stubEnv("OPENCLAW_AGENT_DIR", "");
650+oauthMocks.refreshOpenAICodexToken.mockResolvedValueOnce({
651+access: "main-refreshed-access-token",
652+refresh: "main-refreshed-refresh-token",
653+expires: Date.now() + 60_000,
654+accountId: "account-main-refreshed",
655+});
656+try {
657+upsertAuthProfile({
658+profileId: "openai-codex:work",
659+credential: {
660+type: "oauth",
661+provider: "openai-codex",
662+access: "main-current-access-token",
663+refresh: "main-refresh-token",
664+expires: Date.now() + 60_000,
665+accountId: "account-main",
666+email: "main-codex@example.test",
667+},
668+});
669+670+await expect(
671+refreshCodexAppServerAuthTokens({
672+agentDir: childAgentDir,
673+authProfileId: "openai-codex:work",
674+}),
675+).resolves.toEqual({
676+accessToken: "main-refreshed-access-token",
677+chatgptAccountId: "account-main-refreshed",
678+chatgptPlanType: null,
679+});
680+681+expect(oauthMocks.refreshOpenAICodexToken).toHaveBeenCalledWith("main-refresh-token");
682+await expect(fs.access(childAuthPath)).rejects.toMatchObject({ code: "ENOENT" });
683+expect(loadAuthProfileStoreForSecretsRuntime().profiles["openai-codex:work"]).toMatchObject({
684+type: "oauth",
685+provider: "openai-codex",
686+access: "main-refreshed-access-token",
687+refresh: "main-refreshed-refresh-token",
688+});
689+} finally {
690+await fs.rm(root, { recursive: true, force: true });
691+}
692+});
693+694+it("force-refreshes the owner credential instead of a stale child OAuth clone", async () => {
695+const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
696+const stateDir = path.join(root, "state");
697+const childAgentDir = path.join(stateDir, "agents", "worker", "agent");
698+const childAuthPath = path.join(childAgentDir, "auth-profiles.json");
699+vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
700+vi.stubEnv("OPENCLAW_AGENT_DIR", "");
701+oauthMocks.refreshOpenAICodexToken.mockResolvedValueOnce({
702+access: "main-refreshed-access-token",
703+refresh: "main-refreshed-refresh-token",
704+expires: Date.now() + 60_000,
705+accountId: "account-main-refreshed",
706+});
707+try {
708+upsertAuthProfile({
709+profileId: "openai-codex:work",
710+credential: {
711+type: "oauth",
712+provider: "openai-codex",
713+access: "main-current-access-token",
714+refresh: "main-owner-refresh-token",
715+expires: Date.now() + 60_000,
716+accountId: "account-main",
717+email: "main-codex@example.test",
718+},
719+});
720+await fs.mkdir(childAgentDir, { recursive: true });
721+await fs.writeFile(
722+childAuthPath,
723+JSON.stringify({
724+version: 1,
725+profiles: {
726+"openai-codex:work": {
727+type: "oauth",
728+provider: "openai-codex",
729+access: "child-stale-access-token",
730+refresh: "child-stale-refresh-token",
731+expires: Date.now() - 60_000,
732+accountId: "account-main",
733+email: "main-codex@example.test",
734+},
735+},
736+}),
737+);
738+739+await expect(
740+refreshCodexAppServerAuthTokens({
741+agentDir: childAgentDir,
742+authProfileId: "openai-codex:work",
743+}),
744+).resolves.toEqual({
745+accessToken: "main-refreshed-access-token",
746+chatgptAccountId: "account-main-refreshed",
747+chatgptPlanType: null,
748+});
749+750+expect(oauthMocks.refreshOpenAICodexToken).toHaveBeenCalledWith("main-owner-refresh-token");
751+expect(loadAuthProfileStoreForSecretsRuntime().profiles["openai-codex:work"]).toMatchObject({
752+type: "oauth",
753+provider: "openai-codex",
754+access: "main-refreshed-access-token",
755+refresh: "main-refreshed-refresh-token",
756+});
757+const child = JSON.parse(await fs.readFile(childAuthPath, "utf8")) as {
758+profiles: Record<string, { access?: string; refresh?: string }>;
759+};
760+expect(child.profiles["openai-codex:work"]).toMatchObject({
761+access: "child-stale-access-token",
762+refresh: "child-stale-refresh-token",
763+});
764+} finally {
765+await fs.rm(root, { recursive: true, force: true });
766+}
767+});
768+638769it("accepts a refreshed Codex OAuth credential when the stored provider is a legacy alias", async () => {
639770const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-app-server-"));
640771oauthMocks.refreshOpenAICodexToken.mockResolvedValueOnce({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。