diff --git a/plugins/voyage/tests/validators/brief-validator-annotation-fields.test.mjs b/plugins/voyage/tests/validators/brief-validator-annotation-fields.test.mjs new file mode 100644 index 0000000..33fee13 --- /dev/null +++ b/plugins/voyage/tests/validators/brief-validator-annotation-fields.test.mjs @@ -0,0 +1,87 @@ +// tests/validators/brief-validator-annotation-fields.test.mjs +// Pin forward-compat for v4.2 annotation frontmatter fields on brief.md. +// Adding revision/source_annotations/annotation_digest/revision_reason must NOT +// trigger BRIEF_UNKNOWN_FIELD or similar — validator is purely additive-tolerant +// per source_findings precedent. No code change required; this test pins the policy. + +import { test } from 'node:test'; +import { strict as assert } from 'node:assert'; +import { validateBriefContent } from '../../lib/validators/brief-validator.mjs'; + +const BASE_BRIEF = `--- +type: trekbrief +brief_version: "2.0" +created: 2026-05-09 +task: "Annotated revision for testing forward-compat" +slug: ann-fwd-compat +project_dir: .claude/projects/2026-05-09-ann-fwd-compat/ +research_topics: 0 +research_status: skipped +auto_research: false +interview_turns: 1 +source: interview +--- + +# Task: Annotated revision + +## Intent + +Why. + +## Goal + +What. + +## Success Criteria + +- Done. +`; + +test('brief-validator forward-compat — baseline (no annotation fields) still valid', () => { + const r = validateBriefContent(BASE_BRIEF, { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('brief-validator forward-compat — accepts revision: 0', () => { + const t = BASE_BRIEF.replace('---\ninterview_turns: 1', '---\ninterview_turns: 1\nrevision: 0'); + const r = validateBriefContent(t, { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('brief-validator forward-compat — accepts revision: 5', () => { + const t = BASE_BRIEF.replace('source: interview', 'source: interview\nrevision: 5'); + const r = validateBriefContent(t, { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('brief-validator forward-compat — accepts source_annotations list-of-dict', () => { + const inject = `\nrevision: 1\nsource_annotations:\n - id: ANN-0001\n target_artifact: brief.md\n target_anchor: intent\n intent: change\n comment: "tighten the intent paragraph"\n timestamp: "2026-05-09T10:00:00Z"`; + const t = BASE_BRIEF.replace('source: interview', 'source: interview' + inject); + const r = validateBriefContent(t, { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('brief-validator forward-compat — accepts annotation_digest string', () => { + const t = BASE_BRIEF.replace('source: interview', 'source: interview\nrevision: 1\nannotation_digest: abc123def4567890'); + const r = validateBriefContent(t, { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('brief-validator forward-compat — accepts revision_reason for non-additive revision', () => { + const t = BASE_BRIEF.replace('source: interview', 'source: interview\nrevision: 2\nrevision_reason: "restructured Goals section"'); + const r = validateBriefContent(t, { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('brief-validator forward-compat — all 4 fields together still valid', () => { + const inject = `\nrevision: 3\nrevision_reason: "applied 5 annotations"\nannotation_digest: 0123456789abcdef\nsource_annotations:\n - id: ANN-0001\n target_artifact: brief.md\n target_anchor: goal\n intent: change`; + const t = BASE_BRIEF.replace('source: interview', 'source: interview' + inject); + const r = validateBriefContent(t, { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('brief-validator forward-compat — unrecognized future field still tolerated (forward-compat policy)', () => { + const t = BASE_BRIEF.replace('source: interview', 'source: interview\nfuture_v4_3_field: "anything"'); + const r = validateBriefContent(t, { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); diff --git a/plugins/voyage/tests/validators/plan-validator-annotation-fields.test.mjs b/plugins/voyage/tests/validators/plan-validator-annotation-fields.test.mjs new file mode 100644 index 0000000..6a74f5d --- /dev/null +++ b/plugins/voyage/tests/validators/plan-validator-annotation-fields.test.mjs @@ -0,0 +1,79 @@ +// tests/validators/plan-validator-annotation-fields.test.mjs +// Pin forward-compat for v4.2 annotation frontmatter fields on plan.md. +// Adding revision/source_annotations/annotation_digest/revision_reason must NOT +// trigger PLAN_UNKNOWN_FIELD or similar — validator is purely additive-tolerant +// per source_findings precedent. No code change required; this test pins the policy. + +import { test } from 'node:test'; +import { strict as assert } from 'node:assert'; +import { validatePlanContent } from '../../lib/validators/plan-validator.mjs'; + +const STEP_BLOCK = `### Step 1: Do thing + +- Files: a.ts +- Manifest: + \`\`\`yaml + manifest: + expected_paths: + - a.ts + min_file_count: 1 + commit_message_pattern: "^feat:" + bash_syntax_check: [] + forbidden_paths: [] + must_contain: [] + \`\`\` +`; + +const baseFm = (extra = '') => `--- +plan_version: "1.7" +profile: balanced${extra} +--- + +# Plan + +## Implementation Plan + +${STEP_BLOCK} +`; + +test('plan-validator forward-compat — baseline (no annotation fields) still valid', () => { + const r = validatePlanContent(baseFm(), { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('plan-validator forward-compat — accepts revision: 0', () => { + const r = validatePlanContent(baseFm('\nrevision: 0'), { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('plan-validator forward-compat — accepts revision: 5', () => { + const r = validatePlanContent(baseFm('\nrevision: 5'), { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('plan-validator forward-compat — accepts source_annotations list-of-dict', () => { + const inject = `\nrevision: 1\nsource_annotations:\n - id: ANN-0001\n target_artifact: plan.md\n target_anchor: step-3\n intent: change\n comment: "reorder ahead of step 4"\n timestamp: "2026-05-09T10:00:00Z"`; + const r = validatePlanContent(baseFm(inject), { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('plan-validator forward-compat — accepts annotation_digest string', () => { + const r = validatePlanContent(baseFm('\nrevision: 1\nannotation_digest: 0123456789abcdef'), { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('plan-validator forward-compat — accepts revision_reason', () => { + const r = validatePlanContent(baseFm('\nrevision: 2\nrevision_reason: "structural step reorder"'), { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('plan-validator forward-compat — all 4 fields together with source_findings', () => { + const inject = `\nrevision: 3\nrevision_reason: "applied 5 annotations"\nannotation_digest: abc1234567890def\nsource_annotations:\n - id: ANN-0001\n target_artifact: plan.md\n target_anchor: step-3\n intent: change\nsource_findings:\n - 0123456789abcdef0123456789abcdef01234567`; + const r = validatePlanContent(baseFm(inject), { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('plan-validator forward-compat — unrecognized future field tolerated', () => { + const r = validatePlanContent(baseFm('\nfuture_v4_3_key: "any"'), { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); diff --git a/plugins/voyage/tests/validators/review-validator-annotation-fields.test.mjs b/plugins/voyage/tests/validators/review-validator-annotation-fields.test.mjs new file mode 100644 index 0000000..d791509 --- /dev/null +++ b/plugins/voyage/tests/validators/review-validator-annotation-fields.test.mjs @@ -0,0 +1,89 @@ +// tests/validators/review-validator-annotation-fields.test.mjs +// Pin forward-compat for v4.2 annotation frontmatter fields on review.md. +// Adding revision/source_annotations/annotation_digest/revision_reason must NOT +// trigger REVIEW_UNKNOWN_FIELD or similar — validator is purely additive-tolerant +// per source_findings precedent. No code change required; this test pins the policy. + +import { test } from 'node:test'; +import { strict as assert } from 'node:assert'; +import { validateReviewContent } from '../../lib/validators/review-validator.mjs'; + +const BASE_REVIEW = `--- +type: trekreview +review_version: "1.0" +created: 2026-05-09 +task: "Annotated revision forward-compat" +slug: ann-fwd-compat +project_dir: .claude/projects/2026-05-09-ann-fwd-compat/ +brief_path: .claude/projects/2026-05-09-ann-fwd-compat/brief.md +scope_sha_start: abc123 +scope_sha_end: def456 +reviewed_files_count: 1 +findings: [] +--- + +# Review + +## Executive Summary + +Verdict: ALLOW. + +## Coverage + +| File | Treatment | Reason | +|------|-----------|--------| +| lib/foo.mjs | deep-review | risk | + +## Remediation Summary + +None. +`; + +test('review-validator forward-compat — baseline (no annotation fields) still valid', () => { + const r = validateReviewContent(BASE_REVIEW, { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('review-validator forward-compat — accepts revision: 0', () => { + const t = BASE_REVIEW.replace('findings: []', 'findings: []\nrevision: 0'); + const r = validateReviewContent(t, { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('review-validator forward-compat — accepts revision: 5', () => { + const t = BASE_REVIEW.replace('findings: []', 'findings: []\nrevision: 5'); + const r = validateReviewContent(t, { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('review-validator forward-compat — accepts source_annotations alongside source-style findings', () => { + const inject = `\nrevision: 1\nsource_annotations:\n - id: ANN-0001\n target_artifact: review.md\n target_anchor: executive-summary\n intent: question\n comment: "wording is ambiguous"\n timestamp: "2026-05-09T10:00:00Z"`; + const t = BASE_REVIEW.replace('findings: []', 'findings: []' + inject); + const r = validateReviewContent(t, { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('review-validator forward-compat — accepts annotation_digest string', () => { + const t = BASE_REVIEW.replace('findings: []', 'findings: []\nrevision: 1\nannotation_digest: 0123456789abcdef'); + const r = validateReviewContent(t, { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('review-validator forward-compat — accepts revision_reason for non-additive revision', () => { + const t = BASE_REVIEW.replace('findings: []', 'findings: []\nrevision: 2\nrevision_reason: "removed coverage section"'); + const r = validateReviewContent(t, { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('review-validator forward-compat — all 4 annotation fields together still valid', () => { + const inject = `\nrevision: 3\nrevision_reason: "applied 2 annotations"\nannotation_digest: 0123456789abcdef\nsource_annotations:\n - id: ANN-0001\n target_artifact: review.md\n target_anchor: coverage\n intent: change`; + const t = BASE_REVIEW.replace('findings: []', 'findings: []' + inject); + const r = validateReviewContent(t, { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +}); + +test('review-validator forward-compat — unrecognized future field tolerated', () => { + const t = BASE_REVIEW.replace('findings: []', 'findings: []\nfuture_v4_3_key: "any"'); + const r = validateReviewContent(t, { strict: true }); + assert.equal(r.valid, true, JSON.stringify(r.errors)); +});