From b00f6292c407c5352fc9e9670da7a925a2236c58 Mon Sep 17 00:00:00 2001 From: murashit Date: Mon, 20 Jul 2026 18:09:42 +0900 Subject: [PATCH] refactor: remove test-only source exports --- .../connection/connection-manager.ts | 2 +- src/app-server/connection/transport.ts | 5 +- src/app-server/protocol/request-input.ts | 11 +- .../services/ephemeral-structured-turn.ts | 2 +- src/domain/chat/context-manifest.ts | 2 +- src/domain/runtime/config.ts | 2 +- src/domain/server/diagnostics.ts | 6 - src/domain/threads/rename-lifecycle.ts | 2 +- .../composer/context-references.ts | 4 - .../application/composer/slash-commands.ts | 11 +- .../connection/connection-actions.ts | 6 +- .../application/panel-operation-policy.ts | 4 +- .../application/turns/plan-implementation.ts | 2 +- .../chat/host/bundles/connection-bundle.ts | 8 +- .../vault-daily-note-references.obsidian.ts | 2 +- .../chat/panel/composer-controller.ts | 2 +- src/features/chat/ui/composer.tsx | 2 +- src/settings/dynamic-sections-controller.ts | 2 +- tests/app-server/connection-manager.test.ts | 3 +- .../ephemeral-structured-turn.test.ts | 8 +- tests/app-server/request-input.test.ts | 46 +++-- tests/app-server/transport.test.ts | 80 ++++---- tests/domain/chat/context-manifest.test.ts | 17 +- tests/domain/runtime/config.test.ts | 5 +- tests/domain/threads/rename-lifecycle.test.ts | 3 +- .../chat/app-server/transports.test.ts | 22 +- .../composer/slash-commands.test.ts | 8 +- .../application/composer/suggestions.test.ts | 17 +- .../composer/wikilink-context.test.ts | 6 +- .../connection/connection-actions.test.ts | 12 +- .../connection/server-actions.test.ts | 4 +- .../panel-operation-policy.test.ts | 63 +++++- .../runtime/settings-actions.test.ts | 6 +- .../application/state/root-reducer.test.ts | 6 +- .../threads/thread-start-actions.test.ts | 8 +- .../turns/plan-implementation.test.ts | 15 +- .../chat/host/connection-bundle.test.ts | 191 ++++++++++++------ .../vault-note-candidate-provider.test.ts | 28 +-- .../chat/host/view-connection-harness.ts | 4 +- .../chat/host/view-restoration.test.ts | 4 +- .../chat/panel/composer-controller.test.ts | 25 ++- .../runtime/diagnostic-sections.test.ts | 31 --- tests/features/chat/ui/composer.test.ts | 4 +- .../dynamic-sections-controller.test.ts | 4 +- tests/support/runtime-config.ts | 8 + 45 files changed, 410 insertions(+), 293 deletions(-) create mode 100644 tests/support/runtime-config.ts diff --git a/src/app-server/connection/connection-manager.ts b/src/app-server/connection/connection-manager.ts index fc40b2e5..f789d8c5 100644 --- a/src/app-server/connection/connection-manager.ts +++ b/src/app-server/connection/connection-manager.ts @@ -11,7 +11,7 @@ export interface ConnectionManagerHandlers { onExit: () => void; } -export type AppServerClientFactory = (codexPath: string, cwd: string, handlers: AppServerClientHandlers) => AppServerClient; +type AppServerClientFactory = (codexPath: string, cwd: string, handlers: AppServerClientHandlers) => AppServerClient; type ConnectionLifecycleState = | { kind: "idle"; generation: number } diff --git a/src/app-server/connection/transport.ts b/src/app-server/connection/transport.ts index b1234098..fd318f38 100644 --- a/src/app-server/connection/transport.ts +++ b/src/app-server/connection/transport.ts @@ -24,10 +24,7 @@ export interface AppServerTransportHandlers { onError: (error: Error) => void; } -export function createAppServerSpawnSpec( - codexPath: string, - options: { platform?: NodeJS.Platform; comSpec?: string } = {}, -): AppServerSpawnSpec { +function createAppServerSpawnSpec(codexPath: string, options: { platform?: NodeJS.Platform; comSpec?: string } = {}): AppServerSpawnSpec { const platform = options.platform ?? process.platform; if (platform !== "win32" || !isWindowsCommandScript(codexPath)) { return { command: codexPath, args: ["app-server"], killProcessTreeOnStop: false }; diff --git a/src/app-server/protocol/request-input.ts b/src/app-server/protocol/request-input.ts index 6d200545..39392ac9 100644 --- a/src/app-server/protocol/request-input.ts +++ b/src/app-server/protocol/request-input.ts @@ -26,8 +26,8 @@ export interface AppServerTurnInput { additionalContext?: Record; } -export const ADDITIONAL_CONTEXT_PART_BODY_MAX_BYTES = 2_800; -export const ADDITIONAL_CONTEXT_MAX_PARTS = 8; +const ADDITIONAL_CONTEXT_PART_BODY_MAX_BYTES = 2_800; +const ADDITIONAL_CONTEXT_MAX_PARTS = 8; export function toAppServerUserInput(input: readonly CodexInputItem[], manifest: TurnContextManifest | null = null): AppServerUserInput[] { const userInput = input.flatMap((item) => appServerUserInputItemFromCodexInputItem(item)); @@ -37,13 +37,6 @@ export function toAppServerUserInput(input: readonly CodexInputItem[], manifest: return userInput; } -export function additionalContextFromCodexInput( - input: readonly CodexInputItem[], - submissionId = "submission", -): Record | undefined { - return appServerTurnInputFromCodexInput(input, submissionId).additionalContext; -} - export function appServerTurnInputFromCodexInput(input: readonly CodexInputItem[], submissionId: string): AppServerTurnInput { const additionalContext: Record = {}; const manifest: TurnContextManifestEntry[] = []; diff --git a/src/app-server/services/ephemeral-structured-turn.ts b/src/app-server/services/ephemeral-structured-turn.ts index 9c88614b..097abdfc 100644 --- a/src/app-server/services/ephemeral-structured-turn.ts +++ b/src/app-server/services/ephemeral-structured-turn.ts @@ -38,7 +38,7 @@ export interface EphemeralStructuredTurnClient { type EphemeralStructuredTurnRuntimeCapableClient = EphemeralStructuredTurnClient & ModelMetadataClient; -export type EphemeralStructuredTurnClientFactory = ( +type EphemeralStructuredTurnClientFactory = ( codexPath: string, cwd: string, handlers: AppServerClientHandlers, diff --git a/src/domain/chat/context-manifest.ts b/src/domain/chat/context-manifest.ts index d8a6db68..df78e3df 100644 --- a/src/domain/chat/context-manifest.ts +++ b/src/domain/chat/context-manifest.ts @@ -79,7 +79,7 @@ export function boundedTurnContextManifest( return { ...base, ...(included.length > 0 ? { fileReferences: included } : {}) }; } -export function turnContextManifestFromText(text: string): TurnContextManifest | null { +function turnContextManifestFromText(text: string): TurnContextManifest | null { const trimmed = text.trim(); if (!trimmed.startsWith(TURN_CONTEXT_MANIFEST_PREFIX)) return null; if (utf8ByteLength(trimmed) > TURN_CONTEXT_MANIFEST_MAX_BYTES) return null; diff --git a/src/domain/runtime/config.ts b/src/domain/runtime/config.ts index 84dc72e6..7b508baf 100644 --- a/src/domain/runtime/config.ts +++ b/src/domain/runtime/config.ts @@ -19,7 +19,7 @@ export interface RuntimeConfigSnapshot { readonly autoCompactTokenLimit: number | null; } -export function emptyRuntimeConfigSnapshot(): RuntimeConfigSnapshot { +function emptyRuntimeConfigSnapshot(): RuntimeConfigSnapshot { return { profile: null, model: null, diff --git a/src/domain/server/diagnostics.ts b/src/domain/server/diagnostics.ts index 0a5f0292..ce186c22 100644 --- a/src/domain/server/diagnostics.ts +++ b/src/domain/server/diagnostics.ts @@ -125,12 +125,6 @@ export function upsertMcpServerDiagnostic(diagnostics: Diagnostics, server: McpS }; } -export function upsertMcpServerStatusDiagnostics(diagnostics: Diagnostics, servers: readonly McpServerStatusSummary[]): Diagnostics { - let next = diagnostics; - for (const server of servers) next = upsertMcpServerDiagnostic(next, mcpServerDiagnosticFromStatus(server)); - return next; -} - export function replaceMcpServerStatusDiagnostics(diagnostics: Diagnostics, servers: readonly McpServerStatusSummary[]): Diagnostics { return { ...diagnostics, diff --git a/src/domain/threads/rename-lifecycle.ts b/src/domain/threads/rename-lifecycle.ts index 29b1ab7b..1d61521a 100644 --- a/src/domain/threads/rename-lifecycle.ts +++ b/src/domain/threads/rename-lifecycle.ts @@ -4,7 +4,7 @@ export type ThreadRenameLifecycleState = | { kind: "generating"; draft: string; originalDraft: string; generationToken: number }; export type ThreadRenameActiveState = Exclude; -export type ThreadRenameGeneratingState = Extract; +type ThreadRenameGeneratingState = Extract; export type ThreadRenameLifecycleEvent = | { type: "started"; draft: string } diff --git a/src/features/chat/application/composer/context-references.ts b/src/features/chat/application/composer/context-references.ts index a46b55f0..6829a987 100644 --- a/src/features/chat/application/composer/context-references.ts +++ b/src/features/chat/application/composer/context-references.ts @@ -34,10 +34,6 @@ export interface ComposerContextReferenceProvider { dispose(): void; } -export function emptyComposerContextReferences(): ComposerContextReferences { - return { activeNote: null, selection: null, activeNoteSnapshots: [], selectionSnapshots: [] }; -} - export function formatComposerContextRange(range: ComposerContextRange): string { return `${formatComposerContextPosition(range.from)}-${formatComposerContextPosition(range.to)}`; } diff --git a/src/features/chat/application/composer/slash-commands.ts b/src/features/chat/application/composer/slash-commands.ts index 92c2df21..aee7a9e5 100644 --- a/src/features/chat/application/composer/slash-commands.ts +++ b/src/features/chat/application/composer/slash-commands.ts @@ -1,4 +1,4 @@ -import { type ActivePanelOperation, activePanelOperationDecisionForFacts } from "../panel-operation-policy"; +import type { ActivePanelOperation } from "../panel-operation-policy"; type SlashCommandArgsKind = | "none" @@ -217,15 +217,6 @@ export function slashCommandRequiresConnection(command: SlashCommandName): boole return !CONNECTION_INDEPENDENT_SLASH_COMMANDS.has(command); } -export function slashCommandAvailableInSideChat(command: SlashCommandName): boolean { - const operation = activePanelOperationForSlashCommandSuggestion(command); - return ( - !operation || - activePanelOperationDecisionForFacts({ phase: "active", lifetime: "ephemeral", provenance: "interactive" }, operation).kind === - "allowed" - ); -} - /** Maps a parsed command to the active-panel operation it performs, if any. */ export function activePanelOperationForSlashCommand(command: SlashCommandName, args: string): ActivePanelOperation | null { switch (command) { diff --git a/src/features/chat/application/connection/connection-actions.ts b/src/features/chat/application/connection/connection-actions.ts index 7e89d3ae..aadc7b36 100644 --- a/src/features/chat/application/connection/connection-actions.ts +++ b/src/features/chat/application/connection/connection-actions.ts @@ -7,16 +7,16 @@ const STATUS_CONNECTION_STARTING = "Starting Codex app-server..."; const STATUS_CONNECTED = "Connected."; const STATUS_CONNECTION_FAILED = "Connection failed."; -export interface ChatConnectionAdapter { +interface ChatConnectionAdapter { connect(): Promise; isConnected(): boolean; } -export interface ChatConnectionMetadataActions { +interface ChatConnectionMetadataActions { refreshAppServerMetadata: () => Promise; } -export interface ChatConnectionDiagnosticsActions { +interface ChatConnectionDiagnosticsActions { refreshServerDiagnostics: (options?: { appServerMetadataSnapshot?: boolean; forceResourceProbes?: boolean }) => Promise; } diff --git a/src/features/chat/application/panel-operation-policy.ts b/src/features/chat/application/panel-operation-policy.ts index 93cd0849..544a008a 100644 --- a/src/features/chat/application/panel-operation-policy.ts +++ b/src/features/chat/application/panel-operation-policy.ts @@ -24,7 +24,7 @@ export type ActivePanelOperationDecision = | { readonly kind: "blocked"; readonly message: string } | { readonly kind: "resume-required" }; -export type ActivePanelThreadFacts = +type ActivePanelThreadFacts = | { readonly phase: "empty" } | { readonly phase: "awaiting-resume"; readonly provenance: "interactive" | "subagent" | null } | { @@ -39,7 +39,7 @@ export function activePanelOperationDecision(state: ChatState, operation: Active return activePanelOperationDecisionForFacts(activePanelThreadFacts(state), operation); } -export function activePanelOperationDecisionForFacts( +function activePanelOperationDecisionForFacts( facts: ActivePanelThreadFacts, operation: ActivePanelOperation, ): ActivePanelOperationDecision { diff --git a/src/features/chat/application/turns/plan-implementation.ts b/src/features/chat/application/turns/plan-implementation.ts index 08e67a65..4527e78b 100644 --- a/src/features/chat/application/turns/plan-implementation.ts +++ b/src/features/chat/application/turns/plan-implementation.ts @@ -24,7 +24,7 @@ interface PlanImplementationState { threadStream: Pick; } -export function implementPlanTargetFromState(state: ChatState): PlanImplementationTarget | null { +function implementPlanTargetFromState(state: ChatState): PlanImplementationTarget | null { return implementPlanTarget({ activeThread: activeThreadState(state), modeAllowed: activePanelOperationDecision(state, "implement-plan").kind === "allowed", diff --git a/src/features/chat/host/bundles/connection-bundle.ts b/src/features/chat/host/bundles/connection-bundle.ts index 42b29787..8f7a14d5 100644 --- a/src/features/chat/host/bundles/connection-bundle.ts +++ b/src/features/chat/host/bundles/connection-bundle.ts @@ -54,14 +54,14 @@ export interface ChatPanelConnectionBundle { refreshSharedThreads: () => Promise; } -export interface DeferredDiagnosticsRefreshHost { +interface DeferredDiagnosticsRefreshHost { scheduleDiagnostics(callback: () => void): void; isConnected(): boolean; refreshServerDiagnostics(options: { appServerMetadataSnapshot: true }): Promise; addSystemMessage(text: string): void; } -export function scheduleDeferredDiagnosticsRefresh(host: DeferredDiagnosticsRefreshHost): void { +function scheduleDeferredDiagnosticsRefresh(host: DeferredDiagnosticsRefreshHost): void { host.scheduleDiagnostics(() => { if (!host.isConnected()) return; void host.refreshServerDiagnostics({ appServerMetadataSnapshot: true }).catch((error: unknown) => { @@ -70,14 +70,14 @@ export function scheduleDeferredDiagnosticsRefresh(host: DeferredDiagnosticsRefr }); } -export interface ServerRequestResponderRegistry { +interface ServerRequestResponderRegistry { remember(requestId: RespondRequestId, responder: AppServerServerRequestResponder): void; respond(requestId: RespondRequestId, result: unknown): boolean; reject(requestId: RejectRequestId, code: number, message: string): boolean; clear(): void; } -export function createServerRequestResponderRegistry(): ServerRequestResponderRegistry { +function createServerRequestResponderRegistry(): ServerRequestResponderRegistry { const responders = new Map(); const take = (requestId: RespondRequestId): AppServerServerRequestResponder | null => { const responder = responders.get(requestId) ?? null; diff --git a/src/features/chat/host/obsidian/vault-daily-note-references.obsidian.ts b/src/features/chat/host/obsidian/vault-daily-note-references.obsidian.ts index cfae80be..56639e97 100644 --- a/src/features/chat/host/obsidian/vault-daily-note-references.obsidian.ts +++ b/src/features/chat/host/obsidian/vault-daily-note-references.obsidian.ts @@ -26,7 +26,7 @@ export function configuredDailyNoteReferences(app: App, sourcePath: string): rea } } -export function dailyNoteReferencesFromSettings( +function dailyNoteReferencesFromSettings( app: App, sourcePath: string, settings: IPeriodicNoteSettings, diff --git a/src/features/chat/panel/composer-controller.ts b/src/features/chat/panel/composer-controller.ts index 3aed10dc..5a015946 100644 --- a/src/features/chat/panel/composer-controller.ts +++ b/src/features/chat/panel/composer-controller.ts @@ -72,7 +72,7 @@ export interface ChatComposerControllerOptions { onAttachmentError?: (message: string) => void; } -export interface ChatComposerRenderActions { +interface ChatComposerRenderActions { submit: () => void; } diff --git a/src/features/chat/ui/composer.tsx b/src/features/chat/ui/composer.tsx index a58f8a4f..e34b5067 100644 --- a/src/features/chat/ui/composer.tsx +++ b/src/features/chat/ui/composer.tsx @@ -15,7 +15,7 @@ import { syncComposerHeight, } from "./composer.dom"; -export interface ComposerSuggestion { +interface ComposerSuggestion { display: string; detail: string; replacement: string; diff --git a/src/settings/dynamic-sections-controller.ts b/src/settings/dynamic-sections-controller.ts index dc731e95..939f86f7 100644 --- a/src/settings/dynamic-sections-controller.ts +++ b/src/settings/dynamic-sections-controller.ts @@ -18,7 +18,7 @@ type SettingsDynamicSectionLifecycleState = | { kind: "loaded"; status: string } | { kind: "failed"; status: string }; -export interface SettingsDynamicSectionsSnapshot { +interface SettingsDynamicSectionsSnapshot { archivedThreads: readonly Thread[]; archivedThreadsLifecycle: SettingsDynamicSectionLifecycleState; archivedThreadsLoaded: boolean; diff --git a/tests/app-server/connection-manager.test.ts b/tests/app-server/connection-manager.test.ts index 2e368c97..d8b3ec1b 100644 --- a/tests/app-server/connection-manager.test.ts +++ b/tests/app-server/connection-manager.test.ts @@ -2,7 +2,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { AppServerClient } from "../../src/app-server/connection/client"; import { - type AppServerClientFactory, ConnectionManager, type ConnectionManagerHandlers, StaleConnectionError, @@ -186,6 +185,8 @@ function silentConnectionHandlers(): ConnectionManagerHandlers { }; } +type AppServerClientFactory = NonNullable[3]>; + function testClientFactory(options: { requestTimeoutMs?: number; onTransport: (transport: SilentTransport) => void; diff --git a/tests/app-server/ephemeral-structured-turn.test.ts b/tests/app-server/ephemeral-structured-turn.test.ts index 09d7443f..2b8518c7 100644 --- a/tests/app-server/ephemeral-structured-turn.test.ts +++ b/tests/app-server/ephemeral-structured-turn.test.ts @@ -4,11 +4,7 @@ import { describe, expect, it, vi } from "vitest"; import type { AppServerClientHandlers, ClientResponseByMethod, TypedClientRequestMethod } from "../../src/app-server/connection/client"; import type { ClientRequestParams } from "../../src/app-server/connection/rpc-messages"; import type { TurnItem, TurnRecord } from "../../src/app-server/protocol/turn"; -import { - type EphemeralStructuredTurnClient, - type EphemeralStructuredTurnClientFactory, - runEphemeralStructuredTurn, -} from "../../src/app-server/services/ephemeral-structured-turn"; +import { type EphemeralStructuredTurnClient, runEphemeralStructuredTurn } from "../../src/app-server/services/ephemeral-structured-turn"; import type { AppServerStartEphemeralThreadOptions } from "../../src/app-server/services/threads"; import type { AppServerStartStructuredTurnOptions } from "../../src/app-server/services/turns"; import type { InitializeResponse } from "../../src/generated/app-server/InitializeResponse"; @@ -302,6 +298,8 @@ function runOptions(): Parameters[0] { }; } +type EphemeralStructuredTurnClientFactory = NonNullable[1]>["clientFactory"]; + function fakeStructuredTurnClientFactory(configure?: (client: FakeStructuredTurnClient) => void): { clientFactory: EphemeralStructuredTurnClientFactory; client: { current: FakeStructuredTurnClient | null }; diff --git a/tests/app-server/request-input.test.ts b/tests/app-server/request-input.test.ts index c46ed9f9..33edcb07 100644 --- a/tests/app-server/request-input.test.ts +++ b/tests/app-server/request-input.test.ts @@ -1,15 +1,22 @@ import { describe, expect, it } from "vitest"; -import { - ADDITIONAL_CONTEXT_MAX_PARTS, - ADDITIONAL_CONTEXT_PART_BODY_MAX_BYTES, - additionalContextFromCodexInput, - appServerTurnInputFromCodexInput, - toAppServerUserInput, -} from "../../src/app-server/protocol/request-input"; +import { appServerTurnInputFromCodexInput, toAppServerUserInput } from "../../src/app-server/protocol/request-input"; import { utf8ByteLength } from "../../src/domain/chat/context-budget"; -import { turnContextManifestFromText } from "../../src/domain/chat/context-manifest"; +import { type TurnContextManifest, userMessageContextProjection } from "../../src/domain/chat/context-manifest"; import { type CodexInput, codexTextInputWithAttachments, codexTextInputWithReferences } from "../../src/domain/chat/input"; +const ADDITIONAL_CONTEXT_MAX_PARTS = 8; +const PANEL_SUBMISSION_ID = "local-user-1-seed-1-1"; + +function turnContextManifestFromText(text: string, submissionId: string): TurnContextManifest | null { + return userMessageContextProjection( + [ + { type: "text", text: "visible request" }, + { type: "text", text: text.startsWith("\n") ? text : `\n${text}` }, + ], + submissionId, + ).manifest; +} + describe("app-server request input", () => { it("builds text input with file references and skills", () => { expect( @@ -61,7 +68,7 @@ describe("app-server request input", () => { ); expect(toAppServerUserInput(input)).toEqual([{ type: "text", text: "Use [[Note]]", text_elements: [] }]); - expect(additionalContextFromCodexInput(input, "local-user")).toEqual({ + expect(appServerTurnInputFromCodexInput(input, "local-user").additionalContext).toEqual({ "codex_panel.local-user.00.codex_panel_obsidian_context.part_01_of_01": { kind: "untrusted", value: "Codex Panel context part 1/1.\nSource: codex_panel_obsidian_context\n\n- [[Note]] -> Note.md", @@ -82,7 +89,7 @@ describe("app-server request input", () => { expect(prepared.input).toHaveLength(2); expect(prepared.input).not.toContainEqual(expect.objectContaining({ type: "mention" })); const descriptor = prepared.input.at(-1); - const manifest = descriptor?.type === "text" ? turnContextManifestFromText(descriptor.text) : null; + const manifest = descriptor?.type === "text" ? turnContextManifestFromText(descriptor.text, "local-user-1-seed-1-1") : null; expect(manifest).toEqual({ version: 2, submissionId: "local-user-1-seed-1-1", @@ -115,7 +122,7 @@ describe("app-server request input", () => { const descriptor = prepared.input.at(-1); const descriptorText = descriptor?.type === "text" ? descriptor.text : ""; - const manifest = turnContextManifestFromText(descriptorText); + const manifest = turnContextManifestFromText(descriptorText, "local-user-1-seed-1-1"); expect(utf8ByteLength(descriptorText)).toBeLessThanOrEqual(2_801); expect(manifest?.contexts).toEqual([expect.objectContaining({ kind: "web" })]); expect(manifest?.fileReferences?.length).toBeGreaterThan(0); @@ -135,7 +142,7 @@ describe("app-server request input", () => { attachment: { kind: "web" }, }, ], - "local-user-1", + PANEL_SUBMISSION_ID, ); const entries = Object.entries(prepared.additionalContext ?? {}); @@ -143,11 +150,10 @@ describe("app-server request input", () => { expect(entries.map(([key]) => key)).toEqual([...entries.map(([key]) => key)].sort()); expect(entries.every(([, entry]) => utf8ByteLength(entry.value) < 4_000)).toBe(true); expect(entries.every(([, entry]) => entry.value.includes("Codex Panel context part"))).toBe(true); - expect(ADDITIONAL_CONTEXT_PART_BODY_MAX_BYTES).toBeLessThan(4_000); const manifestInput = prepared.input.at(-1); expect(manifestInput?.type).toBe("text"); - const manifest = manifestInput?.type === "text" ? turnContextManifestFromText(manifestInput.text) : null; + const manifest = manifestInput?.type === "text" ? turnContextManifestFromText(manifestInput.text, PANEL_SUBMISSION_ID) : null; expect(manifest?.contexts).toEqual([ expect.objectContaining({ kind: "web", @@ -180,13 +186,13 @@ describe("app-server request input", () => { attachment: { kind: "obsidian", inlineExcerpts: 1 }, }, ], - "local-user", + PANEL_SUBMISSION_ID, ); const firstPart = Object.values(prepared.additionalContext ?? {})[0]; expect(firstPart?.value).toContain(reference); const manifestInput = prepared.input.at(-1); - const manifest = manifestInput?.type === "text" ? turnContextManifestFromText(manifestInput.text) : null; + const manifest = manifestInput?.type === "text" ? turnContextManifestFromText(manifestInput.text, PANEL_SUBMISSION_ID) : null; expect(manifest?.contexts).toEqual([expect.objectContaining({ kind: "obsidian", inlineExcerpts: 1, truncated: true })]); }); @@ -195,8 +201,8 @@ describe("app-server request input", () => { { type: "text", text: "read it" }, { type: "additionalContext", key: "same", kind: "untrusted", value: "same" }, ]; - const first = Object.keys(additionalContextFromCodexInput(input, "first") ?? {}); - const second = Object.keys(additionalContextFromCodexInput(input, "second") ?? {}); + const first = Object.keys(appServerTurnInputFromCodexInput(input, "first").additionalContext ?? {}); + const second = Object.keys(appServerTurnInputFromCodexInput(input, "second").additionalContext ?? {}); expect(first).not.toEqual(second); }); @@ -207,14 +213,14 @@ describe("app-server request input", () => { { type: "additionalContext", key: "web", kind: "untrusted", value: "w".repeat(30_000), attachment: { kind: "web" } }, { type: "additionalContext", key: "selection", kind: "untrusted", value: "selected text" }, ], - "local-user", + PANEL_SUBMISSION_ID, ); const entries = Object.entries(prepared.additionalContext ?? {}); expect(entries).toHaveLength(ADDITIONAL_CONTEXT_MAX_PARTS); expect(entries.some(([key, entry]) => key.includes(".selection.") && entry.value.includes("selected text"))).toBe(true); const manifestInput = prepared.input.at(-1); - const manifest = manifestInput?.type === "text" ? turnContextManifestFromText(manifestInput.text) : null; + const manifest = manifestInput?.type === "text" ? turnContextManifestFromText(manifestInput.text, PANEL_SUBMISSION_ID) : null; expect(manifest?.contexts.map((context) => context.parts)).toEqual([7]); }); diff --git a/tests/app-server/transport.test.ts b/tests/app-server/transport.test.ts index bd658206..8f0bbe71 100644 --- a/tests/app-server/transport.test.ts +++ b/tests/app-server/transport.test.ts @@ -1,12 +1,8 @@ import { EventEmitter } from "node:events"; import { PassThrough } from "node:stream"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { - type AppServerTransportHandlers, - createAppServerSpawnSpec, - StdioAppServerTransport, -} from "../../src/app-server/connection/transport"; +import { type AppServerTransportHandlers, StdioAppServerTransport } from "../../src/app-server/connection/transport"; const spawnMock = vi.hoisted(() => vi.fn()); @@ -19,42 +15,16 @@ interface TestableTransport { flushStderr(): void; } -describe("createAppServerSpawnSpec", () => { - it("starts regular commands directly", () => { - expect(createAppServerSpawnSpec("codex", { platform: "darwin" })).toEqual({ - command: "codex", - args: ["app-server"], - killProcessTreeOnStop: false, - }); - }); - - it("starts Windows executables directly", () => { - const codexExe = String.raw`C:\Program Files\Codex\codex.exe`; - - expect(createAppServerSpawnSpec(codexExe, { platform: "win32" })).toEqual({ - command: codexExe, - args: ["app-server"], - killProcessTreeOnStop: false, - }); - }); - - it("quotes Windows command shim paths that contain spaces", () => { - const codexBat = String.raw`C:\Program Files\nodejs\codex.bat`; - - expect(createAppServerSpawnSpec(codexBat, { platform: "win32", comSpec: " " })).toEqual({ - command: "cmd.exe", - args: ["/d", "/s", "/c", `""${codexBat}" app-server"`], - killProcessTreeOnStop: true, - windowsVerbatimArguments: true, - }); - }); -}); - describe("StdioAppServerTransport", () => { beforeEach(() => { spawnMock.mockReset(); }); + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + }); + it("starts once, forwards stdout lines, and reports its running state", () => { const child = fakeChildProcess(); spawnMock.mockReturnValue(child); @@ -73,6 +43,38 @@ describe("StdioAppServerTransport", () => { expect(() => instance.start()).toThrow("Codex app-server is already running."); }); + it("starts Windows executables directly", () => { + vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + const child = fakeChildProcess(); + const codexExe = String.raw`C:\Program Files\Codex\codex.exe`; + spawnMock.mockReturnValue(child); + + transportInstance(codexExe).instance.start(); + + expect(spawnMock).toHaveBeenCalledWith(codexExe, ["app-server"], { + cwd: "/vault", + stdio: ["pipe", "pipe", "pipe"], + windowsVerbatimArguments: undefined, + }); + }); + + it("quotes Windows command shim paths that contain spaces", () => { + vi.spyOn(process, "platform", "get").mockReturnValue("win32"); + vi.stubEnv("ComSpec", " "); + vi.stubEnv("COMSPEC", " "); + const child = fakeChildProcess(); + const codexBat = String.raw`C:\Program Files\nodejs\codex.bat`; + spawnMock.mockReturnValue(child); + + transportInstance(codexBat).instance.start(); + + expect(spawnMock).toHaveBeenCalledWith("cmd.exe", ["/d", "/s", "/c", `""${codexBat}" app-server"`], { + cwd: "/vault", + stdio: ["pipe", "pipe", "pipe"], + windowsVerbatimArguments: true, + }); + }); + it("serializes outbound messages and reports asynchronous write failures", () => { const child = fakeChildProcess(); spawnMock.mockReturnValue(child); @@ -195,7 +197,7 @@ describe("StdioAppServerTransport", () => { }); }); -function transportInstance() { +function transportInstance(codexPath = "codex") { const handlers: AppServerTransportHandlers = { onLine: vi.fn(), onLog: vi.fn(), @@ -204,7 +206,7 @@ function transportInstance() { }; return { handlers, - instance: new StdioAppServerTransport("codex", "/vault", handlers), + instance: new StdioAppServerTransport(codexPath, "/vault", handlers), }; } diff --git a/tests/domain/chat/context-manifest.test.ts b/tests/domain/chat/context-manifest.test.ts index e1021961..7b4fe8c7 100644 --- a/tests/domain/chat/context-manifest.test.ts +++ b/tests/domain/chat/context-manifest.test.ts @@ -1,14 +1,19 @@ import { describe, expect, it } from "vitest"; -import { - type TurnContextManifest, - turnContextManifestFromText, - turnContextManifestText, - userMessageContextProjection, -} from "../../../src/domain/chat/context-manifest"; +import { type TurnContextManifest, turnContextManifestText, userMessageContextProjection } from "../../../src/domain/chat/context-manifest"; const SUBMISSION_ID = "local-user-1-seed-1-1"; +function turnContextManifestFromText(text: string): TurnContextManifest | null { + return userMessageContextProjection( + [ + { type: "text", text: "visible request" }, + { type: "text", text: text.startsWith("\n") ? text : `\n${text}` }, + ], + SUBMISSION_ID, + ).manifest; +} + function webEntry(): Record { return { kind: "web", diff --git a/tests/domain/runtime/config.test.ts b/tests/domain/runtime/config.test.ts index 3f1a2340..83830f60 100644 --- a/tests/domain/runtime/config.test.ts +++ b/tests/domain/runtime/config.test.ts @@ -1,10 +1,11 @@ import { describe, expect, it } from "vitest"; -import { emptyRuntimeConfigSnapshot, runtimeConfigOrDefault } from "../../../src/domain/runtime/config"; +import { runtimeConfigOrDefault } from "../../../src/domain/runtime/config"; +import { runtimeConfigFixture } from "../../support/runtime-config"; describe("runtime config", () => { it("clones nested permission policy state", () => { const config = { - ...emptyRuntimeConfigSnapshot(), + ...runtimeConfigFixture(), startupPermissions: { activePermissionProfile: null, sandboxPolicy: null, diff --git a/tests/domain/threads/rename-lifecycle.test.ts b/tests/domain/threads/rename-lifecycle.test.ts index df7342a4..263864cf 100644 --- a/tests/domain/threads/rename-lifecycle.test.ts +++ b/tests/domain/threads/rename-lifecycle.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from "vitest"; import { initialThreadRenameLifecycleState, - type ThreadRenameGeneratingState, type ThreadRenameLifecycleState, transitionThreadRenameLifecycleState, } from "../../../src/domain/threads/rename-lifecycle"; @@ -41,6 +40,8 @@ describe("thread rename lifecycle", () => { }); }); +type ThreadRenameGeneratingState = Extract; + function generatingRenameState(draft: string, generationToken: number): ThreadRenameGeneratingState { const editing = expectRenameState(transitionThreadRenameLifecycleState(initialThreadRenameLifecycleState(), { type: "started", draft })); const generating = transitionThreadRenameLifecycleState(editing, { type: "generation-started", generationToken }); diff --git a/tests/features/chat/app-server/transports.test.ts b/tests/features/chat/app-server/transports.test.ts index 7b7d64ce..bdc04565 100644 --- a/tests/features/chat/app-server/transports.test.ts +++ b/tests/features/chat/app-server/transports.test.ts @@ -4,7 +4,7 @@ import type { AppServerClient, ClientResponseByMethod } from "../../../../src/ap import * as shortLivedClient from "../../../../src/app-server/connection/short-lived-client"; import type { ThreadRecord } from "../../../../src/app-server/protocol/thread"; import type { TurnItem, TurnRecord } from "../../../../src/app-server/protocol/turn"; -import { turnContextManifestFromText } from "../../../../src/domain/chat/context-manifest"; +import { userMessageContextProjection } from "../../../../src/domain/chat/context-manifest"; import type { CodexInput } from "../../../../src/domain/chat/input"; import { createServerDiagnostics, diagnosticProbeOk } from "../../../../src/domain/server/diagnostics"; import { createChatAppServerGateway, createChatCurrentAppServerGateway } from "../../../../src/features/chat/app-server/session-gateway"; @@ -126,21 +126,31 @@ describe("chat app-server transports", () => { await transport.startTurn({ threadId: "thread", input: prepared.input, - clientUserMessageId: "local-user", + clientUserMessageId: "local-user-1-seed-1-1", }); const [, params] = request.mock.calls[0] ?? []; expect(params).toMatchObject({ threadId: "thread", cwd: "/vault", - clientUserMessageId: "local-user", + clientUserMessageId: "local-user-1-seed-1-1", }); expect(params?.input[0]).toEqual({ type: "text", text, text_elements: [] }); expect(params?.input).toHaveLength(2); const descriptor = params?.input.at(-1); - expect(descriptor?.type === "text" ? turnContextManifestFromText(descriptor.text) : null).toEqual({ + expect( + descriptor?.type === "text" + ? userMessageContextProjection( + [ + { type: "text", text }, + { type: "text", text: descriptor.text }, + ], + "local-user-1-seed-1-1", + ).manifest + : null, + ).toEqual({ version: 2, - submissionId: "local-user", + submissionId: "local-user-1-seed-1-1", contexts: [], fileReferences: [ { name: "Alpha", path: "notes/Alpha.md" }, @@ -148,7 +158,7 @@ describe("chat app-server transports", () => { ], }); expect(params?.additionalContext).toEqual({ - "codex_panel.local-user.00.codex_panel_obsidian_context.part_01_of_01": { + "codex_panel.local-user-1-seed-1-1.00.codex_panel_obsidian_context.part_01_of_01": { kind: "untrusted", value: "Codex Panel context part 1/1.\nSource: codex_panel_obsidian_context\n\nObsidian references for the current user input:\n- [[Alpha]] -> notes/Alpha.md\n- -> notes/Alpha.md", diff --git a/tests/features/chat/application/composer/slash-commands.test.ts b/tests/features/chat/application/composer/slash-commands.test.ts index 0c960469..b085630c 100644 --- a/tests/features/chat/application/composer/slash-commands.test.ts +++ b/tests/features/chat/application/composer/slash-commands.test.ts @@ -3,7 +3,6 @@ import { describe, expect, it } from "vitest"; import { SLASH_COMMANDS, type SlashCommandName, - slashCommandAvailableInSideChat, slashCommandHelpSections, slashCommandRequiresConnection, } from "../../../../../src/features/chat/application/composer/slash-commands"; @@ -28,16 +27,11 @@ describe("slash command catalog", () => { expect(keys("Composition")).toEqual(["/refer ", "/web [message]"]); }); - it("owns connection and side-chat availability metadata", () => { + it("owns connection availability metadata", () => { expect( SLASH_COMMANDS.filter((definition) => !slashCommandRequiresConnection(definition.command.slice(1) as SlashCommandName)).map( (item) => item.command, ), ).toEqual(["/reconnect", "/compact"]); - expect( - SLASH_COMMANDS.filter((definition) => !slashCommandAvailableInSideChat(definition.command.slice(1) as SlashCommandName)).map( - (item) => item.command, - ), - ).toEqual(["/fork", "/btw", "/rollback", "/auto-review", "/fast", "/plan", "/goal"]); }); }); diff --git a/tests/features/chat/application/composer/suggestions.test.ts b/tests/features/chat/application/composer/suggestions.test.ts index 0a9aca8f..ace41acb 100644 --- a/tests/features/chat/application/composer/suggestions.test.ts +++ b/tests/features/chat/application/composer/suggestions.test.ts @@ -1,8 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import type { ModelMetadata, ReasoningEffort, SkillMetadata } from "../../../../../src/domain/catalog/metadata"; import type { Thread } from "../../../../../src/domain/threads/model"; -import { emptyComposerContextReferences } from "../../../../../src/features/chat/application/composer/context-references"; -import { slashCommandAvailableInSideChat } from "../../../../../src/features/chat/application/composer/slash-commands"; +import type { ComposerContextReferences } from "../../../../../src/features/chat/application/composer/context-references"; import { activeComposerSuggestions, applyComposerSuggestionInsertion, @@ -25,6 +24,10 @@ function wikiLinkSuggestions(query: string, notes: Parameters { expect(activeComposerSuggestions("/help", notes, [])).toEqual([]); }); - it("omits unavailable thread mutations from side-chat slash suggestions", () => { - const options = { slashCommandAvailable: slashCommandAvailableInSideChat }; - - expect(suggestionReplacements(activeComposerSuggestions("/f", notes, [], [], [], null, options))).not.toContain("/fork"); - expect(suggestionReplacements(activeComposerSuggestions("/r", notes, [], [], [], null, options))).not.toContain("/rollback"); - expect(suggestionReplacements(activeComposerSuggestions("/b", notes, [], [], [], null, options))).not.toContain("/btw"); - expect(suggestionReplacements(activeComposerSuggestions("/c", notes, [], [], [], null, options))).toContain("/compact"); - expect(suggestionReplacements(activeComposerSuggestions("/g", notes, [], [], [], null, options))).not.toContain("/goal"); - }); - it("omits slash suggestions from subagent threads", () => { const options = { slashCommandAvailable: () => false }; expect(activeComposerSuggestions("/", notes, [], [], [], null, options)).toEqual([]); diff --git a/tests/features/chat/application/composer/wikilink-context.test.ts b/tests/features/chat/application/composer/wikilink-context.test.ts index 13c90bfe..bd116c83 100644 --- a/tests/features/chat/application/composer/wikilink-context.test.ts +++ b/tests/features/chat/application/composer/wikilink-context.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import type { SkillMetadata } from "../../../../../src/domain/catalog/metadata"; import type { CodexInput } from "../../../../../src/domain/chat/input"; -import { emptyComposerContextReferences } from "../../../../../src/features/chat/application/composer/context-references"; +import type { ComposerContextReferences } from "../../../../../src/features/chat/application/composer/context-references"; import { preparedUserInputWithWikiLinkReferencesSkillsAndContext, type WikiLinkFileReferenceResolver, @@ -17,6 +17,10 @@ const obsidianContext = (...sections: string[]) => ({ const wikilinkContext = (...mappings: string[]) => obsidianContext(...mappings); +function emptyComposerContextReferences(): ComposerContextReferences { + return { activeNote: null, selection: null, activeNoteSnapshots: [], selectionSnapshots: [] }; +} + function userInputWithWikiLinkReferencesAndSkills( text: string, resolveFileReference: WikiLinkFileReferenceResolver, diff --git a/tests/features/chat/application/connection/connection-actions.test.ts b/tests/features/chat/application/connection/connection-actions.test.ts index e2238139..21e35cc3 100644 --- a/tests/features/chat/application/connection/connection-actions.test.ts +++ b/tests/features/chat/application/connection/connection-actions.test.ts @@ -1,16 +1,16 @@ import { describe, expect, it, vi } from "vitest"; - -import { emptyRuntimeConfigSnapshot } from "../../../../../src/domain/runtime/config"; import { type ChatConnectionActionsHost, - type ChatConnectionAdapter, - type ChatConnectionDiagnosticsActions, - type ChatConnectionMetadataActions, createChatConnectionActions, } from "../../../../../src/features/chat/application/connection/connection-actions"; import { createChatState } from "../../../../../src/features/chat/application/state/root-reducer"; import { createChatStateStore } from "../../../../../src/features/chat/application/state/store"; import { deferred } from "../../../../support/async"; +import { runtimeConfigFixture } from "../../../../support/runtime-config"; + +type ChatConnectionAdapter = ChatConnectionActionsHost["connection"]; +type ChatConnectionMetadataActions = ChatConnectionActionsHost["metadata"]; +type ChatConnectionDiagnosticsActions = ChatConnectionActionsHost["diagnostics"]; function createActionsHarness({ connected = false, canConnect = true } = {}) { const stateStore = createChatStateStore(createChatState()); @@ -183,7 +183,7 @@ describe("ChatConnectionActions", () => { it("clears disconnected connection state on server exit while keeping last startup metadata", () => { const { actions, host, stateStore } = createActionsHarness({ connected: true }); const initializeResponse = { codexHome: "/codex", platformFamily: "unix", platformOs: "macos", userAgent: "test" } as const; - const runtimeConfig = { ...emptyRuntimeConfigSnapshot(), model: "gpt-5.1" }; + const runtimeConfig = { ...runtimeConfigFixture(), model: "gpt-5.1" }; stateStore.dispatch({ type: "connection/initialized", initializeResponse }); stateStore.dispatch({ type: "thread-list/applied", diff --git a/tests/features/chat/application/connection/server-actions.test.ts b/tests/features/chat/application/connection/server-actions.test.ts index c2d9f57c..384a3ab8 100644 --- a/tests/features/chat/application/connection/server-actions.test.ts +++ b/tests/features/chat/application/connection/server-actions.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import type { SkillMetadata } from "../../../../../src/domain/catalog/metadata"; -import { emptyRuntimeConfigSnapshot } from "../../../../../src/domain/runtime/config"; import { createServerDiagnostics, type DiagnosticProbeResult, @@ -18,6 +17,7 @@ import { createServerMetadataActions } from "../../../../../src/features/chat/ap import { createChatStateStore } from "../../../../../src/features/chat/application/state/store"; import { toolInventoryDiagnosticSections } from "../../../../../src/features/chat/presentation/runtime/tool-inventory-diagnostic-sections"; import { deferred } from "../../../../support/async"; +import { runtimeConfigFixture } from "../../../../support/runtime-config"; import { chatStateFixture, chatStateWith } from "../../support/state"; describe("server metadata actions", () => { @@ -408,7 +408,7 @@ function skillFixture(name: string): SkillMetadata { function serverMetadataFixture(overrides: Partial = {}): SharedServerMetadata { return { - runtimeConfig: emptyRuntimeConfigSnapshot(), + runtimeConfig: runtimeConfigFixture(), availableSkills: [], availablePermissionProfiles: [], rateLimit: null, diff --git a/tests/features/chat/application/panel-operation-policy.test.ts b/tests/features/chat/application/panel-operation-policy.test.ts index db125739..576f3309 100644 --- a/tests/features/chat/application/panel-operation-policy.test.ts +++ b/tests/features/chat/application/panel-operation-policy.test.ts @@ -1,9 +1,15 @@ import { describe, expect, it } from "vitest"; -import { - type ActivePanelOperation, - type ActivePanelThreadFacts, - activePanelOperationDecisionForFacts, -} from "../../../../src/features/chat/application/panel-operation-policy"; +import { type ActivePanelOperation, activePanelOperationDecision } from "../../../../src/features/chat/application/panel-operation-policy"; +import { type ChatState, createChatState } from "../../../../src/features/chat/application/state/root-reducer"; + +type ActivePanelThreadFacts = + | { readonly phase: "empty" } + | { readonly phase: "awaiting-resume"; readonly provenance: "interactive" | "subagent" | null } + | { + readonly phase: "active"; + readonly lifetime: "persistent" | "ephemeral"; + readonly provenance: "interactive" | "subagent" | null; + }; const operations: readonly ActivePanelOperation[] = [ "submit", @@ -39,13 +45,13 @@ describe("activePanelOperationDecisionForFacts", () => { }); it("requires restoration before a persistent goal mutation", () => { - expect(activePanelOperationDecisionForFacts({ phase: "awaiting-resume", provenance: "interactive" }, "goal-mutation")).toEqual({ + expect(activePanelOperationDecision(stateFromFacts({ phase: "awaiting-resume", provenance: "interactive" }), "goal-mutation")).toEqual({ kind: "resume-required", }); }); it("keeps restored subagent goals read-only before loading", () => { - expect(activePanelOperationDecisionForFacts({ phase: "awaiting-resume", provenance: "subagent" }, "goal-mutation")).toEqual({ + expect(activePanelOperationDecision(stateFromFacts({ phase: "awaiting-resume", provenance: "subagent" }), "goal-mutation")).toEqual({ kind: "blocked", message: "Goals are read-only in agent threads.", }); @@ -69,6 +75,47 @@ function decisions(allowed: Partial>): R function expectDecisionKinds(facts: ActivePanelThreadFacts, expected: Record): void { for (const operation of operations) { - expect(activePanelOperationDecisionForFacts(facts, operation).kind).toBe(expected[operation]); + expect(activePanelOperationDecision(stateFromFacts(facts), operation).kind).toBe(expected[operation]); } } + +function stateFromFacts(facts: ActivePanelThreadFacts): ChatState { + const state = createChatState(); + if (facts.phase === "empty") return state; + const provenance = + facts.provenance === null + ? null + : facts.provenance === "interactive" + ? ({ kind: "interactive" } as const) + : ({ + kind: "subagent", + subagentKind: "other", + parentThreadId: null, + sessionId: null, + depth: null, + agentNickname: null, + agentRole: null, + } as const); + if (facts.phase === "awaiting-resume") { + return { + ...state, + panelThread: { kind: "awaiting-resume", threadId: "thread", fallbackTitle: null, provenance }, + }; + } + return { + ...state, + panelThread: { + kind: "active", + thread: { + id: "thread", + goal: null, + tokenUsage: null, + lifetime: + facts.lifetime === "persistent" + ? { kind: "persistent" } + : { kind: "ephemeral", sourceThreadId: "source", sourceThreadTitle: null }, + provenance, + }, + }, + }; +} diff --git a/tests/features/chat/application/runtime/settings-actions.test.ts b/tests/features/chat/application/runtime/settings-actions.test.ts index 835a95bd..36f76272 100644 --- a/tests/features/chat/application/runtime/settings-actions.test.ts +++ b/tests/features/chat/application/runtime/settings-actions.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it, vi } from "vitest"; import type { ModelMetadata } from "../../../../../src/domain/catalog/metadata"; -import { emptyRuntimeConfigSnapshot } from "../../../../../src/domain/runtime/config"; import { type ChatRuntimeSettingsActions, createChatRuntimeSettingsActions, @@ -12,6 +11,7 @@ import { activeThreadId, type ChatState } from "../../../../../src/features/chat import { createChatStateStore } from "../../../../../src/features/chat/application/state/store"; import { setCollaborationModeIntent, setRuntimeIntentValue } from "../../../../../src/features/chat/domain/runtime/intent"; import { createKeyedOperationQueue, type KeyedOperationQueue } from "../../../../../src/shared/runtime/keyed-operation-queue"; +import { runtimeConfigFixture } from "../../../../support/runtime-config"; import { chatStateFixture, chatStateWith } from "../../support/state"; describe("createChatRuntimeSettingsActions", () => { @@ -234,7 +234,7 @@ describe("createChatRuntimeSettingsActions", () => { it("keeps Fast disabled after clearing a thread tier when config defaults to Fast", async () => { let state = chatStateFixture(); state = chatStateWith(state, { activeThread: { id: "thread" } }); - state = chatStateWith(state, { connection: { runtimeConfig: { ...emptyRuntimeConfigSnapshot(), serviceTier: "fast" } } }); + state = chatStateWith(state, { connection: { runtimeConfig: { ...runtimeConfigFixture(), serviceTier: "fast" } } }); state = chatStateWith(state, { runtime: { active: { serviceTier: "fast" } } }); const store = createChatStateStore(state); const transport = settingsTransportFixture(); @@ -318,7 +318,7 @@ describe("createChatRuntimeSettingsActions", () => { state = chatStateWith(state, { connection: { runtimeConfig: { - ...emptyRuntimeConfigSnapshot(), + ...runtimeConfigFixture(), model: "gpt-config", reasoningEffort: "medium", }, diff --git a/tests/features/chat/application/state/root-reducer.test.ts b/tests/features/chat/application/state/root-reducer.test.ts index 59d5c894..277a0c09 100644 --- a/tests/features/chat/application/state/root-reducer.test.ts +++ b/tests/features/chat/application/state/root-reducer.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; -import { emptyRuntimeConfigSnapshot } from "../../../../../src/domain/runtime/config"; import { createServerDiagnostics, diagnosticProbeOk, diagnosticsWithProbe } from "../../../../../src/domain/server/diagnostics"; import type { ThreadGoal } from "../../../../../src/domain/threads/goal"; import type { Thread } from "../../../../../src/domain/threads/model"; @@ -11,6 +10,7 @@ import { activeTurnId, chatTurnBusy, pendingTurnStart } from "../../../../../src import { pendingWebSubmissionItem } from "../../../../../src/features/chat/application/turns/web-submission"; import { setCollaborationModeIntent, setRuntimeIntentValue } from "../../../../../src/features/chat/domain/runtime/intent"; import type { ThreadStreamItem } from "../../../../../src/features/chat/domain/thread-stream/items"; +import { runtimeConfigFixture } from "../../../../support/runtime-config"; import { chatStateFixture, chatStateWith } from "../../support/state"; import { chatStateThreadStreamItems, withChatStateThreadStreamItems } from "../../support/thread-stream"; @@ -227,7 +227,7 @@ describe("chatReducer", () => { }; const state = chatStateWith(chatStateFixture(), { connection: { - runtimeConfig: { ...emptyRuntimeConfigSnapshot(), model: "gpt-old" }, + runtimeConfig: { ...runtimeConfigFixture(), model: "gpt-old" }, initializeResponse: { codexHome: "/old/codex-home", platformFamily: "unix", @@ -264,7 +264,7 @@ describe("chatReducer", () => { const serverDiagnostics = diagnosticsWithProbe(createServerDiagnostics(), diagnosticProbeOk("models", "1 model", 1)); let state = chatStateWith(chatStateFixture(), { connection: { - runtimeConfig: { ...emptyRuntimeConfigSnapshot(), model: "gpt-old" }, + runtimeConfig: { ...runtimeConfigFixture(), model: "gpt-old" }, initializeResponse: { codexHome: "/old/codex-home", platformFamily: "unix", diff --git a/tests/features/chat/application/threads/thread-start-actions.test.ts b/tests/features/chat/application/threads/thread-start-actions.test.ts index e072a27b..f8e831b8 100644 --- a/tests/features/chat/application/threads/thread-start-actions.test.ts +++ b/tests/features/chat/application/threads/thread-start-actions.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; -import { emptyRuntimeConfigSnapshot } from "../../../../../src/domain/runtime/config"; import type { ThreadActivationSnapshot } from "../../../../../src/domain/threads/activation"; import type { Thread } from "../../../../../src/domain/threads/model"; import type { EffectOutcome } from "../../../../../src/features/chat/application/effect-outcome"; @@ -11,6 +10,7 @@ import { createThreadStartActions } from "../../../../../src/features/chat/appli import { pendingWebSubmissionItem } from "../../../../../src/features/chat/application/turns/web-submission"; import { setCollaborationModeIntent } from "../../../../../src/features/chat/domain/runtime/intent"; import { deferred } from "../../../../support/async"; +import { runtimeConfigFixture } from "../../../../support/runtime-config"; import { chatStateFixture, chatStateWith } from "../../support/state"; describe("thread start actions", () => { @@ -161,7 +161,7 @@ describe("thread start actions", () => { it("starts threads with service tier from explicit effective config", async () => { let state = chatStateFixture(); - state = chatStateWith(state, { connection: { runtimeConfig: { ...emptyRuntimeConfigSnapshot(), serviceTier: "flex" } } }); + state = chatStateWith(state, { connection: { runtimeConfig: { ...runtimeConfigFixture(), serviceTier: "flex" } } }); const stateStore = createChatStateStore(state); const startThread = vi .fn() @@ -184,9 +184,9 @@ describe("thread start actions", () => { state = chatStateWith(state, { connection: { runtimeConfig: { - ...emptyRuntimeConfigSnapshot(), + ...runtimeConfigFixture(), startupPermissions: { - ...emptyRuntimeConfigSnapshot().startupPermissions, + ...runtimeConfigFixture().startupPermissions, activePermissionProfile: { id: ":workspace", extends: null }, }, }, diff --git a/tests/features/chat/application/turns/plan-implementation.test.ts b/tests/features/chat/application/turns/plan-implementation.test.ts index fb2e7d43..4eb18b67 100644 --- a/tests/features/chat/application/turns/plan-implementation.test.ts +++ b/tests/features/chat/application/turns/plan-implementation.test.ts @@ -1,9 +1,10 @@ import { describe, expect, it, vi } from "vitest"; -import { createChatState } from "../../../../../src/features/chat/application/state/root-reducer"; +import { activePanelOperationDecision } from "../../../../../src/features/chat/application/panel-operation-policy"; +import { activeThreadState, createChatState } from "../../../../../src/features/chat/application/state/root-reducer"; import { type ChatStateStore, createChatStateStore } from "../../../../../src/features/chat/application/state/store"; import { implementPlan, - implementPlanTargetFromState, + implementPlanTarget, type PlanImplementationHost, } from "../../../../../src/features/chat/application/turns/plan-implementation"; import { setCollaborationModeIntent } from "../../../../../src/features/chat/domain/runtime/intent"; @@ -77,6 +78,16 @@ function createPlanImplementationHost() { }; } +function implementPlanTargetFromState(state: ReturnType) { + return implementPlanTarget({ + activeThread: activeThreadState(state), + modeAllowed: activePanelOperationDecision(state, "implement-plan").kind === "allowed", + turn: state.turn, + runtime: state.runtime, + threadStream: state.threadStream, + }); +} + describe("implementPlan", () => { it("finds the latest proposed plan only when the thread is idle and in plan mode", () => { const stateStore = createChatStateStore(createChatState()); diff --git a/tests/features/chat/host/connection-bundle.test.ts b/tests/features/chat/host/connection-bundle.test.ts index 8a59f0c9..bfde69f7 100644 --- a/tests/features/chat/host/connection-bundle.test.ts +++ b/tests/features/chat/host/connection-bundle.test.ts @@ -1,79 +1,156 @@ import { describe, expect, it, vi } from "vitest"; +import type { ConnectionManagerHandlers } from "../../../../src/app-server/connection/connection-manager"; +import type { ServerRequest } from "../../../../src/app-server/connection/rpc-messages"; +import { createChatStateStore } from "../../../../src/features/chat/application/state/store"; +import { createConnectionBundle } from "../../../../src/features/chat/host/bundles/connection-bundle"; +import { chatStateFixture } from "../support/state"; -import { - createServerRequestResponderRegistry, - scheduleDeferredDiagnosticsRefresh, -} from "../../../../src/features/chat/host/bundles/connection-bundle"; +type ConnectionBundleHost = Parameters[0]; +type ConnectionBundleInput = Parameters[1]; -describe("connection bundle server request responders", () => { - it("responds through the client that delivered the request", () => { - const currentClient = { respondToServerRequest: vi.fn() }; - const originalResponder = { respond: vi.fn(), reject: vi.fn() }; - const registry = createServerRequestResponderRegistry(); - registry.remember(1, originalResponder); +describe("connection bundle", () => { + it("responds through the responder that delivered the request", async () => { + const fixture = connectionBundleFixture(); + await fixture.bundle.connection.actions.ensureConnected(); + const responder = { respond: vi.fn(), reject: vi.fn() }; - registry.respond(1, { decision: "accept" }); + fixture.connectionHandlers().onServerRequest(userInputRequest(1), responder); + fixture.bundle.inboundHandler.resolveUserInput(1, { note: "Continue" }); - expect(originalResponder.respond).toHaveBeenCalledWith({ decision: "accept" }); - expect(currentClient.respondToServerRequest).not.toHaveBeenCalled(); + expect(responder.respond).toHaveBeenCalledWith({ answers: { note: { answers: ["Continue"] } } }); }); - it("cannot reuse a responder after the connection generation is cleared", () => { + it("cannot reuse a responder after its connection scope is invalidated", async () => { + const fixture = connectionBundleFixture(); + await fixture.bundle.connection.actions.ensureConnected(); const responder = { respond: vi.fn(), reject: vi.fn() }; - const registry = createServerRequestResponderRegistry(); - registry.remember(1, responder); - registry.clear(); - expect(registry.respond(1, { decision: "accept" })).toBe(false); - expect(registry.reject(1, -32000, "cancelled")).toBe(false); + fixture.connectionHandlers().onServerRequest(userInputRequest(1), responder); + fixture.bundle.invalidateConnectionScope(); + fixture.bundle.inboundHandler.resolveUserInput(1, { note: "Continue" }); + expect(responder.respond).not.toHaveBeenCalled(); expect(responder.reject).not.toHaveBeenCalled(); }); -}); -describe("connection bundle deferred diagnostics", () => { - it("reports deferred diagnostics failures without returning a blocking promise", async () => { - const callbacks: Array<() => void> = []; + it("reports deferred diagnostics failures as system messages", async () => { const error = new Error("diagnostics failed"); - const refreshServerDiagnostics = vi.fn().mockRejectedValue(error); - const addSystemMessage = vi.fn(); - - scheduleDeferredDiagnosticsRefresh({ - scheduleDiagnostics: (scheduled) => { - callbacks.push(scheduled); - }, - isConnected: () => true, - refreshServerDiagnostics, - addSystemMessage, + const fixture = connectionBundleFixture({ + readServerDiagnostics: vi.fn().mockRejectedValue(error), }); + await fixture.bundle.connection.actions.ensureConnected(); - expect(callbacks).toHaveLength(1); - const scheduled = callbacks[0]; - if (!scheduled) throw new Error("Expected deferred diagnostics callback."); - scheduled(); - await Promise.resolve(); - - expect(refreshServerDiagnostics).toHaveBeenCalledWith({ appServerMetadataSnapshot: true }); - expect(addSystemMessage).toHaveBeenCalledWith("diagnostics failed"); + fixture.runScheduledDiagnostics(); + await vi.waitFor(() => { + expect(fixture.addSystemMessage).toHaveBeenCalledWith("diagnostics failed"); + }); }); - it("does not refresh deferred diagnostics after disconnect", () => { - const callbacks: Array<() => void> = []; - const refreshServerDiagnostics = vi.fn().mockResolvedValue(undefined); + it("does not run deferred diagnostics after disconnect", async () => { + const readServerDiagnostics = vi.fn().mockResolvedValue(null); + const fixture = connectionBundleFixture({ readServerDiagnostics }); + await fixture.bundle.connection.actions.ensureConnected(); + fixture.setConnected(false); - scheduleDeferredDiagnosticsRefresh({ - scheduleDiagnostics: (scheduled) => { - callbacks.push(scheduled); - }, - isConnected: () => false, - refreshServerDiagnostics, - addSystemMessage: vi.fn(), - }); + fixture.runScheduledDiagnostics(); - const scheduled = callbacks[0]; - if (!scheduled) throw new Error("Expected deferred diagnostics callback."); - scheduled(); - - expect(refreshServerDiagnostics).not.toHaveBeenCalled(); + expect(readServerDiagnostics).not.toHaveBeenCalled(); }); }); + +function connectionBundleFixture(overrides: { readServerDiagnostics?: ReturnType } = {}) { + let connected = false; + let handlers: ConnectionManagerHandlers | null = null; + let scheduledDiagnostics: (() => void) | null = null; + const addSystemMessage = vi.fn(); + const stateStore = createChatStateStore( + chatStateFixture({ + activeThread: { id: "thread-active" }, + turn: { lifecycle: { kind: "running", turnId: "turn-active" } }, + }), + ); + const host = { + environment: { + plugin: { + appServerQueries: { + appServerMetadataSnapshot: () => null, + refreshAppServerMetadata: vi.fn().mockResolvedValue(undefined), + refreshSkills: vi.fn().mockResolvedValue(undefined), + refreshRateLimits: vi.fn().mockResolvedValue(undefined), + }, + threadCatalog: { + refreshActive: vi.fn().mockResolvedValue(undefined), + apply: vi.fn(), + }, + settingsRef: { + settings: { + codexPath: () => "codex", + }, + }, + }, + }, + stateStore, + canConnect: () => true, + deferredTasks: { + scheduleDiagnostics: (callback: () => void) => { + scheduledDiagnostics = callback; + }, + clearDiagnostics: vi.fn(), + }, + invalidateThreadWork: vi.fn(), + refreshTabHeader: vi.fn(), + } as unknown as ConnectionBundleHost; + const input = { + connection: { + connect: async (nextHandlers: ConnectionManagerHandlers) => { + handlers = nextHandlers; + connected = true; + return { codexHome: "/codex", platformFamily: "unix", platformOs: "macos", userAgent: "test" }; + }, + isConnected: () => connected, + }, + diagnosticsTransport: { + readServerDiagnostics: overrides.readServerDiagnostics ?? vi.fn().mockResolvedValue(null), + }, + localItemIds: { + next: (prefix: string) => `${prefix}-1`, + }, + status: { + set: vi.fn(), + addSystemMessage, + }, + autoTitleCoordinator: { + maybeAutoTitleThread: vi.fn(), + resetThreadTurnPresence: vi.fn(), + }, + } as unknown as ConnectionBundleInput; + return { + addSystemMessage, + bundle: createConnectionBundle(host, input), + connectionHandlers: () => { + if (!handlers) throw new Error("Expected connection handlers."); + return handlers; + }, + runScheduledDiagnostics: () => { + if (!scheduledDiagnostics) throw new Error("Expected deferred diagnostics callback."); + scheduledDiagnostics(); + }, + setConnected: (value: boolean) => { + connected = value; + }, + }; +} + +function userInputRequest(id: number): Extract { + return { + id, + method: "item/tool/requestUserInput", + params: { + threadId: "thread-active", + turnId: "turn-active", + itemId: "input-1", + questions: [{ id: "note", header: "Note", question: "What now?", isOther: false, isSecret: false, options: null }], + autoResolutionMs: null, + }, + }; +} diff --git a/tests/features/chat/host/vault-note-candidate-provider.test.ts b/tests/features/chat/host/vault-note-candidate-provider.test.ts index 25d5fbc8..e40d2763 100644 --- a/tests/features/chat/host/vault-note-candidate-provider.test.ts +++ b/tests/features/chat/host/vault-note-candidate-provider.test.ts @@ -1,5 +1,5 @@ import { type App, type EventRef, TFile } from "obsidian"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const dailyNotesInterface = vi.hoisted(() => ({ appHasDailyNotesPluginLoaded: vi.fn<() => boolean>(), @@ -9,10 +9,7 @@ const dailyNotesInterface = vi.hoisted(() => ({ vi.mock("obsidian-daily-notes-interface", () => dailyNotesInterface); import { VaultComposerContextReferenceProvider } from "../../../../src/features/chat/host/obsidian/vault-composer-context-reference-provider.obsidian"; -import { - configuredDailyNoteReferences, - dailyNoteReferencesFromSettings, -} from "../../../../src/features/chat/host/obsidian/vault-daily-note-references.obsidian"; +import { configuredDailyNoteReferences } from "../../../../src/features/chat/host/obsidian/vault-daily-note-references.obsidian"; import { VaultNoteCandidateProvider } from "../../../../src/features/chat/host/obsidian/vault-note-candidate-provider.obsidian"; describe("VaultNoteCandidateProvider", () => { @@ -21,6 +18,10 @@ describe("VaultNoteCandidateProvider", () => { dailyNotesInterface.getDailyNoteSettings.mockReset().mockReturnValue(undefined); }); + afterEach(() => { + vi.useRealTimers(); + }); + it("keeps optional daily-note integration failures out of composer suggestions", () => { dailyNotesInterface.appHasDailyNotesPluginLoaded.mockImplementation(() => { throw new Error("Daily Notes API unavailable"); @@ -30,6 +31,14 @@ describe("VaultNoteCandidateProvider", () => { }); it("builds relative references from the configured daily-note folder and format", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date(2026, 6, 10, 12)); + dailyNotesInterface.appHasDailyNotesPluginLoaded.mockReturnValue(true); + dailyNotesInterface.getDailyNoteSettings.mockReturnValue({ + folder: "Journal", + format: "YYYY/MM/YYYY-MM-DD", + template: "", + }); const existingToday = tFile("Journal/2026/07/2026-07-10.md", "2026-07-10"); const fileToLinktext = vi.fn((file: TFile) => (file === existingToday ? "2026-07-10" : file.path)); const app = appFixture({ @@ -37,14 +46,7 @@ describe("VaultNoteCandidateProvider", () => { fileToLinktext, }); - expect( - dailyNoteReferencesFromSettings( - app, - "Projects/Codex.md", - { folder: "Journal", format: "YYYY/MM/YYYY-MM-DD", template: "" }, - new Date(2026, 6, 10, 12), - ), - ).toEqual([ + expect(configuredDailyNoteReferences(app, "Projects/Codex.md")).toEqual([ { keyword: "today", display: "Today", diff --git a/tests/features/chat/host/view-connection-harness.ts b/tests/features/chat/host/view-connection-harness.ts index 40a70f13..485a029b 100644 --- a/tests/features/chat/host/view-connection-harness.ts +++ b/tests/features/chat/host/view-connection-harness.ts @@ -6,7 +6,6 @@ import { modelMetadataFromCatalogModels } from "../../../../src/app-server/proto import type { ThreadRecord } from "../../../../src/app-server/protocol/thread"; import type { ObservedPaginatedResult, ObservedResult } from "../../../../src/app-server/query/observed-result"; import type { ModelMetadata } from "../../../../src/domain/catalog/metadata"; -import { emptyRuntimeConfigSnapshot } from "../../../../src/domain/runtime/config"; import { createServerDiagnostics, diagnosticProbeOk } from "../../../../src/domain/server/diagnostics"; import type { SharedServerMetadata, SharedServerMetadataResource } from "../../../../src/domain/server/metadata"; import type { Thread } from "../../../../src/domain/threads/model"; @@ -18,6 +17,7 @@ import { type CodexPanelSettings, DEFAULT_SETTINGS } from "../../../../src/setti import { createKeyedOperationQueue } from "../../../../src/shared/runtime/keyed-operation-queue"; import { notices } from "../../../mocks/obsidian"; import { installObsidianDomShims } from "../../../support/dom"; +import { runtimeConfigFixture } from "../../../support/runtime-config"; import { chatPanelSettingsAccess } from "../support/settings"; export interface TestCodexChatHost extends CodexChatHost { @@ -501,7 +501,7 @@ export function chatHost(overrides: ChatHostFixtureOverrides = {}): TestCodexCha await client.request("account/rateLimits/read", undefined); if (!connectionStillCurrent()) return null; return { - runtimeConfig: emptyRuntimeConfigSnapshot(), + runtimeConfig: runtimeConfigFixture(), availableSkills: skillsResponse.data.flatMap( (entry: { skills: { name: string; description?: string; path?: string; enabled?: boolean }[] }) => entry.skills.map((skill) => ({ diff --git a/tests/features/chat/host/view-restoration.test.ts b/tests/features/chat/host/view-restoration.test.ts index 62200245..117436b4 100644 --- a/tests/features/chat/host/view-restoration.test.ts +++ b/tests/features/chat/host/view-restoration.test.ts @@ -2,10 +2,10 @@ import { describe, expect, it, vi } from "vitest"; import type { ServerNotification } from "../../../../src/app-server/connection/rpc-messages"; -import { emptyRuntimeConfigSnapshot } from "../../../../src/domain/runtime/config"; import { createServerDiagnostics } from "../../../../src/domain/server/diagnostics"; import type { ChatPanelSession } from "../../../../src/features/chat/host/session"; import { deferred, waitForAsyncWork } from "../../../support/async"; +import { runtimeConfigFixture } from "../../../support/runtime-config"; import { chatHost, chatView, @@ -185,7 +185,7 @@ describe("CodexChatView workspace restoration", () => { appServerMetadataSnapshot: vi.fn( () => ({ - runtimeConfig: { ...emptyRuntimeConfigSnapshot(), model: "gpt-cached" }, + runtimeConfig: { ...runtimeConfigFixture(), model: "gpt-cached" }, availableSkills: [{ name: "writer", enabled: true }], availablePermissionProfiles: [], rateLimit: null, diff --git a/tests/features/chat/panel/composer-controller.test.ts b/tests/features/chat/panel/composer-controller.test.ts index 9cc0e87f..41a89216 100644 --- a/tests/features/chat/panel/composer-controller.test.ts +++ b/tests/features/chat/panel/composer-controller.test.ts @@ -14,7 +14,7 @@ import { createChatStateStore } from "../../../../src/features/chat/application/ import { threadStreamItems } from "../../../../src/features/chat/application/state/thread-stream"; import { pendingWebSubmissionItem } from "../../../../src/features/chat/application/turns/web-submission"; import type { ThreadStreamItem } from "../../../../src/features/chat/domain/thread-stream/items"; -import { ChatComposerController, type ChatComposerRenderActions } from "../../../../src/features/chat/panel/composer-controller"; +import { ChatComposerController } from "../../../../src/features/chat/panel/composer-controller"; import type { ChatPanelComposerModel } from "../../../../src/features/chat/panel/shell-selectors"; import { ComposerShell } from "../../../../src/features/chat/ui/composer"; import { renderUiRoot, unmountUiRoot } from "../../../../src/shared/dom/preact-root.dom"; @@ -25,6 +25,8 @@ import { chatStateFixture, chatStateWith } from "../support/state"; installObsidianDomShims(); +type ChatComposerRenderActions = Parameters[1]; + const renderedComposerParents = new Set(); const composerControllerTestCleanups: (() => void)[] = []; @@ -303,7 +305,14 @@ describe("ChatComposerController", () => { expect(controller.captureInputSnapshot().threadCommandTarget).toBeUndefined(); }); - it("applies ephemeral-thread suggestion restrictions on input without waiting for keyup", () => { + it.each([ + ["/f", "/fork", false], + ["/f", "/fast", false], + ["/b", "/btw", false], + ["/r", "/rollback", false], + ["/g", "/goal", false], + ["/c", "/compact", true], + ])("applies ephemeral-thread suggestion policy for %s -> %s", (draft, suggestion, available) => { const stateStore = createChatStateStore( chatStateFixture({ activeThread: { @@ -315,12 +324,16 @@ describe("ChatComposerController", () => { const { controller, parent } = composerControllerFixture({ stateStore }); renderComposerController(parent, controller, stateStore); - setTextAreaValue(composer(parent), "/f"); - composer(parent).setSelectionRange(2, 2); + setTextAreaValue(composer(parent), draft); + composer(parent).setSelectionRange(draft.length, draft.length); composer(parent).dispatchEvent(new Event("input", { bubbles: true })); - expect(stateStore.getState().composer.suggestions.map((suggestion) => suggestion.replacement)).not.toContain("/fork"); - expect(stateStore.getState().composer.suggestions.map((suggestion) => suggestion.replacement)).not.toContain("/fast"); + expect( + stateStore + .getState() + .composer.suggestions.map((item) => item.replacement) + .includes(suggestion), + ).toBe(available); }); it("keeps a disconnected subagent composer read-only without slash suggestions", () => { diff --git a/tests/features/chat/presentation/runtime/diagnostic-sections.test.ts b/tests/features/chat/presentation/runtime/diagnostic-sections.test.ts index d1824a34..0847f99f 100644 --- a/tests/features/chat/presentation/runtime/diagnostic-sections.test.ts +++ b/tests/features/chat/presentation/runtime/diagnostic-sections.test.ts @@ -6,7 +6,6 @@ import { diagnosticProbeOk, diagnosticsWithProbe, upsertMcpServerDiagnostic, - upsertMcpServerStatusDiagnostics, } from "../../../../../src/domain/server/diagnostics"; import type { ToolInventorySnapshot } from "../../../../../src/domain/server/tool-inventory"; import { appServerDiagnosticSections } from "../../../../../src/features/chat/presentation/runtime/diagnostic-sections"; @@ -64,36 +63,6 @@ describe("connection diagnostics", () => { expect(rows.find((row) => row.label === "mcp github")).toBeUndefined(); }); - it("maps app-server MCP status snapshots into diagnostics", () => { - const diagnostics = upsertMcpServerDiagnostic(createServerDiagnostics(), { - name: "github", - startupStatus: "starting", - authStatus: null, - toolCount: null, - message: "launching", - }); - - const next = upsertMcpServerStatusDiagnostics(diagnostics, [ - { - name: "github", - authStatus: "oAuth", - toolCount: 2, - resourceCount: 0, - resourceTemplateCount: 0, - }, - ]); - - expect(next.mcpServers).toEqual([ - { - name: "github", - startupStatus: "starting", - authStatus: "oAuth", - toolCount: 2, - message: "launching", - }, - ]); - }); - it("summarizes usable Codex capabilities and groups skills by provenance", () => { const inventory: ToolInventorySnapshot = { checkedAt: 1, diff --git a/tests/features/chat/ui/composer.test.ts b/tests/features/chat/ui/composer.test.ts index 094a68a2..292d8b32 100644 --- a/tests/features/chat/ui/composer.test.ts +++ b/tests/features/chat/ui/composer.test.ts @@ -3,7 +3,7 @@ import { h } from "preact"; import { describe, expect, it, vi } from "vitest"; import type { ComposerMetaViewModel } from "../../../../src/features/chat/ui/composer"; -import { type ComposerCallbacks, ComposerShell, type ComposerSuggestion } from "../../../../src/features/chat/ui/composer"; +import { type ComposerCallbacks, ComposerShell } from "../../../../src/features/chat/ui/composer"; import { scrollComposerSuggestionIntoView, syncComposerHeight } from "../../../../src/features/chat/ui/composer.dom"; import { renderUiRoot } from "../../../../src/shared/dom/preact-root.dom"; import { waitForAsyncWork } from "../../../support/async"; @@ -11,6 +11,8 @@ import { changeInputValue, composerSuggestionScrollFixture, installObsidianDomSh installObsidianDomShims(); +type ComposerSuggestion = Parameters[0]; + function mountComposerShell( parent: HTMLElement, viewId: string, diff --git a/tests/settings/dynamic-sections-controller.test.ts b/tests/settings/dynamic-sections-controller.test.ts index aa59b5e3..0f168a88 100644 --- a/tests/settings/dynamic-sections-controller.test.ts +++ b/tests/settings/dynamic-sections-controller.test.ts @@ -6,7 +6,7 @@ import type { ObservedResult } from "../../src/app-server/query/observed-result" 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 { SettingsDynamicSectionsController, type SettingsDynamicSectionsSnapshot } from "../../src/settings/dynamic-sections-controller"; +import { SettingsDynamicSectionsController } from "../../src/settings/dynamic-sections-controller"; import { deferred } from "../support/async"; import { appServerThread, @@ -21,6 +21,8 @@ import { useShortLivedClients, } from "./test-support"; +type SettingsDynamicSectionsSnapshot = ReturnType; + const { withShortLivedAppServerClientMock } = vi.hoisted(() => ({ withShortLivedAppServerClientMock: vi.fn(), })); diff --git a/tests/support/runtime-config.ts b/tests/support/runtime-config.ts new file mode 100644 index 00000000..e52bbdd1 --- /dev/null +++ b/tests/support/runtime-config.ts @@ -0,0 +1,8 @@ +import { type RuntimeConfigSnapshot, runtimeConfigOrDefault } from "../../src/domain/runtime/config"; + +export function runtimeConfigFixture(overrides: Partial = {}): RuntimeConfigSnapshot { + return { + ...runtimeConfigOrDefault(null), + ...overrides, + }; +}