import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import { ServerConfig } from "../config.ts"; import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; import * as VcsProcess from "../vcs/VcsProcess.ts"; import { detectPrTemplate } from "./PrTemplateDetection.ts"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; const SINGLE_TEMPLATE_PATHS = [ ".github/pull_request_template.md", ".github/PULL_REQUEST_TEMPLATE.md", "pull_request_template.md", "PULL_REQUEST_TEMPLATE.md", "docs/pull_request_template.md", "docs/PULL_REQUEST_TEMPLATE.md", ] as const; const TEMPLATE_DIRECTORIES = [ ".github/PULL_REQUEST_TEMPLATE", "PULL_REQUEST_TEMPLATE", "docs/PULL_REQUEST_TEMPLATE", ] as const; const PrTemplateDetectionTestLayer = GitVcsDriver.layer.pipe( Layer.provide( ServerConfig.layerTest(process.cwd(), { prefix: "t3-pr-template-test-", }), ), Layer.provideMerge(VcsProcess.layer), Layer.provideMerge(NodeServices.layer), ); const runGit = (cwd: string, args: ReadonlyArray) => Effect.gen(function* () { const git = yield* GitVcsDriver.GitVcsDriver; return yield* git.execute({ operation: "PrTemplateDetection.test.runGit", cwd, args, }); }); const runWithTempDirectory = ( test: (cwd: string) => Effect.Effect, ) => Effect.scoped( Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const cwd = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-pr-template-" }); yield* runGit(cwd, ["init", "--initial-branch=main"]); yield* runGit(cwd, ["config", "user.email", "test@example.com"]); yield* runGit(cwd, ["config", "user.name", "Test User"]); return yield* test(cwd); }), ).pipe(Effect.provide(PrTemplateDetectionTestLayer)); const writeTemplate = (cwd: string, relativePath: string, contents: string) => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const templatePath = path.join(cwd, relativePath); yield* fileSystem.makeDirectory(path.dirname(templatePath), { recursive: true }); yield* fileSystem.writeFileString(templatePath, contents); return templatePath; }); const commitTemplates = (cwd: string) => Effect.gen(function* () { yield* runGit(cwd, ["add", "-A"]); yield* runGit(cwd, ["commit", "--allow-empty", "-m", "Add pull request templates"]); }); const detectTemplate = (cwd: string, treeish = "HEAD") => Effect.gen(function* () { const git = yield* GitVcsDriver.GitVcsDriver; return yield* detectPrTemplate(cwd, treeish, git.execute); }); it.effect.each(SINGLE_TEMPLATE_PATHS)("recognizes $0", (relativePath) => runWithTempDirectory((cwd) => Effect.gen(function* () { yield* writeTemplate(cwd, relativePath, `template from ${relativePath}`); yield* commitTemplates(cwd); const template = yield* detectTemplate(cwd); assert.strictEqual(Option.getOrUndefined(template), `template from ${relativePath}`); }), ), ); it.effect("reads templates from the requested base tree", () => runWithTempDirectory((cwd) => Effect.gen(function* () { yield* writeTemplate(cwd, "README.md", "initial\n"); yield* commitTemplates(cwd); yield* runGit(cwd, ["branch", "feature"]); yield* writeTemplate(cwd, ".github/pull_request_template.md", "base template"); yield* commitTemplates(cwd); yield* runGit(cwd, ["checkout", "feature"]); assert.isTrue(Option.isNone(yield* detectTemplate(cwd))); assert.strictEqual( Option.getOrUndefined(yield* detectTemplate(cwd, "main")), "base template", ); }), ), ); it.effect("uses the first non-empty template in the configured path order", () => runWithTempDirectory((cwd) => Effect.gen(function* () { yield* writeTemplate(cwd, ".github/pull_request_template.md", " \n"); yield* writeTemplate(cwd, ".github/PULL_REQUEST_TEMPLATE.md", " ## Preferred template \n"); yield* writeTemplate(cwd, "pull_request_template.md", "## Later template"); yield* commitTemplates(cwd); const template = yield* detectTemplate(cwd); assert.strictEqual(Option.getOrUndefined(template), "## Preferred template"); }), ), ); it.effect.each(TEMPLATE_DIRECTORIES)("recognizes the $0 directory", (relativeDirectory) => runWithTempDirectory((cwd) => Effect.gen(function* () { yield* writeTemplate(cwd, `${relativeDirectory}/template.MD`, "directory template"); yield* commitTemplates(cwd); const template = yield* detectTemplate(cwd); assert.strictEqual(Option.getOrUndefined(template), "directory template"); }), ), ); it.effect.skipIf(!symlinksSupported)( "skips unusable directory entries and uses the one valid template", () => runWithTempDirectory((cwd) => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const templateDirectory = path.join(cwd, ".github", "PULL_REQUEST_TEMPLATE"); yield* fileSystem.makeDirectory(path.join(templateDirectory, "b-directory.md"), { recursive: true, }); yield* fileSystem.writeFileString(path.join(templateDirectory, "a-empty.md"), " \n"); yield* fileSystem.symlink( path.join(templateDirectory, "missing.md"), path.join(templateDirectory, "c-broken.md"), ); yield* fileSystem.writeFileString(path.join(templateDirectory, "z-valid.md"), "valid"); yield* commitTemplates(cwd); const template = yield* detectTemplate(cwd); assert.strictEqual(Option.getOrUndefined(template), "valid"); }), ), ); it.effect("does not guess between multiple directory templates", () => runWithTempDirectory((cwd) => Effect.gen(function* () { yield* writeTemplate(cwd, ".github/PULL_REQUEST_TEMPLATE/a.md", "first"); yield* writeTemplate(cwd, ".github/PULL_REQUEST_TEMPLATE/b.md", "second"); yield* writeTemplate(cwd, "PULL_REQUEST_TEMPLATE/fallback.md", "fallback"); yield* commitTemplates(cwd); const template = yield* detectTemplate(cwd); assert.isTrue(Option.isNone(template)); }), ), ); it.effect.skipIf(!symlinksSupported)( "rejects a committed template symlink escaping the repository", () => runWithTempDirectory((cwd) => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const outsideDirectory = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-pr-template-outside-", }); const outsideTemplate = path.join(outsideDirectory, "secret.md"); yield* fileSystem.writeFileString(outsideTemplate, "LOCAL_SECRET_SENTINEL"); const escapedTemplatePath = path.join(cwd, ".github", "pull_request_template.md"); yield* fileSystem.makeDirectory(path.dirname(escapedTemplatePath), { recursive: true }); yield* fileSystem.symlink(outsideTemplate, escapedTemplatePath); yield* writeTemplate(cwd, "pull_request_template.md", "safe template"); yield* commitTemplates(cwd); const template = yield* detectTemplate(cwd); assert.strictEqual(Option.getOrUndefined(template), "safe template"); assert.notInclude( Option.getOrElse(template, () => ""), "LOCAL_SECRET_SENTINEL", ); }), ), ); it.effect.skipIf(!symlinksSupported)( "reads the committed template when a worktree parent is replaced", () => runWithTempDirectory((cwd) => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const outsideDirectory = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-pr-template-outside-", }); const templatePath = yield* writeTemplate( cwd, ".github/pull_request_template.md", "committed template", ); yield* commitTemplates(cwd); yield* writeTemplate(outsideDirectory, "pull_request_template.md", "LOCAL_SECRET_SENTINEL"); const templateDirectory = path.dirname(templatePath); yield* fileSystem.rename(templateDirectory, path.join(cwd, ".github-original")); yield* fileSystem.symlink(outsideDirectory, templateDirectory); const template = yield* detectTemplate(cwd); assert.strictEqual(Option.getOrUndefined(template), "committed template"); assert.notInclude( Option.getOrElse(template, () => ""), "LOCAL_SECRET_SENTINEL", ); }), ), ); it.effect("bounds template reads and marks truncated content", () => runWithTempDirectory((cwd) => Effect.gen(function* () { const prefix = "a".repeat(8_000); yield* writeTemplate(cwd, ".github/pull_request_template.md", `${prefix}SECRET_SENTINEL`); yield* commitTemplates(cwd); const template = Option.getOrThrow(yield* detectTemplate(cwd)); assert.strictEqual(template, `${prefix}\n\n[truncated]`); assert.lengthOf(template.match(/\[truncated\]/g) ?? [], 1); assert.notInclude(template, "SECRET_SENTINEL"); }), ), );