ktg-plugin-marketplace/plugins/linkedin-studio/agents/__tests__/fact-reviewer-fixture.test.mjs
Kjell Tore Guttormsen e69ea1f4c9 feat(linkedin-studio): v3.1.0 — Endring 9 adversarial review-pakke + per-artefakt personas
Cold, adversarial review package for the long-form pipeline + configurable
per-edition personas. Motivated by Del 4 (Security Champions pivot): the
in-session editor + persona sweep shared the drafting session's framing-bias,
so the shipped version was never independently re-reviewed.

Headless package (9a/9b):
- New Step 6.5 (headless-review) in /linkedin:newsletter, after the persona
  sweep, before lock — the independence layer the in-session gates can't be.
- New standalone /linkedin:headless-review command (run in a fresh session for
  maximum isolation; reconstructs frozen draft + contract + personas from disk).
- 3 new Opus archetypes, each with a cardinal context-isolation block that
  refuses drafting-session framing as "context pollution":
  - content-reviewer (argument integrity C1–C5, ≤8 flags)
  - language-reviewer (Norwegian language L1–L5, ≤10 flags)
  - fact-reviewer (cold re-verification F1–F4, risk-sort + pivot-risk, WebSearch)
- Deliberate redundancy with fact-checker / editorial-reviewer documented so
  the pairs are never de-duplicated.

Pivot-reopen (9c):
- New /linkedin:pivot command: logs articles.NN.pivots[], resets currentPhase,
  un-locks, marks gates to re-run.
- Pivot-detection gate in Step 8 lock precondition (>20% word-count change or
  >2 new sections re-opens cleared gates). Del 4 v8→v11 worked example.

Per-artifact personas (new requirement):
- articles.NN.personas with resolution order (edition-state → series file →
  plugin library → interactive). One or more readers configurable per edition.

Schema/docs:
- edition-state.template.json: additive personas[], pivots[], headlessReview,
  headless-review phase (16 phases); personaSweep.resonance.wordCount baseline.
- 3 fasit fixtures + 3 structural lint tests (Del 4 worked cases).
- Counts: 24→26 commands, 16→19 agents, 15→16 newsletter phases.
- README + CLAUDE.md (plugin + root) + CHANGELOG synced.

Verification: 35 agent-fixture + 59 hook + 20 render tests green. Backward-
compatible (additive state); reload required before the 3 new agents resolve.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 13:01:24 +02:00

92 lines
2.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
// Lint-test for the fact-reviewer fasit fixture.
// Mirrors the structure-only discipline of fact-checker-fixture.test.mjs and
// editorial-reviewer-fixture.test.mjs: this test asserts the SHAPE of the
// fixture — the single axis (faktisk-korrekthet), the four checks (F1F4), the
// 🔴/🟡/🟢 risk sort, the six Del 4 (Security Champions) cases, the
// direction-not-copy boundary, and the cold-reader / pivot-risk rationale.
// Whether the agent's live verdicts actually reproduce the fasit is
// [GATE]/[OPERATØR], never self-certified here.
const FIXTURE_PATH = fileURLToPath(
new URL('../fixtures/fact-reviewer-cases.md', import.meta.url)
);
const fixture = readFileSync(FIXTURE_PATH, 'utf8');
// The four checks: F1 verifiable claims · F2 quote precision · F3 number
// attribution · F4 source quality.
const CHECKS = ['F1', 'F2', 'F3', 'F4'];
// The 🔴/🟡/🟢 risk sort (the emoji are the safest assertion).
const VERDICTS = ['🔴', '🟡', '🟢'];
describe('fact-reviewer fixture structure', () => {
test('names the axis "faktisk-korrekthet"', () => {
assert.ok(
/faktisk-korrekthet/i.test(fixture),
'fixture must name the axis "faktisk-korrekthet"'
);
});
test('documents all four checks (F1F4)', () => {
for (const check of CHECKS) {
assert.ok(
fixture.includes(check),
`fixture must reference the check "${check}"`
);
}
});
test('references the 🔴/🟡/🟢 risk sort', () => {
for (const v of VERDICTS) {
assert.ok(
fixture.includes(v),
`fixture must reference the risk verdict "${v}"`
);
}
});
test('references the PASS/REWORK/BLOCK gate', () => {
for (const gate of ['PASS', 'REWORK', 'BLOCK']) {
assert.ok(
fixture.includes(gate),
`fixture must reference the gate decision "${gate}"`
);
}
});
test('documents exactly 6 Del 4 cases', () => {
const cases = fixture.match(/^###\s+Case\s+\d+\b/gim) || [];
assert.equal(
cases.length,
6,
`fixture must document exactly 6 Del 4 cases (found ${cases.length})`
);
});
test('states the direction-not-copy boundary', () => {
assert.ok(
/direction, not rewritten copy/i.test(fixture),
'fixture must state the direction-not-copy boundary'
);
});
test('documents the cold-reader / context-pollution principle', () => {
assert.ok(
/context pollution/i.test(fixture) && /framing-bias/i.test(fixture),
'fixture must document the cold-reader / context-pollution / framing-bias principle'
);
});
test('records the pivot-premise-risk rationale', () => {
assert.ok(
/pivot/i.test(fixture),
'fixture must record why the gate exists (pivot premise never met Step 5)'
);
});
});