feat(phase0): complete shared contracts and milestone plan
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
|
||||
import { ExhibitValidator } from '../tools/validate-exhibit.mjs';
|
||||
import {
|
||||
attackValue,
|
||||
bindingValue,
|
||||
releaseValue,
|
||||
resolveNumericTarget,
|
||||
selectWinningOverride,
|
||||
smoothBinding
|
||||
} from '../tools/gc3-resolution-model.mjs';
|
||||
|
||||
const fixture = JSON.parse(readFileSync(new URL('./fixtures/gc3/resolution-traces.json', import.meta.url)));
|
||||
const closeTo = (actual, expected, epsilon = 1e-12) => assert.ok(Math.abs(actual - expected) <= epsilon, `${actual} != ${expected}`);
|
||||
|
||||
test('direct bus override masks a live binding and releases toward its current value', () => {
|
||||
for (const row of fixture.traces.directBusOverride) {
|
||||
const overrides = row.override === undefined
|
||||
? []
|
||||
: [{ id: 'direct', priority: 0, activationSequence: 1, value: row.override }];
|
||||
let resolved = resolveNumericTarget({ base: 0, binding: row.binding, overrides, safetyClamp: [0, 1] }).resolved;
|
||||
if (row.releaseProgress !== undefined) resolved = releaseValue(0.8, row.binding, row.releaseProgress);
|
||||
closeTo(resolved, row.resolved);
|
||||
}
|
||||
});
|
||||
|
||||
test('a parameter override feeds a downstream binding in the same tick and preserves user edits', () => {
|
||||
for (const row of fixture.traces.parameterOverrideFeedsBinding) {
|
||||
let activity = row.activityOverride ?? row.storedActivity;
|
||||
if (row.releaseProgress !== undefined) activity = releaseValue(0.9, row.storedActivity, row.releaseProgress);
|
||||
const gain = bindingValue(activity, { scale: 0.5, offset: 0.5, clamp: [0, 1] });
|
||||
closeTo(activity, row.resolvedActivity);
|
||||
closeTo(gain, row.busGain);
|
||||
}
|
||||
});
|
||||
|
||||
test('priority and activation sequence decide competition independently of scope', () => {
|
||||
const overrides = [
|
||||
{ id: 'scenario', scope: 'scenario', priority: 4, activationSequence: 2, value: 0.4 },
|
||||
{ id: 'duration-low', scope: 'duration', priority: 3, activationSequence: 99, value: 0.9 }
|
||||
];
|
||||
assert.equal(selectWinningOverride(overrides).id, 'scenario');
|
||||
|
||||
overrides.push({ id: 'duration-tie', scope: 'duration', priority: 4, activationSequence: 3, value: 0.7 });
|
||||
assert.equal(selectWinningOverride(overrides).id, 'duration-tie');
|
||||
});
|
||||
|
||||
test('automation continues while masked and becomes the live release destination', () => {
|
||||
for (const row of fixture.traces.automationMaskedThenReleased) {
|
||||
const overrides = row.override === undefined
|
||||
? []
|
||||
: [{ id: 'mask', priority: 1, activationSequence: 1, value: row.override }];
|
||||
let resolved = resolveNumericTarget({ base: 0, automation: row.automation, overrides }).resolved;
|
||||
if (row.releaseProgress !== undefined) resolved = releaseValue(0.75, row.automation, row.releaseProgress);
|
||||
closeTo(resolved, row.resolved);
|
||||
}
|
||||
});
|
||||
|
||||
test('legal modulation follows override and safety clamp runs last', () => {
|
||||
const result = resolveNumericTarget({
|
||||
base: 0.2,
|
||||
overrides: [{ id: 'boost', priority: 0, activationSequence: 1, value: 0.9 }],
|
||||
modulation: 0.3,
|
||||
safetyClamp: [0, 1]
|
||||
});
|
||||
assert.equal(result.afterOverride, 0.9);
|
||||
closeTo(result.beforeClamp, 1.2);
|
||||
assert.equal(result.resolved, 1);
|
||||
});
|
||||
|
||||
test('binding smoothing is exact, and re-enable initializes without replaying disabled time', () => {
|
||||
const first = smoothBinding(undefined, 0.8, fixture.tickSeconds, 0.05);
|
||||
assert.equal(first, 0.8);
|
||||
const second = smoothBinding(first, 0.2, fixture.tickSeconds, 0.05);
|
||||
closeTo(second, 0.8 + (1 - Math.exp(-fixture.tickSeconds / 0.05)) * (0.2 - 0.8));
|
||||
assert.equal(smoothBinding(undefined, 0.35, fixture.tickSeconds, 0.05), 0.35);
|
||||
});
|
||||
|
||||
test('interrupted transitions begin from the visible value without cancelling lower overrides', () => {
|
||||
closeTo(attackValue(0.2, 0.8, 0.5, 'linear'), 0.5);
|
||||
const lower = { id: 'lower', priority: 1, activationSequence: 1, value: 0.5 };
|
||||
const higher = { id: 'higher', priority: 2, activationSequence: 2, value: attackValue(0.5, 1, 0, 'linear') };
|
||||
assert.equal(selectWinningOverride([lower, higher]).value, 0.5);
|
||||
higher.live = false;
|
||||
assert.equal(selectWinningOverride([lower, higher]).id, 'lower');
|
||||
});
|
||||
|
||||
test('validator rejects conflicting writers, cycles, unsupported targets, and draft aliases', () => {
|
||||
const base = {
|
||||
xzbt: '0.1',
|
||||
meta: { id: 'gc3-validation', name: 'GC3 validation' },
|
||||
parameters: { activity: { type: 'number', default: 0.5 } },
|
||||
state: { energy: { type: 'number', initial: 0.2 } }
|
||||
};
|
||||
|
||||
const cases = [
|
||||
{
|
||||
code: 'ERR_CONFLICTING_BINDING',
|
||||
bindings: [
|
||||
{ source: 'parameters.activity', target: 'state.energy' },
|
||||
{ source: 'parameters.activity', target: 'state.energy', when: { op: 'gt', left: 1, right: 0 } }
|
||||
]
|
||||
},
|
||||
{
|
||||
code: 'ERR_CYCLIC_DEPENDENCY',
|
||||
bindings: [
|
||||
{ source: 'parameters.activity', target: 'state.energy' },
|
||||
{ source: 'state.energy', target: 'parameters.activity' }
|
||||
]
|
||||
},
|
||||
{
|
||||
code: 'ERR_UNSUPPORTED_TARGET',
|
||||
bindings: [{ source: 'parameters.activity', target: 'signals.time.elapsed' }]
|
||||
},
|
||||
{
|
||||
code: 'ERR_UNKNOWN_FIELD',
|
||||
bindings: [{ from: 'parameters.activity', to: 'state.energy' }]
|
||||
}
|
||||
];
|
||||
|
||||
for (const entry of cases) {
|
||||
const result = new ExhibitValidator({ ...base, bindings: entry.bindings }).validate();
|
||||
assert.equal(result.valid, false);
|
||||
assert.ok(result.errors.some((error) => error.code === entry.code), JSON.stringify(result.errors));
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user