refactor(test): remove incidental implementation contracts

This commit is contained in:
murashit 2026-07-16 22:15:45 +09:00
parent a97f80e25e
commit e4e69ca060
8 changed files with 50 additions and 391 deletions

View file

@ -43,7 +43,7 @@ interface ThreadCatalogFacts {
archivedObservedResult: ObservedResult<readonly Thread[]> | null;
}
export type ThreadCatalogEventObserver = (event: ThreadCatalogEvent) => void;
type ThreadCatalogEventObserver = (event: ThreadCatalogEvent) => void;
export interface ThreadCatalogOptions {
store: ThreadCatalogStore;

View file

@ -41,6 +41,21 @@ describe("AppServerQueryCache", () => {
expect(fetchThreads).toHaveBeenCalledOnce();
});
it("shares concurrent active thread refreshes within one resource identity", async () => {
const pending = deferred<readonly ReturnType<typeof thread>[]>();
const fetchThreads = vi.fn(() => pending.promise);
const cache = cacheWithThreads(fetchThreads);
const context = cacheContext();
const first = cache.refreshActiveThreads(context);
const second = cache.refreshActiveThreads(context);
await flushMicrotasks();
expect(fetchThreads).toHaveBeenCalledOnce();
pending.resolve([thread("shared")]);
await expect(Promise.all([first, second])).resolves.toEqual([[thread("shared")], [thread("shared")]]);
});
it("keeps active and archived thread list snapshots separate", async () => {
const fetchThreads = vi.fn((_context: AppServerQueryContext, archived: boolean) =>
Promise.resolve(archived ? [thread("archived", true)] : [thread("active")]),

View file

@ -10,7 +10,7 @@ import {
import type { ThreadStreamItem } from "../../../../../src/features/chat/domain/thread-stream/items";
describe("thread stream selectors", () => {
it("reuses active-segment indexes while appending deltas to an existing item", () => {
it("appends deltas to an existing active-segment item", () => {
const initial = threadStreamStartActiveSegment(initialChatThreadStreamState(), "turn-1", [
{
id: "assistant-1",
@ -23,9 +23,6 @@ describe("thread stream selectors", () => {
turnId: "turn-1",
},
]);
const previousSegment = initial.activeSegment;
if (!previousSegment) throw new Error("Expected active segment");
const next = reduceThreadStreamSlice(initial, {
type: "thread-stream/assistant-delta-appended",
itemId: "source-1",
@ -34,8 +31,6 @@ describe("thread stream selectors", () => {
});
expect(next.activeSegment?.items[0]).toMatchObject({ text: "hello world" });
expect(next.activeSegment?.indexById).toBe(previousSegment.indexById);
expect(next.activeSegment?.indexBySourceItemId).toBe(previousSegment.indexBySourceItemId);
});
it("counts turns after a turn id from thread stream state", () => {

View file

@ -3,7 +3,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { StaleAppServerResourceContextError } from "../../../../src/app-server/query/resource-store";
import type { ModelMetadata } from "../../../../src/domain/catalog/metadata";
import type { Thread } from "../../../../src/domain/threads/model";
import { activeThreadId } from "../../../../src/features/chat/application/state/root-reducer";
import { type ChatStateStore, createChatStateStore } from "../../../../src/features/chat/application/state/store";
@ -93,60 +92,6 @@ describe("ChatPanelSessionRuntime actions", () => {
expect(stateStore.getState().threadList.listedThreads).toEqual([]);
});
it("applies cached shared state from the runtime binding", () => {
const thread = threadFixture({ id: "thread-1", preview: "Cached thread" });
const model = modelFixture({ id: "model-1", model: "gpt-cached" });
const { runtime, stateStore } = sessionRuntimeFixture({
environment: {
plugin: {
threadCatalog: {
activeSnapshot: vi.fn(() => [thread]),
},
appServerQueries: {
modelsSnapshot: vi.fn(() => [model]),
},
},
},
});
runtime.runtime.sharedState.applyCached();
expect(stateStore.getState().threadList.listedThreads).toEqual([thread]);
expect(stateStore.getState().connection.availableModels).toEqual([model]);
});
it("subscribes and unsubscribes fixed shared state observers", () => {
const cleanupThreads = vi.fn();
const cleanupMetadata = vi.fn();
const cleanupModels = vi.fn();
const observeThreads = vi.fn(() => cleanupThreads);
const observeMetadata = vi.fn(() => cleanupMetadata);
const observeModels = vi.fn(() => cleanupModels);
const { runtime } = sessionRuntimeFixture({
environment: {
plugin: {
threadCatalog: {
observeActive: observeThreads,
},
appServerQueries: {
observeAppServerMetadataResult: observeMetadata,
observeModelsResult: observeModels,
},
},
},
});
runtime.runtime.sharedState.subscribe();
runtime.runtime.sharedState.unsubscribe();
expect(observeThreads).toHaveBeenCalledWith(expect.any(Function), { emitCurrent: false });
expect(observeMetadata).toHaveBeenCalledWith(expect.any(Function), { emitCurrent: false });
expect(observeModels).toHaveBeenCalledWith(expect.any(Function), { emitCurrent: false });
expect(cleanupThreads).toHaveBeenCalledOnce();
expect(cleanupMetadata).toHaveBeenCalledOnce();
expect(cleanupModels).toHaveBeenCalledOnce();
});
it("starts a new thread from runtime state and composer actions", async () => {
const focusComposer = vi.spyOn(ChatComposerController.prototype, "focusComposer").mockImplementation(() => undefined);
const refreshLiveState = vi.fn();
@ -413,21 +358,4 @@ describe("ChatPanelSessionRuntime actions", () => {
...overrides,
};
}
function modelFixture(overrides: Partial<ModelMetadata> = {}): ModelMetadata {
return {
id: "model",
model: "gpt-5",
displayName: "GPT-5",
description: "",
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: [],
serviceTiers: [],
defaultServiceTier: null,
isDefault: false,
...overrides,
};
}
});

View file

@ -5,75 +5,16 @@ import type { AppServerQueryContext, AppServerQueryContextIdentity } from "../..
import type { ObservedResult, ObservedResultListener } from "../../../../src/app-server/query/observed-result";
import { AppServerResourceStore } from "../../../../src/app-server/query/resource-store";
import type { Thread } from "../../../../src/domain/threads/model";
import {
createThreadCatalog,
type ThreadCatalog,
type ThreadCatalogEventObserver,
} from "../../../../src/features/threads/catalog/thread-catalog";
import { createThreadCatalog, type ThreadCatalog } from "../../../../src/features/threads/catalog/thread-catalog";
describe("ThreadCatalog", () => {
it("projects active snapshots received from the store observer", () => {
const { catalog } = catalogFixture();
const threads = [thread("thread")];
const listener = vi.fn();
catalog.observeActive(listener);
receiveActive(catalog, threads);
expect(catalog.activeSnapshot()).toEqual(threads);
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ value: threads }));
});
it("projects archived snapshots received from the store observer", () => {
const { catalog } = catalogFixture();
const threads = [thread("thread", true)];
const listener = vi.fn();
catalog.observeArchived(listener);
receiveArchived(catalog, threads);
expect(catalog.archivedSnapshot()).toEqual(threads);
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ value: threads }));
});
it("refreshes thread snapshots through the cache single-flight and notifies observers once", async () => {
const fetchThreads = vi.fn().mockResolvedValue([thread("thread")]);
const { catalog } = catalogFixture({ fetchThreads });
const listener = vi.fn();
catalog.observeActive(listener);
const first = catalog.refreshActive();
const second = catalog.refreshActive();
await expect(first).resolves.toEqual([thread("thread")]);
await expect(second).resolves.toEqual([thread("thread")]);
expect(fetchThreads).toHaveBeenCalledOnce();
expect(catalog.activeSnapshot()).toEqual([thread("thread")]);
expect(listener.mock.calls.filter(([result]) => result.value !== null)).toHaveLength(1);
expect(listener).toHaveBeenLastCalledWith(expect.objectContaining({ value: [thread("thread")] }));
});
it("notifies applied catalog events through a generic observer", () => {
const onEventApplied = vi.fn();
const { catalog } = catalogFixture({ onEventApplied });
catalog.apply({ type: "thread-started", thread: thread("thread") });
expect(onEventApplied).toHaveBeenCalledWith({ type: "thread-started", thread: thread("thread") });
});
it("applies rename mutations after updating the catalog cache", () => {
const { catalog } = catalogFixture();
const listener = vi.fn();
catalog.observeActive(listener);
receiveActive(catalog, [thread("thread"), thread("other")]);
catalog.apply({ type: "thread-renamed", threadId: "thread", name: "Renamed" });
expect(catalog.activeSnapshot()).toEqual([{ ...thread("thread"), name: "Renamed" }, thread("other")]);
expect(listener).toHaveBeenLastCalledWith(
expect.objectContaining({ value: [{ ...thread("thread"), name: "Renamed" }, thread("other")] }),
);
});
it("applies archive mutations after updating catalog membership", () => {
@ -123,10 +64,6 @@ describe("ThreadCatalog", () => {
it("applies known delete mutations to cache", () => {
const { catalog } = catalogFixture();
const listener = vi.fn();
const archivedListener = vi.fn();
catalog.observeActive(listener);
catalog.observeArchived(archivedListener);
receiveActive(catalog, [thread("thread"), thread("other")]);
receiveArchived(catalog, [thread("thread", true), thread("archived", true)]);
@ -134,16 +71,10 @@ describe("ThreadCatalog", () => {
expect(catalog.activeSnapshot()).toEqual([thread("other")]);
expect(catalog.archivedSnapshot()).toEqual([thread("archived", true)]);
expect(listener).toHaveBeenLastCalledWith(expect.objectContaining({ value: [thread("other")] }));
expect(archivedListener).toHaveBeenLastCalledWith(expect.objectContaining({ value: [thread("archived", true)] }));
});
it("records started, forked, and restored thread membership", () => {
const { catalog } = catalogFixture();
const listener = vi.fn();
const archivedListener = vi.fn();
catalog.observeActive(listener);
catalog.observeArchived(archivedListener);
receiveActive(catalog, [thread("existing")]);
receiveArchived(catalog, [thread("restored", true), thread("archived", true)]);
@ -153,10 +84,6 @@ describe("ThreadCatalog", () => {
expect(catalog.activeSnapshot()?.map((item) => item.id)).toEqual(["restored", "forked", "started", "existing"]);
expect(catalog.archivedSnapshot()?.map((item) => item.id)).toEqual(["archived"]);
expect(listener).toHaveBeenLastCalledWith(
expect.objectContaining({ value: [thread("restored"), thread("forked"), thread("started"), thread("existing")] }),
);
expect(archivedListener).toHaveBeenLastCalledWith(expect.objectContaining({ value: [thread("archived", true)] }));
});
it("keeps app-server lifecycle threads visible until the server list acknowledges them", async () => {
@ -318,8 +245,6 @@ describe("ThreadCatalog", () => {
it("records active thread touches as catalog ordering facts", () => {
const { catalog } = catalogFixture();
const listener = vi.fn();
catalog.observeActive(listener);
receiveActive(catalog, [
thread("active", false, { updatedAt: 1, recencyAt: 1 }),
thread("other", false, { updatedAt: 10, recencyAt: 10 }),
@ -331,33 +256,6 @@ describe("ThreadCatalog", () => {
thread("active", false, { updatedAt: 1, recencyAt: 20 }),
thread("other", false, { updatedAt: 10, recencyAt: 10 }),
]);
expect(listener).toHaveBeenLastCalledWith(
expect.objectContaining({
value: [thread("active", false, { updatedAt: 1, recencyAt: 20 }), thread("other", false, { updatedAt: 10, recencyAt: 10 })],
}),
);
});
it("applies thread lifecycle events through one catalog event boundary", () => {
const { catalog } = catalogFixture();
const listener = vi.fn();
const archivedListener = vi.fn();
catalog.observeActive(listener);
catalog.observeArchived(archivedListener);
receiveActive(catalog, [thread("existing")]);
receiveArchived(catalog, [thread("archived", true)]);
catalog.apply({ type: "thread-started", thread: thread("started") });
catalog.apply({ type: "thread-touched", threadId: "existing", recencyAt: 20 });
catalog.apply({ type: "thread-renamed", threadId: "started", name: "Started" });
catalog.apply({ type: "thread-archived", threadId: "existing" });
expect(catalog.activeSnapshot()).toEqual([{ ...thread("started"), name: "Started" }]);
expect(catalog.archivedSnapshot()).toEqual([thread("existing", true, { recencyAt: 20 }), thread("archived", true)]);
expect(listener).toHaveBeenLastCalledWith(expect.objectContaining({ value: [{ ...thread("started"), name: "Started" }] }));
expect(archivedListener).toHaveBeenLastCalledWith(
expect.objectContaining({ value: [thread("existing", true, { recencyAt: 20 }), thread("archived", true)] }),
);
});
it("model-checks stale snapshots around unacknowledged rename and archive facts", () => {
@ -413,10 +311,6 @@ describe("ThreadCatalog", () => {
return Promise.resolve([thread("unknown")]);
});
const { catalog } = catalogFixture({ fetchThreads });
const listener = vi.fn();
const archivedListener = vi.fn();
catalog.observeActive(listener);
catalog.observeArchived(archivedListener);
receiveActive(catalog, [thread("active")]);
receiveArchived(catalog, [thread("known", true)]);
@ -424,8 +318,6 @@ describe("ThreadCatalog", () => {
expect(catalog.activeSnapshot()).toEqual([thread("known"), thread("active")]);
expect(catalog.archivedSnapshot()).toEqual([]);
expect(listener).toHaveBeenLastCalledWith(expect.objectContaining({ value: [thread("known"), thread("active")] }));
expect(archivedListener).toHaveBeenLastCalledWith(expect.objectContaining({ value: [] }));
catalog.apply({ type: "thread-unarchived", threadId: "unknown" });
@ -549,7 +441,6 @@ function catalogFixture(
options: {
fetchThreads?: (context: { codexPath: string; vaultPath: string }, archived: boolean) => Promise<readonly Thread[]>;
context?: () => { codexPath: string; vaultPath: string };
onEventApplied?: ThreadCatalogEventObserver;
} = {},
) {
const cache = cacheWithThreads(options.fetchThreads ?? (() => Promise.resolve([])));
@ -557,14 +448,7 @@ function catalogFixture(
const queries = new AppServerResourceStore({ cache });
queries.initialize(context());
const store = new TestThreadCatalogStore(queries);
const catalog = createThreadCatalog(
options.onEventApplied
? {
store,
onEventApplied: options.onEventApplied,
}
: { store },
);
const catalog = createThreadCatalog({ store });
catalogStores.set(catalog, store);
catalog.observeActive(() => undefined);
catalog.observeArchived(() => undefined);

View file

@ -594,39 +594,6 @@ describe("CodexPanelPlugin workspace panel reconciliation", () => {
expect(secondRenamed).toHaveBeenCalledWith("thread-1", "Renamed thread");
});
it("single-flights shared thread list refreshes and caches successful results", async () => {
const { CodexChatView } = await import("../src/features/chat/host/view.obsidian");
const connectedLeaf = leaf();
connectedLeaf.view = chatView(CodexChatView, connectedLeaf);
const connectedView = connectedLeaf.view as CodexChatView;
vi.spyOn(connectedView.surface, "openPanelSnapshot").mockReturnValue(panelSnapshot({ viewId: "connected", connected: true }));
vi.spyOn(connectedView.surface, "canServeAppServerContext").mockReturnValue(true);
let resolveThreads!: (threads: Thread[]) => void;
const runWithAppServerClient = vi.spyOn(connectedView.surface, "runWithAppServerClient").mockImplementation((operation) =>
operation(
threadListClient(
() =>
new Promise<Thread[]>((resolve) => {
resolveThreads = resolve;
}),
),
),
);
const plugin = await pluginWithLeaves([connectedLeaf]);
plugin.settings.codexPath = "codex";
const first = threadCatalog(plugin).refreshActive();
const second = threadCatalog(plugin).refreshActive();
await flushMicrotasks();
expect(runWithAppServerClient).toHaveBeenCalledOnce();
resolveThreads([thread("first")]);
await expect(first).resolves.toEqual([thread("first")]);
await expect(second).resolves.toEqual([thread("first")]);
expect(threadCatalog(plugin).activeSnapshot()).toEqual([thread("first")]);
});
it("keeps shared thread list refreshes separate across app-server cache contexts", async () => {
const { CodexChatView } = await import("../src/features/chat/host/view.obsidian");
const connectedLeaf = leaf();

View file

@ -204,6 +204,37 @@ describe("Biome Grit policies", () => {
expect(result.status, result.output).toBe(0);
expect(result.pluginErrors, result.output).toBe(0);
});
it.each([
["the relational :has pseudo-class", ".panel:has(.child) { color: var(--text-normal); }"],
["class selectors hidden inside :where", ".panel:where(.child) { color: var(--text-normal); }"],
["ID selectors", "#panel { color: var(--text-normal); }"],
["universal selectors", ".panel * { color: var(--text-normal); }"],
])("no-restricted-css-policy.grit rejects %s", async (_name, invalidSource) => {
const result = await lintPolicyCase(
{
plugin: "no-restricted-css-policy.grit",
invalid: { path: "src/styles/invalid.css", source: invalidSource },
},
"invalid",
);
expect(result.status, result.output).toBe(1);
expect(result.pluginErrors, result.output).toBeGreaterThan(0);
});
it("no-restricted-css-policy.grit accepts shallow panel selectors", async () => {
const result = await lintPolicyCase(
{
plugin: "no-restricted-css-policy.grit",
valid: { path: "src/styles/valid.css", source: ".panel .child:hover { color: var(--text-normal); }" },
},
"valid",
);
expect(result.status, result.output).toBe(0);
expect(result.pluginErrors, result.output).toBe(0);
});
});
function policyCase(plugin, invalidPath, invalidSource, validSource, options = {}) {

View file

@ -12,11 +12,6 @@ function ruleBody(selector: string): string {
return new RegExp(`(?:^|\\n)${escapedSelector} \\{(?<body>[^}]+)\\}`).exec(styles)?.groups?.["body"] ?? "";
}
function standaloneRuleBody(selector: string): string {
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return new RegExp(`(?:^|\\n\\n)${escapedSelector} \\{(?<body>[^}]+)\\}`).exec(styles)?.groups?.["body"] ?? "";
}
describe("panel CSS boundaries", () => {
it("defines design tokens on every standalone UI root", () => {
const tokenScopeEnd = styles.indexOf(" {");
@ -29,12 +24,6 @@ describe("panel CSS boundaries", () => {
expect(tokenScope).toContain(".codex-panel-threads");
expect(tokenScope).toContain(".codex-panel-selection-rewrite");
});
it("keeps selectors shallow enough for Obsidian theme compatibility", () => {
expect(styles).not.toContain(":has(");
expect(styles).not.toMatch(/:where\([^)]*[.#[]/);
expect(styles).not.toMatch(/(^|[\s,{])#[\w-]+/);
});
});
describe("panel CSS layout invariants", () => {
@ -44,154 +33,4 @@ describe("panel CSS layout invariants", () => {
expect(threadStream).toContain("overflow-y: auto");
expect(threadStream).not.toMatch(/^\s+height:/m);
});
it("keeps icon-only toolbar actions on Obsidian toolbar geometry", () => {
const toolbar = ruleBody(".codex-panel__toolbar");
const toolbarAction = ruleBody(".codex-panel-ui__toolbar-action");
expect(toolbarAction).toContain("--icon-size: var(--codex-panel-control-icon-size)");
expect(toolbarAction).toContain("--icon-stroke: var(--icon-m-stroke-width, 1.75px)");
expect(toolbarAction).not.toContain("width:");
expect(toolbarAction).not.toContain("height:");
expect(toolbarAction).not.toContain("padding:");
expect(toolbarAction).not.toContain("border-radius:");
expect(toolbar).not.toContain("padding:");
});
it("keeps shared nav rows aligned with Obsidian nav item geometry", () => {
const navItem = ruleBody(".codex-panel-ui__nav-item");
const navRow = ruleBody(".codex-panel-ui__nav-row");
expect(navItem).toContain("min-height: var(--nav-item-size");
expect(navItem).toContain("padding: var(--nav-item-padding");
expect(navItem).toContain("border-radius: var(--nav-item-radius");
expect(navRow).toContain("padding-inline-end:");
});
it("keeps selected nav items stable when hovered or focused", () => {
const selectedNavItem = ruleBody(".codex-panel-ui__nav-item.is-selected:where(:hover, :focus, :focus-visible, :active, :focus-within)");
const selectedNavRow = ruleBody(
[
".codex-panel-ui__nav-row.is-selected,",
".codex-panel-ui__nav-row.is-selected:hover,",
".codex-panel-ui__nav-row.is-selected:focus-within",
].join("\n"),
);
expect(selectedNavItem).toContain("background: var(--nav-item-background-active, var(--background-modifier-active))");
expect(selectedNavRow).toContain("background: var(--nav-item-background-active, var(--background-modifier-active))");
expect(selectedNavItem).not.toContain("nav-item-background-active-hover");
expect(selectedNavRow).not.toContain("nav-item-background-active-hover");
});
it("keeps nav inline inputs as native inline editors", () => {
const navInlineInput = ruleBody(".codex-panel-ui__nav-inline-input.codex-panel-ui__nav-inline-input");
const navInlineInputFocus = ruleBody(
[
".codex-panel-ui__nav-inline-input.codex-panel-ui__nav-inline-input:focus,",
".codex-panel-ui__nav-inline-input.codex-panel-ui__nav-inline-input:focus-visible,",
".codex-panel-ui__nav-inline-input.codex-panel-ui__nav-inline-input:hover,",
".codex-panel-ui__nav-inline-input.codex-panel-ui__nav-inline-input:active",
].join("\n"),
);
expect(navInlineInput).toContain("appearance: none");
expect(navInlineInput).toContain("border: 0");
expect(navInlineInput).toContain("background: transparent");
expect(navInlineInputFocus).toContain("background: transparent");
});
it("keeps framed text inputs native across panel surfaces", () => {
const frame = ruleBody(".codex-panel-ui__text-input-frame");
const frameFocus = ruleBody(".codex-panel-ui__text-input-frame:focus-within");
const input = ruleBody(".codex-panel-ui__text-input");
const inputInteraction = ruleBody(
[
".codex-panel-ui__text-input.codex-panel-ui__text-input:hover,",
".codex-panel-ui__text-input.codex-panel-ui__text-input:focus,",
".codex-panel-ui__text-input.codex-panel-ui__text-input:focus-visible,",
".codex-panel-ui__text-input.codex-panel-ui__text-input:active",
].join("\n"),
);
expect(frame).toContain("background: var(--background-modifier-form-field)");
expect(frameFocus).toContain("border-color: var(--background-modifier-border-focus)");
expect(input).toContain("border: 0");
expect(inputInteraction).toContain("background: transparent");
expect(inputInteraction).toContain("outline: none");
});
it("shares activity dot timing across active surfaces", () => {
const activityDots = ruleBody(".codex-panel-ui__activity-dots span");
const secondDot = ruleBody(".codex-panel-ui__activity-dots span:where(:nth-child(2))");
const thirdDot = ruleBody(".codex-panel-ui__activity-dots span:where(:nth-child(3))");
expect(activityDots).toContain("animation: codex-panel-activity-dot 1.2s infinite ease-in-out");
expect(secondDot).toContain("animation-delay: 0.18s");
expect(thirdDot).toContain("animation-delay: 0.36s");
});
it("keeps compact status rows from wrapping or stealing pointer interaction", () => {
const contextMeter = ruleBody(".codex-panel__composer-meta-context");
const summary = ruleBody(".codex-panel__composer-meta-summary");
expect(contextMeter).toContain("white-space: nowrap");
expect(contextMeter).toContain("font-variant-numeric: tabular-nums");
expect(summary).toContain("position: absolute");
expect(summary).toContain("pointer-events: none");
});
it("lets usage limit meters absorb available width", () => {
const limitMeterCell = ruleBody(".codex-panel__limit-panel-meter-cell");
const limitValue = ruleBody(".codex-panel__limit-panel-value");
const limitReset = standaloneRuleBody(".codex-panel__limit-panel-reset");
expect(limitMeterCell).toContain("width: 100%");
expect(limitMeterCell).toContain("vertical-align: middle");
expect(limitValue).toContain("font-variant-numeric: tabular-nums");
expect(limitReset).toContain("white-space: nowrap");
});
it("keeps threads view row titles ellipsized while actions stay collapsed until interaction", () => {
const rowTitleBase = ruleBody(".codex-panel-threads__row-title");
const rowTitleDisplay = standaloneRuleBody(".codex-panel-threads__row-title");
const actions = ruleBody(".codex-panel-threads__actions");
const actionReveal = ruleBody(
[
".codex-panel-threads__row:hover .codex-panel-threads__actions,",
".codex-panel-threads__row:focus-within .codex-panel-threads__actions",
].join("\n"),
);
expect(rowTitleDisplay).toContain("display: block");
expect(rowTitleBase).toContain("white-space: nowrap");
expect(rowTitleBase).toContain("text-overflow: ellipsis");
expect(actions).toContain("width: 0");
expect(actions).toContain("pointer-events: none");
expect(actions).toContain("opacity: 0");
expect(actionReveal).toContain("width: auto");
expect(actionReveal).toContain("pointer-events: auto");
expect(actionReveal).toContain("opacity: 1");
});
it("keeps threads view inline rename aligned to nav row height", () => {
const renameForm = ruleBody(".codex-panel-threads__rename-form");
const renameField = ruleBody(".codex-panel-threads__rename-field");
const renameInput = ruleBody(".codex-panel-threads__rename-input");
expect(renameForm).toContain("min-height: var(--nav-item-size");
expect(renameField).toContain("min-height: var(--nav-item-size");
expect(renameInput).toContain("line-height: var(--line-height-tight)");
});
it("keeps selection rewrite controls framed as a compact edit-and-review surface", () => {
const composerFrame = ruleBody(".codex-panel-selection-rewrite__composer-frame");
const sharedFrame = ruleBody(".codex-panel-ui__text-input-frame");
const result = ruleBody(".codex-panel-selection-rewrite__result");
expect(composerFrame).toContain("grid-template-columns:");
expect(sharedFrame).toContain("background: var(--background-modifier-form-field)");
expect(result).toContain("grid-template-columns:");
expect(result).toContain("border: var(--codex-panel-border)");
});
});