| ... | @@ -1,6 +1,12 @@ | ... | @@ -1,6 +1,12 @@ |
| 1 | import { assert, it } from "@effect/vitest"; | 1 | import { assert, it } from "@effect/vitest"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import { CommandId, GitCommandError, ProjectId, ThreadId } from "@t3tools/contracts"; | 3 | import { |
| | 4 | CommandId, |
| | 5 | GitCommandError, |
| | 6 | ProjectId, |
| | 7 | ThreadId, |
| | 8 | VcsProcessSpawnError, |
| | 9 | } from "@t3tools/contracts"; |
| 4 | import * as Deferred from "effect/Deferred"; | 10 | import * as Deferred from "effect/Deferred"; |
| 5 | import * as Effect from "effect/Effect"; | 11 | import * as Effect from "effect/Effect"; |
| 6 | import * as Fiber from "effect/Fiber"; | 12 | import * as Fiber from "effect/Fiber"; |
| ... | @@ -8,9 +14,11 @@ import * as FileSystem from "effect/FileSystem"; | ... | @@ -8,9 +14,11 @@ import * as FileSystem from "effect/FileSystem"; |
| 8 | import * as Layer from "effect/Layer"; | 14 | import * as Layer from "effect/Layer"; |
| 9 | import * as Option from "effect/Option"; | 15 | import * as Option from "effect/Option"; |
| 10 | import * as Path from "effect/Path"; | 16 | import * as Path from "effect/Path"; |
| | 17 | import * as PlatformError from "effect/PlatformError"; |
| 11 | import * as Scope from "effect/Scope"; | 18 | import * as Scope from "effect/Scope"; |
| 12 | | 19 | |
| 13 | import * as ServerConfig from "../config.ts"; | 20 | import * as ServerConfig from "../config.ts"; |
| | 21 | import * as GitManager from "../git/GitManager.ts"; |
| 14 | import * as GitWorkflow from "../git/GitWorkflowService.ts"; | 22 | import * as GitWorkflow from "../git/GitWorkflowService.ts"; |
| 15 | import * as RuntimeLayer from "../orchestration-v2/runtimeLayer.ts"; | 23 | import * as RuntimeLayer from "../orchestration-v2/runtimeLayer.ts"; |
| 16 | import * as SqlitePersistence from "../persistence/Sqlite.ts"; | 24 | import * as SqlitePersistence from "../persistence/Sqlite.ts"; |
| ... | @@ -25,20 +33,13 @@ import * as ProjectService from "./ProjectService.ts"; | ... | @@ -25,20 +33,13 @@ import * as ProjectService from "./ProjectService.ts"; |
| 25 | import * as RepositoryIdentityResolver from "./RepositoryIdentityResolver.ts"; | 33 | import * as RepositoryIdentityResolver from "./RepositoryIdentityResolver.ts"; |
| 26 | import * as ManagedProjectFolders from "./ManagedProjectFolders.ts"; | 34 | import * as ManagedProjectFolders from "./ManagedProjectFolders.ts"; |
| 27 | | 35 | |
| 28 | // Real repository detection: the service only asks the Git workflow whether | 36 | // The real Git workflow over real repository detection, with a mock GitManager |
| 29 | // the data dir is inside a checkout. | 37 | // since the service only asks whether the data dir is inside a checkout. |
| 30 | const layerGitWorkflow = Layer.unwrap( | 38 | const layerGitWorkflow = (vcsProcess?: Layer.Layer<VcsProcess.VcsProcess>) => |
| 31 | Effect.gen(function* () { | 39 | GitWorkflow.layer.pipe( |
| 32 | const registry = yield* VcsDriverRegistry.VcsDriverRegistry; | 40 | Layer.provide(VcsDriverRegistry.layer.pipe(Layer.provide(vcsProcess ?? VcsProcess.layer))), |
| 33 | return Layer.mock(GitWorkflow.GitWorkflowService)({ | 41 | Layer.provide(Layer.mock(GitManager.GitManager)({})), |
| 34 | isRepository: (cwd) => | 42 | ); |
| 35 | registry.detect({ cwd }).pipe( | | |
| 36 | Effect.map((handle) => handle?.kind === "git"), | | |
| 37 | Effect.orDie, | | |
| 38 | ), | | |
| 39 | }); | | |
| 40 | }), | | |
| 41 | ).pipe(Layer.provide(VcsDriverRegistry.layer.pipe(Layer.provide(VcsProcess.layer)))); | | |
| 42 | | 43 | |
| 43 | const layerEnrichment = ProjectEnrichmentService.layer.pipe( | 44 | const layerEnrichment = ProjectEnrichmentService.layer.pipe( |
| 44 | Layer.provide( | 45 | Layer.provide( |
| ... | @@ -56,6 +57,8 @@ const layerEnrichment = ProjectEnrichmentService.layer.pipe( | ... | @@ -56,6 +57,8 @@ const layerEnrichment = ProjectEnrichmentService.layer.pipe( |
| 56 | const layerRealGit = GitVcsDriver.layer.pipe(Layer.provide(VcsProcess.layer)); | 57 | const layerRealGit = GitVcsDriver.layer.pipe(Layer.provide(VcsProcess.layer)); |
| 57 | | 58 | |
| 58 | interface HarnessOptions { | 59 | interface HarnessOptions { |
| | 60 | /** The processes repository detection runs, such as a machine with no Git. */ |
| | 61 | readonly vcsProcess?: Layer.Layer<VcsProcess.VcsProcess>; |
| 59 | /** A git driver for failures real git cannot produce on demand. */ | 62 | /** A git driver for failures real git cannot produce on demand. */ |
| 60 | readonly git?: Layer.Layer<GitVcsDriver.GitVcsDriver>; | 63 | readonly git?: Layer.Layer<GitVcsDriver.GitVcsDriver>; |
| 61 | /** Wraps the real ProjectService, for failures it cannot produce on demand. */ | 64 | /** Wraps the real ProjectService, for failures it cannot produce on demand. */ |
| ... | @@ -81,7 +84,7 @@ const layer = (baseDir: string, options?: HarnessOptions) => | ... | @@ -81,7 +84,7 @@ const layer = (baseDir: string, options?: HarnessOptions) => |
| 81 | Layer.provideMerge(RuntimeLayer.layerProjectService), | 84 | Layer.provideMerge(RuntimeLayer.layerProjectService), |
| 82 | Layer.provideMerge(layerEnrichment), | 85 | Layer.provideMerge(layerEnrichment), |
| 83 | Layer.provideMerge(WorkspacePaths.layer), | 86 | Layer.provideMerge(WorkspacePaths.layer), |
| 84 | Layer.provideMerge(layerGitWorkflow), | 87 | Layer.provideMerge(layerGitWorkflow(options?.vcsProcess)), |
| 85 | Layer.provideMerge(options?.git ?? layerRealGit), | 88 | Layer.provideMerge(options?.git ?? layerRealGit), |
| 86 | Layer.provideMerge(SqlitePersistence.layerMemory), | 89 | Layer.provideMerge(SqlitePersistence.layerMemory), |
| 87 | Layer.provideMerge(ServerConfig.layerTest(baseDir, baseDir)), | 90 | Layer.provideMerge(ServerConfig.layerTest(baseDir, baseDir)), |
| ... | @@ -145,6 +148,34 @@ it.effect("offers nothing when the data dir sits inside a Git checkout", () => | ... | @@ -145,6 +148,34 @@ it.effect("offers nothing when the data dir sits inside a Git checkout", () => |
| 145 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), | 148 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), |
| 146 | ); | 149 | ); |
| 147 | | 150 | |
| | 151 | it.effect("offers a Scratch folder when Git is not installed", () => |
| | 152 | withScratch( |
| | 153 | ({ baseDir }) => |
| | 154 | Effect.gen(function* () { |
| | 155 | const path = yield* Path.Path; |
| | 156 | assert.equal(yield* requireRoot, path.resolve(baseDir, "scratch")); |
| | 157 | }), |
| | 158 | { |
| | 159 | vcsProcess: Layer.succeed(VcsProcess.VcsProcess, { |
| | 160 | run: (input) => |
| | 161 | Effect.fail( |
| | 162 | new VcsProcessSpawnError({ |
| | 163 | operation: input.operation, |
| | 164 | command: input.command, |
| | 165 | cwd: input.cwd, |
| | 166 | cause: PlatformError.systemError({ |
| | 167 | _tag: "NotFound", |
| | 168 | module: "ChildProcess", |
| | 169 | method: "spawn", |
| | 170 | syscall: `spawn ${input.command}`, |
| | 171 | }), |
| | 172 | }), |
| | 173 | ), |
| | 174 | }), |
| | 175 | }, |
| | 176 | ), |
| | 177 | ); |
| | 178 | |
| 148 | it.effect("creates one Scratch project, even for concurrent first requests", () => | 179 | it.effect("creates one Scratch project, even for concurrent first requests", () => |
| 149 | withScratch(() => | 180 | withScratch(() => |
| 150 | Effect.gen(function* () { | 181 | Effect.gen(function* () { |