1import { assert, it, describe } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Layer from "effect/Layer";
5import * as Path from "effect/Path";
6import type * as PlatformError from "effect/PlatformError";
7import type * as Scope from "effect/Scope";
8import * as DateTime from "effect/DateTime";
9import * as Option from "effect/Option";
10
11import type { VcsDriverKind } from "@t3tools/contracts";
12import * as VcsDriver from "../VcsDriver.ts";
13
14function normalizePathForComparison(value: string): string {
15 return value.replaceAll("\\", "/");
16}
17
18export interface VcsDriverFixture<R, E> {
19 readonly createRepo: (cwd: string) => Effect.Effect<void, E, R>;
20 readonly writeFile: (
21 cwd: string,
22 relativePath: string,
23 contents: string,
24 ) => Effect.Effect<void, E, R | FileSystem.FileSystem | Path.Path>;
25 readonly trackFile?: (cwd: string, relativePath: string) => Effect.Effect<void, E, R>;
26 readonly commit?: (cwd: string, message: string) => Effect.Effect<void, E, R>;
27 readonly ignorePath: (
28 cwd: string,
29 pattern: string,
30 ) => Effect.Effect<void, E, R | FileSystem.FileSystem | Path.Path>;
31}
32
33export interface VcsDriverContractSuiteInput<R, E> {
34 readonly name: string;
35 readonly kind: VcsDriverKind;
36 readonly layer: Layer.Layer<
37 VcsDriver.VcsDriver | R | FileSystem.FileSystem | Path.Path,
38 E,
39 never
40 >;
41 readonly fixture: VcsDriverFixture<R, E>;
42}
43
44export function runVcsDriverContractSuite<R, E>(input: VcsDriverContractSuiteInput<R, E>) {
45 const makeTmpDir = (
46 prefix = `t3-${input.kind}-vcs-contract-`,
47 ): Effect.Effect<string, PlatformError.PlatformError, FileSystem.FileSystem | Scope.Scope> =>
48 Effect.gen(function* () {
49 const fileSystem = yield* FileSystem.FileSystem;
50 return yield* fileSystem.makeTempDirectoryScoped({ prefix });
51 });
52
53 it.layer(input.layer)(`${input.name} VCS driver contract`, (it) => {
54 describe("repository detection", () => {
55 it.effect("returns null outside a repository", () =>
56 Effect.gen(function* () {
57 const cwd = yield* makeTmpDir();
58 const driver = yield* VcsDriver.VcsDriver;
59
60 assert.equal(yield* driver.detectRepository(cwd), null);
61 assert.equal(yield* driver.isInsideWorkTree(cwd), false);
62 }),
63 );
64
65 it.effect("detects repository identity inside a repository and nested directories", () =>
66 Effect.gen(function* () {
67 const cwd = yield* makeTmpDir();
68 const driver = yield* VcsDriver.VcsDriver;
69
70 yield* input.fixture.createRepo(cwd);
71 yield* input.fixture.writeFile(cwd, "src/index.ts", "export const value = 1;\n");
72 const identity = yield* driver.detectRepository(cwd);
73 assert.equal(identity?.kind, input.kind);
74 assert.isTrue(
75 normalizePathForComparison(identity?.rootPath ?? "").endsWith(
76 normalizePathForComparison(cwd),
77 ),
78 );
79 assert.equal(identity?.freshness.source, "live-local");
80 assert.isTrue(DateTime.isDateTime(identity?.freshness.observedAt));
81 assert.isTrue(Option.isNone(identity?.freshness.expiresAt ?? Option.none()));
82 assert.equal(yield* driver.isInsideWorkTree(cwd), true);
83
84 const path = yield* Path.Path;
85 const nestedDir = path.join(cwd, "src");
86 const nestedIdentity = yield* driver.detectRepository(nestedDir);
87 assert.equal(nestedIdentity?.rootPath, identity?.rootPath);
88 assert.equal(yield* driver.isInsideWorkTree(nestedDir), true);
89 }),
90 );
91 });
92
93 describe("workspace files", () => {
94 it.effect("lists tracked and untracked non-ignored files", () =>
95 Effect.gen(function* () {
96 const cwd = yield* makeTmpDir();
97 const driver = yield* VcsDriver.VcsDriver;
98
99 yield* input.fixture.createRepo(cwd);
100 yield* input.fixture.writeFile(cwd, "tracked.ts", "export const tracked = true;\n");
101 if (input.fixture.trackFile && input.fixture.commit) {
102 yield* input.fixture.trackFile(cwd, "tracked.ts");
103 yield* input.fixture.commit(cwd, "Track file");
104 }
105 yield* input.fixture.writeFile(cwd, "untracked.ts", "export const untracked = true;\n");
106
107 const result = yield* driver.listWorkspaceFiles(cwd);
108
109 assert.include(result.paths, "tracked.ts");
110 assert.include(result.paths, "untracked.ts");
111 assert.equal(result.truncated, false);
112 assert.equal(result.freshness.source, "live-local");
113 assert.isTrue(DateTime.isDateTime(result.freshness.observedAt));
114 assert.isTrue(Option.isNone(result.freshness.expiresAt));
115 }),
116 );
117
118 it.effect("excludes ignored files from workspace listing", () =>
119 Effect.gen(function* () {
120 const cwd = yield* makeTmpDir();
121 const driver = yield* VcsDriver.VcsDriver;
122
123 yield* input.fixture.createRepo(cwd);
124 yield* input.fixture.ignorePath(cwd, "*.log");
125 yield* input.fixture.writeFile(cwd, "included.ts", "export const included = true;\n");
126 yield* input.fixture.writeFile(cwd, "debug.log", "ignore me\n");
127 yield* input.fixture.writeFile(cwd, "nested/error.log", "ignore me too\n");
128
129 const result = yield* driver.listWorkspaceFiles(cwd);
130
131 assert.include(result.paths, "included.ts");
132 assert.notInclude(result.paths, "debug.log");
133 assert.notInclude(result.paths, "nested/error.log");
134 }),
135 );
136 });
137
138 describe("ignored path filtering", () => {
139 it.effect("filters ignored paths", () =>
140 Effect.gen(function* () {
141 const cwd = yield* makeTmpDir();
142 const driver = yield* VcsDriver.VcsDriver;
143
144 yield* input.fixture.createRepo(cwd);
145 yield* input.fixture.ignorePath(cwd, "*.log");
146
147 const result = yield* driver.filterIgnoredPaths(cwd, [
148 "keep.ts",
149 "debug.log",
150 "nested/error.log",
151 ]);
152
153 assert.deepStrictEqual(result, ["keep.ts"]);
154 }),
155 );
156
157 it.effect("returns empty input unchanged", () =>
158 Effect.gen(function* () {
159 const cwd = yield* makeTmpDir();
160 const driver = yield* VcsDriver.VcsDriver;
161
162 yield* input.fixture.createRepo(cwd);
163
164 assert.deepStrictEqual(yield* driver.filterIgnoredPaths(cwd, []), []);
165 }),
166 );
167 });
168 });
169}