From e4e69ca0606088b8156f4967e65b82cbac99060a Mon Sep 17 00:00:00 2001 From: murashit Date: Thu, 16 Jul 2026 22:15:45 +0900 Subject: [PATCH] refactor(test): remove incidental implementation contracts --- .../threads/catalog/thread-catalog.ts | 2 +- tests/app-server/query-cache.test.ts | 15 ++ .../application/state/thread-stream.test.ts | 7 +- .../chat/host/session-runtime.test.ts | 72 -------- .../threads/catalog/thread-catalog.test.ts | 120 +------------ tests/main.test.ts | 33 ---- tests/scripts/grit-policy.test.mjs | 31 ++++ tests/styles.test.ts | 161 ------------------ 8 files changed, 50 insertions(+), 391 deletions(-) diff --git a/src/features/threads/catalog/thread-catalog.ts b/src/features/threads/catalog/thread-catalog.ts index 45737614..43b83967 100644 --- a/src/features/threads/catalog/thread-catalog.ts +++ b/src/features/threads/catalog/thread-catalog.ts @@ -43,7 +43,7 @@ interface ThreadCatalogFacts { archivedObservedResult: ObservedResult | null; } -export type ThreadCatalogEventObserver = (event: ThreadCatalogEvent) => void; +type ThreadCatalogEventObserver = (event: ThreadCatalogEvent) => void; export interface ThreadCatalogOptions { store: ThreadCatalogStore; diff --git a/tests/app-server/query-cache.test.ts b/tests/app-server/query-cache.test.ts index ad51abf6..e363b857 100644 --- a/tests/app-server/query-cache.test.ts +++ b/tests/app-server/query-cache.test.ts @@ -41,6 +41,21 @@ describe("AppServerQueryCache", () => { expect(fetchThreads).toHaveBeenCalledOnce(); }); + it("shares concurrent active thread refreshes within one resource identity", async () => { + const pending = deferred[]>(); + 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")]), diff --git a/tests/features/chat/application/state/thread-stream.test.ts b/tests/features/chat/application/state/thread-stream.test.ts index 89768a2a..37880f53 100644 --- a/tests/features/chat/application/state/thread-stream.test.ts +++ b/tests/features/chat/application/state/thread-stream.test.ts @@ -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", () => { diff --git a/tests/features/chat/host/session-runtime.test.ts b/tests/features/chat/host/session-runtime.test.ts index 29f6100f..5920a589 100644 --- a/tests/features/chat/host/session-runtime.test.ts +++ b/tests/features/chat/host/session-runtime.test.ts @@ -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 { - return { - id: "model", - model: "gpt-5", - displayName: "GPT-5", - description: "", - hidden: false, - supportedReasoningEfforts: [], - defaultReasoningEffort: null, - inputModalities: [], - serviceTiers: [], - defaultServiceTier: null, - isDefault: false, - ...overrides, - }; - } }); diff --git a/tests/features/threads/catalog/thread-catalog.test.ts b/tests/features/threads/catalog/thread-catalog.test.ts index 32a452fd..c24d7de5 100644 --- a/tests/features/threads/catalog/thread-catalog.test.ts +++ b/tests/features/threads/catalog/thread-catalog.test.ts @@ -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; 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); diff --git a/tests/main.test.ts b/tests/main.test.ts index f4fbe9bf..63cfbfc5 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -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((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(); diff --git a/tests/scripts/grit-policy.test.mjs b/tests/scripts/grit-policy.test.mjs index 280b636d..2e90634f 100644 --- a/tests/scripts/grit-policy.test.mjs +++ b/tests/scripts/grit-policy.test.mjs @@ -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 = {}) { diff --git a/tests/styles.test.ts b/tests/styles.test.ts index e9726dad..5a93d538 100644 --- a/tests/styles.test.ts +++ b/tests/styles.test.ts @@ -12,11 +12,6 @@ function ruleBody(selector: string): string { return new RegExp(`(?:^|\\n)${escapedSelector} \\{(?[^}]+)\\}`).exec(styles)?.groups?.["body"] ?? ""; } -function standaloneRuleBody(selector: string): string { - const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - return new RegExp(`(?:^|\\n\\n)${escapedSelector} \\{(?[^}]+)\\}`).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)"); - }); });