Reduce test-suite bottlenecks and redundant assertions

This commit is contained in:
murashit 2026-06-25 19:05:43 +09:00
parent dd280f74e7
commit b8a085b4bf
12 changed files with 358 additions and 634 deletions

View file

@ -1,21 +1,31 @@
import { spawnSync } from "node:child_process";
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const generatedDir = path.resolve("src/generated/app-server");
const generatedRelativeDir = "src/generated/app-server";
const generatedHeader = "// GENERATED CODE! DO NOT MODIFY BY HAND!";
const normalizationNotice = "// This file was mechanically normalized after generation by scripts/generate-app-server-types.mjs.";
await cleanGeneratedTypes();
run("codex", ["app-server", "generate-ts", "--experimental", "--out", "src/generated/app-server"]);
await normalizeGeneratedTypes();
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
await generateAppServerTypes();
}
async function cleanGeneratedTypes() {
export async function generateAppServerTypes(options = {}) {
const cwd = options.cwd ?? process.cwd();
const generatedDir = path.resolve(cwd, generatedRelativeDir);
const runCommand = options.runCommand ?? run;
await cleanGeneratedTypes(generatedDir);
await runCommand("codex", ["app-server", "generate-ts", "--experimental", "--out", generatedRelativeDir], { cwd });
await normalizeGeneratedTypes(generatedDir);
}
async function cleanGeneratedTypes(generatedDir) {
await rm(generatedDir, { recursive: true, force: true });
await mkdir(generatedDir, { recursive: true });
}
async function normalizeGeneratedTypes() {
async function normalizeGeneratedTypes(generatedDir) {
const files = await listTypeScriptFiles(generatedDir);
await Promise.all(
@ -55,8 +65,9 @@ function addNormalizationNotice(source) {
return source.replace(generatedHeader, `${generatedHeader}\n${normalizationNotice}`);
}
function run(command, args) {
function run(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: options.cwd ?? process.cwd(),
stdio: "inherit",
shell: false,
});

View file

@ -44,6 +44,8 @@ describe("thread helpers", () => {
expect(threadRenameDraftTitle(uuidNamed)).toBe("Useful preview");
expect(threadArchiveTitle(uuidNamed)).toBe("Useful preview");
expect(threadArchiveDisplayTitle(uuidNamed)).toBe("Useful preview");
expect(threadArchiveDisplayTitle(thread({ preview: "A title\nwith extra\tspace" }))).toBe("A title with extra space");
expect(threadArchiveDisplayTitle(thread({ preview: "x".repeat(120) }))).toMatch(/^x{93}\.\.\.$/);
});
it("builds window titles from loaded threads, restored titles, then short ids", () => {

View file

@ -15,7 +15,6 @@ import {
type ChatMessageScrollController,
createChatMessageScrollController,
} from "../../../../../src/features/chat/panel/surface/message-stream-scroll";
import { MESSAGE_CONTENT_RENDERED_EVENT } from "../../../../../src/features/chat/ui/message-stream/content-events";
import { MarkdownMessageRenderer } from "../../../../../src/features/chat/ui/message-stream/markdown-renderer.obsidian";
import { MessageStreamViewport } from "../../../../../src/features/chat/ui/message-stream/stream-blocks";
import { renderUiRoot, unmountUiRoot } from "../../../../../src/shared/ui/ui-root.dom";
@ -225,39 +224,6 @@ describe("MessageStreamPresenter scroll pinning", () => {
expect(scrollIntoView).not.toHaveBeenCalled();
});
it("can repin the current scroll container after composer growth shrinks the viewport", async () => {
let state = chatStateFixture();
state = chatStateWith(state, { activeThread: { id: "thread" } });
state = withChatStateMessageStreamItems(state, [
{
id: "message",
kind: "message",
role: "assistant",
text: "Streaming message",
turnId: "turn",
messageKind: "assistantResponse",
messageState: "completed",
},
]);
const parent = document.createElement("div");
const { presenter, scrollController } = messageStreamPresenter(state);
renderMessageStreamPresenter(parent, presenter, state);
const messages = messageViewport(parent);
Object.defineProperty(messages, "scrollHeight", { value: ESTIMATED_MESSAGE_BLOCK_HEIGHT, configurable: true });
installMessageViewportMetrics(messages, { clientHeight: 160 });
messages.scrollTop = 1000;
await settleMessageRender(messages);
installMessageViewportMetrics(messages, { clientHeight: 100 });
messages.scrollTop = 940;
scrollController.showLatest();
await settleMessageRender(messages);
expect(messages.scrollTop).toBe(0);
});
it("repins after composer growth has changed the scroll viewport height", async () => {
let state = chatStateFixture();
state = chatStateWith(state, { activeThread: { id: "thread" } });
@ -414,45 +380,6 @@ describe("MessageStreamPresenter scroll pinning", () => {
expect(messages.scrollTop).toBe(900);
});
it("keeps bottom pinning after markdown content changes message height", async () => {
let state = chatStateFixture();
state = chatStateWith(state, { activeThread: { id: "thread" } });
state = withChatStateMessageStreamItems(state, [
{
id: "message",
kind: "message",
role: "assistant",
text: "**Rendered** message",
turnId: "turn",
messageKind: "assistantResponse",
messageState: "completed",
},
]);
const parent = document.createElement("div");
const { presenter, scrollController } = messageStreamPresenter(state);
renderMessageStreamPresenter(parent, presenter, state);
const messages = messageViewport(parent);
installMessageViewportMetrics(messages, { clientHeight: 100 });
let scrollHeight = 1000;
Object.defineProperty(messages, "scrollHeight", {
get: () => scrollHeight,
configurable: true,
});
scrollController.showLatest();
await settleMessageRender(messages);
expect(messages.scrollTop).toBe(900);
scrollHeight = 1200;
const content = parent.querySelector<HTMLElement>(".codex-panel__message-content");
if (!content) throw new Error("Expected rendered message content.");
content.dispatchEvent(new Event(MESSAGE_CONTENT_RENDERED_EVENT, { bubbles: true }));
await settleMessageRender(messages);
expect(messages.scrollTop).toBe(1100);
});
it("does not force the bottom into view when the user is reading older messages", async () => {
let state = chatStateFixture();
state = chatStateWith(state, { activeThread: { id: "thread" } });

View file

@ -87,29 +87,6 @@ function composerCallbacks() {
}
describe("ComposerShell decisions", () => {
it("uses the provided composer placeholder for normal input", () => {
const parent = document.createElement("div");
const callbacks = composerCallbacks();
const { composer } = mountComposerShell(
parent,
"view",
"",
false,
false,
"Ask Codex to work on “Refactor terminal streaming”...",
[],
0,
callbacks,
);
expect(composer.getAttribute("placeholder")).toBe("Ask Codex to work on “Refactor terminal streaming”...");
expect(composer.getAttribute("aria-label")).toBeNull();
mountComposerShell(parent, "view", "", false, false, "Ask Codex to work on “Renamed thread”...", [], 0, callbacks);
expect(composer.getAttribute("placeholder")).toBe("Ask Codex to work on “Renamed thread”...");
});
it("renders composer meta as interactive context and runtime text without changing normal text", () => {
const parent = document.createElement("div");

View file

@ -24,25 +24,6 @@ describe("GoalPanel", () => {
expect(parent.textContent).toBe("");
});
it("opens an empty goal editor when editing is requested without a saved goal", async () => {
const parent = document.createElement("div");
document.body.appendChild(parent);
const callbacks = actions();
await act(async () => {
renderGoal(parent, null, callbacks, "enter", { editing: true, objectiveDraft: "", tokenBudgetDraft: null });
});
expect(parent.textContent).toContain("Goal");
expect(document.activeElement).toBe(parent.querySelector("textarea"));
expect(parent.querySelector("textarea")?.getAttribute("aria-label")).toBeNull();
await input(parent, "textarea", "New objective");
await click(parent, '[aria-label="Save goal"]');
expect(callbacks.onSave).toHaveBeenCalledWith("New objective", null);
parent.remove();
});
it("renders active goal details and actions", async () => {
const parent = document.createElement("div");

View file

@ -41,23 +41,14 @@ describe("Toolbar decisions", () => {
expect(parent.querySelector(".codex-panel__runtime-model")).toBeNull();
const newChatButton = parent.querySelector<HTMLButtonElement>(".codex-panel__new-chat");
expect(newChatButton?.getAttribute("aria-label")).toBe("Show chat actions");
expect(newChatButton?.dataset["icon"]).toBe("messages-square");
expect(newChatButton?.classList.contains("nav-action-button")).toBe(true);
expect(newChatButton?.disabled).toBe(false);
newChatButton?.click();
expect(toggleChatActions).toHaveBeenCalled();
expect(startNewThread).not.toHaveBeenCalled();
const statusButton = parent.querySelector(".codex-panel__status-menu-toggle");
expect(statusButton?.tagName).toBe("BUTTON");
expect(statusButton?.getAttribute("role")).toBeNull();
expect(statusButton?.getAttribute("aria-label")).toBe("Show status");
expect((statusButton as HTMLElement | null)?.dataset["icon"]).toBe("waypoints");
expect(statusButton?.classList.contains("nav-action-button")).toBe(true);
expect(statusButton?.classList.contains("clickable-icon")).toBe(true);
const historyButton = parent.querySelector<HTMLButtonElement>(".codex-panel__history-toggle");
expect(historyButton?.getAttribute("aria-label")).toBe("Show thread list");
expect(historyButton?.classList.contains("nav-action-button")).toBe(true);
expect(historyButton?.classList.contains("clickable-icon")).toBe(true);
historyButton?.click();
expect(toggleHistory).toHaveBeenCalled();
parent.empty();
@ -236,7 +227,6 @@ describe("Toolbar decisions", () => {
);
const renameButton = expectPresent(parent.querySelector<HTMLButtonElement>('[aria-label="Rename thread"]'));
expect(renameButton.classList.contains("nav-action-button")).toBe(false);
renameButton.click();
expect(startRenameThread).toHaveBeenCalledWith("thread");
const threadRow = parent.querySelector(".codex-panel__thread-row");
@ -245,7 +235,6 @@ describe("Toolbar decisions", () => {
const input = parent.querySelector<HTMLInputElement>(".codex-panel__thread-rename-input");
if (!input) throw new Error("Missing thread rename input");
expect(input.closest(".codex-panel__thread-rename")?.querySelector(".codex-panel__toolbar-panel-check")).toBeNull();
expect(input.value).toBe("Draft title");
changeInputValue(input, "New title");
expect(updateRenameDraft).toHaveBeenCalledWith("editing", "New title");
@ -275,10 +264,7 @@ describe("Toolbar decisions", () => {
new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
);
expect(cancelRenameThread).toHaveBeenCalledWith("editing");
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Save thread name"]')).toBeNull();
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Cancel rename"]')).toBeNull();
parent.querySelector<HTMLButtonElement>('[aria-label="Auto-name thread"]')?.click();
expect(parent.querySelector<HTMLButtonElement>('[aria-label="Auto-name thread"]')?.classList.contains("nav-action-button")).toBe(false);
expect(autoNameThread).toHaveBeenCalledWith("editing");
});

View file

@ -1,6 +1,6 @@
// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { ServerNotification } from "../../../src/app-server/connection/rpc-messages";
import { modelMetadataFromCatalogModels } from "../../../src/app-server/protocol/catalog";
import type { ThreadRecord } from "../../../src/app-server/protocol/thread";
@ -20,6 +20,7 @@ import { installObsidianDomShims } from "../../support/dom";
const ESTIMATED_MESSAGE_BLOCK_HEIGHT = 96;
type TestCodexChatHost = CodexChatHost;
let CodexChatView: typeof import("../../../src/features/chat/host/view")["CodexChatView"];
const connectionMock = vi.hoisted(() => {
const state = {
@ -110,6 +111,10 @@ installObsidianDomShims();
describe("CodexChatView connection lifecycle", () => {
let restoreDefaultMessageViewportMetrics: (() => void) | null = null;
beforeAll(async () => {
({ CodexChatView } = await import("../../../src/features/chat/host/view"));
});
beforeEach(() => {
vi.useRealTimers();
notices.length = 0;
@ -764,56 +769,6 @@ describe("CodexChatView connection lifecycle", () => {
});
});
it("uses the active explicit thread name in the composer placeholder", async () => {
const client = connectedClient();
connectionMock.state.client = client;
const view = await chatView();
await view.onOpen();
await view.surface.openThread("thread-1");
expect(composerPlaceholder(view)).toBe("Ask Codex to work on “Restored thread”...");
});
it("does not use preview-only thread text in the composer placeholder", async () => {
const client = connectedClient({
resumeThread: vi.fn().mockResolvedValue({
...resumedThread("thread-1"),
thread: { ...resumedThread("thread-1").thread, name: null, preview: "Restored preview" },
}),
});
connectionMock.state.client = client;
const view = await chatView();
await view.onOpen();
await view.surface.openThread("thread-1");
expect(composerPlaceholder(view)).toBe("Ask Codex to work on this task...");
});
it("updates the composer placeholder from shared rename notifications", async () => {
const client = connectedClient();
connectionMock.state.client = client;
const host = chatHost();
const view = await chatView({ host });
await view.onOpen();
await view.surface.openThread("thread-1");
host.threadCatalog.apply({ type: "active-list-snapshot-received", threads: [panelThread({ id: "thread-1", name: "Renamed thread" })] });
view.surface.applyThreadRenamed("thread-1", "Renamed thread");
await waitForAsyncWork(() => {
expect(composerPlaceholder(view)).toBe("Ask Codex to work on “Renamed thread”...");
});
host.threadCatalog.apply({ type: "active-list-snapshot-received", threads: [panelThread({ id: "thread-1", name: null })] });
view.surface.applyThreadRenamed("thread-1", null);
await waitForAsyncWork(() => {
expect(composerPlaceholder(view)).toBe("Ask Codex to work on this task...");
});
});
it("keeps composer draft and selection while updating the placeholder", async () => {
const client = connectedClient();
connectionMock.state.client = client;
@ -1322,7 +1277,7 @@ async function submitComposerByEnter(view: { containerEl: HTMLElement }): Promis
}
async function flushAsyncTicks(): Promise<void> {
for (let index = 0; index < 20; index += 1) {
for (let index = 0; index < 10; index += 1) {
await Promise.resolve();
}
}
@ -1517,7 +1472,6 @@ async function chatView(
options: { activeLeafChangeListeners?: ((leaf: unknown) => void)[]; host?: CodexChatHost; requestSaveLayout?: () => void } = {},
) {
const host = options.host ?? chatHost();
const { CodexChatView } = await import("../../../src/features/chat/host/view");
const containerEl = document.createElement("div");
document.body.appendChild(containerEl);
containerEl.createDiv();

View file

@ -239,31 +239,34 @@ describe("selection rewrite lifecycle", () => {
});
});
it.each([
{ status: "editing-prompt", event: previewUpdatedEvent() },
{ status: "editing-prompt", event: generationSucceededEvent() },
{ status: "editing-prompt", event: generationFailedEvent() },
{ status: "preview", event: previewUpdatedEvent() },
{ status: "preview", event: generationSucceededEvent() },
{ status: "preview", event: generationFailedEvent() },
{ status: "failed", event: previewUpdatedEvent() },
{ status: "failed", event: generationSucceededEvent() },
{ status: "failed", event: generationFailedEvent() },
{ status: "cancelled", event: { type: "generation-started", instruction: "late" } },
{ status: "cancelled", event: previewUpdatedEvent() },
{ status: "cancelled", event: generationSucceededEvent() },
{ status: "cancelled", event: generationFailedEvent() },
{ status: "applied", event: { type: "generation-started", instruction: "late" } },
{ status: "applied", event: previewUpdatedEvent() },
{ status: "applied", event: generationSucceededEvent() },
{ status: "applied", event: generationFailedEvent() },
] satisfies {
status: SelectionRewriteState["status"];
event: SelectionRewriteLifecycleEvent;
}[])("preserves state identity for ignored transition from $status via $event.type", ({ status, event }) => {
const state = selectionRewriteStateWithStatus(status);
it("preserves state identity for ignored transitions", () => {
const ignoredTransitions = [
{ status: "editing-prompt", event: previewUpdatedEvent() },
{ status: "editing-prompt", event: generationSucceededEvent() },
{ status: "editing-prompt", event: generationFailedEvent() },
{ status: "preview", event: previewUpdatedEvent() },
{ status: "preview", event: generationSucceededEvent() },
{ status: "preview", event: generationFailedEvent() },
{ status: "failed", event: previewUpdatedEvent() },
{ status: "failed", event: generationSucceededEvent() },
{ status: "failed", event: generationFailedEvent() },
{ status: "cancelled", event: { type: "generation-started", instruction: "late" } },
{ status: "cancelled", event: previewUpdatedEvent() },
{ status: "cancelled", event: generationSucceededEvent() },
{ status: "cancelled", event: generationFailedEvent() },
{ status: "applied", event: { type: "generation-started", instruction: "late" } },
{ status: "applied", event: previewUpdatedEvent() },
{ status: "applied", event: generationSucceededEvent() },
{ status: "applied", event: generationFailedEvent() },
] satisfies {
status: SelectionRewriteState["status"];
event: SelectionRewriteLifecycleEvent;
}[];
expect(transitionSelectionRewriteState(state, event)).toBe(state);
for (const { status, event } of ignoredTransitions) {
const state = selectionRewriteStateWithStatus(status);
expect(transitionSelectionRewriteState(state, event), `${status} via ${event.type}`).toBe(state);
}
});
});

View file

@ -81,12 +81,6 @@ describe("CodexThreadsView", () => {
namingMock.generateThreadTitleWithCodex.mockReset();
});
it("uses a distinct thread view icon", async () => {
const view = await threadsView();
expect(view.getIcon()).toBe("list-video");
});
it("renders thread list from app-server history", async () => {
connectionMock.state.client = clientFixture({
listThreads: vi.fn().mockResolvedValue({ data: [threadFixture({ id: "thread", preview: "Thread preview" })] }),

View file

@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
import { chmod, mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { describe, expect, it } from "vitest";
const repoRoot = process.cwd();
@ -18,30 +19,28 @@ describe("development scripts", () => {
it("passes the expected codex generate-ts arguments", async () => {
const cwd = await tempWorkspace();
const binDir = path.join(cwd, "bin");
await mkdir(binDir, { recursive: true });
await mkdir(path.join(cwd, "src", "generated", "app-server"), { recursive: true });
const codexBin = path.join(binDir, "codex");
await writeFile(
codexBin,
[
"#!/usr/bin/env node",
"import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs';",
"appendFileSync('codex-args.txt', process.argv.slice(2).join('\\n') + '\\n');",
"mkdirSync('src/generated/app-server/v2', { recursive: true });",
"writeFileSync('src/generated/app-server/v2/Example.ts', '// GENERATED CODE! DO NOT MODIFY BY HAND!\\nexport type Example = string | null | null;\\n');",
].join("\n"),
);
await chmod(codexBin, 0o755);
const calls: { command: string; args: string[]; cwd: string }[] = [];
const { generateAppServerTypes } = await import(pathToFileURL(path.join(repoRoot, "scripts", "generate-app-server-types.mjs")).href);
const result = runNodeScript("scripts/generate-app-server-types.mjs", [], cwd, {
PATH: `${binDir}${path.delimiter}${process.env["PATH"] ?? ""}`,
await generateAppServerTypes({
cwd,
async runCommand(command: string, args: string[], options: { cwd: string }) {
calls.push({ command, args, cwd: options.cwd });
await mkdir(path.join(options.cwd, "src", "generated", "app-server", "v2"), { recursive: true });
await writeFile(
path.join(options.cwd, "src", "generated", "app-server", "v2", "Example.ts"),
"// GENERATED CODE! DO NOT MODIFY BY HAND!\nexport type Example = string | null | null;\n",
);
},
});
expect(result.status).toBe(0);
await expect(readFile(path.join(cwd, "codex-args.txt"), "utf8")).resolves.toBe(
"app-server\ngenerate-ts\n--experimental\n--out\nsrc/generated/app-server\n",
);
expect(calls).toEqual([
{
command: "codex",
args: ["app-server", "generate-ts", "--experimental", "--out", "src/generated/app-server"],
cwd,
},
]);
await expect(readFile(path.join(cwd, "src", "generated", "app-server", "v2", "Example.ts"), "utf8")).resolves.toContain(
"export type Example = string | null;",
);
@ -167,20 +166,6 @@ describe("development scripts", () => {
expect(result.stderr).toContain("codex-panel__task-step--");
});
it("does not let dynamic CSS prefixes hide unconfigured class candidates", async () => {
const cwd = await cssUsageFixture({
"src/styles/10-component.css": ".codex-panel__task-step--stale { display: block; }\n",
"src/component.ts": "export const className = `codex-panel__task-step--$" + "{status}`;\n",
});
const result = runNodeScript("scripts/lint/check-css-usage.mjs", [], cwd);
expect(result.status).toBe(1);
expect(result.stdout).toBe("");
expect(result.stderr).toContain("CSS usage check failed.");
expect(result.stderr).toContain("codex-panel__task-step--stale");
});
it("fails release prepare before changing version files when release notes already exist", async () => {
const cwd = await tempWorkspace();
await mkdir(path.join(cwd, ".github", "release-notes"), { recursive: true });

View file

@ -9,8 +9,13 @@ const biomeBin = path.join(repoRoot, "node_modules", ".bin", "biome");
const workspaceByPlugins = new Map();
describe("GritQL source policy", () => {
it("can report hand-written re-export barrels as Biome plugin diagnostics", async () => {
const cwd = await tempBiomeWorkspace(["no-handwritten-reexports.grit"]);
it("keeps project-wide source-shape policies enforceable as Biome plugin diagnostics", async () => {
const cwd = await tempBiomeWorkspace([
"no-handwritten-reexports.grit",
"no-self-referential-initializer-callback.grit",
"no-unsafe-iterator-value.grit",
"no-uncontrolled-preact-form-state.grit",
]);
await writeFile(
path.join(cwd, "reexports.ts"),
`
@ -21,17 +26,111 @@ const local = 1;
export { local };
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/shared/iterator.ts"),
`
export function first<T>(iterator: Iterator<T>): T | undefined {
return iterator.next().value;
}
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/chat/ui/form-state.tsx"),
`
export function Composer(): JSX.Element {
return (
<>
<input defaultValue="draft" />
<input defaultChecked={true} />
</>
);
}
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/chat/ui/controlled-form-state.tsx"),
`
export function Composer(): JSX.Element {
return <input value="draft" />;
}
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/plugin-runtime.ts"),
`
class Runner {
constructor(readonly stop: () => void) {}
}
const report = biomeLint(["reexports.ts"], cwd);
const runner = new Runner(() => runner.stop());
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/plugin-runtime-function.ts"),
`
class Runner {
constructor(readonly stop: () => void) {}
}
const runner = new Runner(function() {
return runner.stop();
});
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/plugin-runtime-declared.ts"),
`
class Runner {
constructor(readonly stop: () => void) {}
}
let runner: Runner;
runner = new Runner(() => runner.stop());
`.trimStart(),
);
const report = biomeLint(
[
"reexports.ts",
"src/shared/iterator.ts",
"src/features/chat/ui/form-state.tsx",
"src/features/chat/ui/controlled-form-state.tsx",
"src/plugin-runtime.ts",
"src/plugin-runtime-function.ts",
"src/plugin-runtime-declared.ts",
],
cwd,
);
expect(pluginDiagnostics(report, "reexports.ts")).toEqual([
{ line: 1, column: 8, endLine: 1, endColumn: 33 },
{ line: 2, column: 8, endLine: 2, endColumn: 26 },
]);
expect(pluginMessages(report, "src/shared/iterator.ts")).toEqual([
"Avoid reading iterator.next().value directly; use for...of or inspect the typed IteratorResult first.",
]);
expect(pluginMessages(report, "src/features/chat/ui/form-state.tsx")).toEqual([
"Keep Preact form state explicit with controlled value or checked props.",
"Keep Preact form state explicit with controlled value or checked props.",
]);
expect(pluginDiagnostics(report, "src/features/chat/ui/controlled-form-state.tsx")).toEqual([]);
expect(pluginMessages(report, "src/plugin-runtime.ts")).toEqual([
"Avoid referencing a variable from a callback inside its own initializer; declare it first with an explicit type.",
]);
expect(pluginMessages(report, "src/plugin-runtime-function.ts")).toEqual([
"Avoid referencing a variable from a callback inside its own initializer; declare it first with an explicit type.",
]);
expect(pluginDiagnostics(report, "src/plugin-runtime-declared.ts")).toEqual([]);
});
it("keeps Preact signals behind the chat shell-state adapter", async () => {
const cwd = await tempBiomeWorkspace(["no-chat-signal-imports.grit"]);
it("keeps chat architecture policies behind their intended boundaries", async () => {
const cwd = await tempBiomeWorkspace([
"no-chat-domain-outer-layer-imports.grit",
"no-chat-signal-imports.grit",
"no-implicit-dom-bridges.grit",
"no-pure-chat-state-side-effects.grit",
"no-ui-root-imports.grit",
]);
await writeFile(
path.join(cwd, "src/features/chat/panel/shell-state.tsx"),
`
@ -62,26 +161,6 @@ const signals = require("@preact/signals");
export const loadedSignals = signals;
`.trimStart(),
);
const report = biomeLint(
["src/features/chat/panel/shell-state.tsx", "src/shared/ui/components.tsx", "src/shared/ui/signal-escapes.tsx"],
cwd,
);
expect(pluginDiagnostics(report, "src/features/chat/panel/shell-state.tsx")).toEqual([]);
expect(pluginMessages(report, "src/shared/ui/components.tsx")).toEqual([
"Use @preact/signals only in src/features/chat/panel/shell-state.tsx as the reducer-to-Preact derived projection adapter.",
]);
expect(pluginMessages(report, "src/shared/ui/signal-escapes.tsx")).toEqual([
"Use @preact/signals only in src/features/chat/panel/shell-state.tsx as the reducer-to-Preact derived projection adapter.",
"Use @preact/signals only in src/features/chat/panel/shell-state.tsx as the reducer-to-Preact derived projection adapter.",
"Use @preact/signals only in src/features/chat/panel/shell-state.tsx as the reducer-to-Preact derived projection adapter.",
"Use @preact/signals only in src/features/chat/panel/shell-state.tsx as the reducer-to-Preact derived projection adapter.",
]);
});
it("keeps chat domain independent from outer chat layers", async () => {
const cwd = await tempBiomeWorkspace(["no-chat-domain-outer-layer-imports.grit"]);
await writeFile(
path.join(cwd, "src/features/chat/domain/message-stream/selectors.ts"),
`
@ -112,18 +191,113 @@ export async function loadComposer() {
const host = require("../../host/session");
export const outerHost = host;
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/chat/ui/dom-bridge-escape.tsx"),
`
export function render(container: HTMLElement): void {
container.createDiv();
container.addEventListener("click", () => undefined);
}
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/chat/ui/dom-bridge.dom.tsx"),
`
export function render(container: HTMLElement): void {
container.createDiv();
container.addEventListener("click", () => undefined);
}
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/app-server/services/abortable-operation.ts"),
`
export function onAbort(signal: AbortSignal): void {
signal.addEventListener("abort", () => undefined);
}
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/chat/ui/root-import.tsx"),
`
import { renderUiRoot } from '../../../shared/ui/ui-root.dom';
export const render = renderUiRoot;
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/chat/panel/shell.dom.tsx"),
`
import { renderUiRoot } from "../../../shared/ui/ui-root.dom";
export const render = renderUiRoot;
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/chat/ui/root-escapes.tsx"),
`
export { renderUiRoot } from "../../../shared/ui/ui-root.dom";
export type RootRenderer = import("../../../shared/ui/ui-root.dom").RootRenderer;
export async function loadRoot() {
return import("../../../shared/ui/ui-root.dom");
}
const root = require("../../../shared/ui/ui-root.dom");
export const loadedRoot = root;
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/chat/application/state/message-stream.ts"),
`
export function timestamp(): number {
setTimeout(() => undefined, 1);
return Date.now();
}
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/chat/application/threads/resume-actions.ts"),
`
export function timestamp(): number {
setTimeout(() => undefined, 1);
return Date.now();
}
`.trimStart(),
);
const report = biomeLint(
[
"src/features/chat/panel/shell-state.tsx",
"src/shared/ui/components.tsx",
"src/shared/ui/signal-escapes.tsx",
"src/features/chat/domain/message-stream/selectors.ts",
"src/features/chat/domain/message-stream/items.ts",
"src/features/chat/domain/message-stream/outer-shapes.ts",
"src/features/chat/ui/dom-bridge-escape.tsx",
"src/features/chat/ui/dom-bridge.dom.tsx",
"src/app-server/services/abortable-operation.ts",
"src/features/chat/ui/root-import.tsx",
"src/features/chat/panel/shell.dom.tsx",
"src/features/chat/ui/root-escapes.tsx",
"src/features/chat/application/state/message-stream.ts",
"src/features/chat/application/threads/resume-actions.ts",
],
cwd,
);
expect(pluginDiagnostics(report, "src/features/chat/panel/shell-state.tsx")).toEqual([]);
expect(pluginMessages(report, "src/shared/ui/components.tsx")).toEqual([
"Use @preact/signals only in src/features/chat/panel/shell-state.tsx as the reducer-to-Preact derived projection adapter.",
]);
expect(pluginMessages(report, "src/shared/ui/signal-escapes.tsx")).toEqual([
"Use @preact/signals only in src/features/chat/panel/shell-state.tsx as the reducer-to-Preact derived projection adapter.",
"Use @preact/signals only in src/features/chat/panel/shell-state.tsx as the reducer-to-Preact derived projection adapter.",
"Use @preact/signals only in src/features/chat/panel/shell-state.tsx as the reducer-to-Preact derived projection adapter.",
"Use @preact/signals only in src/features/chat/panel/shell-state.tsx as the reducer-to-Preact derived projection adapter.",
]);
expect(pluginMessages(report, "src/features/chat/domain/message-stream/selectors.ts")).toEqual([
"Keep chat/domain as Panel-owned meaning models and pure derivations; app-server, application, host, panel, presentation, and UI layers may depend on domain, not the reverse.",
"Keep chat/domain as Panel-owned meaning models and pure derivations; app-server, application, host, panel, presentation, and UI layers may depend on domain, not the reverse.",
@ -135,9 +309,30 @@ export const outerHost = host;
"Keep chat/domain as Panel-owned meaning models and pure derivations; app-server, application, host, panel, presentation, and UI layers may depend on domain, not the reverse.",
"Keep chat/domain as Panel-owned meaning models and pure derivations; app-server, application, host, panel, presentation, and UI layers may depend on domain, not the reverse.",
]);
expect(pluginMessages(report, "src/features/chat/ui/dom-bridge-escape.tsx")).toEqual([
"Keep imperative DOM writes and event wiring in files named with a .dom, .obsidian, or .measure suffix.",
"Keep imperative DOM writes and event wiring in files named with a .dom, .obsidian, or .measure suffix.",
]);
expect(pluginDiagnostics(report, "src/features/chat/ui/dom-bridge.dom.tsx")).toEqual([]);
expect(pluginDiagnostics(report, "src/app-server/services/abortable-operation.ts")).toEqual([]);
expect(pluginMessages(report, "src/features/chat/ui/root-import.tsx")).toEqual([
"Import the Preact root adapter only from explicit root bridge files.",
]);
expect(pluginDiagnostics(report, "src/features/chat/panel/shell.dom.tsx")).toEqual([]);
expect(pluginMessages(report, "src/features/chat/ui/root-escapes.tsx")).toEqual([
"Import the Preact root adapter only from explicit root bridge files.",
"Import the Preact root adapter only from explicit root bridge files.",
"Import the Preact root adapter only from explicit root bridge files.",
"Import the Preact root adapter only from explicit root bridge files.",
]);
expect(pluginMessages(report, "src/features/chat/application/state/message-stream.ts")).toEqual([
"Keep chat state transforms deterministic and free of app-server, Obsidian, scheduling, and browser side effects.",
"Keep chat state transforms deterministic and free of app-server, Obsidian, scheduling, and browser side effects.",
]);
expect(pluginDiagnostics(report, "src/features/chat/application/threads/resume-actions.ts")).toEqual([]);
});
it("blocks generated app-server imports outside app-server boundaries", async () => {
it("keeps generated app-server imports behind explicit app-server boundaries", async () => {
const cwd = await tempBiomeWorkspace(["no-generated-app-server-boundary-imports.grit"]);
await writeFile(
path.join(cwd, "src/features/chat/domain/generated-thread.ts"),
@ -163,6 +358,40 @@ export type GeneratedThread = import("../../generated/app-server/v2/Thread").Thr
path.join(cwd, "tests/app-server/generated-thread.test.ts"),
`
export type GeneratedThread = import("../../src/generated/app-server/v2/Thread").Thread;
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/app-server/protocol/request-input.ts"),
`
import type { UserInput } from "../../generated/app-server/v2/UserInput";
export type Input = UserInput;
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/app-server/services/runtime-overrides.ts"),
`
import type { ModelListResponse } from "../generated/app-server/v2/ModelListResponse";
export interface RuntimeOverrideModelClient {
listModels(includeHidden: boolean): Promise<ModelListResponse>;
}
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/app-server/protocol/turn.ts"),
`
import type { ThreadItem as GeneratedTurnItem } from "../../generated/app-server/v2/ThreadItem";
export type TurnItem = GeneratedTurnItem;
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/app-server/protocol/server-requests.ts"),
`
import type { ServerRequest } from "../../generated/app-server/ServerRequest";
export type Request = ServerRequest;
`.trimStart(),
);
@ -172,6 +401,10 @@ export type GeneratedThread = import("../../src/generated/app-server/v2/Thread")
"src/features/chat/domain/generated-thread-import.ts",
"src/app-server/connection/generated-thread.ts",
"tests/app-server/generated-thread.test.ts",
"src/app-server/protocol/request-input.ts",
"src/app-server/services/runtime-overrides.ts",
"src/app-server/protocol/turn.ts",
"src/app-server/protocol/server-requests.ts",
],
cwd,
);
@ -184,6 +417,16 @@ export type GeneratedThread = import("../../src/generated/app-server/v2/Thread")
]);
expect(pluginDiagnostics(report, "src/app-server/connection/generated-thread.ts")).toEqual([]);
expect(pluginDiagnostics(report, "tests/app-server/generated-thread.test.ts")).toEqual([]);
expect(pluginMessages(report, "src/app-server/protocol/request-input.ts")).toEqual([
"Keep generated app-server types behind src/app-server and tests/app-server; expose Panel-owned models outside raw app-server adapters.",
]);
expect(pluginMessages(report, "src/app-server/services/runtime-overrides.ts")).toEqual([
"Keep generated app-server types behind src/app-server and tests/app-server; expose Panel-owned models outside raw app-server adapters.",
]);
expect(pluginMessages(report, "src/app-server/protocol/turn.ts")).toEqual([
"Keep generated app-server types behind src/app-server and tests/app-server; expose Panel-owned models outside raw app-server adapters.",
]);
expect(pluginDiagnostics(report, "src/app-server/protocol/server-requests.ts")).toEqual([]);
});
it("keeps app-server protocol modules behind app-server and chat ingestion boundaries", async () => {
@ -222,32 +465,6 @@ import type { TurnItem } from "../../../../../app-server/protocol/turn";
export type Item = TurnItem;
`.trimStart(),
);
const report = biomeLint(
[
"src/features/chat/application/pending-requests/pending-request-actions.ts",
"src/features/chat/application/threads/history-controller.ts",
"src/features/chat/app-server/inbound/notification-plan.ts",
"src/features/chat/app-server/mappers/message-stream/turn-items.ts",
],
cwd,
);
expect(pluginMessages(report, "src/features/chat/application/pending-requests/pending-request-actions.ts")).toEqual([
"Source modules outside app-server must use domain models and app-server services instead of app-server protocol modules. Chat ingestion and message-stream conversion may consume app-server turn protocol at the boundary; feature state and UI must use Panel-owned models.",
]);
expect(pluginMessages(report, "src/features/chat/application/threads/history-controller.ts")).toEqual([
"Source modules outside app-server must use domain models and app-server services instead of app-server protocol modules. Chat ingestion and message-stream conversion may consume app-server turn protocol at the boundary; feature state and UI must use Panel-owned models.",
]);
expect(pluginMessages(report, "src/features/chat/app-server/inbound/notification-plan.ts")).toEqual([
"Chat app-server ingestion and message-stream conversion may consume the app-server turn protocol only. Convert other protocol payloads to local or domain models at the boundary.",
"Chat app-server ingestion and message-stream conversion may consume the app-server turn protocol only. Convert other protocol payloads to local or domain models at the boundary.",
]);
expect(pluginDiagnostics(report, "src/features/chat/app-server/mappers/message-stream/turn-items.ts")).toEqual([]);
});
it("keeps server request protocol imports in chat request boundaries only", async () => {
const cwd = await tempBiomeWorkspace(["no-app-server-protocol-boundary-imports.grit"]);
await writeFile(
path.join(cwd, "src/features/chat/panel/surface/message-stream-presenter.ts"),
`
@ -283,6 +500,10 @@ export const response = appServerUserInputResponse;
const report = biomeLint(
[
"src/features/chat/application/pending-requests/pending-request-actions.ts",
"src/features/chat/application/threads/history-controller.ts",
"src/features/chat/app-server/inbound/notification-plan.ts",
"src/features/chat/app-server/mappers/message-stream/turn-items.ts",
"src/features/chat/panel/surface/message-stream-presenter.ts",
"src/features/chat/app-server/inbound/app-server-logs.ts",
"src/features/chat/app-server/inbound/handler.ts",
@ -291,6 +512,17 @@ export const response = appServerUserInputResponse;
cwd,
);
expect(pluginMessages(report, "src/features/chat/application/pending-requests/pending-request-actions.ts")).toEqual([
"Source modules outside app-server must use domain models and app-server services instead of app-server protocol modules. Chat ingestion and message-stream conversion may consume app-server turn protocol at the boundary; feature state and UI must use Panel-owned models.",
]);
expect(pluginMessages(report, "src/features/chat/application/threads/history-controller.ts")).toEqual([
"Source modules outside app-server must use domain models and app-server services instead of app-server protocol modules. Chat ingestion and message-stream conversion may consume app-server turn protocol at the boundary; feature state and UI must use Panel-owned models.",
]);
expect(pluginMessages(report, "src/features/chat/app-server/inbound/notification-plan.ts")).toEqual([
"Chat app-server ingestion and message-stream conversion may consume the app-server turn protocol only. Convert other protocol payloads to local or domain models at the boundary.",
"Chat app-server ingestion and message-stream conversion may consume the app-server turn protocol only. Convert other protocol payloads to local or domain models at the boundary.",
]);
expect(pluginDiagnostics(report, "src/features/chat/app-server/mappers/message-stream/turn-items.ts")).toEqual([]);
expect(pluginMessages(report, "src/features/chat/panel/surface/message-stream-presenter.ts")).toEqual([
"Source modules outside app-server must use domain models and app-server services instead of app-server protocol modules. Chat ingestion and message-stream conversion may consume app-server turn protocol at the boundary; feature state and UI must use Panel-owned models.",
]);
@ -303,65 +535,6 @@ export const response = appServerUserInputResponse;
expect(pluginDiagnostics(report, "src/features/chat/app-server/inbound/routing.ts")).toEqual([]);
});
it("keeps generated app-server bindings behind explicit exceptions", async () => {
const cwd = await tempBiomeWorkspace(["no-generated-app-server-boundary-imports.grit"]);
await writeFile(
path.join(cwd, "src/app-server/protocol/request-input.ts"),
`
import type { UserInput } from "../../generated/app-server/v2/UserInput";
export type Input = UserInput;
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/app-server/services/runtime-overrides.ts"),
`
import type { ModelListResponse } from "../generated/app-server/v2/ModelListResponse";
export interface RuntimeOverrideModelClient {
listModels(includeHidden: boolean): Promise<ModelListResponse>;
}
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/app-server/protocol/turn.ts"),
`
import type { ThreadItem as GeneratedTurnItem } from "../../generated/app-server/v2/ThreadItem";
export type TurnItem = GeneratedTurnItem;
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/app-server/protocol/server-requests.ts"),
`
import type { ServerRequest } from "../../generated/app-server/ServerRequest";
export type Request = ServerRequest;
`.trimStart(),
);
const report = biomeLint(
[
"src/app-server/protocol/request-input.ts",
"src/app-server/services/runtime-overrides.ts",
"src/app-server/protocol/turn.ts",
"src/app-server/protocol/server-requests.ts",
],
cwd,
);
expect(pluginMessages(report, "src/app-server/protocol/request-input.ts")).toEqual([
"Keep generated app-server types behind src/app-server and tests/app-server; expose Panel-owned models outside raw app-server adapters.",
]);
expect(pluginMessages(report, "src/app-server/services/runtime-overrides.ts")).toEqual([
"Keep generated app-server types behind src/app-server and tests/app-server; expose Panel-owned models outside raw app-server adapters.",
]);
expect(pluginMessages(report, "src/app-server/protocol/turn.ts")).toEqual([
"Keep generated app-server types behind src/app-server and tests/app-server; expose Panel-owned models outside raw app-server adapters.",
]);
expect(pluginDiagnostics(report, "src/app-server/protocol/server-requests.ts")).toEqual([]);
});
it("keeps lower-level source independent from connection and feature layers", async () => {
const cwd = await tempBiomeWorkspace(["no-lower-level-boundary-imports.grit"]);
await writeFile(
@ -568,229 +741,6 @@ export type AppServerThreadResumeClient = Pick<AppServerClient, "resumeThread">;
expect(pluginDiagnostics(report, "src/features/chat/host/connection-bundle.ts")).toEqual([]);
});
it("keeps imperative DOM bridges behind filename suffixes", async () => {
const cwd = await tempBiomeWorkspace(["no-implicit-dom-bridges.grit"]);
await writeFile(
path.join(cwd, "src/features/chat/ui/composer.tsx"),
`
export function render(container: HTMLElement): void {
container.createDiv();
container.addEventListener("click", () => undefined);
}
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/chat/ui/composer.dom.tsx"),
`
export function render(container: HTMLElement): void {
container.createDiv();
container.addEventListener("click", () => undefined);
}
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/app-server/services/abortable-operation.ts"),
`
export function onAbort(signal: AbortSignal): void {
signal.addEventListener("abort", () => undefined);
}
`.trimStart(),
);
const report = biomeLint(
["src/features/chat/ui/composer.tsx", "src/features/chat/ui/composer.dom.tsx", "src/app-server/services/abortable-operation.ts"],
cwd,
);
expect(pluginMessages(report, "src/features/chat/ui/composer.tsx")).toEqual([
"Keep imperative DOM writes and event wiring in files named with a .dom, .obsidian, or .measure suffix.",
"Keep imperative DOM writes and event wiring in files named with a .dom, .obsidian, or .measure suffix.",
]);
expect(pluginDiagnostics(report, "src/features/chat/ui/composer.dom.tsx")).toEqual([]);
expect(pluginDiagnostics(report, "src/app-server/services/abortable-operation.ts")).toEqual([]);
});
it("keeps the Preact root adapter in explicit root bridge files", async () => {
const cwd = await tempBiomeWorkspace(["no-ui-root-imports.grit"]);
await writeFile(
path.join(cwd, "src/features/chat/ui/composer.tsx"),
`
import { renderUiRoot } from '../../../shared/ui/ui-root.dom';
export const render = renderUiRoot;
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/chat/panel/shell.dom.tsx"),
`
import { renderUiRoot } from "../../../shared/ui/ui-root.dom";
export const render = renderUiRoot;
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/chat/ui/root-escapes.tsx"),
`
export { renderUiRoot } from "../../../shared/ui/ui-root.dom";
export type RootRenderer = import("../../../shared/ui/ui-root.dom").RootRenderer;
export async function loadRoot() {
return import("../../../shared/ui/ui-root.dom");
}
const root = require("../../../shared/ui/ui-root.dom");
export const loadedRoot = root;
`.trimStart(),
);
const report = biomeLint(
["src/features/chat/ui/composer.tsx", "src/features/chat/panel/shell.dom.tsx", "src/features/chat/ui/root-escapes.tsx"],
cwd,
);
expect(pluginMessages(report, "src/features/chat/ui/composer.tsx")).toEqual([
"Import the Preact root adapter only from explicit root bridge files.",
]);
expect(pluginDiagnostics(report, "src/features/chat/panel/shell.dom.tsx")).toEqual([]);
expect(pluginMessages(report, "src/features/chat/ui/root-escapes.tsx")).toEqual([
"Import the Preact root adapter only from explicit root bridge files.",
"Import the Preact root adapter only from explicit root bridge files.",
"Import the Preact root adapter only from explicit root bridge files.",
"Import the Preact root adapter only from explicit root bridge files.",
]);
});
it("keeps chat state transforms pure and catches global scheduling calls", async () => {
const cwd = await tempBiomeWorkspace(["no-pure-chat-state-side-effects.grit"]);
await writeFile(
path.join(cwd, "src/features/chat/application/state/message-stream.ts"),
`
export function timestamp(): number {
setTimeout(() => undefined, 1);
return Date.now();
}
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/chat/application/threads/resume-actions.ts"),
`
export function timestamp(): number {
setTimeout(() => undefined, 1);
return Date.now();
}
`.trimStart(),
);
const report = biomeLint(
["src/features/chat/application/state/message-stream.ts", "src/features/chat/application/threads/resume-actions.ts"],
cwd,
);
expect(pluginMessages(report, "src/features/chat/application/state/message-stream.ts")).toEqual([
"Keep chat state transforms deterministic and free of app-server, Obsidian, scheduling, and browser side effects.",
"Keep chat state transforms deterministic and free of app-server, Obsidian, scheduling, and browser side effects.",
]);
expect(pluginDiagnostics(report, "src/features/chat/application/threads/resume-actions.ts")).toEqual([]);
});
it("reports unsafe iterator value reads", async () => {
const cwd = await tempBiomeWorkspace(["no-unsafe-iterator-value.grit"]);
await writeFile(
path.join(cwd, "src/shared/iterator.ts"),
`
export function first<T>(iterator: Iterator<T>): T | undefined {
return iterator.next().value;
}
`.trimStart(),
);
const report = biomeLint(["src/shared/iterator.ts"], cwd);
expect(pluginMessages(report, "src/shared/iterator.ts")).toEqual([
"Avoid reading iterator.next().value directly; use for...of or inspect the typed IteratorResult first.",
]);
});
it("reports uncontrolled Preact form state attributes", async () => {
const cwd = await tempBiomeWorkspace(["no-uncontrolled-preact-form-state.grit"]);
await writeFile(
path.join(cwd, "src/features/chat/ui/composer.tsx"),
`
export function Composer(): JSX.Element {
return (
<>
<input defaultValue="draft" />
<input defaultChecked={true} />
</>
);
}
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/features/chat/ui/controlled-composer.tsx"),
`
export function Composer(): JSX.Element {
return <input value="draft" />;
}
`.trimStart(),
);
const report = biomeLint(["src/features/chat/ui/composer.tsx", "src/features/chat/ui/controlled-composer.tsx"], cwd);
expect(pluginMessages(report, "src/features/chat/ui/composer.tsx")).toEqual([
"Keep Preact form state explicit with controlled value or checked props.",
"Keep Preact form state explicit with controlled value or checked props.",
]);
expect(pluginDiagnostics(report, "src/features/chat/ui/controlled-composer.tsx")).toEqual([]);
});
it("keeps initializer callbacks from capturing their own variable", async () => {
const cwd = await tempBiomeWorkspace(["no-self-referential-initializer-callback.grit"]);
await writeFile(
path.join(cwd, "src/plugin-runtime.ts"),
`
class Runner {
constructor(readonly stop: () => void) {}
}
const runner = new Runner(() => runner.stop());
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/plugin-runtime-function.ts"),
`
class Runner {
constructor(readonly stop: () => void) {}
}
const runner = new Runner(function() {
return runner.stop();
});
`.trimStart(),
);
await writeFile(
path.join(cwd, "src/plugin-runtime-declared.ts"),
`
class Runner {
constructor(readonly stop: () => void) {}
}
let runner: Runner;
runner = new Runner(() => runner.stop());
`.trimStart(),
);
const report = biomeLint(["src/plugin-runtime.ts", "src/plugin-runtime-function.ts", "src/plugin-runtime-declared.ts"], cwd);
expect(pluginMessages(report, "src/plugin-runtime.ts")).toEqual([
"Avoid referencing a variable from a callback inside its own initializer; declare it first with an explicit type.",
]);
expect(pluginMessages(report, "src/plugin-runtime-function.ts")).toEqual([
"Avoid referencing a variable from a callback inside its own initializer; declare it first with an explicit type.",
]);
expect(pluginDiagnostics(report, "src/plugin-runtime-declared.ts")).toEqual([]);
});
it("keeps CSS on design tokens and scoped selectors", async () => {
const cwd = await tempBiomeWorkspace(["no-restricted-css-policy.grit"]);
await writeFile(

View file

@ -8,7 +8,6 @@ import type { ThreadRecord } from "../../src/app-server/protocol/thread";
import type { ModelMetadata, ReasoningEffort } from "../../src/domain/catalog/metadata";
import type { ObservedDataResult } from "../../src/domain/observed-data";
import type { Thread } from "../../src/domain/threads/model";
import { threadArchiveDisplayTitle } from "../../src/domain/threads/title";
import { SettingsDynamicDataController, type SettingsDynamicDataSnapshot } from "../../src/settings/dynamic-data-controller";
import type { CodexPanelSettingTabHost } from "../../src/settings/host";
import { CodexPanelSettingTab } from "../../src/settings/tab.obsidian";
@ -32,22 +31,6 @@ describe("settings tab", () => {
notices.length = 0;
});
it("uses a placeholder for threads without a useful title", () => {
expect(threadArchiveDisplayTitle(panelThread({ name: null, preview: "" }))).toBe("Untitled thread");
expect(threadArchiveDisplayTitle(panelThread({ name: "019e0182-cb70-7a72-ab48-8bc9d0b0d781", preview: "" }))).toBe("Untitled thread");
expect(threadArchiveDisplayTitle(panelThread({ name: "019e0182-cb70-7a72-ab48-8bc9d0b0d781", preview: "Preview title" }))).toBe(
"Preview title",
);
});
it("normalizes and truncates archived thread titles", () => {
expect(threadArchiveDisplayTitle(panelThread({ preview: "A title\nwith extra\tspace" }))).toBe("A title with extra space");
const title = threadArchiveDisplayTitle(panelThread({ preview: "x".repeat(120) }));
expect(title).toHaveLength(96);
expect(title.endsWith("...")).toBe(true);
});
it("auto-loads settings data once and keeps one global refresh button", async () => {
const client = settingsClient();
const fetchModels = vi.fn().mockResolvedValue(modelMetadataFromCatalogModels([model("gpt-5.5")]));
@ -73,34 +56,6 @@ describe("settings tab", () => {
expect(buttonTexts(tab)).not.toContain("Load models");
expect(buttonTexts(tab)).not.toContain("Load hooks");
expect(buttonTexts(tab)).not.toContain("Load archive list");
expect(
tab.containerEl.querySelector(".codex-panel-settings__header.setting-item-heading .setting-item-description")?.textContent,
).toContain("Codex Panel stores panel preferences only");
expect(tab.containerEl.querySelector(".codex-panel-settings__header button")?.getAttribute("data-icon")).toBe("refresh-cw");
expect(tab.containerEl.querySelector("h2")).toBeNull();
expect(tab.containerEl.querySelector(".codex-panel-settings__general-section.setting-group > .setting-items")?.children).toHaveLength(
2,
);
expect(tab.containerEl.querySelector(".codex-panel-settings__composer-section.setting-group > .setting-items")?.children).toHaveLength(
2,
);
expect(tab.containerEl.querySelector(".codex-panel-settings__helper-section.setting-group > .setting-items")?.children).toHaveLength(2);
expect(
tab.containerEl.querySelector(".codex-panel-settings__archived-section > .setting-item-heading .setting-item-description")
?.textContent,
).toContain("Set the default archive action");
expect(
tab.containerEl.querySelector(".codex-panel-settings__archived-threads-section > .setting-item-heading .setting-item-description")
?.textContent,
).toContain("Restore or permanently delete");
expect(
tab.containerEl.querySelector(".codex-panel-settings__hook-section > .setting-item-heading .setting-item-description")?.textContent,
).toContain("Trust, enable, or disable");
expect(
tab.containerEl.querySelector(
".codex-panel-settings__archived-section.setting-group > .setting-items:not(.codex-panel-settings__dynamic-list)",
)?.children,
).toHaveLength(4);
expect(settingNames(tab)).toEqual([
"Codex executable",
"Show chat toolbar",
@ -767,7 +722,6 @@ describe("settings tab", () => {
expect(tab.containerEl.querySelector(".codex-panel-settings__archived-row--delete-confirming")).not.toBeNull();
expect(tab.containerEl.textContent).toContain("Permanently delete this archived thread? This cannot be undone.");
expect(tab.containerEl.querySelector("[aria-label='Delete thread']")?.getAttribute("data-icon")).toBe("check");
tab.containerEl.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true }));