| ... | ... | @@ -1,6 +1,12 @@ |
| 1 | 1 | import { assert, it } from "@effect/vitest"; |
| 2 | 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 | 10 | import * as Deferred from "effect/Deferred"; |
| 5 | 11 | import * as Effect from "effect/Effect"; |
| 6 | 12 | import * as Fiber from "effect/Fiber"; |
| ... | ... | @@ -8,9 +14,11 @@ import * as FileSystem from "effect/FileSystem"; |
| 8 | 14 | import * as Layer from "effect/Layer"; |
| 9 | 15 | import * as Option from "effect/Option"; |
| 10 | 16 | import * as Path from "effect/Path"; |
| 17 | import * as PlatformError from "effect/PlatformError"; |
| 11 | 18 | import * as Scope from "effect/Scope"; |
| 12 | 19 | |
| 13 | 20 | import * as ServerConfig from "../config.ts"; |
| 21 | import * as GitManager from "../git/GitManager.ts"; |
| 14 | 22 | import * as GitWorkflow from "../git/GitWorkflowService.ts"; |
| 15 | 23 | import * as RuntimeLayer from "../orchestration-v2/runtimeLayer.ts"; |
| 16 | 24 | import * as SqlitePersistence from "../persistence/Sqlite.ts"; |
| ... | ... | @@ -25,20 +33,13 @@ import * as ProjectService from "./ProjectService.ts"; |
| 25 | 33 | import * as RepositoryIdentityResolver from "./RepositoryIdentityResolver.ts"; |
| 26 | 34 | import * as ManagedProjectFolders from "./ManagedProjectFolders.ts"; |
| 27 | 35 | |
| 28 | | // Real repository detection: the service only asks the Git workflow whether |
| 29 | | // the data dir is inside a checkout. |
| 30 | | const layerGitWorkflow = Layer.unwrap( |
| 31 | | Effect.gen(function* () { |
| 32 | | const registry = yield* VcsDriverRegistry.VcsDriverRegistry; |
| 33 | | return Layer.mock(GitWorkflow.GitWorkflowService)({ |
| 34 | | isRepository: (cwd) => |
| 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)))); |
| 36 | // The real Git workflow over real repository detection, with a mock GitManager |
| 37 | // since the service only asks whether the data dir is inside a checkout. |
| 38 | const layerGitWorkflow = (vcsProcess?: Layer.Layer<VcsProcess.VcsProcess>) => |
| 39 | GitWorkflow.layer.pipe( |
| 40 | Layer.provide(VcsDriverRegistry.layer.pipe(Layer.provide(vcsProcess ?? VcsProcess.layer))), |
| 41 | Layer.provide(Layer.mock(GitManager.GitManager)({})), |
| 42 | ); |
| 42 | 43 | |
| 43 | 44 | const layerEnrichment = ProjectEnrichmentService.layer.pipe( |
| 44 | 45 | Layer.provide( |
| ... | ... | @@ -56,6 +57,8 @@ const layerEnrichment = ProjectEnrichmentService.layer.pipe( |
| 56 | 57 | const layerRealGit = GitVcsDriver.layer.pipe(Layer.provide(VcsProcess.layer)); |
| 57 | 58 | |
| 58 | 59 | interface HarnessOptions { |
| 60 | /** The processes repository detection runs, such as a machine with no Git. */ |
| 61 | readonly vcsProcess?: Layer.Layer<VcsProcess.VcsProcess>; |
| 59 | 62 | /** A git driver for failures real git cannot produce on demand. */ |
| 60 | 63 | readonly git?: Layer.Layer<GitVcsDriver.GitVcsDriver>; |
| 61 | 64 | /** Wraps the real ProjectService, for failures it cannot produce on demand. */ |
| ... | ... | @@ -81,7 +84,7 @@ const layer = (baseDir: string, options?: HarnessOptions) => |
| 81 | 84 | Layer.provideMerge(RuntimeLayer.layerProjectService), |
| 82 | 85 | Layer.provideMerge(layerEnrichment), |
| 83 | 86 | Layer.provideMerge(WorkspacePaths.layer), |
| 84 | | Layer.provideMerge(layerGitWorkflow), |
| 87 | Layer.provideMerge(layerGitWorkflow(options?.vcsProcess)), |
| 85 | 88 | Layer.provideMerge(options?.git ?? layerRealGit), |
| 86 | 89 | Layer.provideMerge(SqlitePersistence.layerMemory), |
| 87 | 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 | 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 | 179 | it.effect("creates one Scratch project, even for concurrent first requests", () => |
| 149 | 180 | withScratch(() => |
| 150 | 181 | Effect.gen(function* () { |