open-standards/wai/wai-studio/test/actions.test.mjs
Transaction Science 5b0dca60f1 WAI Studio e2e: a publisher in the room before a viewer joins; WebKit on Linux in Playwright's image; scrollable blocks focusable; Quantum Studio's critique a named group
A viewer follows the room's clock, whose events track opens with the room's first participant.
The macOS runner and the Studio host run WebKit from SSH, where it never launches, so the CI
runs WebKit in Playwright's image on the linux runner.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 16:17:25 -04:00

103 lines
6.7 KiB
JavaScript

// Studio's actions from the catalogue (scripts/actions.mjs): what is generated, and every
// rule of the drift check, each broken in a copy and caught; then the tree itself holds.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';
import { HERE, CORE, SERVER, GENERATED, action, catalogueFile, coreCalls, findings, generate, readCatalogue, serverOps, sourceOps } from '../scripts/actions.mjs';
import { BINDINGS } from '../src/bindings.js';
const op = (id, placement = 'local', more = {}) => ({
id, summary: `the ${id}`, placement, effect: 'reads', toolset: 'core', stability: 'stable',
cli: `wai ${id.split('.')[1]}`, doors: { cli: true, mcp: false, http: false }, names: { mcp: null }, route: null, ...more,
});
const DOC = { format: 'wai.ops/1', toolchain: '0.3.0', operations: [op('envelope.inspect'), op('room.join', 'client', { cli: null, doors: { cli: false, mcp: true, http: false }, names: { mcp: 'room_join' } }), op('room.grant', 'service', { route: { service: 'wai-room', method: 'POST', path: '/grant' } })] };
const CORE_SRC = '#[wasm_bindgen]\npub fn envelope_inspect(bytes: &[u8]) -> Output {}\npub const OPERATIONS: &[&str] = &[\n "envelope.inspect",\n];\n';
const SERVER_SRC = 'const LOCAL_OPS: &[(&str, &str)] = &[\n ("envelope.inspect", "inspect"),\n];\n';
const OK_BINDINGS = { 'envelope.inspect': { panel: 'inspect', hosted: 'tab', local: 'tab+local' }, 'room.join': { panel: 'rooms', hosted: 'page', local: 'page' } };
const clean = (over = {}) => ({ doc: DOC, sha256: 'x', generated: generate(DOC, 'x'), bindings: OK_BINDINGS, core: CORE_SRC, server: SERVER_SRC, files: [['src/x.js', "el('button', { 'data-op': 'envelope.inspect' })"]], ...over });
test('an action carries its operation\'s names in every door', () => {
const a = action(op('room.grant', 'service', { cli: null, doors: { cli: false, mcp: true, http: true }, names: { mcp: 'room_grant' }, route: { service: 'wai-room', method: 'POST', path: '/grant' } }));
assert.deepEqual([a.menu, a.item, a.palette, a.call, a.mcp, a.cli], ['Room', 'Grant', 'room grant', 'room_grant', 'room_grant', null]);
assert.deepEqual(a.route, { service: 'wai-room', method: 'POST', path: '/grant' });
});
test('the generated table is the catalogue\'s, in its order, and internal operations are left out', () => {
const doc = { ...DOC, operations: [...DOC.operations, op('bench.run', 'local', { stability: 'internal' })] };
const text = generate(doc, 'abc');
assert.match(text, /^\/\/ GENERATED/);
assert.ok(text.includes('"id":"envelope.inspect"') && !text.includes('bench.run'));
assert.ok(text.indexOf('envelope.inspect') < text.indexOf('room.join'));
assert.equal(generate(doc, 'abc'), text, 'the same catalogue gives the same bytes');
});
test('a clean tree has no finding', () => {
assert.deepEqual(findings(clean()), []);
});
test('an action renamed so it names no operation is caught', () => {
const b = { ...OK_BINDINGS, 'envelope.inspekt': OK_BINDINGS['envelope.inspect'] };
delete b['envelope.inspect'];
const f = findings(clean({ bindings: b }));
assert.ok(f.some((x) => x.includes('the action envelope.inspekt has no operation')), f.join('\n'));
});
test('an operation dropped from the catalogue is caught', () => {
const doc = { ...DOC, operations: DOC.operations.filter((o) => o.id !== 'envelope.inspect') };
const f = findings(clean({ doc }));
assert.ok(f.some((x) => x.includes('is not what the catalogue generates')), f.join('\n'));
assert.ok(f.some((x) => x.includes('the action envelope.inspect has no operation')), f.join('\n'));
assert.ok(f.some((x) => x.includes("OPERATIONS lists envelope.inspect, which the catalogue does not hold")), f.join('\n'));
});
test('an operation placed for the browser with no action is caught', () => {
const b = { ...OK_BINDINGS };
delete b['room.join'];
const f = findings(clean({ bindings: b }));
assert.ok(f.some((x) => x.includes('room.join is placed client') && x.includes('no action')), f.join('\n'));
});
test('a mismatched name is caught: the core\'s call, the server\'s table, the source', () => {
let f = findings(clean({ core: CORE_SRC.replace('envelope_inspect', 'envelope_read') }));
assert.ok(f.some((x) => x.includes('the core has no call envelope_inspect')), f.join('\n'));
assert.ok(f.some((x) => x.includes('envelope_read names envelope.read, which the catalogue does not hold')), f.join('\n'));
f = findings(clean({ server: SERVER_SRC.replace('"envelope.inspect"', '"envelope.show"') }));
assert.ok(f.some((x) => x.includes('LOCAL_OPS runs envelope.show')), f.join('\n'));
assert.ok(f.some((x) => x.includes('the action envelope.inspect runs through the local core, and wai dev\'s LOCAL_OPS does not run it')), f.join('\n'));
f = findings(clean({ files: [['src/y.js', "{ op: 'envelope.unwrapp' }"]] }));
assert.ok(f.some((x) => x.includes('src/y.js: envelope.unwrapp names no operation')), f.join('\n'));
f = findings(clean({ generated: generate(DOC, 'x').replace('"menu":"Envelope"', '"menu":"Envelopes"') }));
assert.ok(f.some((x) => x.includes('not what the catalogue generates')), f.join('\n'));
});
test('an action that says it runs from the command line needs a command line in the catalogue', () => {
// room.join has no command line in DOC (an MCP tool alone).
const f = findings(clean({ bindings: { ...OK_BINDINGS, 'room.join': { elsewhere: 'cli' } } }));
assert.ok(f.some((x) => x.includes('room.join says it runs from the command line')), f.join('\n'));
assert.deepEqual(findings(clean({ bindings: { ...OK_BINDINGS, 'room.join': { elsewhere: 'mcp' } } })), []);
});
test('the readers find what the core, the server and the source name', () => {
assert.deepEqual(coreCalls(CORE_SRC), { calls: ['envelope_inspect'], listed: ['envelope.inspect'] });
assert.deepEqual(serverOps(SERVER_SRC), ['envelope.inspect']);
assert.deepEqual(sourceOps([['a.js', "x = { op: 'a.b' }; el('i', { 'data-op': 'c.d' }); `data-op=\"e.f\"`; local.op('g.h', body)"]]).map((o) => o.id), ['a.b', 'c.d', 'e.f', 'g.h']);
});
function walk(dir) {
return readdirSync(dir).flatMap((n) => { const p = join(dir, n); return statSync(p).isDirectory() ? walk(p) : /\.(m?js|html)$/.test(n) && n !== 'actions.generated.js' ? [p] : []; });
}
test('this tree holds to its catalogue', () => {
const { file } = catalogueFile(null);
const { doc, sha256 } = readCatalogue(file);
const f = findings({
doc, sha256,
generated: readFileSync(GENERATED, 'utf8'),
bindings: BINDINGS,
core: readFileSync(CORE, 'utf8'),
server: readFileSync(SERVER, 'utf8'),
files: walk(join(HERE, 'src')).map((p) => [relative(HERE, p), readFileSync(p, 'utf8')]),
});
assert.deepEqual(f, []);
});