test(contracts): Phase 0.a contract snapshots for audio surface

Snapshot the three public surfaces the audio-engine decomposition must not
change (docs/audio-engine-tlc.md Part IV §4): addon export table, audio-bridge
IPC channels, preload audio/audioEffects API keys. contract-check.test.js
diffs regenerated surfaces against the committed snapshots.

result-shapes.json (golden result key/type shapes) is deferred until the
engine_units harness can run the addon against a null device.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
OmikronApex
2026-07-13 23:21:21 +02:00
co-authored by Claude Fable 5
parent 745e360c89
commit bbb3b58db8
5 changed files with 442 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
// Phase 0.a gate (docs/audio-engine-tlc.md §4): the public audio surface —
// addon exports, IPC channels, preload API keys — must not change during the
// decomposition phases. Removals/renames fail here; deliberate additions
// require regenerating the snapshots in the same commit:
// node tests/contracts/extract.js
'use strict';
const test = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const { extractAddonExports, extractIpcChannels, extractPreloadApi } = require('./contracts/extract.js');
function loadSnapshot(name) {
return JSON.parse(fs.readFileSync(path.join(__dirname, 'contracts', name), 'utf8'));
}
test('addon export table matches snapshot', (t) => {
const current = extractAddonExports();
if (!current) {
t.skip('slopsmith_audio.node not built');
return;
}
assert.deepStrictEqual(current, loadSnapshot('addon-exports.json'));
});
test('audio-bridge IPC channels match snapshot', () => {
assert.deepStrictEqual(extractIpcChannels(), loadSnapshot('ipc-channels.json'));
});
test('preload audio/audioEffects API keys match snapshot', () => {
assert.deepStrictEqual(extractPreloadApi(), loadSnapshot('preload-audio-api.json'));
});
+103
View File
@@ -0,0 +1,103 @@
[
"addSource",
"bindInputDevice",
"clearChain",
"clearStreamOutput",
"closePluginEditor",
"detectNotes",
"enableFileLogging",
"getBackingDuration",
"getBackingLevel",
"getBackingPosition",
"getBufferSizes",
"getChainState",
"getCurrentDevice",
"getDeviceMetrics",
"getDeviceTypes",
"getKnownPlugins",
"getLevels",
"getNoteVerdicts",
"getParameters",
"getPitchDetection",
"getRawAudioFrame",
"getRawPitchDetection",
"getRendererBusMetrics",
"getSampleRate",
"getSampleRates",
"getSourceLevels",
"getSourceNoteVerdicts",
"getSourcePitchDetection",
"getSourceRawAudioFrame",
"getSourceRawPitchDetection",
"getStreamOverflowCount",
"getStreamSinkLevel",
"getStreamUnderflowCount",
"init",
"isAudioRunning",
"isBackingPlaying",
"isMlNoteDetection",
"isMonitorMuted",
"isStreamOutputActive",
"listInputDevices",
"listSources",
"loadBackingTrack",
"loadIR",
"loadNAMModel",
"loadNoteModel",
"loadPluginList",
"loadPreset",
"loadVST",
"moveProcessor",
"openPluginEditor",
"probeDeviceOptions",
"pushRendererAudio",
"removeProcessor",
"removeSource",
"replaceIR",
"resetPeaks",
"savePluginList",
"savePreset",
"scanPlugins",
"scoreChord",
"scoreSourceChord",
"seekBacking",
"sendMidiToSlot",
"setBackingSpeed",
"setBranch",
"setBranchSrc",
"setBypass",
"setChart",
"setCrashedPlugins",
"setDevice",
"setDeviceType",
"setGain",
"setInputChannel",
"setInputDeviceType",
"setMonitorKill",
"setMonitorMute",
"setMonitorMuteSuppressed",
"setMultiBypass",
"setNoiseGate",
"setNoteDetectionEnabled",
"setOutputDeviceType",
"setPan",
"setParameter",
"setPostGain",
"setRendererBus",
"setSlotState",
"setSourceChart",
"setSourceInputChannel",
"setSourceMonitorMute",
"setSourceVerifierOffset",
"setStreamBus",
"setStreamBusGain",
"setStreamOutputDevice",
"setTonePolish",
"setVstCrashSentinelPath",
"shutdown",
"startAudio",
"startBacking",
"stopAudio",
"stopBacking",
"unbindInputDevice"
]
+86
View File
@@ -0,0 +1,86 @@
// Contract-surface extraction for the audio engine TLC refactor (Phase 0.a,
// docs/audio-engine-tlc.md §4). Each extractor returns a sorted, stable JSON
// snapshot of one public surface. contract-check.test.js diffs these against
// the committed snapshots so a decomposition phase cannot silently change the
// public API. Regenerate deliberately with: node tests/contracts/extract.js
'use strict';
const fs = require('fs');
const path = require('path');
const repoRoot = path.join(__dirname, '..', '..');
// Export table of slopsmith_audio.node. Loads the real binary; returns null
// when it hasn't been built (contract-check skips with a warning then).
function extractAddonExports() {
const addonPath = path.join(repoRoot, 'build', 'Release', 'slopsmith_audio.node');
if (!fs.existsSync(addonPath)) return null;
const addon = require(addonPath);
return Object.keys(addon).sort();
}
// Every ipcMain.handle / ipcMain.on channel registered in audio-bridge.ts.
function extractIpcChannels() {
const src = fs.readFileSync(path.join(repoRoot, 'src', 'main', 'audio-bridge.ts'), 'utf8');
const channels = new Set();
const re = /ipcMain\.(?:handle|on)\(\s*'([^']+)'/g;
let m;
while ((m = re.exec(src)) !== null) channels.add(m[1]);
return [...channels].sort();
}
// Top-level method keys of the `audio:` and `audioEffects:` object literals in
// preload.ts — the surface the renderer (and every plugin) programs against.
// Brace-depth walk: record keys only at depth 1 inside the target literal.
function extractPreloadKeys(objectName, src) {
const start = src.indexOf(`${objectName}: {`);
if (start === -1) throw new Error(`preload.ts: '${objectName}: {' not found`);
let i = src.indexOf('{', start);
let depth = 0;
let parenDepth = 0; // multi-line parameter lists must not yield keys
const keys = [];
let lineStart = i;
for (; i < src.length; i++) {
const c = src[i];
if (c === '{') depth++;
else if (c === '}') {
depth--;
if (depth === 0) break;
} else if (c === '(') parenDepth++;
else if (c === ')') parenDepth--;
else if (c === '\n') {
lineStart = i + 1;
} else if (depth === 1 && parenDepth === 0) {
// At a key position: line begins (after whitespace) with `name:`
if (i === lineStart) {
const line = src.slice(lineStart, src.indexOf('\n', lineStart));
const km = line.match(/^\s*([A-Za-z_$][\w$]*)\s*:/);
if (km) keys.push(km[1]);
}
}
}
return keys.sort();
}
function extractPreloadApi() {
const src = fs.readFileSync(path.join(repoRoot, 'src', 'main', 'preload.ts'), 'utf8');
return {
audio: extractPreloadKeys('audio', src),
audioEffects: extractPreloadKeys('audioEffects', src),
};
}
function writeSnapshot(name, data) {
fs.writeFileSync(path.join(__dirname, name), JSON.stringify(data, null, 2) + '\n');
}
module.exports = { extractAddonExports, extractIpcChannels, extractPreloadApi };
if (require.main === module) {
const addonExports = extractAddonExports();
if (addonExports) writeSnapshot('addon-exports.json', addonExports);
else console.warn('addon not built — skipping addon-exports.json');
writeSnapshot('ipc-channels.json', extractIpcChannels());
writeSnapshot('preload-audio-api.json', extractPreloadApi());
console.log('contract snapshots written to tests/contracts/');
}
+107
View File
@@ -0,0 +1,107 @@
[
"audio-effects:activateSegment",
"audio-effects:inspectRoute",
"audio-effects:loadChainPlan",
"audio-effects:releaseRoute",
"audio-effects:setRouteGain",
"audio-effects:setStageBypass",
"audio-effects:setStageParameter",
"audio:addSource",
"audio:bindInputDevice",
"audio:clearChain",
"audio:clearStreamOutput",
"audio:closePluginEditor",
"audio:detectNotes",
"audio:getBackingDuration",
"audio:getBackingLevel",
"audio:getBackingPosition",
"audio:getBufferSizes",
"audio:getChainState",
"audio:getCurrentDevice",
"audio:getDeviceMetrics",
"audio:getDeviceTypes",
"audio:getKnownPlugins",
"audio:getLevels",
"audio:getNoteVerdicts",
"audio:getParameters",
"audio:getPitchDetection",
"audio:getRawAudioFrame",
"audio:getRawPitch",
"audio:getRendererBusMetrics",
"audio:getSampleRate",
"audio:getSampleRates",
"audio:getSourceLevels",
"audio:getSourceNoteVerdicts",
"audio:getSourcePitchDetection",
"audio:getSourceRawAudioFrame",
"audio:getSourceRawPitch",
"audio:getStreamOverflowCount",
"audio:getStreamSinkLevel",
"audio:getStreamUnderflowCount",
"audio:isAudioRunning",
"audio:isAvailable",
"audio:isBackingPlaying",
"audio:isMlNoteDetection",
"audio:isMonitorMuted",
"audio:isStreamOutputActive",
"audio:listInputDevices",
"audio:listSources",
"audio:loadBackingTrack",
"audio:loadDeviceSettings",
"audio:loadIR",
"audio:loadNAMModel",
"audio:loadPluginList",
"audio:loadPreset",
"audio:loadVST",
"audio:moveProcessor",
"audio:openPluginEditor",
"audio:probeDeviceOptions",
"audio:pushRendererAudio",
"audio:removeProcessor",
"audio:removeSource",
"audio:replaceIR",
"audio:resetPeaks",
"audio:saveDeviceSettings",
"audio:savePluginList",
"audio:savePreset",
"audio:scanPlugins",
"audio:scoreChord",
"audio:scoreSourceChord",
"audio:seekBacking",
"audio:sendMidiToSlot",
"audio:setBackingSpeed",
"audio:setBranch",
"audio:setBranchSrc",
"audio:setBypass",
"audio:setChart",
"audio:setDevice",
"audio:setDeviceType",
"audio:setGain",
"audio:setInputChannel",
"audio:setMonitorKill",
"audio:setMonitorMute",
"audio:setMonitorMuteSuppressed",
"audio:setMultiBypass",
"audio:setNoiseGate",
"audio:setNoteDetectionEnabled",
"audio:setOutputDeviceType",
"audio:setPan",
"audio:setParameter",
"audio:setPostGain",
"audio:setRendererBus",
"audio:setSlotState",
"audio:setSourceChart",
"audio:setSourceInputChannel",
"audio:setSourceMonitorMute",
"audio:setSourceVerifierOffset",
"audio:setStreamBus",
"audio:setStreamBusGain",
"audio:setStreamOutputDevice",
"audio:setTonePolish",
"audio:startAudio",
"audio:startBacking",
"audio:stopAudio",
"audio:stopBacking",
"audio:unbindInputDevice",
"debug:isEnabled"
]
+112
View File
@@ -0,0 +1,112 @@
{
"audio": [
"addSource",
"bindInputDevice",
"clearChain",
"clearStreamOutput",
"closePluginEditor",
"debugEnabled",
"detectNotes",
"getBackingDuration",
"getBackingLevel",
"getBackingPosition",
"getBufferSizes",
"getChainState",
"getCurrentDevice",
"getDeviceMetrics",
"getDeviceTypes",
"getKnownPlugins",
"getLevels",
"getNoteVerdicts",
"getParameters",
"getPitchDetection",
"getRawAudioFrame",
"getRawPitch",
"getRendererBusMetrics",
"getSampleRate",
"getSampleRates",
"getSourceLevels",
"getSourceNoteVerdicts",
"getSourcePitchDetection",
"getSourceRawAudioFrame",
"getSourceRawPitch",
"getStreamOverflowCount",
"getStreamSinkLevel",
"getStreamUnderflowCount",
"isAudioRunning",
"isAvailable",
"isBackingPlaying",
"isMlNoteDetection",
"isMonitorMuted",
"isStreamOutputActive",
"listInputDevices",
"listSources",
"loadBackingTrack",
"loadDeviceSettings",
"loadIR",
"loadNAMModel",
"loadPluginList",
"loadPreset",
"loadVST",
"moveProcessor",
"openPluginEditor",
"probeDeviceOptions",
"pushRendererAudio",
"removeProcessor",
"removeSource",
"replaceIR",
"resetPeaks",
"saveDeviceSettings",
"savePluginList",
"savePreset",
"scanPlugins",
"scoreChord",
"scoreSourceChord",
"seekBacking",
"sendMidiToSlot",
"setBackingSpeed",
"setBranch",
"setBranchSrc",
"setBypass",
"setChart",
"setDevice",
"setDeviceType",
"setGain",
"setInputChannel",
"setMonitorKill",
"setMonitorMute",
"setMonitorMuteSuppressed",
"setMultiBypass",
"setNoiseGate",
"setNoteDetectionEnabled",
"setOutputDeviceType",
"setPageMuted",
"setPan",
"setParameter",
"setPostGain",
"setRendererBus",
"setSlotState",
"setSourceChart",
"setSourceInputChannel",
"setSourceMonitorMute",
"setSourceVerifierOffset",
"setStreamBus",
"setStreamBusGain",
"setStreamOutputDevice",
"setTonePolish",
"startAudio",
"startBacking",
"stopAudio",
"stopBacking",
"unbindInputDevice"
],
"audioEffects": [
"activateSegment",
"inspectRoute",
"loadChainPlan",
"releaseRoute",
"setRouteGain",
"setStageBypass",
"setStageParameter"
]
}