| 1 | import { assert, it, describe } from "@effect/vitest"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as FileSystem from "effect/FileSystem"; |
| 5 | import * as Layer from "effect/Layer"; |
| 6 | import * as Logger from "effect/Logger"; |
| 7 | import * as Path from "effect/Path"; |
| 8 | |
| 9 | import * as VcsProjectConfig from "./VcsProjectConfig.ts"; |
| 10 | |
| 11 | const layerTest = VcsProjectConfig.layer.pipe( |
| 12 | Layer.provide(NodeServices.layer), |
| 13 | Layer.provideMerge(NodeServices.layer), |
| 14 | ); |
| 15 | |
| 16 | describe("VcsProjectConfig", () => { |
| 17 | it.layer(layerTest)("uses an explicit requested VCS kind before config", (it) => { |
| 18 | it.effect("returns the requested kind", () => |
| 19 | Effect.gen(function* () { |
| 20 | const config = yield* VcsProjectConfig.VcsProjectConfig; |
| 21 | const kind = yield* config.resolveKind({ |
| 22 | cwd: "/repo", |
| 23 | requestedKind: "jj", |
| 24 | }); |
| 25 | |
| 26 | assert.equal(kind, "jj"); |
| 27 | }), |
| 28 | ); |
| 29 | }); |
| 30 | |
| 31 | it.layer(layerTest)("discovers .t3code/vcs.json from nested workspaces", (it) => { |
| 32 | it.effect("returns the configured kind", () => |
| 33 | Effect.gen(function* () { |
| 34 | const fileSystem = yield* FileSystem.FileSystem; |
| 35 | const path = yield* Path.Path; |
| 36 | const root = yield* fileSystem.makeTempDirectoryScoped({ |
| 37 | prefix: "t3-vcs-config-test-", |
| 38 | }); |
| 39 | const configDir = path.join(root, ".t3code"); |
| 40 | const nested = path.join(root, "packages", "app"); |
| 41 | yield* fileSystem.makeDirectory(configDir, { recursive: true }); |
| 42 | yield* fileSystem.makeDirectory(nested, { recursive: true }); |
| 43 | yield* fileSystem.writeFileString( |
| 44 | path.join(configDir, "vcs.json"), |
| 45 | JSON.stringify({ vcs: { kind: "jj" } }), |
| 46 | ); |
| 47 | |
| 48 | const config = yield* VcsProjectConfig.VcsProjectConfig; |
| 49 | const kind = yield* config.resolveKind({ cwd: nested }); |
| 50 | |
| 51 | assert.equal(kind, "jj"); |
| 52 | }), |
| 53 | ); |
| 54 | }); |
| 55 | |
| 56 | it.layer(layerTest)("continues to parent configs after a candidate inspect failure", (it) => { |
| 57 | it.effect("logs the failed candidate and returns the parent config", () => { |
| 58 | const messages: unknown[] = []; |
| 59 | const logger = Logger.make<unknown, void>(({ message }) => { |
| 60 | messages.push(message); |
| 61 | }); |
| 62 | |
| 63 | return Effect.gen(function* () { |
| 64 | const fileSystem = yield* FileSystem.FileSystem; |
| 65 | const path = yield* Path.Path; |
| 66 | const root = yield* fileSystem.makeTempDirectoryScoped({ |
| 67 | prefix: "t3-vcs-config-test-", |
| 68 | }); |
| 69 | const configDir = path.join(root, ".t3code"); |
| 70 | const cwd = path.join(root, "invalid\0child"); |
| 71 | yield* fileSystem.makeDirectory(configDir, { recursive: true }); |
| 72 | yield* fileSystem.writeFileString( |
| 73 | path.join(configDir, "vcs.json"), |
| 74 | JSON.stringify({ vcs: { kind: "jj" } }), |
| 75 | ); |
| 76 | |
| 77 | const config = yield* VcsProjectConfig.VcsProjectConfig; |
| 78 | const kind = yield* config.resolveKind({ cwd }); |
| 79 | |
| 80 | assert.equal(kind, "jj"); |
| 81 | const failedCandidate = path.join(cwd, ".t3code", "vcs.json"); |
| 82 | const [error] = messages[0] as ReadonlyArray<unknown>; |
| 83 | assert.instanceOf(error, VcsProjectConfig.VcsProjectConfigError); |
| 84 | assert.equal( |
| 85 | error.message, |
| 86 | "Failed to inspect VCS project config at " + failedCandidate + ".", |
| 87 | ); |
| 88 | assert.deepInclude(error, { |
| 89 | operation: "inspect", |
| 90 | cwd, |
| 91 | configPath: failedCandidate, |
| 92 | _tag: "VcsProjectConfigError", |
| 93 | }); |
| 94 | }).pipe(Effect.provide(Logger.layer([logger], { mergeWithExisting: false }))); |
| 95 | }); |
| 96 | }); |
| 97 | |
| 98 | it.layer(layerTest)("falls back to auto when no config exists", (it) => { |
| 99 | it.effect("returns auto", () => |
| 100 | Effect.gen(function* () { |
| 101 | const fileSystem = yield* FileSystem.FileSystem; |
| 102 | const root = yield* fileSystem.makeTempDirectoryScoped({ |
| 103 | prefix: "t3-vcs-config-test-", |
| 104 | }); |
| 105 | const config = yield* VcsProjectConfig.VcsProjectConfig; |
| 106 | const kind = yield* config.resolveKind({ cwd: root }); |
| 107 | |
| 108 | assert.equal(kind, "auto"); |
| 109 | }), |
| 110 | ); |
| 111 | }); |
| 112 | |
| 113 | it.layer(layerTest)("falls back to auto when config JSON is malformed", (it) => { |
| 114 | it.effect("returns auto and logs the failed operation and path", () => { |
| 115 | const messages: unknown[] = []; |
| 116 | const logger = Logger.make<unknown, void>(({ message }) => { |
| 117 | messages.push(message); |
| 118 | }); |
| 119 | |
| 120 | return Effect.gen(function* () { |
| 121 | const fileSystem = yield* FileSystem.FileSystem; |
| 122 | const path = yield* Path.Path; |
| 123 | const root = yield* fileSystem.makeTempDirectoryScoped({ |
| 124 | prefix: "t3-vcs-config-test-", |
| 125 | }); |
| 126 | const configDir = path.join(root, ".t3code"); |
| 127 | yield* fileSystem.makeDirectory(configDir, { recursive: true }); |
| 128 | yield* fileSystem.writeFileString(path.join(configDir, "vcs.json"), "{not json"); |
| 129 | |
| 130 | const config = yield* VcsProjectConfig.VcsProjectConfig; |
| 131 | const kind = yield* config.resolveKind({ cwd: root }); |
| 132 | |
| 133 | assert.equal(kind, "auto"); |
| 134 | const [error] = messages[0] as ReadonlyArray<unknown>; |
| 135 | assert.instanceOf(error, VcsProjectConfig.VcsProjectConfigError); |
| 136 | assert.equal( |
| 137 | error.message, |
| 138 | "Failed to decode VCS project config at " + path.join(configDir, "vcs.json") + ".", |
| 139 | ); |
| 140 | assert.deepInclude(error.cause, { _tag: "SchemaError" }); |
| 141 | assert.deepInclude(error, { |
| 142 | operation: "decode", |
| 143 | cwd: root, |
| 144 | configPath: path.join(configDir, "vcs.json"), |
| 145 | _tag: "VcsProjectConfigError", |
| 146 | }); |
| 147 | }).pipe(Effect.provide(Logger.layer([logger], { mergeWithExisting: false }))); |
| 148 | }); |
| 149 | }); |
| 150 | |
| 151 | it.layer(layerTest)("falls back to auto when the config path cannot be read", (it) => { |
| 152 | it.effect("retains the read failure context", () => { |
| 153 | const messages: unknown[] = []; |
| 154 | const logger = Logger.make<unknown, void>(({ message }) => { |
| 155 | messages.push(message); |
| 156 | }); |
| 157 | |
| 158 | return Effect.gen(function* () { |
| 159 | const fileSystem = yield* FileSystem.FileSystem; |
| 160 | const path = yield* Path.Path; |
| 161 | const root = yield* fileSystem.makeTempDirectoryScoped({ |
| 162 | prefix: "t3-vcs-config-test-", |
| 163 | }); |
| 164 | const configPath = path.join(root, ".t3code", "vcs.json"); |
| 165 | yield* fileSystem.makeDirectory(configPath, { recursive: true }); |
| 166 | |
| 167 | const config = yield* VcsProjectConfig.VcsProjectConfig; |
| 168 | const kind = yield* config.resolveKind({ cwd: root }); |
| 169 | |
| 170 | assert.equal(kind, "auto"); |
| 171 | const [error] = messages[0] as ReadonlyArray<unknown>; |
| 172 | assert.instanceOf(error, VcsProjectConfig.VcsProjectConfigError); |
| 173 | assert.equal(error.message, "Failed to read VCS project config at " + configPath + "."); |
| 174 | assert.deepInclude(error.cause, { _tag: "PlatformError" }); |
| 175 | assert.deepInclude(error, { |
| 176 | operation: "read", |
| 177 | cwd: root, |
| 178 | configPath, |
| 179 | _tag: "VcsProjectConfigError", |
| 180 | }); |
| 181 | }).pipe(Effect.provide(Logger.layer([logger], { mergeWithExisting: false }))); |
| 182 | }); |
| 183 | }); |
| 184 | |
| 185 | it.layer(layerTest)("falls back to auto when config kind is invalid", (it) => { |
| 186 | it.effect("returns auto", () => |
| 187 | Effect.gen(function* () { |
| 188 | const fileSystem = yield* FileSystem.FileSystem; |
| 189 | const path = yield* Path.Path; |
| 190 | const root = yield* fileSystem.makeTempDirectoryScoped({ |
| 191 | prefix: "t3-vcs-config-test-", |
| 192 | }); |
| 193 | const configDir = path.join(root, ".t3code"); |
| 194 | yield* fileSystem.makeDirectory(configDir, { recursive: true }); |
| 195 | yield* fileSystem.writeFileString( |
| 196 | path.join(configDir, "vcs.json"), |
| 197 | `{"vcs":{"kind":"svn"}}`, |
| 198 | ); |
| 199 | |
| 200 | const config = yield* VcsProjectConfig.VcsProjectConfig; |
| 201 | const kind = yield* config.resolveKind({ cwd: root }); |
| 202 | |
| 203 | assert.equal(kind, "auto"); |
| 204 | }), |
| 205 | ); |
| 206 | }); |
| 207 | }); |