2026-06-30 20:57:55 +10:00
|
|
|
import { describe, it } from "node:test";
|
|
|
|
|
import { expect } from "expect";
|
2026-05-15 08:58:31 +10:00
|
|
|
|
|
|
|
|
import { parseTruthDocUpdateDraft } from "../../src/generation/validate.js";
|
2026-06-16 11:03:04 +10:00
|
|
|
import type { ContentPromptContext } from "../../src/generation/types.js";
|
2026-05-15 08:58:31 +10:00
|
|
|
|
2026-06-16 11:03:04 +10:00
|
|
|
const promptContext = {
|
2026-05-15 08:58:31 +10:00
|
|
|
task: "truth-sync",
|
|
|
|
|
changedFiles: ["src/auth/session.ts"],
|
|
|
|
|
owningAreas: ["Authentication"],
|
2026-06-13 04:09:45 +10:00
|
|
|
relevantDocs: ["docs/truthmark/truth/authentication/session-timeout.md"],
|
2026-05-15 08:58:31 +10:00
|
|
|
evidenceSnippets: [
|
|
|
|
|
{
|
|
|
|
|
id: "E1",
|
|
|
|
|
path: "src/auth/session.ts",
|
|
|
|
|
startLine: 10,
|
|
|
|
|
endLine: 18,
|
|
|
|
|
reason: "changed timeout behavior",
|
|
|
|
|
text: "export const SESSION_TIMEOUT_MS = 900000;",
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
openQuestions: [],
|
2026-06-16 11:03:04 +10:00
|
|
|
} satisfies ContentPromptContext;
|
2026-05-15 08:58:31 +10:00
|
|
|
|
|
|
|
|
const validDraft = {
|
|
|
|
|
status: "drafted",
|
2026-06-13 04:09:45 +10:00
|
|
|
targetDocs: ["docs/truthmark/truth/authentication/session-timeout.md"],
|
2026-05-15 08:58:31 +10:00
|
|
|
claims: [
|
|
|
|
|
{
|
|
|
|
|
text: "Session timeout is 15 minutes.",
|
|
|
|
|
evidenceIds: ["E1"],
|
|
|
|
|
support: "supported",
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
patches: [
|
|
|
|
|
{
|
2026-06-13 04:09:45 +10:00
|
|
|
path: "docs/truthmark/truth/authentication/session-timeout.md",
|
2026-05-15 08:58:31 +10:00
|
|
|
section: "Current Behavior",
|
|
|
|
|
operation: "append",
|
|
|
|
|
markdown: "- Sessions expire after 15 minutes of inactivity.",
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
openQuestions: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
describe("parseTruthDocUpdateDraft", () => {
|
|
|
|
|
it("accepts a valid evidence-backed draft", () => {
|
2026-06-30 20:57:55 +10:00
|
|
|
const draft = parseTruthDocUpdateDraft(
|
|
|
|
|
JSON.stringify(validDraft),
|
|
|
|
|
promptContext,
|
|
|
|
|
);
|
2026-05-15 08:58:31 +10:00
|
|
|
|
|
|
|
|
expect(draft.status).toBe("drafted");
|
|
|
|
|
expect(draft.claims[0]?.evidenceIds).toEqual(["E1"]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("accepts blocked output with open questions and no doc changes", () => {
|
|
|
|
|
const draft = parseTruthDocUpdateDraft(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
status: "blocked",
|
|
|
|
|
targetDocs: [],
|
|
|
|
|
claims: [],
|
|
|
|
|
patches: [],
|
|
|
|
|
openQuestions: ["Which bounded doc owns this behavior?"],
|
|
|
|
|
}),
|
2026-06-16 11:03:04 +10:00
|
|
|
promptContext,
|
2026-05-15 08:58:31 +10:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(draft.status).toBe("blocked");
|
2026-06-30 20:57:55 +10:00
|
|
|
expect(draft.openQuestions).toEqual([
|
|
|
|
|
"Which bounded doc owns this behavior?",
|
|
|
|
|
]);
|
2026-05-15 08:58:31 +10:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects non-json output", () => {
|
2026-06-30 20:57:55 +10:00
|
|
|
expect(() => parseTruthDocUpdateDraft("not json", promptContext)).toThrow(
|
|
|
|
|
"Invalid JSON",
|
|
|
|
|
);
|
2026-05-15 08:58:31 +10:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects claims without evidence IDs", () => {
|
|
|
|
|
expect(() =>
|
|
|
|
|
parseTruthDocUpdateDraft(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
...validDraft,
|
|
|
|
|
claims: [{ ...validDraft.claims[0], evidenceIds: [] }],
|
|
|
|
|
}),
|
2026-06-16 11:03:04 +10:00
|
|
|
promptContext,
|
2026-05-15 08:58:31 +10:00
|
|
|
),
|
|
|
|
|
).toThrow("truth-doc-update-draft validation failed");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects unknown evidence IDs", () => {
|
|
|
|
|
expect(() =>
|
|
|
|
|
parseTruthDocUpdateDraft(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
...validDraft,
|
|
|
|
|
claims: [{ ...validDraft.claims[0], evidenceIds: ["E2"] }],
|
|
|
|
|
}),
|
2026-06-16 11:03:04 +10:00
|
|
|
promptContext,
|
2026-05-15 08:58:31 +10:00
|
|
|
),
|
|
|
|
|
).toThrow("unknown evidence id");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects patch paths outside relevant docs", () => {
|
|
|
|
|
expect(() =>
|
|
|
|
|
parseTruthDocUpdateDraft(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
...validDraft,
|
2026-06-13 04:09:45 +10:00
|
|
|
targetDocs: ["docs/truthmark/truth/other.md"],
|
2026-06-30 20:57:55 +10:00
|
|
|
patches: [
|
|
|
|
|
{ ...validDraft.patches[0], path: "docs/truthmark/truth/other.md" },
|
|
|
|
|
],
|
2026-05-15 08:58:31 +10:00
|
|
|
}),
|
2026-06-16 11:03:04 +10:00
|
|
|
promptContext,
|
2026-05-15 08:58:31 +10:00
|
|
|
),
|
|
|
|
|
).toThrow("patch path is not in relevant docs");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects path traversal", () => {
|
|
|
|
|
expect(() =>
|
|
|
|
|
parseTruthDocUpdateDraft(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
...validDraft,
|
|
|
|
|
targetDocs: ["../outside.md"],
|
|
|
|
|
patches: [{ ...validDraft.patches[0], path: "../outside.md" }],
|
|
|
|
|
}),
|
2026-06-16 11:03:04 +10:00
|
|
|
promptContext,
|
2026-05-15 08:58:31 +10:00
|
|
|
),
|
|
|
|
|
).toThrow("unsafe doc path");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects targetDocs that do not match patch paths", () => {
|
|
|
|
|
expect(() =>
|
|
|
|
|
parseTruthDocUpdateDraft(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
...validDraft,
|
2026-06-30 20:57:55 +10:00
|
|
|
targetDocs: [
|
|
|
|
|
"docs/truthmark/truth/authentication/session-timeout.md",
|
|
|
|
|
],
|
2026-05-15 08:58:31 +10:00
|
|
|
patches: [],
|
|
|
|
|
}),
|
2026-06-16 11:03:04 +10:00
|
|
|
promptContext,
|
2026-05-15 08:58:31 +10:00
|
|
|
),
|
|
|
|
|
).toThrow("targetDocs must match patch paths");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects drafted outputs without claims", () => {
|
|
|
|
|
expect(() =>
|
|
|
|
|
parseTruthDocUpdateDraft(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
...validDraft,
|
|
|
|
|
claims: [],
|
|
|
|
|
}),
|
2026-06-16 11:03:04 +10:00
|
|
|
promptContext,
|
2026-05-15 08:58:31 +10:00
|
|
|
),
|
|
|
|
|
).toThrow("drafted output requires at least one claim");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects drafted outputs without patches", () => {
|
|
|
|
|
expect(() =>
|
|
|
|
|
parseTruthDocUpdateDraft(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
...validDraft,
|
|
|
|
|
targetDocs: [],
|
|
|
|
|
patches: [],
|
|
|
|
|
}),
|
2026-06-16 11:03:04 +10:00
|
|
|
promptContext,
|
2026-05-15 08:58:31 +10:00
|
|
|
),
|
|
|
|
|
).toThrow("drafted output requires at least one patch");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects drafted outputs with unsupported claims", () => {
|
|
|
|
|
expect(() =>
|
|
|
|
|
parseTruthDocUpdateDraft(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
...validDraft,
|
|
|
|
|
claims: [{ ...validDraft.claims[0], support: "unsupported" }],
|
|
|
|
|
}),
|
2026-06-16 11:03:04 +10:00
|
|
|
promptContext,
|
2026-05-15 08:58:31 +10:00
|
|
|
),
|
|
|
|
|
).toThrow("drafted output cannot contain unsupported claims");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects blocked outputs with claims or patches", () => {
|
|
|
|
|
expect(() =>
|
|
|
|
|
parseTruthDocUpdateDraft(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
...validDraft,
|
|
|
|
|
status: "blocked",
|
|
|
|
|
openQuestions: ["Need more evidence"],
|
|
|
|
|
}),
|
2026-06-16 11:03:04 +10:00
|
|
|
promptContext,
|
2026-05-15 08:58:31 +10:00
|
|
|
),
|
|
|
|
|
).toThrow("blocked output cannot include claims, target docs, or patches");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("rejects blocked outputs without open questions", () => {
|
|
|
|
|
expect(() =>
|
|
|
|
|
parseTruthDocUpdateDraft(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
status: "blocked",
|
|
|
|
|
targetDocs: [],
|
|
|
|
|
claims: [],
|
|
|
|
|
patches: [],
|
|
|
|
|
openQuestions: [],
|
|
|
|
|
}),
|
2026-06-16 11:03:04 +10:00
|
|
|
promptContext,
|
2026-05-15 08:58:31 +10:00
|
|
|
),
|
|
|
|
|
).toThrow("blocked output requires at least one open question");
|
|
|
|
|
});
|
2026-06-30 20:57:55 +10:00
|
|
|
});
|