Skip to content
Source
Sign inSign up

WorktreeBranchSync.test.ts

110 lines4.3 KB

1import { assert, it } from "@effect/vitest";
2import {
3 ThreadId,
4 type OrchestrationV2ThreadShell,
5 type OrchestrationV2ServerCommand,
6} from "@t3tools/contracts";
7import * as Deferred from "effect/Deferred";
8import * as Stream from "effect/Stream";
9import * as Effect from "effect/Effect";
10import * as FileSystem from "effect/FileSystem";
11import * as Layer from "effect/Layer";
12​
13import * as GitWorkflowService from "../git/GitWorkflowService.ts";
14import * as VcsStatusBroadcaster from "../vcs/VcsStatusBroadcaster.ts";
15import * as ProjectionStore from "./ProjectionStore.ts";
16import * as Orchestrator from "./Orchestrator.ts";
17import * as ThreadManagement from "./ThreadManagementService.ts";
18import * as WorktreeBranchSync from "./WorktreeBranchSync.ts";
19​
20it.effect.each([false, true])(
21 "reconciles shared worktree branches without changing root checkouts or detached HEADs, conflict=%s",
22 (conflict) => {
23 const reconciled = Deferred.makeUnsafe<void>();
24 const commands: OrchestrationV2ServerCommand[] = [];
25 const thread = (id: string, branch: string, worktreePath: string | null) =>
26 ({ id: ThreadId.make(id), branch, worktreePath }) as OrchestrationV2ThreadShell;
27 const snapshot = {
28 schemaVersion: 2,
29 snapshotSequence: 0,
30 threads: [
31 thread("generated", "t3/unify-converter-branches", "/worktree"),
32 thread("shared", "t3/bfc349ae", "/link/worktree"),
33 thread("current", "union", "/worktree"),
34 thread("root", "main", null),
35 thread("other", "feature", "/other"),
36 ],
37 archivedThreads: [thread("archived", "t3/bfc349ae", "/worktree")],
38 };
39 return Effect.gen(function* () {
40 yield* Layer.build(WorktreeBranchSync.layer);
41 yield* Deferred.await(reconciled);
42 assert.deepEqual(
43 commands.map((command) => {
44 assert.equal(command.type, "thread.metadata.update");
45 if (command.type !== "thread.metadata.update") throw new Error("Unexpected command");
46 return [
47 command.threadId,
48 command.branch,
49 command.expectedBranch,
50 command.expectedWorktreePath,
51 ];
52 }),
53 [
54 ["generated", "union", "t3/unify-converter-branches", "/worktree"],
55 ["shared", "union", "t3/bfc349ae", "/link/worktree"],
56 ["archived", "union", "t3/bfc349ae", "/worktree"],
57 ],
58 );
59 }).pipe(
60 Effect.provide(
61 Layer.mergeAll(
62 FileSystem.layerNoop({
63 realPath: (path) => Effect.succeed(path === "/link/worktree" ? "/worktree" : path),
64 }),
65 Layer.mock(ProjectionStore.ProjectionStoreV2)({
66 getThreadWorktreePaths: () =>
67 Effect.succeed(
68 [...snapshot.threads, ...snapshot.archivedThreads].flatMap((thread) =>
69 thread.worktreePath === null
70 ? []
71 : [{ threadId: thread.id, worktreePath: thread.worktreePath }],
72 ),
73 ),
74 }),
75 Layer.mock(VcsStatusBroadcaster.VcsStatusBroadcaster)({
76 streamBranchChanges: Stream.fromIterable([null, "t3/bfc349ae", "union"]).pipe(
77 Stream.map((refName) => ({
78 cwd: "/worktree",
79 refName,
80 })),
81 Stream.ensuring(Deferred.succeed(reconciled, undefined)),
82 ),
83 }),
84 Layer.mock(GitWorkflowService.GitWorkflowService)({
85 currentBranch: () => Effect.succeed("union"),
86 }),
87 Layer.mock(ThreadManagement.ThreadManagementService)({
88 getShellSnapshot: () => Effect.succeed(snapshot),
89 dispatch: (command) =>
90 Effect.gen(function* () {
91 commands.push(command);
92 if (
93 command.type === "thread.metadata.update" &&
94 command.threadId === ThreadId.make("generated") &&
95 conflict
96 ) {
97 return yield* new Orchestrator.OrchestratorDispatchError({
98 commandId: command.commandId,
99 commandType: command.type,
100 cause: "Thread moved before branch metadata could be applied.",
101 });
102 }
103 return { sequence: commands.length, storedEvents: [] };
104 }),
105 }),
106 ),
107 ),
108 );
109 },
110);