Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5583fa7755 | ||
|
|
2d1efa0def |
No files matched your search
@@ -0,0 +1,98 @@
|
||||
# Row 227, Case 2 C2-14: end-to-end acceptance on a scratch root (#1728)
|
||||
|
||||
Owner Gizmoduck (sage-3581, 10-23), reviewer Dewey, who also runs the script.
|
||||
Brief: `docs/plans/2026-10-20_case2-rows.md`, section "C2-14 end-to-end acceptance on a scratch
|
||||
root", read at 45dca69f6. Branch `gizmoduck/row227` on 45dca69f6.
|
||||
|
||||
## Files
|
||||
|
||||
- `scripts/test-case2.sh` (new). The entry point: syntax check, one driver run, one counted line per
|
||||
step, an order check on the step ids, and a check that the made-up token is in no driver output.
|
||||
- `scripts/case2/e2e.mjs` (new), its fixture. Every step runs in one node process, because her remote
|
||||
is row 199's fake Gitea over TLS with a scratch CA, and it lives only as long as the process that
|
||||
started it. `checkMapping` takes `https://` remotes only (`packages/library/src/mapping.mjs:46-48`),
|
||||
so a plain file remote isn't an option.
|
||||
|
||||
The driver reuses the test fixtures already on refactor and adds none of its own:
|
||||
`packages/seat/tests/fake-gitea.mjs` (`scratchCa`, `fakeGitea`) and `packages/library/tests/helpers.mjs`
|
||||
(`BASE`, her layout's tree, `MARKER`, `git`, `mappingEntry`).
|
||||
|
||||
## Two stand-ins
|
||||
|
||||
1. **The human.** `human-cli.mjs` proves a human shell and refuses any caller with an agent among its
|
||||
ancestors (`packages/bus/src/human.mjs`; `packages/cli/tests/access.test.mjs:3-4,285-286` pins the
|
||||
refusal). Dewey and I both run under an agent, so a shell driver of human-cli would refuse every
|
||||
human verb. The driver binds the human in-process with `broker.bindHuman`, as the bus tests do
|
||||
(`packages/bus/tests/library-sync.test.mjs:27`). The bus checks after the bind are the real ones.
|
||||
2. **The fake harness.** No launcher and no model. The driver binds the creative instance's run on her
|
||||
thread (`bindLaunch` with `thread`), claims it, receives her message and answers in reply to it.
|
||||
That covers the thread routing and the companion's messaging lists (`roles/companion.json`), and
|
||||
leaves the launcher out. If Dewey wants the launcher in the path, `fakeHarnesses`
|
||||
(`packages/cli/tests/helpers.mjs:133`) is the way in. That is a bigger fixture, and I'd rather take
|
||||
it with C2-06's own fake harness (brief C2-06) than build a second one here.
|
||||
|
||||
## Steps
|
||||
|
||||
The ids are the driver's. "Waits on" names every row whose code the step needs and that isn't on
|
||||
refactor at 45dca69f6. A waiting step prints `skip <id> waits on row <ids>: <text>`, counted apart from
|
||||
pass and fail. When its rows land, the step gets written and its skip line goes.
|
||||
|
||||
| Step | Brief text | State |
|
||||
| --- | --- | --- |
|
||||
| S1a | bare repo in her layout | runs: classify on the fixture mapping (canon, manuscript, playground, unmapped, second universe) |
|
||||
| S1b | with a supporting folder | waits on 283 (C2-01d, supporting folders and the editor field) |
|
||||
| S2a | it syncs | runs: `library.sync` over https from the fake Gitea, served is her head |
|
||||
| S2b | | runs: `library.file` reads canon and playground at served, refuses an unmapped note `project-read-path` |
|
||||
| S2c | | runs: her fast-forward push is served by the next sync |
|
||||
| S2d | | runs: the token is under no file of the data root, the mirror included |
|
||||
| S3a | asks a creative agent in its thread | runs: project, thread on instance `bean` (companion), `thread.send` |
|
||||
| S3b | through a fake harness | runs: the fake harness receives it and its reply is stored in her thread, `in_reply_to` her message |
|
||||
| S3c | stores a reply whose cites verify | waits on 216 (C2-05a lookups) and 217 (C2-06 guard and page store) |
|
||||
| S4 | refuses a playground cite with `"as": "canon"` | waits on 217 (`playground-cited-as-canon`) |
|
||||
| S5a | holds a force-push as `library-diverged` | runs: refused, served unchanged, `diverged.commit` is the forced tip, the refusal recorded |
|
||||
| S5b | | runs: `accept` of a stale commit refuses `library-accept-stale`, `accept` of the tip serves it |
|
||||
| S6 | fixture-grant write, one commit with the trailer, notice in her thread | waits on 283, 285, 286, 288 |
|
||||
| S7 | story-path write refuses `library-write-story-path`, ref unchanged | waits on 283, 288 |
|
||||
| S8 | her revert lands as one commit | waits on 288 |
|
||||
| S9 | her `library.file.save` lands as one commit, her `editor` as author | waits on 283, 288 |
|
||||
| S10 | the Console renders the reply and the notice | waits on 220, 225 |
|
||||
|
||||
Row notes:
|
||||
|
||||
- Sage's in-flight list was 216, 217, 220, 225, 226, 283, 285 and 286. Rows 284, 287, 288 and 289 also
|
||||
touch these steps, and the brief's "After" names the Q226 rows (told Sage in gizmoduck-0419). I mark
|
||||
a step with the rows its code needs, not every row that touches it:
|
||||
- 288 (C2-02) is the write verb, the notice, the revert and `library.file.save`, so S6-S9 all name it.
|
||||
- 287 (C2-21, the grant executor) isn't named on S6, because the brief asks for a fixture grant.
|
||||
- 284 (C2-05c) adds supporting docs to the lookups. S3c cites canon, so it waits on 216 and 217 only.
|
||||
- 289 (C2-11b) is the Console library page. S9 is the verb, which 288 owns. If Dewey reads S9 as the
|
||||
page, 289 joins its list.
|
||||
- 226 (C2-13, install script and precheck) gates no step. A scratch root isn't an install.
|
||||
- PRD 1.17 (row 308) made story files DOCX. The brief predates it and the fixture tree is Markdown.
|
||||
S1-S5 read that tree, which is what the landed reader takes. S6-S9 take the file shape 288 lands with.
|
||||
|
||||
## Choices
|
||||
|
||||
- One driver process, not one per step: the fake Gitea's port and CA would change with each process,
|
||||
and the library entry's remote is fixed at bus start.
|
||||
- The bus timer stays on with a one-hour period, since `startBroker` needs `librarySync` to carry the
|
||||
CA's `command`. Its pass at start can hold the mirror, so a step's sync retries
|
||||
`library-sync-busy` for up to about 10 s.
|
||||
- The refusal check opens the bus store read-only (`node:sqlite`, `readOnly: true`). The runtime
|
||||
hands back no store, and no bus verb lists the events.
|
||||
- Git in the driver runs with `GIT_CONFIG_GLOBAL=/dev/null` and `GIT_CONFIG_NOSYSTEM=1`, so a caller's
|
||||
hooks, signing or credential helper can't reach it. `helpers.mjs` `git` already passes the
|
||||
identity and `core.hooksPath=/dev/null`.
|
||||
|
||||
## Carry for Dewey
|
||||
|
||||
- The launcher in the path waits for C2-06's fake harness (row 217). Sage accepted both stand-ins,
|
||||
labelled, in sage-3608. When 217 lands, S3b moves to its fake harness behind the launcher.
|
||||
|
||||
## Not yet run
|
||||
|
||||
Darkwing's re-pin window holds the slot (sage-3581), so as of this draft no node ran on it: no
|
||||
`node --check`, no driver run, no suite. `bash -n scripts/test-case2.sh` passes. The first run waits for
|
||||
Sage's CLEAR, and the runs go below.
|
||||
|
||||
## Runs
|
||||
@@ -0,0 +1,219 @@
|
||||
// C2-14 (#1728, row 227): the Case 2 end-to-end steps on a scratch root, driven by
|
||||
// scripts/test-case2.sh. Usage: node scripts/case2/e2e.mjs <scratch dir>
|
||||
//
|
||||
// One process holds her remote (row 199's fake Gitea on 127.0.0.1 over TLS with a scratch CA), a bus
|
||||
// on a scratch data root and the steps, because the remote's port and CA live only as long as it does.
|
||||
// It prints one line per step:
|
||||
// ok <step> <text>
|
||||
// not ok <step> <text>: <detail>
|
||||
// skip <step> waits on row <ids>: <text>
|
||||
// and exits 0 when it got to the end, 1 when a step threw outside its own check.
|
||||
//
|
||||
// Two stand-ins, both honest about what they are:
|
||||
// - The human. human-cli.mjs proves its caller is a human shell and refuses any process with an agent
|
||||
// among its ancestors (packages/bus/src/human.mjs), so a seat or a reviewer running this script can't
|
||||
// use it. The driver binds the human in-process with bindHuman, as the bus tests do.
|
||||
// - The fake harness. No launcher and no model: the driver binds the creative instance's run on her
|
||||
// thread, claims it, receives her message and answers it.
|
||||
//
|
||||
// Nothing here reads a real key or reaches a real host. The token is made up and lives only in the slot.
|
||||
import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
import { scratchCa, fakeGitea } from '../../packages/seat/tests/fake-gitea.mjs';
|
||||
import { BASE, MARKER, git, mappingEntry } from '../../packages/library/tests/helpers.mjs';
|
||||
import { startBroker } from '../../packages/bus/src/runtime.mjs';
|
||||
import * as library from '../../packages/library/src/index.mjs';
|
||||
|
||||
const TOKEN = 'fixture-token-c2-14-made-up-0227';
|
||||
const USER = 'books-bot';
|
||||
const BUSINESS = 'acme';
|
||||
const companion = JSON.parse(readFileSync(fileURLToPath(new URL('../../roles/companion.json', import.meta.url)), 'utf8'));
|
||||
|
||||
const lines = [];
|
||||
const out = (line) => { lines.push(line); process.stdout.write(line + '\n'); };
|
||||
// step(id, text, fn): fn returns nothing on a pass and throws on a fail.
|
||||
async function step(id, text, fn) {
|
||||
try { await fn(); out(`ok ${id} ${text}`); }
|
||||
catch (e) { out(`not ok ${id} ${text}: ${String(e?.code ?? '')} ${String(e?.message ?? e).split('\n')[0].slice(0, 300)}`.trimEnd()); }
|
||||
}
|
||||
const skip = (id, rows, text) => out(`skip ${id} waits on row ${rows.join(',')}: ${text}`);
|
||||
const expect = (cond, message) => { if (!cond) throw new Error(message); };
|
||||
const refuses = async (promise, code) => {
|
||||
try { await promise; } catch (e) { if (e?.code === code) return; throw new Error(`refused ${e?.code ?? e?.message}, expected ${code}`); }
|
||||
throw new Error(`answered, expected ${code}`);
|
||||
};
|
||||
|
||||
// Every file under `dir` that holds the token, as bytes.
|
||||
function holdsToken(dir) {
|
||||
for (const name of readdirSync(dir, { recursive: true })) {
|
||||
const file = join(dir, name);
|
||||
let st;
|
||||
try { st = statSync(file); } catch { continue; }
|
||||
if (st.isFile() && readFileSync(file).includes(TOKEN)) return file;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function main(root) {
|
||||
// Her side: a work repo in her layout's shape, cloned bare into the fake Gitea.
|
||||
const work = join(root, 'her', 'books');
|
||||
mkdirSync(work, { recursive: true });
|
||||
git(work, 'init', '-q', '-b', 'main');
|
||||
for (const [path, text] of Object.entries(BASE)) {
|
||||
mkdirSync(dirname(join(work, path)), { recursive: true });
|
||||
writeFileSync(join(work, path), text);
|
||||
}
|
||||
git(work, 'add', '-A');
|
||||
git(work, 'commit', '-qm', 'her first commit');
|
||||
const first = git(work, 'rev-parse', 'HEAD');
|
||||
const tls = scratchCa(join(root, 'ca'));
|
||||
const gitea = await fakeGitea({ dir: join(root, 'gitea'), tls, user: USER, token: TOKEN });
|
||||
const remote = gitea.repo('writer', 'books');
|
||||
mkdirSync(dirname(remote), { recursive: true });
|
||||
git(root, 'clone', '-q', '--bare', '--no-local', work, remote);
|
||||
// Her next commit, pushed to the bare repo; `from` rewinds first, which makes a force-push.
|
||||
const push = (files, { from = null } = {}) => {
|
||||
if (from) git(work, 'reset', '-q', '--hard', from);
|
||||
for (const [path, text] of Object.entries(files)) {
|
||||
mkdirSync(dirname(join(work, path)), { recursive: true });
|
||||
writeFileSync(join(work, path), text);
|
||||
}
|
||||
git(work, 'add', '-A');
|
||||
git(work, 'commit', '-qm', 'her next commit');
|
||||
git(work, 'push', '-q', '--force', remote, 'HEAD:refs/heads/main');
|
||||
return git(work, 'rev-parse', 'HEAD');
|
||||
};
|
||||
const remoteHead = () => git(remote, 'rev-parse', 'refs/heads/main');
|
||||
|
||||
// Our side: a scratch data root and config dir with the slot, and a bus with one creative instance.
|
||||
const dataRoot = join(root, 'data'), configDir = join(root, 'config');
|
||||
mkdirSync(dataRoot, { mode: 0o700 });
|
||||
mkdirSync(join(configDir, 'secrets', BUSINESS), { recursive: true, mode: 0o700 });
|
||||
writeFileSync(join(configDir, 'secrets', BUSINESS, library.SLOT), TOKEN + '\n', { mode: 0o600 });
|
||||
const entry = { ...mappingEntry(), remote: `${gitea.origin}/writer/books.git`, user: USER };
|
||||
const business = {
|
||||
id: BUSINESS, human: 'jason', arbiters: { technical: 'bean', delivery: 'bean' },
|
||||
roles: { bean: { definition: 'companion', authority: companion.authority } },
|
||||
definitions: { companion: { messaging: companion.messaging } },
|
||||
projects: { books: entry },
|
||||
};
|
||||
// syncCommand's argv with the scratch CA added, as packages/library/tests/sync.test.mjs does. The
|
||||
// timer's period is long, so only its pass at start and the steps' own syncs fetch.
|
||||
const command = (kind, url, opts) => {
|
||||
const c = library.syncCommand(kind, url, opts);
|
||||
return { argv: c.argv, env: { ...c.env, GIT_SSL_CAINFO: tls.ca } };
|
||||
};
|
||||
const bus = await startBroker({ dataRoot, businesses: { [BUSINESS]: business }, librarySync: { command, intervalMs: 3_600_000, configDir } });
|
||||
try {
|
||||
const broker = bus.broker;
|
||||
const human = broker.bindHuman({ business: BUSINESS, human: 'jason', via: 'cli', outsideAgent: true });
|
||||
const call = (cap, verb, args = {}) => Promise.resolve().then(() => broker.request(cap, { verb, args }));
|
||||
// The timer's pass at start may still hold the mirror; a step's sync waits it out.
|
||||
const sync = async (args = {}) => {
|
||||
for (let i = 0; ; i++) {
|
||||
try { return await call(human, 'library.sync', { project: 'books', ...args }); }
|
||||
catch (e) { if (e?.code !== 'library-sync-busy' || i > 100) throw e; await new Promise(r => setTimeout(r, 100)); }
|
||||
}
|
||||
};
|
||||
const refusals = () => readRefusals(dataRoot);
|
||||
|
||||
// S1: her layout and the mapping that reads it.
|
||||
const mapping = library.checkMapping(entry);
|
||||
await step('S1a', 'her layout classifies: a character is canon, a Playground note is playground, a top-level note is unmapped', () => {
|
||||
const of = p => library.classify(mapping, p);
|
||||
expect(of('Book One/Characters/Mara Vell.md').scope === 'canon' && of('Book One/Characters/Mara Vell.md').type === 'character', 'character');
|
||||
expect(of('Book One/Chapters/Chapter 01.md').type === 'manuscript', 'chapter');
|
||||
expect(of('Book One/Playground/What If.md').scope === 'playground', 'playground');
|
||||
expect(of('Inbox Note.md').scope === 'unmapped', 'inbox note');
|
||||
expect(of('Second World/Chapters/01.md').universe === 'second', 'second universe');
|
||||
});
|
||||
skip('S1b', [283], 'a supporting folder in the mapping and the editor field');
|
||||
|
||||
// S2: sync from her bare repo over https, and read at the served commit.
|
||||
await step('S2a', 'library.sync fetches her bare repo and serves its head', async () => {
|
||||
const r = await sync();
|
||||
expect(r.served === remoteHead() && r.served === first, `served ${r.served}, remote ${remoteHead()}`);
|
||||
expect(r.lastError === null && r.diverged === null, 'state has an error or a divergence');
|
||||
});
|
||||
await step('S2b', 'library.file reads her canon and playground notes at the served commit, and refuses an unmapped one', async () => {
|
||||
const canon = await call(human, 'library.file', { project: 'books', path: 'Book One/Characters/Mara Vell.md' });
|
||||
expect(canon.commit === first && canon.content.includes(MARKER), 'canon note');
|
||||
const play = await call(human, 'library.file', { project: 'books', path: 'Book One/Playground/What If.md' });
|
||||
expect(play.content.includes(MARKER), 'playground note');
|
||||
await refuses(call(human, 'library.file', { project: 'books', path: 'Inbox Note.md' }), 'project-read-path');
|
||||
});
|
||||
await step('S2c', 'her next commit is a fast-forward and the next sync serves it', async () => {
|
||||
const next = push({ 'Book One/Places/Harbor Town.md': `---\ntype: place\n---\nSalt and tar. ${MARKER}\n` });
|
||||
const r = await sync();
|
||||
expect(r.served === next, `served ${r.served}, expected ${next}`);
|
||||
});
|
||||
await step('S2d', 'the token is in the slot only: nowhere under the data root, the mirror included', () => {
|
||||
const file = holdsToken(dataRoot);
|
||||
expect(file === null, `token found under the data root (${file === null ? '' : file.slice(dataRoot.length)})`);
|
||||
});
|
||||
|
||||
// S3: she asks the creative instance in its thread; the fake harness answers.
|
||||
let thread, asked;
|
||||
await step('S3a', 'she makes a thread with the creative instance and asks in it', async () => {
|
||||
const project = (await call(human, 'project.create', { name: 'books' })).id;
|
||||
const made = await call(human, 'thread.create', { project, name: 'Book One', instance: 'bean' });
|
||||
expect(made.instance === 'bean', `instance ${made.instance}`);
|
||||
thread = made.id;
|
||||
asked = (await call(human, 'thread.send', { thread, body: 'What colour are Mara Vell\'s eyes?' })).id;
|
||||
});
|
||||
await step('S3b', 'the fake harness receives her message on its thread and its reply is stored in her thread', async () => {
|
||||
const run = broker.bindLaunch({ business: BUSINESS, role: 'bean', run: 'bean-c2-14', harness: 'pi', thread });
|
||||
await call(run, 'role.claim');
|
||||
const inbox = await call(run, 'message.receive');
|
||||
expect(inbox.length === 1 && inbox[0].id === asked, `received ${inbox.length}`);
|
||||
await call(run, 'message.send', { to: 'human', body: 'Grey. (Book One/Characters/Mara Vell.md)', in_reply_to: asked });
|
||||
const history = await call(human, 'thread.messages', { thread });
|
||||
expect(history.length === 2 && history[1].from_role === 'bean' && history[1].in_reply_to === asked,
|
||||
`history ${history.map(m => m.from_role).join(',')}`);
|
||||
});
|
||||
skip('S3c', [216, 217], 'the reply carries cites from the lookups, the guard verifies each one at the served commit, and the store keeps them');
|
||||
skip('S4', [217], 'a reply citing a playground note with "as": "canon" refuses playground-cited-as-canon');
|
||||
|
||||
// S5: a force-push holds as library-diverged until she accepts the new tip.
|
||||
await step('S5a', 'a force-push of her branch holds as library-diverged and served stays put', async () => {
|
||||
const before = (await call(human, 'library.status', { project: 'books' })).served;
|
||||
const forced = push({ 'Book One/Places/Harbor Town.md': `---\ntype: place\n---\nRewritten. ${MARKER}\n` }, { from: first });
|
||||
await refuses(sync(), 'library-diverged');
|
||||
const status = await call(human, 'library.status', { project: 'books' });
|
||||
expect(status.served === before, 'served moved');
|
||||
expect(status.diverged?.commit === forced, `diverged ${JSON.stringify(status.diverged)}`);
|
||||
expect(refusals().includes('library-diverged'), 'no recorded refusal');
|
||||
});
|
||||
await step('S5b', 'accept refuses a stale commit, then serves the new tip and clears the hold', async () => {
|
||||
await refuses(sync({ accept: first }), 'library-accept-stale');
|
||||
const tip = remoteHead();
|
||||
const r = await sync({ accept: tip });
|
||||
expect(r.served === tip && r.diverged === null, `served ${r.served}`);
|
||||
});
|
||||
|
||||
// S6-S9: agent writes, the story-path refusal, her revert and her Console save.
|
||||
skip('S6', [283, 285, 286, 288], 'a seat with a fixture grant writes a supporting doc: one commit with the trailer in the bare repo, and the notice in her thread');
|
||||
skip('S7', [283, 288], 'a write to a story path refuses library-write-story-path, and the bare repo\'s ref is unchanged');
|
||||
skip('S8', [288], 'her revert of the write lands as one commit');
|
||||
skip('S9', [283, 288], 'her library.file.save lands as one commit with her editor as author');
|
||||
// S10: the Console.
|
||||
skip('S10', [220, 225], 'the Console renders the reply and the notice');
|
||||
} finally {
|
||||
await bus.close();
|
||||
await gitea.close();
|
||||
}
|
||||
}
|
||||
|
||||
// The codes of the bus's recorded refusals, oldest first, read from its store on a read-only handle.
|
||||
function readRefusals(dataRoot) {
|
||||
const db = new DatabaseSync(join(dataRoot, 'bus', 'bus.sqlite'), { readOnly: true });
|
||||
try { return db.prepare("SELECT body FROM events WHERE kind='action.refused' ORDER BY seq").all().map(row => JSON.parse(row.body).code); }
|
||||
finally { db.close(); }
|
||||
}
|
||||
|
||||
const root = process.argv[2];
|
||||
if (!root) { process.stderr.write('usage: node scripts/case2/e2e.mjs <scratch dir>\n'); process.exit(2); }
|
||||
try { await main(resolve(root)); }
|
||||
catch (e) { process.stderr.write(`e2e: stopped: ${e?.code ?? ''} ${String(e?.message ?? e).split('\n')[0]}\n`); process.exitCode = 1; }
|
||||
Executable
+94
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
# Case 2 end-to-end acceptance on a scratch root (C2-14, #1728, row 227).
|
||||
#
|
||||
# Her layout in a bare repo behind a fake Gitea on 127.0.0.1 (TLS, scratch CA),
|
||||
# a bus on a scratch data root with one creative instance, and the steps of the
|
||||
# C2-14 brief in order. scripts/case2/e2e.mjs runs the steps in one process,
|
||||
# since the remote lives only as long as that process does, and prints one line
|
||||
# per step. This script checks its syntax, runs it, and counts the lines.
|
||||
#
|
||||
# No live host, no real model, no network past 127.0.0.1, no real key. The
|
||||
# human is an in-process stand-in: human-cli.mjs refuses a caller with an agent
|
||||
# among its ancestors, so a seat can't drive it. The creative instance's run is
|
||||
# a fake harness bound by the driver, with no launcher.
|
||||
#
|
||||
# A step that waits on a row not yet on refactor prints a skip line naming the
|
||||
# row, and counts as neither pass nor fail. When the row lands, its step gets
|
||||
# written in the driver and its skip line goes.
|
||||
#
|
||||
# scripts/test-case2.sh full run
|
||||
# NO_COLOR=1 scripts/test-case2.sh plain output
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
SANDBOX="$(mktemp -d)"
|
||||
trap 'rm -rf "$SANDBOX"' EXIT
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
SKIP=0
|
||||
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
||||
C_OK=$'\033[0;32m'; C_FAIL=$'\033[0;31m'; C_RESET=$'\033[0m'
|
||||
else
|
||||
C_OK=""; C_FAIL=""; C_RESET=""
|
||||
fi
|
||||
|
||||
check() {
|
||||
if [ "$2" = "0" ]; then PASS=$((PASS+1)); echo "${C_OK}OK${C_RESET} $1"; else FAIL=$((FAIL+1)); echo "${C_FAIL}FAIL${C_RESET} $1"; fi
|
||||
}
|
||||
|
||||
DRIVER="scripts/case2/e2e.mjs"
|
||||
# The steps the driver must print, in order. A missing or extra line fails.
|
||||
STEPS="S1a S1b S2a S2b S2c S2d S3a S3b S3c S4 S5a S5b S6 S7 S8 S9 S10"
|
||||
|
||||
echo "toolchain: node $(node --version), git $(git --version | cut -d' ' -f3), openssl $(openssl version | cut -d' ' -f2)"
|
||||
echo
|
||||
|
||||
# --- syntax ---
|
||||
node --check "$DRIVER" >/dev/null 2>&1
|
||||
check "syntax: $DRIVER" $?
|
||||
|
||||
# --- the steps ---
|
||||
# The driver's own state stays in the sandbox. Git reads no global or system
|
||||
# config, so a caller's hooks, signing or credential helpers can't reach it.
|
||||
OUT="$SANDBOX/steps.txt"
|
||||
ERR="$SANDBOX/stderr.txt"
|
||||
GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 \
|
||||
node "$DRIVER" "$SANDBOX/root" >"$OUT" 2>"$ERR"
|
||||
RC=$?
|
||||
check "driver ran to the end (exit $RC)" "$RC"
|
||||
if [ "$RC" -ne 0 ]; then
|
||||
# The driver prints codes and first lines only; never git's stderr with a token in it.
|
||||
grep '^e2e: ' "$ERR" | head -5
|
||||
fi
|
||||
|
||||
seen=""
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
"ok "*)
|
||||
id="${line#ok }"; id="${id%% *}"
|
||||
check "${line#ok }" 0 ;;
|
||||
"not ok "*)
|
||||
id="${line#not ok }"; id="${id%% *}"
|
||||
check "${line#not ok }" 1 ;;
|
||||
"skip "*)
|
||||
id="${line#skip }"; id="${id%% *}"
|
||||
SKIP=$((SKIP+1)); echo "skip ${line#skip }" ;;
|
||||
*)
|
||||
id=""
|
||||
check "driver line not understood: ${line:0:200}" 1 ;;
|
||||
esac
|
||||
[ -n "$id" ] && seen="$seen $id"
|
||||
done <"$OUT"
|
||||
[ "${seen# }" = "$STEPS" ]
|
||||
check "the driver printed each step once, in order" $?
|
||||
|
||||
# --- the token ---
|
||||
# The made-up token is in the slot and nowhere else the bus writes. The driver
|
||||
# checks the data root; this checks its output too.
|
||||
! grep -rqF 'fixture-token-c2-14-made-up-0227' "$OUT" "$ERR"
|
||||
check "the fixture token is in no driver output" $?
|
||||
|
||||
echo
|
||||
echo "selftest: $PASS passed, $FAIL failed, $SKIP skipped (each skip names the row it waits on)"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
Reference in new issue
Block a user