114 lines
5.3 KiB
JavaScript
114 lines
5.3 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import {
|
|
CLEANUP_TICKS,
|
|
ConditionTrigger,
|
|
DeferredStartQueue,
|
|
DISPATCH_BUDGET,
|
|
drainDispatch,
|
|
OwnershipModel
|
|
} from '../tools/gc5-lifecycle-model.mjs';
|
|
|
|
test('resources created through nested events inherit the scenario owner', () => {
|
|
const runtime = new OwnershipModel();
|
|
runtime.startScenario('scenario-1');
|
|
runtime.executeActions([{ type: 'event', actions: [{ type: 'event', actions: [{ type: 'resource', kind: 'sound' }] }] }], 'scenario-1');
|
|
assert.equal(runtime.ownedResourceCount('scenario-1'), 1);
|
|
});
|
|
|
|
test('owner termination bounds duration override release and eventually disposes it', () => {
|
|
const runtime = new OwnershipModel();
|
|
runtime.startScenario('scenario-1');
|
|
runtime.executeActions([{ type: 'resource', kind: 'override', releaseTicks: CLEANUP_TICKS * 10 }], 'scenario-1');
|
|
runtime.terminate('scenario-1', 'cancel');
|
|
assert.equal(runtime.ownedResourceCount('scenario-1'), 0);
|
|
assert.equal(runtime.resources.size, 1);
|
|
runtime.tick(CLEANUP_TICKS);
|
|
assert.equal(runtime.resources.size, 0);
|
|
assert.ok(runtime.diagnostics.some((entry) => entry.code === 'WARN_CLEANUP_FORCED'));
|
|
});
|
|
|
|
test('persistent set survives a later critical ordinary-action failure', () => {
|
|
const runtime = new OwnershipModel();
|
|
runtime.startScenario('scenario-1', { onCancel: [{ type: 'set', target: 'state.cleanup-ran', value: true }] });
|
|
runtime.executeActions([
|
|
{ type: 'set', target: 'state.mode', value: 'changed' },
|
|
{ type: 'resource', kind: 'visual' },
|
|
{ type: 'fail', critical: true }
|
|
], 'scenario-1');
|
|
assert.equal(runtime.state.get('state.mode'), 'changed');
|
|
assert.equal(runtime.state.get('state.cleanup-ran'), true);
|
|
assert.equal(runtime.owners.get('scenario-1').status, 'FAILED');
|
|
assert.equal(runtime.resources.size, 0);
|
|
});
|
|
|
|
test('startup and termination-hook failures still complete cleanup with the original terminal cause', () => {
|
|
const runtime = new OwnershipModel();
|
|
runtime.startScenario('scenario-1', {
|
|
onStart: [{ type: 'resource', kind: 'audio' }, { type: 'fail', critical: true }],
|
|
onCancel: [{ type: 'fail', critical: true }, { type: 'set', target: 'state.after-hook-failure', value: 1 }]
|
|
});
|
|
assert.equal(runtime.owners.get('scenario-1').status, 'FAILED');
|
|
assert.equal(runtime.state.get('state.after-hook-failure'), 1);
|
|
assert.equal(runtime.resources.size, 0);
|
|
assert.equal(runtime.diagnostics.filter((entry) => entry.code === 'ERR_ACTION_FAILURE').length, 2);
|
|
});
|
|
|
|
test('condition trigger requires a false tick before initial and subsequent firing', () => {
|
|
const trigger = new ConditionTrigger(2);
|
|
assert.equal(trigger.evaluate(true), false);
|
|
assert.equal(trigger.evaluate(false), false);
|
|
assert.equal(trigger.evaluate(true), false);
|
|
assert.equal(trigger.evaluate(true), true);
|
|
assert.equal(trigger.evaluate(true), false);
|
|
assert.equal(trigger.evaluate(false), false);
|
|
assert.equal(trigger.evaluate(true), false);
|
|
assert.equal(trigger.evaluate(true), true);
|
|
});
|
|
|
|
test('deferred queue keeps one request per definition, expires, rechecks, and orders deterministically', () => {
|
|
const queue = new DeferredStartQueue(10);
|
|
assert.equal(queue.defer({ definitionId: 'low', priority: 10, creationTick: 1, documentOrder: 0, sampledInputs: 0.2 }), true);
|
|
assert.equal(queue.defer({ definitionId: 'low', priority: 99, creationTick: 2, documentOrder: 0, sampledInputs: 0.9 }), false);
|
|
queue.defer({ definitionId: 'high-late', priority: 80, creationTick: 2, documentOrder: 2 });
|
|
queue.defer({ definitionId: 'high-early', priority: 80, creationTick: 1, documentOrder: 1 });
|
|
|
|
const started = queue.dispatch(3, (request) => request.definitionId !== 'low');
|
|
assert.deepEqual(started.map((request) => request.definitionId), ['high-early', 'high-late']);
|
|
assert.equal(queue.requests.get('low').sampledInputs, 0.2);
|
|
assert.deepEqual(queue.dispatch(11, () => true), []);
|
|
assert.equal(queue.requests.size, 0);
|
|
});
|
|
|
|
test('feedback is bounded and leaves cleanup capacity outside the ordinary budget', () => {
|
|
const result = drainDispatch([{ path: 'event.loop', ownerId: 'scenario-1' }], (unit) => [unit]);
|
|
assert.equal(result.exhausted, true);
|
|
assert.equal(result.consumed, DISPATCH_BUDGET);
|
|
assert.equal(result.discarded.length, 1);
|
|
assert.deepEqual(result.failedOwnerIds, ['scenario-1']);
|
|
|
|
const runtime = new OwnershipModel();
|
|
runtime.startScenario('scenario-1');
|
|
runtime.executeActions([{ type: 'resource' }], 'scenario-1');
|
|
runtime.terminate('scenario-1', 'failure');
|
|
assert.equal(runtime.resources.size, 0);
|
|
});
|
|
|
|
test('repeated completion, cancellation, and failure cycles return counters to baseline', () => {
|
|
const runtime = new OwnershipModel();
|
|
for (let index = 0; index < 30; index++) {
|
|
const id = `scenario-${index}`;
|
|
runtime.startScenario(id);
|
|
runtime.executeActions([
|
|
{ type: 'resource', kind: 'sound', releaseTicks: 2 },
|
|
{ type: 'event', actions: [{ type: 'resource', kind: 'visual', releaseTicks: 3 }] }
|
|
], id);
|
|
runtime.terminate(id, index % 3 === 0 ? 'complete' : index % 3 === 1 ? 'cancel' : 'failure');
|
|
runtime.tick(3);
|
|
}
|
|
assert.equal(runtime.resources.size, 0);
|
|
assert.equal(runtime.owners.size, 30);
|
|
assert.ok([...runtime.owners.values()].every((owner) => ['COMPLETED', 'CANCELLED', 'FAILED'].includes(owner.status)));
|
|
});
|