From e408f54483a9a532bc6418d4ace6c68aef643293 Mon Sep 17 00:00:00 2001 From: icebear0828 Date: Thu, 14 May 2026 08:38:53 -0700 Subject: [PATCH 1/2] fix: extend mac x64 packaged smoke timeout --- .github/workflows/release.yml | 3 +++ tests/unit/ci/electron-smoke-script.test.ts | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 12eaf36e..83c8fd24 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -281,6 +281,9 @@ jobs: env: RELEASE_DIR: packages/electron/release MAC_ARCH: x64 + # x64 app startup runs through Rosetta on macOS runners and can + # legitimately take just over the default 90s smoke timeout. + SMOKE_TIMEOUT: 180 SMOKE_LOG: ${{ runner.temp }}/electron-smoke.log run: bash .github/scripts/electron-smoke.sh diff --git a/tests/unit/ci/electron-smoke-script.test.ts b/tests/unit/ci/electron-smoke-script.test.ts index e1178a4a..30ad62e2 100644 --- a/tests/unit/ci/electron-smoke-script.test.ts +++ b/tests/unit/ci/electron-smoke-script.test.ts @@ -12,7 +12,7 @@ import { describe, it, expect, beforeAll } from "vitest"; import { execFileSync } from "child_process"; -import { existsSync, statSync } from "fs"; +import { existsSync, readFileSync, statSync } from "fs"; import { resolve } from "path"; const SCRIPT = resolve(__dirname, "..", "..", "..", ".github", "scripts", "electron-smoke.sh"); @@ -113,4 +113,17 @@ describe("electron-smoke.sh script", () => { expect(result.status).not.toBe(0); expect(result.stderr + result.stdout).toContain("Unsupported RUNNER_OS"); }); + + it("gives mac x64 packaged smoke extra startup time", () => { + const workflow = readFileSync( + resolve(__dirname, "..", "..", "..", ".github", "workflows", "release.yml"), + "utf-8", + ); + const block = workflow.match( + /- name: Smoke test packaged binary \(mac-x64\)[\s\S]*?run: bash \.github\/scripts\/electron-smoke\.sh/, + )?.[0] ?? ""; + + expect(block).toContain("MAC_ARCH: x64"); + expect(block).toContain("SMOKE_TIMEOUT: 180"); + }); }); From b46fe288f203943609ff0f6b3e66b5b8f974f062 Mon Sep 17 00:00:00 2001 From: icebear0828 Date: Thu, 14 May 2026 23:07:46 -0700 Subject: [PATCH 2/2] fix: clear error logs and update Anthropic defaults --- CHANGELOG.md | 1 + shared/hooks/use-error-logs.test.ts | 17 +++++++- shared/hooks/use-error-logs.ts | 24 ++++++++++- shared/i18n/translations.ts | 2 + src/auth/api-key-catalog.ts | 2 +- src/logs/error-log.ts | 13 ++++++ src/routes/admin/error-logs.ts | 6 +++ tests/unit/routes/admin/error-logs.test.ts | 48 +++++++++++++++++++++- tests/unit/routes/api-keys.test.ts | 17 ++++++++ tests/unit/web/anthropic-setup.test.ts | 20 +++++++++ tests/unit/web/errors-page.test.ts | 12 ++++++ web/src/components/AnthropicSetup.tsx | 22 ++++++---- web/src/pages/ErrorsPage.tsx | 18 +++++++- 13 files changed, 189 insertions(+), 13 deletions(-) create mode 100644 tests/unit/web/anthropic-setup.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d93a10b6..60b944de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ### Fixed +- Dashboard Errors tab now has a real clear action: `DELETE /admin/error-logs` removes current + backup JSONL files and the read cursor so repeated `StreamUpstreamPrematureClose` groups can be cleared from the page instead of only marked read. Anthropic setup defaults now map Opus 4.7 → `gpt-5.5` and Sonnet 4.6 → `gpt-5.4`, and the built-in Anthropic API-key catalog lists Claude Opus 4.7 (`src/logs/error-log.ts`, `src/routes/admin/error-logs.ts`, `shared/hooks/use-error-logs.ts`, `web/src/pages/ErrorsPage.tsx`, `web/src/components/AnthropicSetup.tsx`, `src/auth/api-key-catalog.ts`). - Lockfile tarball sources now point to the official npm registry instead of `registry.npmmirror.com`, and the CI package boundary guard fails if any root/web/native lockfile resolves npm packages from a non-`registry.npmjs.org` host. Root production dependency audit is also clean after non-breaking lockfile updates for `hono`, `@hono/node-server`, `undici`, `minimatch`, and `brace-expansion`; the remaining full-audit finding is the existing Electron major-version upgrade requirement (`package-lock.json`, `web/package-lock.json`, `tests/unit/ci/package-boundary.test.ts`). - Update checker now keeps `config/default.yaml` in sync when it auto-applies a Codex Desktop appcast version, while still writing `data/version-state.json` for cold-start runtime overrides. When a matching `data/extracted-fingerprint.json` is present, the checker also carries `chromium_version` through to version state, YAML, and in-memory config so User-Agent and `sec-ch-ua` fingerprint fields do not drift. The checked-in default fingerprint is updated to Codex Desktop `26.506.31421` / build `2620` / Chromium `146` (`src/update-checker.ts`, `config/default.yaml`, `tests/unit/update-checker.test.ts`). - Root package boundary now has a CI-enforced guard for proxy package metadata, root/workspace lockfile version sync, core npm entrypoints, local `tsx` script targets, and strict TypeScript coverage for public update scripts. This also restores the public update script entrypoints plus their extraction pattern config that `package.json` and `update-scripts-path.test.ts` already referenced, keeps them trackable by removing stale ignore rules, prevents `promote-dev-to-master` from treating missing checks as green, makes the runtime update checker fork `full-update` only when `CODEX_DESKTOP_PATH` / `CODEX_APP_PATH` points at a local Codex Desktop source, and broadens model extraction to current `gpt-*` IDs such as `gpt-5-codex` (`package-lock.json`, `.gitignore`, `.github/workflows/ci-quality.yml`, `.github/workflows/promote-dev-to-master.yml`, `.github/workflows/bump-electron.yml`, `tsconfig.scripts.json`, `config/extraction-patterns.yaml`, `src/update-checker.ts`, `scripts/build/check-update.ts`, `scripts/build/apply-update.ts`, `scripts/build/full-update.ts`, `scripts/build/types.ts`, `scripts/build/vendor-types.d.ts`, `tests/unit/ci/package-boundary.test.ts`, `tests/unit/update-checker.test.ts`, `tests/unit/update-scripts-path.test.ts`). diff --git a/shared/hooks/use-error-logs.test.ts b/shared/hooks/use-error-logs.test.ts index fee47368..c79883bb 100644 --- a/shared/hooks/use-error-logs.test.ts +++ b/shared/hooks/use-error-logs.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { formatRelativeTime } from "./use-error-logs.js"; +import { clearErrorLogsRequest, formatRelativeTime } from "./use-error-logs.js"; describe("formatRelativeTime", () => { const now = new Date("2026-05-10T12:00:00Z").getTime(); @@ -28,3 +28,18 @@ describe("formatRelativeTime", () => { expect(formatRelativeTime("not-a-date", now)).toBe("not-a-date"); }); }); + +describe("clearErrorLogsRequest", () => { + it("sends a collection DELETE to the error log endpoint", async () => { + const fetchImpl = async ( + input: string, + init: RequestInit, + ): Promise> => { + expect(input).toBe("/admin/error-logs"); + expect(init).toEqual({ method: "DELETE" }); + return { ok: true }; + }; + + await expect(clearErrorLogsRequest(fetchImpl)).resolves.toBe(true); + }); +}); diff --git a/shared/hooks/use-error-logs.ts b/shared/hooks/use-error-logs.ts index 1fcd0cba..edafa00b 100644 --- a/shared/hooks/use-error-logs.ts +++ b/shared/hooks/use-error-logs.ts @@ -30,6 +30,15 @@ export interface ErrorLogCount { const POLL_MS = 30_000; +type ErrorLogsFetch = (input: string, init: RequestInit) => Promise>; + +export async function clearErrorLogsRequest( + fetchImpl: ErrorLogsFetch = (input, init) => fetch(input, init), +): Promise { + const res = await fetchImpl("/admin/error-logs", { method: "DELETE" }); + return res.ok; +} + export function useErrorLogs() { const [groups, setGroups] = useState([]); const [count, setCount] = useState({ total: 0, unread: 0 }); @@ -68,6 +77,19 @@ export function useErrorLogs() { } }, [load]); + const clearAll = useCallback(async () => { + try { + const ok = await clearErrorLogsRequest(); + if (!ok) { + setError("Failed to clear error logs"); + return; + } + await load(); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to clear error logs"); + } + }, [load]); + useEffect(() => { void load(); timerRef.current = setInterval(() => void load(), POLL_MS); @@ -76,7 +98,7 @@ export function useErrorLogs() { }; }, [load]); - return { groups, count, loading, error, refresh: load, markAllSeen }; + return { groups, count, loading, error, refresh: load, markAllSeen, clearAll }; } /** diff --git a/shared/i18n/translations.ts b/shared/i18n/translations.ts index 784759b0..b3f85eb7 100644 --- a/shared/i18n/translations.ts +++ b/shared/i18n/translations.ts @@ -225,6 +225,7 @@ export const translations = { errorsTabDesc: "Uncaught crashes from the proxy backend, Electron main, and the dashboard renderer. Stored locally — nothing leaves this machine.", errorsRefresh: "Refresh", errorsMarkSeen: "Mark all read", + errorsClear: "Clear all", errorsBadge: "errors", errorsBadgeTooltip: "Unread errors — click to view", errorsNone: "No errors recorded.", @@ -629,6 +630,7 @@ export const translations = { errorsTabDesc: "Proxy 后端、Electron 主进程、Dashboard 渲染进程的 uncaught 崩溃。仅本地存储,不上传任何数据。", errorsRefresh: "刷新", errorsMarkSeen: "全部标记已读", + errorsClear: "清空错误", errorsBadge: "条错误", errorsBadgeTooltip: "未读错误 — 点击查看", errorsNone: "暂无错误记录。", diff --git a/src/auth/api-key-catalog.ts b/src/auth/api-key-catalog.ts index 2c9c10cc..21dc7806 100644 --- a/src/auth/api-key-catalog.ts +++ b/src/auth/api-key-catalog.ts @@ -18,7 +18,7 @@ export interface ProviderMeta { } const ANTHROPIC_MODELS: CatalogModel[] = [ - { id: "claude-opus-4-6", displayName: "Claude Opus 4.6" }, + { id: "claude-opus-4-7", displayName: "Claude Opus 4.7" }, { id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6" }, { id: "claude-haiku-4-5", displayName: "Claude Haiku 4.5" }, ]; diff --git a/src/logs/error-log.ts b/src/logs/error-log.ts index 118c2567..dfc0fbd4 100644 --- a/src/logs/error-log.ts +++ b/src/logs/error-log.ts @@ -27,6 +27,7 @@ import { readFileSync, renameSync, statSync, + unlinkSync, writeFileSync, } from "fs"; import { resolve } from "path"; @@ -214,6 +215,18 @@ export function readErrorLog(limit?: number): ErrorLogEntry[] { return combined; } +/** Remove all persisted error log entries and the read cursor. */ +export function clearErrorLog(): void { + for (const file of [LOG_FILE, BACKUP_FILE, CURSOR_FILE]) { + try { + const path = resolve(getDataDir(), file); + if (existsSync(path)) unlinkSync(path); + } catch { + // Clearing is best-effort; a failed delete must not break the admin UI. + } + } +} + function firstStackFrame(stack: string | undefined): string { if (!stack) return ""; for (const line of stack.split("\n")) { diff --git a/src/routes/admin/error-logs.ts b/src/routes/admin/error-logs.ts index 0bcd2a9e..820de4d0 100644 --- a/src/routes/admin/error-logs.ts +++ b/src/routes/admin/error-logs.ts @@ -2,6 +2,7 @@ import { Hono } from "hono"; import { z } from "zod"; import { appendErrorLog, + clearErrorLog, groupErrorLog, getUnreadCount, readErrorLog, @@ -59,6 +60,11 @@ export function createErrorLogRoutes(): Hono { return c.json({ ok: true, cursor }); }); + app.delete("/admin/error-logs", (c) => { + clearErrorLog(); + return c.json({ ok: true }); + }); + app.post("/admin/error-logs/report", async (c) => { const raw = await c.req.json().catch(() => null); if (raw === null) { diff --git a/tests/unit/routes/admin/error-logs.test.ts b/tests/unit/routes/admin/error-logs.test.ts index 0fdf07eb..7f131254 100644 --- a/tests/unit/routes/admin/error-logs.test.ts +++ b/tests/unit/routes/admin/error-logs.test.ts @@ -7,7 +7,7 @@ */ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; -import { mkdtempSync, existsSync, rmSync, readFileSync } from "fs"; +import { mkdtempSync, existsSync, rmSync, readFileSync, writeFileSync } from "fs"; import { tmpdir } from "os"; import { resolve } from "path"; import { Hono } from "hono"; @@ -183,6 +183,52 @@ describe("POST /admin/error-logs/seen", () => { }); }); +describe("DELETE /admin/error-logs", () => { + it("clears current, rotated backup, and cursor files so grouped logs and counts become empty", async () => { + await appendFew(); + writeFileSync( + resolve(tmpDataDir, "error-log.1.jsonl"), + JSON.stringify({ + ts: "2026-05-01T00:00:00.000Z", + version: "0.0.0-test", + platform: "darwin", + source: "server", + error: { name: "StreamUpstreamPrematureClose", message: "closed early" }, + }) + "\n", + "utf-8", + ); + const { setReadCursor } = await import("@src/logs/error-log.js"); + setReadCursor("2025-01-01T00:00:00.000Z"); + const app = await buildApp(); + + expect(existsSync(resolve(tmpDataDir, "error-log.jsonl"))).toBe(true); + expect(existsSync(resolve(tmpDataDir, "error-log.1.jsonl"))).toBe(true); + expect(existsSync(resolve(tmpDataDir, "error-log.cursor"))).toBe(true); + + const before = await app.request("/admin/error-logs/count"); + expect(((await before.json()) as { total: number; unread: number })).toEqual({ + total: 4, + unread: 4, + }); + + const clearRes = await app.request("/admin/error-logs", { method: "DELETE" }); + expect(clearRes.status).toBe(200); + expect(await clearRes.json()).toEqual({ ok: true }); + expect(existsSync(resolve(tmpDataDir, "error-log.jsonl"))).toBe(false); + expect(existsSync(resolve(tmpDataDir, "error-log.1.jsonl"))).toBe(false); + expect(existsSync(resolve(tmpDataDir, "error-log.cursor"))).toBe(false); + + const grouped = await app.request("/admin/error-logs"); + expect((await grouped.json()) as { groups: unknown[] }).toEqual({ groups: [] }); + + const after = await app.request("/admin/error-logs/count"); + expect(((await after.json()) as { total: number; unread: number })).toEqual({ + total: 0, + unread: 0, + }); + }); +}); + describe("POST /admin/error-logs/report", () => { it("appends a renderer-reported error to the log with sanitized context", async () => { const app = await buildApp(); diff --git a/tests/unit/routes/api-keys.test.ts b/tests/unit/routes/api-keys.test.ts index d63246ec..0552fd39 100644 --- a/tests/unit/routes/api-keys.test.ts +++ b/tests/unit/routes/api-keys.test.ts @@ -26,6 +26,23 @@ describe("api key routes", () => { app = createApiKeyRoutes(pool); }); + it("returns current built-in Anthropic catalog defaults", async () => { + const res = await app.request("/auth/api-keys/catalog"); + expect(res.status).toBe(200); + + const body = await res.json() as { + catalog: { + anthropic: { + models: Array<{ id: string; displayName: string }>; + }; + }; + }; + expect(body.catalog.anthropic.models.slice(0, 2)).toEqual([ + { id: "claude-opus-4-7", displayName: "Claude Opus 4.7" }, + { id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6" }, + ]); + }); + it("adds one stored entry per selected model and masks returned keys", async () => { const res = await app.request("/auth/api-keys", { method: "POST", diff --git a/tests/unit/web/anthropic-setup.test.ts b/tests/unit/web/anthropic-setup.test.ts new file mode 100644 index 00000000..a262107b --- /dev/null +++ b/tests/unit/web/anthropic-setup.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect } from "vitest"; +import { + ANTHROPIC_MODEL_PRESETS, + DEFAULT_ANTHROPIC_MODELS, +} from "../../../web/src/components/AnthropicSetup"; + +describe("AnthropicSetup defaults", () => { + it("maps current Claude families to the desired Codex defaults", () => { + expect(DEFAULT_ANTHROPIC_MODELS).toEqual({ + opus: "gpt-5.5", + sonnet: "gpt-5.4", + haiku: "gpt-5.4-mini", + }); + + expect(ANTHROPIC_MODEL_PRESETS.slice(0, 2)).toEqual([ + { label: "gpt-5.5 (Opus 4.7)", value: "gpt-5.5" }, + { label: "gpt-5.4 (Sonnet 4.6)", value: "gpt-5.4" }, + ]); + }); +}); diff --git a/tests/unit/web/errors-page.test.ts b/tests/unit/web/errors-page.test.ts index c62a1e17..babb11db 100644 --- a/tests/unit/web/errors-page.test.ts +++ b/tests/unit/web/errors-page.test.ts @@ -12,4 +12,16 @@ describe("ErrorsPage", () => { expect(source).toContain("group.sample_context"); expect(source).toContain("JSON.stringify(group.sample_context, null, 2)"); }); + + it("wires a clear-all control for persisted error log entries", () => { + const source = readFileSync( + resolve(__dirname, "../../../web/src/pages/ErrorsPage.tsx"), + "utf-8", + ); + + expect(source).toContain("clearAll"); + expect(source).toContain("errorsClear"); + expect(source).toContain("aria-label={t(\"errorsClear\")}"); + expect(source).toContain("onClick={() => void clearAll()}"); + }); }); diff --git a/web/src/components/AnthropicSetup.tsx b/web/src/components/AnthropicSetup.tsx index c1acbdee..1d44e644 100644 --- a/web/src/components/AnthropicSetup.tsx +++ b/web/src/components/AnthropicSetup.tsx @@ -9,9 +9,15 @@ interface AnthropicSetupProps { serviceTier: string | null; } -const PRESETS: Array<{ label: string; value: string }> = [ - { label: "gpt-5.4 (Opus)", value: "gpt-5.4" }, - { label: "gpt-5.3-codex (Sonnet)", value: "gpt-5.3-codex" }, +export const DEFAULT_ANTHROPIC_MODELS = { + opus: "gpt-5.5", + sonnet: "gpt-5.4", + haiku: "gpt-5.4-mini", +}; + +export const ANTHROPIC_MODEL_PRESETS: Array<{ label: string; value: string }> = [ + { label: "gpt-5.5 (Opus 4.7)", value: DEFAULT_ANTHROPIC_MODELS.opus }, + { label: "gpt-5.4 (Sonnet 4.6)", value: DEFAULT_ANTHROPIC_MODELS.sonnet }, { label: "gpt-5.4-mini (Haiku)", value: "gpt-5.4-mini" }, ]; @@ -19,9 +25,9 @@ export function AnthropicSetup({ apiKey, selectedModel, reasoningEffort, service const t = useT(); const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost:8080"; - const [opusModel, setOpusModel] = useState("gpt-5.4"); - const [sonnetModel, setSonnetModel] = useState("gpt-5.3-codex"); - const [haikuModel, setHaikuModel] = useState("gpt-5.4-mini"); + const [opusModel, setOpusModel] = useState(DEFAULT_ANTHROPIC_MODELS.opus); + const [sonnetModel, setSonnetModel] = useState(DEFAULT_ANTHROPIC_MODELS.sonnet); + const [haikuModel, setHaikuModel] = useState(DEFAULT_ANTHROPIC_MODELS.haiku); // Custom model from ApiConfig const customModel = useMemo(() => { @@ -40,7 +46,7 @@ export function AnthropicSetup({ apiKey, selectedModel, reasoningEffort, service .catch(() => {}); }, []); - const presetValues = new Set(PRESETS.map((p) => p.value)); + const presetValues = new Set(ANTHROPIC_MODEL_PRESETS.map((p) => p.value)); const extraModels = allModels.filter((id) => !presetValues.has(id)); const envText = useMemo(() => [ @@ -59,7 +65,7 @@ export function AnthropicSetup({ apiKey, selectedModel, reasoningEffort, service const modelDropdown = (value: string, onChange: (v: string) => void) => (