* test(#3674): characterize existing wave-dispatch output and add tests for the extracted partitioner Pins resolveWaveDispatch's and emitWorkflowScript's current, unextracted output (chain-overlap, disjoint-empty-set, and a multi-wave/multi-stage golden script) as a regression safety net ahead of extracting partitionStages into a standalone module. Also adds the new module's unit and property tests (test matrix rows 1-11) against its expected public API, which does not exist yet and is added in the next commit. * feat(#3674): extract file-overlap partitioner into a shared, generic module Moves partitionStages' greedy first-fit file-overlap algorithm into a new, dependency-free src/file-overlap-partitioner.cts module (partitionByFileOverlap), generalized over a plain {id, files}[] shape rather than claude-orchestration.cts's Plan/Wave interfaces. partitionStages becomes a thin adapter mapping its own Plan[] shape onto the generic input and back — behavior-preserving, no dependency ordering, no path normalization, no filesystem access moved or added. Enables a future consumer (quick-batch, #3675 / ADR-1239) to reuse the same primitive without pulling in orchestration internals. * docs(#3674): register the file-overlap-partitioner module bookkeeping New src/*.cts -> bin/lib/*.cjs modules need four hand-maintained registrations beyond the code itself: .gitignore (compiled artifact), eslint.config.mjs (ADR-457: lint the .cts, not the emitted .cjs), docs/INVENTORY.md's CLI Modules roster row (regenerated via gen-inventory-manifest.cjs --write), and a CONTEXT.md glossary entry matching the convention set by similarly-scoped leaf modules (text-lines.cts, plan-dependency-graph.cts, spec-section.cts). * fix(#3674): alphabetize INVENTORY.md row, manifest regen no-op, fast-check import already correct - docs/INVENTORY.md: move file-overlap-partitioner.cjs row to alphabetical position - docs/INVENTORY-MANIFEST.json: regenerated via gen-inventory-manifest.cjs --write, produced no diff (manifest is keyed by content, not row order) - tests/claude-orchestration.test.cjs's direct require('fast-check') is correct as-is: tests/helpers/fast-check-setup.cjs's own docstring scopes the shared-seed wrapper to "every *.property.test.cjs file"; claude-orchestration.test.cjs is not a .property.test.cjs file, and every .property.test.cjs file sampled uses the wrapper consistently. No outlier. * fix(#3674): constrain the no-overlap property test to unique ids, fixing an ambiguous duplicate-id reconstruction The `no two plans in the same stage share a modified file` property reconstructs which physical item produced each output id via `remaining.findIndex(r => r.id === id)`. Under duplicate ids (an explicitly-supported input shape for `partitionByFileOverlap`) that reconstruction can pick the wrong physical occurrence, producing a false-positive overlap failure (observed counterexample: p0(f1), p208(f1), p208([]) — correctly staged as [[p0,p208#2],[p208#1]], but misread by id-order as [[p0,p208#1],...], which do overlap). Properties (a) determinism and (b) totality already exercise duplicate ids correctly and are left unchanged; only this property's generated items are now constrained to unique ids via `fc.uniqueArray`, where the reconstruction is unambiguous. --------- Co-authored-by: sim <sim@local>
104 lines
4.7 KiB
JavaScript
104 lines
4.7 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Property-based tests for file-overlap-partitioner.cjs (#3674)
|
|
*
|
|
* Module: gsd-core/bin/lib/file-overlap-partitioner.cjs
|
|
* Exported: partitionByFileOverlap(items: { id: string; files: string[] }[]) -> string[][]
|
|
*
|
|
* Properties tested (test matrix rows 9-11):
|
|
* (a) determinism — two runs on the same input produce an identical partition
|
|
* (b) totality — every input item appears in exactly one output stage,
|
|
* never lost or duplicated
|
|
* (c) the core invariant — no two items in the same stage share a file
|
|
*
|
|
* Duplicate `id`s are a real, intentionally-supported input shape (see
|
|
* `partitionByFileOverlap`'s doc comment) and properties (a) and (b) verify
|
|
* it correctly — (a) needs no per-item identity at all, and (b) only compares
|
|
* the sorted id multiset, so it never needs to pick out *which* physical item
|
|
* a given output id refers to. Property (c) is different: checking "do these
|
|
* two co-staged items' file sets overlap" requires reconstructing which
|
|
* physical item (files included) produced each id in the output, and under
|
|
* duplicate ids that reconstruction is ambiguous — a stage's "p208" could be
|
|
* either physical p208 occurrence, and picking the wrong one produces a false
|
|
* failure (see #3674 regression: `p0(f1)`, `p208(f1)`, `p208([])` staged
|
|
* correctly as `[[p0,p208#2],[p208#1]]`, misread as `[[p0,p208#1],...]` by an
|
|
* id-order reconstruction). So (c) alone constrains its generated items to
|
|
* unique ids, where the reconstruction is unambiguous by construction.
|
|
*/
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fc = require('./helpers/fast-check-setup.cjs');
|
|
|
|
const { partitionByFileOverlap } = require('../gsd-core/bin/lib/file-overlap-partitioner.cjs');
|
|
|
|
/** A single overlap item: a small id plus a small set of file tokens (some shared, some not). */
|
|
const itemArb = fc.record({
|
|
id: fc.integer({ min: 0, max: 999 }).map((n) => 'p' + n),
|
|
files: fc.array(fc.constantFrom('f0', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7'), { maxLength: 4 }),
|
|
});
|
|
|
|
describe('partitionByFileOverlap: properties', () => {
|
|
|
|
test('property: repeated runs on the same input are deterministic', () => {
|
|
fc.assert(fc.property(
|
|
fc.array(itemArb, { minLength: 0, maxLength: 60 }),
|
|
(items) => {
|
|
const a = partitionByFileOverlap(items);
|
|
const b = partitionByFileOverlap(items);
|
|
assert.deepStrictEqual(a, b);
|
|
},
|
|
));
|
|
});
|
|
|
|
test('property: every input plan appears in exactly one output stage', () => {
|
|
fc.assert(fc.property(
|
|
fc.array(itemArb, { minLength: 0, maxLength: 60 }),
|
|
(items) => {
|
|
const stages = partitionByFileOverlap(items);
|
|
const flat = stages.flatMap((s) => s);
|
|
assert.strictEqual(flat.length, items.length, 'no item lost or duplicated across stages');
|
|
// Positional identity: nth occurrence in flattened output must match
|
|
// input order's ids, one-to-one (id alone is not unique under
|
|
// duplicates, so compare the full multiset via sorted copies).
|
|
assert.deepStrictEqual(flat.slice().sort(), items.map((i) => i.id).sort());
|
|
},
|
|
));
|
|
});
|
|
|
|
test('property: no two plans in the same stage share a modified file', () => {
|
|
fc.assert(fc.property(
|
|
// Unique ids only: this property reconstructs which physical item
|
|
// produced each output id, and that reconstruction is ambiguous when
|
|
// ids duplicate (see the module doc comment above). Properties (a)
|
|
// and (b) still fuzz duplicate ids via the shared `itemArb`.
|
|
fc.uniqueArray(itemArb, { minLength: 0, maxLength: 60, selector: (it) => it.id }),
|
|
(items) => {
|
|
const stages = partitionByFileOverlap(items);
|
|
// Positional lookup (not strictly required once ids are unique, but
|
|
// kept for symmetry with the reconstruction shape and to tolerate
|
|
// any future relaxation of the uniqueness constraint above).
|
|
const remaining = items.map((i) => ({ id: i.id, files: new Set(i.files) }));
|
|
for (const stageIds of stages) {
|
|
const stageItems = stageIds.map((id) => {
|
|
const idx = remaining.findIndex((r) => r.id === id);
|
|
const found = remaining[idx];
|
|
remaining.splice(idx, 1);
|
|
return found;
|
|
});
|
|
for (let i = 0; i < stageItems.length; i++) {
|
|
for (let j = i + 1; j < stageItems.length; j++) {
|
|
const a = stageItems[i].files;
|
|
const b = stageItems[j].files;
|
|
let overlap = false;
|
|
for (const f of a) if (b.has(f)) { overlap = true; break; }
|
|
assert.ok(!overlap, `stage-mates ${stageItems[i].id} and ${stageItems[j].id} must not share a file`);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
));
|
|
});
|
|
});
|