feat(queue): Piece D, reviews as issue comments, raw per-seat token helper (row 12, #1508)

queue move ID in-review posts the review request as a Gitea comment and
review record reads verdicts back, so reviews stop being files in
docs/plans/reviews/. On a comment round, in-review to waiting-on-jason
now needs every listed reviewer's approval for the current round, the
same as in-review to done (Filbert r1 C1). scripts/gitea-api.sh reads
the raw per-seat token files (lead decisions 37 to 39): config built and
checked before curl starts, export attribute cleared, fixed base URL.
test-queue.sh skips its live checks outside the canonical root.

Darkwing authored. Filbert approved D r2 (cf1d3fd0) after r1 (a2dc2302)
and corrected the plan (293747cd). Rocko reviewed the helper (e896192f,
2096b0a3), and Sage's lead check passed under decision 38. Manifest
b402fb38, 19 files.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-09-27 10:07:29 -05:00
co-authored by Claude Opus 5.5
parent cdcedb2741
commit f539466fcb
19 changed files with 2541 additions and 78 deletions
@@ -0,0 +1,205 @@
// scripts/gitea-api.sh with a raw per-seat token file (lead decision 37).
// Every credential file here is a dummy written by the test. curl and git
// are stubs: curl records its arguments and the config stream it was given,
// and never reaches a network.
import test from 'node:test';
import assert from 'node:assert/strict';
import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
const helper = fileURLToPath(new URL('../../../scripts/gitea-api.sh', import.meta.url));
const DUMMY = '0123456789abcdef0123456789abcdef01234567';
function setup(t) {
const dir = mkdtempSync(path.join(os.tmpdir(), 'gitea-helper-raw-'));
t.after(() => rmSync(dir, { recursive: true, force: true }));
const tool = (name, content) => { const p = path.join(dir, name); writeFileSync(p, '#!/usr/bin/env bash\n' + content); chmodSync(p, 0o755); };
// git runs between the helper's two reads of the file, so STUB_SWAP
// changes the file there.
tool('git', [
'f="$MOSAIC_GITEA_CREDENTIAL_FILE"',
'case "${STUB_SWAP:-}" in',
' invalid) printf "invalid-token\\n" > "$f";;',
' json) printf "{}" > "$f";;',
' mode) chmod 644 "$f";;',
' symlink) mv "$f" "$f.moved"; ln -s "$f.moved" "$f";;',
' missing) rm -f "$f";;',
'esac',
'printf %s https://git.mosaicstack.dev/mosaicstack/stack.git',
'',
].join('\n'));
tool('curl', [
'printf "%s\\n" "$@" > "$STUB_DIR/curl-args"',
'env > "$STUB_DIR/curl-env"',
'while (($#)); do case "$1" in -K) shift; cat "$1" > "$STUB_DIR/curl-cfg";; -o) shift; out="$1";; esac; shift; done',
'printf "{}" > "$out"',
'printf 200',
'',
].join('\n'));
const cred = (content, mode = 0o600, name = 'gitea-mosaicstack-darkwing.token') => {
const p = path.join(dir, name);
rmSync(p, { force: true });
writeFileSync(p, content);
chmodSync(p, mode);
return p;
};
const run = (file, extraEnv = {}, argv = ['GET', 'user']) => {
const r = spawnSync('bash', [helper, ...argv], {
encoding: 'utf8',
env: { ...process.env, PATH: `${dir}:${process.env.PATH}`, STUB_DIR: dir, MOSAIC_GITEA_CREDENTIAL_FILE: file, ...extraEnv },
});
const called = existsSync(path.join(dir, 'curl-args'));
const args = called ? readFileSync(path.join(dir, 'curl-args'), 'utf8').split('\n') : null;
const cfg = called ? readFileSync(path.join(dir, 'curl-cfg'), 'utf8') : null;
const env = called ? readFileSync(path.join(dir, 'curl-env'), 'utf8') : null;
for (const f of ['curl-args', 'curl-cfg', 'curl-env']) rmSync(path.join(dir, f), { force: true });
return { status: r.status, stdout: r.stdout, stderr: r.stderr, called, args, cfg, env };
};
return { dir, cred, run };
}
const header = (token) => `header = "Authorization: token ${token}"\nheader = "Content-Type: application/json"\n`;
test('a raw token file, with or without one trailing newline, reaches curl only through the config stream', t => {
const s = setup(t);
for (const content of [DUMMY, `${DUMMY}\n`]) {
const r = s.run(s.cred(content));
assert.equal(r.status, 0, r.stderr);
assert.ok(r.called);
assert.equal(r.cfg, header(DUMMY));
assert.ok(r.args.includes('https://git.mosaicstack.dev/api/v1/user'), r.args.join(' '));
assert.ok(!r.args.some((a) => a.includes(DUMMY)), 'the token is not in argv');
assert.ok(!r.env.includes(DUMMY), 'the token is not in the environment curl gets');
assert.ok(!r.stdout.includes(DUMMY) && !r.stderr.includes(DUMMY), 'the token is not printed');
assert.equal(r.stdout, '{}');
assert.match(r.stderr, /^HTTP 200$/m);
}
});
test('the raw path accepts nothing else, and refuses before curl runs', t => {
const s = setup(t);
const bad = [
['empty', ''],
['39 characters', DUMMY.slice(1)],
['41 characters', `${DUMMY}8`],
['upper case', DUMMY.toUpperCase().replace(/^0/, 'A')],
['CRLF', `${DUMMY}\r\n`],
['a trailing CR', `${DUMMY}\r`],
['a trailing space', `${DUMMY} `],
['a trailing tab', `${DUMMY}\t`],
['two newlines', `${DUMMY}\n\n`],
['leading space', ` ${DUMMY.slice(1)}`],
['trailing space', `${DUMMY.slice(1)} `],
['a second line', `${DUMMY}\nx`],
['a quote', `${DUMMY.slice(2)}"\n`],
['not hex', `${DUMMY.slice(1)}g`],
['non-ASCII', `${DUMMY.slice(2)}é`],
['80 characters', DUMMY + DUMMY],
];
for (const [what, content] of bad) {
const r = s.run(s.cred(content));
assert.equal(r.status, 3, `${what}: ${r.stderr}`);
assert.equal(r.called, false, `${what}: curl ran`);
assert.equal(r.stdout, '', what);
}
});
test('the file checks still apply on the raw path: mode, symlink, missing, directory', t => {
const s = setup(t);
// 000 and 200 pass the group and other check; the read then refuses.
for (const mode of [0o640, 0o604, 0o660, 0o644, 0o000, 0o200]) {
const r = s.run(s.cred(`${DUMMY}\n`, mode));
assert.equal(r.status, 3, `mode ${mode.toString(8)}`);
assert.equal(r.called, false);
}
const real = s.cred(`${DUMMY}\n`, 0o600, 'real.token');
const link = path.join(s.dir, 'link.token');
symlinkSync(real, link);
assert.deepEqual([s.run(link).status, s.run(link).called], [3, false]);
assert.deepEqual([s.run(path.join(s.dir, 'missing.token')).status, s.run(path.join(s.dir, 'missing.token')).called], [3, false]);
assert.deepEqual([s.run(s.dir).status, s.run(s.dir).called], [3, false]);
});
test('the raw path base URL has no override', t => {
const s = setup(t);
const r = s.run(s.cred(`${DUMMY}\n`), { MOSAIC_GITEA_URL: 'https://evil.example', GITEA_URL: 'https://evil.example', MOSAIC_GITEA_BASE_URL: 'https://evil.example' });
assert.equal(r.status, 0, r.stderr);
assert.ok(r.args.includes('https://git.mosaicstack.dev/api/v1/user'));
assert.ok(!r.args.some((a) => a.includes('evil')));
});
test('the JSON path is unchanged, and JSON never falls through to the raw path', t => {
const s = setup(t);
const json = (o) => s.cred(JSON.stringify(o), 0o600, 'mosaic.gitea.json');
const good = s.run(json({ mosaicstack: { url: 'https://git.mosaicstack.dev/', api_token: 'json-dummy' } }));
assert.equal(good.status, 0, good.stderr);
assert.equal(good.cfg, header('json-dummy'));
assert.ok(good.args.includes('https://git.mosaicstack.dev/api/v1/user'));
const refused = [
['another host', { mosaicstack: { url: 'https://evil.example', api_token: 'json-dummy' } }],
['no token', { mosaicstack: { url: 'https://git.mosaicstack.dev' } }],
['an empty token', { mosaicstack: { url: 'https://git.mosaicstack.dev', api_token: '' } }],
['no mosaicstack key', { url: 'https://git.mosaicstack.dev', api_token: 'json-dummy' }],
];
for (const [what, o] of refused) {
const r = s.run(json(o));
assert.deepEqual([r.status, r.called], [3, false], what);
}
// Content that parses as JSON takes the JSON path even when it would pass
// the raw pattern. A token of 40 decimal digits refuses there.
for (const content of ['null', '"x"', '1234567890123456789012345678901234567890', '1234567890123456789012345678901234567890\n']) {
const r = s.run(s.cred(content));
assert.deepEqual([r.status, r.called], [3, false], JSON.stringify(content));
}
});
test('a file that changes between the two reads refuses before curl runs, with or without a body', t => {
const s = setup(t);
const post = ['POST', 'repos/mosaicstack/stack/issues/1508/comments', '{"body":"dummy"}'];
// Unchanged, both calls reach curl, and the POST carries its body.
for (const argv of [['GET', 'user'], post]) {
const r = s.run(s.cred(`${DUMMY}\n`), {}, argv);
assert.deepEqual([r.status, r.called, r.cfg], [0, true, header(DUMMY)], r.stderr);
}
assert.ok(s.run(s.cred(`${DUMMY}\n`), {}, post).args.includes('--data-binary'));
for (const swap of ['invalid', 'json', 'mode', 'symlink', 'missing']) {
for (const argv of [['GET', 'user'], post]) {
const what = `${swap} ${argv[0]}`;
const r = s.run(s.cred(`${DUMMY}\n`), { STUB_SWAP: swap }, argv);
assert.deepEqual([r.status, r.called, r.stdout], [3, false, ''], what);
assert.ok(!r.stderr.includes(DUMMY), what);
}
}
const json = s.cred(JSON.stringify({ mosaicstack: { url: 'https://git.mosaicstack.dev', api_token: 'json-dummy' } }), 0o600, 'mosaic.gitea.json');
const r = s.run(json, { STUB_SWAP: 'invalid' }, post);
assert.deepEqual([r.status, r.called], [3, false], 'a JSON file that changes');
});
test('the token reaches no child environment, even with an inherited CFG or SHELLOPTS=allexport', t => {
const s = setup(t);
const post = ['POST', 'repos/mosaicstack/stack/issues/1508/comments', '{"body":"dummy"}'];
const files = [
['raw', DUMMY, () => s.cred(`${DUMMY}\n`)],
['JSON', 'json-dummy', () => s.cred(JSON.stringify({ mosaicstack: { url: 'https://git.mosaicstack.dev', api_token: 'json-dummy' } }), 0o600, 'mosaic.gitea.json')],
];
const seeds = [
['an inherited CFG', { CFG: 'inherited harmless value' }],
['SHELLOPTS=allexport', { SHELLOPTS: 'allexport' }],
['both', { CFG: 'inherited harmless value', SHELLOPTS: 'allexport' }],
];
for (const [kind, token, make] of files) {
for (const [seed, env] of seeds) {
for (const argv of [['GET', 'user'], post]) {
const what = `${kind}, ${seed}, ${argv[0]}`;
const r = s.run(make(), env, argv);
assert.deepEqual([r.status, r.called, r.cfg], [0, true, header(token)], `${what}: ${r.stderr}`);
assert.ok(!r.env.includes(token), `${what}: the token is in curl's environment`);
assert.ok(!r.args.some((a) => a.includes(token)), `${what}: the token is in argv`);
assert.ok(!r.stdout.includes(token) && !r.stderr.includes(token), `${what}: the token is printed`);
}
}
}
});