diff --git a/biome.jsonc b/biome.jsonc index 2cbf643d..4740abe1 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -195,6 +195,7 @@ "scripts/**/*.mjs", "src/**/*.{css,ts,tsx}", "tests/**/*.{mjs,ts,tsx}", + "!tests/fixtures/grit-policy", "!src/generated" ] }, diff --git a/docs/development.md b/docs/development.md index 42af54fb..0c09d909 100644 --- a/docs/development.md +++ b/docs/development.md @@ -31,6 +31,8 @@ Use focused scripts while iterating, but run `npm run check` before handoff. CI Keep rule suppressions local and include the Obsidian-specific reason when a native Obsidian UI pattern intentionally diverges from a generic browser rule. +Grit policy fixtures protect matcher semantics rather than diagnostic wording, source locations, or the exact shape of Biome configuration. Change the checked-in valid and invalid fixtures only when a policy's accepted or rejected source shape intentionally changes. + ## Generated and Loaded Files `main.js`, `styles.css`, `data.json`, and `node_modules/` are ignored by Git. `main.js` and `styles.css` are still the files Obsidian loads, so run `npm run build` before live Obsidian validation if you have not already run `npm run check` after the source change. diff --git a/knip.json b/knip.json index 804bc894..016d3c6e 100644 --- a/knip.json +++ b/knip.json @@ -1,6 +1,6 @@ { "entry": ["src/main.ts", "scripts/**/*.mjs", "*.config.{ts,mjs}", "tests/**/*.test.{ts,tsx}"], "project": ["src/**/*.{ts,tsx}", "tests/**/*.{ts,tsx}", "scripts/**/*.mjs", "*.config.{ts,mjs}"], - "ignore": ["src/generated/**"], + "ignore": ["src/generated/**", "tests/fixtures/grit-policy/**"], "ignoreBinaries": ["codex", "taskkill"] } diff --git a/tests/features/chat/panel/shell.test.tsx b/tests/features/chat/panel/shell.test.tsx index d60014d9..b1eafdb2 100644 --- a/tests/features/chat/panel/shell.test.tsx +++ b/tests/features/chat/panel/shell.test.tsx @@ -133,155 +133,6 @@ describe("ChatPanelShell", () => { }); }); - it("keeps composer rendering off thread stream updates until turn presence changes", async () => { - const store = createChatStateStore(); - const container = document.createElement("div"); - document.body.appendChild(container); - const parts = shellParts(); - const renderComposerState = vi.fn(parts.composer.presenter.renderState.bind(parts.composer.presenter)); - parts.composer.presenter.renderState = renderComposerState; - - await act(async () => { - renderChatPanelShell(container, { ...shellProps(store), parts }); - await settleShellEffects(); - }); - renderComposerState.mockClear(); - - await act(async () => { - store.dispatch({ - type: "thread-stream/system-item-added", - item: { id: "system-1", kind: "system", role: "system", text: "Status only." }, - }); - await settleShellEffects(); - }); - expect(renderComposerState).not.toHaveBeenCalled(); - - await act(async () => { - store.dispatch({ - type: "thread-stream/item-added", - item: { - id: "assistant-1", - turnId: "turn-1", - kind: "dialogue", - dialogueKind: "assistantResponse", - dialogueState: "completed", - role: "assistant", - text: "Turn response.", - }, - }); - await settleShellEffects(); - }); - expect(renderComposerState).toHaveBeenCalledTimes(1); - - await act(async () => { - unmountChatPanelShell(container); - }); - }); - - it("keeps toolbar rendering off turn updates until busy state changes", async () => { - const store = createChatStateStore(); - const container = document.createElement("div"); - document.body.appendChild(container); - const toolbarConnected = vi.fn(() => false); - - await act(async () => { - renderChatPanelShell(container, { ...shellProps(store), parts: shellParts({ toolbarConnected }) }); - await settleShellEffects(); - }); - toolbarConnected.mockClear(); - - await act(async () => { - store.dispatch({ - type: "turn/optimistic-started", - item: { id: "local-user", kind: "dialogue", dialogueKind: "user", role: "user", text: "hello" }, - pendingTurnStart: { anchorItemId: "local-user", promptSubmitHookItemIds: [] }, - }); - await settleShellEffects(); - }); - expect(toolbarConnected).toHaveBeenCalledTimes(1); - toolbarConnected.mockClear(); - - await act(async () => { - store.dispatch({ - type: "turn/start-acknowledged", - turnId: "turn-1", - items: [{ id: "local-user", turnId: "turn-1", kind: "dialogue", dialogueKind: "user", role: "user", text: "hello" }], - }); - await settleShellEffects(); - }); - expect(toolbarConnected).not.toHaveBeenCalled(); - - await act(async () => { - unmountChatPanelShell(container); - }); - }); - - it("keeps goal rendering off active thread updates until goal state changes", async () => { - const store = createChatStateStore(); - const container = document.createElement("div"); - document.body.appendChild(container); - const goalSendShortcut = vi.fn(() => "enter" as const); - - await act(async () => { - renderChatPanelShell(container, { ...shellProps(store), parts: shellParts({ goalSendShortcut }) }); - await settleShellEffects(); - }); - goalSendShortcut.mockClear(); - - await act(async () => { - store.dispatch({ - type: "active-thread/token-usage-set", - tokenUsage: tokenUsageFixture(), - }); - await settleShellEffects(); - }); - expect(goalSendShortcut).not.toHaveBeenCalled(); - - await act(async () => { - unmountChatPanelShell(container); - }); - }); - - it("keeps thread stream rendering off active thread updates until stream thread fields change", async () => { - const store = createChatStateStore(); - const container = document.createElement("div"); - document.body.appendChild(container); - const parts = shellParts(); - const renderThreadStreamState = vi.fn(parts.threadStream.renderState.bind(parts.threadStream)); - parts.threadStream.renderState = renderThreadStreamState; - - await act(async () => { - renderChatPanelShell(container, { ...shellProps(store), parts }); - await settleShellEffects(); - }); - renderThreadStreamState.mockClear(); - - await act(async () => { - store.dispatch({ - type: "active-thread/token-usage-set", - tokenUsage: tokenUsageFixture(), - }); - await settleShellEffects(); - }); - expect(renderThreadStreamState).not.toHaveBeenCalled(); - - await act(async () => { - store.dispatch({ type: "ui/disclosure-set", bucket: "goalObjectiveExpanded", id: "thread", open: true }); - await settleShellEffects(); - }); - expect(renderThreadStreamState).not.toHaveBeenCalled(); - - await act(async () => { - store.dispatch({ type: "active-thread/cwd-set", cwd: "/workspace" }); - await settleShellEffects(); - }); - expect(renderThreadStreamState).toHaveBeenCalledTimes(1); - - await act(async () => { - unmountChatPanelShell(container); - }); - }); - it("removes and restores the toolbar without losing composer or thread viewport state", async () => { const store = createChatStateStore(); store.dispatch({ type: "composer/draft-set", draft: "toolbar continuity" }); @@ -653,14 +504,6 @@ function toolbarActionsFixture(): ChatPanelShellParts["toolbar"]["actions"] { }; } -function tokenUsageFixture() { - return { - total: { totalTokens: 10, inputTokens: 7, cachedInputTokens: 0, outputTokens: 3, reasoningOutputTokens: 0 }, - last: { totalTokens: 10, inputTokens: 7, cachedInputTokens: 0, outputTokens: 3, reasoningOutputTokens: 0 }, - modelContextWindow: 100, - }; -} - async function settleShellEffects(): Promise { await Promise.resolve(); await Promise.resolve(); diff --git a/tests/fixtures/grit-policy/invalid/src/app-server/protocol/escape.ts b/tests/fixtures/grit-policy/invalid/src/app-server/protocol/escape.ts new file mode 100644 index 00000000..38ca5c88 --- /dev/null +++ b/tests/fixtures/grit-policy/invalid/src/app-server/protocol/escape.ts @@ -0,0 +1,4 @@ +import type { Client } from "../connection/client"; +import type { Root } from "../root"; + +export type Escape = Client | Root; diff --git a/tests/fixtures/grit-policy/invalid/src/domain/root.ts b/tests/fixtures/grit-policy/invalid/src/domain/root.ts new file mode 100644 index 00000000..b6ccfea3 --- /dev/null +++ b/tests/fixtures/grit-policy/invalid/src/domain/root.ts @@ -0,0 +1,4 @@ +import "obsidian"; + +export { value } from "./owner"; +export * from "./barrel"; diff --git a/tests/fixtures/grit-policy/invalid/src/features/chat/app-server/escape.ts b/tests/fixtures/grit-policy/invalid/src/features/chat/app-server/escape.ts new file mode 100644 index 00000000..f4e53493 --- /dev/null +++ b/tests/fixtures/grit-policy/invalid/src/features/chat/app-server/escape.ts @@ -0,0 +1,3 @@ +import type { Panel } from "../panel/toolbar-actions"; + +export type Escape = Panel; diff --git a/tests/fixtures/grit-policy/invalid/src/features/chat/app-server/mappers/thread-stream/turn-items.ts b/tests/fixtures/grit-policy/invalid/src/features/chat/app-server/mappers/thread-stream/turn-items.ts new file mode 100644 index 00000000..bfff9090 --- /dev/null +++ b/tests/fixtures/grit-policy/invalid/src/features/chat/app-server/mappers/thread-stream/turn-items.ts @@ -0,0 +1,3 @@ +import type { Catalog } from "../../../../../../app-server/protocol/catalog"; + +export type Escape = Catalog; diff --git a/tests/fixtures/grit-policy/invalid/src/features/chat/application/escape.ts b/tests/fixtures/grit-policy/invalid/src/features/chat/application/escape.ts new file mode 100644 index 00000000..03a2c366 --- /dev/null +++ b/tests/fixtures/grit-policy/invalid/src/features/chat/application/escape.ts @@ -0,0 +1,5 @@ +import type { Protocol } from "../../../../app-server/protocol/catalog"; +import type { Host } from "../host/contracts"; +import type { Workspace } from "../../../../workspace/panel-coordinator"; + +export type Escape = Protocol | Host | Workspace; diff --git a/tests/fixtures/grit-policy/invalid/src/features/chat/application/state/escape.ts b/tests/fixtures/grit-policy/invalid/src/features/chat/application/state/escape.ts new file mode 100644 index 00000000..ead6245b --- /dev/null +++ b/tests/fixtures/grit-policy/invalid/src/features/chat/application/state/escape.ts @@ -0,0 +1 @@ +export const checkedAt = Date.now(); diff --git a/tests/fixtures/grit-policy/invalid/src/features/chat/host/escape.ts b/tests/fixtures/grit-policy/invalid/src/features/chat/host/escape.ts new file mode 100644 index 00000000..930a9c6e --- /dev/null +++ b/tests/fixtures/grit-policy/invalid/src/features/chat/host/escape.ts @@ -0,0 +1,3 @@ +import type { View } from "../presentation/thread-stream/view-model"; + +export type Escape = View; diff --git a/tests/fixtures/grit-policy/invalid/src/features/chat/panel/escape.ts b/tests/fixtures/grit-policy/invalid/src/features/chat/panel/escape.ts new file mode 100644 index 00000000..30cf1524 --- /dev/null +++ b/tests/fixtures/grit-policy/invalid/src/features/chat/panel/escape.ts @@ -0,0 +1,3 @@ +import type { Runtime } from "../host/session-runtime"; + +export type Escape = Runtime; diff --git a/tests/fixtures/grit-policy/invalid/src/features/chat/presentation/escape.ts b/tests/fixtures/grit-policy/invalid/src/features/chat/presentation/escape.ts new file mode 100644 index 00000000..a98f9bc0 --- /dev/null +++ b/tests/fixtures/grit-policy/invalid/src/features/chat/presentation/escape.ts @@ -0,0 +1,6 @@ +import { signal } from "@preact/signals"; +import type { State } from "../application/state/store"; +import type { ReadModel } from "../panel/shell-read-model"; + +export const escaped = signal(1); +export type Escape = State | ReadModel | Signal; diff --git a/tests/fixtures/grit-policy/invalid/src/features/chat/ui/escape.tsx b/tests/fixtures/grit-policy/invalid/src/features/chat/ui/escape.tsx new file mode 100644 index 00000000..c51c25bf --- /dev/null +++ b/tests/fixtures/grit-policy/invalid/src/features/chat/ui/escape.tsx @@ -0,0 +1,10 @@ +import type { State } from "../application/state/store"; +import { addDomEventListener } from "../../../../shared/dom/events.dom"; +import { renderPreactRoot } from "../../../../shared/dom/preact-root.dom"; + +export function Escape(props: { state: State }): JSX.Element { + document.body.append(document.createElement("div")); + void addDomEventListener; + void renderPreactRoot; + return ; +} diff --git a/tests/fixtures/grit-policy/invalid/src/features/selection-rewrite/escape.ts b/tests/fixtures/grit-policy/invalid/src/features/selection-rewrite/escape.ts new file mode 100644 index 00000000..2a5f1533 --- /dev/null +++ b/tests/fixtures/grit-policy/invalid/src/features/selection-rewrite/escape.ts @@ -0,0 +1,4 @@ +import type { Connection } from "../../app-server/connection/client"; +import type { Workspace } from "../../workspace/panel-coordinator"; + +export type Escape = Connection | Workspace; diff --git a/tests/fixtures/grit-policy/invalid/src/features/threads/escape.tsx b/tests/fixtures/grit-policy/invalid/src/features/threads/escape.tsx new file mode 100644 index 00000000..335c63fb --- /dev/null +++ b/tests/fixtures/grit-policy/invalid/src/features/threads/escape.tsx @@ -0,0 +1 @@ +export const misplaced = 1; diff --git a/tests/fixtures/grit-policy/invalid/src/features/threads/workflows/escape.ts b/tests/fixtures/grit-policy/invalid/src/features/threads/workflows/escape.ts new file mode 100644 index 00000000..af873c2f --- /dev/null +++ b/tests/fixtures/grit-policy/invalid/src/features/threads/workflows/escape.ts @@ -0,0 +1,3 @@ +import type { Client } from "../../../app-server/connection/client"; + +export type Escape = Client; diff --git a/tests/fixtures/grit-policy/invalid/src/plugin-runtime.ts b/tests/fixtures/grit-policy/invalid/src/plugin-runtime.ts new file mode 100644 index 00000000..2615035e --- /dev/null +++ b/tests/fixtures/grit-policy/invalid/src/plugin-runtime.ts @@ -0,0 +1,5 @@ +class Runner { + constructor(readonly stop: () => void) {} +} + +export const runner = new Runner(() => runner.stop()); diff --git a/tests/fixtures/grit-policy/invalid/src/settings/escape.ts b/tests/fixtures/grit-policy/invalid/src/settings/escape.ts new file mode 100644 index 00000000..eb4439d9 --- /dev/null +++ b/tests/fixtures/grit-policy/invalid/src/settings/escape.ts @@ -0,0 +1,7 @@ +import type { Generated } from "../generated/app-server/types"; + +export async function read(client: { request(method: string): Promise }): Promise { + await client.request("config/read"); +} + +export type Escape = Generated; diff --git a/tests/fixtures/grit-policy/invalid/src/shared/runtime/escape.ts b/tests/fixtures/grit-policy/invalid/src/shared/runtime/escape.ts new file mode 100644 index 00000000..7ef5b9d5 --- /dev/null +++ b/tests/fixtures/grit-policy/invalid/src/shared/runtime/escape.ts @@ -0,0 +1,7 @@ +import type { Feature } from "../../features/escape"; + +export function first(iterator: Iterator): T { + return iterator.next().value as T; +} + +export type EscapedFeature = Feature; diff --git a/tests/fixtures/grit-policy/invalid/src/styles/escape.css b/tests/fixtures/grit-policy/invalid/src/styles/escape.css new file mode 100644 index 00000000..bb69d90d --- /dev/null +++ b/tests/fixtures/grit-policy/invalid/src/styles/escape.css @@ -0,0 +1,4 @@ +#codex-panel-fixture * { + color: #fff; + padding: 12px; +} diff --git a/tests/fixtures/grit-policy/invalid/src/workspace/escape.ts b/tests/fixtures/grit-policy/invalid/src/workspace/escape.ts new file mode 100644 index 00000000..43c347a8 --- /dev/null +++ b/tests/fixtures/grit-policy/invalid/src/workspace/escape.ts @@ -0,0 +1,3 @@ +import type { ChatState } from "../features/chat/application/state/store"; + +export type Escape = ChatState; diff --git a/tests/fixtures/grit-policy/valid/src/app-server/protocol/safe.ts b/tests/fixtures/grit-policy/valid/src/app-server/protocol/safe.ts new file mode 100644 index 00000000..bf4dc05e --- /dev/null +++ b/tests/fixtures/grit-policy/valid/src/app-server/protocol/safe.ts @@ -0,0 +1,3 @@ +export interface ProtocolValue { + id: string; +} diff --git a/tests/fixtures/grit-policy/valid/src/app-server/services/safe.ts b/tests/fixtures/grit-policy/valid/src/app-server/services/safe.ts new file mode 100644 index 00000000..bbbd6988 --- /dev/null +++ b/tests/fixtures/grit-policy/valid/src/app-server/services/safe.ts @@ -0,0 +1,3 @@ +export function serviceValue(): string { + return "safe"; +} diff --git a/tests/fixtures/grit-policy/valid/src/domain/example/safe.ts b/tests/fixtures/grit-policy/valid/src/domain/example/safe.ts new file mode 100644 index 00000000..a910fe21 --- /dev/null +++ b/tests/fixtures/grit-policy/valid/src/domain/example/safe.ts @@ -0,0 +1 @@ +export type DomainValue = string; diff --git a/tests/fixtures/grit-policy/valid/src/features/chat/app-server/mappers/thread-stream/turn-items.ts b/tests/fixtures/grit-policy/valid/src/features/chat/app-server/mappers/thread-stream/turn-items.ts new file mode 100644 index 00000000..8a9739cf --- /dev/null +++ b/tests/fixtures/grit-policy/valid/src/features/chat/app-server/mappers/thread-stream/turn-items.ts @@ -0,0 +1,3 @@ +export function convert(value: string): string { + return value; +} diff --git a/tests/fixtures/grit-policy/valid/src/features/chat/application/state/safe.ts b/tests/fixtures/grit-policy/valid/src/features/chat/application/state/safe.ts new file mode 100644 index 00000000..08dea1a3 --- /dev/null +++ b/tests/fixtures/grit-policy/valid/src/features/chat/application/state/safe.ts @@ -0,0 +1,3 @@ +export interface ChatStateValue { + ready: boolean; +} diff --git a/tests/fixtures/grit-policy/valid/src/features/chat/host/safe.ts b/tests/fixtures/grit-policy/valid/src/features/chat/host/safe.ts new file mode 100644 index 00000000..e2efd365 --- /dev/null +++ b/tests/fixtures/grit-policy/valid/src/features/chat/host/safe.ts @@ -0,0 +1 @@ +export type HostValue = string; diff --git a/tests/fixtures/grit-policy/valid/src/features/chat/panel/safe.ts b/tests/fixtures/grit-policy/valid/src/features/chat/panel/safe.ts new file mode 100644 index 00000000..9fe715ce --- /dev/null +++ b/tests/fixtures/grit-policy/valid/src/features/chat/panel/safe.ts @@ -0,0 +1 @@ +export type PanelValue = string; diff --git a/tests/fixtures/grit-policy/valid/src/features/chat/presentation/safe.ts b/tests/fixtures/grit-policy/valid/src/features/chat/presentation/safe.ts new file mode 100644 index 00000000..260abe13 --- /dev/null +++ b/tests/fixtures/grit-policy/valid/src/features/chat/presentation/safe.ts @@ -0,0 +1 @@ +export type PresentationValue = string; diff --git a/tests/fixtures/grit-policy/valid/src/features/chat/ui/safe.ts b/tests/fixtures/grit-policy/valid/src/features/chat/ui/safe.ts new file mode 100644 index 00000000..200f0388 --- /dev/null +++ b/tests/fixtures/grit-policy/valid/src/features/chat/ui/safe.ts @@ -0,0 +1,3 @@ +export function label(value: string): string { + return value.trim(); +} diff --git a/tests/fixtures/grit-policy/valid/src/features/chat/ui/safe.tsx b/tests/fixtures/grit-policy/valid/src/features/chat/ui/safe.tsx new file mode 100644 index 00000000..67b32ece --- /dev/null +++ b/tests/fixtures/grit-policy/valid/src/features/chat/ui/safe.tsx @@ -0,0 +1,3 @@ +export function SafeView(): JSX.Element { + return Safe; +} diff --git a/tests/fixtures/grit-policy/valid/src/features/selection-rewrite/safe.ts b/tests/fixtures/grit-policy/valid/src/features/selection-rewrite/safe.ts new file mode 100644 index 00000000..fd8d56a1 --- /dev/null +++ b/tests/fixtures/grit-policy/valid/src/features/selection-rewrite/safe.ts @@ -0,0 +1 @@ +export type RewriteValue = string; diff --git a/tests/fixtures/grit-policy/valid/src/features/threads/workflows/safe.ts b/tests/fixtures/grit-policy/valid/src/features/threads/workflows/safe.ts new file mode 100644 index 00000000..3cc38f8a --- /dev/null +++ b/tests/fixtures/grit-policy/valid/src/features/threads/workflows/safe.ts @@ -0,0 +1 @@ +export type WorkflowValue = string; diff --git a/tests/fixtures/grit-policy/valid/src/plugin-runtime.ts b/tests/fixtures/grit-policy/valid/src/plugin-runtime.ts new file mode 100644 index 00000000..5b422cc2 --- /dev/null +++ b/tests/fixtures/grit-policy/valid/src/plugin-runtime.ts @@ -0,0 +1,3 @@ +export function createRuntime(): object { + return {}; +} diff --git a/tests/fixtures/grit-policy/valid/src/settings/safe.ts b/tests/fixtures/grit-policy/valid/src/settings/safe.ts new file mode 100644 index 00000000..cf1efa51 --- /dev/null +++ b/tests/fixtures/grit-policy/valid/src/settings/safe.ts @@ -0,0 +1 @@ +export type SettingsValue = string; diff --git a/tests/fixtures/grit-policy/valid/src/shared/runtime/safe.ts b/tests/fixtures/grit-policy/valid/src/shared/runtime/safe.ts new file mode 100644 index 00000000..5aca6fd9 --- /dev/null +++ b/tests/fixtures/grit-policy/valid/src/shared/runtime/safe.ts @@ -0,0 +1,3 @@ +export function identity(value: T): T { + return value; +} diff --git a/tests/fixtures/grit-policy/valid/src/styles/safe.css b/tests/fixtures/grit-policy/valid/src/styles/safe.css new file mode 100644 index 00000000..084dd1ce --- /dev/null +++ b/tests/fixtures/grit-policy/valid/src/styles/safe.css @@ -0,0 +1,4 @@ +.codex-panel-fixture { + color: var(--text-normal); + padding: var(--size-4-2); +} diff --git a/tests/fixtures/grit-policy/valid/src/workspace/safe.ts b/tests/fixtures/grit-policy/valid/src/workspace/safe.ts new file mode 100644 index 00000000..2f25d67c --- /dev/null +++ b/tests/fixtures/grit-policy/valid/src/workspace/safe.ts @@ -0,0 +1 @@ +export type WorkspaceValue = string; diff --git a/tests/scripts/grit-policy.test.mjs b/tests/scripts/grit-policy.test.mjs index 3d8ed16a..76d42117 100644 --- a/tests/scripts/grit-policy.test.mjs +++ b/tests/scripts/grit-policy.test.mjs @@ -1,1703 +1,116 @@ import { spawnSync } from "node:child_process"; -import { readFileSync } from "node:fs"; -import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; +import { cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; const repoRoot = process.cwd(); -const tempWorkspaces = new Set(); +const biomeBin = path.join(repoRoot, "node_modules", ".bin", "biome"); +const fixtureRoot = path.join(repoRoot, "tests", "fixtures", "grit-policy"); +const workspaces = new Set(); afterEach(async () => { - await Promise.all([...tempWorkspaces].map((workspace) => rm(workspace, { recursive: true, force: true }))); - tempWorkspaces.clear(); + await Promise.all([...workspaces].map((workspace) => rm(workspace, { recursive: true, force: true }))); + workspaces.clear(); }); -const biomeBin = path.join(repoRoot, "node_modules", ".bin", "biome"); -const projectBiomeConfig = parseJsonc(readFileSync(path.join(repoRoot, "biome.jsonc"), "utf8")); -const projectPluginEntries = projectBiomeConfig.plugins; -const projectPluginByName = new Map( - projectPluginEntries.map((plugin) => { - const pluginPath = projectPluginPath(plugin); - return [path.basename(pluginPath), plugin]; - }), -); -const APP_SERVER_PROTOCOL_BOUNDARY_MESSAGE = - "Source modules outside root src/app-server must use domain models and app-server services instead of app-server protocol modules. Chat turn-item conversion may consume turn protocol at its app-server boundary; feature state and UI must use Panel-owned models."; -const RESPONSIBILITY_ROOT_MODULE_FILE_MESSAGE = - "Keep responsibility-split source roots free of module files; add modules to the matching subfolder instead of the root."; -const APP_SERVER_SUBFOLDER_ROOT_IMPORT_MESSAGE = - "App-server subfolders must not import sibling root modules; move the dependency into a responsibility subfolder."; -const CHAT_APPLICATION_OUTER_LAYER_MESSAGE = - "Chat application modules must not import app-server, sibling feature, host, panel, presentation, or UI layers; expose state and workflow contracts instead."; -const CHAT_APP_SERVER_OUTER_LAYER_MESSAGE = "Chat app-server adapters must not import chat host, panel, presentation, or UI layers."; -const CHAT_WORKSPACE_BOUNDARY_MESSAGE = - "Chat modules must not import workspace modules; pass workspace capabilities through chat host contracts."; -const FEATURE_WORKSPACE_BOUNDARY_MESSAGE = - "Feature modules must not import workspace modules; pass workspace capabilities through feature host contracts."; -const WORKSPACE_CHAT_INTERNAL_MESSAGE = "Workspace modules may coordinate chat only through chat host contracts and Obsidian views."; -const CHAT_HOST_RENDERING_LAYER_MESSAGE = - "Chat host modules must not import chat presentation or UI layers; route render-facing projections through panel-owned adapters."; -const CHAT_PANEL_RUNTIME_BOUNDARY_MESSAGE = "Chat panel modules must not import app-server adapters or chat host internals."; -const CHAT_PRESENTATION_OUTER_LAYER_MESSAGE = - "Chat presentation modules must stay pure view-model projection; keep application, app-server, host, panel, and UI dependencies outward."; -const CHAT_UI_OUTER_LAYER_MESSAGE = - "Chat UI modules must not import application, app-server, host, or panel layers; pass render-ready props and actions through UI contracts."; -const THREAD_WORKFLOW_APP_SERVER_MESSAGE = "Thread workflows must depend on feature-owned ports instead of app-server clients or services."; -const DOM_BOUNDARY_MESSAGE = - "Keep DOM reads, writes, measurements, hit-tests, focus, and event wiring in files named with a .dom, .obsidian, or .measure suffix."; -const DOM_EVENTS_IMPORT_MESSAGE = "Import DOM event listener helpers only from explicit .dom, .obsidian, or .measure bridge files."; -const CHAT_SHELL_READ_MODEL_IMPORT_MESSAGE = - "Import chat panel signal read models only from the shell and panel surface rendering adapters."; -const CHAT_SIGNAL_TYPE_REFERENCE_MESSAGE = "Keep Preact signal types inside the chat panel read-model and surface rendering adapter layer."; -const APP_SERVER_DIRECT_RPC_MESSAGE = "Keep direct app-server RPC calls behind app-server services and chat app-server adapters."; -let appServerBoundaryPolicyReportPromise; -let renderingAndCssPolicyReportPromise; -describe("Biome Grit plugin wiring", () => { - it("keeps Biome wired to every checked-in Grit policy", async () => { - const gritPolicyPaths = await listRelativeGritPolicyPaths(path.join(repoRoot, "scripts/grit")); - const configuredGritPluginPaths = projectPluginEntries - .map((plugin) => projectPluginPath(plugin)) - .filter((pluginPath) => pluginPath.endsWith(".grit")) - .map(normalizeProjectRelativePath) - .sort(); - const configuredGritPluginNames = configuredGritPluginPaths.map((pluginPath) => path.basename(pluginPath)); +describe("Biome Grit policies", () => { + it("wires every checked-in policy once with a non-empty scope", async () => { + const config = parseJsonc(await readFile(path.join(repoRoot, "biome.jsonc"), "utf8")); + const configured = config.plugins.map((plugin) => ({ + path: typeof plugin === "string" ? plugin : plugin.path, + includes: typeof plugin === "string" ? [] : plugin.includes, + })); + const checkedIn = await gritPolicyPaths(path.join(repoRoot, "scripts", "grit")); - expect(new Set(configuredGritPluginPaths).size).toBe(configuredGritPluginPaths.length); - expect(new Set(configuredGritPluginNames).size).toBe(configuredGritPluginNames.length); - expect(configuredGritPluginPaths).toEqual(gritPolicyPaths); + expect(new Set(configured.map((plugin) => plugin.path)).size).toBe(configured.length); + expect(configured.every((plugin) => plugin.includes.length > 0)).toBe(true); + expect(configured.map((plugin) => plugin.path.replace(/^\.\//, "")).sort()).toEqual(checkedIn); }); - it("keeps every Grit policy scoped by explicit Biome plugin includes", () => { - for (const plugin of projectPluginEntries) { - expect(typeof plugin).toBe("object"); - expect(projectPluginPath(plugin)).toMatch(/^\.\/scripts\/grit\/.*\.grit$/); - expect(plugin.includes).toEqual(expect.any(Array)); - expect(plugin.includes.length).toBeGreaterThan(0); - expect(plugin.includes.every((include) => typeof include === "string" && include.length > 0)).toBe(true); + it("rejects the checked-in invalid fixture for every retained matcher", async () => { + const config = parseJsonc(await readFile(path.join(repoRoot, "biome.jsonc"), "utf8")); + for (const plugin of config.plugins) { + const workspace = await fixtureWorkspace(); + await writePluginConfig(workspace, plugin); + const result = biomeLint(workspace, await fixtureSourcePaths(workspace)); + expect(result.status, `${pluginPath(plugin)}\n${result.output}`).toBe(1); + expect(result.pluginErrors, `${pluginPath(plugin)}\n${result.output}`).toBeGreaterThan(0); } - }); + }, 30_000); - it("keeps Biome file includes broad enough for Grit policies and their target files", () => { - expect(projectBiomeConfig.files.includes).toEqual( - expect.arrayContaining(["scripts/**/*.grit", "src/**/*.{css,ts,tsx}", "tests/**/*.{mjs,ts,tsx}", "!src/generated"]), - ); - }); - - it("keeps representative plugin include boundaries visible in Biome config", () => { - expect(projectPluginIncludes("no-misplaced-tsx.grit")).toEqual([ - "**/src/**/*.tsx", - "!**/src/{settings,shared/ui}/**", - "!**/src/features/chat/{panel,ui}/**", - "!**/src/features/{selection-rewrite,threads-view,turn-diff}/*.dom.tsx", - "!**/src/shared/dom/*.dom.tsx", - "!**/src/shared/obsidian/*.obsidian.tsx", - ]); - expect(projectPluginIncludes("no-generated-app-server-boundary-imports.grit")).toEqual([ - "**/src/**/*.{ts,tsx}", - "!**/src/app-server/connection/**", - "!**/src/app-server/protocol/server-requests.ts", - "!**/src/app-server/protocol/thread.ts", - "!**/src/app-server/protocol/turn.ts", - ]); - expect(projectPluginIncludes("no-restricted-css-policy.grit")).toEqual(["**/src/styles/**/*.css"]); - }); + it("accepts the checked-in valid fixture for every retained matcher", async () => { + const config = parseJsonc(await readFile(path.join(repoRoot, "biome.jsonc"), "utf8")); + for (const plugin of config.plugins) { + const workspace = await fixtureWorkspace("valid"); + await writePluginConfig(workspace, plugin); + const result = biomeLint(workspace, await fixtureSourcePaths(workspace)); + expect(result.status, `${pluginPath(plugin)}\n${result.output}`).toBe(0); + expect(result.pluginErrors, pluginPath(plugin)).toBe(0); + } + }, 30_000); }); -describe("GritQL matcher assumptions", () => { - it("keeps thread workflows behind feature-owned app-server ports", async () => { - const cwd = await tempBiomeWorkspace(["no-thread-workflow-app-server-imports.grit"]); - await writeFile( - path.join(cwd, "src/features/threads/workflows/bad.ts"), - ` -import type { AppServerClientAccess } from "../../../app-server/connection/client-access"; -import { renameThread } from "../../../app-server/services/threads"; - -export const values = [renameThread] satisfies unknown[]; -export type Access = AppServerClientAccess; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/threads/workflows/good.ts"), - ` -import type { ThreadOperationsTransport } from "./ports"; - -export type Transport = ThreadOperationsTransport; -`.trimStart(), - ); - - const report = biomeLint(["src/features/threads/workflows/bad.ts", "src/features/threads/workflows/good.ts"], cwd); - - expect(pluginMessages(report, "src/features/threads/workflows/bad.ts")).toEqual([ - THREAD_WORKFLOW_APP_SERVER_MESSAGE, - THREAD_WORKFLOW_APP_SERVER_MESSAGE, - ]); - expect(pluginDiagnostics(report, "src/features/threads/workflows/good.ts")).toEqual([]); - }); - - it("documents project-wide source-shape policy matchers 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"), - ` -export { value } from "./owner"; -export * from "./barrel"; - -const local = 1; -export { local }; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/domain/iterator.ts"), - ` -export function first(iterator: Iterator): 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 ( - <> - - - - ); -} -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/ui/controlled-form-state.tsx"), - ` -export function Composer(): JSX.Element { - return ; -} -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/plugin-runtime.ts"), - ` -class Runner { - constructor(readonly stop: () => void) {} +async function fixtureWorkspace(kind = "invalid") { + const parent = await mkdtemp(path.join(tmpdir(), "codex-panel-grit-policy-")); + const workspace = path.join(parent, "fixture"); + workspaces.add(parent); + await cp(path.join(fixtureRoot, kind), workspace, { recursive: true }); + return workspace; } -const runner = new Runner(() => runner.stop()); -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/plugin-runtime-function.ts"), - ` -class Runner { - constructor(readonly stop: () => void) {} +async function writePluginConfig(workspace, plugin) { + const config = { + $schema: "https://biomejs.dev/schemas/2.5.1/schema.json", + vcs: { enabled: false }, + plugins: [typeof plugin === "string" ? path.resolve(repoRoot, plugin) : { ...plugin, path: path.resolve(repoRoot, plugin.path) }], + }; + await writeFile(path.join(workspace, "biome.json"), JSON.stringify(config)); } -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/domain/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/domain/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("documents chat runtime and DOM policy matcher boundaries", async () => { - const cwd = await tempBiomeWorkspace([ - "no-implicit-dom-bridges.grit", - "no-dom-events-imports.grit", - "no-chat-shell-read-model-imports.grit", - "no-chat-signal-type-references.grit", - "no-preact-signal-imports.grit", - "no-state-module-side-effects.grit", - "no-preact-root-imports.grit", - ]); - await writeFile( - path.join(cwd, "src/features/chat/panel/shell-read-model.ts"), - ` -import { signal, type ReadonlySignal } from "@preact/signals"; - -export const status = signal("idle"); -export type SurfaceSignal = ReadonlySignal; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/presentation/signal-helper.ts"), - ` -import type { ChatPanelComposerReadModel } from "../panel/shell-read-model"; - -export type BadSignals = ReadonlySignal | Signal; -export function read(model: ChatPanelComposerReadModel): string | null { - return model.activeListedThreadName.value; -} -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/panel/surface/signal-surface.tsx"), - ` -import { signal } from "@preact/signals"; - -export const status = signal("idle"); -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/shared/obsidian/components.obsidian.tsx"), - ` -import { signal } from '@preact/signals'; - -export const status = signal("idle"); -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/shared/ui/signal-escapes.tsx"), - ` -import type { Signal } from "@preact/signals"; - -export type SignalValue = Signal; - -export async function loadSignals() { - return import("@preact/signals"); -} - -const signals = await import("@preact/signals"); -export const loadedSignals = signals; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/ui/dom-bridge-escape.tsx"), - ` -export function render(container: HTMLElement): void { - const child = document.body; - const input = container as HTMLInputElement; - container.createDiv(); - container.append(child); - container.setAttribute("role", "region"); - container.setAttr("aria-label", "Chat"); - container.addClass("codex-panel-chat"); - container.removeClass("codex-panel-chat--hidden"); - container.toggleClass("codex-panel-chat--active", true); - container.setText("Ready"); - container.classList.add("codex-panel-chat--ready"); - container.classList.remove("codex-panel-chat--stale"); - container.classList.toggle("codex-panel-chat--busy", false); - container.addEventListener("click", () => undefined); - container.querySelector(".codex-panel-chat"); - container.querySelectorAll(".codex-panel-chat"); - container.closest(".codex-panel-chat"); - container.contains(child); - container.getBoundingClientRect(); - container.focus(); - input.select(); - input.setSelectionRange(0, 0); - container.setCssProps({ display: "block" }); - container.scrollHeight; - container.scrollWidth; - container.clientHeight; - container.clientWidth; - container.offsetTop; - container.offsetHeight; - input.selectionStart; - input.selectionEnd; - input.selectionDirection; - const doc = container.ownerDocument; - doc.defaultView; - container.style.display; - container.style.color; - container.style.setProperty("display", "block"); - container.style.removeProperty("display"); - child.remove(); -} -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/ui/dom-bridge.dom.tsx"), - ` -export function render(container: HTMLElement): void { - const child = document.body; - const input = container as HTMLInputElement; - container.createDiv(); - container.append(child); - container.setAttribute("role", "region"); - container.setAttr("aria-label", "Chat"); - container.addClass("codex-panel-chat"); - container.removeClass("codex-panel-chat--hidden"); - container.toggleClass("codex-panel-chat--active", true); - container.setText("Ready"); - container.classList.add("codex-panel-chat--ready"); - container.classList.remove("codex-panel-chat--stale"); - container.classList.toggle("codex-panel-chat--busy", false); - container.addEventListener("click", () => undefined); - container.querySelector(".codex-panel-chat"); - container.querySelectorAll(".codex-panel-chat"); - container.closest(".codex-panel-chat"); - container.contains(child); - container.getBoundingClientRect(); - container.focus(); - input.select(); - input.setSelectionRange(0, 0); - container.setCssProps({ display: "block" }); - container.scrollHeight; - container.scrollWidth; - container.clientHeight; - container.clientWidth; - container.offsetTop; - container.offsetHeight; - input.selectionStart; - input.selectionEnd; - input.selectionDirection; - const doc = container.ownerDocument; - doc.defaultView; - container.style.display; - container.style.color; - container.style.setProperty("display", "block"); - container.style.removeProperty("display"); - child.remove(); -} -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/ui/dom-event-escape.tsx"), - ` -import { listenDomEvent } from "../../../shared/dom/events.dom"; - -export const listen = listenDomEvent; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/app-server/services/runtime-overrides.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/dom/preact-root.dom'; - -export const render = renderUiRoot; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/panel/shell.dom.tsx"), - ` -import { renderUiRoot } from "../../../shared/dom/preact-root.dom"; -import { createChatPanelShellReadModelBinding } from "./shell-read-model"; - -export const render = renderUiRoot; -export const binding = createChatPanelShellReadModelBinding; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/panel/composer-controller.ts"), - ` -import type { ChatPanelComposerReadModel } from "./shell-read-model"; - -export type ComposerModel = ChatPanelComposerReadModel; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/ui/root-escapes.tsx"), - ` -import type { RootRenderer } from "../../../shared/dom/preact-root.dom"; - -export type { RootRenderer }; - -export async function loadRoot() { - return import("../../../shared/dom/preact-root.dom"); -} - -const root = await import("../../../shared/dom/preact-root.dom"); -export const loadedRoot = root; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/application/state/thread-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-read-model.ts", - "src/features/chat/panel/surface/signal-surface.tsx", - "src/features/chat/presentation/signal-helper.ts", - "src/shared/obsidian/components.obsidian.tsx", - "src/shared/ui/signal-escapes.tsx", - "src/features/chat/ui/dom-bridge-escape.tsx", - "src/features/chat/ui/dom-event-escape.tsx", - "src/features/chat/ui/dom-bridge.dom.tsx", - "src/app-server/services/runtime-overrides.ts", - "src/features/chat/ui/root-import.tsx", - "src/features/chat/panel/shell.dom.tsx", - "src/features/chat/panel/composer-controller.ts", - "src/features/chat/ui/root-escapes.tsx", - "src/features/chat/application/state/thread-stream.ts", - "src/features/chat/application/threads/resume-actions.ts", - ], - cwd, - ); - - expect(pluginDiagnostics(report, "src/features/chat/panel/shell-read-model.ts")).toEqual([]); - expect(pluginDiagnostics(report, "src/features/chat/panel/surface/signal-surface.tsx")).toEqual([]); - expect([...pluginMessages(report, "src/features/chat/presentation/signal-helper.ts")].sort()).toEqual( - [CHAT_SHELL_READ_MODEL_IMPORT_MESSAGE, CHAT_SIGNAL_TYPE_REFERENCE_MESSAGE, CHAT_SIGNAL_TYPE_REFERENCE_MESSAGE].sort(), - ); - expect(pluginMessages(report, "src/shared/obsidian/components.obsidian.tsx")).toEqual([ - "Do not import @preact/signals from this module.", - ]); - expect(pluginMessages(report, "src/shared/ui/signal-escapes.tsx")).toEqual([ - "Do not import @preact/signals from this module.", - "Do not import @preact/signals from this module.", - "Do not import @preact/signals from this module.", - ]); - expectOnlyPluginMessage(report, "src/features/chat/ui/dom-bridge-escape.tsx", DOM_BOUNDARY_MESSAGE); - expect(pluginMessages(report, "src/features/chat/ui/dom-event-escape.tsx")).toEqual([DOM_EVENTS_IMPORT_MESSAGE]); - expect(pluginDiagnostics(report, "src/features/chat/ui/dom-bridge.dom.tsx")).toEqual([]); - expect(pluginDiagnostics(report, "src/app-server/services/runtime-overrides.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(pluginDiagnostics(report, "src/features/chat/panel/composer-controller.ts")).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.", - ]); - expect(pluginMessages(report, "src/features/chat/application/state/thread-stream.ts")).toEqual([ - "Keep this state module deterministic and free of app-server, Obsidian, scheduling, and browser side effects.", - "Keep this state module 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("documents chat folder ownership matchers without filename-scoped Grit checks", async () => { - const cwd = await tempBiomeWorkspace([ - "no-chat-application-outer-layer-imports.grit", - "no-chat-app-server-outer-layer-imports.grit", - "no-chat-workspace-boundary-imports.grit", - "no-feature-workspace-boundary-imports.grit", - "no-workspace-chat-internal-imports.grit", - "no-responsibility-root-module-files.grit", - "no-chat-host-rendering-layer-imports.grit", - "no-chat-panel-runtime-boundary-imports.grit", - "no-chat-presentation-outer-layer-imports.grit", - "no-chat-ui-outer-layer-imports.grit", - ]); - await writeFile( - path.join(cwd, "src/features/chat/application/outer.ts"), - ` -import type { Host } from "../host/contracts"; -import type { ThreadHistoryTransport } from "../app-server/transports/thread-loading-transport"; -import type { ToolbarPanelActions } from "../panel/toolbar-actions"; -import { statusText } from "../presentation/runtime/status"; -import { Toolbar } from "../ui/toolbar"; - -export type Escape = ThreadHistoryTransport | Host | ToolbarPanelActions; -export const values = [statusText, Toolbar] satisfies unknown[]; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/application/allowed.ts"), - ` -import type { ThreadStreamItem } from "../domain/thread-stream/items"; - -export type Item = ThreadStreamItem; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/application/root-app-server.ts"), - ` -import type { AppServerClient } from "../../../app-server/connection/client"; - -export type Escape = AppServerClient; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/application/sibling-feature.ts"), - ` -import type { ThreadRenameLifecycleState } from "../../../threads/list/rename-lifecycle"; -import type { ThreadPickerItem } from "../../../thread-picker/model"; - -export type Escape = ThreadRenameLifecycleState | ThreadPickerItem; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/app-server/outer.ts"), - ` -import type { Host } from "../host/contracts"; -import type { ToolbarPanelActions } from "../panel/toolbar-actions"; -import { statusText } from "../presentation/runtime/status"; -import { Toolbar } from "../ui/toolbar"; - -export type Escape = Host | ToolbarPanelActions; -export const values = [statusText, Toolbar] satisfies unknown[]; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/app-server/allowed.ts"), - ` -import type { AppServerClient } from "../../../app-server/connection/client"; -import type { ChatStateStore } from "../application/state/store"; -import type { ThreadStreamItem } from "../domain/thread-stream/items"; - -export type Allowed = AppServerClient | ChatStateStore | ThreadStreamItem; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/host/workspace-escape.ts"), - ` -import type { WorkspacePanelCoordinator } from "../../../workspace/panel-coordinator"; - -export type Escape = WorkspacePanelCoordinator; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/host/workspace-allowed.ts"), - ` -import type { ChatStateStore } from "../application/state/store"; - -export type Allowed = ChatStateStore; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/host/rendering-escape.ts"), - ` -import { statusText } from "../presentation/runtime/status"; -import { Toolbar } from "../ui/toolbar"; - -export const values = [statusText, Toolbar] satisfies unknown[]; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/host/rendering-allowed.ts"), - ` -import type { ChatStateStore } from "../application/state/store"; -import type { ChatPanelToolbarSurface } from "../panel/surface/toolbar-projection"; - -export type Allowed = ChatStateStore | ChatPanelToolbarSurface; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/threads-view/workspace-escape.ts"), - ` -import type { WorkspacePanelCoordinator } from "../../workspace/panel-coordinator"; - -export type Escape = WorkspacePanelCoordinator; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/threads-view/workspace-allowed.ts"), - ` -import type { Thread } from "../../domain/threads/model"; - -export type Allowed = Thread; -`.trimStart(), - ); - await mkdir(path.join(cwd, "src/workspace"), { recursive: true }); - await writeFile( - path.join(cwd, "src/workspace/chat-internal-escape.ts"), - ` -import type { ToolbarPanelActions } from "../features/chat/panel/toolbar-actions"; - -export type Escape = ToolbarPanelActions; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/workspace/chat-host-allowed.ts"), - ` -import type { ChatWorkspacePanelSurface } from "../features/chat/host/contracts"; -import type { CodexChatView } from "../features/chat/host/view.obsidian"; - -export type Allowed = ChatWorkspacePanelSurface | CodexChatView; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/domain/mixed-root.ts"), - ` -export const misplaced = true; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/mixed-root.ts"), - ` -export const misplaced = true; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/threads/mixed-root.ts"), - ` -export const misplaced = true; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/threads/list/rename-lifecycle.ts"), - ` -export const allowed = true; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/panel/outer.tsx"), - ` - import type { AppServerClient } from "../../../app-server/connection/client"; - import type { ChatAppServerGateway } from "../app-server/session-gateway"; - import type { Host } from "../host/contracts"; - - export type Escape = AppServerClient | ChatAppServerGateway | Host; - `.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/panel/allowed.tsx"), - ` -import type { ChatStateStore } from "../application/state/store"; -import { Toolbar } from "../ui/toolbar"; - -export type Allowed = ChatStateStore; -export const toolbar = Toolbar; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/presentation/outer.ts"), - ` - import type { AppServerClient } from "../../../app-server/connection/client"; - import type { ChatStateStore } from "../application/state/store"; - import type { ChatAppServerGateway } from "../app-server/session-gateway"; - import type { Host } from "../host/contracts"; - import type { ToolbarPanelActions } from "../panel/toolbar-actions"; - import { Toolbar } from "../ui/toolbar"; - - export type Escape = AppServerClient | ChatStateStore | ChatAppServerGateway | Host | ToolbarPanelActions; - export const toolbar = Toolbar; - `.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/presentation/allowed.ts"), - ` -import type { Thread } from "../../../domain/threads/model"; -import type { ThreadStreamItem } from "../domain/thread-stream/items"; - -export type Allowed = Thread | ThreadStreamItem; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/ui/outer.tsx"), - ` - import type { AppServerClient } from "../../../app-server/connection/client"; - import type { ChatStateStore } from "../application/state/store"; - import type { ChatAppServerGateway } from "../app-server/session-gateway"; - import type { Host } from "../host/contracts"; - import type { ToolbarPanelActions } from "../panel/toolbar-actions"; - - export type Escape = AppServerClient | ChatStateStore | ChatAppServerGateway | Host | ToolbarPanelActions; - `.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/ui/allowed.tsx"), - ` -import type { Thread } from "../../../domain/threads/model"; -import type { ThreadStreamItem } from "../domain/thread-stream/items"; -import { statusText } from "../presentation/runtime/status"; - -export type Allowed = Thread | ThreadStreamItem; -export const value = statusText; -`.trimStart(), - ); - - const ownershipFiles = [ - "src/features/chat/application/outer.ts", - "src/features/chat/application/allowed.ts", - "src/features/chat/application/root-app-server.ts", - "src/features/chat/application/sibling-feature.ts", - "src/features/chat/app-server/outer.ts", - "src/features/chat/app-server/allowed.ts", - "src/features/chat/host/workspace-escape.ts", - "src/features/chat/host/workspace-allowed.ts", - "src/features/chat/host/rendering-escape.ts", - "src/features/chat/host/rendering-allowed.ts", - "src/features/threads-view/workspace-escape.ts", - "src/features/threads-view/workspace-allowed.ts", - "src/workspace/chat-internal-escape.ts", - "src/workspace/chat-host-allowed.ts", - "src/domain/mixed-root.ts", - "src/features/chat/mixed-root.ts", - "src/features/threads/mixed-root.ts", - "src/features/threads/list/rename-lifecycle.ts", - "src/features/chat/panel/outer.tsx", - "src/features/chat/panel/allowed.tsx", - "src/features/chat/presentation/outer.ts", - "src/features/chat/presentation/allowed.ts", - "src/features/chat/ui/outer.tsx", - "src/features/chat/ui/allowed.tsx", - ]; - - const report = biomeLint(ownershipFiles, cwd); - - expectOnlyPluginMessage(report, "src/features/chat/application/outer.ts", CHAT_APPLICATION_OUTER_LAYER_MESSAGE); - expect(pluginDiagnostics(report, "src/features/chat/application/allowed.ts")).toEqual([]); - expect(pluginMessages(report, "src/features/chat/application/root-app-server.ts")).toEqual([CHAT_APPLICATION_OUTER_LAYER_MESSAGE]); - expectOnlyPluginMessage(report, "src/features/chat/application/sibling-feature.ts", CHAT_APPLICATION_OUTER_LAYER_MESSAGE); - - expectOnlyPluginMessage(report, "src/features/chat/app-server/outer.ts", CHAT_APP_SERVER_OUTER_LAYER_MESSAGE); - expect(pluginDiagnostics(report, "src/features/chat/app-server/allowed.ts")).toEqual([]); - - expect(pluginMessages(report, "src/features/chat/host/workspace-escape.ts")).toEqual([CHAT_WORKSPACE_BOUNDARY_MESSAGE]); - expect(pluginDiagnostics(report, "src/features/chat/host/workspace-allowed.ts")).toEqual([]); - - expectOnlyPluginMessage(report, "src/features/chat/host/rendering-escape.ts", CHAT_HOST_RENDERING_LAYER_MESSAGE); - expect(pluginDiagnostics(report, "src/features/chat/host/rendering-allowed.ts")).toEqual([]); - - expect(pluginMessages(report, "src/features/threads-view/workspace-escape.ts")).toEqual([FEATURE_WORKSPACE_BOUNDARY_MESSAGE]); - expect(pluginDiagnostics(report, "src/features/threads-view/workspace-allowed.ts")).toEqual([]); - - expect(pluginMessages(report, "src/workspace/chat-internal-escape.ts")).toEqual([WORKSPACE_CHAT_INTERNAL_MESSAGE]); - expect(pluginDiagnostics(report, "src/workspace/chat-host-allowed.ts")).toEqual([]); - - expect(pluginMessages(report, "src/domain/mixed-root.ts")).toEqual([RESPONSIBILITY_ROOT_MODULE_FILE_MESSAGE]); - expect(pluginMessages(report, "src/features/chat/mixed-root.ts")).toEqual([RESPONSIBILITY_ROOT_MODULE_FILE_MESSAGE]); - expect(pluginMessages(report, "src/features/threads/mixed-root.ts")).toEqual([RESPONSIBILITY_ROOT_MODULE_FILE_MESSAGE]); - expect(pluginDiagnostics(report, "src/features/threads/list/rename-lifecycle.ts")).toEqual([]); - - expectOnlyPluginMessage(report, "src/features/chat/panel/outer.tsx", CHAT_PANEL_RUNTIME_BOUNDARY_MESSAGE); - expect(pluginDiagnostics(report, "src/features/chat/panel/allowed.tsx")).toEqual([]); - - expectOnlyPluginMessage(report, "src/features/chat/presentation/outer.ts", CHAT_PRESENTATION_OUTER_LAYER_MESSAGE); - expect(pluginDiagnostics(report, "src/features/chat/presentation/allowed.ts")).toEqual([]); - - expectOnlyPluginMessage(report, "src/features/chat/ui/outer.tsx", CHAT_UI_OUTER_LAYER_MESSAGE); - expect(pluginDiagnostics(report, "src/features/chat/ui/allowed.tsx")).toEqual([]); - }); -}); - -describe("Biome Grit include boundaries", () => { - it("keeps generated app-server imports behind explicit app-server boundaries", async () => { - const report = await generatedAppServerBoundaryPolicyReport(); - - expect(pluginMessages(report, "src/features/chat/domain/generated-thread.ts")).toEqual([ - "Keep generated app-server types behind src/app-server adapters; expose Panel-owned models outside raw app-server boundaries.", - ]); - 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 adapters; expose Panel-owned models outside raw app-server boundaries.", - ]); - expect(pluginMessages(report, "src/app-server/services/runtime-overrides.ts")).toEqual([ - "Keep generated app-server types behind src/app-server adapters; expose Panel-owned models outside raw app-server boundaries.", - ]); - expect(pluginDiagnostics(report, "src/app-server/protocol/turn.ts")).toEqual([]); - expect(pluginDiagnostics(report, "src/app-server/protocol/server-requests.ts")).toEqual([]); - }); - - it("keeps TSX files in rendering-owned source folders", async () => { - const report = await renderingAndCssPolicyReport(); - - expect(pluginMessages(report, "src/features/chat/application/state/view.tsx")).toEqual([ - "Keep TSX files in rendering-owned source folders; non-rendering source should use .ts.", - ]); - expect(pluginDiagnostics(report, "src/features/chat/ui/message.tsx")).toEqual([]); - expect(pluginDiagnostics(report, "src/features/selection-rewrite/popover.dom.tsx")).toEqual([]); - expect(pluginDiagnostics(report, "src/features/threads-view/shell.dom.tsx")).toEqual([]); - expect(pluginDiagnostics(report, "src/settings/section.tsx")).toEqual([]); - expect(pluginDiagnostics(report, "src/shared/obsidian/components.obsidian.tsx")).toEqual([]); - }); - - it("keeps app-server protocol modules behind app-server and chat ingestion boundaries", async () => { - const report = await appServerProtocolBoundaryPolicyReport(); - - expect(pluginMessages(report, "src/features/chat/application/pending-requests/pending-request-actions.ts")).toEqual([ - APP_SERVER_PROTOCOL_BOUNDARY_MESSAGE, - ]); - expect(pluginMessages(report, "src/features/chat/application/threads/history-controller.ts")).toEqual([ - APP_SERVER_PROTOCOL_BOUNDARY_MESSAGE, - ]); - expect(pluginMessages(report, "src/features/chat/panel/surface/thread-stream-presenter.ts")).toEqual([ - APP_SERVER_PROTOCOL_BOUNDARY_MESSAGE, - ]); - expect(pluginMessages(report, "src/features/chat/ui/protocol-leak.tsx")).toEqual([APP_SERVER_PROTOCOL_BOUNDARY_MESSAGE]); - expect(pluginMessages(report, "src/features/chat/app-server/inbound/app-server-logs.ts")).toEqual([ - APP_SERVER_PROTOCOL_BOUNDARY_MESSAGE, - ]); - expectOnlyPluginMessage( - report, - "src/features/chat/app-server/mappers/thread-stream/turn-items.ts", - APP_SERVER_PROTOCOL_BOUNDARY_MESSAGE, - ); - expectOnlyPluginMessage( - report, - "src/features/chat/app-server/inbound/server-request-protocol-leak.ts", - APP_SERVER_PROTOCOL_BOUNDARY_MESSAGE, - ); - }); - - it("keeps domain modules independent from outer layers", async () => { - const report = await domainOuterLayerPolicyReport(); - - expectOnlyPluginMessage( - report, - "src/domain/threads/model.ts", - "Domain modules must stay pure; outer layers may depend on domain, not the reverse.", - ); - expect(pluginDiagnostics(report, "src/domain/threads/format.ts")).toEqual([]); - expectOnlyPluginMessage( - report, - "src/features/chat/domain/thread-stream/selectors.ts", - "Domain modules must stay pure; outer layers may depend on domain, not the reverse.", - ); - expect(pluginDiagnostics(report, "src/features/chat/domain/thread-stream/items.ts")).toEqual([]); - expectOnlyPluginMessage( - report, - "src/features/chat/domain/thread-stream/outer-shapes.ts", - "Domain modules must stay pure; outer layers may depend on domain, not the reverse.", - ); - }); - - it("keeps lower-level modules independent from feature modules", async () => { - const report = await lowerLevelFeaturePolicyReport(); - - expect(pluginMessages(report, "src/app-server/protocol/diagnostics.ts")).toEqual([ - "Do not import feature modules from this layer. Move reusable behavior to domain, shared DOM/Obsidian/runtime/UI, or app-server adapters.", - ]); - expect(pluginMessages(report, "src/shared/obsidian/thread-picker.ts")).toEqual([ - "Do not import feature modules from this layer. Move reusable behavior to domain, shared DOM/Obsidian/runtime/UI, or app-server adapters.", - ]); - expect(pluginDiagnostics(report, "src/domain/display/date.ts")).toEqual([]); - }); - - it("keeps app-server connection internals behind app-server adapters", async () => { - const report = await appServerConnectionBoundaryPolicyReport(); - - expect(pluginMessages(report, "src/app-server/protocol/catalog.ts")).toEqual([ - "Do not import app-server connection internals from this module. Keep connection usage at app-server adapters.", - ]); - expect(pluginDiagnostics(report, "src/app-server/services/catalog.ts")).toEqual([]); - const domainConnectionMessages = pluginMessages(report, "src/domain/connection-client.ts"); - expect(domainConnectionMessages).toEqual( - expect.arrayContaining([ - "Domain modules must stay pure; outer layers may depend on domain, not the reverse.", - "Do not import app-server connection internals from this module. Keep connection usage at app-server adapters.", - ]), - ); - expect(pluginMessages(report, "src/shared/runtime/connection-client.ts")).toEqual([ - "Do not import app-server connection internals from this module. Keep connection usage at app-server adapters.", - ]); - expect(pluginMessages(report, "src/features/selection-rewrite/session.ts")).toEqual([ - "Do not import app-server connection internals from this module. Keep connection usage at app-server adapters.", - ]); - expect(pluginDiagnostics(report, "src/features/selection-rewrite/app-server-transport.ts")).toEqual([]); - }); - - it("keeps app-server root modules from becoming boundary escape hatches", async () => { - const cwd = await tempBiomeWorkspace(["no-responsibility-root-module-files.grit", "no-app-server-subfolder-root-imports.grit"]); - await writeFile( - path.join(cwd, "src/app-server/escape.ts"), - ` -import type { AppServerClient } from "./connection/client"; - -export type Escape = AppServerClient; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/host/chat-app-server-root-import.ts"), - ` -import { createChatAppServerGateway } from "../app-server/session-gateway"; - -export const gateway = createChatAppServerGateway; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/host/bundles/chat-app-server-root-import.ts"), - ` -import { createChatAppServerGateway } from "../../app-server/session-gateway"; - -export const gateway = createChatAppServerGateway; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/chat/host/session/chat-app-server-root-import.ts"), - ` -import { createChatAppServerGateway } from "../../app-server/session-gateway"; - -export const gateway = createChatAppServerGateway; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/app-server/services/root-import.ts"), - ` -import { listThreads } from "../threads"; - -export const read = listThreads; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/app-server/services/allowed.ts"), - ` -import type { AppServerClient } from "../connection/client"; -import { listThreads } from "./threads"; - -export type Allowed = AppServerClient; -export const read = listThreads; -`.trimStart(), - ); - - const report = biomeLint( - [ - "src/app-server/escape.ts", - "src/features/chat/host/chat-app-server-root-import.ts", - "src/features/chat/host/bundles/chat-app-server-root-import.ts", - "src/features/chat/host/session/chat-app-server-root-import.ts", - "src/app-server/services/root-import.ts", - "src/app-server/services/allowed.ts", - ], - cwd, - ); - - expect(pluginMessages(report, "src/app-server/escape.ts")).toEqual([RESPONSIBILITY_ROOT_MODULE_FILE_MESSAGE]); - expect(pluginDiagnostics(report, "src/features/chat/host/chat-app-server-root-import.ts")).toEqual([]); - expect(pluginDiagnostics(report, "src/features/chat/host/bundles/chat-app-server-root-import.ts")).toEqual([]); - expect(pluginDiagnostics(report, "src/features/chat/host/session/chat-app-server-root-import.ts")).toEqual([]); - expect(pluginMessages(report, "src/app-server/services/root-import.ts")).toEqual([APP_SERVER_SUBFOLDER_ROOT_IMPORT_MESSAGE]); - expect(pluginDiagnostics(report, "src/app-server/services/allowed.ts")).toEqual([]); - }); - - it("keeps direct app-server RPCs behind app-server boundary adapters", async () => { - const report = await appServerDirectRpcPolicyReport(); - - expect(pluginMessages(report, "src/features/chat/application/threads/history.ts")).toEqual([APP_SERVER_DIRECT_RPC_MESSAGE]); - expect(pluginMessages(report, "src/features/chat/host/bundles/connection-bundle.ts")).toEqual([APP_SERVER_DIRECT_RPC_MESSAGE]); - expect(pluginMessages(report, "src/settings/runtime-config.ts")).toEqual([APP_SERVER_DIRECT_RPC_MESSAGE]); - expect(pluginDiagnostics(report, "src/app-server/services/threads.ts")).toEqual([]); - expect(pluginDiagnostics(report, "src/features/chat/app-server/transports/threads.ts")).toEqual([]); - }); - - it("keeps CSS on design tokens and scoped selectors", async () => { - const report = await renderingAndCssPolicyReport(); - - expect(pluginMessages(report, "src/styles/bad.css")).toEqual( - expect.arrayContaining([ - "Avoid :has() because it can cause broad selector invalidation.", - "Use Obsidian or Codex Panel design tokens instead of hardcoded colors.", - "Prefer Obsidian or Codex Panel spacing and size tokens for layout dimensions.", - "Do not hide class, id, or attribute selectors inside :where().", - "Use Obsidian or Codex Panel typography tokens instead of hardcoded font sizes.", - "Avoid ID selectors in Codex Panel CSS.", - "Use Obsidian or Codex Panel typography tokens instead of hardcoded font weights.", - "Prefix keyframes with codex-panel-.", - ]), - ); - expect(pluginDiagnostics(report, "src/styles/good.css")).toEqual([]); - }); -}); - -function renderingAndCssPolicyReport() { - renderingAndCssPolicyReportPromise ??= createRenderingAndCssPolicyReport(); - return renderingAndCssPolicyReportPromise; -} - -async function createRenderingAndCssPolicyReport() { - const cwd = await tempBiomeWorkspace(["no-misplaced-tsx.grit", "no-restricted-css-policy.grit"]); - await writeFile( - path.join(cwd, "src/features/chat/application/state/view.tsx"), - ` -export const value =
; -`.trimStart(), - ); - await writeFile( - path.join(cwd, "src/features/selection-rewrite/popover.dom.tsx"), - ` -export const value =