Skip to content

Commit 4608f8f

Browse files
committed
test(core): align post-push wrapper coverage with effect lint
1 parent f9b1b40 commit 4608f8f

1 file changed

Lines changed: 183 additions & 127 deletions

File tree

packages/lib/tests/core/git-post-push-wrapper.test.ts

Lines changed: 183 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@
33
// REF: issue-201
44
// PURITY: SHELL (executes generated bash scripts in isolated temp directories)
55

6-
import { execFileSync } from "node:child_process"
7-
import fs from "node:fs"
8-
import os from "node:os"
9-
import path from "node:path"
10-
import { afterEach, describe, expect, it } from "vitest"
6+
import * as Command from "@effect/platform/Command"
7+
import * as CommandExecutor from "@effect/platform/CommandExecutor"
8+
import * as FileSystem from "@effect/platform/FileSystem"
9+
import * as Path from "@effect/platform/Path"
10+
import { NodeContext } from "@effect/platform-node"
11+
import { describe, expect, it } from "@effect/vitest"
12+
import { Effect, pipe } from "effect"
13+
import * as Chunk from "effect/Chunk"
14+
import * as Stream from "effect/Stream"
1115

1216
import { renderEntrypointGitHooks } from "../../src/core/templates-entrypoint/git.js"
1317
import { renderEntrypointGitPostPushWrapperInstall } from "../../src/core/templates-entrypoint/git-post-push-wrapper.js"
1418

1519
type WrapperHarness = {
16-
readonly rootDir: string
1720
readonly repoDir: string
1821
readonly externalDir: string
1922
readonly binDir: string
@@ -24,8 +27,6 @@ type WrapperHarness = {
2427
readonly nodeScriptLogPath: string
2528
}
2629

27-
const tempRoots: string[] = []
28-
2930
const fakeGitScript = `#!/usr/bin/env bash
3031
set -euo pipefail
3132
@@ -100,11 +101,13 @@ set -euo pipefail
100101
exit 0
101102
`
102103

103-
const writeExecutable = (filePath: string, content: string): void => {
104-
fs.mkdirSync(path.dirname(filePath), { recursive: true })
105-
fs.writeFileSync(filePath, content)
106-
fs.chmodSync(filePath, 0o755)
107-
}
104+
const collectUint8Array = (chunks: Chunk.Chunk<Uint8Array>): Uint8Array =>
105+
Chunk.reduce(chunks, new Uint8Array(), (acc, curr) => {
106+
const next = new Uint8Array(acc.length + curr.length)
107+
next.set(acc)
108+
next.set(curr, acc.length)
109+
return next
110+
})
108111

109112
const extractEmbeddedScript = (template: string, target: string): string => {
110113
const marker = `cat <<'EOF' > "${target}"\n`
@@ -122,68 +125,65 @@ const extractEmbeddedScript = (template: string, target: string): string => {
122125
return template.slice(bodyStart, bodyEnd)
123126
}
124127

125-
const readLogLines = (filePath: string): ReadonlyArray<string> => {
126-
if (!fs.existsSync(filePath)) {
127-
return []
128-
}
128+
const writeExecutable = (
129+
filePath: string,
130+
content: string
131+
): Effect.Effect<void, Error, FileSystem.FileSystem | Path.Path> =>
132+
Effect.gen(function*(_) {
133+
const fs = yield* _(FileSystem.FileSystem)
134+
const path = yield* _(Path.Path)
135+
yield* _(fs.makeDirectory(path.dirname(filePath), { recursive: true }))
136+
yield* _(fs.writeFileString(filePath, content))
137+
yield* _(fs.chmod(filePath, 0o755))
138+
})
129139

130-
const contents = fs.readFileSync(filePath, "utf8").trim()
131-
return contents.length === 0 ? [] : contents.split("\n")
132-
}
140+
const readLogLines = (
141+
filePath: string
142+
): Effect.Effect<ReadonlyArray<string>, Error, FileSystem.FileSystem> =>
143+
Effect.gen(function*(_) {
144+
const fs = yield* _(FileSystem.FileSystem)
145+
const exists = yield* _(fs.exists(filePath))
146+
if (!exists) {
147+
return []
148+
}
149+
150+
const contents = yield* _(fs.readFileString(filePath))
151+
const trimmed = contents.trim()
152+
return trimmed.length === 0 ? [] : trimmed.split("\n")
153+
})
154+
155+
const runCommand = (
156+
command: string,
157+
args: ReadonlyArray<string>,
158+
cwd: string,
159+
env?: Readonly<Record<string, string | undefined>>
160+
): Effect.Effect<string, Error, CommandExecutor.CommandExecutor> =>
161+
Effect.scoped(
162+
Effect.gen(function*(_) {
163+
const executor = yield* _(CommandExecutor.CommandExecutor)
164+
const cmd = pipe(
165+
Command.make(command, ...args),
166+
Command.workingDirectory(cwd),
167+
env ? Command.env(env) : (value) => value,
168+
Command.stdout("pipe"),
169+
Command.stderr("pipe"),
170+
Command.stdin("pipe")
171+
)
172+
const proc = yield* _(executor.start(cmd))
173+
yield* _(Effect.forkDaemon(Stream.runDrain(proc.stderr)))
174+
const stdoutBytes = yield* _(
175+
pipe(proc.stdout, Stream.runCollect, Effect.map((chunks) => collectUint8Array(chunks)))
176+
)
177+
const exitCode = yield* _(proc.exitCode)
178+
if (Number(exitCode) !== 0) {
179+
return yield* _(Effect.fail(new Error(`${command} ${args.join(" ")} exited with ${String(exitCode)}`)))
180+
}
133181

134-
const makeHarness = (): WrapperHarness => {
135-
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "docker-git-post-push-"))
136-
tempRoots.push(rootDir)
137-
138-
const repoDir = path.join(rootDir, "repo")
139-
const externalDir = path.join(rootDir, "external")
140-
const binDir = path.join(rootDir, "bin")
141-
const hooksDir = path.join(rootDir, "hooks")
142-
const gitLogPath = path.join(rootDir, "git.log")
143-
const nodeCwdLogPath = path.join(rootDir, "node-cwd.log")
144-
const nodeRepoRootLogPath = path.join(rootDir, "node-repo-root.log")
145-
const nodeScriptLogPath = path.join(rootDir, "node-script.log")
146-
147-
fs.mkdirSync(path.join(repoDir, ".git"), { recursive: true })
148-
fs.mkdirSync(path.join(repoDir, "scripts"), { recursive: true })
149-
fs.mkdirSync(externalDir, { recursive: true })
150-
fs.mkdirSync(binDir, { recursive: true })
151-
fs.mkdirSync(hooksDir, { recursive: true })
152-
fs.writeFileSync(path.join(repoDir, "scripts", "session-backup-gist.js"), "// test placeholder\n")
153-
154-
writeExecutable(path.join(binDir, "git"), fakeGitScript)
155-
writeExecutable(path.join(binDir, "git-real"), fakeGitScript)
156-
writeExecutable(path.join(binDir, "gh"), fakeGhScript)
157-
writeExecutable(path.join(binDir, "node"), fakeNodeScript)
158-
159-
const postPushScript = extractEmbeddedScript(renderEntrypointGitHooks(), "$POST_PUSH_ACTION")
160-
const postPushPath = path.join(hooksDir, "post-push")
161-
writeExecutable(postPushPath, postPushScript)
162-
163-
const wrapperTemplate = extractEmbeddedScript(
164-
renderEntrypointGitPostPushWrapperInstall(),
165-
"$GIT_WRAPPER_BIN"
182+
return new TextDecoder("utf-8").decode(stdoutBytes).trim()
183+
})
166184
)
167-
const wrapperPath = path.join(rootDir, "git-wrapper")
168-
const wrapperScript = wrapperTemplate
169-
.replace("__DOCKER_GIT_REAL_BIN__", path.join(binDir, "git-real"))
170-
.replace("/opt/docker-git/hooks/post-push", postPushPath)
171-
writeExecutable(wrapperPath, wrapperScript)
172-
173-
return {
174-
rootDir,
175-
repoDir,
176-
externalDir,
177-
binDir,
178-
wrapperPath,
179-
gitLogPath,
180-
nodeCwdLogPath,
181-
nodeRepoRootLogPath,
182-
nodeScriptLogPath
183-
}
184-
}
185185

186-
const makeHarnessEnv = (harness: WrapperHarness): NodeJS.ProcessEnv => ({
186+
const makeHarnessEnv = (harness: WrapperHarness): Readonly<Record<string, string | undefined>> => ({
187187
...process.env,
188188
PATH: `${harness.binDir}:${process.env["PATH"] ?? ""}`,
189189
FAKE_GIT_LOG_PATH: harness.gitLogPath,
@@ -196,62 +196,118 @@ const runWrapper = (
196196
harness: WrapperHarness,
197197
cwd: string,
198198
args: ReadonlyArray<string>
199-
): void => {
200-
execFileSync(harness.wrapperPath, args, {
201-
cwd,
202-
env: makeHarnessEnv(harness),
203-
encoding: "utf8",
204-
stdio: "pipe"
205-
})
206-
}
199+
): Effect.Effect<void, Error, CommandExecutor.CommandExecutor> =>
200+
runCommand(harness.wrapperPath, args, cwd, makeHarnessEnv(harness)).pipe(Effect.asVoid)
201+
202+
const withHarness = <A, E, R>(
203+
use: (harness: WrapperHarness) => Effect.Effect<A, E, R>
204+
): Effect.Effect<A, E, R | FileSystem.FileSystem | Path.Path> =>
205+
Effect.scoped(
206+
Effect.gen(function*(_) {
207+
const fs = yield* _(FileSystem.FileSystem)
208+
const path = yield* _(Path.Path)
209+
const rootDir = yield* _(
210+
fs.makeTempDirectoryScoped({
211+
prefix: "docker-git-post-push-"
212+
})
213+
)
214+
215+
const repoDir = path.join(rootDir, "repo")
216+
const externalDir = path.join(rootDir, "external")
217+
const binDir = path.join(rootDir, "bin")
218+
const hooksDir = path.join(rootDir, "hooks")
219+
const gitLogPath = path.join(rootDir, "git.log")
220+
const nodeCwdLogPath = path.join(rootDir, "node-cwd.log")
221+
const nodeRepoRootLogPath = path.join(rootDir, "node-repo-root.log")
222+
const nodeScriptLogPath = path.join(rootDir, "node-script.log")
223+
224+
yield* _(fs.makeDirectory(path.join(repoDir, ".git"), { recursive: true }))
225+
yield* _(fs.makeDirectory(path.join(repoDir, "scripts"), { recursive: true }))
226+
yield* _(fs.makeDirectory(externalDir, { recursive: true }))
227+
yield* _(fs.makeDirectory(binDir, { recursive: true }))
228+
yield* _(fs.makeDirectory(hooksDir, { recursive: true }))
229+
yield* _(fs.writeFileString(path.join(repoDir, "scripts", "session-backup-gist.js"), "// test placeholder\n"))
230+
231+
yield* _(writeExecutable(path.join(binDir, "git"), fakeGitScript))
232+
yield* _(writeExecutable(path.join(binDir, "git-real"), fakeGitScript))
233+
yield* _(writeExecutable(path.join(binDir, "gh"), fakeGhScript))
234+
yield* _(writeExecutable(path.join(binDir, "node"), fakeNodeScript))
235+
236+
const postPushScript = extractEmbeddedScript(renderEntrypointGitHooks(), "$POST_PUSH_ACTION")
237+
const postPushPath = path.join(hooksDir, "post-push")
238+
yield* _(writeExecutable(postPushPath, postPushScript))
239+
240+
const wrapperTemplate = extractEmbeddedScript(
241+
renderEntrypointGitPostPushWrapperInstall(),
242+
"$GIT_WRAPPER_BIN"
243+
)
244+
const wrapperPath = path.join(rootDir, "git-wrapper")
245+
const wrapperScript = wrapperTemplate
246+
.replace("__DOCKER_GIT_REAL_BIN__", path.join(binDir, "git-real"))
247+
.replace("/opt/docker-git/hooks/post-push", postPushPath)
248+
yield* _(writeExecutable(wrapperPath, wrapperScript))
249+
250+
return yield* _(
251+
use({
252+
repoDir,
253+
externalDir,
254+
binDir,
255+
wrapperPath,
256+
gitLogPath,
257+
nodeCwdLogPath,
258+
nodeRepoRootLogPath,
259+
nodeScriptLogPath
260+
})
261+
)
262+
})
263+
)
207264

208265
describe("git post-push wrapper", () => {
209-
afterEach(() => {
210-
while (tempRoots.length > 0) {
211-
const root = tempRoots.pop()
212-
if (root !== undefined) {
213-
fs.rmSync(root, { recursive: true, force: true })
214-
}
215-
}
216-
})
217-
218-
it("runs session backup from the repository root for a normal push", () => {
219-
const harness = makeHarness()
220-
221-
runWrapper(harness, harness.repoDir, ["push", "origin", "HEAD"])
222-
223-
expect(readLogLines(harness.nodeCwdLogPath)).toEqual([harness.repoDir])
224-
expect(readLogLines(harness.nodeRepoRootLogPath)).toEqual([harness.repoDir])
225-
expect(readLogLines(harness.nodeScriptLogPath)).toEqual([
226-
path.join(harness.repoDir, "scripts", "session-backup-gist.js")
227-
])
228-
})
229-
230-
it("preserves the pushed repository context for git -C push invocations", () => {
231-
const harness = makeHarness()
232-
233-
runWrapper(harness, harness.externalDir, ["-C", harness.repoDir, "push", "origin", "HEAD"])
234-
235-
expect(readLogLines(harness.nodeCwdLogPath)).toEqual([harness.repoDir])
236-
expect(readLogLines(harness.nodeRepoRootLogPath)).toEqual([harness.repoDir])
237-
expect(readLogLines(harness.nodeScriptLogPath)).toEqual([
238-
path.join(harness.repoDir, "scripts", "session-backup-gist.js")
239-
])
240-
expect(readLogLines(harness.gitLogPath).some((line) => line.startsWith(`${harness.externalDir}\t-C ${harness.repoDir} push`))).toBe(
241-
true
242-
)
243-
})
244-
245-
it.each([
246-
["--dry-run"],
247-
["-n"]
248-
])("does not run session backup for dry-run push (%s)", (dryRunFlag) => {
249-
const harness = makeHarness()
250-
251-
runWrapper(harness, harness.externalDir, ["-C", harness.repoDir, "push", dryRunFlag, "origin", "HEAD"])
252-
253-
expect(readLogLines(harness.nodeCwdLogPath)).toEqual([])
254-
expect(readLogLines(harness.nodeRepoRootLogPath)).toEqual([])
255-
expect(readLogLines(harness.nodeScriptLogPath)).toEqual([])
256-
})
266+
it.effect("runs session backup from the repository root for a normal push", () =>
267+
withHarness((harness) =>
268+
Effect.gen(function*(_) {
269+
yield* _(runWrapper(harness, harness.repoDir, ["push", "origin", "HEAD"]))
270+
271+
const nodeCwd = yield* _(readLogLines(harness.nodeCwdLogPath))
272+
const nodeRepoRoot = yield* _(readLogLines(harness.nodeRepoRootLogPath))
273+
const nodeScript = yield* _(readLogLines(harness.nodeScriptLogPath))
274+
275+
expect(nodeCwd).toEqual([harness.repoDir])
276+
expect(nodeRepoRoot).toEqual([harness.repoDir])
277+
expect(nodeScript).toEqual([`${harness.repoDir}/scripts/session-backup-gist.js`])
278+
})
279+
).pipe(Effect.provide(NodeContext.layer)))
280+
281+
it.effect("preserves the pushed repository context for git -C push invocations", () =>
282+
withHarness((harness) =>
283+
Effect.gen(function*(_) {
284+
yield* _(runWrapper(harness, harness.externalDir, ["-C", harness.repoDir, "push", "origin", "HEAD"]))
285+
286+
const nodeCwd = yield* _(readLogLines(harness.nodeCwdLogPath))
287+
const nodeRepoRoot = yield* _(readLogLines(harness.nodeRepoRootLogPath))
288+
const nodeScript = yield* _(readLogLines(harness.nodeScriptLogPath))
289+
const gitLog = yield* _(readLogLines(harness.gitLogPath))
290+
291+
expect(nodeCwd).toEqual([harness.repoDir])
292+
expect(nodeRepoRoot).toEqual([harness.repoDir])
293+
expect(nodeScript).toEqual([`${harness.repoDir}/scripts/session-backup-gist.js`])
294+
expect(gitLog.some((line) => line.startsWith(`${harness.externalDir}\t-C ${harness.repoDir} push`))).toBe(true)
295+
})
296+
).pipe(Effect.provide(NodeContext.layer)))
297+
298+
it.effect("does not run session backup for dry-run push variants", () =>
299+
withHarness((harness) =>
300+
Effect.gen(function*(_) {
301+
yield* _(runWrapper(harness, harness.externalDir, ["-C", harness.repoDir, "push", "--dry-run", "origin", "HEAD"]))
302+
yield* _(runWrapper(harness, harness.externalDir, ["-C", harness.repoDir, "push", "-n", "origin", "HEAD"]))
303+
304+
const nodeCwd = yield* _(readLogLines(harness.nodeCwdLogPath))
305+
const nodeRepoRoot = yield* _(readLogLines(harness.nodeRepoRootLogPath))
306+
const nodeScript = yield* _(readLogLines(harness.nodeScriptLogPath))
307+
308+
expect(nodeCwd).toEqual([])
309+
expect(nodeRepoRoot).toEqual([])
310+
expect(nodeScript).toEqual([])
311+
})
312+
).pipe(Effect.provide(NodeContext.layer)))
257313
})

0 commit comments

Comments
 (0)