import { assert, it, describe } from "@effect/vitest"; import * as NodeServices from "@effect/platform-node/NodeServices"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Logger from "effect/Logger"; import * as Path from "effect/Path"; import * as VcsProjectConfig from "./VcsProjectConfig.ts"; const TestLayer = VcsProjectConfig.layer.pipe( Layer.provide(NodeServices.layer), Layer.provideMerge(NodeServices.layer), ); describe("VcsProjectConfig", () => { it("keeps operation context and the original cause on config errors", () => { const cause = new Error("permission denied"); const error = new VcsProjectConfig.VcsProjectConfigError({ operation: "read", cwd: "/repo/packages/app", configPath: "/repo/.t3code/vcs.json", cause, }); assert.equal(error.operation, "read"); assert.equal(error.cwd, "/repo/packages/app"); assert.equal(error.configPath, "/repo/.t3code/vcs.json"); assert.strictEqual(error.cause, cause); assert.equal(error.message, "Failed to read VCS project config at /repo/.t3code/vcs.json."); }); it.layer(TestLayer)("uses an explicit requested VCS kind before config", (it) => { it.effect("returns the requested kind", () => Effect.gen(function* () { const config = yield* VcsProjectConfig.VcsProjectConfig; const kind = yield* config.resolveKind({ cwd: "/repo", requestedKind: "jj", }); assert.equal(kind, "jj"); }), ); }); it.layer(TestLayer)("discovers .t3code/vcs.json from nested workspaces", (it) => { it.effect("returns the configured kind", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-vcs-config-test-", }); const configDir = path.join(root, ".t3code"); const nested = path.join(root, "packages", "app"); yield* fileSystem.makeDirectory(configDir, { recursive: true }); yield* fileSystem.makeDirectory(nested, { recursive: true }); yield* fileSystem.writeFileString( path.join(configDir, "vcs.json"), // @effect-diagnostics-next-line preferSchemaOverJson:off JSON.stringify({ vcs: { kind: "jj" } }), ); const config = yield* VcsProjectConfig.VcsProjectConfig; const kind = yield* config.resolveKind({ cwd: nested }); assert.equal(kind, "jj"); }), ); }); it.layer(TestLayer)("continues to parent configs after a candidate inspect failure", (it) => { it.effect("logs the failed candidate and returns the parent config", () => { const messages: unknown[] = []; const logger = Logger.make(({ message }) => { messages.push(message); }); return Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-vcs-config-test-", }); const configDir = path.join(root, ".t3code"); const cwd = path.join(root, "invalid\0child"); yield* fileSystem.makeDirectory(configDir, { recursive: true }); yield* fileSystem.writeFileString( path.join(configDir, "vcs.json"), // @effect-diagnostics-next-line preferSchemaOverJson:off JSON.stringify({ vcs: { kind: "jj" } }), ); const config = yield* VcsProjectConfig.VcsProjectConfig; const kind = yield* config.resolveKind({ cwd }); assert.equal(kind, "jj"); const failedCandidate = path.join(cwd, ".t3code", "vcs.json"); const [error] = messages[0] as ReadonlyArray; assert.instanceOf(error, VcsProjectConfig.VcsProjectConfigError); assert.equal( error.message, "Failed to inspect VCS project config at " + failedCandidate + ".", ); assert.deepInclude(error, { operation: "inspect", cwd, configPath: failedCandidate, _tag: "VcsProjectConfigError", }); }).pipe(Effect.provide(Logger.layer([logger], { mergeWithExisting: false }))); }); }); it.layer(TestLayer)("falls back to auto when no config exists", (it) => { it.effect("returns auto", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const root = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-vcs-config-test-", }); const config = yield* VcsProjectConfig.VcsProjectConfig; const kind = yield* config.resolveKind({ cwd: root }); assert.equal(kind, "auto"); }), ); }); it.layer(TestLayer)("falls back to auto when config JSON is malformed", (it) => { it.effect("returns auto and logs the failed operation and path", () => { const messages: unknown[] = []; const logger = Logger.make(({ message }) => { messages.push(message); }); return Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-vcs-config-test-", }); const configDir = path.join(root, ".t3code"); yield* fileSystem.makeDirectory(configDir, { recursive: true }); yield* fileSystem.writeFileString(path.join(configDir, "vcs.json"), "{not json"); const config = yield* VcsProjectConfig.VcsProjectConfig; const kind = yield* config.resolveKind({ cwd: root }); assert.equal(kind, "auto"); const [error] = messages[0] as ReadonlyArray; assert.instanceOf(error, VcsProjectConfig.VcsProjectConfigError); assert.equal( error.message, "Failed to decode VCS project config at " + path.join(configDir, "vcs.json") + ".", ); assert.deepInclude(error.cause, { _tag: "SchemaError" }); assert.deepInclude(error, { operation: "decode", cwd: root, configPath: path.join(configDir, "vcs.json"), _tag: "VcsProjectConfigError", }); }).pipe(Effect.provide(Logger.layer([logger], { mergeWithExisting: false }))); }); }); it.layer(TestLayer)("falls back to auto when the config path cannot be read", (it) => { it.effect("retains the read failure context", () => { const messages: unknown[] = []; const logger = Logger.make(({ message }) => { messages.push(message); }); return Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-vcs-config-test-", }); const configPath = path.join(root, ".t3code", "vcs.json"); yield* fileSystem.makeDirectory(configPath, { recursive: true }); const config = yield* VcsProjectConfig.VcsProjectConfig; const kind = yield* config.resolveKind({ cwd: root }); assert.equal(kind, "auto"); const [error] = messages[0] as ReadonlyArray; assert.instanceOf(error, VcsProjectConfig.VcsProjectConfigError); assert.equal(error.message, "Failed to read VCS project config at " + configPath + "."); assert.deepInclude(error.cause, { _tag: "PlatformError" }); assert.deepInclude(error, { operation: "read", cwd: root, configPath, _tag: "VcsProjectConfigError", }); }).pipe(Effect.provide(Logger.layer([logger], { mergeWithExisting: false }))); }); }); it.layer(TestLayer)("falls back to auto when config kind is invalid", (it) => { it.effect("returns auto", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-vcs-config-test-", }); const configDir = path.join(root, ".t3code"); yield* fileSystem.makeDirectory(configDir, { recursive: true }); yield* fileSystem.writeFileString( path.join(configDir, "vcs.json"), `{"vcs":{"kind":"svn"}}`, ); const config = yield* VcsProjectConfig.VcsProjectConfig; const kind = yield* config.resolveKind({ cwd: root }); assert.equal(kind, "auto"); }), ); }); });