Files
truthmark/tests/init/interactive-platform-selection.test.ts
e3edcc8afa feat: v2.3.0 add interactive platform selection to init (#38)
* 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>
2026-07-31 18:54:10 +10:00

275 lines
8.3 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, it } from "node:test";
import { expect } from "expect";
import { parse } from "yaml";
import { runInit } from "../../src/init/init.js";
import { createTempRepo } from "../helpers/temp-repo.js";
describe("runInit platform selection", () => {
it("creates config and selected platform surfaces from explicit input", async () => {
const repo = await createTempRepo();
try {
const result = await runInit(repo.rootDir, {
platforms: ["cursor", "codex", "cursor"],
});
const config = parse(await repo.readFile(".truthmark/config.yml")) as {
version: number;
platforms: string[];
};
expect(result.diagnostics).toEqual(
expect.not.arrayContaining([
expect.objectContaining({ severity: "error" }),
]),
);
expect(config.version).toBe(2);
expect(config.platforms).toEqual(["codex", "cursor"]);
expect(await repo.readFile("AGENTS.md")).toContain("Truthmark Workflow");
expect(
await repo.readFile(".cursor/skills/truthmark-sync/SKILL.md"),
).toContain("Truth Sync");
await expect(fs.stat(`${repo.rootDir}/CLAUDE.md`)).rejects.toThrow();
} finally {
await repo.cleanup();
}
});
it("keeps a first noninteractive init host-neutral", async () => {
const repo = await createTempRepo();
try {
await runInit(repo.rootDir);
const config = parse(await repo.readFile(".truthmark/config.yml")) as Record<
string,
unknown
>;
expect(config.version).toBe(2);
expect(config).not.toHaveProperty("platforms");
await expect(fs.stat(`${repo.rootDir}/AGENTS.md`)).rejects.toThrow();
} finally {
await repo.cleanup();
}
});
it("applies a prompted selection on first run", async () => {
const repo = await createTempRepo();
try {
let defaults: readonly string[] = ["unexpected"];
await runInit(repo.rootDir, {
selectPlatforms: async (saved) => {
defaults = saved;
return ["opencode", "antigravity"];
},
});
const config = parse(await repo.readFile(".truthmark/config.yml")) as {
platforms: string[];
};
expect(defaults).toEqual([]);
expect(config.platforms).toEqual(["opencode", "antigravity"]);
} finally {
await repo.cleanup();
}
});
it("clears saved selections and generated host surfaces", async () => {
const repo = await createTempRepo();
try {
await runInit(repo.rootDir, { platforms: ["codex"] });
await runInit(repo.rootDir, {
selectPlatforms: async () => [],
});
const config = parse(await repo.readFile(".truthmark/config.yml")) as Record<
string,
unknown
>;
expect(config).not.toHaveProperty("platforms");
await expect(fs.stat(`${repo.rootDir}/AGENTS.md`)).rejects.toThrow();
} finally {
await repo.cleanup();
}
});
it("preselects saved platforms and preserves other config content", async () => {
const repo = await createTempRepo();
try {
const source = `# keep this comment
version: 2
platforms:
- codex
truthmark:
workspace: custom/truth
generated:
portal:
enabled: false
frontmatter:
required: []
recommended: []
ignore: []
`;
await repo.writeFile(".truthmark/config.yml", source);
let defaults: readonly string[] = [];
await runInit(repo.rootDir, {
selectPlatforms: async (saved) => {
defaults = saved;
return ["cursor"];
},
});
const updated = await repo.readFile(".truthmark/config.yml");
expect(defaults).toEqual(["codex"]);
expect(updated).toContain("# keep this comment");
expect(updated).toContain("workspace: custom/truth");
expect((parse(updated) as { platforms: string[] }).platforms).toEqual([
"cursor",
]);
} finally {
await repo.cleanup();
}
});
it("cancels before writing any repository files", async () => {
const repo = await createTempRepo();
try {
const result = await runInit(repo.rootDir, {
selectPlatforms: async () => null,
});
expect(result.summary).toContain("cancelled");
expect(result.diagnostics).toEqual([]);
await expect(
fs.stat(`${repo.rootDir}/.truthmark/config.yml`),
).rejects.toThrow();
await expect(
fs.stat(`${repo.rootDir}/docs/truthmark`),
).rejects.toThrow();
} finally {
await repo.cleanup();
}
});
it("rejects unsupported explicit platforms without writing", async () => {
const repo = await createTempRepo();
try {
const result = await runInit(repo.rootDir, {
platforms: ["not-a-platform"],
});
expect(result.diagnostics).toEqual(
expect.arrayContaining([
expect.objectContaining({
severity: "error",
message: expect.stringContaining("not-a-platform"),
}),
]),
);
await expect(
fs.stat(`${repo.rootDir}/.truthmark/config.yml`),
).rejects.toThrow();
} finally {
await repo.cleanup();
}
});
it("does not prompt or overwrite an invalid existing config", async () => {
const repo = await createTempRepo();
try {
const invalid = "version: 1\ncustom: true\n";
await repo.writeFile(".truthmark/config.yml", invalid);
let prompted = false;
const result = await runInit(repo.rootDir, {
selectPlatforms: async () => {
prompted = true;
return ["codex"];
},
});
expect(prompted).toBe(false);
expect(result.diagnostics).toEqual(
expect.arrayContaining([
expect.objectContaining({ severity: "error" }),
]),
);
expect(await repo.readFile(".truthmark/config.yml")).toBe(invalid);
} finally {
await repo.cleanup();
}
});
it("does not update config when lifecycle preflight is inapplicable", async () => {
const repo = await createTempRepo();
try {
await runInit(repo.rootDir, { platforms: ["codex"] });
const before = await repo.readFile(".truthmark/config.yml");
await repo.writeFile(
"AGENTS.md",
"<!-- truthmark:start -->\nfirst\n<!-- truthmark:start -->\nsecond\n<!-- truthmark:end -->\n",
);
const result = await runInit(repo.rootDir, { platforms: ["cursor"] });
expect(result.diagnostics).toEqual(
expect.arrayContaining([
expect.objectContaining({ severity: "error" }),
]),
);
expect(await repo.readFile(".truthmark/config.yml")).toBe(before);
} finally {
await repo.cleanup();
}
});
it("rejects an aliased config before removing generated surfaces", async () => {
const repo = await createTempRepo();
const outside = await fs.mkdtemp(path.join(os.tmpdir(), "truthmark-config-"));
try {
await runInit(repo.rootDir, { platforms: ["codex"] });
const configPath = path.join(repo.rootDir, ".truthmark/config.yml");
const configSource = await fs.readFile(configPath, "utf8");
const agentsSource = await repo.readFile("AGENTS.md");
const outsideConfigPath = path.join(outside, "config.yml");
await fs.writeFile(outsideConfigPath, configSource, "utf8");
await fs.rm(configPath);
await fs.symlink(outsideConfigPath, configPath);
const result = await runInit(repo.rootDir, { platforms: [] });
expect(result.diagnostics).toEqual(
expect.arrayContaining([
expect.objectContaining({
category: "config",
severity: "error",
file: ".truthmark/config.yml",
}),
]),
);
expect(await repo.readFile("AGENTS.md")).toBe(agentsSource);
expect(await fs.readFile(outsideConfigPath, "utf8")).toBe(configSource);
} finally {
await repo.cleanup();
await fs.rm(outside, { recursive: true, force: true });
}
});
it("keeps existing config bytes when saved selections are reused", async () => {
const repo = await createTempRepo();
try {
await runInit(repo.rootDir, { platforms: ["codex"] });
const before = await repo.readFile(".truthmark/config.yml");
await runInit(repo.rootDir);
expect(await repo.readFile(".truthmark/config.yml")).toBe(before);
} finally {
await repo.cleanup();
}
});
});