Files
XZBT/test/gc5-lifecycle.test.mjs
T

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