mirror of
https://github.com/merlinhu1/truthmark.git
synced 2026-08-25 07:53:25 +02:00
* feat: add interactive platform selection to init * chore: remove completed OpenSpec artifacts * chore: remove implemented design note * fix: protect init lifecycle from unsafe config paths * fix: support clearing init platforms from CLI * docs: retire config command from repository rules * docs: align init lifecycle and clear-platform contracts * docs: keep repository guidance current-state focused * docs: limit cleanup to repository-native documents * docs: preserve historical version notes --------- Co-authored-by: MerlinH <merlinh221@gmail.com>
196 lines
6.8 KiB
TypeScript
196 lines
6.8 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
import { describe, it } from "node:test";
|
|
import { expect } from "expect";
|
|
|
|
import { writeTruthmarkConfig } from "../helpers/truthmark-config.js";
|
|
import { runInit } from "../../src/init/init.js";
|
|
import { runUninstall } from "../../src/init/uninstall.js";
|
|
import type { LifecyclePlan, LifecyclePlanEntry } from "../../src/init/lifecycle.js";
|
|
import { TRUTHMARK_BLOCK_START } from "../../src/templates/agents-block.js";
|
|
|
|
import { createTempRepo } from "../helpers/temp-repo.js";
|
|
|
|
const getLifecyclePlan = (
|
|
result: Awaited<ReturnType<typeof runUninstall>>,
|
|
): LifecyclePlan | undefined =>
|
|
(result.data?.lifecyclePlan as LifecyclePlan | undefined);
|
|
|
|
describe("uninstall command", () => {
|
|
it("fails fast with missing configuration and applies no writes", async () => {
|
|
const repo = await createTempRepo();
|
|
|
|
try {
|
|
const result = await runUninstall(repo.rootDir, "dry-run");
|
|
const lifecyclePlan = getLifecyclePlan(result);
|
|
|
|
expect(result.summary).toContain("Truthmark uninstall requires a valid");
|
|
expect(lifecyclePlan?.applicable).toBe(false);
|
|
expect(lifecyclePlan?.applied).toBe(false);
|
|
await expect(
|
|
fs.access(`${repo.rootDir}/AGENTS.md`),
|
|
).rejects.toThrow();
|
|
} finally {
|
|
await repo.cleanup();
|
|
}
|
|
});
|
|
|
|
it("reconciles install output with dry-run then apply with shared preservation of authored files", async () => {
|
|
const repo = await createTempRepo();
|
|
|
|
try {
|
|
await writeTruthmarkConfig(repo.rootDir);
|
|
const configPath = `${repo.rootDir}/.truthmark/config.yml`;
|
|
const configFile = await fs.readFile(configPath, "utf8");
|
|
await fs.writeFile(
|
|
configPath,
|
|
configFile.replace("version: 2\n", "version: 2\nplatforms:\n - codex\n"),
|
|
"utf8",
|
|
);
|
|
await runInit(repo.rootDir);
|
|
|
|
await fs.writeFile(`${repo.rootDir}/AGENTS.md`, "manual\n", "utf8");
|
|
await fs.writeFile(
|
|
`${repo.rootDir}/.agents/user.txt`,
|
|
"authored\n",
|
|
"utf8",
|
|
);
|
|
const dryRun = await runUninstall(repo.rootDir, "dry-run");
|
|
const apply = await runUninstall(repo.rootDir, "apply");
|
|
const dryPlan = getLifecyclePlan(dryRun);
|
|
const applyPlan = getLifecyclePlan(apply);
|
|
|
|
expect(dryPlan?.entries).toEqual(applyPlan?.entries);
|
|
expect(dryPlan?.applicable).toBe(true);
|
|
expect(dryPlan?.applied).toBe(false);
|
|
expect(applyPlan?.applied).toBe(true);
|
|
await expect(fs.access(`${repo.rootDir}/AGENTS.md`)).resolves.toBeUndefined();
|
|
expect(await fs.readFile(`${repo.rootDir}/.agents/user.txt`, "utf8")).toBe(
|
|
"authored\n",
|
|
);
|
|
expect(await fs.readFile(`${repo.rootDir}/.truthmark/config.yml`, "utf8")).toContain(
|
|
"platforms:\n - codex",
|
|
);
|
|
} finally {
|
|
await repo.cleanup();
|
|
}
|
|
});
|
|
|
|
it("blocks uninstall when managed instruction markers are malformed", async () => {
|
|
const repo = await createTempRepo();
|
|
|
|
try {
|
|
await writeTruthmarkConfig(repo.rootDir);
|
|
const configPath = `${repo.rootDir}/.truthmark/config.yml`;
|
|
const configFile = await fs.readFile(configPath, "utf8");
|
|
await fs.writeFile(
|
|
configPath,
|
|
configFile.replace("version: 2\n", "version: 2\nplatforms:\n - codex\n"),
|
|
"utf8",
|
|
);
|
|
await runInit(repo.rootDir);
|
|
|
|
const malformed = `${TRUTHMARK_BLOCK_START}\nalpha\n${TRUTHMARK_BLOCK_START}\nbeta\n`;
|
|
await fs.writeFile(`${repo.rootDir}/AGENTS.md`, malformed, "utf8");
|
|
const before = await fs.readFile(`${repo.rootDir}/AGENTS.md`, "utf8");
|
|
|
|
const result = await runUninstall(repo.rootDir, "apply");
|
|
const plan = getLifecyclePlan(result);
|
|
|
|
expect(plan?.applicable).toBe(false);
|
|
expect(plan?.applied).toBe(false);
|
|
expect(await fs.readFile(`${repo.rootDir}/AGENTS.md`, "utf8")).toBe(before);
|
|
expect(plan?.entries).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
action: "manual-review",
|
|
path: "AGENTS.md",
|
|
}),
|
|
]),
|
|
);
|
|
} finally {
|
|
await repo.cleanup();
|
|
}
|
|
});
|
|
|
|
it("runs the same lifecycle plan for nested invocation and preserves config", async () => {
|
|
const repo = await createTempRepo();
|
|
|
|
try {
|
|
await writeTruthmarkConfig(repo.rootDir);
|
|
const configPath = `${repo.rootDir}/.truthmark/config.yml`;
|
|
const configFile = await fs.readFile(configPath, "utf8");
|
|
await fs.writeFile(
|
|
configPath,
|
|
configFile.replace("version: 2\n", "version: 2\nplatforms:\n - codex\n"),
|
|
"utf8",
|
|
);
|
|
await runInit(repo.rootDir);
|
|
|
|
await fs.mkdir(`${repo.rootDir}/nested`);
|
|
const nested = `${repo.rootDir}/nested`;
|
|
|
|
const dry = await runUninstall(nested, "dry-run");
|
|
const apply = await runUninstall(nested, "apply");
|
|
const dryPlan = getLifecyclePlan(dry);
|
|
const applyPlan = getLifecyclePlan(apply);
|
|
|
|
expect(dryPlan?.mode).toBe("dry-run");
|
|
expect(applyPlan?.mode).toBe("apply");
|
|
expect(dryPlan?.entries).toEqual(applyPlan?.entries);
|
|
expect(applyPlan?.applicable).toBe(true);
|
|
expect(applyPlan?.applied).toBe(true);
|
|
expect(await fs.readFile(configPath, "utf8")).toContain(
|
|
"platforms:\n - codex",
|
|
);
|
|
} finally {
|
|
await repo.cleanup();
|
|
}
|
|
});
|
|
|
|
it("does not remove unsafe generated files when a managed surface escapes containment", async () => {
|
|
const repo = await createTempRepo();
|
|
|
|
try {
|
|
await writeTruthmarkConfig(repo.rootDir);
|
|
const configPath = `${repo.rootDir}/.truthmark/config.yml`;
|
|
const configFile = await fs.readFile(configPath, "utf8");
|
|
await fs.writeFile(
|
|
configPath,
|
|
configFile.replace("version: 2\n", "version: 2\nplatforms:\n - codex\n"),
|
|
"utf8",
|
|
);
|
|
await runInit(repo.rootDir);
|
|
|
|
const outsideAgents = path.join(repo.rootDir, "..", "truthmark-uninstall-unsafe-agents");
|
|
await fs.mkdir(outsideAgents, { recursive: true });
|
|
const outsideTarget = path.join(outsideAgents, "AGENTS.md");
|
|
await fs.writeFile(outsideTarget, "# outside\n", "utf8");
|
|
await fs.rm(`${repo.rootDir}/AGENTS.md`);
|
|
await fs.symlink(outsideTarget, `${repo.rootDir}/AGENTS.md`);
|
|
|
|
const result = await runUninstall(repo.rootDir, "apply");
|
|
const plan = getLifecyclePlan(result);
|
|
const agentsEntry = plan?.entries.find(
|
|
(entry: LifecyclePlanEntry) => entry.path === "AGENTS.md",
|
|
);
|
|
|
|
expect(plan?.applicable).toBe(false);
|
|
expect(plan?.applied).toBe(false);
|
|
expect(agentsEntry).toEqual(
|
|
expect.objectContaining({
|
|
action: "manual-review",
|
|
path: "AGENTS.md",
|
|
}),
|
|
);
|
|
} finally {
|
|
await repo.cleanup();
|
|
await fs.rm(path.join(path.dirname(repo.rootDir), "truthmark-uninstall-unsafe-agents"), {
|
|
force: true,
|
|
recursive: true,
|
|
});
|
|
}
|
|
});
|
|
});
|