aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/workbench/presentation.test.mjs
blob: 98bef6eb83ed420efe9ad043c1bf3ef38e8d3055 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { createRuntime } from './web/runtime.mjs';
import { createRuntime as before } from './results/2026-09-23-presentation/before/web/runtime.mjs';

const wasm = await readFile(new URL('./web/core.wasm', import.meta.url));
const source = `state x = 3
state radius = 5
input move = KeyM
input shrink = KeyS
box tile(x, 10, x * 2, 20, "#123456")
circle dot(50, 50, radius, "#abcdef")
text label(0, 0, 10 / x, "Value: ")
on move.down { x = x + 0.25 }
on shrink.down { radius = radius - 10 }
on frame { x = x + dt }
`;

// The archived adapter predates route diagnostics. These legacy fixtures have
// no routes; keep all original fields in the equality comparison.
function archivedSnapshot(runtime) {
  const value = runtime.snapshot();
  value.memory.pointerTargetBytes = 3;
  value.memory.pointerTargets = 0;
  return value;
}

// Reconstruct only in tests, for comparison with the archived snapshot path.
function collect(runtime, hooks = {}) {
  let states, views, declared;
  const result = runtime.present({
    begin(program) {
      declared = program; states = {}; views = [];
      hooks.begin?.();
    },
    state(i, value) { states[declared.states[i].name] = value; },
    view(i, valid) {
      if (!valid) return;
      const {id, type, color, prefix} = declared.views[i];
      views.push({id, type, color, prefix});
    },
    value(i, name, value) { views.at(-1)[name] = value; hooks.value?.(); },
    end(count) { assert.equal(views.length, count); },
  });
  return {states, views, ...result};
}

test('direct publication matches archived snapshots and work counts through authored changes', async () => {
  for (const cacheEntries of [0, 1, 32]) {
    const left = await createRuntime(wasm, {cacheEntries}), right = await before(wasm, {cacheEntries});
    const compare = () => {
      assert.deepEqual(collect(left), archivedSnapshot(right));
      assert.deepEqual(left.evaluationStats(), right.evaluationStats());
    };
    compare();
    for (const r of [left, right]) r.apply(source);
    compare();
    for (let i = 0; i < 120; i++) {
      for (const r of [left, right]) {
        r.down('move', 'keyboard'); r.down('move', 'pointer');
        r.up('move', 'keyboard'); r.up('move', 'pointer');
        if (i % 11 === 0) r.frame(2);
        if (i % 17 === 0) r.forgetEvaluation('history');
        if (i % 23 === 0) r.reset();
      }
      compare();
    }
    for (const r of [left, right]) { r.down('shrink'); r.up('shrink'); }
    compare(); // invalid circle only; other views still publish
    for (const r of [left, right]) assert.throws(() => r.apply('state broken ='));
    compare();
    for (const r of [left, right]) r.apply('state radius = 1\nstate x = 0\ntext label(1, 2, x)');
    compare();
    for (const r of [left, right]) r.apply('');
    compare();
  }
});

test('whole-scene budget failure never publishes partial bindings and recovers exactly', async () => {
  const left = await createRuntime(wasm, {cacheEntries:32}), right = await before(wasm, {cacheEntries:32});
  for (const r of [left, right]) r.apply(source);
  for (let workLimit = 0; workLimit <= 80; workLimit++) {
    for (const r of [left, right]) r.configureEvaluation({workLimit});
    let calls = 0;
    const result = collect(left, {value: () => calls++});
    assert.deepEqual(result, archivedSnapshot(right));
    assert.deepEqual(left.evaluationStats(), right.evaluationStats());
    if (result.displayError?.code === 101) assert.equal(calls, 0);
    else assert.equal(calls, 10);
  }
});

test('latched event rollback, nonfinite bindings, reset and signed zero keep their semantics', async () => {
  const left = await createRuntime(wasm), right = await before(wasm);
  const definition = 'state x = 0\ninput fail = Space\ntext value(0, 0, 1 / x)\non fail.down { x = 2\nx = 1 / 0 }';
  for (const r of [left, right]) { r.apply(definition); r.down('fail'); r.up('fail'); }
  assert.deepEqual(collect(left), archivedSnapshot(right));
  for (const r of [left, right]) r.configureEvaluation({workLimit:0});
  assert.deepEqual(collect(left), archivedSnapshot(right));
  for (const r of [left, right]) {
    r.apply('state x = -0\ntext value(x, 0, x)'); r.reset(); r.configureEvaluation({workLimit:2048});
  }
  const result = collect(left);
  assert.deepEqual(result, archivedSnapshot(right));
  assert(Object.is(result.states.x, -0));
});

test('publication rejects reentrant runtime changes and reads before they affect the scene', async () => {
  const runtime = await createRuntime(wasm);
  runtime.apply(source);
  const calls = [() => runtime.apply(''), () => runtime.reset(), () => runtime.frame(),
    () => runtime.down('move'), () => runtime.up('move'), () => runtime.releaseAll(),
    () => runtime.configureEvaluation({workLimit:0}), () => runtime.forgetEvaluation(),
    () => runtime.snapshot(), () => runtime.present({})];
  const expected = runtime.snapshot();
  assert.deepEqual(collect(runtime, {begin() {
    for (const call of calls) assert.throws(call, /출력 전달 중/);
  }}), expected);
  assert.deepEqual(runtime.snapshot(), expected);
});

test('invalid or throwing output adapters do not strand the runtime or alter authored state', async () => {
  const runtime = await createRuntime(wasm);
  runtime.apply(source);
  const expected = runtime.snapshot();
  assert.throws(() => runtime.present({}), TypeError);
  assert.throws(() => collect(runtime, {value() { throw new Error('destination failed'); }}), /destination failed/);
  assert.deepEqual(collect(runtime), expected);
  runtime.down('move'); runtime.up('move');
  assert.equal(collect(runtime).states.x, 3.25);
});