| 1 | import { readFileSync } from "node:fs"; |
| 2 | import { |
| 3 | formatContextMaintenanceNotice, |
| 4 | isNewMaintenanceOperation, |
| 5 | rememberMaintenanceOperation, |
| 6 | } from "../lib/contextMaintenanceTypes"; |
| 7 | import type { DictKey, Translator } from "../lib/i18n"; |
| 8 | |
| 9 | function ok(value: unknown, message: string) { |
| 10 | if (!value) throw new Error(message); |
| 11 | } |
| 12 | |
| 13 | const messages: Partial<Record<DictKey, string>> = { |
| 14 | "context.maintenanceTitle": "上下文短视图", |
| 15 | "context.maintenanceAppliedSummary": "已生成短视图", |
| 16 | "context.maintenanceBlockedSummary": "摘要未形成短视图 · 已停重试", |
| 17 | "context.maintenanceFailedSummary": "摘要失败 · 已停重试", |
| 18 | "context.maintenanceTruncatedSummary": "已裁剪上下文视图", |
| 19 | "context.tokensValue": "{value} tokens", |
| 20 | "summary.detail": "摘要", |
| 21 | }; |
| 22 | |
| 23 | const translate: Translator = (key, vars) => { |
| 24 | const value = messages[key] ?? key; |
| 25 | return value.replace(/\{(\w+)\}/g, (_, name: string) => String(vars?.[name] ?? `{${name}}`)); |
| 26 | }; |
| 27 | |
| 28 | const applied = formatContextMaintenanceNotice({ |
| 29 | status: "applied", |
| 30 | action: "summary", |
| 31 | inputTokens: 120, |
| 32 | resultTokens: 80, |
| 33 | savedTokens: 40, |
| 34 | }, translate); |
| 35 | ok(applied === "已生成短视图", `unexpected applied notice: ${applied}`); |
| 36 | |
| 37 | const blocked = formatContextMaintenanceNotice({ status: "blocked" }, translate); |
| 38 | ok(blocked === "摘要未形成短视图 · 已停重试", `unexpected blocked notice: ${blocked}`); |
| 39 | |
| 40 | const failed = formatContextMaintenanceNotice({ status: "failed" }, translate); |
| 41 | ok(failed === "摘要失败 · 已停重试", `unexpected failed notice: ${failed}`); |
| 42 | |
| 43 | const truncated = formatContextMaintenanceNotice({ status: "applied", action: "truncate" }, translate); |
| 44 | ok(truncated === "已裁剪上下文视图", `unexpected truncated notice: ${truncated}`); |
| 45 | |
| 46 | const contextPanelSource = readFileSync(new URL("../components/ContextPanel.tsx", import.meta.url), "utf8"); |
| 47 | ok( |
| 48 | !contextPanelSource.includes('className="context-panel__maintenance"'), |
| 49 | "ContextPanel must not render the context checkpoint detail block", |
| 50 | ); |
| 51 | ok( |
| 52 | !contextPanelSource.includes('t("context.maintenanceCanonical")') |
| 53 | && !contextPanelSource.includes('t("context.maintenanceCheckpoint")'), |
| 54 | "ContextPanel must not expose canonical, model-visible, or checkpoint details", |
| 55 | ); |
| 56 | ok( |
| 57 | !contextPanelSource.includes("checkpointState") |
| 58 | && !contextPanelSource.includes("canonicalTokens") |
| 59 | && !contextPanelSource.includes("projectedTokens"), |
| 60 | "ContextPanel must not derive hidden checkpoint presentation state", |
| 61 | ); |
| 62 | ok( |
| 63 | !contextPanelSource.includes("snipTrigger") && !contextPanelSource.includes("forceTrigger"), |
| 64 | "ContextPanel must not present retired multi-threshold triggers as user settings", |
| 65 | ); |
| 66 | |
| 67 | ok(isNewMaintenanceOperation([], "op-1"), "empty seen list accepts first operationId"); |
| 68 | ok(isNewMaintenanceOperation(["op-1"], "op-1") === false, "duplicate operationId is rejected"); |
| 69 | ok(isNewMaintenanceOperation(["op-1"], "op-2"), "distinct operationId is accepted"); |
| 70 | ok(isNewMaintenanceOperation(["op-1"], ""), "missing operationId is still shown"); |
| 71 | const remembered = rememberMaintenanceOperation(["op-1"], "op-2"); |
| 72 | ok(remembered.includes("op-1") && remembered.includes("op-2"), "remember keeps prior and new ids"); |
| 73 | ok(rememberMaintenanceOperation(["op-1"], "op-1").length === 1, "remember is idempotent for same id"); |
| 74 | const many = Array.from({ length: 70 }, (_, i) => `op-${i}`); |
| 75 | const bounded = rememberMaintenanceOperation(many.slice(0, 64), "op-new"); |
| 76 | ok(bounded.length === 64 && bounded[bounded.length - 1] === "op-new", "remember bounds to 64 ids"); |
| 77 | |
| 78 | const settingsSource = readFileSync(new URL("../components/SettingsPanel.tsx", import.meta.url), "utf8"); |
| 79 | ok( |
| 80 | !settingsSource.includes("settings.coldResumePrune") && !settingsSource.includes("SetColdResumePrune"), |
| 81 | "SettingsPanel must not expose retired coldResumePrune control", |
| 82 | ); |
| 83 | |
| 84 | console.log("context-maintenance-notice: ok"); |
| 85 |