| ... | @@ -3,6 +3,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; | ... | @@ -3,6 +3,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as FileSystem from "effect/FileSystem"; | 4 | import * as FileSystem from "effect/FileSystem"; |
| 5 | import * as Layer from "effect/Layer"; | 5 | import * as Layer from "effect/Layer"; |
| | 6 | import * as Logger from "effect/Logger"; |
| 6 | import * as Path from "effect/Path"; | 7 | import * as Path from "effect/Path"; |
| 7 | | 8 | |
| 8 | import * as VcsProjectConfig from "./VcsProjectConfig.ts"; | 9 | import * as VcsProjectConfig from "./VcsProjectConfig.ts"; |
| ... | @@ -13,6 +14,22 @@ const TestLayer = VcsProjectConfig.layer.pipe( | ... | @@ -13,6 +14,22 @@ const TestLayer = VcsProjectConfig.layer.pipe( |
| 13 | ); | 14 | ); |
| 14 | | 15 | |
| 15 | describe("VcsProjectConfig", () => { | 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 | it.layer(TestLayer)("uses an explicit requested VCS kind before config", (it) => { | 33 | it.layer(TestLayer)("uses an explicit requested VCS kind before config", (it) => { |
| 17 | it.effect("returns the requested kind", () => | 34 | it.effect("returns the requested kind", () => |
| 18 | Effect.gen(function* () { | 35 | Effect.gen(function* () { |
| ... | @@ -53,6 +70,46 @@ describe("VcsProjectConfig", () => { | ... | @@ -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 | it.layer(TestLayer)("falls back to auto when no config exists", (it) => { | 113 | it.layer(TestLayer)("falls back to auto when no config exists", (it) => { |
| 57 | it.effect("returns auto", () => | 114 | it.effect("returns auto", () => |
| 58 | Effect.gen(function* () { | 115 | Effect.gen(function* () { |
| ... | @@ -69,8 +126,13 @@ describe("VcsProjectConfig", () => { | ... | @@ -69,8 +126,13 @@ describe("VcsProjectConfig", () => { |
| 69 | }); | 126 | }); |
| 70 | | 127 | |
| 71 | it.layer(TestLayer)("falls back to auto when config JSON is malformed", (it) => { | 128 | it.layer(TestLayer)("falls back to auto when config JSON is malformed", (it) => { |
| 72 | it.effect("returns auto", () => | 129 | it.effect("returns auto and logs the failed operation and path", () => { |
| 73 | Effect.gen(function* () { | 130 | const messages: unknown[] = []; |
| | 131 | const logger = Logger.make<unknown, void>(({ message }) => { |
| | 132 | messages.push(message); |
| | 133 | }); |
| | 134 | |
| | 135 | return Effect.gen(function* () { |
| 74 | const fileSystem = yield* FileSystem.FileSystem; | 136 | const fileSystem = yield* FileSystem.FileSystem; |
| 75 | const path = yield* Path.Path; | 137 | const path = yield* Path.Path; |
| 76 | const root = yield* fileSystem.makeTempDirectoryScoped({ | 138 | const root = yield* fileSystem.makeTempDirectoryScoped({ |
| ... | @@ -84,8 +146,53 @@ describe("VcsProjectConfig", () => { | ... | @@ -84,8 +146,53 @@ describe("VcsProjectConfig", () => { |
| 84 | const kind = yield* config.resolveKind({ cwd: root }); | 146 | const kind = yield* config.resolveKind({ cwd: root }); |
| 85 | | 147 | |
| 86 | assert.equal(kind, "auto"); | 148 | assert.equal(kind, "auto"); |
| 87 | }), | 149 | const [message, context] = messages[0] as [string, Record<string, unknown>]; |
| 88 | ); | 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 | it.layer(TestLayer)("falls back to auto when config kind is invalid", (it) => { | 198 | it.layer(TestLayer)("falls back to auto when config kind is invalid", (it) => { |