| ... | ... | @@ -3,6 +3,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | 3 | import * as Effect from "effect/Effect"; |
| 4 | 4 | import * as FileSystem from "effect/FileSystem"; |
| 5 | 5 | import * as Layer from "effect/Layer"; |
| 6 | import * as Logger from "effect/Logger"; |
| 6 | 7 | import * as Path from "effect/Path"; |
| 7 | 8 | |
| 8 | 9 | import * as VcsProjectConfig from "./VcsProjectConfig.ts"; |
| ... | ... | @@ -13,6 +14,22 @@ const TestLayer = VcsProjectConfig.layer.pipe( |
| 13 | 14 | ); |
| 14 | 15 | |
| 15 | 16 | describe("VcsProjectConfig", () => { |
| 17 | it("keeps operation context and the original cause on config errors", () => { |
| 18 | const cause = new Error("permission denied"); |
| 19 | const error = new VcsProjectConfig.VcsProjectConfigError({ |
| 20 | operation: "read", |
| 21 | cwd: "/repo/packages/app", |
| 22 | configPath: "/repo/.t3code/vcs.json", |
| 23 | cause, |
| 24 | }); |
| 25 | |
| 26 | assert.equal(error.operation, "read"); |
| 27 | assert.equal(error.cwd, "/repo/packages/app"); |
| 28 | assert.equal(error.configPath, "/repo/.t3code/vcs.json"); |
| 29 | assert.strictEqual(error.cause, cause); |
| 30 | assert.equal(error.message, "Failed to read VCS project config at /repo/.t3code/vcs.json."); |
| 31 | }); |
| 32 | |
| 16 | 33 | it.layer(TestLayer)("uses an explicit requested VCS kind before config", (it) => { |
| 17 | 34 | it.effect("returns the requested kind", () => |
| 18 | 35 | Effect.gen(function* () { |
| ... | ... | @@ -53,6 +70,46 @@ describe("VcsProjectConfig", () => { |
| 53 | 70 | ); |
| 54 | 71 | }); |
| 55 | 72 | |
| 73 | it.layer(TestLayer)("continues to parent configs after a candidate inspect failure", (it) => { |
| 74 | it.effect("logs the failed candidate and returns the parent config", () => { |
| 75 | const messages: unknown[] = []; |
| 76 | const logger = Logger.make<unknown, void>(({ message }) => { |
| 77 | messages.push(message); |
| 78 | }); |
| 79 | |
| 80 | return Effect.gen(function* () { |
| 81 | const fileSystem = yield* FileSystem.FileSystem; |
| 82 | const path = yield* Path.Path; |
| 83 | const root = yield* fileSystem.makeTempDirectoryScoped({ |
| 84 | prefix: "t3-vcs-config-test-", |
| 85 | }); |
| 86 | const configDir = path.join(root, ".t3code"); |
| 87 | const cwd = path.join(root, "invalid\0child"); |
| 88 | yield* fileSystem.makeDirectory(configDir, { recursive: true }); |
| 89 | yield* fileSystem.writeFileString( |
| 90 | path.join(configDir, "vcs.json"), |
| 91 | // @effect-diagnostics-next-line preferSchemaOverJson:off |
| 92 | JSON.stringify({ vcs: { kind: "jj" } }), |
| 93 | ); |
| 94 | |
| 95 | const config = yield* VcsProjectConfig.VcsProjectConfig; |
| 96 | const kind = yield* config.resolveKind({ cwd }); |
| 97 | |
| 98 | assert.equal(kind, "jj"); |
| 99 | const [message, context] = messages[0] as [string, Record<string, unknown>]; |
| 100 | const failedCandidate = path.join(cwd, ".t3code", "vcs.json"); |
| 101 | assert.equal(message, "Failed to inspect VCS project config at " + failedCandidate + "."); |
| 102 | assert.deepInclude(context, { |
| 103 | operation: "inspect", |
| 104 | cwd, |
| 105 | configPath: failedCandidate, |
| 106 | errorTag: "VcsProjectConfigError", |
| 107 | }); |
| 108 | assert.equal("cause" in context, false); |
| 109 | }).pipe(Effect.provide(Logger.layer([logger], { mergeWithExisting: false }))); |
| 110 | }); |
| 111 | }); |
| 112 | |
| 56 | 113 | it.layer(TestLayer)("falls back to auto when no config exists", (it) => { |
| 57 | 114 | it.effect("returns auto", () => |
| 58 | 115 | Effect.gen(function* () { |
| ... | ... | @@ -69,8 +126,13 @@ describe("VcsProjectConfig", () => { |
| 69 | 126 | }); |
| 70 | 127 | |
| 71 | 128 | it.layer(TestLayer)("falls back to auto when config JSON is malformed", (it) => { |
| 72 | | it.effect("returns auto", () => |
| 73 | | Effect.gen(function* () { |
| 129 | it.effect("returns auto and logs the failed operation and path", () => { |
| 130 | const messages: unknown[] = []; |
| 131 | const logger = Logger.make<unknown, void>(({ message }) => { |
| 132 | messages.push(message); |
| 133 | }); |
| 134 | |
| 135 | return Effect.gen(function* () { |
| 74 | 136 | const fileSystem = yield* FileSystem.FileSystem; |
| 75 | 137 | const path = yield* Path.Path; |
| 76 | 138 | const root = yield* fileSystem.makeTempDirectoryScoped({ |
| ... | ... | @@ -84,8 +146,53 @@ describe("VcsProjectConfig", () => { |
| 84 | 146 | const kind = yield* config.resolveKind({ cwd: root }); |
| 85 | 147 | |
| 86 | 148 | assert.equal(kind, "auto"); |
| 87 | | }), |
| 88 | | ); |
| 149 | const [message, context] = messages[0] as [string, Record<string, unknown>]; |
| 150 | assert.equal( |
| 151 | message, |
| 152 | "Failed to decode VCS project config at " + path.join(configDir, "vcs.json") + ".", |
| 153 | ); |
| 154 | assert.deepInclude(context, { |
| 155 | operation: "decode", |
| 156 | cwd: root, |
| 157 | configPath: path.join(configDir, "vcs.json"), |
| 158 | errorTag: "VcsProjectConfigError", |
| 159 | }); |
| 160 | assert.equal("cause" in context, false); |
| 161 | }).pipe(Effect.provide(Logger.layer([logger], { mergeWithExisting: false }))); |
| 162 | }); |
| 163 | }); |
| 164 | |
| 165 | it.layer(TestLayer)("falls back to auto when the config path cannot be read", (it) => { |
| 166 | it.effect("retains the read failure context", () => { |
| 167 | const messages: unknown[] = []; |
| 168 | const logger = Logger.make<unknown, void>(({ message }) => { |
| 169 | messages.push(message); |
| 170 | }); |
| 171 | |
| 172 | return Effect.gen(function* () { |
| 173 | const fileSystem = yield* FileSystem.FileSystem; |
| 174 | const path = yield* Path.Path; |
| 175 | const root = yield* fileSystem.makeTempDirectoryScoped({ |
| 176 | prefix: "t3-vcs-config-test-", |
| 177 | }); |
| 178 | const configPath = path.join(root, ".t3code", "vcs.json"); |
| 179 | yield* fileSystem.makeDirectory(configPath, { recursive: true }); |
| 180 | |
| 181 | const config = yield* VcsProjectConfig.VcsProjectConfig; |
| 182 | const kind = yield* config.resolveKind({ cwd: root }); |
| 183 | |
| 184 | assert.equal(kind, "auto"); |
| 185 | const [message, context] = messages[0] as [string, Record<string, unknown>]; |
| 186 | assert.equal(message, "Failed to read VCS project config at " + configPath + "."); |
| 187 | assert.deepInclude(context, { |
| 188 | operation: "read", |
| 189 | cwd: root, |
| 190 | configPath, |
| 191 | errorTag: "VcsProjectConfigError", |
| 192 | }); |
| 193 | assert.equal("cause" in context, false); |
| 194 | }).pipe(Effect.provide(Logger.layer([logger], { mergeWithExisting: false }))); |
| 195 | }); |
| 89 | 196 | }); |
| 90 | 197 | |
| 91 | 198 | it.layer(TestLayer)("falls back to auto when config kind is invalid", (it) => { |