Skip to content
Merged
11 changes: 6 additions & 5 deletions desktop/frontend/scripts/check-bundle-budget.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,10 @@ const rawInitialBytes = [...initialJS, ...initialCSS, ...appShellCSS]
.reduce((total, path) => total + statSync(path).size, 0);
// Native Web Animations and frame-batched scrolling avoid an eager animation
// runtime. Goal request observability plus transcript scroll arbitration,
// logical selection state/DOM adapters, native input-session ownership, and
// measurement invalidation, deferred tail replay, and startup config-warning
// delivery add small always-available contracts. The expanded contract remains
// tightly bounded while the gzip startup budgets stay flat.
assertBudget("initial raw JavaScript and CSS", rawInitialBytes, 2_248 * 1024);
// logical selection state/DOM adapters, native input-session ownership,
// durable inbox recovery controls, measurement invalidation and deferred tail
// replay, and startup config-warning delivery add small always-available
// contracts. The expanded contract remains tightly bounded while the gzip
// startup budgets stay flat.
assertBudget("initial raw JavaScript and CSS", rawInitialBytes, 2_251 * 1024);
assertBudget("largest initial JavaScript chunk raw", largestInitialJSRaw, 1_000 * 1024);
280 changes: 271 additions & 9 deletions desktop/frontend/src/__tests__/composer-inbox-recovery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class TestResizeObserver {
disconnect() {}
}

function installDom() {
function installDom(language = "en-US") {
const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", {
pretendToBeVisual: true,
url: "http://localhost/",
Expand All @@ -49,6 +49,7 @@ function installDom() {
globalThis.window = dom.window as unknown as Window & typeof globalThis;
globalThis.document = dom.window.document;
Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator });
Object.defineProperty(dom.window.navigator, "language", { configurable: true, value: language });
globalThis.Node = dom.window.Node;
globalThis.HTMLElement = dom.window.HTMLElement;
globalThis.HTMLTextAreaElement = dom.window.HTMLTextAreaElement;
Expand Down Expand Up @@ -100,7 +101,7 @@ function installBridgeApp(methods: Record<string, unknown>) {
};
}

async function renderComposer(props: Partial<Parameters<typeof Composer>[0]> = {}) {
async function renderComposer(props: Partial<Parameters<typeof Composer>[0]> = {}, strictMode = false) {
const rootEl = document.getElementById("root");
if (!rootEl) throw new Error("missing root");
const root = createRoot(rootEl);
Expand Down Expand Up @@ -130,14 +131,15 @@ async function renderComposer(props: Partial<Parameters<typeof Composer>[0]> = {
};
const paint = async (nextProps: Partial<Parameters<typeof Composer>[0]> = {}) => {
currentProps = { ...currentProps, ...nextProps };
const view = (
<LocaleProvider>
<ToastProvider>
<Composer {...currentProps} />
</ToastProvider>
</LocaleProvider>
);
await act(async () => {
root.render(
<LocaleProvider>
<ToastProvider>
<Composer {...currentProps} />
</ToastProvider>
</LocaleProvider>,
);
root.render(strictMode ? <React.StrictMode>{view}</React.StrictMode> : view);
await flushTimers();
});
};
Expand Down Expand Up @@ -225,6 +227,266 @@ console.log("\ncomposer inbox recovery");
dom.window.close();
}

{
const dom = installDom("zh-CN");
const snapshot = {
revision: 3,
paused: false,
recovered: false,
recoveredCount: 0,
items: [{
id: "queued-before-pause",
intent: "followup",
state: "queued",
preview: "引导当前回合",
byteSize: 64,
position: 1,
}],
itemsCount: 1,
bytes: 64,
maxItems: 64,
maxBytes: 64 * 1024 * 1024,
};
installBridgeApp({
InboxSnapshot: async () => snapshot,
SteerInboxItem: async () => { throw new Error("reasonix_error:inbox_paused"); },
});
const { root } = await renderComposer({ running: true });

await waitFor("queued guidance rendered for localization check", () => document.querySelector(".composer-guidance-item__guide") !== null);
const guide = document.querySelector(".composer-guidance-item__guide") as HTMLButtonElement;
await act(async () => { guide.click(); await flushTimers(); });
await waitFor("localized paused toast rendered", () => document.querySelector(".toast__text")?.textContent === "收件箱已暂停");
ok(document.querySelector(".toast__text")?.textContent === "收件箱已暂停", "coded paused error renders in the active Chinese locale");

await act(async () => { root.unmount(); });
dom.window.close();
}

{
const dom = installDom();
let snapshot = {
revision: 2,
paused: true,
recovered: false,
recoveredCount: 0,
items: [{
id: "paused-queued-1",
intent: "followup",
state: "queued",
preview: "Guide the active turn",
byteSize: 64,
position: 1,
}],
itemsCount: 1,
bytes: 64,
maxItems: 64,
maxBytes: 64 * 1024 * 1024,
};
const pauseCalls: boolean[] = [];
installBridgeApp({
InboxSnapshot: async () => snapshot,
SetInboxPaused: async (_tabId: string, paused: boolean) => {
pauseCalls.push(paused);
if (!paused) snapshot = { ...snapshot, paused: false };
},
});
const { root } = await renderComposer({ running: true }, true);

await waitFor("ordinary pause banner rendered", () => document.querySelector(".composer-inbox-recovery") !== null);
const pauseBanner = document.querySelector(".composer-inbox-recovery");
ok(pauseBanner?.textContent?.includes("Inbox is paused") === true, "non-recovered pause exposes an actionable pause banner");
const guide = document.querySelector(".composer-guidance-item__guide") as HTMLButtonElement | null;
ok(guide?.disabled === true, "paused inbox disables guide admission instead of surfacing a backend error");

const buttons = Array.from(document.querySelectorAll(".composer-inbox-recovery .btn")) as HTMLButtonElement[];
if (buttons[1]) {
await act(async () => { buttons[1].click(); await flushTimers(); });
ok(pauseCalls.at(-1) === false, "continue resumes a non-recovered paused inbox");
await waitFor("ordinary pause banner cleared after resume", () => document.querySelector(".composer-inbox-recovery") === null);
}

await act(async () => { root.unmount(); });
dom.window.close();
}

{
const dom = installDom();
let snapshot = {
revision: 4,
paused: false,
recovered: false,
recoveredCount: 0,
items: [
{
id: "idle-queued-1",
intent: "followup",
state: "queued",
preview: "Send the next instruction",
byteSize: 64,
position: 1,
},
{
id: "idle-queued-2",
intent: "followup",
state: "queued",
preview: "Wait behind the first instruction",
byteSize: 64,
position: 2,
},
],
itemsCount: 2,
bytes: 128,
maxItems: 64,
maxBytes: 64 * 1024 * 1024,
};
const pauseCalls: Array<{ tabId: string; paused: boolean }> = [];
let steerCalls = 0;
installBridgeApp({
InboxSnapshot: async () => snapshot,
SetInboxPaused: async (tabId: string, paused: boolean) => {
pauseCalls.push({ tabId, paused });
snapshot = { ...snapshot, items: [], itemsCount: 0, bytes: 0 };
},
SteerInboxItem: async () => { steerCalls += 1; },
});
const { root } = await renderComposer({ running: false });

await waitFor("idle durable guidance rendered", () => document.querySelectorAll(".composer-guidance-item__guide").length === 2);
const guides = Array.from(document.querySelectorAll(".composer-guidance-item__guide")) as HTMLButtonElement[];
ok(!guides[0].disabled && guides[0].textContent === "Send", "idle FIFO head exposes an explicit send fallback");
ok(guides[0].getAttribute("aria-label") === "Send this guidance to the transcript", "idle send fallback has an accessible label");
ok(guides[1].disabled, "idle non-head guidance remains disabled to preserve FIFO order");
ok(guides[1].getAttribute("aria-label") === "Waiting for earlier queued guidance", "idle non-head guidance explains the FIFO gate");
await act(async () => { guides[0].click(); await flushTimers(); });
ok(pauseCalls.length === 1 && pauseCalls[0].tabId === "tab-a" && pauseCalls[0].paused === false, "idle send re-kicks the Controller drain without changing pause state");
ok(steerCalls === 0, "idle send never attempts active-turn steering");
await waitFor("idle durable guidance clears after dispatch kick", () => document.querySelector(".composer-guidance-item") === null);

await act(async () => { root.unmount(); });
dom.window.close();
}

{
const dom = installDom();
let steerCalls = 0;
let deleteCalls = 0;
installBridgeApp({
InboxSnapshot: async () => ({
revision: 4,
paused: false,
recovered: false,
recoveredCount: 0,
items: [{
id: "active-steer",
intent: "steer",
state: "steer_accepted",
preview: "Already admitted guidance",
byteSize: 64,
position: 1,
}],
itemsCount: 1,
bytes: 64,
maxItems: 64,
maxBytes: 64 * 1024 * 1024,
}),
SteerInboxItem: async () => { steerCalls += 1; },
DeleteInboxItem: async () => { deleteCalls += 1; },
});
const { root } = await renderComposer({ running: true });

await waitFor("active steer rendered", () => document.querySelector(".composer-guidance-item__guide") !== null);
const guide = document.querySelector(".composer-guidance-item__guide") as HTMLButtonElement;
const dismiss = document.querySelector(".composer-guidance-item__action") as HTMLButtonElement;
ok(guide.disabled && dismiss.disabled, "active steer disables send and delete actions");
ok(guide.getAttribute("aria-label") === "Guidance is already being applied", "active steer explains why actions are disabled");
await act(async () => { guide.click(); dismiss.click(); await flushTimers(); });
ok(steerCalls === 0 && deleteCalls === 0, "active steer never sends invalid backend operations");

await act(async () => { root.unmount(); });
dom.window.close();
}

{
const dom = installDom();
let retryCalls = 0;
let steerCalls = 0;
const snapshot = {
revision: 5,
paused: false,
recovered: true,
recoveredCount: 1,
items: [{
id: "uncertain-guidance",
intent: "followup",
state: "uncertain",
preview: "Retry recovered guidance",
byteSize: 64,
position: 1,
}],
itemsCount: 1,
bytes: 64,
maxItems: 64,
maxBytes: 64 * 1024 * 1024,
};
installBridgeApp({
InboxSnapshot: async () => snapshot,
RetryInboxItem: async () => { retryCalls += 1; },
SteerInboxItem: async () => { steerCalls += 1; },
});
const { root } = await renderComposer({ running: false });

await waitFor("uncertain guidance rendered", () => document.querySelector(".composer-guidance-item__guide") !== null);
const retry = document.querySelector(".composer-guidance-item__guide") as HTMLButtonElement;
ok(!retry.disabled && retry.textContent === "Retry", "uncertain idle guidance exposes an explicit retry action");
ok(retry.getAttribute("aria-label") === "Retry this guidance", "retry action has a state-aware accessible label");
await act(async () => { retry.click(); await flushTimers(); });
ok(retryCalls === 1 && steerCalls === 0, "idle retry requeues through the Controller without an invalid steer");

await act(async () => { root.unmount(); });
dom.window.close();
}

{
const dom = installDom();
let retryCalls = 0;
let steerCalls = 0;
installBridgeApp({
InboxSnapshot: async () => ({
revision: 6,
paused: false,
recovered: false,
recoveredCount: 0,
items: [{
id: "blocked-guidance",
intent: "steer",
state: "blocked",
preview: "Retry blocked guidance",
byteSize: 64,
position: 1,
}],
itemsCount: 1,
bytes: 64,
maxItems: 64,
maxBytes: 64 * 1024 * 1024,
}),
RetryInboxItem: async () => { retryCalls += 1; },
SteerInboxItem: async () => {
steerCalls += 1;
return { disposition: "queued_followup" };
},
});
const { root } = await renderComposer({ running: true });

await waitFor("blocked guidance rendered", () => document.querySelector(".composer-guidance-item__guide") !== null);
const retry = document.querySelector(".composer-guidance-item__guide") as HTMLButtonElement;
await act(async () => { retry.click(); await flushTimers(); });
ok(retryCalls === 1 && steerCalls === 1, "busy retry requeues before attempting active-turn admission");

await act(async () => { root.unmount(); });
dom.window.close();
}

if (failed > 0) {
process.stderr.write(`\n${failed} failed, ${passed} passed\n`);
process.exit(1);
Expand Down
58 changes: 58 additions & 0 deletions desktop/frontend/src/__tests__/inbox-error-localization.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Run: tsx src/__tests__/inbox-error-localization.test.ts

import { formatInboxCancelError, formatInboxError } from "../lib/inboxError";

let passed = 0;
let failed = 0;

function eq(actual: unknown, expected: unknown, label: string) {
if (actual === expected) {
process.stdout.write(` PASS ${label}\n`);
passed += 1;
} else {
process.stdout.write(` FAIL ${label}: got ${JSON.stringify(actual)}, want ${JSON.stringify(expected)}\n`);
failed += 1;
}
}

console.log("\ninbox error localization");

const stableCases = [
["inbox_paused", "收件箱已暂停"],
["inbox_capacity_items", "收件箱已达到条目上限"],
["inbox_capacity_bytes", "收件箱已达到容量上限"],
["inbox_item_too_large", "这条指令太大,无法加入收件箱"],
["inbox_item_not_found", "这条收件箱指令已不存在"],
["inbox_invalid_state", "当前状态下无法操作这条收件箱指令"],
["inbox_schema_readonly", "收件箱由较新版本创建,当前只能读取"],
["inbox_closed", "收件箱已关闭"],
["inbox_empty", "不能加入空白指令"],
["inbox_idempotency_conflict", "这条指令与已提交的请求冲突"],
["channel_read_only", "当前会话为只读,无法修改收件箱"],
["workspace_starting", "工作区还在启动,请稍后重试"],
["workspace_start_failed", "工作区启动失败"],
] as const;

for (const [code, expected] of stableCases) {
eq(formatInboxError(new Error(`reasonix_error:${code}`), "zh"), expected, `zh maps ${code}`);
}

eq(formatInboxError(new Error("inbox is paused"), "zh"), "收件箱已暂停", "legacy backend English is localized");
eq(formatInboxError(new Error("workspace failed to start: internal detail"), "zh"), "工作区启动失败", "legacy startup detail is sanitized and localized");
eq(formatInboxError("reasonix_error:inbox_paused", "zh-TW"), "收件匣已暫停", "traditional Chinese maps stable code");
eq(formatInboxError(new Error("reasonix_error:inbox_paused"), "en"), "Inbox is paused", "English maps stable code");
eq(
formatInboxCancelError(new Error("reasonix_error:inbox_invalid_state"), "zh"),
"取消失败:当前状态下无法操作这条收件箱指令",
"cancel failures localize both context and stable code",
);

const diagnostic = "filesystem detail: /private/example";
eq(formatInboxError(new Error(diagnostic), "zh"), diagnostic, "unknown diagnostic details stay intact");
eq(formatInboxCancelError(new Error(diagnostic), "zh"), `取消失败:${diagnostic}`, "localized cancel context preserves unknown diagnostics");

if (failed > 0) {
process.stderr.write(`\n${failed} failed, ${passed} passed\n`);
process.exit(1);
}
process.stdout.write(`\n${passed} passed\n`);
Loading