Files
open-design/apps/daemon/tests/critique-conformance.test.ts
AmyandGitHub a338a13aab test(ci): harden Functional E2E and daemon release gates (#6708)
* test(e2e): harden coverage and runtime isolation

* test(e2e): gate prerelease on full functional suite

* test(daemon): enforce prerelease coverage

* test(daemon): stabilize full-suite fixtures

Generated-By: looper 0.11.8 (runner=fixer, agent=codex)

* test(e2e): preserve interaction regression coverage

Keep deck navigation subject to Playwright actionability checks and restore the current rail updater/composer stacking witness as an explicit expected failure.

Generated-By: looper 0.11.8 (runner=fixer, agent=codex)

* fix(ci): close conformance validation gaps

Generated-By: looper 0.11.8 (runner=fixer, agent=codex)

* test(e2e): restore MCP snippet style guard

Restore the browser-computed style witness that was over-deleted when the OAuth flow moved to Integrations, and navigate through the current Settings MCP server entry.\n\nGenerated-By: looper 0.11.8 (runner=fixer, agent=codex)

* fix(ci): close directive and job-boundary gaps

Compare normalized MUST_FIX directives as sets so identical multi-item rounds remain weak debates, and bound workflow topology assertions to their immediate sibling jobs.\n\nGenerated-By: looper 0.11.8 (runner=fixer, agent=codex)
2026-08-11 08:31:14 +00:00

561 lines
27 KiB
TypeScript

/**
* End-to-end coverage for the adapter conformance harness
* (Phase 10, Task 10.1).
*
* Drives the same `parseCritiqueStream` the production orchestrator
* uses, but with the synthetic adapter fixtures so the assertion is
* about the harness's classification logic (shipped / degraded /
* failed) rather than the parser's correctness (already covered by
* the v1 parser tests).
*/
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { PARSER_WARNING_KINDS } from '@open-design/contracts/critique';
import { runAdapterConformance } from '../src/critique/conformance.js';
import {
syntheticGoodStream,
} from '../src/critique/__fixtures__/adapters/synthetic-good.js';
import {
syntheticBadStream,
} from '../src/critique/__fixtures__/adapters/synthetic-bad.js';
import {
__resetDegradedRegistryForTests,
__setDegradedClockForTests,
isDegraded,
} from '../src/critique/adapter-degraded.js';
let now = 1_000_000;
beforeEach(() => {
now = 1_000_000;
__setDegradedClockForTests({ now: () => now });
});
afterEach(() => {
__setDegradedClockForTests(null);
__resetDegradedRegistryForTests();
});
describe('adapter conformance harness (Phase 10)', () => {
it('synthetic-good emits shipped and leaves the adapter undegraded', async () => {
const outcome = await runAdapterConformance({
adapterId: 'synthetic-good',
runId: 'run-good-1',
source: syntheticGoodStream(),
});
expect(outcome.kind).toBe('shipped');
if (outcome.kind !== 'shipped') return;
expect(outcome.round).toBeGreaterThan(0);
expect(outcome.composite).toBeGreaterThan(0);
// The harness must NOT mark the adapter degraded on success.
expect(isDegraded('synthetic-good')).toBe(false);
// Every panel event for the run should land in the events array
// for downstream inspection.
expect(outcome.events.length).toBeGreaterThan(0);
expect(outcome.events.find((e) => e.type === 'ship')).toBeTruthy();
// The shipped outcome must surface the artifact bytes the parser
// handed back via onArtifact, so a prerelease cycle can pin MIME /
// byte-length / hash without re-parsing the transcript (lefarcen
// P2 on PR #1317).
expect(outcome.artifact).not.toBeNull();
expect(outcome.artifact?.mime).toMatch(/^text\/(html|markdown)/);
expect(outcome.artifact?.body.length).toBeGreaterThan(0);
});
it('synthetic-bad emits degraded with the parser-derived reason and marks the adapter', async () => {
const outcome = await runAdapterConformance({
adapterId: 'synthetic-bad',
runId: 'run-bad-1',
source: syntheticBadStream(),
});
expect(outcome.kind).toBe('degraded');
if (outcome.kind !== 'degraded') return;
expect(['malformed_block', 'oversize_block', 'missing_artifact']).toContain(
outcome.reason,
);
expect(isDegraded('synthetic-bad')).toBe(true);
});
it('marks the adapter degraded for the default 24h TTL after a bad run', async () => {
await runAdapterConformance({
adapterId: 'synthetic-bad-2',
runId: 'run-bad-2',
source: syntheticBadStream(),
});
expect(isDegraded('synthetic-bad-2')).toBe(true);
// Advance the clock just shy of 24h, still degraded.
now += 24 * 60 * 60 * 1000 - 1;
expect(isDegraded('synthetic-bad-2')).toBe(true);
// Cross the boundary, mark falls off.
now += 2;
expect(isDegraded('synthetic-bad-2')).toBe(false);
});
it('classifies a stream that finishes without a ship event as failed (no_ship)', async () => {
async function* truncated(): AsyncIterable<string> {
// Open the critique-run envelope, emit a single panelist tag, then
// close cleanly. The parser yields no SHIP, so the harness must
// surface `failed: no_ship` rather than spinning forever or
// returning `shipped`.
yield '<CRITIQUE_RUN version="1" runId="run-x" projectId="p" artifactId="a">\n';
yield '</CRITIQUE_RUN>\n';
}
const outcome = await runAdapterConformance({
adapterId: 'synthetic-truncated',
runId: 'run-x',
source: truncated(),
});
expect(outcome.kind).toBe('failed');
if (outcome.kind !== 'failed') return;
expect(outcome.cause).toBe('no_ship');
});
it('threads the projectId / artifactId / runId through to the parser SHIP event', async () => {
const outcome = await runAdapterConformance({
adapterId: 'synthetic-good',
runId: 'custom-run-id',
source: syntheticGoodStream(),
projectId: 'proj-conformance',
artifactId: 'artifact-conformance',
});
if (outcome.kind !== 'shipped') {
throw new Error('expected shipped outcome');
}
const ship = outcome.events.find((e) => e.type === 'ship');
expect(ship?.type).toBe('ship');
if (ship?.type !== 'ship') return;
expect(ship.artifactRef.projectId).toBe('proj-conformance');
expect(ship.artifactRef.artifactId).toBe('artifact-conformance');
});
it('classifies an oversize block as degraded oversize_block (lefarcen P2)', async () => {
// The synthetic-good transcript is fine under the default 256 KB
// block budget. Replay it through the harness with a tiny budget so
// the parser throws OversizeBlockError on the first ARTIFACT body
// and the harness has to surface `degraded: oversize_block`.
const outcome = await runAdapterConformance({
adapterId: 'synthetic-oversize',
runId: 'run-oversize',
source: syntheticGoodStream(),
parserMaxBlockBytes: 256,
});
expect(outcome.kind).toBe('degraded');
if (outcome.kind !== 'degraded') return;
expect(outcome.reason).toBe('oversize_block');
expect(isDegraded('synthetic-oversize')).toBe(true);
});
it('classifies an adapter that throws mid-stream as failed unexpected_error (lefarcen P2)', async () => {
class AdapterBoom extends Error {
constructor() {
super('adapter blew up');
this.name = 'AdapterBoom';
}
}
async function* throwing(): AsyncIterable<string> {
yield '<CRITIQUE_RUN version="1" runId="run-boom" projectId="p" artifactId="a">\n';
yield '<ROUND n="1">\n';
throw new AdapterBoom();
}
const outcome = await runAdapterConformance({
adapterId: 'synthetic-throwing',
runId: 'run-boom',
source: throwing(),
});
expect(outcome.kind).toBe('failed');
if (outcome.kind !== 'failed') return;
expect(outcome.cause).toBe('unexpected_error');
expect(outcome.error).toContain('adapter blew up');
// The adapter is NOT marked degraded here: an unexpected throw is a
// failure to evaluate, not evidence of a malformed stream. A real
// policy could choose to mark it after N consecutive throws; the
// harness leaves that decision to the caller.
expect(isDegraded('synthetic-throwing')).toBe(false);
});
it('classifies a clean SHIP that arrived alongside parser warnings as degraded parser_warning (lefarcen P2)', async () => {
// A panelist score outside [0, scale] makes the parser yield a
// `parser_warning` with kind=`score_clamped` BEFORE the panelist
// closes. The harness must promote the run to degraded even though
// a syntactically valid SHIP arrives later.
async function* withClampedScore(): AsyncIterable<string> {
yield '<CRITIQUE_RUN version="1" maxRounds="1" threshold="0.1" scale="10">\n';
yield ' <ROUND n="1">\n';
// designer must include an ARTIFACT in round 1 (parser invariant).
yield ' <PANELIST role="designer">\n';
yield ' <ARTIFACT mime="text/html"><![CDATA[<p>x</p>]]></ARTIFACT>\n';
yield ' </PANELIST>\n';
// Out-of-range score on `critic` triggers score_clamped warning.
yield ' <PANELIST role="critic" score="99"><DIM name="x" score="6">n</DIM></PANELIST>\n';
yield ' <PANELIST role="brand" score="6"><DIM name="x" score="6">n</DIM></PANELIST>\n';
yield ' <PANELIST role="a11y" score="6"><DIM name="x" score="6">n</DIM></PANELIST>\n';
yield ' <PANELIST role="copy" score="6"><DIM name="x" score="6">n</DIM></PANELIST>\n';
yield ' <ROUND_END n="1" composite="6.0" must_fix="0" decision="ship">\n';
yield ' <REASON>ok</REASON>\n';
yield ' </ROUND_END>\n';
yield ' </ROUND>\n';
yield ' <SHIP round="1" composite="6.0" status="shipped">\n';
yield ' <ARTIFACT mime="text/html"><![CDATA[<p>final</p>]]></ARTIFACT>\n';
yield ' <SUMMARY>ok</SUMMARY>\n';
yield ' </SHIP>\n';
yield '</CRITIQUE_RUN>\n';
}
const outcome = await runAdapterConformance({
adapterId: 'synthetic-warned',
runId: 'run-warned',
source: withClampedScore(),
});
expect(outcome.kind).toBe('degraded');
if (outcome.kind !== 'degraded') return;
expect(outcome.reason).toBe('parser_warning');
expect(outcome.events.some((e) => e.type === 'parser_warning')).toBe(true);
expect(isDegraded('synthetic-warned')).toBe(true);
});
it('classifies a SHIP that arrived before every panelist closed as degraded incomplete_panel (codex P2)', async () => {
// run_started declares the full 5-role cast, but only `designer`
// and `critic` ever emit panelist_close. The parser does not reject
// this on its own; the harness is the gate that catches it.
async function* incomplete(): AsyncIterable<string> {
yield '<CRITIQUE_RUN version="1" maxRounds="1" threshold="0.1" scale="10">\n';
yield ' <ROUND n="1">\n';
yield ' <PANELIST role="designer">\n';
yield ' <ARTIFACT mime="text/html"><![CDATA[<p>x</p>]]></ARTIFACT>\n';
yield ' </PANELIST>\n';
yield ' <PANELIST role="critic" score="6"><DIM name="x" score="6">n</DIM></PANELIST>\n';
yield ' <ROUND_END n="1" composite="6.0" must_fix="0" decision="ship">\n';
yield ' <REASON>ok</REASON>\n';
yield ' </ROUND_END>\n';
yield ' </ROUND>\n';
yield ' <SHIP round="1" composite="6.0" status="shipped">\n';
yield ' <ARTIFACT mime="text/html"><![CDATA[<p>final</p>]]></ARTIFACT>\n';
yield ' <SUMMARY>ok</SUMMARY>\n';
yield ' </SHIP>\n';
yield '</CRITIQUE_RUN>\n';
}
const outcome = await runAdapterConformance({
adapterId: 'synthetic-incomplete',
runId: 'run-incomplete',
source: incomplete(),
});
expect(outcome.kind).toBe('degraded');
if (outcome.kind !== 'degraded') return;
expect(outcome.reason).toBe('incomplete_panel');
expect(isDegraded('synthetic-incomplete')).toBe(true);
});
it('classifies a duplicate-SHIP stream as degraded parser_warning even though ship arrives first (lefarcen P2 follow-up)', async () => {
// Two `<SHIP>` blocks in the same transcript. The parser emits a
// SHIP event for the first and a `parser_warning` of kind
// `duplicate_ship` for the second; the warning arrives AFTER the
// ship. The harness must drain the rest of the stream and
// classify as degraded rather than returning on the first ship.
async function* duplicateShip(): AsyncIterable<string> {
yield '<CRITIQUE_RUN version="1" maxRounds="1" threshold="0.1" scale="10">\n';
yield ' <ROUND n="1">\n';
yield ' <PANELIST role="designer">\n';
yield ' <ARTIFACT mime="text/html"><![CDATA[<p>x</p>]]></ARTIFACT>\n';
yield ' </PANELIST>\n';
yield ' <PANELIST role="critic" score="6"><DIM name="x" score="6">n</DIM></PANELIST>\n';
yield ' <PANELIST role="brand" score="6"><DIM name="x" score="6">n</DIM></PANELIST>\n';
yield ' <PANELIST role="a11y" score="6"><DIM name="x" score="6">n</DIM></PANELIST>\n';
yield ' <PANELIST role="copy" score="6"><DIM name="x" score="6">n</DIM></PANELIST>\n';
yield ' <ROUND_END n="1" composite="6.0" must_fix="0" decision="ship">\n';
yield ' <REASON>ok</REASON>\n';
yield ' </ROUND_END>\n';
yield ' </ROUND>\n';
yield ' <SHIP round="1" composite="6.0" status="shipped">\n';
yield ' <ARTIFACT mime="text/html"><![CDATA[<p>first</p>]]></ARTIFACT>\n';
yield ' <SUMMARY>first</SUMMARY>\n';
yield ' </SHIP>\n';
// Second SHIP block triggers the parser_warning (duplicate_ship).
yield ' <SHIP round="1" composite="6.0" status="shipped">\n';
yield ' <ARTIFACT mime="text/html"><![CDATA[<p>second</p>]]></ARTIFACT>\n';
yield ' <SUMMARY>second</SUMMARY>\n';
yield ' </SHIP>\n';
yield '</CRITIQUE_RUN>\n';
}
const outcome = await runAdapterConformance({
adapterId: 'synthetic-duplicate-ship',
runId: 'run-dup',
source: duplicateShip(),
});
expect(outcome.kind).toBe('degraded');
if (outcome.kind !== 'degraded') return;
expect(outcome.reason).toBe('parser_warning');
// The events array must hold both the first ship AND the
// duplicate_ship warning so a debugger can see what happened.
expect(outcome.events.filter((e) => e.type === 'ship')).toHaveLength(1);
expect(
outcome.events.some(
(e) => e.type === 'parser_warning' && e.kind === 'duplicate_ship',
),
).toBe(true);
expect(isDegraded('synthetic-duplicate-ship')).toBe(true);
});
it('classifies a SHIP whose round did not close every cast role as incomplete_panel even if earlier rounds closed everyone (lefarcen P2 follow-up)', async () => {
// Round 1 closes all five cast roles cleanly. Round 2 closes only
// designer + critic before <SHIP round="2"> arrives. A cumulative
// (non-per-round) tracker would happily say "all five closed
// somewhere, ship is fine"; the corrected per-round tracker
// looks only at the shipping round's panelist_close set and
// flags incomplete_panel because brand / a11y / copy never
// closed in round 2.
async function* incompleteShippingRound(): AsyncIterable<string> {
yield '<CRITIQUE_RUN version="1" maxRounds="2" threshold="0.1" scale="10">\n';
// Round 1 — all five close.
yield ' <ROUND n="1">\n';
yield ' <PANELIST role="designer">\n';
yield ' <ARTIFACT mime="text/html"><![CDATA[<p>v1</p>]]></ARTIFACT>\n';
yield ' </PANELIST>\n';
yield ' <PANELIST role="critic" score="6"><DIM name="x" score="6">n</DIM><MUST_FIX>Fix hierarchy.</MUST_FIX></PANELIST>\n';
yield ' <PANELIST role="brand" score="6"><DIM name="x" score="6">n</DIM><MUST_FIX>Use brand color.</MUST_FIX></PANELIST>\n';
yield ' <PANELIST role="a11y" score="6"><DIM name="x" score="6">n</DIM></PANELIST>\n';
yield ' <PANELIST role="copy" score="6"><DIM name="x" score="6">n</DIM></PANELIST>\n';
yield ' <ROUND_END n="1" composite="6.0" must_fix="2" decision="continue">\n';
yield ' <REASON>more work</REASON>\n';
yield ' </ROUND_END>\n';
yield ' </ROUND>\n';
// Round 2 — only designer + critic close (the cumulative bug
// would let this slide; the fix catches it).
yield ' <ROUND n="2">\n';
yield ' <PANELIST role="designer">\n';
yield ' <NOTES>iterating</NOTES>\n';
yield ' </PANELIST>\n';
yield ' <PANELIST role="critic" score="7"><DIM name="x" score="7">n</DIM></PANELIST>\n';
yield ' <ROUND_END n="2" composite="7.0" must_fix="0" decision="ship">\n';
yield ' <REASON>ok</REASON>\n';
yield ' </ROUND_END>\n';
yield ' </ROUND>\n';
yield ' <SHIP round="2" composite="7.0" status="shipped">\n';
yield ' <ARTIFACT mime="text/html"><![CDATA[<p>final</p>]]></ARTIFACT>\n';
yield ' <SUMMARY>ok</SUMMARY>\n';
yield ' </SHIP>\n';
yield '</CRITIQUE_RUN>\n';
}
const outcome = await runAdapterConformance({
adapterId: 'synthetic-incomplete-round-2',
runId: 'run-r2',
source: incompleteShippingRound(),
});
expect(outcome.kind).toBe('degraded');
if (outcome.kind !== 'degraded') return;
expect(outcome.reason).toBe('incomplete_panel');
expect(isDegraded('synthetic-incomplete-round-2')).toBe(true);
});
it('classifies a parser_warning followed by EOF without SHIP as degraded parser_warning, not failed no_ship (PerishCode P3 on PR #1317)', async () => {
// The bug the priority-order fix in conformance.ts addresses: a
// stream that emits a `parser_warning` (out-of-range score) and
// then dies before a `SHIP` arrives (adapter crash, network
// drop, run-out-of-rounds) used to fall through to
// `failed:no_ship` because the `parserWarningSeen` check sat
// inside the post-no_ship branch. Rule 3 in the conformance
// docstring says parser_warning wins over no_ship; this test
// pins the docstring's "top-to-bottom priority" promise for the
// no-ship path so a future refactor cannot silently flip it.
async function* warnedThenEof(): AsyncIterable<string> {
// Well-formed stream that emits a score_clamped warning and
// ends with a `continue` decision on the last allowed round,
// so no SHIP block arrives but the parser does not flag
// malformed_block either. This is the exact shape the priority
// fix in conformance.ts is built to catch: rule 3 (warning) must
// win over rule 6 (no_ship).
yield '<CRITIQUE_RUN version="1" maxRounds="1" threshold="0.1" scale="10">\n';
yield ' <ROUND n="1">\n';
yield ' <PANELIST role="designer">\n';
yield ' <ARTIFACT mime="text/html"><![CDATA[<p>x</p>]]></ARTIFACT>\n';
yield ' </PANELIST>\n';
// Out-of-range score triggers score_clamped warning.
yield ' <PANELIST role="critic" score="99"><DIM name="x" score="6">n</DIM></PANELIST>\n';
yield ' <PANELIST role="brand" score="6"><DIM name="x" score="6">n</DIM></PANELIST>\n';
yield ' <PANELIST role="a11y" score="6"><DIM name="x" score="6">n</DIM></PANELIST>\n';
yield ' <PANELIST role="copy" score="6"><DIM name="x" score="6">n</DIM></PANELIST>\n';
// decision="continue" with no SHIP block on a maxRounds=1 run.
yield ' <ROUND_END n="1" composite="6.0" must_fix="1" decision="continue">\n';
yield ' <REASON>more work needed but ran out of rounds</REASON>\n';
yield ' </ROUND_END>\n';
yield ' </ROUND>\n';
yield '</CRITIQUE_RUN>\n';
}
const outcome = await runAdapterConformance({
adapterId: 'synthetic-warned-then-died',
runId: 'run-warned-eof',
source: warnedThenEof(),
});
// Rule 3 (parser_warning) wins over rule 6 (no_ship); the adapter
// is marked degraded for 24h, not silently dropped as failed.
expect(outcome.kind).toBe('degraded');
if (outcome.kind !== 'degraded') return;
expect(outcome.reason).toBe('parser_warning');
expect(outcome.events.some((e) => e.type === 'parser_warning')).toBe(true);
expect(isDegraded('synthetic-warned-then-died')).toBe(true);
});
// PerishCode P3 follow-up on PR #1317: the score_clamped case above
// exercises one of the five ParserWarningKind values. Rule 3 fires on
// ANY parser_warning kind, so this matrix drives the conformance gate
// off PARSER_WARNING_KINDS directly. Adding a sixth kind to the
// contracts export auto-grows the matrix without a harness-test edit.
// Kinds reachable in a single-fixture generator are covered here;
// multi-round and cross-panelist cases use dedicated fixtures below.
describe('parser_warning matrix across PARSER_WARNING_KINDS (PerishCode P3 on PR #1317)', () => {
it('all kinds documented match the contracts enum', () => {
// Bare guard: if PARSER_WARNING_KINDS changes shape without the
// matrix being updated, this test points at the missing fixtures.
expect([...PARSER_WARNING_KINDS]).toEqual([
'weak_debate',
'unknown_role',
'score_clamped',
'composite_mismatch',
'duplicate_ship',
]);
});
it('classifies score_clamped as degraded parser_warning', async () => {
async function* fixture(): AsyncIterable<string> {
yield '<CRITIQUE_RUN version="1" maxRounds="1" threshold="0.1" scale="10">\n';
yield ' <ROUND n="1">\n';
yield ' <PANELIST role="designer">\n';
yield ' <ARTIFACT mime="text/html"><![CDATA[<p>x</p>]]></ARTIFACT>\n';
yield ' </PANELIST>\n';
yield ' <PANELIST role="critic" score="99"><DIM name="x" score="6">n</DIM></PANELIST>\n';
yield ' <PANELIST role="brand" score="6"><DIM name="x" score="6">n</DIM></PANELIST>\n';
yield ' <PANELIST role="a11y" score="6"><DIM name="x" score="6">n</DIM></PANELIST>\n';
yield ' <PANELIST role="copy" score="6"><DIM name="x" score="6">n</DIM></PANELIST>\n';
yield ' <ROUND_END n="1" composite="6.0" must_fix="0" decision="ship">\n';
yield ' <REASON>ok</REASON>\n';
yield ' </ROUND_END>\n';
yield ' </ROUND>\n';
yield ' <SHIP round="1" composite="6.0" status="shipped">\n';
yield ' <ARTIFACT mime="text/html"><![CDATA[<p>final</p>]]></ARTIFACT>\n';
yield ' <SUMMARY>ok</SUMMARY>\n';
yield ' </SHIP>\n';
yield '</CRITIQUE_RUN>\n';
}
const outcome = await runAdapterConformance({
adapterId: 'synthetic-warned-score-clamped',
runId: 'run-warned-score-clamped',
source: fixture(),
});
expect(outcome.kind).toBe('degraded');
if (outcome.kind !== 'degraded') return;
expect(outcome.reason).toBe('parser_warning');
const warnings = outcome.events.filter((e) => e.type === 'parser_warning');
expect(warnings.length).toBeGreaterThan(0);
expect(warnings.some((w) => w.type === 'parser_warning' && w.kind === 'score_clamped')).toBe(true);
});
it('classifies weak_debate as degraded parser_warning', async () => {
async function* fixture(): AsyncIterable<string> {
yield `<CRITIQUE_RUN version="1" maxRounds="2" threshold="8" scale="10">
<ROUND n="1">
<PANELIST role="designer"><ARTIFACT mime="text/html"><![CDATA[<p>v1</p>]]></ARTIFACT></PANELIST>
<PANELIST role="critic" score="6"><MUST_FIX>Fix contrast.</MUST_FIX></PANELIST>
<PANELIST role="brand" score="6"><MUST_FIX>Fix contrast.</MUST_FIX></PANELIST>
<PANELIST role="a11y" score="6"></PANELIST>
<PANELIST role="copy" score="6"></PANELIST>
<ROUND_END n="1" composite="6" must_fix="2" decision="continue"><REASON>iterate</REASON></ROUND_END>
</ROUND>
<ROUND n="2">
<PANELIST role="designer" score="9"><NOTES>done</NOTES></PANELIST>
<PANELIST role="critic" score="9"></PANELIST>
<PANELIST role="brand" score="9"></PANELIST>
<PANELIST role="a11y" score="9"></PANELIST>
<PANELIST role="copy" score="9"></PANELIST>
<ROUND_END n="2" composite="9" must_fix="0" decision="ship"><REASON>ship</REASON></ROUND_END>
</ROUND>
<SHIP round="2" composite="9" status="shipped"><ARTIFACT mime="text/html"><![CDATA[<p>final</p>]]></ARTIFACT><SUMMARY>done</SUMMARY></SHIP>
</CRITIQUE_RUN>`;
}
const outcome = await runAdapterConformance({
adapterId: 'synthetic-weak-debate',
runId: 'run-weak-debate',
source: fixture(),
});
expect(outcome.kind).toBe('degraded');
expect(outcome.events.some(
(event) => event.type === 'parser_warning' && event.kind === 'weak_debate',
)).toBe(true);
});
it('classifies unknown_role as degraded parser_warning', async () => {
async function* fixture(): AsyncIterable<string> {
yield `<CRITIQUE_RUN version="1" maxRounds="1" threshold="8" scale="10">
<ROUND n="1">
<PANELIST role="designer"><ARTIFACT mime="text/html"><![CDATA[<p>v1</p>]]></ARTIFACT></PANELIST>
<PANELIST role="phantom" score="9"><DIM name="x" score="9">ignored</DIM></PANELIST>
<PANELIST role="critic" score="9"></PANELIST>
<PANELIST role="brand" score="9"></PANELIST>
<PANELIST role="a11y" score="9"></PANELIST>
<PANELIST role="copy" score="9"></PANELIST>
<ROUND_END n="1" composite="9" must_fix="0" decision="ship"><REASON>ship</REASON></ROUND_END>
</ROUND>
<SHIP round="1" composite="9" status="shipped"><ARTIFACT mime="text/html"><![CDATA[<p>final</p>]]></ARTIFACT><SUMMARY>done</SUMMARY></SHIP>
</CRITIQUE_RUN>`;
}
const outcome = await runAdapterConformance({
adapterId: 'synthetic-unknown-role',
runId: 'run-unknown-role',
source: fixture(),
});
expect(outcome.kind).toBe('degraded');
expect(outcome.events.some(
(event) => event.type === 'parser_warning' && event.kind === 'unknown_role',
)).toBe(true);
});
it('classifies composite_mismatch as degraded parser_warning', async () => {
async function* fixture(): AsyncIterable<string> {
yield `<CRITIQUE_RUN version="1" maxRounds="1" threshold="8" scale="10">
<ROUND n="1">
<PANELIST role="designer"><ARTIFACT mime="text/html"><![CDATA[<p>v1</p>]]></ARTIFACT></PANELIST>
<PANELIST role="critic" score="9"></PANELIST>
<PANELIST role="brand" score="9"></PANELIST>
<PANELIST role="a11y" score="9"></PANELIST>
<PANELIST role="copy" score="9"></PANELIST>
<ROUND_END n="1" composite="2" must_fix="0" decision="ship"><REASON>ship</REASON></ROUND_END>
</ROUND>
<SHIP round="1" composite="2" status="shipped"><ARTIFACT mime="text/html"><![CDATA[<p>final</p>]]></ARTIFACT><SUMMARY>done</SUMMARY></SHIP>
</CRITIQUE_RUN>`;
}
const outcome = await runAdapterConformance({
adapterId: 'synthetic-composite-mismatch',
runId: 'run-composite-mismatch',
source: fixture(),
});
expect(outcome.kind).toBe('degraded');
expect(outcome.events.some(
(event) => event.type === 'parser_warning' && event.kind === 'composite_mismatch',
)).toBe(true);
});
it('classifies a SHIP composite mismatch after a correct ROUND_END as degraded', async () => {
async function* fixture(): AsyncIterable<string> {
yield `<CRITIQUE_RUN version="1" maxRounds="1" threshold="8" scale="10">
<ROUND n="1">
<PANELIST role="designer"><ARTIFACT mime="text/html"><![CDATA[<p>v1</p>]]></ARTIFACT></PANELIST>
<PANELIST role="critic" score="9"></PANELIST>
<PANELIST role="brand" score="9"></PANELIST>
<PANELIST role="a11y" score="9"></PANELIST>
<PANELIST role="copy" score="9"></PANELIST>
<ROUND_END n="1" composite="9" must_fix="0" decision="ship"><REASON>ship</REASON></ROUND_END>
</ROUND>
<SHIP round="1" composite="2" status="shipped"><ARTIFACT mime="text/html"><![CDATA[<p>final</p>]]></ARTIFACT><SUMMARY>done</SUMMARY></SHIP>
</CRITIQUE_RUN>`;
}
const outcome = await runAdapterConformance({
adapterId: 'synthetic-ship-composite-mismatch',
runId: 'run-ship-composite-mismatch',
source: fixture(),
});
expect(outcome.kind).toBe('degraded');
expect(outcome.events.some(
(event) => event.type === 'parser_warning' && event.kind === 'composite_mismatch',
)).toBe(true);
});
// duplicate_ship is covered by the dedicated post-SHIP conformance case
// above, where the harness must continue draining after the first SHIP.
});
});