2026-06-30 20:57:55 +10:00
|
|
|
import { describe, it } from "node:test";
|
|
|
|
|
import { expect } from "expect";
|
2026-05-09 17:58:44 +10:00
|
|
|
|
|
|
|
|
import { loadConfig } from "../../src/config/load.js";
|
2026-07-31 18:54:10 +10:00
|
|
|
import { renderConfig } from "../../src/config/render.js";
|
2026-06-13 04:09:45 +10:00
|
|
|
import { createTempRepo } from "../helpers/temp-repo.js";
|
2026-05-09 17:58:44 +10:00
|
|
|
|
2026-06-13 04:09:45 +10:00
|
|
|
const validConfig = (portalProperties = "") => `version: 2
|
2026-05-09 17:58:44 +10:00
|
|
|
platforms:
|
|
|
|
|
- codex
|
2026-06-13 04:09:45 +10:00
|
|
|
truthmark:
|
|
|
|
|
workspace: docs/truthmark
|
|
|
|
|
generated:
|
|
|
|
|
portal:
|
|
|
|
|
enabled: true
|
|
|
|
|
${portalProperties}instruction_targets:
|
|
|
|
|
- AGENTS.md
|
2026-05-09 17:58:44 +10:00
|
|
|
frontmatter:
|
|
|
|
|
required: []
|
2026-06-13 04:09:45 +10:00
|
|
|
recommended: []
|
|
|
|
|
ignore: []
|
|
|
|
|
`;
|
2026-05-09 17:58:44 +10:00
|
|
|
|
2026-06-13 04:09:45 +10:00
|
|
|
describe("loadConfig", () => {
|
|
|
|
|
it("rejects legacy config shape", async () => {
|
2026-05-09 17:58:44 +10:00
|
|
|
const repo = await createTempRepo();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await repo.writeFile(
|
|
|
|
|
".truthmark/config.yml",
|
2026-06-13 04:09:45 +10:00
|
|
|
"version: 1\ndocs:\n roots:\n - docs/truth\n",
|
2026-05-09 17:58:44 +10:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const result = await loadConfig(repo.rootDir);
|
|
|
|
|
|
|
|
|
|
expect(result.status).toBe("invalid");
|
|
|
|
|
expect(result.config).toBeNull();
|
2026-06-30 20:57:55 +10:00
|
|
|
expect(
|
|
|
|
|
result.diagnostics.map((diagnostic) => diagnostic.message).join("\n"),
|
|
|
|
|
).toContain("Unsupported Truthmark config shape");
|
2026-05-09 17:58:44 +10:00
|
|
|
} finally {
|
|
|
|
|
await repo.cleanup();
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-06-13 04:09:45 +10:00
|
|
|
|
2026-06-16 00:54:46 +10:00
|
|
|
it("rejects unsupported truth config blocks", async () => {
|
|
|
|
|
const repo = await createTempRepo();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await repo.writeFile(
|
|
|
|
|
".truthmark/config.yml",
|
|
|
|
|
validConfig().replace(
|
|
|
|
|
" generated:\n",
|
|
|
|
|
" truth:\n root: truth\n product_root: custom-product\n engineering_root: custom-engineering\n generated:\n",
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const result = await loadConfig(repo.rootDir);
|
|
|
|
|
|
|
|
|
|
expect(result.status).toBe("invalid");
|
|
|
|
|
expect(result.config).toBeNull();
|
|
|
|
|
expect(result.diagnostics).toContainEqual(
|
|
|
|
|
expect.objectContaining({
|
2026-06-30 20:57:55 +10:00
|
|
|
message: expect.stringContaining(
|
|
|
|
|
"additional property truth is not allowed",
|
|
|
|
|
),
|
2026-06-16 00:54:46 +10:00
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
} finally {
|
|
|
|
|
await repo.cleanup();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects unsupported routes and templates config blocks", async () => {
|
|
|
|
|
const repo = await createTempRepo();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await repo.writeFile(
|
|
|
|
|
".truthmark/config.yml",
|
|
|
|
|
validConfig().replace(
|
|
|
|
|
" generated:\n",
|
|
|
|
|
" routes:\n index: routes/custom.md\n areas: routes/custom\n default_area: custom\n max_delegation_depth: 1\n templates:\n root: custom-templates\n generated:\n",
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const result = await loadConfig(repo.rootDir);
|
|
|
|
|
|
|
|
|
|
expect(result.status).toBe("invalid");
|
|
|
|
|
expect(result.config).toBeNull();
|
|
|
|
|
expect(result.diagnostics).toEqual(
|
|
|
|
|
expect.arrayContaining([
|
|
|
|
|
expect.objectContaining({
|
2026-06-30 20:57:55 +10:00
|
|
|
message: expect.stringContaining(
|
|
|
|
|
"additional property routes is not allowed",
|
|
|
|
|
),
|
2026-06-16 00:54:46 +10:00
|
|
|
}),
|
|
|
|
|
expect.objectContaining({
|
2026-06-30 20:57:55 +10:00
|
|
|
message: expect.stringContaining(
|
|
|
|
|
"additional property templates is not allowed",
|
|
|
|
|
),
|
2026-06-16 00:54:46 +10:00
|
|
|
}),
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
} finally {
|
|
|
|
|
await repo.cleanup();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects unsafe workspace paths", async () => {
|
2026-05-09 17:58:44 +10:00
|
|
|
const repo = await createTempRepo();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await repo.writeFile(
|
|
|
|
|
".truthmark/config.yml",
|
2026-06-30 20:57:55 +10:00
|
|
|
validConfig().replace(
|
|
|
|
|
"workspace: docs/truthmark",
|
|
|
|
|
"workspace: ../truthmark",
|
|
|
|
|
),
|
2026-06-13 04:09:45 +10:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const result = await loadConfig(repo.rootDir);
|
|
|
|
|
|
|
|
|
|
expect(result.status).toBe("invalid");
|
|
|
|
|
expect(result.config).toBeNull();
|
|
|
|
|
expect(result.diagnostics).toContainEqual(
|
|
|
|
|
expect.objectContaining({
|
2026-06-30 20:57:55 +10:00
|
|
|
message: expect.stringContaining(
|
|
|
|
|
"truthmark.workspace must be a non-empty repo-relative directory",
|
|
|
|
|
),
|
2026-06-13 04:09:45 +10:00
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
} finally {
|
|
|
|
|
await repo.cleanup();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects custom Portal output and template properties", async () => {
|
|
|
|
|
const repo = await createTempRepo();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await repo.writeFile(
|
|
|
|
|
".truthmark/config.yml",
|
2026-06-30 20:57:55 +10:00
|
|
|
validConfig(
|
|
|
|
|
" output: docs/custom-portal\n template: docs/custom-template.md\n",
|
|
|
|
|
),
|
2026-05-09 17:58:44 +10:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const result = await loadConfig(repo.rootDir);
|
|
|
|
|
|
|
|
|
|
expect(result.status).toBe("invalid");
|
|
|
|
|
expect(result.config).toBeNull();
|
|
|
|
|
expect(result.diagnostics).toEqual(
|
|
|
|
|
expect.arrayContaining([
|
|
|
|
|
expect.objectContaining({
|
2026-06-30 20:57:55 +10:00
|
|
|
message: expect.stringContaining(
|
|
|
|
|
"additional property output is not allowed",
|
|
|
|
|
),
|
2026-06-13 04:09:45 +10:00
|
|
|
}),
|
|
|
|
|
expect.objectContaining({
|
2026-06-30 20:57:55 +10:00
|
|
|
message: expect.stringContaining(
|
|
|
|
|
"additional property template is not allowed",
|
|
|
|
|
),
|
2026-05-09 17:58:44 +10:00
|
|
|
}),
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
} finally {
|
|
|
|
|
await repo.cleanup();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-13 04:09:45 +10:00
|
|
|
it("derives Portal paths from the Truthmark workspace defaults", async () => {
|
2026-05-09 17:58:44 +10:00
|
|
|
const repo = await createTempRepo();
|
|
|
|
|
|
|
|
|
|
try {
|
2026-06-13 04:09:45 +10:00
|
|
|
await repo.writeFile(".truthmark/config.yml", validConfig());
|
2026-05-28 14:39:21 +10:00
|
|
|
|
|
|
|
|
const result = await loadConfig(repo.rootDir);
|
|
|
|
|
|
|
|
|
|
expect(result.status).toBe("loaded");
|
2026-06-30 20:57:55 +10:00
|
|
|
expect(result.config?.truthmark.generated.portal).toEqual({
|
|
|
|
|
enabled: true,
|
|
|
|
|
});
|
|
|
|
|
expect(result.config?.truthmark.paths.portalOutput).toBe(
|
|
|
|
|
"docs/truthmark/generated/portal",
|
|
|
|
|
);
|
|
|
|
|
expect(result.config?.truthmark.paths.portalTemplate).toBe(
|
|
|
|
|
"docs/truthmark/templates/portal.html",
|
|
|
|
|
);
|
2026-05-09 17:58:44 +10:00
|
|
|
} finally {
|
|
|
|
|
await repo.cleanup();
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-06-16 00:54:46 +10:00
|
|
|
|
2026-06-21 01:16:10 +10:00
|
|
|
it("accepts active platform support values", async () => {
|
|
|
|
|
const repo = await createTempRepo();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await repo.writeFile(
|
|
|
|
|
".truthmark/config.yml",
|
|
|
|
|
validConfig().replace(
|
|
|
|
|
"platforms:\n - codex\n",
|
|
|
|
|
[
|
|
|
|
|
"platforms:",
|
|
|
|
|
" - codex",
|
|
|
|
|
" - opencode",
|
|
|
|
|
" - claude-code",
|
|
|
|
|
" - github-copilot",
|
|
|
|
|
" - antigravity",
|
|
|
|
|
" - cursor",
|
|
|
|
|
"",
|
|
|
|
|
].join("\n"),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const result = await loadConfig(repo.rootDir);
|
|
|
|
|
|
|
|
|
|
expect(result.status).toBe("loaded");
|
|
|
|
|
expect(result.config?.platforms).toEqual([
|
|
|
|
|
"codex",
|
|
|
|
|
"opencode",
|
|
|
|
|
"claude-code",
|
|
|
|
|
"github-copilot",
|
|
|
|
|
"antigravity",
|
|
|
|
|
"cursor",
|
|
|
|
|
]);
|
|
|
|
|
} finally {
|
|
|
|
|
await repo.cleanup();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects retired Gemini CLI platform support", async () => {
|
|
|
|
|
const repo = await createTempRepo();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await repo.writeFile(
|
|
|
|
|
".truthmark/config.yml",
|
|
|
|
|
validConfig().replace(" - codex", " - gemini-cli"),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const result = await loadConfig(repo.rootDir);
|
|
|
|
|
|
|
|
|
|
expect(result.status).toBe("invalid");
|
|
|
|
|
expect(result.config).toBeNull();
|
|
|
|
|
expect(result.diagnostics).toContainEqual(
|
|
|
|
|
expect.objectContaining({
|
2026-06-30 20:57:55 +10:00
|
|
|
message: expect.stringContaining(
|
|
|
|
|
"must be equal to one of the allowed values",
|
|
|
|
|
),
|
2026-06-21 01:16:10 +10:00
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
} finally {
|
|
|
|
|
await repo.cleanup();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-20 20:14:07 +10:00
|
|
|
it("does not assume a host platform when platforms are omitted", async () => {
|
|
|
|
|
const repo = await createTempRepo();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await repo.writeFile(
|
|
|
|
|
".truthmark/config.yml",
|
|
|
|
|
validConfig().replace("platforms:\n - codex\n", ""),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const result = await loadConfig(repo.rootDir);
|
|
|
|
|
|
|
|
|
|
expect(result.status).toBe("loaded");
|
|
|
|
|
expect(result.config?.platforms).toEqual([]);
|
|
|
|
|
} finally {
|
|
|
|
|
await repo.cleanup();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-12 20:48:09 +10:00
|
|
|
it("accepts legacy instruction_targets with review guidance", async () => {
|
|
|
|
|
const repo = await createTempRepo();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await repo.writeFile(
|
|
|
|
|
".truthmark/config.yml",
|
|
|
|
|
validConfig().replace(
|
|
|
|
|
" - AGENTS.md\n",
|
|
|
|
|
" - src/session.ts\n - AGENTS.md\n",
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const result = await loadConfig(repo.rootDir);
|
|
|
|
|
|
|
|
|
|
expect(result.status).toBe("loaded");
|
|
|
|
|
expect(result.config?.platforms).toEqual(["codex"]);
|
|
|
|
|
expect(result.diagnostics).toEqual(
|
|
|
|
|
expect.arrayContaining([
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
message: expect.stringContaining(
|
|
|
|
|
"instruction_targets is accepted for compatibility but ignored",
|
|
|
|
|
),
|
|
|
|
|
}),
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
} finally {
|
|
|
|
|
await repo.cleanup();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-16 00:54:46 +10:00
|
|
|
it("derives fixed internal paths from a custom workspace", async () => {
|
|
|
|
|
const repo = await createTempRepo();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await repo.writeFile(
|
|
|
|
|
".truthmark/config.yml",
|
2026-06-30 20:57:55 +10:00
|
|
|
validConfig().replace(
|
|
|
|
|
"workspace: docs/truthmark",
|
|
|
|
|
"workspace: docs/custom-truthmark",
|
|
|
|
|
),
|
2026-06-16 00:54:46 +10:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const result = await loadConfig(repo.rootDir);
|
|
|
|
|
|
|
|
|
|
expect(result.status).toBe("loaded");
|
|
|
|
|
expect(result.config?.truthmark.routes).toEqual({
|
|
|
|
|
index: "routes/areas.md",
|
|
|
|
|
areas: "routes/areas",
|
|
|
|
|
defaultArea: "repository",
|
|
|
|
|
maxDelegationDepth: 1,
|
|
|
|
|
});
|
|
|
|
|
expect(result.config?.truthmark.truth.productRoot).toBe("product");
|
2026-06-30 20:57:55 +10:00
|
|
|
expect(result.config?.truthmark.truth.engineeringRoot).toBe(
|
|
|
|
|
"engineering",
|
|
|
|
|
);
|
2026-06-16 00:54:46 +10:00
|
|
|
expect(result.config?.truthmark.templates.root).toBe("templates");
|
|
|
|
|
expect(result.config?.truthmark.paths.routesIndex).toBe(
|
|
|
|
|
"docs/custom-truthmark/routes/areas.md",
|
|
|
|
|
);
|
|
|
|
|
expect(result.config?.truthmark.paths.routeAreasRoot).toBe(
|
|
|
|
|
"docs/custom-truthmark/routes/areas",
|
|
|
|
|
);
|
2026-06-30 20:57:55 +10:00
|
|
|
expect(result.config?.truthmark.paths.productTruthRoot).toBe(
|
|
|
|
|
"docs/custom-truthmark/product",
|
|
|
|
|
);
|
2026-06-16 00:54:46 +10:00
|
|
|
expect(result.config?.truthmark.paths.engineeringTruthRoot).toBe(
|
|
|
|
|
"docs/custom-truthmark/engineering",
|
|
|
|
|
);
|
|
|
|
|
expect(result.config?.truthmark.paths.templatesRoot).toBe(
|
|
|
|
|
"docs/custom-truthmark/templates",
|
|
|
|
|
);
|
|
|
|
|
expect(result.config?.truthmark.paths.portalOutput).toBe(
|
|
|
|
|
"docs/custom-truthmark/generated/portal",
|
|
|
|
|
);
|
|
|
|
|
expect(result.config?.truthmark.paths.portalTemplate).toBe(
|
|
|
|
|
"docs/custom-truthmark/templates/portal.html",
|
|
|
|
|
);
|
|
|
|
|
} finally {
|
|
|
|
|
await repo.cleanup();
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-07-12 20:48:09 +10:00
|
|
|
|
2026-07-31 18:54:10 +10:00
|
|
|
it("renders a default config without legacy instruction_targets", () => {
|
|
|
|
|
const rendered = renderConfig();
|
2026-07-12 20:48:09 +10:00
|
|
|
|
2026-07-31 18:54:10 +10:00
|
|
|
expect(rendered).not.toContain("instruction_targets:");
|
2026-07-12 20:48:09 +10:00
|
|
|
});
|
2026-05-09 17:58:44 +10:00
|
|
|
});
|