diff --git a/src/execution-runtime.ts b/src/execution-runtime.ts index e234d8dc..5ccf9dd1 100644 --- a/src/execution-runtime.ts +++ b/src/execution-runtime.ts @@ -17,7 +17,6 @@ import type { SelectionRewriteTransport } from "./features/selection-rewrite/tra import { openThreadPicker, type ThreadPickerController } from "./features/thread-picker/modal.obsidian"; import { createThreadOperationsTransport, createThreadTitleTransport } from "./features/threads/app-server/workflow-transports"; import { createThreadCatalog, type ThreadCatalog, type ThreadCatalogEvent } from "./features/threads/catalog/thread-catalog"; -import { createThreadNameMutationCoordinator } from "./features/threads/workflows/thread-name-mutation-coordinator"; import type { ThreadsViewHost, ThreadsViewSettingsAccess } from "./features/threads-view/session"; import type { ThreadsViewPanelActivity } from "./features/threads-view/state"; import type { ThreadsRuntimeView } from "./features/threads-view/view.obsidian"; @@ -49,7 +48,7 @@ export class CodexExecutionRuntime implements AppServerClientAccess { private readonly appServerQueries: AppServerQueryCache; private readonly threadCatalog: ThreadCatalog; readonly settingsDynamicData: SettingsDynamicDataAccess; - private readonly threadNameMutations = createThreadNameMutationCoordinator(); + private readonly threadNameMutations = createKeyedOperationQueue(); private readonly threadGoalOperations = createThreadGoalOperationCoordinator(); private readonly runtimeSettingsCommitQueue = createKeyedOperationQueue(); private readonly shortLivedClients = new Set(); diff --git a/src/features/chat/app-server/mappers/thread-stream/execution-state.ts b/src/features/chat/app-server/mappers/thread-stream/execution-state.ts index ed0360db..1b0033e1 100644 --- a/src/features/chat/app-server/mappers/thread-stream/execution-state.ts +++ b/src/features/chat/app-server/mappers/thread-stream/execution-state.ts @@ -1,5 +1,4 @@ -import { RUNNING_EXECUTION_STATE, type ThreadStreamExecutionState } from "../../../domain/thread-stream/execution-state"; -import type { ExecutionState } from "../../../domain/thread-stream/items"; +import { type ExecutionState, RUNNING_EXECUTION_STATE, type ThreadStreamExecutionState } from "../../../domain/thread-stream/items"; export { RUNNING_EXECUTION_STATE }; diff --git a/src/features/chat/application/composer/daily-note-references.ts b/src/features/chat/application/composer/daily-note-references.ts deleted file mode 100644 index 7a133a48..00000000 --- a/src/features/chat/application/composer/daily-note-references.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface DailyNoteReferenceCandidate { - keyword: "today" | "tomorrow" | "yesterday"; - display: string; - name: string; - path: string; - linktext: string; -} diff --git a/src/features/chat/application/composer/note-context.ts b/src/features/chat/application/composer/note-context.ts index f4c643d3..a96a941d 100644 --- a/src/features/chat/application/composer/note-context.ts +++ b/src/features/chat/application/composer/note-context.ts @@ -1,6 +1,28 @@ import type { VaultFileReference } from "../../../../domain/chat/input"; -import type { DailyNoteReferenceCandidate } from "./daily-note-references"; -import type { NoteCandidate } from "./suggestions"; + +export interface DailyNoteReferenceCandidate { + keyword: "today" | "tomorrow" | "yesterday"; + display: string; + name: string; + path: string; + linktext: string; +} + +export interface NoteHeadingCandidate { + heading: string; + linkHeading: string; + level: number; +} + +export interface NoteCandidate { + basename: string; + displayName: string; + path: string; + mtime: number; + linktext: string; + headings: NoteHeadingCandidate[]; + recentIndex: number | null; +} export interface NoteCandidateProvider { candidates(sourcePath: string): readonly NoteCandidate[]; diff --git a/src/features/chat/application/composer/suggestions.ts b/src/features/chat/application/composer/suggestions.ts index 11fab97f..d16bf133 100644 --- a/src/features/chat/application/composer/suggestions.ts +++ b/src/features/chat/application/composer/suggestions.ts @@ -19,7 +19,7 @@ import { type SelectionContextReference, selectionContextReferenceMarker, } from "./context-references"; -import type { DailyNoteReferenceCandidate } from "./daily-note-references"; +import type { DailyNoteReferenceCandidate, NoteCandidate, NoteHeadingCandidate } from "./note-context"; import { isSlashCommandName, SLASH_COMMANDS, type SlashCommandName, slashCommandSubcommands } from "./slash-commands"; import { partialThreadTitleQuery, @@ -51,22 +51,6 @@ export interface ComposerSuggestionOptions { tagCandidates?: readonly string[] | (() => readonly string[]); } -export interface NoteCandidate { - basename: string; - displayName: string; - path: string; - mtime: number; - linktext: string; - headings: NoteHeadingCandidate[]; - recentIndex: number | null; -} - -interface NoteHeadingCandidate { - heading: string; - linkHeading: string; - level: number; -} - interface NoteCandidateMatch { file: NoteCandidate; match: SearchResult; diff --git a/src/features/chat/domain/thread-stream/execution-state.ts b/src/features/chat/domain/thread-stream/execution-state.ts deleted file mode 100644 index 81267a9c..00000000 --- a/src/features/chat/domain/thread-stream/execution-state.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { ExecutionState } from "./items"; - -export type ThreadStreamExecutionState = Exclude; -export const RUNNING_EXECUTION_STATE: ThreadStreamExecutionState = "running"; diff --git a/src/features/chat/domain/thread-stream/factories/streaming-items.ts b/src/features/chat/domain/thread-stream/factories/streaming-items.ts index 8bdce080..a8c69674 100644 --- a/src/features/chat/domain/thread-stream/factories/streaming-items.ts +++ b/src/features/chat/domain/thread-stream/factories/streaming-items.ts @@ -1,5 +1,4 @@ -import { RUNNING_EXECUTION_STATE } from "../execution-state"; -import type { ThreadStreamItem, ThreadStreamItemKind } from "../items"; +import { RUNNING_EXECUTION_STATE, type ThreadStreamItem, type ThreadStreamItemKind } from "../items"; export const STREAMED_COMMAND_RUNNING_TEXT = "Command running"; export const STREAMED_MCP_PROGRESS_LABEL = "mcp progress"; diff --git a/src/features/chat/domain/thread-stream/items.ts b/src/features/chat/domain/thread-stream/items.ts index 964014c2..e3dcfbb6 100644 --- a/src/features/chat/domain/thread-stream/items.ts +++ b/src/features/chat/domain/thread-stream/items.ts @@ -19,6 +19,8 @@ export type ThreadStreamItemKind = | "reviewResult"; type ThreadStreamRole = "user" | "assistant" | "system" | "tool"; export type ExecutionState = "running" | "completed" | "failed" | null; +export type ThreadStreamExecutionState = Exclude; +export const RUNNING_EXECUTION_STATE: ThreadStreamExecutionState = "running"; type DialogueState = "streaming" | "completed"; interface ThreadStreamBase { diff --git a/src/features/chat/host/contracts.ts b/src/features/chat/host/contracts.ts index c3f91654..69194722 100644 --- a/src/features/chat/host/contracts.ts +++ b/src/features/chat/host/contracts.ts @@ -10,7 +10,6 @@ import type { KeyedOperationQueue } from "../../../shared/runtime/keyed-operatio import type { ThreadCatalogEventSink, ThreadCatalogPaginatedActiveReader } from "../../threads/catalog/thread-catalog"; import type { ArchiveExportDestination, ArchiveExportSettings } from "../../threads/workflows/archive-export"; import type { ThreadTitleTransport } from "../../threads/workflows/ports"; -import type { ThreadNameMutationCoordinator } from "../../threads/workflows/thread-name-mutation-coordinator"; import type { TurnDiffViewState } from "../../turn-diff/model"; import type { ThreadGoalOperationCoordinator } from "../application/threads/goal-actions"; @@ -21,7 +20,7 @@ export interface CodexChatHost { readonly workspace: WorkspacePanels; readonly appServerQueries: ChatAppServerQueries; readonly threadCatalog: ChatThreadCatalog; - readonly threadNameMutations: ThreadNameMutationCoordinator; + readonly threadNameMutations: KeyedOperationQueue; readonly threadTitleTransport: ThreadTitleTransport; readonly threadGoalOperations: ThreadGoalOperationCoordinator; readonly runtimeSettingsCommitQueue: KeyedOperationQueue; 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 56639e97..b0245a36 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 @@ -1,7 +1,7 @@ import { type App, moment, normalizePath, TFile } from "obsidian"; import { appHasDailyNotesPluginLoaded, getDailyNoteSettings, type IPeriodicNoteSettings } from "obsidian-daily-notes-interface"; -import type { DailyNoteReferenceCandidate } from "../../application/composer/daily-note-references"; +import type { DailyNoteReferenceCandidate } from "../../application/composer/note-context"; import { displayNameForFile, linktextForFile } from "./vault-note-links.obsidian"; const RELATIVE_DAILY_NOTES = [ diff --git a/src/features/chat/host/obsidian/vault-note-candidate-provider.obsidian.ts b/src/features/chat/host/obsidian/vault-note-candidate-provider.obsidian.ts index 17265f49..84d05109 100644 --- a/src/features/chat/host/obsidian/vault-note-candidate-provider.obsidian.ts +++ b/src/features/chat/host/obsidian/vault-note-candidate-provider.obsidian.ts @@ -2,8 +2,7 @@ import type { App, EventRef } from "obsidian"; import { stripHeadingForLink, TFile } from "obsidian"; import type { VaultFileReference } from "../../../../domain/chat/input"; -import type { NoteCandidateProvider } from "../../application/composer/note-context"; -import type { NoteCandidate } from "../../application/composer/suggestions"; +import type { NoteCandidate, NoteCandidateProvider } from "../../application/composer/note-context"; import { configuredDailyNoteReferences } from "./vault-daily-note-references.obsidian"; import { displayNameForFile, linktextForFile } from "./vault-note-links.obsidian"; diff --git a/src/features/chat/panel/composer-controller.ts b/src/features/chat/panel/composer-controller.ts index 2490e8c0..a7242c84 100644 --- a/src/features/chat/panel/composer-controller.ts +++ b/src/features/chat/panel/composer-controller.ts @@ -14,14 +14,13 @@ import { selectionContextReferenceMarker, } from "../application/composer/context-references"; import type { ComposerInputSnapshot } from "../application/composer/input-snapshot"; -import type { NoteCandidateProvider } from "../application/composer/note-context"; +import type { NoteCandidate, NoteCandidateProvider } from "../application/composer/note-context"; import { activePanelOperationForSlashCommandSuggestion } from "../application/composer/slash-commands"; import { activeComposerSuggestions, applyComposerSuggestionInsertion, type ComposerSuggestion, composerSuggestionNavigationDirection, - type NoteCandidate, nextComposerSuggestionIndex, } from "../application/composer/suggestions"; import { type ThreadCommandTarget, threadCommandTargetForDraft } from "../application/composer/thread-title-argument"; diff --git a/src/features/threads-view/session.ts b/src/features/threads-view/session.ts index 56f6e397..94824f7a 100644 --- a/src/features/threads-view/session.ts +++ b/src/features/threads-view/session.ts @@ -6,11 +6,11 @@ import type { Thread } from "../../domain/threads/model"; import type { ThreadRenameLifecycleEvent } from "../../domain/threads/rename-lifecycle"; import { DeferredTask } from "../../shared/runtime/deferred-task"; import { isStaleExecutionRuntimeError } from "../../shared/runtime/execution-runtime-lifetime"; +import type { KeyedOperationQueue } from "../../shared/runtime/keyed-operation-queue"; import { OwnerLifetime } from "../../shared/runtime/owner-lifetime"; import type { ThreadCatalogEventSink, ThreadCatalogPaginatedActiveReader } from "../threads/catalog/thread-catalog"; import type { ArchiveExportDestination, ArchiveExportSettings } from "../threads/workflows/archive-export"; import type { ThreadOperationsTransport, ThreadTitleTransport } from "../threads/workflows/ports"; -import type { ThreadNameMutationCoordinator } from "../threads/workflows/thread-name-mutation-coordinator"; import { createThreadOperations, type ThreadOperations } from "../threads/workflows/thread-operations"; import { createThreadTitleService, type ThreadTitleService } from "../threads/workflows/thread-title-service"; import { isThreadsArchiveConfirmPointer, renderThreadsViewShell, unmountThreadsViewShell } from "./shell.dom"; @@ -19,7 +19,7 @@ export interface ThreadsViewHost { readonly settings: ThreadsViewSettingsAccess; readonly vaultPath: string; readonly threadCatalog: ThreadsViewThreadCatalog; - readonly threadNameMutations: ThreadNameMutationCoordinator; + readonly threadNameMutations: KeyedOperationQueue; readonly threadOperationsTransport: ThreadOperationsTransport; readonly threadTitleTransport: ThreadTitleTransport; openNewPanel(): Promise; diff --git a/src/features/threads/workflows/thread-name-mutation-coordinator.ts b/src/features/threads/workflows/thread-name-mutation-coordinator.ts deleted file mode 100644 index 1cae9471..00000000 --- a/src/features/threads/workflows/thread-name-mutation-coordinator.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createKeyedOperationQueue } from "../../../shared/runtime/keyed-operation-queue"; - -export interface ThreadNameMutationCoordinator { - run(threadId: string, operation: () => Promise): Promise; -} - -export function createThreadNameMutationCoordinator(): ThreadNameMutationCoordinator { - return createKeyedOperationQueue(); -} diff --git a/src/features/threads/workflows/thread-operations.ts b/src/features/threads/workflows/thread-operations.ts index ef52f29b..29d6264f 100644 --- a/src/features/threads/workflows/thread-operations.ts +++ b/src/features/threads/workflows/thread-operations.ts @@ -1,13 +1,13 @@ import { normalizeExplicitThreadName, type Thread } from "../../../domain/threads/model"; import { threadDisplayTitle } from "../../../domain/threads/title"; +import type { KeyedOperationQueue } from "../../../shared/runtime/keyed-operation-queue"; import type { ThreadCatalogEventSink } from "../catalog/thread-catalog"; import { type ArchiveExportDestination, type ArchiveExportSettings, exportArchivedThreadMarkdown } from "./archive-export"; import type { ThreadOperationsTransport } from "./ports"; -import type { ThreadNameMutationCoordinator } from "./thread-name-mutation-coordinator"; export interface ThreadOperationsHost { transport: ThreadOperationsTransport; - nameMutations: ThreadNameMutationCoordinator; + nameMutations: KeyedOperationQueue; archiveExport: { settings(): ArchiveExportSettings; enabled(): boolean; diff --git a/tests/features/chat/application/threads/auto-title-coordinator.test.ts b/tests/features/chat/application/threads/auto-title-coordinator.test.ts index 057cbdce..aab22aac 100644 --- a/tests/features/chat/application/threads/auto-title-coordinator.test.ts +++ b/tests/features/chat/application/threads/auto-title-coordinator.test.ts @@ -12,10 +12,10 @@ import { } from "../../../../../src/features/chat/application/threads/auto-title-coordinator"; import { threadTitleContextFromThreadStreamItems } from "../../../../../src/features/chat/application/threads/title-context"; import { createThreadOperationsTransport } from "../../../../../src/features/threads/app-server/workflow-transports"; -import { createThreadNameMutationCoordinator } from "../../../../../src/features/threads/workflows/thread-name-mutation-coordinator"; import { createThreadOperations } from "../../../../../src/features/threads/workflows/thread-operations"; import { createThreadTitleService } from "../../../../../src/features/threads/workflows/thread-title-service"; import { DEFAULT_SETTINGS } from "../../../../../src/settings/model"; +import { createKeyedOperationQueue } from "../../../../../src/shared/runtime/keyed-operation-queue"; import { deferred } from "../../../../support/async"; describe("AutoTitleCoordinator", () => { @@ -193,7 +193,7 @@ function coordinatorFixture( transport: createThreadOperationsTransport({ withClient: async (operation) => operation(currentClient()), }), - nameMutations: createThreadNameMutationCoordinator(), + nameMutations: createKeyedOperationQueue(), archiveExport: { settings: () => DEFAULT_SETTINGS, enabled: () => false, diff --git a/tests/features/chat/host/session-runtime.test.ts b/tests/features/chat/host/session-runtime.test.ts index fe5c7891..6db0fec1 100644 --- a/tests/features/chat/host/session-runtime.test.ts +++ b/tests/features/chat/host/session-runtime.test.ts @@ -10,7 +10,6 @@ import type { ChatPanelEnvironment } from "../../../../src/features/chat/host/co import { createChatViewDeferredTasks } from "../../../../src/features/chat/host/session/deferred-work"; import { ChatPanelSessionRuntime } from "../../../../src/features/chat/host/session-runtime"; import { createChatThreadStreamScrollBinding } from "../../../../src/features/chat/panel/thread-stream-scroll-binding"; -import { createThreadNameMutationCoordinator } from "../../../../src/features/threads/workflows/thread-name-mutation-coordinator"; import { type CodexPanelSettings, DEFAULT_SETTINGS } from "../../../../src/settings/model"; import { StaleExecutionRuntimeError } from "../../../../src/shared/runtime/execution-runtime-lifetime"; import { createKeyedOperationQueue } from "../../../../src/shared/runtime/keyed-operation-queue"; @@ -269,7 +268,7 @@ describe("ChatPanelSessionRuntime actions", () => { }, appServerQueries, threadCatalog, - threadNameMutations: createThreadNameMutationCoordinator(), + threadNameMutations: createKeyedOperationQueue(), threadGoalOperations: createThreadGoalOperationCoordinator(), runtimeSettingsCommitQueue: createKeyedOperationQueue(), }, diff --git a/tests/features/chat/host/view-connection-harness.ts b/tests/features/chat/host/view-connection-harness.ts index 1657018e..367586fe 100644 --- a/tests/features/chat/host/view-connection-harness.ts +++ b/tests/features/chat/host/view-connection-harness.ts @@ -12,7 +12,6 @@ import type { Thread } from "../../../../src/domain/threads/model"; import { createThreadGoalOperationCoordinator } from "../../../../src/features/chat/application/threads/goal-actions"; import type { ChatRuntimeView, ChatViewRuntimeOwner, CodexChatHost } from "../../../../src/features/chat/host/contracts"; import type { ThreadCatalogEvent } from "../../../../src/features/threads/catalog/thread-catalog"; -import { createThreadNameMutationCoordinator } from "../../../../src/features/threads/workflows/thread-name-mutation-coordinator"; import { type CodexPanelSettings, DEFAULT_SETTINGS } from "../../../../src/settings/model"; import { createKeyedOperationQueue } from "../../../../src/shared/runtime/keyed-operation-queue"; import { notices } from "../../../mocks/obsidian"; @@ -558,7 +557,7 @@ export function chatHost(overrides: ChatHostFixtureOverrides = {}): TestCodexCha activeThreads = threads; emitActiveThreads(); }, - threadNameMutations: overrides.threadNameMutations ?? createThreadNameMutationCoordinator(), + threadNameMutations: overrides.threadNameMutations ?? createKeyedOperationQueue(), threadTitleTransport: { persistedContext: vi.fn().mockResolvedValue(null), generateTitle: vi.fn().mockResolvedValue(null), diff --git a/tests/features/threads-view/view.test.ts b/tests/features/threads-view/view.test.ts index 8f2937be..6aff253a 100644 --- a/tests/features/threads-view/view.test.ts +++ b/tests/features/threads-view/view.test.ts @@ -6,9 +6,9 @@ import type { ObservedPaginatedResult } from "../../../src/app-server/query/obse import type * as ThreadTitleGeneratorModule from "../../../src/app-server/services/thread-title-generation"; import type { Thread } from "../../../src/domain/threads/model"; import { createThreadOperationsTransport, createThreadTitleTransport } from "../../../src/features/threads/app-server/workflow-transports"; -import { createThreadNameMutationCoordinator } from "../../../src/features/threads/workflows/thread-name-mutation-coordinator"; import type { ThreadsViewHost } from "../../../src/features/threads-view/session"; import { DEFAULT_SETTINGS } from "../../../src/settings/model"; +import { createKeyedOperationQueue } from "../../../src/shared/runtime/keyed-operation-queue"; import { notices } from "../../mocks/obsidian"; import { deferred, waitForAsyncWork } from "../../support/async"; import { changeInputValue, installObsidianDomShims } from "../../support/dom"; @@ -844,7 +844,7 @@ function threadsHost(overrides: Record = {}) { }), }, vaultPath: "/vault", - threadNameMutations: createThreadNameMutationCoordinator(), + threadNameMutations: createKeyedOperationQueue(), threadOperationsTransport: createThreadOperationsTransport(clientAccess), threadTitleTransport: createThreadTitleTransport({ clientAccess, diff --git a/tests/features/threads/workflows/thread-name-mutation-coordinator.test.ts b/tests/features/threads/workflows/thread-name-mutation-coordinator.test.ts deleted file mode 100644 index ba2d0030..00000000 --- a/tests/features/threads/workflows/thread-name-mutation-coordinator.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -import { createThreadNameMutationCoordinator } from "../../../../src/features/threads/workflows/thread-name-mutation-coordinator"; -import { deferred } from "../../../support/async"; - -describe("ThreadNameMutationCoordinator", () => { - it("does not let a failed mutation block the next mutation for the same thread", async () => { - const coordinator = createThreadNameMutationCoordinator(); - const firstMutation = deferred(); - const nextMutation = vi.fn().mockResolvedValue("saved"); - - const failed = coordinator.run("thread", async () => { - await firstMutation.promise; - throw new Error("Save failed."); - }); - const failure = expect(failed).rejects.toThrow("Save failed."); - const next = coordinator.run("thread", nextMutation); - - expect(nextMutation).not.toHaveBeenCalled(); - firstMutation.resolve(); - - await failure; - await expect(next).resolves.toBe("saved"); - expect(nextMutation).toHaveBeenCalledOnce(); - }); - - it("does not serialize mutations for different threads", async () => { - const coordinator = createThreadNameMutationCoordinator(); - const firstMutation = deferred(); - const blocked = coordinator.run("first", () => firstMutation.promise); - - await expect(coordinator.run("second", async () => "saved")).resolves.toBe("saved"); - - firstMutation.resolve(); - await blocked; - }); -}); diff --git a/tests/features/threads/workflows/thread-operations.test.ts b/tests/features/threads/workflows/thread-operations.test.ts index af131b6b..16549e2b 100644 --- a/tests/features/threads/workflows/thread-operations.test.ts +++ b/tests/features/threads/workflows/thread-operations.test.ts @@ -5,13 +5,13 @@ import type { ThreadRecord } from "../../../../src/app-server/protocol/thread"; import type { Thread } from "../../../../src/domain/threads/model"; import { createThreadOperationsTransport } from "../../../../src/features/threads/app-server/workflow-transports"; import type { ArchiveExportDestination } from "../../../../src/features/threads/workflows/archive-export"; -import { createThreadNameMutationCoordinator } from "../../../../src/features/threads/workflows/thread-name-mutation-coordinator"; import { type ArchiveThreadResult, createThreadOperations, type ThreadOperationsHost, } from "../../../../src/features/threads/workflows/thread-operations"; import { DEFAULT_SETTINGS } from "../../../../src/settings/model"; +import { createKeyedOperationQueue } from "../../../../src/shared/runtime/keyed-operation-queue"; import { deferred } from "../../../support/async"; import { legacyTurnContextManifestText } from "../../../support/legacy-turn-context-manifest"; @@ -228,7 +228,7 @@ function operationsFixture(options: { client?: MockClient | null | (() => MockCl return result; }, }), - nameMutations: createThreadNameMutationCoordinator(), + nameMutations: createKeyedOperationQueue(), archiveExport: { settings: archiveExportSettings, enabled: () => false, diff --git a/tests/shared/runtime/keyed-operation-queue.test.ts b/tests/shared/runtime/keyed-operation-queue.test.ts new file mode 100644 index 00000000..a325a67f --- /dev/null +++ b/tests/shared/runtime/keyed-operation-queue.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it, vi } from "vitest"; + +import { createKeyedOperationQueue } from "../../../src/shared/runtime/keyed-operation-queue"; +import { deferred } from "../../support/async"; + +describe("KeyedOperationQueue", () => { + it("does not let a failed operation block the next operation for the same key", async () => { + const queue = createKeyedOperationQueue(); + const firstOperation = deferred(); + const nextOperation = vi.fn().mockResolvedValue("saved"); + + const failed = queue.run("key", async () => { + await firstOperation.promise; + throw new Error("Save failed."); + }); + const failure = expect(failed).rejects.toThrow("Save failed."); + const next = queue.run("key", nextOperation); + + expect(nextOperation).not.toHaveBeenCalled(); + firstOperation.resolve(); + + await failure; + await expect(next).resolves.toBe("saved"); + expect(nextOperation).toHaveBeenCalledOnce(); + }); + + it("does not serialize operations for different keys", async () => { + const queue = createKeyedOperationQueue(); + const firstOperation = deferred(); + const blocked = queue.run("first", () => firstOperation.promise); + + await expect(queue.run("second", async () => "saved")).resolves.toBe("saved"); + + firstOperation.resolve(); + await blocked; + }); +}); diff --git a/tests/support/plugin-fixtures.ts b/tests/support/plugin-fixtures.ts index 095a33ea..11845cc4 100644 --- a/tests/support/plugin-fixtures.ts +++ b/tests/support/plugin-fixtures.ts @@ -8,7 +8,6 @@ import type { Thread } from "../../src/domain/threads/model"; import { createThreadGoalOperationCoordinator } from "../../src/features/chat/application/threads/goal-actions"; import type { CodexChatHost } from "../../src/features/chat/host/contracts"; import type { CodexChatView } from "../../src/features/chat/host/view.obsidian"; -import { createThreadNameMutationCoordinator } from "../../src/features/threads/workflows/thread-name-mutation-coordinator"; import type CodexPanelPlugin from "../../src/main"; import { type CodexPanelSettings, DEFAULT_SETTINGS } from "../../src/settings/model"; import { createKeyedOperationQueue } from "../../src/shared/runtime/keyed-operation-queue"; @@ -129,7 +128,7 @@ function chatHostFixture(): CodexChatHost { withClient: vi.fn(() => Promise.reject(new Error("Unexpected app-server client request."))), }, appServerContext: { codexPath: settings.codexPath, vaultPath: "/vault" }, - threadNameMutations: createThreadNameMutationCoordinator(), + threadNameMutations: createKeyedOperationQueue(), threadTitleTransport: { persistedContext: vi.fn().mockResolvedValue(null), generateTitle: vi.fn().mockResolvedValue(null),