Files
msd-core/tests/file-overlap-partitioner.property.test.cjs
Tom Boucher b0572c0108 feat(#3674): extract shared file-overlap wave partitioner (#4166)
* 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>
2026-09-02 07:33:42 -04:00

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`);
}
}
}
},
));
});
});