test: focus architecture checks on durable invariants

This commit is contained in:
murashit 2026-07-15 13:45:00 +09:00
parent bbd981f255
commit 8ae6aab1fb
41 changed files with 191 additions and 1819 deletions

View file

@ -195,6 +195,7 @@
"scripts/**/*.mjs",
"src/**/*.{css,ts,tsx}",
"tests/**/*.{mjs,ts,tsx}",
"!tests/fixtures/grit-policy",
"!src/generated"
]
},

View file

@ -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.

View file

@ -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"]
}

View file

@ -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<void> {
await Promise.resolve();
await Promise.resolve();

View file

@ -0,0 +1,4 @@
import type { Client } from "../connection/client";
import type { Root } from "../root";
export type Escape = Client | Root;

View file

@ -0,0 +1,4 @@
import "obsidian";
export { value } from "./owner";
export * from "./barrel";

View file

@ -0,0 +1,3 @@
import type { Panel } from "../panel/toolbar-actions";
export type Escape = Panel;

View file

@ -0,0 +1,3 @@
import type { Catalog } from "../../../../../../app-server/protocol/catalog";
export type Escape = Catalog;

View file

@ -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;

View file

@ -0,0 +1 @@
export const checkedAt = Date.now();

View file

@ -0,0 +1,3 @@
import type { View } from "../presentation/thread-stream/view-model";
export type Escape = View;

View file

@ -0,0 +1,3 @@
import type { Runtime } from "../host/session-runtime";
export type Escape = Runtime;

View file

@ -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<string>;

View file

@ -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 <input defaultValue={String(props.state)} />;
}

View file

@ -0,0 +1,4 @@
import type { Connection } from "../../app-server/connection/client";
import type { Workspace } from "../../workspace/panel-coordinator";
export type Escape = Connection | Workspace;

View file

@ -0,0 +1 @@
export const misplaced = 1;

View file

@ -0,0 +1,3 @@
import type { Client } from "../../../app-server/connection/client";
export type Escape = Client;

View file

@ -0,0 +1,5 @@
class Runner {
constructor(readonly stop: () => void) {}
}
export const runner = new Runner(() => runner.stop());

View file

@ -0,0 +1,7 @@
import type { Generated } from "../generated/app-server/types";
export async function read(client: { request(method: string): Promise<void> }): Promise<void> {
await client.request("config/read");
}
export type Escape = Generated;

View file

@ -0,0 +1,7 @@
import type { Feature } from "../../features/escape";
export function first<T>(iterator: Iterator<T>): T {
return iterator.next().value as T;
}
export type EscapedFeature = Feature;

View file

@ -0,0 +1,4 @@
#codex-panel-fixture * {
color: #fff;
padding: 12px;
}

View file

@ -0,0 +1,3 @@
import type { ChatState } from "../features/chat/application/state/store";
export type Escape = ChatState;

View file

@ -0,0 +1,3 @@
export interface ProtocolValue {
id: string;
}

View file

@ -0,0 +1,3 @@
export function serviceValue(): string {
return "safe";
}

View file

@ -0,0 +1 @@
export type DomainValue = string;

View file

@ -0,0 +1,3 @@
export function convert(value: string): string {
return value;
}

View file

@ -0,0 +1,3 @@
export interface ChatStateValue {
ready: boolean;
}

View file

@ -0,0 +1 @@
export type HostValue = string;

View file

@ -0,0 +1 @@
export type PanelValue = string;

View file

@ -0,0 +1 @@
export type PresentationValue = string;

View file

@ -0,0 +1,3 @@
export function label(value: string): string {
return value.trim();
}

View file

@ -0,0 +1,3 @@
export function SafeView(): JSX.Element {
return <span>Safe</span>;
}

View file

@ -0,0 +1 @@
export type RewriteValue = string;

View file

@ -0,0 +1 @@
export type WorkflowValue = string;

View file

@ -0,0 +1,3 @@
export function createRuntime(): object {
return {};
}

View file

@ -0,0 +1 @@
export type SettingsValue = string;

View file

@ -0,0 +1,3 @@
export function identity<T>(value: T): T {
return value;
}

View file

@ -0,0 +1,4 @@
.codex-panel-fixture {
color: var(--text-normal);
padding: var(--size-4-2);
}

View file

@ -0,0 +1 @@
export type WorkspaceValue = string;

File diff suppressed because it is too large Load diff

View file

@ -26,5 +26,6 @@
"types": ["node", "vitest"],
"lib": ["ES2022", "DOM"]
},
"include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx", "vitest.config.ts"]
"include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx", "vitest.config.ts"],
"exclude": ["tests/fixtures/grit-policy/**"]
}