Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD across contents and paths, upstream package/repo coordinates -> @golem15/msd-core and golem15com/msd-core. Deep links into upstream history, sibling upstream packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is. Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line, package/plugin identity, regenerated lockfile, install-tree fixtures, derived registries and benchmark baseline; migration checksum baseline re-locked (MSD keeps its own install state, so no install had applied the old sums); sort-order and regex-escaped expectations in tests adjusted.
125 lines
4.9 KiB
JavaScript
125 lines
4.9 KiB
JavaScript
/**
|
||
* OpenCode review reconstruction — property tests (#1936).
|
||
*
|
||
* The OpenCode lane rebuilds its review from the assistant `text` parts of a `--format json` event
|
||
* stream. `--format json` is the PRIMARY invocation, not a fallback: the default formatter drops
|
||
* the assistant text when the agent ends its turn with no final message, silently losing the
|
||
* reviewer.
|
||
*
|
||
* This suite used to extract two embedded `jq` programs from `review.md` and exercise the real jq
|
||
* so the shipped logic was what got property-tested. Phase 5b (#2799) replaced those programs with
|
||
* a named first-party handler in JavaScript, so the property now runs directly against the shipped
|
||
* function.
|
||
*
|
||
* That deletes the entire #2099 problem this file was architected around. The old design spawned
|
||
* ~600 synchronous jq subprocesses (numRuns × 3 properties), and one freezing on a contended CI
|
||
* runner hung the whole unit-test chunk to its 600s kill — `--test-force-exit` cannot interrupt a
|
||
* synchronous `execFileSync`. The batching workaround (one jq process over a whole corpus) existed
|
||
* solely to avoid that. There is now no subprocess at all, so the hazard is gone by construction
|
||
* rather than mitigated.
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
const { describe, test } = require('node:test');
|
||
const fc = require('fast-check');
|
||
|
||
const { handleOpencodeOutput } = require('../msd-core/bin/lib/review-lane-runner.cjs');
|
||
|
||
/** Deterministic: pinned seed, bounded runs, replay data printed on failure. */
|
||
const FC = { seed: 42, numRuns: 200 };
|
||
|
||
const NL = String.fromCharCode(10);
|
||
|
||
/** An assistant text event, as opencode emits it. */
|
||
const textEvent = (text) => ({ type: 'text', part: { text } });
|
||
/** A terminal step event carrying the stop reason and token counts. */
|
||
const stepFinish = (reason, output) => ({ type: 'step_finish', part: { reason, tokens: { output } } });
|
||
|
||
/** Serialize events the way opencode does: one compact JSON value per line. */
|
||
const streamOf = (events) => events.map((e) => JSON.stringify(e)).join(NL);
|
||
|
||
/** Text that survives a JSON round-trip, including the shapes that broke the jq version. */
|
||
const hostileText = fc.oneof(
|
||
fc.string(),
|
||
fc.constantFrom('', ' ', ' ', NL, 'a' + NL + 'b', '"quoted"', '\\backslash', 'emoji 🎉', 'null', '-n'),
|
||
fc.string({ unit: 'grapheme' }),
|
||
);
|
||
|
||
describe('opencode reconstruction — properties', () => {
|
||
test('every assistant text part appears, in order, joined by newlines', () => {
|
||
fc.assert(
|
||
fc.property(fc.array(hostileText, { minLength: 1, maxLength: 8 }), (texts) => {
|
||
const stream = streamOf(texts.map(textEvent));
|
||
return handleOpencodeOutput(stream).review === texts.join(NL);
|
||
}),
|
||
FC,
|
||
);
|
||
});
|
||
|
||
test('non-text events never contribute to the review', () => {
|
||
fc.assert(
|
||
fc.property(
|
||
fc.array(hostileText, { minLength: 1, maxLength: 5 }),
|
||
fc.array(fc.record({ reason: fc.string(), output: fc.nat() }), { maxLength: 4 }),
|
||
(texts, steps) => {
|
||
const events = [...texts.map(textEvent), ...steps.map((s) => stepFinish(s.reason, s.output))];
|
||
return handleOpencodeOutput(streamOf(events)).review === texts.join(NL);
|
||
},
|
||
),
|
||
FC,
|
||
);
|
||
});
|
||
|
||
test('a malformed line is skipped without losing the surrounding review', () => {
|
||
// Losing an entire review to one unparseable line would be strictly worse than the bug this
|
||
// handler exists to fix, so a partial stream must still yield its usable text.
|
||
fc.assert(
|
||
fc.property(
|
||
fc.array(hostileText, { minLength: 1, maxLength: 4 }),
|
||
fc.constantFrom('NOT JSON', '{"truncated":', '}{', '[', 'null', ' '),
|
||
(texts, garbage) => {
|
||
const lines = [...texts.map((t) => JSON.stringify(textEvent(t))), garbage];
|
||
return handleOpencodeOutput(lines.join(NL)).review === texts.join(NL);
|
||
},
|
||
),
|
||
FC,
|
||
);
|
||
});
|
||
|
||
test('is total — never throws on arbitrary input', () => {
|
||
fc.assert(
|
||
fc.property(fc.anything(), (anything) => {
|
||
try {
|
||
const r = handleOpencodeOutput(anything);
|
||
return typeof r.review === 'string' && typeof r.diagnostic === 'string';
|
||
} catch {
|
||
return false;
|
||
}
|
||
}),
|
||
FC,
|
||
);
|
||
});
|
||
|
||
test('CRLF streams reconstruct identically to LF', () => {
|
||
fc.assert(
|
||
fc.property(fc.array(fc.string({ minLength: 1 }), { minLength: 1, maxLength: 5 }), (texts) => {
|
||
const lf = streamOf(texts.map(textEvent));
|
||
const crlf = lf.split(NL).join(String.fromCharCode(13) + NL);
|
||
return handleOpencodeOutput(lf).review === handleOpencodeOutput(crlf).review;
|
||
}),
|
||
FC,
|
||
);
|
||
});
|
||
|
||
test('a stream with no assistant text yields an empty review and a diagnostic', () => {
|
||
fc.assert(
|
||
fc.property(fc.string({ minLength: 1 }), fc.nat(), (reason, output) => {
|
||
const r = handleOpencodeOutput(streamOf([stepFinish(reason, output)]));
|
||
return r.review === '' && r.diagnostic.includes(String(output));
|
||
}),
|
||
FC,
|
||
);
|
||
});
|
||
});
|