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>
588 lines
22 KiB
TypeScript
588 lines
22 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
import { describe, it } from "node:test";
|
|
import { expect } from "expect";
|
|
|
|
import { createDefaultConfig } from "../../src/config/defaults.js";
|
|
import { renderTruthSyncSkillBody } from "../../src/agents/truth-sync.js";
|
|
import { renderAgentsBlock } from "../../src/templates/agents-block.js";
|
|
import {
|
|
renderGeneratedSurfaces,
|
|
renderGeneratedSurfaceCatalog,
|
|
type GeneratedSurfaceOwner,
|
|
} from "../../src/templates/generated-surfaces.js";
|
|
|
|
const platformOwners = (owners: GeneratedSurfaceOwner[]) =>
|
|
owners.filter(
|
|
(owner): owner is Extract<GeneratedSurfaceOwner, { platform: string }> =>
|
|
"platform" in owner,
|
|
);
|
|
|
|
const allPlatforms = [
|
|
"codex",
|
|
"opencode",
|
|
"claude-code",
|
|
"github-copilot",
|
|
"antigravity",
|
|
"cursor",
|
|
] as const;
|
|
|
|
const portalPaths = [
|
|
".agents/skills/truthmark-portal/SKILL.md",
|
|
".agents/skills/truthmark-portal/agents/openai.yaml",
|
|
".opencode/skills/truthmark-portal/SKILL.md",
|
|
".claude/skills/truthmark-portal/SKILL.md",
|
|
".github/skills/truthmark-portal/SKILL.md",
|
|
".github/prompts/truthmark-portal.prompt.md",
|
|
".antigravity/rules/truthmark-portal.md",
|
|
".cursor/skills/truthmark-portal/SKILL.md",
|
|
];
|
|
|
|
const readOnlyProcedurePaths = [
|
|
".agents/skills/truthmark-check/support/procedure.md",
|
|
".opencode/skills/truthmark-check/support/procedure.md",
|
|
".claude/skills/truthmark-check/support/procedure.md",
|
|
".github/skills/truthmark-check/support/procedure.md",
|
|
];
|
|
|
|
const syncProcedurePaths = [
|
|
".agents/skills/truthmark-sync/support/procedure.md",
|
|
".opencode/skills/truthmark-sync/support/procedure.md",
|
|
".claude/skills/truthmark-sync/support/procedure.md",
|
|
".github/skills/truthmark-sync/support/procedure.md",
|
|
];
|
|
|
|
const staleWriteAuthorizingLaneText =
|
|
"before writing canonical truth docs, classify the request or change as product-lane, engineering-lane, both-lane, or ambiguous";
|
|
|
|
describe("Truthmark Portal generated surfaces", () => {
|
|
it("keeps checked-in read-only procedures free of write-authorizing lane wording", () => {
|
|
for (const procedurePath of readOnlyProcedurePaths) {
|
|
const content = readFileSync(join(process.cwd(), procedurePath), "utf8");
|
|
|
|
expect(content).not.toContain(staleWriteAuthorizingLaneText);
|
|
expect(content).toContain(
|
|
"classify the request or changed surface as product-lane, engineering-lane, both-lane, or ambiguous for reporting only",
|
|
);
|
|
}
|
|
});
|
|
|
|
it("keeps checked-in Sync procedures on the cheap product-truth decision", () => {
|
|
for (const procedurePath of syncProcedurePaths) {
|
|
const content = readFileSync(join(process.cwd(), procedurePath), "utf8");
|
|
|
|
expect(content).toContain("Product truth decision");
|
|
expect(content).toContain(
|
|
"ask whether a user-visible promise, capability boundary, API contract, acceptance criterion, or explicit user/product evidence changed",
|
|
);
|
|
expect(content).toContain(
|
|
"if no, default to engineering truth under docs/truthmark/engineering for internal implementation changes",
|
|
);
|
|
expect(content).toContain(
|
|
"Product truth is opt-in for externally visible promises, product boundaries, APIs, acceptance criteria, or explicit user/product evidence.",
|
|
);
|
|
expect(content).toContain("User-provided decisions/rationale");
|
|
expect(content).not.toContain(staleWriteAuthorizingLaneText);
|
|
expect(content).not.toContain(
|
|
"classify lane impact as product-lane, engineering-lane, both-lane, or ambiguous before writing",
|
|
);
|
|
}
|
|
});
|
|
|
|
it("omits generic optional CLI validation from generated user-facing workflow surfaces", () => {
|
|
const config = createDefaultConfig();
|
|
config.platforms = [...allPlatforms];
|
|
const generatedSurfaces = renderGeneratedSurfaces(config);
|
|
const publicWorkflowSurfaces = generatedSurfaces.filter(
|
|
(surface) =>
|
|
surface.path.endsWith("/SKILL.md") ||
|
|
surface.path.startsWith(".github/prompts/") ||
|
|
surface.path.startsWith(".antigravity/rules/"),
|
|
);
|
|
const forbiddenText = [
|
|
"## Optional local CLI validation",
|
|
"truthmark workflow instructions --json",
|
|
["truthmark", "workflow", "status", "--json"].join(" "),
|
|
"workflow instructions --json",
|
|
["workflow", "status", "--json"].join(" "),
|
|
"live preflight",
|
|
"OpenSpec-style",
|
|
"proposal/spec/task",
|
|
"spec lifecycle",
|
|
"archive/apply",
|
|
"truthmark/changes",
|
|
];
|
|
|
|
expect(publicWorkflowSurfaces.length).toBeGreaterThan(0);
|
|
for (const surface of publicWorkflowSurfaces) {
|
|
for (const text of forbiddenText) {
|
|
expect(surface.content).not.toContain(text);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("keeps generated runtime surfaces free of repeated host/support overhead", () => {
|
|
const config = createDefaultConfig();
|
|
config.platforms = [...allPlatforms];
|
|
const surfaces = renderGeneratedSurfaces(config);
|
|
const runtimeSurfaces = surfaces.filter(
|
|
(surface) =>
|
|
surface.path.startsWith(".agents/") ||
|
|
surface.path.startsWith(".opencode/") ||
|
|
surface.path.startsWith(".claude/") ||
|
|
surface.path.startsWith(".github/") ||
|
|
surface.path.startsWith(".antigravity/") ||
|
|
surface.path.startsWith(".cursor/"),
|
|
);
|
|
const crossHostInvocationText = [
|
|
"Invocations:",
|
|
"OpenCode /skill truthmark-",
|
|
"Codex /truthmark-",
|
|
"Claude Code /truthmark-",
|
|
"GitHub Copilot /truthmark-",
|
|
"Antigravity @truthmark-",
|
|
"Cursor @truthmark-",
|
|
];
|
|
|
|
for (const surface of runtimeSurfaces) {
|
|
for (const text of crossHostInvocationText) {
|
|
expect(surface.content).not.toContain(text);
|
|
}
|
|
}
|
|
|
|
const skillEntrypoints = runtimeSurfaces.filter((surface) =>
|
|
surface.path.endsWith("/SKILL.md"),
|
|
);
|
|
for (const surface of skillEntrypoints) {
|
|
expect(surface.content).toContain("Progressive disclosure:");
|
|
expect(surface.content).not.toContain("Read support/");
|
|
}
|
|
|
|
const flatRules = runtimeSurfaces.filter(
|
|
(surface) =>
|
|
surface.path.startsWith(".antigravity/rules/") ||
|
|
surface.path.startsWith(".cursor/rules/"),
|
|
);
|
|
expect(flatRules.length).toBeGreaterThan(0);
|
|
for (const surface of flatRules) {
|
|
expect(surface.content).not.toContain("Quick procedure:");
|
|
expect(surface.content).not.toContain("support/procedure.md");
|
|
expect(surface.content).not.toContain("support/report-template.md");
|
|
expect(surface.content).not.toContain("support/subagents-and-leases.md");
|
|
expect(surface.content).toContain("## Procedure");
|
|
expect(surface.content).toContain("## Report Template");
|
|
}
|
|
});
|
|
|
|
it("keeps compact workflow-specific validation and procedure guidance", () => {
|
|
const config = createDefaultConfig();
|
|
config.platforms = [...allPlatforms];
|
|
const byPath = new Map(
|
|
renderGeneratedSurfaces(config).map((surface) => [
|
|
surface.path,
|
|
surface.content,
|
|
]),
|
|
);
|
|
|
|
const syncSkill =
|
|
byPath.get(".agents/skills/truthmark-sync/SKILL.md") ?? "";
|
|
const syncProcedure =
|
|
byPath.get(".agents/skills/truthmark-sync/support/procedure.md") ?? "";
|
|
|
|
expect(syncSkill).toContain(
|
|
"Use this skill automatically before finishing",
|
|
);
|
|
expect(syncSkill).not.toContain("Parent workflow:");
|
|
expect(syncSkill).toContain("support/procedure.md");
|
|
expect(syncSkill).toContain("support/report-template.md");
|
|
expect(byPath.get(".opencode/skills/truthmark-sync/SKILL.md")).toContain(
|
|
"Use this skill automatically before finishing",
|
|
);
|
|
expect(syncProcedure).toContain("Code verification is parent-owned");
|
|
expect(syncProcedure).toContain(
|
|
"truthmark validate sync-report <report-file> --json",
|
|
);
|
|
expect(
|
|
byPath.has(".agents/skills/truthmark-sync/helper-manifest.yml"),
|
|
).toBe(false);
|
|
expect(
|
|
byPath.has(".agents/skills/truthmark-sync/support/helper-policy.md"),
|
|
).toBe(false);
|
|
expect(byPath.has(".agents/skills/truthmark-preview/SKILL.md")).toBe(false);
|
|
expect(byPath.has(".github/prompts/truthmark-preview.prompt.md")).toBe(
|
|
false,
|
|
);
|
|
expect(byPath.has(".gemini/commands/truthmark/preview.toml")).toBe(false);
|
|
expect(byPath.has("GEMINI.md")).toBe(false);
|
|
expect(byPath.has(".gemini/skills/truthmark-sync/SKILL.md")).toBe(false);
|
|
expect(byPath.has(".gemini/commands/truthmark/sync.toml")).toBe(false);
|
|
expect(byPath.has(".antigravity/rules/truthmark-sync.md")).toBe(true);
|
|
expect(byPath.has(".cursor/skills/truthmark-sync/SKILL.md")).toBe(true);
|
|
expect(
|
|
byPath.has(".cursor/skills/truthmark-sync/support/procedure.md"),
|
|
).toBe(true);
|
|
expect(
|
|
byPath.has(".cursor/skills/truthmark-sync/support/report-template.md"),
|
|
).toBe(true);
|
|
expect(
|
|
byPath.has(".agents/skills/truthmark-preview/agents/openai.yaml"),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("does not render unused repo-local agent package copies", () => {
|
|
const config = createDefaultConfig();
|
|
config.platforms = [...allPlatforms];
|
|
const paths = renderGeneratedSurfaces(config).map(
|
|
(surface) => surface.path,
|
|
);
|
|
|
|
expect(paths.some((path) => path.startsWith(".truthmark/agent/"))).toBe(
|
|
false,
|
|
);
|
|
expect(paths).toContain(".agents/skills/truthmark-sync/SKILL.md");
|
|
expect(paths).toContain(
|
|
".agents/skills/truthmark-sync/support/procedure.md",
|
|
);
|
|
expect(paths).toContain(
|
|
".opencode/skills/truthmark-sync/support/procedure.md",
|
|
);
|
|
});
|
|
|
|
it("renders host skill packages with colocated native resources", () => {
|
|
const config = createDefaultConfig();
|
|
config.platforms = [...allPlatforms];
|
|
const byPath = new Map(
|
|
renderGeneratedSurfaces(config).map((surface) => [
|
|
surface.path,
|
|
surface.content,
|
|
]),
|
|
);
|
|
const claudeProcedure =
|
|
byPath.get(".claude/skills/truthmark-sync/support/procedure.md") ?? "";
|
|
const codexProcedure =
|
|
byPath.get(".agents/skills/truthmark-sync/support/procedure.md") ?? "";
|
|
const opencodeReport =
|
|
byPath.get(
|
|
".opencode/skills/truthmark-sync/support/report-template.md",
|
|
) ?? "";
|
|
|
|
expect(claudeProcedure).toContain("Parent workflow:");
|
|
expect(codexProcedure).toContain("Parent workflow:");
|
|
expect(opencodeReport).toContain("Changed code reviewed:");
|
|
expect(opencodeReport).toContain("Decision/rationale captured:");
|
|
expect(claudeProcedure).not.toContain(
|
|
"truthmark:adapter-mode=expanded-adapter",
|
|
);
|
|
});
|
|
|
|
it("does not teach Sync to update bootstrap routing as behavior truth", () => {
|
|
const config = createDefaultConfig();
|
|
config.platforms = [...allPlatforms];
|
|
const renderedSurfaces = renderGeneratedSurfaces(config);
|
|
const generatedSyncReportSurfaces = renderedSurfaces.filter(
|
|
(surface) =>
|
|
surface.path.endsWith("truthmark-sync/support/report-template.md") ||
|
|
surface.path === ".antigravity/rules/truthmark-sync.md" ||
|
|
surface.path ===
|
|
".cursor/skills/truthmark-sync/support/report-template.md",
|
|
);
|
|
const checkedInSyncReportPaths = [
|
|
".agents/skills/truthmark-sync/support/report-template.md",
|
|
".opencode/skills/truthmark-sync/support/report-template.md",
|
|
".claude/skills/truthmark-sync/support/report-template.md",
|
|
".github/skills/truthmark-sync/support/report-template.md",
|
|
".antigravity/rules/truthmark-sync.md",
|
|
".cursor/skills/truthmark-sync/support/report-template.md",
|
|
];
|
|
const surfacesToCheck = [
|
|
{
|
|
path: "renderTruthSyncSkillBody",
|
|
content: renderTruthSyncSkillBody(config),
|
|
},
|
|
...generatedSyncReportSurfaces,
|
|
...checkedInSyncReportPaths.map((path) => ({
|
|
path,
|
|
content: readFileSync(join(process.cwd(), path), "utf8"),
|
|
})),
|
|
];
|
|
|
|
expect(generatedSyncReportSurfaces.length).toBeGreaterThan(0);
|
|
for (const surface of surfacesToCheck) {
|
|
expect(surface.content).toContain(
|
|
"docs/truthmark/routes/areas/authentication.md",
|
|
);
|
|
expect(surface.content).toContain(
|
|
"docs/truthmark/engineering/behaviors/session-timeout.md",
|
|
);
|
|
expect(surface.content).toContain(
|
|
"Changed code maps only to the provisional bootstrap route.",
|
|
);
|
|
expect(surface.content).toContain(
|
|
"Run Truth Structure for src/auth/** before updating behavior truth.",
|
|
);
|
|
expect(surface.content).not.toContain(
|
|
"- Target truth docs: docs/truthmark/engineering/repository/bootstrap-routing.md",
|
|
);
|
|
expect(surface.content).not.toContain(
|
|
[
|
|
"Truth docs updated:",
|
|
"- docs/truthmark/engineering/repository/bootstrap-routing.md",
|
|
].join("\n"),
|
|
);
|
|
expect(surface.content).not.toContain(
|
|
"Session timeout behavior is documented in the mapped repository truth doc.",
|
|
);
|
|
}
|
|
});
|
|
|
|
it("omits Portal surfaces and AGENTS wording when disabled", () => {
|
|
const config = createDefaultConfig();
|
|
const paths = renderGeneratedSurfaces(config).map(
|
|
(surface) => surface.path,
|
|
);
|
|
|
|
expect(config.truthmark.generated.portal.enabled).toBe(false);
|
|
for (const portalPath of portalPaths) {
|
|
expect(paths).not.toContain(portalPath);
|
|
}
|
|
expect(renderAgentsBlock(config)).not.toContain("Truthmark Portal");
|
|
});
|
|
|
|
it("renders Portal surfaces for all configured platforms when enabled", () => {
|
|
const config = createDefaultConfig();
|
|
config.platforms = [...allPlatforms];
|
|
config.truthmark.generated.portal = {
|
|
enabled: true,
|
|
};
|
|
|
|
const surfaces = renderGeneratedSurfaces(config);
|
|
const byPath = new Map(
|
|
surfaces.map((surface) => [surface.path, surface.content]),
|
|
);
|
|
|
|
for (const portalPath of portalPaths) {
|
|
expect(byPath.has(portalPath)).toBe(true);
|
|
}
|
|
|
|
const portalSkill =
|
|
byPath.get(".agents/skills/truthmark-portal/SKILL.md") ?? "";
|
|
const portalProcedure =
|
|
byPath.get(".agents/skills/truthmark-portal/support/procedure.md") ?? "";
|
|
const copilotPrompt =
|
|
byPath.get(".github/prompts/truthmark-portal.prompt.md") ?? "";
|
|
const antigravityRule =
|
|
byPath.get(".antigravity/rules/truthmark-portal.md") ?? "";
|
|
const cursorSkill =
|
|
byPath.get(".cursor/skills/truthmark-portal/SKILL.md") ?? "";
|
|
const cursorProcedure =
|
|
byPath.get(".cursor/skills/truthmark-portal/support/procedure.md") ?? "";
|
|
const agentsBlock = renderAgentsBlock(config);
|
|
|
|
for (const text of [portalSkill, portalProcedure]) {
|
|
expect(text).toContain("manual-only");
|
|
expect(text).toContain("Markdown remains canonical");
|
|
expect(text).toContain("does not require the truthmark CLI");
|
|
expect(text).toContain("docs/truthmark/generated/portal");
|
|
expect(text).toContain("docs/truthmark/templates/portal.html");
|
|
expect(text).toContain("no remote dependencies");
|
|
expect(text).toContain("no .truthmark/index.json dependency");
|
|
expect(text).toContain("source provenance");
|
|
expect(text).toContain("generated non-canonical static files");
|
|
}
|
|
|
|
expect(copilotPrompt).toContain(
|
|
"This prompt is the GitHub Copilot entrypoint for Truthmark Portal.",
|
|
);
|
|
expect(copilotPrompt).toContain(".github/skills/truthmark-portal/SKILL.md");
|
|
expect(antigravityRule).toContain(
|
|
"This rule is the Antigravity entrypoint for Truthmark Portal.",
|
|
);
|
|
expect(cursorSkill).toContain("Use as a Cursor Agent Skill.");
|
|
expect(cursorSkill).toContain(".cursor/skills/");
|
|
expect(cursorSkill).toContain("Progressive disclosure:");
|
|
expect(cursorProcedure).toContain("manual-only");
|
|
|
|
expect(portalProcedure).toContain("replace the entire output directory");
|
|
expect(portalProcedure).toContain("fixed Portal output directory only");
|
|
expect(portalProcedure).not.toContain("custom Portal output");
|
|
expect(agentsBlock).toContain(
|
|
"Truthmark Portal is a separate manual-only presentation workflow",
|
|
);
|
|
expect(agentsBlock).toContain("docs/truthmark/generated/portal/");
|
|
expect(agentsBlock).toContain("Markdown remains canonical");
|
|
});
|
|
|
|
it("maps platform-derived instruction files to AGENTS.md and CLAUDE.md with exact-path dedupe", () => {
|
|
const config = createDefaultConfig();
|
|
config.platforms = ["codex", "opencode", "claude-code"];
|
|
const allSurfaces = renderGeneratedSurfaces(config);
|
|
const allPaths = allSurfaces.map((surface) => surface.path);
|
|
const catalog = renderGeneratedSurfaceCatalog(config);
|
|
const agentsEntry = catalog.find((entry) => entry.path === "AGENTS.md");
|
|
const claudeEntry = catalog.find((entry) => entry.path === "CLAUDE.md");
|
|
|
|
expect(allPaths).toContain("AGENTS.md");
|
|
expect(allPaths).toContain("CLAUDE.md");
|
|
expect(new Set(allPaths).size).toBe(allPaths.length);
|
|
expect(allPaths.filter((path) => path === "AGENTS.md")).toHaveLength(1);
|
|
expect(allPaths.filter((path) => path === "CLAUDE.md")).toHaveLength(1);
|
|
expect(agentsEntry).toBeDefined();
|
|
const agentsPlatforms =
|
|
agentsEntry === undefined ? [] : platformOwners(agentsEntry.owners).map((owner) => owner.platform);
|
|
expect(new Set(agentsPlatforms).size).toBe(2);
|
|
expect(
|
|
agentsPlatforms.sort(),
|
|
).toEqual(["codex", "opencode"]);
|
|
expect(claudeEntry).toBeDefined();
|
|
expect(claudeEntry?.owners).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
kind: "platform",
|
|
platform: "claude-code",
|
|
}),
|
|
]),
|
|
);
|
|
expect(renderGeneratedSurfaces({ ...config, platforms: [] })).toEqual([]);
|
|
});
|
|
|
|
it("builds an ownership catalog with stable content signatures across all platforms", () => {
|
|
const config = createDefaultConfig();
|
|
const normalized = (content: string): string =>
|
|
content.endsWith("\n") ? content : `${content}\n`;
|
|
config.platforms = [...allPlatforms];
|
|
|
|
const catalog = renderGeneratedSurfaceCatalog(config);
|
|
const configForPlatformAndPortal = (
|
|
platform: (typeof allPlatforms)[number],
|
|
portalEnabled: boolean,
|
|
) => ({
|
|
...config,
|
|
truthmark: {
|
|
...config.truthmark,
|
|
generated: {
|
|
...config.truthmark.generated,
|
|
portal: { enabled: portalEnabled },
|
|
},
|
|
},
|
|
platforms: [platform],
|
|
});
|
|
const platformSurfaceContents = new Map<string, Set<string>>();
|
|
const addSurfaceContents = (surfaces: ReturnType<typeof renderGeneratedSurfaces>) => {
|
|
for (const surface of surfaces) {
|
|
const signatures = platformSurfaceContents.get(surface.path) ?? new Set();
|
|
signatures.add(normalized(surface.content));
|
|
platformSurfaceContents.set(surface.path, signatures);
|
|
}
|
|
};
|
|
|
|
for (const platform of allPlatforms) {
|
|
addSurfaceContents(renderGeneratedSurfaces(configForPlatformAndPortal(platform, false)));
|
|
addSurfaceContents(renderGeneratedSurfaces(configForPlatformAndPortal(platform, true)));
|
|
}
|
|
|
|
for (const entry of catalog) {
|
|
const expectedContents = platformSurfaceContents.get(entry.path);
|
|
if (expectedContents === undefined) continue;
|
|
|
|
expect(entry.recognizedContents.length).toBeGreaterThan(0);
|
|
for (const content of entry.recognizedContents) {
|
|
expect(expectedContents).toContain(content);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("classifies shared renderer outputs as managed blocks and keeps all-platform ownership stable", () => {
|
|
const config = createDefaultConfig();
|
|
config.platforms = [...allPlatforms];
|
|
config.truthmark.generated.portal.enabled = true;
|
|
|
|
const surfaces = renderGeneratedSurfaces(config);
|
|
const managedBlockPaths = new Set(
|
|
["AGENTS.md", "CLAUDE.md", ".github/copilot-instructions.md"].filter(
|
|
(surfacePath) =>
|
|
surfaces.some((surface) => surface.path === surfacePath),
|
|
),
|
|
);
|
|
|
|
for (const surface of surfaces) {
|
|
if (managedBlockPaths.has(surface.path)) {
|
|
expect(surface.managedBlock).toBe(true);
|
|
} else {
|
|
expect(surface).not.toHaveProperty("managedBlock");
|
|
}
|
|
}
|
|
|
|
const catalog = renderGeneratedSurfaceCatalog(config);
|
|
const agentsEntry = catalog.find((entry) => entry.path === "AGENTS.md");
|
|
const claudeEntry = catalog.find((entry) => entry.path === "CLAUDE.md");
|
|
const copilotEntry = catalog.find(
|
|
(entry) => entry.path === ".github/copilot-instructions.md",
|
|
);
|
|
|
|
expect(
|
|
agentsEntry === undefined
|
|
? []
|
|
: platformOwners(agentsEntry.owners).map((owner) => owner.platform).sort(),
|
|
).toEqual(["codex", "opencode"].sort());
|
|
expect(
|
|
claudeEntry === undefined
|
|
? []
|
|
: platformOwners(claudeEntry.owners).map((owner) => owner.platform).sort(),
|
|
).toEqual(["claude-code"]);
|
|
expect(
|
|
copilotEntry === undefined
|
|
? []
|
|
: platformOwners(copilotEntry.owners).map((owner) => owner.platform).sort(),
|
|
).toEqual([
|
|
"github-copilot",
|
|
]);
|
|
expect(agentsEntry?.recognizedContents.length).toBeGreaterThanOrEqual(1);
|
|
expect(copilotEntry?.recognizedContents.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it("keeps portal-specific host surfaces when enabled and removes them only for removed hosts", () => {
|
|
const enabledConfig = createDefaultConfig();
|
|
enabledConfig.platforms = [...allPlatforms];
|
|
enabledConfig.truthmark.generated.portal.enabled = true;
|
|
|
|
const allEnabled = renderGeneratedSurfaces(enabledConfig);
|
|
const allEnabledPaths = allEnabled.map((surface) => surface.path);
|
|
expect(
|
|
allEnabledPaths.includes(".opencode/skills/truthmark-portal/SKILL.md"),
|
|
).toBe(true);
|
|
expect(
|
|
allEnabledPaths.includes(".github/prompts/truthmark-portal.prompt.md"),
|
|
).toBe(true);
|
|
|
|
const oneHostRemoved = createDefaultConfig();
|
|
oneHostRemoved.platforms = [...allPlatforms.filter((platform) => platform !== "github-copilot")];
|
|
oneHostRemoved.truthmark.generated.portal.enabled = true;
|
|
const oneHostPaths = renderGeneratedSurfaces(oneHostRemoved).map(
|
|
(surface) => surface.path,
|
|
);
|
|
|
|
expect(
|
|
oneHostPaths.includes(".github/skills/truthmark-portal/SKILL.md"),
|
|
).toBe(false);
|
|
expect(
|
|
oneHostPaths.includes(".github/prompts/truthmark-portal.prompt.md"),
|
|
).toBe(false);
|
|
|
|
const portalDisabled = createDefaultConfig();
|
|
portalDisabled.platforms = ["codex"];
|
|
portalDisabled.truthmark.generated.portal.enabled = false;
|
|
const disabledPaths = renderGeneratedSurfaces(portalDisabled).map(
|
|
(surface) => surface.path,
|
|
);
|
|
|
|
expect(disabledPaths.includes("AGENTS.md")).toBe(true);
|
|
expect(disabledPaths.includes(".agents/skills/truthmark-portal/SKILL.md")).toBe(
|
|
false,
|
|
);
|
|
expect(disabledPaths.includes(".agents/skills/truthmark-portal/SKILL.md")).toBe(
|
|
false,
|
|
);
|
|
});
|
|
});
|