Showing the changes to apps/server/src/sourceControl/SourceControlRepositoryService.test.ts (1 of 33 files). Show all
Unified · Split
| @@ -0,0 +1,322 @@ | |||
| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| 2 | import { assert, it } from "@effect/vitest"; | ||
| 3 | import { Effect, FileSystem, Layer } from "effect"; | ||
| 4 | import { ChildProcessSpawner } from "effect/unstable/process"; | ||
| 5 | |||
| 6 | import { GitCommandError, type SourceControlProviderError } from "@t3tools/contracts"; | ||
| 7 | |||
| 8 | import { ServerConfig } from "../config.ts"; | ||
| 9 | import { | ||
| 10 | GitVcsDriver, | ||
| 11 | type ExecuteGitResult, | ||
| 12 | type GitVcsDriverShape, | ||
| 13 | } from "../vcs/GitVcsDriver.ts"; | ||
| 14 | import { SourceControlProviderRegistry } from "./SourceControlProviderRegistry.ts"; | ||
| 15 | import type { SourceControlProviderShape } from "./SourceControlProvider.ts"; | ||
| 16 | import { SourceControlRepositoryService, layer } from "./SourceControlRepositoryService.ts"; | ||
| 17 | |||
| 18 | const CLONE_URLS = { | ||
| 19 | nameWithOwner: "octocat/t3code", | ||
| 20 | url: "https://github.com/octocat/t3code", | ||
| 21 | sshUrl: "git@github.com:octocat/t3code.git", | ||
| 22 | }; | ||
| 23 | |||
| 24 | function makeProvider( | ||
| 25 | overrides: Partial<SourceControlProviderShape> = {}, | ||
| 26 | ): SourceControlProviderShape { | ||
| 27 | const unsupported = (operation: string) => | ||
| 28 | Effect.die(`unexpected provider operation ${operation}`) as Effect.Effect< | ||
| 29 | never, | ||
| 30 | SourceControlProviderError | ||
| 31 | >; | ||
| 32 | |||
| 33 | return { | ||
| 34 | kind: "github", | ||
| 35 | listChangeRequests: () => unsupported("listChangeRequests"), | ||
| 36 | getChangeRequest: () => unsupported("getChangeRequest"), | ||
| 37 | createChangeRequest: () => unsupported("createChangeRequest"), | ||
| 38 | getRepositoryCloneUrls: () => Effect.succeed(CLONE_URLS), | ||
| 39 | createRepository: () => Effect.succeed(CLONE_URLS), | ||
| 40 | getDefaultBranch: () => Effect.succeed(null), | ||
| 41 | checkoutChangeRequest: () => unsupported("checkoutChangeRequest"), | ||
| 42 | ...overrides, | ||
| 43 | }; | ||
| 44 | } | ||
| 45 | |||
| 46 | function processOutput(): ExecuteGitResult { | ||
| 47 | return { | ||
| 48 | exitCode: ChildProcessSpawner.ExitCode(0), | ||
| 49 | stdout: "", | ||
| 50 | stderr: "", | ||
| 51 | stdoutTruncated: false, | ||
| 52 | stderrTruncated: false, | ||
| 53 | }; | ||
| 54 | } | ||
| 55 | |||
| 56 | function makeLayer(input: { | ||
| 57 | readonly provider?: SourceControlProviderShape; | ||
| 58 | readonly git?: Partial<GitVcsDriverShape>; | ||
| 59 | }) { | ||
| 60 | return layer.pipe( | ||
| 61 | Layer.provide( | ||
| 62 | Layer.mock(SourceControlProviderRegistry)({ | ||
| 63 | get: () => Effect.succeed(input.provider ?? makeProvider()), | ||
| 64 | }), | ||
| 65 | ), | ||
| 66 | Layer.provide( | ||
| 67 | Layer.mock(GitVcsDriver)({ | ||
| 68 | execute: () => Effect.succeed(processOutput()), | ||
| 69 | ensureRemote: () => Effect.succeed("origin"), | ||
| 70 | pushCurrentBranch: () => | ||
| 71 | Effect.succeed({ | ||
| 72 | status: "pushed" as const, | ||
| 73 | branch: "feature/remote-v1", | ||
| 74 | upstreamBranch: "origin/feature/remote-v1", | ||
| 75 | setUpstream: true, | ||
| 76 | }), | ||
| 77 | ...input.git, | ||
| 78 | }), | ||
| 79 | ), | ||
| 80 | Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-source-control-repos-" })), | ||
| 81 | Layer.provideMerge(NodeServices.layer), | ||
| 82 | ); | ||
| 83 | } | ||
| 84 | |||
| 85 | it.effect("looks up repositories through the requested provider without search", () => { | ||
| 86 | const calls: Array<{ cwd: string; repository: string }> = []; | ||
| 87 | const provider = makeProvider({ | ||
| 88 | getRepositoryCloneUrls: (input) => | ||
| 89 | Effect.sync(() => { | ||
| 90 | calls.push({ cwd: input.cwd, repository: input.repository }); | ||
| 91 | return CLONE_URLS; | ||
| 92 | }), | ||
| 93 | }); | ||
| 94 | |||
| 95 | return Effect.gen(function* () { | ||
| 96 | const service = yield* SourceControlRepositoryService; | ||
| 97 | const result = yield* service.lookupRepository({ | ||
| 98 | provider: "github", | ||
| 99 | repository: "octocat/t3code", | ||
| 100 | cwd: "/workspace", | ||
| 101 | }); | ||
| 102 | |||
| 103 | assert.deepStrictEqual(result, { provider: "github", ...CLONE_URLS }); | ||
| 104 | assert.deepStrictEqual(calls, [{ cwd: "/workspace", repository: "octocat/t3code" }]); | ||
| 105 | }).pipe(Effect.provide(makeLayer({ provider }))); | ||
| 106 | }); | ||
| 107 | |||
| 108 | it.effect("clones a looked-up repository into the requested destination", () => | ||
| 109 | Effect.gen(function* () { | ||
| 110 | const fs = yield* FileSystem.FileSystem; | ||
| 111 | const parent = yield* fs.makeTempDirectoryScoped({ | ||
| 112 | prefix: "t3-source-control-clone-parent-", | ||
| 113 | }); | ||
| 114 | const destinationPath = `${parent}/t3code`; | ||
| 115 | const cloneCalls: Array<{ cwd: string; args: ReadonlyArray<string> }> = []; | ||
| 116 | |||
| 117 | yield* Effect.gen(function* () { | ||
| 118 | const service = yield* SourceControlRepositoryService; | ||
| 119 | const result = yield* service.cloneRepository({ | ||
| 120 | provider: "github", | ||
| 121 | repository: "octocat/t3code", | ||
| 122 | destinationPath, | ||
| 123 | protocol: "https", | ||
| 124 | }); | ||
| 125 | |||
| 126 | assert.deepStrictEqual(result, { | ||
| 127 | cwd: destinationPath, | ||
| 128 | remoteUrl: CLONE_URLS.url, | ||
| 129 | repository: { provider: "github", ...CLONE_URLS }, | ||
| 130 | }); | ||
| 131 | assert.deepStrictEqual(cloneCalls, [ | ||
| 132 | { | ||
| 133 | cwd: parent, | ||
| 134 | args: ["clone", CLONE_URLS.url, "t3code"], | ||
| 135 | }, | ||
| 136 | ]); | ||
| 137 | }).pipe( | ||
| 138 | Effect.provide( | ||
| 139 | makeLayer({ | ||
| 140 | git: { | ||
| 141 | execute: (input) => | ||
| 142 | Effect.sync(() => { | ||
| 143 | cloneCalls.push({ cwd: input.cwd, args: input.args }); | ||
| 144 | return processOutput(); | ||
| 145 | }), | ||
| 146 | }, | ||
| 147 | }), | ||
| 148 | ), | ||
| 149 | ); | ||
| 150 | }).pipe(Effect.provide(NodeServices.layer)), | ||
| 151 | ); | ||
| 152 | |||
| 153 | it.effect("publishes by creating the repository, adding a remote, and pushing upstream", () => { | ||
| 154 | const createCalls: Array<{ cwd: string; repository: string; visibility: string }> = []; | ||
| 155 | const remoteCalls: Array<{ cwd: string; preferredName: string; url: string }> = []; | ||
| 156 | const pushCalls: Array<{ cwd: string; remoteName: string | null | undefined }> = []; | ||
| 157 | const provider = makeProvider({ | ||
| 158 | createRepository: (input) => | ||
| 159 | Effect.sync(() => { | ||
| 160 | createCalls.push({ | ||
| 161 | cwd: input.cwd, | ||
| 162 | repository: input.repository, | ||
| 163 | visibility: input.visibility, | ||
| 164 | }); | ||
| 165 | return CLONE_URLS; | ||
| 166 | }), | ||
| 167 | }); | ||
| 168 | |||
| 169 | return Effect.gen(function* () { | ||
| 170 | const service = yield* SourceControlRepositoryService; | ||
| 171 | const result = yield* service.publishRepository({ | ||
| 172 | cwd: "/workspace", | ||
| 173 | provider: "github", | ||
| 174 | repository: "octocat/t3code", | ||
| 175 | visibility: "private", | ||
| 176 | remoteName: "origin", | ||
| 177 | protocol: "ssh", | ||
| 178 | }); | ||
| 179 | |||
| 180 | assert.deepStrictEqual(result, { | ||
| 181 | repository: { provider: "github", ...CLONE_URLS }, | ||
| 182 | remoteName: "origin", | ||
| 183 | remoteUrl: CLONE_URLS.sshUrl, | ||
| 184 | branch: "feature/remote-v1", | ||
| 185 | upstreamBranch: "origin/feature/remote-v1", | ||
| 186 | status: "pushed", | ||
| 187 | }); | ||
| 188 | assert.deepStrictEqual(createCalls, [ | ||
| 189 | { cwd: "/workspace", repository: "octocat/t3code", visibility: "private" }, | ||
| 190 | ]); | ||
| 191 | assert.deepStrictEqual(remoteCalls, [ | ||
| 192 | { cwd: "/workspace", preferredName: "origin", url: CLONE_URLS.sshUrl }, | ||
| 193 | ]); | ||
| 194 | assert.deepStrictEqual(pushCalls, [{ cwd: "/workspace", remoteName: "origin" }]); | ||
| 195 | }).pipe( | ||
| 196 | Effect.provide( | ||
| 197 | makeLayer({ | ||
| 198 | provider, | ||
| 199 | git: { | ||
| 200 | ensureRemote: (input) => | ||
| 201 | Effect.sync(() => { | ||
| 202 | remoteCalls.push(input); | ||
| 203 | return "origin"; | ||
| 204 | }), | ||
| 205 | pushCurrentBranch: (cwd, _fallbackBranch, options) => | ||
| 206 | Effect.sync(() => { | ||
| 207 | pushCalls.push({ cwd, remoteName: options?.remoteName }); | ||
| 208 | return { | ||
| 209 | status: "pushed" as const, | ||
| 210 | branch: "feature/remote-v1", | ||
| 211 | upstreamBranch: "origin/feature/remote-v1", | ||
| 212 | setUpstream: true, | ||
| 213 | }; | ||
| 214 | }), | ||
| 215 | }, | ||
| 216 | }), | ||
| 217 | ), | ||
| 218 | ); | ||
| 219 | }); | ||
| 220 | |||
| 221 | it.effect("publishes to the remote name returned by ensureRemote", () => { | ||
| 222 | const pushCalls: Array<{ cwd: string; remoteName: string | null | undefined }> = []; | ||
| 223 | |||
| 224 | return Effect.gen(function* () { | ||
| 225 | const service = yield* SourceControlRepositoryService; | ||
| 226 | const result = yield* service.publishRepository({ | ||
| 227 | cwd: "/workspace", | ||
| 228 | provider: "github", | ||
| 229 | repository: "octocat/t3code", | ||
| 230 | visibility: "private", | ||
| 231 | remoteName: "origin", | ||
| 232 | protocol: "ssh", | ||
| 233 | }); | ||
| 234 | |||
| 235 | assert.equal(result.remoteName, "origin-1"); | ||
| 236 | assert.deepStrictEqual(pushCalls, [{ cwd: "/workspace", remoteName: "origin-1" }]); | ||
| 237 | }).pipe( | ||
| 238 | Effect.provide( | ||
| 239 | makeLayer({ | ||
| 240 | git: { | ||
| 241 | ensureRemote: () => Effect.succeed("origin-1"), | ||
| 242 | pushCurrentBranch: (cwd, _fallbackBranch, options) => | ||
| 243 | Effect.sync(() => { | ||
| 244 | pushCalls.push({ cwd, remoteName: options?.remoteName }); | ||
| 245 | return { | ||
| 246 | status: "pushed" as const, | ||
| 247 | branch: "feature/remote-v1", | ||
| 248 | upstreamBranch: `${options?.remoteName ?? "missing"}/feature/remote-v1`, | ||
| 249 | setUpstream: true, | ||
| 250 | }; | ||
| 251 | }), | ||
| 252 | }, | ||
| 253 | }), | ||
| 254 | ), | ||
| 255 | ); | ||
| 256 | }); | ||
| 257 | |||
| 258 | it.effect("publish succeeds with status remote_added when the local repo has no commits", () => { | ||
| 259 | let pushCalls = 0; | ||
| 260 | return Effect.gen(function* () { | ||
| 261 | const service = yield* SourceControlRepositoryService; | ||
| 262 | const result = yield* service.publishRepository({ | ||
| 263 | cwd: "/workspace", | ||
| 264 | provider: "github", | ||
| 265 | repository: "octocat/t3code", | ||
| 266 | visibility: "private", | ||
| 267 | remoteName: "origin", | ||
| 268 | protocol: "ssh", | ||
| 269 | }); | ||
| 270 | |||
| 271 | assert.deepStrictEqual(result, { | ||
| 272 | repository: { provider: "github", ...CLONE_URLS }, | ||
| 273 | remoteName: "origin", | ||
| 274 | remoteUrl: CLONE_URLS.sshUrl, | ||
| 275 | branch: "main", | ||
| 276 | status: "remote_added", | ||
| 277 | }); | ||
| 278 | assert.strictEqual(pushCalls, 0); | ||
| 279 | }).pipe( | ||
| 280 | Effect.provide( | ||
| 281 | makeLayer({ | ||
| 282 | git: { | ||
| 283 | execute: (input) => | ||
| 284 | input.args[0] === "rev-parse" | ||
| 285 | ? Effect.fail( | ||
| 286 | new GitCommandError({ | ||
| 287 | operation: input.operation, | ||
| 288 | command: "git rev-parse --verify HEAD", | ||
| 289 | cwd: input.cwd, | ||
| 290 | detail: "fatal: Needed a single revision", | ||
| 291 | }), | ||
| 292 | ) | ||
| 293 | : Effect.succeed(processOutput()), | ||
| 294 | statusDetails: () => | ||
| 295 | Effect.succeed({ | ||
| 296 | isRepo: true, | ||
| 297 | hasOriginRemote: true, | ||
| 298 | isDefaultBranch: true, | ||
| 299 | branch: "main", | ||
| 300 | upstreamRef: null, | ||
| 301 | hasWorkingTreeChanges: false, | ||
| 302 | workingTree: { files: [], insertions: 0, deletions: 0 }, | ||
| 303 | hasUpstream: false, | ||
| 304 | aheadCount: 0, | ||
| 305 | behindCount: 0, | ||
| 306 | aheadOfDefaultCount: 0, | ||
| 307 | }), | ||
| 308 | pushCurrentBranch: () => | ||
| 309 | Effect.sync(() => { | ||
| 310 | pushCalls += 1; | ||
| 311 | return { | ||
| 312 | status: "pushed" as const, | ||
| 313 | branch: "main", | ||
| 314 | upstreamBranch: "origin/main", | ||
| 315 | setUpstream: true, | ||
| 316 | }; | ||
| 317 | }), | ||
| 318 | }, | ||
| 319 | }), | ||
| 320 | ), | ||
| 321 | ); | ||
| 322 | }); | ||