1import { assert, describe, expect, it, vi } from "@effect/vitest";
2import * as DateTime from "effect/DateTime";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6
7import { VcsRepositoryDetectionError } from "@t3tools/contracts";
8
9import * as NodeServices from "@effect/platform-node/NodeServices";
10import * as FileSystem from "effect/FileSystem";
11import * as Path from "effect/Path";
12
13import * as ServerConfig from "../config.ts";
14import * as VcsProcess from "../vcs/VcsProcess.ts";
15import * as GitManager from "./GitManager.ts";
16import * as GitWorkflowService from "./GitWorkflowService.ts";
17import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
18import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
19
20function layer(input: {
21 readonly detect: VcsDriverRegistry.VcsDriverRegistry["Service"]["detect"];
22}) {
23 return GitWorkflowService.layer.pipe(
24 Layer.provide(
25 Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({
26 detect: input.detect,
27 }),
28 ),
29 Layer.provide(Layer.mock(GitVcsDriver.GitVcsDriver)({})),
30 Layer.provide(Layer.mock(GitManager.GitManager)({})),
31 Layer.provide(NodeServices.layer),
32 );
33}
34
35describe("GitWorkflowService", () => {
36 it.effect("reports a non-Git VCS repository as not a Git repository", () =>
37 Effect.gen(function* () {
38 const workflow = yield* GitWorkflowService.GitWorkflowService;
39 const isRepository = yield* workflow.isRepository("/jj-repo");
40
41 assert.equal(isRepository, false);
42 }).pipe(
43 Effect.provide(
44 layer({
45 detect: () =>
46 Effect.succeed({
47 kind: "jj",
48 repository: {
49 kind: "jj",
50 rootPath: "/jj-repo",
51 metadataPath: "/jj-repo/.jj",
52 freshness: {
53 source: "live-local",
54 observedAt: DateTime.makeUnsafe("2026-01-01T00:00:00.000Z"),
55 expiresAt: Option.none(),
56 },
57 },
58 driver: {} as VcsDriverRegistry.VcsDriverHandle["driver"],
59 }),
60 }),
61 ),
62 ),
63 );
64
65 it.effect("returns an empty local status when no VCS repository is detected", () =>
66 Effect.gen(function* () {
67 const workflow = yield* GitWorkflowService.GitWorkflowService;
68 const status = yield* workflow.localStatus({ cwd: "/not-a-repo" });
69
70 assert.deepStrictEqual(status, {
71 isRepo: false,
72 hasPrimaryRemote: false,
73 isDefaultRef: false,
74 refName: null,
75 hasWorkingTreeChanges: false,
76 workingTree: {
77 files: [],
78 insertions: 0,
79 deletions: 0,
80 },
81 });
82 }).pipe(
83 Effect.provide(
84 layer({
85 detect: () => Effect.succeed(null),
86 }),
87 ),
88 ),
89 );
90
91 it.effect(
92 "keeps non-colocated Jujutsu changes available without reporting a Git repository",
93 () =>
94 Effect.gen(function* () {
95 const workflow = yield* GitWorkflowService.GitWorkflowService;
96 const fs = yield* FileSystem.FileSystem;
97 const path = yield* Path.Path;
98 const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-jj-workflow-" });
99 yield* fs.makeDirectory(path.join(root, ".jj"));
100 const nested = path.join(root, "packages", "app");
101 yield* fs.makeDirectory(nested, { recursive: true });
102
103 for (const cwd of [root, nested]) {
104 const local = yield* workflow.localStatus({ cwd });
105 const status = yield* workflow.status({ cwd });
106 expect(local.isRepo).toBe(false);
107 expect(local.isJjWorkspace).toBe(true);
108 expect(status.isRepo).toBe(false);
109 expect(status.isJjWorkspace).toBe(true);
110 expect(status.refName).toBeNull();
111 }
112 }).pipe(
113 Effect.scoped,
114 Effect.provide(
115 Layer.mergeAll(layer({ detect: () => Effect.succeed(null) }), NodeServices.layer),
116 ),
117 ),
118 );
119
120 it.effect("returns an empty full status when no VCS repository is detected", () =>
121 Effect.gen(function* () {
122 const workflow = yield* GitWorkflowService.GitWorkflowService;
123 const status = yield* workflow.status({ cwd: "/not-a-repo" });
124
125 assert.deepStrictEqual(status, {
126 isRepo: false,
127 hasPrimaryRemote: false,
128 isDefaultRef: false,
129 refName: null,
130 hasWorkingTreeChanges: false,
131 workingTree: {
132 files: [],
133 insertions: 0,
134 deletions: 0,
135 },
136 hasUpstream: false,
137 aheadCount: 0,
138 behindCount: 0,
139 aheadOfDefaultCount: 0,
140 pr: null,
141 });
142 }).pipe(
143 Effect.provide(
144 layer({
145 detect: () => Effect.succeed(null),
146 }),
147 ),
148 ),
149 );
150
151 it.effect("does not call GitManager status methods when no VCS repository is detected", () => {
152 const localStatus = vi.fn();
153 const remoteStatus = vi.fn();
154 const status = vi.fn();
155
156 const layerTest = GitWorkflowService.layer.pipe(
157 Layer.provide(
158 Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({
159 detect: () => Effect.succeed(null),
160 }),
161 ),
162 Layer.provide(Layer.mock(GitVcsDriver.GitVcsDriver)({})),
163 Layer.provide(
164 Layer.mock(GitManager.GitManager)({
165 localStatus,
166 remoteStatus,
167 status,
168 }),
169 ),
170 Layer.provide(NodeServices.layer),
171 );
172
173 return Effect.gen(function* () {
174 const workflow = yield* GitWorkflowService.GitWorkflowService;
175 yield* workflow.localStatus({ cwd: "/not-a-repo" });
176 yield* workflow.remoteStatus({ cwd: "/not-a-repo" });
177 yield* workflow.status({ cwd: "/not-a-repo" });
178
179 assert.equal(localStatus.mock.calls.length, 0);
180 assert.equal(remoteStatus.mock.calls.length, 0);
181 assert.equal(status.mock.calls.length, 0);
182 }).pipe(Effect.provide(layerTest));
183 });
184
185 it.effect("returns an empty ref list when no VCS repository is detected", () =>
186 Effect.gen(function* () {
187 const workflow = yield* GitWorkflowService.GitWorkflowService;
188 const refs = yield* workflow.listRefs({ cwd: "/not-a-repo" });
189
190 assert.deepStrictEqual(refs, {
191 refs: [],
192 isRepo: false,
193 hasPrimaryRemote: false,
194 nextCursor: null,
195 totalCount: 0,
196 });
197 }).pipe(
198 Effect.provide(
199 layer({
200 detect: () => Effect.succeed(null),
201 }),
202 ),
203 ),
204 );
205
206 it.effect("structures workflow detection failures without exposing upstream details", () => {
207 const cause = new VcsRepositoryDetectionError({
208 operation: "VcsDriverRegistry.detect",
209 cwd: "/repo",
210 detail: "upstream detail must stay in the cause chain",
211 });
212
213 return Effect.gen(function* () {
214 const workflow = yield* GitWorkflowService.GitWorkflowService;
215 const error = yield* workflow.status({ cwd: "/repo" }).pipe(Effect.flip);
216
217 expect(error).toMatchObject({
218 _tag: "GitManagerError",
219 operation: "GitWorkflowService.status",
220 cwd: "/repo",
221 detail: "Failed to detect a VCS repository for this Git workflow.",
222 });
223 expect(error.message).not.toContain(cause.detail);
224 }).pipe(
225 Effect.provide(
226 layer({
227 detect: () => Effect.fail(cause),
228 }),
229 ),
230 );
231 });
232
233 it.effect("structures command detection failures without exposing upstream details", () => {
234 const cause = new VcsRepositoryDetectionError({
235 operation: "VcsDriverRegistry.detect",
236 cwd: "/repo",
237 detail: "upstream command detail must stay in the cause chain",
238 });
239
240 return Effect.gen(function* () {
241 const workflow = yield* GitWorkflowService.GitWorkflowService;
242 const error = yield* workflow.listRefs({ cwd: "/repo" }).pipe(Effect.flip);
243
244 expect(error).toMatchObject({
245 _tag: "GitCommandError",
246 operation: "GitWorkflowService.listRefs",
247 command: "vcs-route",
248 cwd: "/repo",
249 detail: "Failed to detect a VCS repository for this Git command.",
250 });
251 expect(error.message).not.toContain(cause.detail);
252 }).pipe(
253 Effect.provide(
254 layer({
255 detect: () => Effect.fail(cause),
256 }),
257 ),
258 );
259 });
260
261 it.effect("finds a name among the refs of every namespace", () =>
262 Effect.gen(function* () {
263 const fileSystem = yield* FileSystem.FileSystem;
264 const cwd = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-ref-named-" });
265 const workflow = yield* GitWorkflowService.GitWorkflowService;
266 const driver = yield* GitVcsDriver.GitVcsDriver;
267 for (const args of [
268 ["init", "-b", "main"],
269 ["-c", "user.email=t@t", "-c", "user.name=t", "commit", "--allow-empty", "-m", "init"],
270 ["update-ref", "refs/remotes/upstream/feature/topic", "HEAD"],
271 ["symbolic-ref", "refs/remotes/origin/alias", "refs/heads/main"],
272 ["update-ref", "refs/mirror/origin/mirrored", "HEAD"],
273 ]) {
274 yield* driver.execute({ operation: "test.setupRepo", cwd, args });
275 }
276 const found = yield* Effect.forEach(
277 ["main", "feature/topic", "alias", "mirrored", "t3/renamed", "feature/*"],
278 (refName) => workflow.hasRefNamed({ cwd, refName }),
279 );
280 assert.deepEqual(found, [true, true, true, true, false, false]);
281 }).pipe(
282 Effect.scoped,
283 Effect.provide(
284 GitWorkflowService.layer.pipe(
285 Layer.provide(
286 Layer.mock(VcsDriverRegistry.VcsDriverRegistry)({
287 resolve: () => Effect.succeed({ kind: "git" } as VcsDriverRegistry.VcsDriverHandle),
288 }),
289 ),
290 Layer.provide(Layer.mock(GitManager.GitManager)({})),
291 Layer.provideMerge(GitVcsDriver.layer),
292 Layer.provideMerge(VcsProcess.layer),
293 Layer.provideMerge(
294 ServerConfig.layerTest(process.cwd(), { prefix: "t3-ref-named-" }).pipe(
295 Layer.provide(NodeServices.layer),
296 ),
297 ),
298 Layer.provideMerge(NodeServices.layer),
299 ),
300 ),
301 ),
302 );
303});