diff options
Diffstat (limited to 'prototypes/workbench/cli.test.mjs')
| -rw-r--r-- | prototypes/workbench/cli.test.mjs | 121 |
1 files changed, 121 insertions, 0 deletions
diff --git a/prototypes/workbench/cli.test.mjs b/prototypes/workbench/cli.test.mjs new file mode 100644 index 0000000..f355d2d --- /dev/null +++ b/prototypes/workbench/cli.test.mjs @@ -0,0 +1,121 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRuntime } from './web/runtime.mjs'; + +const root = fileURLToPath(new URL('.', import.meta.url)); +function run(input, file) { + const result = spawnSync(process.execPath, [join(root, 'cli.mjs'), ...(file ? [file] : [])], { + input, encoding: 'utf8', cwd: root, + }); + if (result.error) throw result.error; + return { ...result, snapshots: result.stdout.split('\n').filter(line => line.startsWith('{')).map(line => JSON.parse(line)) }; +} + +test('terminal starts empty and produces the same charge trace as the shared runtime', async () => { + assert.deepEqual(run('show\nquit\n').snapshots[0].states, {}); + const file = join(root, 'examples/charge.pure'); + const result = run('down jump\nframe 30\nup jump\nframe 1\nshow\nquit\n', file); + assert.equal(result.status, 0, result.stderr); + const runtime = await createRuntime(await readFile(join(root, 'web/core.wasm'))); + runtime.apply(await readFile(file, 'utf8')); + runtime.down('jump', 'terminal'); runtime.frame(30); runtime.up('jump', 'terminal'); runtime.frame(); + // JSON has no undefined-valued optional fields; compare the same wire form. + assert.deepEqual(result.snapshots.at(-1), JSON.parse(JSON.stringify(runtime.snapshot()))); + assert(result.snapshots.at(-1).states.y < 240); +}); + +test('invalid terminal commands do not alter state and fail piped execution', () => { + const result = run('show\nframe 0\nframe -1\nframe 1.5\nframe 1e2\nframe 601\nshow extra\ndown unknown\nshow\nquit\n', join(root, 'examples/counter.pure')); + assert.equal(result.status, 1); + assert.deepEqual(result.snapshots[0], result.snapshots[1]); + assert.equal(result.stderr.trim().split('\n').length, 7); +}); + +test('terminal replays authored pointer movement and cancellation through the shared runtime', async () => { + const file = join(root, 'examples/direct.pure'); + const result = run('point drag down 90 170\npoint drag move 360 220\npoint drag up 410 240\npoint drag down 410 240\npoint drag move 500 260\npoint drag cancel 500 260\nshow\nquit\n', file); + assert.equal(result.status, 0, result.stderr); + const runtime = await createRuntime(await readFile(join(root, 'web/core.wasm'))); + runtime.apply(await readFile(file, 'utf8')); + for (const [phase, x, y] of [['down',90,170],['move',360,220],['up',410,240],['down',410,240],['move',500,260],['cancel',500,260]]) + runtime.point('drag', phase, x, y, 'terminal'); + assert.deepEqual(result.snapshots.at(-1), JSON.parse(JSON.stringify(runtime.snapshot()))); + assert.equal(result.snapshots.at(-1).states.x, 350); + assert.equal(result.snapshots.at(-1).states.y, 210); + assert.deepEqual(result.snapshots.at(-1).held, []); +}); + +test('file load preserves by name, rejected load preserves active work, and save never overwrites', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-author-cli-')); + t.after(() => rm(directory, { recursive: true, force: true })); + const first = join(directory, 'first.pure'), second = join(directory, 'second with space.pure'); + const invalid = join(directory, 'invalid.pure'), saved = join(directory, 'saved.pure'); + await writeFile(first, 'state n = 7'); + await writeFile(second, 'state n = 100\nstate added = 9'); + await writeFile(invalid, 'state n = broken'); + const result = run(`load ${second}\nload ${invalid}\nshow\nsave ${saved}\nreset\nsave ${saved}\nquit\n`, first); + assert.equal(result.status, 1); + assert.deepEqual(result.snapshots[0].states, { n: 7, added: 9 }); + assert.deepEqual(result.snapshots[1], result.snapshots[0]); + assert.deepEqual(result.snapshots.at(-1).states, { n: 100, added: 9 }); + assert.equal(await readFile(saved, 'utf8'), await readFile(second, 'utf8')); + assert.match(result.stderr, /EEXIST/); +}); + +test('terminal exposes arithmetic rollback and retains failure through matching release', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-author-error-')); + t.after(() => rm(directory, { recursive: true, force: true })); + const file = join(directory, 'error.pure'); + await writeFile(file, 'state n = 7\ninput tap = Space\non tap.down { n = 1 / 0 }'); + const result = run('down tap\nup tap\nshow\nreset\nquit\n', file); + assert.equal(result.status, 1); + for (const snapshot of result.snapshots.slice(0, 3)) { + assert.equal(snapshot.states.n, 7); assert.equal(snapshot.lastError, 4); + assert.equal(snapshot.errorLine, 3); + } + assert.equal(result.snapshots[3].lastError, 0); +}); + +test('terminal configures and discards evaluation records and recovers from a display limit', () => { + const result = run('limits 32 8 2048\nshow\ncosts\nforget all\nlimits 32 8 0\nlimits 32 8 2048\nshow\nquit\n', join(root, 'examples/counter.pure')); + assert.equal(result.status, 1); // The explicit zero-budget display failed. + assert.equal(result.stderr, ''); + const [configured, warm, costs, forgotten, limited, recovered, final] = result.snapshots; + assert.deepEqual(warm.views, configured.views); + assert(costs.evaluation.cacheHits > 0); + assert.deepEqual(forgotten.states, configured.states); + assert.deepEqual(limited.views, []); + assert.equal(limited.lastError, 101); + assert.deepEqual(recovered, final); + assert.deepEqual(recovered.views, configured.views); + assert.deepEqual(recovered.states, configured.states); +}); + +test('terminal saves and restores placement independently of reset and preserves a browser draft', async t => { + const directory = await mkdtemp(join(tmpdir(), 'pure-layout-cli-')); + t.after(() => rm(directory, {recursive:true,force:true})); + const saved=join(directory,'layout.json'), copied=join(directory,'copy.json'); + const first=run(`point drag down 90 170\npoint drag move 360 220\npoint drag up 90 170\nsave-project ${saved}\nreset\nrestore ${saved}\nshow\nquit\n`,join(root,'examples/direct.pure')); + assert.equal(first.status,0,first.stderr);assert.equal(first.snapshots.at(-1).states.x,350); + const document=JSON.parse(await readFile(saved,'utf8'));document.draft='state incomplete ='; + await writeFile(saved,JSON.stringify(document)); + const second=run(`save-project ${copied}\nreset\nshow\nquit\n`,saved); + assert.equal(second.status,0,second.stderr);assert.equal(second.snapshots.at(-1).states.x,80); + const copy=JSON.parse(await readFile(copied,'utf8'));assert.equal(copy.draft,document.draft);assert.deepEqual(copy.states,document.states); + const rejected=run(`save-project ${saved}\nquit\n`,saved); + assert.equal(rejected.status,1);assert.match(rejected.stderr,/EEXIST/); +}); + +test('invalid restoration leaves terminal code and state unchanged', async t => { + const directory=await mkdtemp(join(tmpdir(),'pure-invalid-layout-')); + t.after(()=>rm(directory,{recursive:true,force:true})); + const file=join(directory,'bad.json'); + await writeFile(file,JSON.stringify({format:'pure-workbench',version:2,source:'state x=1',states:[]})); + const result=run(`show\nrestore ${file}\nshow\nquit\n`,join(root,'examples/counter.pure')); + assert.equal(result.status,1);assert.deepEqual(result.snapshots[0],result.snapshots[1]); +}); |
