
























@@ -385,6 +385,30 @@ describe("cli credentials", () => {
385385});
386386});
387387388+it("rejects Codex keychain fallback expiry when the process clock is invalid", () => {
389+const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-"));
390+process.env.CODEX_HOME = tempHome;
391+const accountHash = "cli|";
392+const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(Number.NaN);
393+try {
394+execSyncMock.mockImplementation((command: unknown) => {
395+const cmd = String(command);
396+expect(cmd).toContain("Codex Auth");
397+expect(cmd).toContain(accountHash);
398+return JSON.stringify({
399+tokens: {
400+access_token: createJwtWithExp(8_700_000_000_000),
401+refresh_token: "keychain-refresh",
402+},
403+});
404+});
405+406+expect(readCodexCliCredentials({ platform: "darwin", execSync: execSyncMock })).toBeNull();
407+} finally {
408+dateNowSpy.mockRestore();
409+}
410+});
411+388412it("falls back to Codex auth.json when keychain is unavailable", () => {
389413const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-"));
390414process.env.CODEX_HOME = tempHome;
@@ -418,6 +442,69 @@ describe("cli credentials", () => {
418442});
419443});
420444445+it("rejects Codex auth.json fallback expiry when stat and process clock are invalid", () => {
446+const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-invalid-clock-"));
447+process.env.CODEX_HOME = tempHome;
448+const authPath = path.join(tempHome, "auth.json");
449+fs.mkdirSync(tempHome, { recursive: true, mode: 0o700 });
450+fs.writeFileSync(
451+authPath,
452+JSON.stringify({
453+tokens: {
454+access_token: createJwtWithExp(8_700_000_000_000),
455+refresh_token: "file-refresh",
456+},
457+}),
458+"utf8",
459+);
460+execSyncMock.mockImplementation(() => {
461+throw new Error("not found");
462+});
463+const statSyncSpy = vi.spyOn(fs, "statSync").mockImplementation(() => {
464+throw new Error("stat unavailable");
465+});
466+const dateNowSpy = vi.spyOn(Date, "now").mockReturnValue(Number.NaN);
467+try {
468+expect(readCodexCliCredentials({ platform: "linux", execSync: execSyncMock })).toBeNull();
469+} finally {
470+dateNowSpy.mockRestore();
471+statSyncSpy.mockRestore();
472+}
473+});
474+475+it("uses Codex auth.json fallback expiry when file mtime has fractional milliseconds", () => {
476+const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-fractional-mtime-"));
477+process.env.CODEX_HOME = tempHome;
478+const authPath = path.join(tempHome, "auth.json");
479+fs.mkdirSync(tempHome, { recursive: true, mode: 0o700 });
480+fs.writeFileSync(
481+authPath,
482+JSON.stringify({
483+tokens: {
484+access_token: createJwtWithExp(8_700_000_000_000),
485+refresh_token: "file-refresh",
486+},
487+}),
488+"utf8",
489+);
490+execSyncMock.mockImplementation(() => {
491+throw new Error("not found");
492+});
493+const mtimeMs = Date.parse("2026-03-24T10:00:00Z") + 0.75;
494+const statSyncSpy = vi.spyOn(fs, "statSync").mockReturnValue({ mtimeMs } as fs.Stats);
495+try {
496+const creds = readCodexCliCredentials({ platform: "linux", execSync: execSyncMock });
497+498+expectFields(creds, {
499+refresh: "file-refresh",
500+provider: "openai-codex",
501+expires: Math.floor(mtimeMs) + 60 * 60 * 1000,
502+});
503+} finally {
504+statSyncSpy.mockRestore();
505+}
506+});
507+421508it("does not read Codex keychain when keychain prompts are disabled", () => {
422509const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-no-prompt-"));
423510process.env.CODEX_HOME = tempHome;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。