| ... | @@ -1,11 +1,13 @@ | ... | @@ -1,11 +1,13 @@ |
| | 1 | import * as NodePath from "@effect/platform-node/NodePath"; |
| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { assert, it } from "@effect/vitest"; | 3 | import { assert, it } from "@effect/vitest"; |
| 3 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| 4 | import * as FileSystem from "effect/FileSystem"; | 5 | import * as FileSystem from "effect/FileSystem"; |
| 5 | import * as Layer from "effect/Layer"; | 6 | import * as Layer from "effect/Layer"; |
| | 7 | import * as PlatformError from "effect/PlatformError"; |
| 6 | import { ChildProcessSpawner } from "effect/unstable/process"; | 8 | import { ChildProcessSpawner } from "effect/unstable/process"; |
| 7 | | 9 | |
| 8 | import { GitCommandError, type SourceControlProviderError } from "@t3tools/contracts"; | 10 | import { GitCommandError, SourceControlProviderError } from "@t3tools/contracts"; |
| 9 | | 11 | |
| 10 | import * as ServerConfig from "../config.ts"; | 12 | import * as ServerConfig from "../config.ts"; |
| 11 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; | 13 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; |
| ... | @@ -54,8 +56,9 @@ function processOutput(): GitVcsDriver.ExecuteGitResult { | ... | @@ -54,8 +56,9 @@ function processOutput(): GitVcsDriver.ExecuteGitResult { |
| 54 | function makeLayer(input: { | 56 | function makeLayer(input: { |
| 55 | readonly provider?: SourceControlProvider.SourceControlProvider["Service"]; | 57 | readonly provider?: SourceControlProvider.SourceControlProvider["Service"]; |
| 56 | readonly git?: Partial<GitVcsDriver.GitVcsDriver["Service"]>; | 58 | readonly git?: Partial<GitVcsDriver.GitVcsDriver["Service"]>; |
| | 59 | readonly fileSystem?: FileSystem.FileSystem; |
| 57 | }) { | 60 | }) { |
| 58 | return SourceControlRepositoryService.layer.pipe( | 61 | const serviceLayer = SourceControlRepositoryService.layer.pipe( |
| 59 | Layer.provide( | 62 | Layer.provide( |
| 60 | Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({ | 63 | Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({ |
| 61 | get: () => Effect.succeed(input.provider ?? makeProvider()), | 64 | get: () => Effect.succeed(input.provider ?? makeProvider()), |
| ... | @@ -75,9 +78,20 @@ function makeLayer(input: { | ... | @@ -75,9 +78,20 @@ function makeLayer(input: { |
| 75 | ...input.git, | 78 | ...input.git, |
| 76 | }), | 79 | }), |
| 77 | ), | 80 | ), |
| 78 | Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-source-control-repos-" })), | 81 | Layer.provide( |
| 79 | Layer.provideMerge(NodeServices.layer), | 82 | ServerConfig.layerTest( |
| | 83 | process.cwd(), |
| | 84 | input.fileSystem ? "/tmp/t3-source-control-repos" : { prefix: "t3-source-control-repos-" }, |
| | 85 | ), |
| | 86 | ), |
| 80 | ); | 87 | ); |
| | 88 | |
| | 89 | return input.fileSystem |
| | 90 | ? serviceLayer.pipe( |
| | 91 | Layer.provide(Layer.succeed(FileSystem.FileSystem, input.fileSystem)), |
| | 92 | Layer.provideMerge(NodePath.layer), |
| | 93 | ) |
| | 94 | : serviceLayer.pipe(Layer.provideMerge(NodeServices.layer)); |
| 81 | } | 95 | } |
| 82 | | 96 | |
| 83 | it.effect("looks up repositories through the requested provider without search", () => { | 97 | it.effect("looks up repositories through the requested provider without search", () => { |
| ... | @@ -103,6 +117,39 @@ it.effect("looks up repositories through the requested provider without search", | ... | @@ -103,6 +117,39 @@ it.effect("looks up repositories through the requested provider without search", |
| 103 | }).pipe(Effect.provide(makeLayer({ provider }))); | 117 | }).pipe(Effect.provide(makeLayer({ provider }))); |
| 104 | }); | 118 | }); |
| 105 | | 119 | |
| | 120 | it.effect("preserves provider failures without deriving the repository message from them", () => { |
| | 121 | const providerCause = new SourceControlProviderError({ |
| | 122 | provider: "github", |
| | 123 | operation: "getRepositoryCloneUrls", |
| | 124 | cwd: "/workspace", |
| | 125 | repository: "octocat/t3code", |
| | 126 | detail: "credential token abc123 was rejected", |
| | 127 | }); |
| | 128 | const provider = makeProvider({ |
| | 129 | getRepositoryCloneUrls: () => Effect.fail(providerCause), |
| | 130 | }); |
| | 131 | |
| | 132 | return Effect.gen(function* () { |
| | 133 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| | 134 | const error = yield* Effect.flip( |
| | 135 | service.lookupRepository({ |
| | 136 | provider: "github", |
| | 137 | repository: "octocat/t3code", |
| | 138 | cwd: "/workspace", |
| | 139 | }), |
| | 140 | ); |
| | 141 | |
| | 142 | assert.strictEqual(error.provider, "github"); |
| | 143 | assert.strictEqual(error.operation, "lookupRepository"); |
| | 144 | assert.strictEqual(error.detail, "The source control operation could not be completed."); |
| | 145 | assert.strictEqual( |
| | 146 | error.message, |
| | 147 | "Source control repository operation lookupRepository failed for github: The source control operation could not be completed.", |
| | 148 | ); |
| | 149 | assert.strictEqual(error.cause, providerCause); |
| | 150 | }).pipe(Effect.provide(makeLayer({ provider }))); |
| | 151 | }); |
| | 152 | |
| 106 | it.effect("clones a looked-up repository into the requested destination", () => | 153 | it.effect("clones a looked-up repository into the requested destination", () => |
| 107 | Effect.gen(function* () { | 154 | Effect.gen(function* () { |
| 108 | const fs = yield* FileSystem.FileSystem; | 155 | const fs = yield* FileSystem.FileSystem; |
| ... | @@ -148,6 +195,38 @@ it.effect("clones a looked-up repository into the requested destination", () => | ... | @@ -148,6 +195,38 @@ it.effect("clones a looked-up repository into the requested destination", () => |
| 148 | }).pipe(Effect.provide(NodeServices.layer)), | 195 | }).pipe(Effect.provide(NodeServices.layer)), |
| 149 | ); | 196 | ); |
| 150 | | 197 | |
| | 198 | it.effect("preserves destination probe failures instead of treating them as missing paths", () => { |
| | 199 | const fileSystemCause = PlatformError.systemError({ |
| | 200 | _tag: "PermissionDenied", |
| | 201 | module: "FileSystem", |
| | 202 | method: "exists", |
| | 203 | pathOrDescriptor: "/restricted/t3code", |
| | 204 | }); |
| | 205 | |
| | 206 | return Effect.gen(function* () { |
| | 207 | const service = yield* SourceControlRepositoryService.SourceControlRepositoryService; |
| | 208 | const error = yield* Effect.flip( |
| | 209 | service.cloneRepository({ |
| | 210 | remoteUrl: CLONE_URLS.sshUrl, |
| | 211 | destinationPath: "/restricted/t3code", |
| | 212 | }), |
| | 213 | ); |
| | 214 | |
| | 215 | assert.strictEqual(error.provider, "unknown"); |
| | 216 | assert.strictEqual(error.operation, "cloneRepository"); |
| | 217 | assert.strictEqual(error.cause, fileSystemCause); |
| | 218 | }).pipe( |
| | 219 | Effect.provide( |
| | 220 | makeLayer({ |
| | 221 | fileSystem: FileSystem.makeNoop({ |
| | 222 | exists: () => Effect.fail(fileSystemCause), |
| | 223 | makeDirectory: () => Effect.void, |
| | 224 | }), |
| | 225 | }), |
| | 226 | ), |
| | 227 | ); |
| | 228 | }); |
| | 229 | |
| 151 | it.effect("publishes by creating the repository, adding a remote, and pushing upstream", () => { | 230 | it.effect("publishes by creating the repository, adding a remote, and pushing upstream", () => { |
| 152 | const createCalls: Array<{ cwd: string; repository: string; visibility: string }> = []; | 231 | const createCalls: Array<{ cwd: string; repository: string; visibility: string }> = []; |
| 153 | const remoteCalls: Array<{ cwd: string; preferredName: string; url: string }> = []; | 232 | const remoteCalls: Array<{ cwd: string; preferredName: string; url: string }> = []; |