* Fix red main: typecheck, lint, i18n and stale unit tests Direct pushes to main left typecheck, lint and 16 unit tests red. - Typecheck: AskCard test uses the current ask block shape, the sandbox fault test calls the two-argument desktop writeFile, removing a queued message notifies with the thread's latest event seq, and the replay fixture only reads ref from steps that carry one. - Lint: format and import order. - i18n: transcript status labels were German source strings; they are English now with German translations, catalogs re-extracted, and the mobile "Failed to remove message" string is translated. - Tests follow intended behavior changes: the time instruction now closes the system prompt, short chat turns start at low reasoning, owner-owned read-only files (git objects) pass the home check, and the desktop computer may read and write host files outside its home. The old desktop write-containment suite is replaced by a host file test. - The composer task pill test mocks the Lingui macros like other tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TnT7eVHuaCzZKY5CnYzpr1 * Bump Expo patch releases so expo install --check passes Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TnT7eVHuaCzZKY5CnYzpr1 * Bring web E2E back in line with the new side panel The side panel lost its computer tab, which left the full computer window (take control, teach, release) unreachable from the web app. The browser view now offers an "Open in full window" button, and opening the panel no longer boots a stopped computer. Also: the slash picker scrolls instead of growing under the header, user skills come first in it, the agent overview hides an empty routines block, the hover time hides again with collapsed details, and the light destructive token meets contrast on the sidebar. E2E specs follow the new panel layout, and the cursor and hover tests no longer race the UI. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TnT7eVHuaCzZKY5CnYzpr1 --------- Co-authored-by: Claude <noreply@anthropic.com>
250 lines
7.3 KiB
TypeScript
250 lines
7.3 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { i18n } from "@lingui/core";
|
|
import { I18nProvider } from "@lingui/react";
|
|
import type { ThreadMessage } from "@rakazo/contracts";
|
|
import { act } from "react";
|
|
import { createRoot } from "react-dom/client";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
function formatTemplate(parts: TemplateStringsArray, ...values: unknown[]) {
|
|
return parts.reduce(
|
|
(text, part, index) => text + part + (index < values.length ? values[index] : ""),
|
|
"",
|
|
);
|
|
}
|
|
|
|
vi.mock("@lingui/core/macro", () => ({
|
|
t: formatTemplate,
|
|
msg: (parts: TemplateStringsArray, ...values: unknown[]) => ({
|
|
id: parts.join(""),
|
|
message: formatTemplate(parts, ...values),
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@lingui/react/macro", () => {
|
|
return {
|
|
useLingui: () => ({ t: formatTemplate }),
|
|
Trans: ({ children }: { children: React.ReactNode }) => children,
|
|
Plural: ({ value, one, other }: { value: number; one: string; other: string }) =>
|
|
(value === 1 ? one : other).replace("#", String(value)),
|
|
};
|
|
});
|
|
|
|
import { ComposerTaskPill, extractActivePlanState, PlanCard } from "./message-cards";
|
|
|
|
// @ts-expect-error React act environment flag
|
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
function buildSampleMessages(): ThreadMessage[] {
|
|
return [
|
|
{
|
|
id: "u1",
|
|
threadId: "t1",
|
|
seq: 1,
|
|
role: "user",
|
|
runId: "r1",
|
|
createdAt: "2026-10-22T12:00:00Z",
|
|
blocks: [{ kind: "text", text: "Build brand concept" }],
|
|
},
|
|
{
|
|
id: "b-plan",
|
|
threadId: "t1",
|
|
seq: 2,
|
|
role: "bot",
|
|
botId: "bot-1",
|
|
runId: "r1",
|
|
createdAt: "2026-10-22T12:00:05Z",
|
|
blocks: [
|
|
{
|
|
kind: "plan",
|
|
planId: "p1",
|
|
title: "Branding-Konzept finalisieren",
|
|
status: "active",
|
|
steps: [
|
|
{ id: "s1", label: "Slogans erstellen", status: "completed" },
|
|
{
|
|
id: "s2",
|
|
label: "Brand Guide HTML-Seite erstellen",
|
|
status: "in_progress",
|
|
note: "Komplette Zusammenfassung aller Brand-Elemente",
|
|
},
|
|
{ id: "s3", label: "Alles auf Server kopieren", status: "pending" },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "b-step-1",
|
|
threadId: "t1",
|
|
seq: 3,
|
|
role: "bot",
|
|
botId: "bot-1",
|
|
runId: "r1",
|
|
createdAt: "2026-10-22T12:00:10Z",
|
|
blocks: [{ kind: "text", text: "running: pnpm build" }],
|
|
},
|
|
{
|
|
id: "b-step-2",
|
|
threadId: "t1",
|
|
seq: 4,
|
|
role: "bot",
|
|
botId: "bot-1",
|
|
runId: "r1",
|
|
createdAt: "2026-10-22T12:00:15Z",
|
|
blocks: [{ kind: "text", text: "writing brand-guide.html" }],
|
|
},
|
|
{
|
|
id: "progress:r1",
|
|
threadId: "t1",
|
|
seq: 5,
|
|
role: "bot",
|
|
botId: "bot-1",
|
|
runId: "r1",
|
|
createdAt: "2026-10-22T12:00:20Z",
|
|
blocks: [{ kind: "progress", text: "Working..." }],
|
|
},
|
|
];
|
|
}
|
|
|
|
describe("ComposerTaskPill & PlanCard integration", () => {
|
|
beforeEach(() => {
|
|
i18n.load("en", {});
|
|
i18n.activate("en");
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("extractActivePlanState collects the active plan and folded narration steps from the current turn", () => {
|
|
const messages = buildSampleMessages();
|
|
const state = extractActivePlanState(messages, true, "2026-10-22T12:00:00Z");
|
|
expect(state).not.toBeNull();
|
|
expect(state?.block.planId).toBe("p1");
|
|
expect(state?.steps.map((s) => s.id)).toEqual(["b-step-1", "b-step-2"]);
|
|
expect(state?.running).toBe(true);
|
|
});
|
|
|
|
it("renders compact active step + progress badge and expands to reveal full checklist & intermediate steps", async () => {
|
|
const state = extractActivePlanState(buildSampleMessages(), true, "2026-10-22T12:00:00Z")!;
|
|
const container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
const root = createRoot(container);
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
<I18nProvider i18n={i18n}>
|
|
<ComposerTaskPill activePlan={state} />
|
|
</I18nProvider>,
|
|
);
|
|
});
|
|
|
|
const pill = container.querySelector('[data-testid="composer-task-pill"]');
|
|
expect(pill).not.toBeNull();
|
|
expect(pill?.getAttribute("data-expanded")).toBe("false");
|
|
expect(container.querySelector('[data-testid="composer-task-pill-label"]')?.textContent).toBe(
|
|
"Brand Guide HTML-Seite erstellen",
|
|
);
|
|
expect(
|
|
container.querySelector('[data-testid="composer-task-pill-progress"]')?.textContent,
|
|
).toBe("1/3");
|
|
|
|
const toggle = container.querySelector(
|
|
'[data-testid="composer-task-pill-toggle"]',
|
|
) as HTMLButtonElement;
|
|
await act(async () => {
|
|
toggle.click();
|
|
});
|
|
|
|
expect(pill?.getAttribute("data-expanded")).toBe("true");
|
|
const intermediate = container.querySelector(
|
|
'[data-testid="composer-plan-intermediate-steps"]',
|
|
);
|
|
expect(intermediate).not.toBeNull();
|
|
|
|
const stepToggle = intermediate?.querySelector("button") as HTMLButtonElement;
|
|
await act(async () => {
|
|
stepToggle.click();
|
|
});
|
|
|
|
expect(intermediate?.textContent).toContain("running: pnpm build");
|
|
expect(intermediate?.textContent).toContain("writing brand-guide.html");
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
});
|
|
container.remove();
|
|
});
|
|
|
|
it("auto-collapses and hides after all steps complete", async () => {
|
|
vi.useFakeTimers();
|
|
const completedPlan = {
|
|
block: {
|
|
kind: "plan" as const,
|
|
planId: "p-done",
|
|
title: "Branding-Konzept finalisieren",
|
|
status: "completed" as const,
|
|
steps: [
|
|
{ id: "s1", label: "Slogans erstellen", status: "completed" as const },
|
|
{ id: "s2", label: "Brand Guide erstellen", status: "completed" as const },
|
|
],
|
|
},
|
|
steps: [],
|
|
running: false,
|
|
};
|
|
|
|
const container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
const root = createRoot(container);
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
<I18nProvider i18n={i18n}>
|
|
<ComposerTaskPill activePlan={completedPlan} />
|
|
</I18nProvider>,
|
|
);
|
|
});
|
|
|
|
expect(container.querySelector('[data-testid="composer-task-pill"]')).not.toBeNull();
|
|
expect(
|
|
container.querySelector('[data-testid="composer-task-pill-progress"]')?.textContent,
|
|
).toBe("Done");
|
|
|
|
await act(async () => {
|
|
vi.advanceTimersByTime(3600);
|
|
});
|
|
|
|
expect(container.querySelector('[data-testid="composer-task-pill"]')).toBeNull();
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
});
|
|
container.remove();
|
|
});
|
|
|
|
it("PlanCard embeds intermediate steps under the active step", async () => {
|
|
const state = extractActivePlanState(buildSampleMessages(), true, "2026-10-22T12:00:00Z")!;
|
|
const container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
const root = createRoot(container);
|
|
|
|
await act(async () => {
|
|
root.render(
|
|
<I18nProvider i18n={i18n}>
|
|
<PlanCard block={state.block} steps={state.steps} />
|
|
</I18nProvider>,
|
|
);
|
|
});
|
|
|
|
const activeStepRow = container.querySelector('[data-testid="plan-step-s2"]');
|
|
expect(activeStepRow).not.toBeNull();
|
|
expect(activeStepRow?.querySelector('[data-testid="plan-intermediate-steps"]')).not.toBeNull();
|
|
|
|
await act(async () => {
|
|
root.unmount();
|
|
});
|
|
container.remove();
|
|
});
|
|
});
|