Files
c92b46ad65 feat: harden init routing and generated lifecycle (#33)
* feat: harden init routing and generated lifecycle

* test: align restored coverage with platform routing

* docs: translate localized uninstall guidance

* test: complete init routing and lifecycle coverage

* docs: record completed OpenSpec tasks

* chore: archive implemented init lifecycle OpenSpec change

* chore: remove OpenSpec configuration

* docs: clarify platform omission reconciliation

---------

Co-authored-by: MerlinH <merlinh221@gmail.com>
2026-07-12 20:48:09 +10:00

178 lines
5.0 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import { describe, it } from "node:test";
import { expect } from "expect";
import {
ensureRepoFile,
isSafeExactFile,
writeRepoFile,
} from "../../src/fs/paths.js";
import { createTempRepo } from "../helpers/temp-repo.js";
describe("repo path writes", () => {
it("rejects writeRepoFile when a parent directory is a symlink outside the repo", async () => {
const repo = await createTempRepo();
try {
const outsideDir = path.resolve(
repo.rootDir,
"..",
"truthmark-paths-write-outside",
);
await fs.mkdir(outsideDir, { recursive: true });
await fs.symlink(outsideDir, path.join(repo.rootDir, "docs"));
await expect(
writeRepoFile(repo.rootDir, "docs/escaped.md", "# Escaped\n"),
).rejects.toThrow("must stay inside the repository root");
} finally {
await fs.rm(
path.resolve(repo.rootDir, "..", "truthmark-paths-write-outside"),
{
force: true,
recursive: true,
},
);
await repo.cleanup();
}
});
it("rejects ensureRepoFile when a parent directory is a symlink outside the repo", async () => {
const repo = await createTempRepo();
try {
const outsideDir = path.resolve(
repo.rootDir,
"..",
"truthmark-paths-ensure-outside",
);
await fs.mkdir(outsideDir, { recursive: true });
await fs.symlink(outsideDir, path.join(repo.rootDir, "docs"));
await expect(
ensureRepoFile(repo.rootDir, "docs/escaped.md", "# Escaped\n"),
).rejects.toThrow("must stay inside the repository root");
} finally {
await fs.rm(
path.resolve(repo.rootDir, "..", "truthmark-paths-ensure-outside"),
{
force: true,
recursive: true,
},
);
await repo.cleanup();
}
});
it("rejects ensureRepoFile when the target is a broken symlink outside the repo", async () => {
const repo = await createTempRepo();
const outsidePath = path.resolve(
repo.rootDir,
"..",
"truthmark-paths-broken-link.md",
);
try {
await repo.writeFile("docs/truthmark/templates/.keep", "");
await fs.symlink(
outsidePath,
path.join(
repo.rootDir,
"docs",
"truthmark",
"templates",
"engineering-behavior.md",
),
);
await expect(
ensureRepoFile(
repo.rootDir,
"docs/truthmark/templates/engineering-behavior.md",
"# Template\n",
),
).rejects.toThrow("must stay inside the repository root");
await expect(fs.stat(outsidePath)).rejects.toThrow();
} finally {
await fs.rm(outsidePath, { force: true });
await repo.cleanup();
}
});
it("checks exact-file safety without allowing missing file parents", async () => {
const repo = await createTempRepo();
try {
await repo.writeFile("src/session.ts", "export const session = true;\n");
await expect(isSafeExactFile(repo.rootDir, "src/session.ts", true)).resolves.toBe(
true,
);
await expect(
isSafeExactFile(repo.rootDir, "src/session.ts", false),
).resolves.toBe(true);
await expect(
isSafeExactFile(repo.rootDir, "src/missing.ts", true),
).resolves.toBe(true);
await expect(
isSafeExactFile(repo.rootDir, "src/missing.ts", false),
).resolves.toBe(false);
} finally {
await repo.cleanup();
}
});
it("rejects hard-linked exact files as unsafe lifecycle targets", async () => {
const repo = await createTempRepo();
try {
const sourcePath = path.join(repo.rootDir, "src", "session.ts");
const linkPath = path.join(repo.rootDir, "src", "session-copy.ts");
await repo.writeFile("src/session.ts", "export const session = true;\n");
await fs.link(sourcePath, linkPath);
await expect(
isSafeExactFile(repo.rootDir, "src/session.ts", false),
).resolves.toBe(false);
await expect(
isSafeExactFile(repo.rootDir, "src/session-copy.ts", false),
).resolves.toBe(false);
} finally {
await repo.cleanup();
}
});
it("rejects hard-linked descendants during lifecycle preflight", async () => {
const repo = await createTempRepo();
try {
await repo.writeFile(
"docs/truthmark/engineering/overview.md",
"# Overview\n",
);
await repo.writeFile(
"src/session.ts",
"export const session = true;\n",
);
await fs.link(
path.join(repo.rootDir, "src", "session.ts"),
path.join(repo.rootDir, "src", "session-link.ts"),
);
await expect(
isSafeExactFile(repo.rootDir, "src/session.ts", false),
).resolves.toBe(false);
await expect(
isSafeExactFile(repo.rootDir, "docs/truthmark/engineering/overview.md", false),
).resolves.toBe(true);
} finally {
await repo.cleanup();
}
});
});