Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b31300d79e |
@@ -0,0 +1,74 @@
|
||||
# Row 254 build: library sync follow-up (C2-03a deferrals)
|
||||
|
||||
Row 254, #1759, reviewer Dewey, gate owner Sage. Brief: docs/plans/2026-10-21_library-sync-followup.md
|
||||
(0de97f429), for the deferrals Dewey named in the row 222 approval (#1723 comment 30138). Built on
|
||||
origin/refactor c3d88840c, which carries row 222 (0b33f5e9d) and row 196 (7247d80c7). Launch: sage-2163.
|
||||
|
||||
## What it does
|
||||
|
||||
1. **Client timeout.** `packages/bus/src/human-cli.mjs` gives `library.sync` a 130 s client timeout. Every
|
||||
other verb keeps Client's 5 s default (`timeout: undefined` takes the default). The server holds a library
|
||||
verb's socket for 120 s (`packages/bus/src/server.mjs`, the `socket.setTimeout(120_000)` branch), and the
|
||||
fetch alone may take 100 s (GIT_LIMITS). So the client waits past the server and gets the server's own
|
||||
answer, including its `request-timeout`, instead of giving up first.
|
||||
- The Console has no library route, so there is no Console timeout to change (brief item 1, "when one
|
||||
exists"). When a route is added, note that webui `humanCall` kills its child at 30 s by default. The
|
||||
route would need `timeoutMs` above 130 s for library.sync.
|
||||
- `tester-cli.mjs` is unchanged: library.sync is human-only, so the tester gets `human-required`.
|
||||
2. **STATUS codes (215 N3).** `packages/webui/src/bus.mjs` maps six codes:
|
||||
|
||||
| Code | Status | Why |
|
||||
|---|---|---|
|
||||
| library-sync-busy | 409 | a sync is running; read status again |
|
||||
| library-diverged | 409 | held for a human accept |
|
||||
| library-accept-stale | 409 | the tip moved; read status again |
|
||||
| library-accept-invalid | 400 | a malformed accept |
|
||||
| library-credential-missing | 503 | the remote can't be reached, like no bus to ask |
|
||||
| library-fetch-failed | 503 | the same |
|
||||
|
||||
Neither 503 is 502, the default for an unknown code, so dropping any one entry changes its status and the
|
||||
test catches it.
|
||||
3. **README (222 N4).** `packages/library/README.md` Sync section: the transport timeouts, that a client
|
||||
timeout leaves the sync running, that a second call gets `library-sync-busy`, and that `library.status`
|
||||
shows the result. It also says the Console has no library route yet.
|
||||
- Correction to row 222's BUILD.md, "What it does": "`webui/src/cli.mjs`: the Console gets the call" was
|
||||
not true. `cli.mjs` builds `librarySync` and passes it to `startServer`, and `serve.mjs` never reads it
|
||||
(checked on c3d88840c: no library reference in serve.mjs).
|
||||
|
||||
## Channel proof re-pin
|
||||
|
||||
`packages/bus/tests/channel-proof.test.mjs` pins both changed files by hash. The test change is two baseline
|
||||
values and a header comment, nothing else:
|
||||
|
||||
| File | Old | New |
|
||||
|---|---|---|
|
||||
| packages/bus/src/human-cli.mjs | 269c3a66008c0d57… | 9c9af66b98ea0a490c9a880fc81c3c62414a8ee0b5ba6478608771b8c4f87932 |
|
||||
| packages/webui/src/bus.mjs | 934ab2f9b6bde72a… | e69d17d4385a4d190d15373ed1a26e270a5c6906eec9e61fcc2bb6b05849cabb |
|
||||
|
||||
human.mjs and transport.mjs are unchanged, and so are the entry-path test and the plug-in import ban. No
|
||||
other test pins either file: `git grep` for both old hashes finds only queue.json history and old packets,
|
||||
and the one 64-hex string in webui service.test.mjs is a made-up UID.
|
||||
|
||||
## Dewey's other 222 notes
|
||||
|
||||
None is a few lines in a file this row changes, so all three stay deferred:
|
||||
- N1: a close waits on every entry left in a pass (runtime.mjs).
|
||||
- N2: a held tip still gets its tree read, and a refused read hides the divergence (sync.mjs).
|
||||
- N3: held states log every tick (runtime.mjs).
|
||||
|
||||
## Tests
|
||||
|
||||
- `packages/webui/tests/library-status.test.mjs` (new): each of the six codes maps to its status, and an
|
||||
unknown code is 502.
|
||||
- `packages/bus/tests/library-sync.test.mjs`, a new test. A stand-in bus socket answers every request after 6 s,
|
||||
and the real human-cli.mjs runs two requests in parallel. library.sync gets the answer, and library.status
|
||||
gets `outcome-unknown` at 5 s. It takes about 6 s.
|
||||
- `packages/bus/tests/channel-proof.test.mjs`: the new baselines.
|
||||
|
||||
Planned mutants on Sage's slot:
|
||||
1. Drop the human-cli timeout line: the library-sync test fails (sync gets outcome-unknown) and the
|
||||
channel-proof hash fails.
|
||||
2. Drop one STATUS entry (`library-fetch-failed`): library-status test fails, and so does the channel-proof hash.
|
||||
|
||||
Both files are hash-pinned, so channel-proof kills any edit to them. Each mutant is also expected to be killed
|
||||
by its behaviour test, and the run lists every failing test so that can be checked.
|
||||
@@ -24,6 +24,9 @@ try {
|
||||
const me = readProcess(process.pid);
|
||||
const client = new Client({
|
||||
path: process.argv[2],
|
||||
// Row 254: library.sync fetches for up to 100 s and the server holds its socket for 120 s,
|
||||
// so wait past that for the server's own answer. Every other verb keeps the 5 s default.
|
||||
timeout: request.verb === 'library.sync' ? 130_000 : undefined,
|
||||
human: {
|
||||
business: request.business,
|
||||
pid: me.pid,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// Row 196 intentionally re-pins human.mjs for the distinct user nonce and shared human
|
||||
// walk, and webui bus.mjs for initial-spawn nonce plus peer refusal status. Entry paths
|
||||
// and the ban on channel plug-ins importing a proof caller are retained.
|
||||
// Row 254 (#1759) re-pins human-cli.mjs for the 130 s library.sync client timeout and webui
|
||||
// bus.mjs for six library-* STATUS entries; no proof or transport change.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
@@ -12,14 +14,14 @@ import { Client } from "../src/client.mjs";
|
||||
|
||||
const baselines = {
|
||||
"packages/bus/src/human.mjs": "0c6d96abe4607cba0b02b0638a2fc217ff8135bac9a47761a0ef015fbe9d3079",
|
||||
"packages/bus/src/human-cli.mjs": "269c3a66008c0d57a54b14b73cf988d5f61f5e815455486296716e98ce5e8073",
|
||||
"packages/bus/src/human-cli.mjs": "9c9af66b98ea0a490c9a880fc81c3c62414a8ee0b5ba6478608771b8c4f87932",
|
||||
"packages/cli/src/transport.mjs": "5129c44b4d9f642e825344df65c9bfb4bac4fc8d6b25343b42d39853dbd40774",
|
||||
// Row 165 (#1662): seven STATUS entries for the role instance routes (organization-instance-exists,
|
||||
// organization-unknown-instance, organization-credentials-out-of-scope, organization-launch-not-configured,
|
||||
// organization-no-change, organization-project-mismatch, organization-unresolvable-instance), no proof or
|
||||
// transport change. Row 155 (#1649) added eight for the assistant routes.
|
||||
// Row 171 (#1668): project-name-is-repo maps to 409; no proof or transport change.
|
||||
"packages/webui/src/bus.mjs": "934ab2f9b6bde72aafe7814758550c283eb25a4f3a23b284021b13718d67178f",
|
||||
"packages/webui/src/bus.mjs": "e69d17d4385a4d190d15373ed1a26e270a5c6906eec9e61fcc2bb6b05849cabb",
|
||||
};
|
||||
const repoFile = path => new URL("../../../" + path, import.meta.url);
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ import assert from 'node:assert/strict';
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { createServer } from 'node:net';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Store } from '../src/store.mjs';
|
||||
import { Broker, LIBRARY_VERBS } from '../src/broker.mjs';
|
||||
import { startBroker } from '../src/runtime.mjs';
|
||||
@@ -107,3 +110,36 @@ test('C2-03a runtime: the timer syncs each entry with a user at start and on eac
|
||||
const mirror = library.mirrorPath({ business, project: 'books', dataRoot });
|
||||
assert.equal((await library.readSyncState(mirror, { branch: 'main' })).lastError.code, 'library-fetch-failed');
|
||||
});
|
||||
|
||||
// Row 254 (#1759): the human transport waits past the server's 120 s library hold for library.sync
|
||||
// only. A stand-in bus answers each request after 6 s, past the 5 s default every other verb keeps.
|
||||
test('row 254 human-cli: library.sync outlasts the 5 s client default, and library.status does not', { timeout: 20000 }, async t => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'bus-library-cli-'));
|
||||
const socket = join(root, 'bus.sock'), timers = new Set();
|
||||
const server = createServer(conn => {
|
||||
let input = '';
|
||||
conn.on('error', () => {});
|
||||
conn.on('data', b => {
|
||||
input += b;
|
||||
if (!input.includes('\n')) return;
|
||||
const { verb } = JSON.parse(input);
|
||||
timers.add(setTimeout(() => { if (!conn.destroyed) conn.end(JSON.stringify({ ok: true, result: { verb } }) + '\n'); }, 6000));
|
||||
});
|
||||
});
|
||||
await new Promise(r => server.listen(socket, r));
|
||||
t.after(() => { for (const timer of timers) clearTimeout(timer); server.close(); rmSync(root, { recursive: true, force: true }); });
|
||||
const cli = fileURLToPath(new URL('../src/human-cli.mjs', import.meta.url));
|
||||
const run = verb => new Promise(resolve => {
|
||||
const child = spawn(process.execPath, [cli, socket], { stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
let out = '', err = '';
|
||||
child.stdout.on('data', b => { out += b; });
|
||||
child.stderr.on('data', b => { err += b; });
|
||||
child.on('close', status => resolve({ status, out, err }));
|
||||
child.stdin.end(JSON.stringify({ business: 'acme', verb, args: { project: 'books' } }) + '\n');
|
||||
});
|
||||
const [sync, status] = await Promise.all([run('library.sync'), run('library.status')]);
|
||||
assert.equal(sync.status, 0, sync.err);
|
||||
assert.deepEqual(JSON.parse(sync.out), { verb: 'library.sync' });
|
||||
assert.equal(status.status, 2);
|
||||
assert.equal(status.err.trim(), 'outcome-unknown');
|
||||
});
|
||||
|
||||
@@ -165,6 +165,12 @@ lets go, so an object must outlive the next move of `served`. The bus host runs
|
||||
with a `user`, once at start and then every 60 seconds; the slot is
|
||||
`<configDir>/secrets/<business>/library-gitea`. `library.sync` is the human's bus verb for the same.
|
||||
|
||||
The human transport (`packages/bus/src/human-cli.mjs`) waits 130 seconds for `library.sync`, past the
|
||||
server's 120-second hold, and 5 seconds for every other verb. A client that gives up before the answer
|
||||
gets `outcome-unknown`, but the sync keeps running: a second call meanwhile gets `library-sync-busy`,
|
||||
and `library.status` shows the result once it ends. The Console has no library route yet: `cli.mjs`
|
||||
passes `librarySync` to `startServer`, and `serve.mjs` doesn't read it.
|
||||
|
||||
## Tests
|
||||
|
||||
```sh
|
||||
|
||||
@@ -102,5 +102,10 @@ const STATUS = {
|
||||
'organization-instance-exists': 409, 'organization-unknown-instance': 400,
|
||||
'organization-credentials-out-of-scope': 400, 'organization-launch-not-configured': 400,
|
||||
'organization-no-change': 400, 'organization-project-mismatch': 400, 'organization-unresolvable-instance': 400,
|
||||
// Row 254 (#1759): library sync. A sync already running, a held divergence and a stale accept are
|
||||
// the user's to resolve by reading library.status again; a missing credential or a failed fetch
|
||||
// leaves the remote unreachable, like no bus to ask.
|
||||
'library-sync-busy': 409, 'library-diverged': 409, 'library-accept-stale': 409, 'library-accept-invalid': 400,
|
||||
'library-credential-missing': 503, 'library-fetch-failed': 503,
|
||||
};
|
||||
export const busStatus = code => STATUS[code] ?? 502;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// Row 254 (#1759): the library.sync refusal codes map to their HTTP status, not the 502 default.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { busStatus } from '../src/bus.mjs';
|
||||
|
||||
test('row 254: each library sync code maps to its status', () => {
|
||||
const expected = {
|
||||
'library-sync-busy': 409, 'library-diverged': 409, 'library-accept-stale': 409, 'library-accept-invalid': 400,
|
||||
'library-credential-missing': 503, 'library-fetch-failed': 503,
|
||||
};
|
||||
for (const [code, status] of Object.entries(expected)) assert.equal(busStatus(code), status, code);
|
||||
assert.equal(busStatus('library-not-a-code'), 502, 'an unknown code keeps the default');
|
||||
});
|
||||
Reference in New Issue
Block a user