import test from "node:test"; import assert from "node:assert/strict"; import { mkdtemp, rm, writeFile, readFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { createRequire } from "node:module"; const exec = promisify(execFile); const require = createRequire(import.meta.url); const { GitService } = require("../src/main/git-service.cjs"); const { GitValidatorService, isSensitiveTrackedPath, sameRemote, } = require("../src/main/git-validator-service.cjs"); async function git(args, cwd) { return exec("git", args, { cwd, encoding: "utf8" }); } test("Git Validator scores repository hygiene and offers bounded safe repairs", async (t) => { const root = await mkdtemp(path.join(os.tmpdir(), "forgeflow-validator-")); t.after(() => rm(root, { recursive: true, force: true })); await git(["init", "-b", "main"], root); await git(["config", "user.name", "ForgeFlow Test"], root); await git(["config", "user.email", "forgeflow@example.invalid"], root); await git( ["remote", "add", "origin", "https://gitea.example.test/jens/app.git"], root, ); await writeFile(path.join(root, "README.md"), "# App\n", "utf8"); await writeFile(path.join(root, ".gitignore"), ".env\n", "utf8"); await git(["add", "."], root); await git(["commit", "-m", "Initial"], root); const validator = new GitValidatorService({ git: new GitService(), gitea: { getBranchProtection: async () => ({ protected: false, enableForcePush: false, }), }, }); const repository = { fullName: "jens/app", name: "app", owner: { login: "jens" }, defaultBranch: "main", localPath: root, cloneUrl: "https://gitea.example.test/jens/app.git", sshUrl: "git@gitea.example.test:jens/app.git", }; const report = await validator.scan(repository); assert.ok(report.score > 60); assert.equal( report.checks.find((check) => check.id === "origin").status, "pass", ); assert.equal( report.checks.find((check) => check.id === "default-branch-protection") .fixAction, "protect-default-branch", ); const safety = report.checks.find((check) => check.id === "local-safety"); assert.equal(safety.safe, true); await validator.repair(repository, safety); const rescanned = await validator.scan(repository); assert.equal( rescanned.checks.find((check) => check.id === "local-safety").status, "pass", ); }); test("Git Validator creates a reviewable gitignore without committing it", async (t) => { const root = await mkdtemp(path.join(os.tmpdir(), "forgeflow-ignore-")); t.after(() => rm(root, { recursive: true, force: true })); await git(["init", "-b", "main"], root); const validator = new GitValidatorService({ git: new GitService() }); const repository = { localPath: root }; await validator.repair(repository, { fixAction: "add-gitignore" }); const content = await readFile(path.join(root, ".gitignore"), "utf8"); assert.match(content, /\.env/); const status = await git(["status", "--short"], root); assert.match(status.stdout, /\?\? \.gitignore/); }); test("Git Validator recognizes remote aliases and secret-shaped tracked paths", () => { assert.equal( sameRemote( "git@gitea.example.test:jens/app.git", "https://gitea.example.test/jens/app", ), true, ); assert.equal(isSensitiveTrackedPath(".env.production"), true); assert.equal(isSensitiveTrackedPath("config/private.pem"), true); assert.equal(isSensitiveTrackedPath(".env.example"), false); }); test("Git Validator rejects stale or forged repair requests", async () => { const validator = new GitValidatorService({ git: new GitService() }); validator.scan = async () => ({ checks: [{ id: "local-safety", fixAction: "configure-local-safety", status: "warning" }], }); assert.equal( (await validator.resolveRepairCheck({}, { id: "local-safety", fixAction: "configure-local-safety" })).id, "local-safety", ); await assert.rejects( validator.resolveRepairCheck({}, { id: "local-safety", fixAction: "align-origin" }), /stale/i, ); await assert.rejects( validator.resolveRepairCheck({}, { id: "resolved-check", fixAction: "align-origin" }), /resolved|no longer repairable/i, ); }); test("Git Validator reports reproducibility, CI and editor hygiene and creates reviewable defaults", async (t) => { const root = await mkdtemp(path.join(os.tmpdir(), "forgeflow-hygiene-")); t.after(() => rm(root, { recursive: true, force: true })); await git(["init", "-b", "main"], root); await git(["config", "user.name", "ForgeFlow Test"], root); await git(["config", "user.email", "forgeflow@example.invalid"], root); await git(["remote", "add", "origin", "https://gitea.example.test/jens/app.git"], root); await writeFile(path.join(root, "package.json"), '{"name":"app"}\n', "utf8"); await writeFile(path.join(root, "README.md"), "# App\n", "utf8"); await writeFile(path.join(root, ".gitignore"), ".env\n", "utf8"); await git(["add", "."], root); await git(["commit", "-m", "Initial"], root); const validator = new GitValidatorService({ git: new GitService(), gitea: { getBranchProtection: async () => ({ protected: true, enableForcePush: false }) } }); const repository = { fullName: "jens/app", name: "app", owner: { login: "jens" }, defaultBranch: "main", localPath: root, cloneUrl: "https://gitea.example.test/jens/app.git" }; const report = await validator.scan(repository); assert.equal(report.checks.find((check) => check.id === "dependency-lock").status, "warning"); assert.equal(report.checks.find((check) => check.id === "continuous-integration").status, "warning"); for (const action of ["add-gitattributes", "add-editorconfig"]) await validator.repair(repository, { fixAction: action }); assert.match(await readFile(path.join(root, ".gitattributes"), "utf8"), /text=auto/); assert.match(await readFile(path.join(root, ".editorconfig"), "utf8"), /root = true/); });