1/**
2 * A `SourceControlHost.SourceControlHost` for provider tests. Settings start from the defaults
3 * or what the test supplies, and `TestSourceControlHostSettings` patches them mid-test the way a
4 * client's settings write would. Process runs and git operations go to what the test supplies;
5 * anything else dies, so an unexpected CLI or git call is visible.
6 *
7 * @module source-control-testing/TestSourceControlHost
8 */
9import {
10 DEFAULT_SERVER_SETTINGS,
11 type ServerSettings,
12 type ServerSettingsPatch,
13} from "@t3tools/contracts";
14import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";
15import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost";
16import * as Context from "effect/Context";
17import * as Effect from "effect/Effect";
18import * as Layer from "effect/Layer";
19import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
20import * as Ref from "effect/Ref";
21
22export interface TestSourceControlHostOptions {
23 readonly settings?: ServerSettings;
24 readonly process?: Partial<SourceControlHost.SourceControlHost["Service"]["process"]>;
25 readonly git?: Partial<SourceControlHost.SourceControlHost["Service"]["git"]>;
26}
27
28/** Lets a test change the settings its `layer` reports. */
29export class TestSourceControlHostSettings extends Context.Service<
30 TestSourceControlHostSettings,
31 { readonly update: (patch: ServerSettingsPatch) => Effect.Effect<void> }
32>()("@t3tools/source-control-testing/TestSourceControlHost/TestSourceControlHostSettings") {}
33
34type Git = SourceControlHost.SourceControlHost["Service"]["git"];
35
36const unexpectedGit = (operation: string) => () =>
37 Effect.die(`Unexpected git ${operation} in a test that supplied none.`);
38
39const failingGit: Git = {
40 execute: unexpectedGit("execute"),
41 resolveCommit: unexpectedGit("resolveCommit"),
42 remotes: unexpectedGit("remotes"),
43 readConfigValue: unexpectedGit("readConfigValue"),
44 resolvePrimaryRemoteName: unexpectedGit("resolvePrimaryRemoteName"),
45 ensureRemote: unexpectedGit("ensureRemote"),
46 listLocalBranchNames: unexpectedGit("listLocalBranchNames"),
47 fetchRemoteBranch: unexpectedGit("fetchRemoteBranch"),
48 fetchRemoteTrackingBranch: unexpectedGit("fetchRemoteTrackingBranch"),
49 setBranchUpstream: unexpectedGit("setBranchUpstream"),
50 switchRef: unexpectedGit("switchRef"),
51};
52
53export const layer = (
54 options: TestSourceControlHostOptions = {},
55): Layer.Layer<SourceControlHost.SourceControlHost | TestSourceControlHostSettings> =>
56 Layer.effectContext(
57 Effect.gen(function* () {
58 const settings = yield* Ref.make(options.settings ?? DEFAULT_SERVER_SETTINGS);
59 const host = SourceControlHost.SourceControlHost.of({
60 settings: { get: Ref.get(settings) },
61 process: {
62 run:
63 options.process?.run ??
64 ((input) => Effect.die(`Unexpected ${input.command} run in ${input.operation}.`)),
65 },
66 git: { ...failingGit, ...options.git },
67 });
68 return Context.make(SourceControlHost.SourceControlHost, host).pipe(
69 Context.add(TestSourceControlHostSettings, {
70 update: (patch) =>
71 Ref.update(settings, (current) => applyServerSettingsPatch(current, patch)),
72 }),
73 );
74 }),
75 );
76
77/** A successful run's output, for tests that script CLI responses. */
78export const processOutput = (
79 stdout: string,
80 options?: {
81 readonly stderr?: string;
82 readonly exitCode?: ChildProcessSpawner.ExitCode;
83 },
84): SourceControlHost.SourceControlProcessOutput => ({
85 exitCode: options?.exitCode ?? ChildProcessSpawner.ExitCode(0),
86 stdout,
87 stderr: options?.stderr ?? "",
88 stdoutTruncated: false,
89 stderrTruncated: false,
90});