notepilot / scripts /test_subset_acceptance.mjs
jasonmdong's picture
Sync from GitHub via hub-sync
626e298 verified
Raw History Blame Contribute Delete
4.57 kB
#!/usr/bin/env node
// The authoritative acceptance rule: E subset-of D, singles and chords alike.
// The rule itself is pure, so it is executed here (not pattern-matched); the
// wiring around it is asserted against the served source.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import vm from 'node:vm';
const app = fs.readFileSync(new URL('../static/app.js', import.meta.url), 'utf8');
// ---- run the real helper -------------------------------------------------
const src = app.slice(app.indexOf('function expectedSubsetOfDetected'),
app.indexOf('function detectedPitchesFromSalience'));
const ctx = vm.createContext({});
vm.runInContext(src + '\nthis.f = expectedSubsetOfDetected;', ctx);
const sub = ctx.f;
const ACCEPT = true, REJECT = false;
const cases = [
// singles
['single exact', [67], [67], ACCEPT],
['single + one extra', [67], [48, 67], ACCEPT],
['single + many extras', [67], [48, 60, 64, 67, 72], ACCEPT],
['single wrong neighbour', [67], [66], REJECT],
['single absent', [67], [48, 60, 64], REJECT],
// chords
['chord exact', [67, 71], [67, 71], ACCEPT],
['chord + extras', [67, 71], [48, 52, 55, 67, 71], ACCEPT],
['chord missing one', [67, 71], [48, 52, 55, 67], REJECT],
['chord one wrong', [67, 71], [67, 72], REJECT],
// larger chord
['triad + extras', [48, 52, 55], [48, 52, 55, 60, 67], ACCEPT],
['triad missing one', [48, 52, 55], [48, 52], REJECT],
// the spec's numeric examples
['spec {4,6} in {1..5}', [4, 6], [1, 2, 3, 4, 5], REJECT],
['spec {4,5} in {1..5}', [4, 5], [1, 2, 3, 4, 5], ACCEPT],
// extra-hand notes
['extra hand, single', [67], [48, 52, 55, 67], ACCEPT],
['extra hand, partial chord',[67, 71], [48, 52, 55, 67], REJECT],
];
let pass = 0;
for (const [name, E, D, want] of cases) {
const got = sub(E, D);
assert.equal(got, want, `${name}: expected ${want}, got ${got} E=${JSON.stringify(E)} D=${JSON.stringify(D)}`);
pass++;
}
// degenerate input must never accept
assert.equal(sub([], [60]), false, 'empty expected must not accept');
assert.equal(sub([60], []), false, 'empty detected must not accept');
// ---- wiring assertions ---------------------------------------------------
const frame = app.slice(app.indexOf('function handleFollowerFrame'), app.indexOf('function traceFollowerFrame'));
// compare against the real CALL, not the comment that mentions it
assert.ok(frame.indexOf('tryGestureSubsetAdvance()') < frame.indexOf('if (out.advance) deliverFollowerAdvance('),
'the subset rule must run BEFORE the follower proposal (no follower veto)');
const accept = app.slice(app.indexOf('function tryGestureSubsetAdvance'), app.indexOf('function logGesture'));
assert.match(accept, /maxJump: 0/, 'exactly one event, never a jump');
assert.match(accept, /attack\.serial !== g\.attackSerial/, 'gesture must be bound to its own token');
assert.match(accept, /attack\.serial > _hostConsumedAttackSerial/, 'token must be unconsumed');
assert.match(accept, /g\.consumed = true/, 'gesture consumed exactly once');
assert.match(accept, /_hostConsumedAttackSerial = attack\.serial/);
assert.match(accept, /seek\(state\.tracker\.position/, 'follower re-anchored after direct acceptance');
// singles and chords must NOT diverge in this path
assert.doesNotMatch(accept, /length === 1|length > 1|isChord/, 'singles and chords share one rule');
// D must not be built from the score
const build = app.slice(app.indexOf('function detectedPitchesFromSalience'), app.indexOf('function beginGesture'));
for (const forbidden of ['expectedPitches', 'tracker.score', '_minFreq', 'leadPitchFromEvent']) {
assert.ok(!build.includes(forbidden), `D construction must not consult ${forbidden}`);
}
// observability
const log = app.slice(app.indexOf('function logGesture'), app.indexOf('function logGesture') + 1600);
for (const field of ['gestureId','detectedPitches','expectedPitches','subsetSatisfied',
'missingExpectedPitches','extraDetectedPitches','accepted',
'trackerBefore','trackerAfter','followerPositionBefore','followerPositionAfter']) {
assert.ok(log.includes(field), `gesture record must carry ${field}`);
}
console.log(`PASS: subset acceptance — ${pass} matrix cases, singles+chords unified, follower cannot veto, D is score-independent`);