@bongos/core 1.19.1031 → 1.19.1033
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +167 -57
- package/.claude/skills/grade-audit/SKILL.md +1 -1
- package/.claude/skills/grader-health/SKILL.md +1 -1
- package/docs/adr/0110-untrack-whole-file-generated-nav-docs.md +26 -0
- package/docs/adr/0175-the-board-room-full-idea-ratification.md +3 -2
- package/docs/adr/0178-the-copy-desk-flag-and-queue.md +3 -3
- package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
- package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +1 -1
- package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +1 -1
- package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +11 -3
- package/docs/adr/0338-modules-travel-through-a-bongos-hosted-store.md +119 -0
- package/docs/adr/README.md +4 -3
- package/docs/architecture.md +1 -1
- package/docs/file-map.md +4 -3
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/autobongos-windows-host.md +3 -1
- package/docs/recipes/packaging-the-core.md +2 -0
- package/docs/recipes/redteam-patrol.md +2 -2
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/modules/autonomy/gauge.js +57 -9
- package/modules/hall-ui/records/builders-roster.md +10 -0
- package/modules/hall-ui/records/catalog.md +10 -0
- package/modules/hall-ui/records/collaboration.md +14 -0
- package/modules/hall-ui/records/goal-dependency-forest.md +11 -0
- package/modules/hall-ui/records/oversight-family.md +19 -0
- package/modules/hall-ui/records/panel-family.md +19 -0
- package/modules/hall-ui/records/reading-rooms.md +12 -0
- package/modules/hall-ui/records/record-family.md +13 -0
- package/modules/hall-ui/records/reference-room.md +9 -0
- package/modules/hall-ui/records/roster-and-ledger.md +9 -0
- package/modules/hall-ui/records/sky.md +23 -0
- package/modules/lifecycle/db-claim-reads.js +8 -2
- package/modules/lifecycle/dependency-advisory.js +2 -1
- package/modules/lifecycle/ship-preflight.js +91 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +7411 -0
- package/scripts/gds/address-park-sweep.sh +3 -1
- package/scripts/gds/autobongos-run.js +81 -1
- package/scripts/gds/baseline-staleness.js +1 -1
- package/scripts/gds/db-backup-nightly.sh +9 -4
- package/scripts/gds/db-backup-offsite.sh +4 -2
- package/scripts/gds/package-core.js +72 -1
- package/scripts/gds/provision-core-upgrade.js +128 -77
- package/scripts/gds/redteam-patrol.js +4 -4
- package/scripts/gds/release-notes.js +271 -0
- package/scripts/gds/seed-board-room-tasks.js +1 -1
- package/scripts/gds/ship-flow.js +5 -3
- package/scripts/gds/ship-land.js +86 -7
- package/scripts/gds/ship-preflight-steps.js +56 -3
- package/scripts/gds/stray-unit-sweep.sh +3 -1
- package/scripts/gds/upgrade-migrate.js +67 -0
- package/scripts/gds/upgrade.js +6 -4
- package/src/module-api.js +1 -1
- package/tests/adr_0162_citation.mjs +184 -0
- package/tests/autobongos_loop.mjs +114 -0
- package/tests/autonomy_gauge.mjs +56 -0
- package/tests/core_upgrade_runner.mjs +89 -1
- package/tests/db_backup_nightly.mjs +21 -3
- package/tests/db_backup_offsite.mjs +81 -0
- package/tests/grade_diff_hash.mjs +77 -2
- package/tests/hall_records.mjs +85 -0
- package/tests/helpers.mjs +25 -0
- package/tests/instance_db_migrated_on_code_move.mjs +8 -1
- package/tests/plain_cards.mjs +1 -1
- package/tests/redteam_patrol.mjs +1 -1
- package/tests/release_notes.mjs +327 -0
- package/tests/shell_stat_portability.mjs +49 -0
- package/tests/ship_land_conflict.mjs +181 -0
- package/tests/ship_preflight.mjs +160 -3
- package/tests/sweep_app_account.mjs +15 -2
- package/tests/upgrade_migrate.mjs +195 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// tests/db_backup_offsite.mjs — the weekly offsite copy sizes what it ships the same way
|
|
2
|
+
// on every platform (task 1004170).
|
|
3
|
+
//
|
|
4
|
+
// THE BUG. db-backup-offsite.sh read both of its sizes — the local dump it picks and the
|
|
5
|
+
// ciphertext it uploads — with GNU-only `stat -c '%s'`, which BSD/macOS stat refuses
|
|
6
|
+
// outright. Every run on a Mac died before it encrypted anything, and CI's Linux never
|
|
7
|
+
// saw it, because nothing ran the script at all. The empty-ciphertext guard rides on the
|
|
8
|
+
// second of those sizes, so it is pinned here too.
|
|
9
|
+
//
|
|
10
|
+
// The script runs for real under macOS-shaped `stat` and `wc` (tests/helpers.mjs), with
|
|
11
|
+
// stub `age` and `python3` first on PATH: nothing is encrypted, no bucket is touched and
|
|
12
|
+
// no network is used.
|
|
13
|
+
//
|
|
14
|
+
// Run: node --test tests/db_backup_offsite.mjs
|
|
15
|
+
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import { test } from 'node:test';
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import os from 'node:os';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { macShapedTools } from './helpers.mjs';
|
|
24
|
+
|
|
25
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const SCRIPT = path.join(ROOT, 'scripts', 'gds', 'db-backup-offsite.sh');
|
|
27
|
+
const skip = process.platform === 'win32' ? 'needs a POSIX bash + coreutils (runs in CI)' : false;
|
|
28
|
+
// The script's env-var prefix, built rather than written — the same reason as in
|
|
29
|
+
// tests/db_backup_nightly.mjs: the legacy-literal ratchet counts the spelled-out form.
|
|
30
|
+
const ENV = 'gds_'.toUpperCase();
|
|
31
|
+
|
|
32
|
+
const DUMP_NAME = 'smallproj-2026-09-20T000000Z.sql.gz';
|
|
33
|
+
const DUMP = '--\n-- PostgreSQL database dump complete\n--\n';
|
|
34
|
+
const CIPHER = 'age-encryption.org/v1\n-> X25519 stub\n';
|
|
35
|
+
|
|
36
|
+
// `cipher` is what the stub age writes to its -o file. The stub python3 answers the
|
|
37
|
+
// boto3 probe and, for the upload program, records the object key it was handed.
|
|
38
|
+
function runOffsite({ cipher = CIPHER } = {}) {
|
|
39
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'db-offsite-'));
|
|
40
|
+
const bin = path.join(tmp, 'bin');
|
|
41
|
+
const dir = path.join(tmp, 'backups');
|
|
42
|
+
const uploads = path.join(tmp, 'uploads.log');
|
|
43
|
+
fs.mkdirSync(bin);
|
|
44
|
+
fs.mkdirSync(dir);
|
|
45
|
+
macShapedTools(bin);
|
|
46
|
+
fs.writeFileSync(path.join(dir, DUMP_NAME), DUMP);
|
|
47
|
+
fs.writeFileSync(path.join(tmp, 'cipher'), cipher);
|
|
48
|
+
fs.writeFileSync(path.join(bin, 'age'),
|
|
49
|
+
`#!/bin/bash\nwhile [ $# -gt 0 ]; do [ "$1" = -o ] && out="$2"; shift; done\ncp "${path.join(tmp, 'cipher')}" "$out"\n`, { mode: 0o755 });
|
|
50
|
+
fs.writeFileSync(path.join(bin, 'python3'),
|
|
51
|
+
`#!/bin/bash\n[ "$1" = -c ] && exit 0\ncat > /dev/null\necho "$${ENV}OFFSITE_KEY" >> "${uploads}"\n`, { mode: 0o755 });
|
|
52
|
+
const r = spawnSync('bash', [SCRIPT], {
|
|
53
|
+
encoding: 'utf8',
|
|
54
|
+
env: {
|
|
55
|
+
...process.env,
|
|
56
|
+
PATH: `${bin}:${process.env.PATH}`,
|
|
57
|
+
[`${ENV}BACKUP_DB`]: 'smallproj',
|
|
58
|
+
[`${ENV}BACKUP_DIR`]: dir,
|
|
59
|
+
[`${ENV}SPACES_ENV`]: path.join(tmp, 'no-such-spaces.env'),
|
|
60
|
+
[`${ENV}OFFSITE_AGE_RECIPIENT`]: 'age1stubrecipient',
|
|
61
|
+
SPACES_KEY: 'k', SPACES_SECRET: 's', SPACES_BUCKET: 'b', SPACES_REGION: 'r', SPACES_ENDPOINT: 'https://example.invalid',
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
const sent = fs.existsSync(uploads) ? fs.readFileSync(uploads, 'utf8').split('\n').filter(Boolean) : [];
|
|
65
|
+
return { status: r.status, stdout: r.stdout, stderr: r.stderr, sent };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
test('it sizes the dump and its ciphertext on macOS as on Linux, then ships it (task 1004170)', { skip }, () => {
|
|
69
|
+
const r = runOffsite();
|
|
70
|
+
assert.equal(r.status, 0, r.stderr);
|
|
71
|
+
assert.match(r.stdout, new RegExp(`\\(${Buffer.byteLength(DUMP)} bytes → ${Buffer.byteLength(CIPHER)} encrypted\\)`),
|
|
72
|
+
'both sizes are the real byte counts, unpadded');
|
|
73
|
+
assert.deepEqual(r.sent, [`gds-offsite/${DUMP_NAME}.age`]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('an empty ciphertext is refused, never uploaded as a backup', { skip }, () => {
|
|
77
|
+
const r = runOffsite({ cipher: '' });
|
|
78
|
+
assert.equal(r.status, 2, r.stderr);
|
|
79
|
+
assert.match(r.stderr, /encryption produced an empty file/);
|
|
80
|
+
assert.deepEqual(r.sent, [], 'nothing reached the bucket');
|
|
81
|
+
});
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
// Covers:
|
|
6
6
|
// 1. diffHash / shouldReuseGrade — pure helpers in ship.js (no git/network)
|
|
7
7
|
// 2. db.applyGrade stores diff_hash — SQL-aware fakePool (NODE_ENV=test seam)
|
|
8
|
-
// 3. db.getTaskGrade
|
|
9
|
-
//
|
|
8
|
+
// 3. db.getTaskGrade — exported, returns null for a task with no grade, and
|
|
9
|
+
// lifts signals.issues to the top level without removing the nested key
|
|
10
|
+
// (task 1003490; driven through the NODE_ENV=test deps.pool seam)
|
|
10
11
|
//
|
|
11
12
|
// Run: node tests/grade_diff_hash.mjs
|
|
12
13
|
|
|
@@ -210,6 +211,80 @@ await test('getTaskGrade is exported from lifecycle/db.js', () => {
|
|
|
210
211
|
assert.equal(typeof db.getTaskGrade, 'function', 'getTaskGrade must be exported');
|
|
211
212
|
});
|
|
212
213
|
|
|
214
|
+
// task 1003490 — the issues LIFT (task 1003488's second half). task_grades has
|
|
215
|
+
// no `issues` column, so applyGrade stores the findings under signals.issues;
|
|
216
|
+
// getTaskGrade copies them to the top level so a reader that does not know that
|
|
217
|
+
// storage detail still sees them. These drive the real function through the
|
|
218
|
+
// deps.pool seam (honoured because NODE_ENV === 'test' above) with a fake row,
|
|
219
|
+
// so dropping or inverting the lift turns this file red.
|
|
220
|
+
function gradeRowPool(row) {
|
|
221
|
+
const calls = [];
|
|
222
|
+
return {
|
|
223
|
+
calls,
|
|
224
|
+
async query(sql, params) {
|
|
225
|
+
calls.push({ sql, params });
|
|
226
|
+
return { rows: row === undefined ? [] : [row] };
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function storedGradeRow(signals) {
|
|
231
|
+
return {
|
|
232
|
+
diff_hash: 'abc123', passed: false, score: 4.5, threshold: 6,
|
|
233
|
+
grader_kind: 'subagent-panel', grader_version: 'v4.0',
|
|
234
|
+
rubric_scores: { functional_fidelity: 9, code_quality: 2 },
|
|
235
|
+
signals, notes_md: 'FAIL',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await test('getTaskGrade lifts signals.issues onto the returned row', async () => {
|
|
240
|
+
const stored = [
|
|
241
|
+
{ severity: 'blocker', file: 'a.js', summary: 'breaks the build', worker: 'quality' },
|
|
242
|
+
{ severity: 'nit', file: 'b.js', summary: 'naming', worker: 'narc' },
|
|
243
|
+
];
|
|
244
|
+
const pool = gradeRowPool(storedGradeRow({ panel_outcome: 'fail', issues: stored }));
|
|
245
|
+
const grade = await db.getTaskGrade(42, { pool });
|
|
246
|
+
assert.deepEqual(grade.issues, stored, 'the stored findings appear at the top level');
|
|
247
|
+
assert.equal(pool.calls.length, 1, 'one read');
|
|
248
|
+
assert.match(pool.calls[0].sql, /FROM task_grades/);
|
|
249
|
+
assert.deepEqual(pool.calls[0].params, [42], 'keyed on the task id');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
await test('getTaskGrade leaves signals.issues in place (the lift is additive)', async () => {
|
|
253
|
+
const stored = [{ severity: 'major', file: 'c.js', summary: 'unchecked input' }];
|
|
254
|
+
const grade = await db.getTaskGrade(42, {
|
|
255
|
+
pool: gradeRowPool(storedGradeRow({ panel_outcome: 'fail', issue_count: 1, issues: stored })),
|
|
256
|
+
});
|
|
257
|
+
assert.deepEqual(grade.signals.issues, stored, 'signals.issues is still there for readers of the nested key');
|
|
258
|
+
assert.equal(grade.signals.panel_outcome, 'fail', 'the rest of signals is untouched');
|
|
259
|
+
assert.equal(grade.signals.issue_count, 1);
|
|
260
|
+
assert.equal(grade.score, 4.5, 'the row columns ride through unchanged');
|
|
261
|
+
assert.deepEqual(grade.rubric_scores, { functional_fidelity: 9, code_quality: 2 });
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await test('getTaskGrade yields issues: [] (not undefined) when signals carries no issues', async () => {
|
|
265
|
+
const grade = await db.getTaskGrade(42, { pool: gradeRowPool(storedGradeRow({ panel_outcome: 'pass' })) });
|
|
266
|
+
assert.ok(Array.isArray(grade.issues), `issues must be an array, got ${typeof grade.issues}`);
|
|
267
|
+
assert.deepEqual(grade.issues, []);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
await test('getTaskGrade yields issues: [] when signals itself is null', async () => {
|
|
271
|
+
const grade = await db.getTaskGrade(42, { pool: gradeRowPool(storedGradeRow(null)) });
|
|
272
|
+
assert.deepEqual(grade.issues, []);
|
|
273
|
+
assert.equal(grade.signals, null, 'a null signals column is passed through, not invented');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
for (const [label, malformed] of [['a string', 'blocker: it is broken'], ['null', null], ['an object', { severity: 'blocker' }]]) {
|
|
277
|
+
await test(`getTaskGrade yields issues: [] when signals.issues is ${label}`, async () => {
|
|
278
|
+
const grade = await db.getTaskGrade(42, { pool: gradeRowPool(storedGradeRow({ issues: malformed })) });
|
|
279
|
+
assert.deepEqual(grade.issues, [], 'a malformed stored value never reaches the top level');
|
|
280
|
+
assert.deepEqual(grade.signals.issues, malformed, 'and the nested key is still left exactly as stored');
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
await test('getTaskGrade returns null when the task has no grade row', async () => {
|
|
285
|
+
assert.equal(await db.getTaskGrade(42, { pool: gradeRowPool(undefined) }), null);
|
|
286
|
+
});
|
|
287
|
+
|
|
213
288
|
// ======================================================================
|
|
214
289
|
// Summary
|
|
215
290
|
// ======================================================================
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// tests/hall_records.mjs — the hall's page records live one file per family (task 1003499).
|
|
2
|
+
//
|
|
3
|
+
// WHY. modules/hall-ui/CLAUDE.md used to carry every page family's "**The X landed
|
|
4
|
+
// next (task N)**" record, each appended immediately above the same heading. So every
|
|
5
|
+
// concurrent hall task inserted at the SAME line and collided with every other one —
|
|
6
|
+
// the file needed five hand-resolved merges in September 2026 alone, and each new
|
|
7
|
+
// record made the next collision likelier. Git cannot conflict on two branches that
|
|
8
|
+
// each ADD a file, so the records moved to modules/hall-ui/records/, one per family,
|
|
9
|
+
// and CLAUDE.md keeps a pointer. This pins the convention, so the next record lands
|
|
10
|
+
// as a file rather than as a fifteenth section of the one file every hall task edits.
|
|
11
|
+
//
|
|
12
|
+
// Run: node tests/hall_records.mjs
|
|
13
|
+
|
|
14
|
+
import { strict as assert } from 'node:assert';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
const DOC = path.join(ROOT, 'modules/hall-ui/CLAUDE.md');
|
|
21
|
+
const DIR = path.join(ROOT, 'modules/hall-ui/records');
|
|
22
|
+
|
|
23
|
+
let passed = 0;
|
|
24
|
+
let failed = 0;
|
|
25
|
+
function test(name, fn) {
|
|
26
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
27
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const doc = fs.readFileSync(DOC, 'utf8').replace(/\r\n/g, '\n');
|
|
31
|
+
const records = fs.existsSync(DIR) ? fs.readdirSync(DIR).filter((f) => f.endsWith('.md')).sort() : [];
|
|
32
|
+
|
|
33
|
+
// The shape every record took: a bold lead-in naming a page family and saying it
|
|
34
|
+
// "landed" (first / next / last / at / on). Anchored to a line start so prose that
|
|
35
|
+
// merely uses the word stays legal.
|
|
36
|
+
const RECORD_LEAD = /^\*\*(?:The|THE)\b[^*\n]*\blanded\b/gm;
|
|
37
|
+
|
|
38
|
+
test('the hall doc carries no page record of its own', () => {
|
|
39
|
+
const leads = doc.match(RECORD_LEAD) || [];
|
|
40
|
+
assert.deepEqual(leads, [],
|
|
41
|
+
'a page record was added to modules/hall-ui/CLAUDE.md — give it its own file in modules/hall-ui/records/ instead (task 1003499)');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('the hall doc points at the records, and says a new one is a new file', () => {
|
|
45
|
+
const at = doc.indexOf('### Page records');
|
|
46
|
+
assert.ok(at > 0, 'the "### Page records" pointer is gone, so nothing tells an agent the records exist');
|
|
47
|
+
const pointer = doc.slice(at, doc.indexOf('\n### ', at + 1));
|
|
48
|
+
assert.match(pointer, /\]\(records\/\)/, 'the pointer links the records directory');
|
|
49
|
+
assert.match(pointer, /head -n 1 modules\/hall-ui\/records\/\*\.md/, 'and says how to find the record for a page');
|
|
50
|
+
assert.match(pointer, /adds a NEW file/, 'and says a new record is a new file, not a new section here');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('the records directory holds the records', () => {
|
|
54
|
+
assert.ok(records.length >= 11, `expected the moved records in modules/hall-ui/records/, found ${records.length}`);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('every record opens with a one-line title naming its pages and its task', () => {
|
|
58
|
+
// `head -n 1 records/*.md` is the index — there is deliberately no hand-kept list,
|
|
59
|
+
// because a list every record appends to would be the same collision in a new place.
|
|
60
|
+
for (const f of records) {
|
|
61
|
+
const text = fs.readFileSync(path.join(DIR, f), 'utf8').replace(/\r\n/g, '\n');
|
|
62
|
+
const [first, blank, ...rest] = text.split('\n');
|
|
63
|
+
assert.match(first, /^# \S/, `${f}: line 1 must be the "# " title that head -n 1 lists`);
|
|
64
|
+
assert.match(first, /\btasks? \d{7}\b/, `${f}: the title names the task(s) the record belongs to`);
|
|
65
|
+
assert.equal(blank, '', `${f}: a blank line follows the title`);
|
|
66
|
+
assert.ok(rest.join('\n').trim().length > 200, `${f}: the record has a body`);
|
|
67
|
+
assert.equal((text.match(/^# /gm) || []).length, 1, `${f}: one title per file — a second one is a second record`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('every relative link in a record resolves from records/', () => {
|
|
72
|
+
// records/ sits one folder below CLAUDE.md, so a section copied out of it with its
|
|
73
|
+
// `../../docs/...` links intact points one level short — the trap this move met.
|
|
74
|
+
for (const f of records) {
|
|
75
|
+
const text = fs.readFileSync(path.join(DIR, f), 'utf8');
|
|
76
|
+
for (const [, href] of text.matchAll(/\]\(([^)\s]+)\)/g)) {
|
|
77
|
+
if (/^(?:[a-z]+:|#|\/)/i.test(href)) continue;
|
|
78
|
+
const target = path.resolve(DIR, href.split('#')[0]);
|
|
79
|
+
assert.ok(fs.existsSync(target), `${f}: link ${href} resolves to nothing (${path.relative(ROOT, target)})`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
console.log(`\nhall page records (task 1003499) tests: ${passed} passed, ${failed} failed`);
|
|
85
|
+
process.exitCode = failed > 0 ? 1 : 0;
|
package/tests/helpers.mjs
CHANGED
|
@@ -987,3 +987,28 @@ export function namesFieldKey(src, key) {
|
|
|
987
987
|
|| new RegExp(`\\.${k}\\b`).test(src)
|
|
988
988
|
|| new RegExp(`=\\s*${k}\\b`).test(src);
|
|
989
989
|
}
|
|
990
|
+
|
|
991
|
+
// macShapedTools — write BSD/macOS-shaped `stat` and `wc` into binDir, for a shell
|
|
992
|
+
// script under test to meet first on PATH (task 1004170).
|
|
993
|
+
//
|
|
994
|
+
// The backup scripts died on every Mac builder's machine and passed on CI's Linux, so
|
|
995
|
+
// the red was invisible to anyone reading Actions. These bring the two differences to
|
|
996
|
+
// any platform: BSD stat refuses GNU's `-c` outright, and BSD wc pads the number it
|
|
997
|
+
// prints (" 11"). Only the stdin forms the scripts use are modelled (`wc -c < f`,
|
|
998
|
+
// `... | wc -l`); any stat call other than `-c` fails loudly rather than pretend to be
|
|
999
|
+
// BSD. Shared because both backup suites need the same pair.
|
|
1000
|
+
export function macShapedTools(binDir) {
|
|
1001
|
+
const realWc = execFileSync('bash', ['-c', 'command -v wc'], { encoding: 'utf8' }).trim();
|
|
1002
|
+
fs.writeFileSync(path.join(binDir, 'stat'), [
|
|
1003
|
+
'#!/bin/bash',
|
|
1004
|
+
'if [ "$1" = -c ]; then echo "stat: illegal option -- c" >&2; exit 1; fi',
|
|
1005
|
+
'echo "macShapedTools: only the -c refusal is modelled, not BSD stat itself" >&2; exit 1',
|
|
1006
|
+
'',
|
|
1007
|
+
].join('\n'), { mode: 0o755 });
|
|
1008
|
+
fs.writeFileSync(path.join(binDir, 'wc'), [
|
|
1009
|
+
'#!/bin/bash',
|
|
1010
|
+
`n=$("${realWc}" "$@") || exit $?`,
|
|
1011
|
+
'printf "%8s\\n" "${n//[[:space:]]/}"',
|
|
1012
|
+
'',
|
|
1013
|
+
].join('\n'), { mode: 0o755 });
|
|
1014
|
+
}
|
|
@@ -24,6 +24,8 @@ import path from 'node:path';
|
|
|
24
24
|
import { fileURLToPath } from 'node:url';
|
|
25
25
|
import { upgradeInvocation } from '../scripts/gds/provision-core-upgrade.js';
|
|
26
26
|
import { migrateCmd, standaloneMigrateCmd, dbName } from '../scripts/gds/provision-repo.js';
|
|
27
|
+
import { CORE_MIGRATE_SCRIPT } from '../scripts/gds/upgrade-migrate.js';
|
|
28
|
+
import { INSTANCE_MIGRATE_SCRIPT } from '../scripts/gds/init.js';
|
|
27
29
|
|
|
28
30
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
31
|
const tenant = { slug: 'charter', db_name: 'charter_db', hosting_shape: 'co-tenant', port: 3107, status: 'active' };
|
|
@@ -35,7 +37,12 @@ test('bongos upgrade migrates before it restarts, and only --skip-migrate opts o
|
|
|
35
37
|
assert.ok(migrate > 0, 'the upgrade flow runs migrate');
|
|
36
38
|
assert.ok(restart > migrate, 'the restart that makes new code live comes AFTER migrate');
|
|
37
39
|
assert.match(src.slice(migrate - 200, migrate), /if \(!opts\.skipMigrate\)/, 'the only way past migrate is the explicit flag');
|
|
38
|
-
|
|
40
|
+
// WHICH command is upgrade-migrate.js's call (task 1004274): the instance's declared script,
|
|
41
|
+
// else the one a scaffold declares — the core migrate.sh either way, so an adopted project with
|
|
42
|
+
// no scripts is migrated rather than rolled back. tests/upgrade_migrate.mjs exercises both.
|
|
43
|
+
assert.match(src, /function runMigrate\([^)]*\) \{\n\s*const \{ cmd, args, env \} = require\('\.\/upgrade-migrate'\)\.migratePlan\(instanceDir\)/,
|
|
44
|
+
'the migrate step spawns what migratePlan decides');
|
|
45
|
+
assert.equal(`bash ${CORE_MIGRATE_SCRIPT}`, INSTANCE_MIGRATE_SCRIPT, 'migrate is the instance scaffold script, i.e. the core migrate.sh');
|
|
39
46
|
});
|
|
40
47
|
|
|
41
48
|
test('the control-plane runner targets the instance database, not its own', () => {
|
package/tests/plain_cards.mjs
CHANGED
|
@@ -167,7 +167,7 @@ test('the sign-in printers, the release refusals and the claim guard each print
|
|
|
167
167
|
'scripts/gds/cli-lib.js': ['session_invalid', 'no_session'],
|
|
168
168
|
'scripts/gds/release.js': ['no_active_claim', 'CLAIM_SESSION_MISMATCH'],
|
|
169
169
|
'scripts/gds/claim.js': ['main_checkout_claim'],
|
|
170
|
-
'scripts/gds/ship-land.js': ['land_pending', 'reconcile_blocked'],
|
|
170
|
+
'scripts/gds/ship-land.js': ['land_pending', 'reconcile_blocked', 'merge_conflict'], // merge_conflict: task 1003499
|
|
171
171
|
'scripts/gds/push-path-brief.js': ['no_push_credential'],
|
|
172
172
|
};
|
|
173
173
|
for (const [file, codes] of Object.entries(wired)) {
|
package/tests/redteam_patrol.mjs
CHANGED
|
@@ -83,7 +83,7 @@ test('disposition: red is reserved for findings that could not be recorded', ()
|
|
|
83
83
|
// A dry run prints; it never claims to have filed, so it never fails.
|
|
84
84
|
assert.deepEqual(p.disposition({ findingClasses: 3, doFile: false, blocker: null }),
|
|
85
85
|
{ action: 'dry-run', exitCode: 0 });
|
|
86
|
-
// Findings filed for adjudication = the patrol working as designed (ADR
|
|
86
|
+
// Findings filed for adjudication = the patrol working as designed (ADR 0158 §2).
|
|
87
87
|
// If this ever became a failure, a weekly red would train everyone to ignore it.
|
|
88
88
|
assert.deepEqual(p.disposition({ findingClasses: 3, doFile: true, blocker: null }),
|
|
89
89
|
{ action: 'file', exitCode: 0 });
|