1import { assert, it, describe } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as Logger from "effect/Logger";
7import * as Path from "effect/Path";
8
9import * as VcsProjectConfig from "./VcsProjectConfig.ts";
10
11const layerTest = VcsProjectConfig.layer.pipe(
12 Layer.provide(NodeServices.layer),
13 Layer.provideMerge(NodeServices.layer),
14);
15
16describe("VcsProjectConfig", () => {
17 it.layer(layerTest)("discovers .t3code/vcs.json from nested workspaces", (it) => {
18 it.effect("returns the configured kind", () =>
19 Effect.gen(function* () {
20 const fileSystem = yield* FileSystem.FileSystem;
21 const path = yield* Path.Path;
22 const root = yield* fileSystem.makeTempDirectoryScoped({
23 prefix: "t3-vcs-config-test-",
24 });
25 const configDir = path.join(root, ".t3code");
26 const nested = path.join(root, "packages", "app");
27 yield* fileSystem.makeDirectory(configDir, { recursive: true });
28 yield* fileSystem.makeDirectory(nested, { recursive: true });
29 yield* fileSystem.writeFileString(
30 path.join(configDir, "vcs.json"),
31 JSON.stringify({ vcs: { kind: "jj" } }),
32 );
33
34 const config = yield* VcsProjectConfig.VcsProjectConfig;
35 const kind = yield* config.resolveKind({ cwd: nested });
36
37 assert.equal(kind, "jj");
38 }),
39 );
40 });
41
42 it.layer(layerTest)("continues to parent configs after a candidate inspect failure", (it) => {
43 it.effect("logs the failed candidate and returns the parent config", () => {
44 const messages: unknown[] = [];
45 const logger = Logger.make<unknown, void>(({ message }) => {
46 messages.push(message);
47 });
48
49 return Effect.gen(function* () {
50 const fileSystem = yield* FileSystem.FileSystem;
51 const path = yield* Path.Path;
52 const root = yield* fileSystem.makeTempDirectoryScoped({
53 prefix: "t3-vcs-config-test-",
54 });
55 const configDir = path.join(root, ".t3code");
56 const cwd = path.join(root, "invalid\0child");
57 yield* fileSystem.makeDirectory(configDir, { recursive: true });
58 yield* fileSystem.writeFileString(
59 path.join(configDir, "vcs.json"),
60 JSON.stringify({ vcs: { kind: "jj" } }),
61 );
62
63 const config = yield* VcsProjectConfig.VcsProjectConfig;
64 const kind = yield* config.resolveKind({ cwd });
65
66 assert.equal(kind, "jj");
67 const failedCandidate = path.join(cwd, ".t3code", "vcs.json");
68 const [error] = messages[0] as ReadonlyArray<unknown>;
69 assert.instanceOf(error, VcsProjectConfig.VcsProjectConfigError);
70 assert.equal(
71 error.message,
72 "Failed to inspect VCS project config at " + failedCandidate + ".",
73 );
74 assert.deepInclude(error, {
75 operation: "inspect",
76 cwd,
77 configPath: failedCandidate,
78 _tag: "VcsProjectConfigError",
79 });
80 }).pipe(Effect.provide(Logger.layer([logger], { mergeWithExisting: false })));
81 });
82 });
83
84 it.layer(layerTest)("falls back to auto when config JSON is malformed", (it) => {
85 it.effect("returns auto and logs the failed operation and path", () => {
86 const messages: unknown[] = [];
87 const logger = Logger.make<unknown, void>(({ message }) => {
88 messages.push(message);
89 });
90
91 return Effect.gen(function* () {
92 const fileSystem = yield* FileSystem.FileSystem;
93 const path = yield* Path.Path;
94 const root = yield* fileSystem.makeTempDirectoryScoped({
95 prefix: "t3-vcs-config-test-",
96 });
97 const configDir = path.join(root, ".t3code");
98 yield* fileSystem.makeDirectory(configDir, { recursive: true });
99 yield* fileSystem.writeFileString(path.join(configDir, "vcs.json"), "{not json");
100
101 const config = yield* VcsProjectConfig.VcsProjectConfig;
102 const kind = yield* config.resolveKind({ cwd: root });
103
104 assert.equal(kind, "auto");
105 const [error] = messages[0] as ReadonlyArray<unknown>;
106 assert.instanceOf(error, VcsProjectConfig.VcsProjectConfigError);
107 assert.equal(
108 error.message,
109 "Failed to decode VCS project config at " + path.join(configDir, "vcs.json") + ".",
110 );
111 assert.deepInclude(error.cause, { _tag: "SchemaError" });
112 assert.deepInclude(error, {
113 operation: "decode",
114 cwd: root,
115 configPath: path.join(configDir, "vcs.json"),
116 _tag: "VcsProjectConfigError",
117 });
118 }).pipe(Effect.provide(Logger.layer([logger], { mergeWithExisting: false })));
119 });
120 });
121});