Files
stack/packages/webui/tests/reads-fixture.mjs
T
jason.woltjeandClaude Opus 5.5 cbd79cf666 feat(webui): read-only Queue, Business and Settings views (#1543, row 54)
The Console gains three read-only views. Queue lists every row from
rows() in packages/queue (lead decision 83: the same lock-free read as
`queue list`, writing nothing), run in a child with a timeout and an
output cap; a row page shows the full row. Business shows names, an
allowlist of vars, arbiters, authority per action and credential
metadata (service, account, role, date, never a value, file or
variable). Settings shows RELEASE, the board origin and the notifier
binding. Every route is GET only, and every string in a body or
refusal passes a redactor: the config directory and dataRoot become
<config> and <dataRoot>, other absolute, ~/ and file:// paths <path>,
and 17 to 20 digit runs <id>. A queue-read that fails to start sends a
fixed message, never node's stderr.

Four fixes to HEAD behaviour, each with a test: the bus view's refresh
timer is cleared at render, a same-page refresh keeps focus on the H1,
#conv-pick is emptied when a conversation opens, and error() returns
focus to <main> only when something had focus. Filbert's T8 tests the
failed first read.

Dewey built it in two rounds. Round 1 (dba2429e) got changes from
Filbert (27185: After rendered [object Object], ~/ and :/ paths leaked)
and Darkwing (27186: Arbiters always none, queue-read import failure
leaked a file:// path and stack). Round 2 (afb2ae0e) was approved by
Filbert (27190) and Darkwing (27191, correction 27192). Sage's gate on
d64f434f plus the candidate: 13 package node suites 0 failed (queue
149, webui 39), every scripts/test-*.sh 0 failed (task 98/0 with
Docker, 26/0 without; release 14/0), build-tokens --check current.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 17:11:17 -05:00

62 lines
4.1 KiB
JavaScript

// Row 54 (#1543): the seeded fixture the reads and views tests share. A
// business holds a Discord user id, token files, an environment variable
// name and bot ids; a data root holds a notifier config and a Discord
// binding with guild, channel and user ids and a token file; an agent's
// model var holds a path. A queue row's note names an id and token paths in
// the shapes real notes use (row 35: "~/.config/mosaic-dev/secrets/…").
// None may reach a response, the page or a log, nor may the temporary
// directory. Rows 9 and 11 have After entries in the stored {id, when} shape.
import assert from 'node:assert/strict';
import { cpSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { scratchRepo, genesisCommitted, mapText, MAP_ROWS } from '../../queue/tests/helpers.mjs';
import { businessDoc, writeJson } from '../../business/tests/helpers.mjs';
export const SRC = join(import.meta.dirname, '..', 'src');
export const SNOWFLAKES = ['123456789012345678', '223456789012345678', '323456789012345678', '423456789012345678', '523456789012345678'];
export const day = n => new Date(Date.now() + n * 86400000).toISOString().slice(0, 10);
export const NOTE = `token at /srv/secret/x.token for ${SNOWFLAKES[4]}; tokens 0600 under ~/.config/mosaic-dev/secrets/mosaic-stack (receipt file:/srv/secret/r.json)`;
export const NOTE_SHOWN = 'token at <path> for <id>; tokens 0600 under <path> (receipt file:<path>)';
// A canonical scratch checkout with a committed queue, the reader script
// beside a copy of packages/queue/src, and a RELEASE file.
export function queueRepo(t) {
const rows = MAP_ROWS.map(r => r.id === 6 ? { ...r, note: NOTE } : r.id === 11 ? { ...r, after: [{ id: 9, when: 'done' }] } : r);
const repo = scratchRepo(t, { map: mapText(rows) });
genesisCommitted(repo);
mkdirSync(join(repo.root, 'packages/webui/src'), { recursive: true });
cpSync(join(SRC, 'queue-read.mjs'), join(repo.root, 'packages/webui/src/queue-read.mjs'));
writeFileSync(join(repo.root, 'RELEASE'), '0.0.99\n');
return repo;
}
// The business, notifier and binding files under one temporary base.
export function seeded(t, { business = true, notify = true } = {}) {
const base = realpathSync(mkdtempSync(join(tmpdir(), 'mosaic-webui-reads-')));
t.after(() => rmSync(base, { recursive: true, force: true }));
const configDir = join(base, 'config'), dataRoot = join(base, 'data');
const doc = businessDoc(base);
doc.vars['human.discordUserId'] = SNOWFLAKES[0];
doc.roles.coder.credentials.gitea = { env: 'CODER_GITEA_SECRET_ENV', rotateBy: day(-1) };
doc.roles.coder.credentials.vikunja.expires = day(3);
doc.roles.reviewer.credentials.vikunja.expires = day(-2);
doc.roles.reviewer.vars = { model: '/srv/secret/models/local.gguf' };
if (business) writeJson(join(configDir, 'businesses', 'acme.json'), doc);
if (notify) writeJson(join(dataRoot, 'notify', 'acme', 'notify.json'), { notifyVersion: 1, binding: 'jason-dm' });
writeJson(join(dataRoot, 'discord', 'jason-dm.json'), {
bindingVersion: 1, name: 'jason-dm', seat: 'sage', guildId: SNOWFLAKES[1], guildName: 'g', botUserId: SNOWFLAKES[2],
tokenFile: join(base, 'secrets', 'discord.token'), channels: [{ id: SNOWFLAKES[3], name: 'c', mode: 'open' }], users: [{ id: SNOWFLAKES[0], name: 'jason' }],
});
const system = { configVersion: 1, environment: 'development', dataRoot, execution: { backend: 'docker', provider: 'zai', model: 'glm-5.3-flash', adapter: 'mock' } };
return { base, configDir, dataRoot, system, doc };
}
// Nothing secret, no id, no temporary path, in any body.
export function clean(text, ...bases) {
for (const b of bases) assert.equal(text.includes(b), false, `response names ${b}`);
for (const s of SNOWFLAKES) assert.equal(text.includes(s), false, `response carries id ${s}`);
for (const s of ['placeholder-not-a-token', 'CODER_GITEA_SECRET_ENV', '"file"', '"env"', '"botId"', 'discordUserId', '"tokenFile"', '"guildId"', '"channels"', '/srv/secret', '~/', 'secrets/mosaic-stack']) assert.equal(text.includes(s), false, `response carries ${s}`);
}