History

test(projects): prove shared creation admission

Julius Marminge committed 2 months ago

Commit 9fb259c1 parent adec2225 files · +260 −191Browse files

Unified · Split

5 files
  1. apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts +237 −6
  2. apps/server/src/mcp/ProjectMcpService.test.ts +0 −155
  3. apps/server/src/orchestration-v2/ClientCommandDispatch.test.ts +5 −9
  4. apps/server/src/orchestration-v2/ClientCommandDispatch.ts +15 −15
  5. apps/server/src/ws.ts +3 −6
apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts+237−6
@@ -19,6 +19,7 @@ import { · 18 unmodified lines
1919 OrchestratorMcpThreadReadResult,
2020 OrchestratorMcpThreadSendResult,
2121 OrchestratorMcpThreadWaitResult,
22+ type Project,
2223 ProjectId,
2324 ProviderDriverKind,
2425 ProviderInstanceId,
@@ -37,6 +38,7 @@ import * as DateTime from "effect/DateTime"; · 12 unmodified lines
3738 import * as Deferred from "effect/Deferred";
3839 import * as Effect from "effect/Effect";
3940 import * as FileSystem from "effect/FileSystem";
41+import * as Fiber from "effect/Fiber";
4042 import * as Layer from "effect/Layer";
4143 import * as Option from "effect/Option";
4244 import * as PubSub from "effect/PubSub";
@@ -48,8 +50,9 @@ import { McpSchema, McpServer } from "effect/unstable/ai"; · 5 unmodified lines
4850 import { ClaudeProviderCapabilitiesV2 } from "../orchestration-v2/Adapters/ClaudeAdapterV2.ts";
4951 import { CodexProviderCapabilitiesV2 } from "../orchestration-v2/Adapters/CodexAdapterV2.ts";
5052 import { CodexOrchestratorReplayHarness } from "../orchestration-v2/Adapters/CodexAdapterV2.testkit.ts";
53+import * as ClientCommandDispatch from "../orchestration-v2/ClientCommandDispatch.ts";
5154 import { OrchestratorV2, type OrchestratorV2Shape } from "../orchestration-v2/Orchestrator.ts";
52−import { layer as threadManagementServiceLayer } from "../orchestration-v2/ThreadManagementService.ts";
55+import * as ThreadManagement from "../orchestration-v2/ThreadManagementService.ts";
5356 import {
5457 type ProviderAdapterV2Event,
5558 ProviderAdapterProtocolError,
@@ -71,13 +74,18 @@ import { · 15 unmodified lines
7174 materializeReplayTranscriptWorkspace,
7275 } from "../orchestration-v2/testkit/ReplayTranscriptNdjson.ts";
7376 import { makeProviderRegistryLayer } from "../provider/testUtils/providerRegistryMock.ts";
77+import * as ProjectService from "../project/ProjectService.ts";
7478 import { ScheduledTaskService } from "../scheduledTasks/ScheduledTaskService.ts";
79+import * as ServerSettings from "../serverSettings.ts";
80+import * as SourceControlRepositoryService from "../sourceControl/SourceControlRepositoryService.ts";
7581 import * as McpHttpServer from "./McpHttpServer.ts";
7682 import * as McpInvocationContext from "./McpInvocationContext.ts";
7783 import { delegatedTaskRun, hasPendingChildRuns } from "./OrchestratorMcpService.ts";
7884
7985 const parentThreadId = ThreadId.make("thread:mcp-orchestrator-parent");
8086 const projectId = ProjectId.make("project:mcp-orchestrator");
87+const deletionRaceProjectId = ProjectId.make("project:mcp-orchestrator-delete-race");
88+const deletionRaceParentThreadId = ThreadId.make("thread:mcp-orchestrator-delete-race-parent");
8189 const codexInstanceId = ProviderInstanceId.make("codex");
8290 const claudeInstanceId = ProviderInstanceId.make("claudeAgent");
8391 const codexModel = "gpt-5.4";
@@ -472,6 +480,28 @@ describe("orchestrator MCP toolkit", () => { · 388 unmodified lines
472480 Effect.scoped(
473481 Effect.gen(function* () {
474482 const cwd = yield* checkpointWorkspace("orchestrator-mcp-toolkit");
483+ const deletionRaceProject = {
484+ id: deletionRaceProjectId,
485+ title: "MCP deletion race project",
486+ workspaceRoot: cwd,
487+ repositoryIdentity: null,
488+ faviconPath: null,
489+ defaultModelSelection: codexSelection,
490+ defaultThreadEnvMode: "local",
491+ scripts: [],
492+ createdAt: "2026-08-30T00:00:00.000Z",
493+ updatedAt: "2026-08-30T00:00:00.000Z",
494+ deletedAt: null,
495+ } satisfies Project;
496+ const deletionRaceProjectState = yield* Ref.make<Project>(deletionRaceProject);
497+ const creationAdmissionEntered = yield* Deferred.make<void>();
498+ const allowCreationAdmission = yield* Deferred.make<void>();
499+ const deletionLockAttempted = yield* Deferred.make<void>();
500+ const deletionLockAcquired = yield* Deferred.make<void>();
501+ const allowDeletion = yield* Deferred.make<void>();
502+ const deletionRaceCommandId = CommandId.make(
503+ "command:mcp:mcp-provider-session-delete-race:create-thread:delete-create-race:0",
504+ );
475505 const capturedTurns = yield* Ref.make<ReadonlyArray<CapturedTurn>>([]);
476506 const parentTerminalGates = new Map<ThreadId, Deferred.Deferred<void>>();
477507 const deliveryTerminalGates = new Map<ThreadId, Deferred.Deferred<void>>();
@@ -482,7 +512,9 @@ describe("orchestrator MCP toolkit", () => { · 4 unmodified lines
482512 capabilities: CodexProviderCapabilitiesV2,
483513 capturedTurns,
484514 shouldComplete: (turn) =>
485− turn.threadId !== parentThreadId && turn.message.text !== cancellationPrompt,
515+ turn.threadId !== parentThreadId &&
516+ turn.threadId !== deletionRaceParentThreadId &&
517+ turn.message.text !== cancellationPrompt,
486518 terminalGate: (turn) =>
487519 turn.message.text.startsWith("Delegated task") ||
488520 turn.message.text.startsWith("Delegated tasks")
@@ -554,10 +586,42 @@ describe("orchestrator MCP toolkit", () => { · 65 unmodified lines
554586 },
555587 registryLayer,
556588 ).pipe(Layer.provide(continuationProbeLayer));
557− const orchestrationLayer = Layer.merge(
558− orchestratorLayer,
559− threadManagementServiceLayer.pipe(Layer.provide(orchestratorLayer)),
589+ const threadManagementLayer = ThreadManagement.layer.pipe(
590+ Layer.provide(orchestratorLayer),
560591 );
592+ const gatedThreadManagementLayer = Layer.effect(
593+ ThreadManagement.ThreadManagementService,
594+ Effect.gen(function* () {
595+ const actual = yield* ThreadManagement.ThreadManagementService;
596+ return ThreadManagement.ThreadManagementService.of({
597+ ...actual,
598+ withProjectCreationAdmission: (input, effect) =>
599+ actual.withProjectCreationAdmission(input, (receipt) =>
600+ input.commandId === deletionRaceCommandId && Option.isNone(receipt)
601+ ? Deferred.succeed(creationAdmissionEntered, undefined).pipe(
602+ Effect.andThen(Deferred.await(allowCreationAdmission)),
603+ Effect.andThen(effect(receipt)),
604+ )
605+ : effect(receipt),
606+ ),
607+ withProjectMutationLock: (lockedProjectId, effect) =>
608+ lockedProjectId === deletionRaceProjectId
609+ ? Deferred.succeed(deletionLockAttempted, undefined).pipe(
610+ Effect.andThen(
611+ actual.withProjectMutationLock(
612+ lockedProjectId,
613+ Deferred.succeed(deletionLockAcquired, undefined).pipe(
614+ Effect.andThen(Deferred.await(allowDeletion)),
615+ Effect.andThen(effect),
616+ ),
617+ ),
618+ ),
619+ )
620+ : actual.withProjectMutationLock(lockedProjectId, effect),
621+ });
622+ }),
623+ ).pipe(Layer.provide(threadManagementLayer));
624+ const orchestrationLayer = Layer.merge(orchestratorLayer, gatedThreadManagementLayer);
561625 const providerRegistryLayer = makeProviderRegistryLayer([
562626 makeProviderSnapshot({
563627 instanceId: codexInstanceId,
@@ -613,11 +677,40 @@ describe("orchestrator MCP toolkit", () => { · 49 unmodified lines
613677 runNow: () => Effect.die("ScheduledTaskService.runNow is unused in this test"),
614678 }),
615679 );
616− const testLayer = McpHttpServer.OrchestratorToolkitRegistrationLive.pipe(
680+ const projectLayer = Layer.mock(ProjectService.ProjectService)({
681+ getById: (requestedProjectId, options) =>
682+ requestedProjectId === deletionRaceProjectId
683+ ? Ref.get(deletionRaceProjectState).pipe(
684+ Effect.map((project) =>
685+ project.deletedAt === null || options?.includeDeleted === true
686+ ? Option.some(project)
687+ : Option.none(),
688+ ),
689+ )
690+ : Effect.succeed(Option.none()),
691+ delete: ({ projectId: deletedProjectId }) =>
692+ deletedProjectId === deletionRaceProjectId
693+ ? Ref.updateAndGet(deletionRaceProjectState, (project) => ({
694+ ...project,
695+ deletedAt: "2026-08-30T00:01:00.000Z",
696+ updatedAt: "2026-08-30T00:01:00.000Z",
697+ }))
698+ : Effect.die(`Unexpected project deletion: ${deletedProjectId}`),
699+ });
700+ const toolkitRegistrationLayer = Layer.merge(
701+ McpHttpServer.OrchestratorToolkitRegistrationLive,
702+ McpHttpServer.ProjectToolkitRegistrationLive,
703+ );
704+ const testLayer = toolkitRegistrationLayer.pipe(
617705 Layer.provideMerge(McpServer.McpServer.layer),
618706 Layer.provideMerge(orchestrationLayer),
619707 Layer.provide(providerRegistryLayer),
620708 Layer.provide(scheduledTaskStubLayer),
709+ Layer.provideMerge(projectLayer),
710+ Layer.provide(
711+ Layer.mock(SourceControlRepositoryService.SourceControlRepositoryService)({}),
712+ ),
713+ Layer.provide(ServerSettings.layerTest({})),
621714 Layer.provide(NodeServices.layer),
622715 );
623716
@@ -2876,6 +2969,144 @@ describe("orchestrator MCP toolkit", () => { · 2252 unmodified lines
28762969 removedDelivery.subagents.find((task) => task.id === removeTask.id),
28772970 ).toMatchObject({ result: expect.any(String), status: "completed" });
28782971 yield* expectOffersToStay(0);
2972+
2973+ yield* orchestrator.dispatch({
2974+ type: "thread.create",
2975+ createdBy: "user",
2976+ creationSource: "web",
2977+ commandId: CommandId.make("command:mcp-delete-race-parent:create"),
2978+ threadId: deletionRaceParentThreadId,
2979+ projectId: deletionRaceProjectId,
2980+ title: "MCP deletion race parent",
2981+ modelSelection: codexSelection,
2982+ runtimeMode: "full-access",
2983+ interactionMode: "default",
2984+ branch: null,
2985+ worktreePath: cwd,
2986+ });
2987+ yield* orchestrator.dispatch({
2988+ type: "message.dispatch",
2989+ createdBy: "user",
2990+ creationSource: "web",
2991+ commandId: CommandId.make("command:mcp-delete-race-parent:start"),
2992+ threadId: deletionRaceParentThreadId,
2993+ messageId: MessageId.make("message:mcp-delete-race-parent:start"),
2994+ text: "Stay active while project deletion races thread creation.",
2995+ attachments: [],
2996+ modelSelection: codexSelection,
2997+ dispatchMode: { type: "start_immediately" },
2998+ });
2999+ yield* waitForProjection(orchestrator, deletionRaceParentThreadId, (projection) =>
3000+ projection.runs.some((run) => run.status === "running"),
3001+ );
3002+
3003+ const deletionRaceInvocation: McpInvocationContext.McpInvocationScope = {
3004+ ...invocation,
3005+ threadId: deletionRaceParentThreadId,
3006+ providerSessionId: "mcp-provider-session-delete-race",
3007+ };
3008+ const invokeDeletionRace = (name: string, args: Record<string, unknown>) =>
3009+ server
3010+ .callTool({ name, arguments: args })
3011+ .pipe(
3012+ Effect.provideService(
3013+ McpInvocationContext.McpInvocationContext,
3014+ deletionRaceInvocation,
3015+ ),
3016+ Effect.provideService(McpSchema.McpServerClient, client),
3017+ );
3018+ const creation = yield* Effect.forkChild(
3019+ invokeDeletionRace("create_threads", {
3020+ clientRequestId: "delete-create-race",
3021+ threads: [{ title: "Racing child" }],
3022+ }),
3023+ { startImmediately: true },
3024+ );
3025+ yield* Deferred.await(creationAdmissionEntered);
3026+ const deletion = yield* Effect.forkChild(
3027+ invokeDeletionRace("t3_project_delete", {
3028+ projectId: deletionRaceProjectId,
3029+ cascadeThreads: true,
3030+ clientRequestId: "delete-create-race-project",
3031+ }),
3032+ { startImmediately: true },
3033+ );
3034+ yield* Deferred.await(deletionLockAttempted);
3035+ expect(yield* Deferred.isDone(deletionLockAcquired)).toBe(false);
3036+
3037+ yield* Deferred.succeed(allowCreationAdmission, undefined);
3038+ yield* Deferred.await(deletionLockAcquired);
3039+ const creationCall = yield* Fiber.join(creation);
3040+ expect(creationCall.isError).toBe(false);
3041+ const creationResult = yield* decodeCreateThreadsResult(
3042+ creationCall.structuredContent,
3043+ ).pipe(Effect.orDie);
3044+ const createdThread = creationResult.threads[0];
3045+ if (createdThread === undefined) {
3046+ return yield* Effect.die(new Error("Racing MCP thread was not created."));
3047+ }
3048+ expect(
3049+ (yield* orchestrator.getThreadProjection(createdThread.threadId)).thread.projectId,
3050+ ).toBe(deletionRaceProjectId);
3051+
3052+ yield* Deferred.succeed(allowDeletion, undefined);
3053+ const deletionCall = yield* Fiber.join(deletion);
3054+ expect(deletionCall.isError).toBe(false);
3055+ expect(deletionCall.structuredContent).toMatchObject({
3056+ projectId: deletionRaceProjectId,
3057+ deleted: true,
3058+ alreadyDeleted: false,
3059+ deletedThreadCount: 2,
3060+ workspaceFilesDeleted: false,
3061+ });
3062+ expect(
3063+ (yield* orchestrator.getThreadProjection(createdThread.threadId)).thread.deletedAt,
3064+ ).not.toBeNull();
3065+ expect(
3066+ (yield* orchestrator.getThreadProjection(deletionRaceParentThreadId)).thread
3067+ .deletedAt,
3068+ ).not.toBeNull();
3069+
3070+ const threads = yield* ThreadManagement.ThreadManagementService;
3071+ const dispatchClientCommand = yield* ClientCommandDispatch.make;
3072+ const acceptedCreateCommand = {
3073+ type: "thread.create",
3074+ createdBy: "agent",
3075+ creationSource: "mcp",
3076+ commandId: deletionRaceCommandId,
3077+ threadId: createdThread.threadId,
3078+ projectId: deletionRaceProjectId,
3079+ title: "Racing child",
3080+ modelSelection: codexSelection,
3081+ runtimeMode: "full-access",
3082+ interactionMode: "default",
3083+ branch: null,
3084+ worktreePath: cwd,
3085+ } as const;
3086+ const sequenceBeforeReplay = yield* threads.getThreadEventSequence(
3087+ createdThread.threadId,
3088+ );
3089+ const replayed = yield* dispatchClientCommand(acceptedCreateCommand);
3090+ expect(
3091+ replayed.storedEvents.some((stored) => stored.event.type === "thread.created"),
3092+ ).toBe(true);
3093+ expect(yield* threads.getThreadEventSequence(createdThread.threadId)).toBe(
3094+ sequenceBeforeReplay,
3095+ );
3096+ expect(
3097+ (yield* orchestrator.getThreadProjection(createdThread.threadId)).thread.deletedAt,
3098+ ).not.toBeNull();
3099+
3100+ const freshThreadId = ThreadId.make("thread:mcp-delete-race:fresh-after-delete");
3101+ const freshFailure = yield* dispatchClientCommand({
3102+ ...acceptedCreateCommand,
3103+ commandId: CommandId.make("command:mcp-delete-race:fresh-after-delete"),
3104+ threadId: freshThreadId,
3105+ }).pipe(Effect.flip);
3106+ expect(freshFailure).toMatchObject({ _tag: "ProjectMutationError" });
3107+ expect(
3108+ Option.isNone(yield* Effect.option(orchestrator.getThreadProjection(freshThreadId))),
3109+ ).toBe(true);
28793110 }).pipe(Effect.provide(testLayer));
28803111 }),
28813112 ),
apps/server/src/mcp/ProjectMcpService.test.ts+0−155
@@ -462,161 +462,6 @@ it.effect("clones before registration and requires explicit cascading for nonemp · 461 unmodified lines
462462 }),
463463 );
464464
465−it.effect("serializes receipt-aware thread creation with project deletion", () =>
466− Effect.gen(function* () {
467− const projectId = ProjectId.make("project:mcp:delete-launch-race");
468− const threadId = ThreadId.make("thread:mcp:delete-launch-race");
469− const modelSelection = {
470− instanceId: ProviderInstanceId.make("codex"),
471− model: "gpt-5.1-codex",
472− } as const;
473− const project = makeProject({
474− id: projectId,
475− title: "Delete race",
476− workspaceRoot: "/work/delete-race",
477− });
478− const projectState = yield* Ref.make<Project>(project);
479− const creationEntered = yield* Deferred.make<void>();
480− const allowCreation = yield* Deferred.make<void>();
481− const deleteLockAttempted = yield* Deferred.make<void>();
482− const deleteLockAcquired = yield* Deferred.make<void>();
483− const adapter = {
484− instanceId: modelSelection.instanceId,
485− driver: ProviderDriverKind.make("codex"),
486− getCapabilities: () => Effect.succeed(CodexProviderCapabilitiesV2),
487− planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" as const }),
488− openSession: () => Effect.die("provider execution is disabled in project deletion tests"),
489− } as ProviderAdapterV2Shape;
490− const registry = ProviderAdapterRegistry.makeLayer([adapter]);
491− const orchestrator = makeOrchestratorV2ReplayLayerWithRegistry(
492− { name: "project-delete-create-race" },
493− registry,
494− { databaseLayer: SqlitePersistenceMemory, runEffectWorker: false },
495− );
496− const actualThreads = ThreadManagement.layer.pipe(Layer.provide(orchestrator));
497− const gatedThreads = Layer.effect(
498− ThreadManagement.ThreadManagementService,
499− Effect.gen(function* () {
500− const actual = yield* ThreadManagement.ThreadManagementService;
501− return ThreadManagement.ThreadManagementService.of({
502− ...actual,
503− withProjectMutationLock: (lockedProjectId, effect) =>
504− Deferred.succeed(deleteLockAttempted, undefined).pipe(
505− Effect.andThen(
506− actual.withProjectMutationLock(
507− lockedProjectId,
508− Deferred.succeed(deleteLockAcquired, undefined).pipe(Effect.andThen(effect)),
509− ),
510− ),
511− ),
512− });
513− }),
514− ).pipe(Layer.provide(actualThreads));
515− const projectServiceLayer = Layer.mock(ProjectService.ProjectService)({
516− getById: () =>
517− Ref.get(projectState).pipe(
518− Effect.map((current) =>
519− current.deletedAt === null ? Option.some(current) : Option.none(),
520− ),
521− ),
522− delete: () => Ref.updateAndGet(projectState, (current) => ({ ...current, deletedAt: now })),
523− });
524− const testLayer = ProjectMcp.layer.pipe(
525− Layer.provideMerge(gatedThreads),
526− Layer.provide(projectServiceLayer),
527− Layer.provide(Layer.mock(T3ProjectFileLoader.T3ProjectFileLoader)({})),
528− Layer.provide(
529− Layer.mock(ServerSettingsService.ServerSettingsService)({
530− getSettings: Effect.succeed({ defaultThreadEnvMode: "local" } as ServerSettings),
531− }),
532− ),
533− Layer.provide(Layer.mock(SourceControlRepositoryService.SourceControlRepositoryService)({})),
534− Layer.provide(NodeServices.layer),
535− );
536−
537− yield* Effect.gen(function* () {
538− const projects = yield* ProjectMcp.ProjectMcpService;
539− const threads = yield* ThreadManagement.ThreadManagementService;
540− const createCommand = {
541− type: "thread.create",
542− createdBy: "user",
543− creationSource: "web",
544− commandId: CommandId.make("command:mcp:delete-launch-race:create"),
545− threadId,
546− projectId,
547− title: "Racing thread",
548− modelSelection,
549− runtimeMode: "full-access",
550− interactionMode: "default",
551− branch: null,
552− worktreePath: null,
553− } as const;
554− const admitCreate = (command: typeof createCommand) =>
555− threads.withProjectCreationAdmission(
556− { projectId, commandId: command.commandId },
557− (receipt) =>
558− Effect.gen(function* () {
559− if (Option.isNone(receipt)) {
560− if (command.commandId === createCommand.commandId) {
561− yield* Deferred.succeed(creationEntered, undefined);
562− yield* Deferred.await(allowCreation);
563− }
564− const current = yield* Ref.get(projectState);
565− if (current.deletedAt !== null) {
566− return yield* new ProjectMcpFailure({
567− code: "project_deleted",
568− message: "Project was deleted before thread creation was admitted.",
569− });
570− }
571− }
572− return yield* threads.dispatch(command);
573− }),
574− );
575−
576− const creation = yield* Effect.forkChild(admitCreate(createCommand), {
577− startImmediately: true,
578− });
579− yield* Deferred.await(creationEntered);
580− const deletion = yield* Effect.forkChild(
581− projects.delete(scope, {
582− projectId,
583− cascadeThreads: true,
584− clientRequestId: "delete-launch-race",
585− }),
586− { startImmediately: true },
587− );
588− yield* Deferred.await(deleteLockAttempted);
589− expect(yield* Deferred.isDone(deleteLockAcquired)).toBe(false);
590−
591− yield* Deferred.succeed(allowCreation, undefined);
592− yield* Fiber.join(creation);
593− yield* Deferred.await(deleteLockAcquired);
594− const deleted = yield* Fiber.join(deletion);
595− expect(deleted).toMatchObject({ deleted: true, deletedThreadCount: 1 });
596−
597− const sequenceBeforeReplay = yield* threads.getThreadEventSequence(threadId);
598− const replayed = yield* admitCreate(createCommand);
599− const sequenceAfterReplay = yield* threads.getThreadEventSequence(threadId);
600− expect(replayed.storedEvents.some((stored) => stored.event.type === "thread.created")).toBe(
601− true,
602− );
603− expect(sequenceAfterReplay).toBe(sequenceBeforeReplay);
604−
605− const freshCommand = {
606− ...createCommand,
607− commandId: CommandId.make("command:mcp:delete-launch-race:fresh-after-delete"),
608− threadId: ThreadId.make("thread:mcp:delete-launch-race:fresh-after-delete"),
609− };
610− const rejected = yield* admitCreate(freshCommand).pipe(Effect.flip);
611− expect(rejected).toMatchObject({ code: "project_deleted" });
612− expect(
613− Option.isNone(yield* Effect.option(threads.getThreadProjection(freshCommand.threadId))),
614− ).toBe(true);
615− expect((yield* Ref.get(projectState)).deletedAt).toBe(now);
616− }).pipe(Effect.provide(testLayer));
617− }),
618−);
619−
620465 it.effect("serializes delegated thread admission with cascading project deletion", () =>
621466 Effect.gen(function* () {
622467 const projectId = ProjectId.make("project:mcp:delete-delegate-race");
apps/server/src/orchestration-v2/ClientCommandDispatch.test.ts+5−9
@@ -15,7 +15,7 @@ import * as Ref from "effect/Ref"; · 14 unmodified lines
1515 import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";
1616 import * as ProjectService from "../project/ProjectService.ts";
1717 import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";
18−import { dispatchClientCommand } from "./ClientCommandDispatch.ts";
18+import * as ClientCommandDispatch from "./ClientCommandDispatch.ts";
1919 import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts";
2020 import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";
2121 import * as ThreadManagement from "./ThreadManagementService.ts";
@@ -59,7 +59,7 @@ it.effect("routes WebSocket thread creation through receipt-aware project admiss · 37 unmodified lines
5959
6060 yield* Effect.gen(function* () {
6161 const threads = yield* ThreadManagement.ThreadManagementService;
62− const projects = yield* ProjectService.ProjectService;
62+ const dispatchClientCommand = yield* ClientCommandDispatch.make;
6363 const command = {
6464 type: "thread.create",
6565 createdBy: "user",
@@ -75,11 +75,11 @@ it.effect("routes WebSocket thread creation through receipt-aware project admiss · 9 unmodified lines
7575 worktreePath: null,
7676 } as const;
7777
78− yield* dispatchClientCommand({ command, projects, threads });
78+ yield* dispatchClientCommand(command);
7979 const sequenceBeforeReplay = yield* threads.getThreadEventSequence(command.threadId);
8080 yield* Ref.set(projectState, null);
8181
82− yield* dispatchClientCommand({ command, projects, threads });
82+ yield* dispatchClientCommand(command);
8383 expect(yield* threads.getThreadEventSequence(command.threadId)).toBe(sequenceBeforeReplay);
8484
8585 const freshCommand = {
@@ -87,11 +87,7 @@ it.effect("routes WebSocket thread creation through receipt-aware project admiss · 1 unmodified line
8787 commandId: CommandId.make("command:client-command-admission:fresh"),
8888 threadId: ThreadId.make("thread:client-command-admission:fresh"),
8989 };
90− const rejected = yield* dispatchClientCommand({
91− command: freshCommand,
92− projects,
93− threads,
94− }).pipe(Effect.flip);
90+ const rejected = yield* dispatchClientCommand(freshCommand).pipe(Effect.flip);
9591 expect(rejected).toMatchObject({ _tag: "ProjectMutationError" });
9692 expect(
9793 Option.isNone(yield* Effect.option(threads.getThreadProjection(freshCommand.threadId))),
apps/server/src/orchestration-v2/ClientCommandDispatch.ts+15−15
@@ -2,25 +2,25 @@ import { ProjectMutationError, type OrchestrationV2Command } from "@t3tools/cont · 1 unmodified line
22 import * as Effect from "effect/Effect";
33 import * as Option from "effect/Option";
44
5−import type * as ProjectService from "../project/ProjectService.ts";
6−import type * as ThreadManagement from "./ThreadManagementService.ts";
5+import * as ProjectService from "../project/ProjectService.ts";
6+import * as ThreadManagement from "./ThreadManagementService.ts";
77
8−export const dispatchClientCommand = Effect.fn("orchestrationV2.dispatchClientCommand")(
9− function* (input: {
10− readonly command: OrchestrationV2Command;
11− readonly projects: ProjectService.ProjectService["Service"];
12− readonly threads: ThreadManagement.ThreadManagementService["Service"];
13− }) {
14− const command = input.command;
8+export const make = Effect.gen(function* () {
9+ const projects = yield* ProjectService.ProjectService;
10+ const threads = yield* ThreadManagement.ThreadManagementService;
11+
12+ return Effect.fn("orchestrationV2.dispatchClientCommand")(function* (
13+ command: OrchestrationV2Command,
14+ ) {
1515 if (command.type !== "thread.create") {
16− return yield* input.threads.dispatch(command);
16+ return yield* threads.dispatch(command);
1717 }
18− return yield* input.threads.withProjectCreationAdmission(
18+ return yield* threads.withProjectCreationAdmission(
1919 { projectId: command.projectId, commandId: command.commandId },
2020 (receipt) =>
2121 Effect.gen(function* () {
2222 if (Option.isNone(receipt)) {
23− const project = yield* input.projects.getById(command.projectId);
23+ const project = yield* projects.getById(command.projectId);
2424 if (Option.isNone(project)) {
2525 return yield* new ProjectMutationError({
2626 commandId: command.commandId,
@@ -28,8 +28,8 @@ export const dispatchClientCommand = Effect.fn("orchestrationV2.dispatchClientCo · 1 unmodified line
2828 });
2929 }
3030 }
31− return yield* input.threads.dispatch(command);
31+ return yield* threads.dispatch(command);
3232 }),
3333 );
34− },
35−);
34+ });
35+});
apps/server/src/ws.ts+3−6
@@ -90,7 +90,7 @@ import * as EnvironmentTheme from "./environmentTheme.ts"; · 89 unmodified lines
9090 import * as Keybindings from "./keybindings.ts";
9191 import * as ExternalLauncher from "./process/externalLauncher.ts";
9292 import * as ThreadManagementService from "./orchestration-v2/ThreadManagementService.ts";
93−import { dispatchClientCommand } from "./orchestration-v2/ClientCommandDispatch.ts";
93+import * as ClientCommandDispatch from "./orchestration-v2/ClientCommandDispatch.ts";
9494 import { ProviderSessionManagerV2 } from "./orchestration-v2/ProviderSessionManager.ts";
9595 import * as ThreadLaunchService from "./orchestration-v2/ThreadLaunchService.ts";
9696 import * as ScheduledTasks from "./scheduledTasks/ScheduledTaskService.ts";
@@ -528,6 +528,7 @@ const makeWsRpcLayer = ( · 431 unmodified lines
528528 const currentSessionId = currentSession.sessionId;
529529 const sql = yield* SqlClient.SqlClient;
530530 const threadManagement = yield* ThreadManagementService.ThreadManagementService;
531+ const dispatchClientCommand = yield* ClientCommandDispatch.make;
531532 const applicationEvents = yield* OrchestrationEventStore.OrchestrationEventStore;
532533 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
533534 const canReplayPersistedRange = Effect.fnUntraced(function* (
@@ -1341,11 +1342,7 @@ const makeWsRpcLayer = ( · 807 unmodified lines
13411342 creationSource: "creationSource" in command ? command.creationSource : "web",
13421343 },
13431344 );
1344− const dispatchCommand = dispatchClientCommand({
1345− command: provenanceCommand,
1346− projects: projectService,
1347− threads: threadManagement,
1348− });
1345+ const dispatchCommand = dispatchClientCommand(provenanceCommand);
13491346 return yield* startup
13501347 .enqueueCommand(dispatchCommand)
13511348 .pipe(

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t