mirror of
https://github.com/merlinhu1/truthmark.git
synced 2026-08-25 07:53:25 +02:00
* 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>
178 lines
5.0 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|