@bongos/core 1.19.1032 → 1.19.1034
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 +322 -117
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/grade-audit/SKILL.md +1 -1
- package/.claude/skills/grade-recover/SKILL.md +2 -1
- package/.claude/skills/grader-health/SKILL.md +5 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -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/README.md +4 -4
- package/docs/api/openapi.json +1 -1
- package/docs/architecture.md +4 -3
- package/docs/canonical-permissions.md +2 -2
- package/docs/copy-inventory.md +34 -30
- package/docs/copy-registry.json +69 -33
- 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/local-dev.md +45 -3
- package/docs/recipes/packaging-the-core.md +3 -1
- package/docs/recipes/redteam-patrol.md +2 -2
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/modules/autonomy/cadence.js +52 -0
- package/modules/autonomy/gauge.js +57 -9
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/grading/grade-engine.js +267 -0
- package/modules/grading/grader-subagent.js +4 -3
- package/modules/grading/grader-workers/index.js +67 -11
- package/modules/grading/grader-workers/worker-verdict.js +32 -2
- package/modules/grading/grader.js +109 -88
- package/modules/hall-ui/public/deploy.js +35 -0
- package/modules/hall-ui/public/project-settings.css +7 -0
- 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/dependency-advisory.js +2 -1
- package/modules/lifecycle/routes/tasks.js +19 -4
- package/modules/lifecycle/ship-preflight.js +91 -0
- package/modules/provisioning/control-plane-guard.js +14 -13
- package/modules/provisioning/core-upgrade.js +30 -17
- package/modules/provisioning/public-refusal.js +44 -0
- package/modules/provisioning/routes/disconnect.js +2 -1
- package/modules/provisioning/routes/env-manifest.js +5 -4
- package/modules/provisioning/routes/provisioning.js +16 -15
- package/modules/provisioning/routes/repo-private.js +2 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +7481 -0
- package/scripts/gds/address-park-sweep.sh +3 -1
- package/scripts/gds/autobongos-run.js +91 -3
- package/scripts/gds/backfill-cost.js +20 -9
- package/scripts/gds/baseline-staleness.js +1 -1
- package/scripts/gds/claim.js +2 -2
- package/scripts/gds/credential-presence.js +281 -0
- package/scripts/gds/db-backup-nightly.sh +9 -4
- package/scripts/gds/db-backup-offsite.sh +4 -2
- package/scripts/gds/deploy-main-audit-gate.sh +11 -4
- package/scripts/gds/dev-lib.js +212 -5
- package/scripts/gds/dev.js +102 -50
- package/scripts/gds/doctor.js +3 -2
- package/scripts/gds/grade-correlation-audit.js +280 -0
- package/scripts/gds/grade-replay.js +237 -0
- package/scripts/gds/hooks/pre-push.js +7 -3
- package/scripts/gds/main-audit.js +19 -10
- package/scripts/gds/memory-map.js +3 -1
- package/scripts/gds/onboarding-config.js +3 -2
- package/scripts/gds/package-core.js +72 -1
- package/scripts/gds/plain-cards.js +4 -0
- package/scripts/gds/preflight.js +12 -4
- package/scripts/gds/provision-core-upgrade.js +282 -80
- package/scripts/gds/provision-repo-auth.js +15 -0
- package/scripts/gds/provision.js +1 -1
- package/scripts/gds/publish-credential-check.js +3 -0
- package/scripts/gds/push-main.js +14 -3
- package/scripts/gds/push-path-brief.js +3 -2
- package/scripts/gds/redteam-patrol.js +4 -4
- package/scripts/gds/release-notes.js +271 -0
- package/scripts/gds/run-routine.js +18 -5
- package/scripts/gds/run-unit-tests.js +14 -3
- package/scripts/gds/seed-board-room-tasks.js +1 -1
- package/scripts/gds/session-cost.js +12 -3
- package/scripts/gds/ship-deploy-target.js +11 -6
- package/scripts/gds/ship-flow.js +5 -3
- package/scripts/gds/ship-grade-steps.js +33 -0
- package/scripts/gds/ship-land.js +241 -16
- package/scripts/gds/ship-merge.js +3 -2
- package/scripts/gds/ship-preflight-steps.js +86 -11
- package/scripts/gds/ship.js +1 -1
- package/scripts/gds/stray-unit-sweep.sh +3 -1
- package/scripts/gds/upgrade-migrate.js +67 -0
- package/scripts/gds/upgrade.js +9 -7
- package/scripts/hall-preview/README.md +20 -3
- package/scripts/hall-preview/live-identity.js +1 -1
- package/scripts/hall-preview/refresh-fixtures.js +71 -11
- package/scripts/hall-preview/scrub-identity.js +296 -0
- package/src/bongos/routes/security.js +3 -3
- package/src/build-info.js +4 -2
- package/src/module-api.js +1 -1
- package/tests/adr_0089_scope.mjs +70 -0
- package/tests/adr_0162_citation.mjs +184 -0
- package/tests/autobongos_cadence.mjs +74 -0
- package/tests/autobongos_loop.mjs +114 -0
- package/tests/autonomy_gauge.mjs +56 -0
- package/tests/consumer_layout_boot.mjs +363 -31
- package/tests/core_upgrade_door.mjs +40 -6
- package/tests/core_upgrade_runner.mjs +329 -5
- package/tests/credential_presence.mjs +153 -0
- package/tests/db_backup_nightly.mjs +21 -3
- package/tests/db_backup_offsite.mjs +81 -0
- package/tests/deploy_audit_gate.mjs +5 -2
- package/tests/deploy_page_projects.mjs +63 -2
- package/tests/dev_lib.mjs +2 -1
- package/tests/dev_windows.mjs +324 -0
- package/tests/env_knobs_neutral_prefix.mjs +203 -0
- package/tests/fixtures/grade-replay/recorded-panel-grades.json +27 -0
- package/tests/government_protected_surfaces.mjs +10 -0
- package/tests/grade_correlation_audit.mjs +159 -0
- package/tests/grade_engine.mjs +249 -0
- package/tests/grade_engine_replay.mjs +99 -0
- package/tests/grader_prompt_window_bound.mjs +132 -0
- package/tests/grader_unbacked_score.mjs +194 -0
- package/tests/hall_preview_scrub.mjs +214 -0
- 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/integration_parked_ratchet.mjs +24 -1
- package/tests/plain_cards.mjs +1 -1
- package/tests/provisioning_public_refusal.mjs +195 -0
- package/tests/publish_status_branch.mjs +128 -0
- 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_land_red_pr.mjs +236 -0
- package/tests/ship_preflight.mjs +226 -3
- package/tests/start_env_guard.mjs +16 -0
- package/tests/sweep_app_account.mjs +15 -2
- package/tests/upgrade.mjs +20 -1
- package/tests/upgrade_migrate.mjs +195 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// tests/grader_unbacked_score.mjs
|
|
2
|
+
//
|
|
3
|
+
// Task 1002903 — a low score no finding stands behind must reach the builder as
|
|
4
|
+
// what it is, and must still not pass.
|
|
5
|
+
//
|
|
6
|
+
// THE SHAPE. The severity gate (task 1002663, worker-verdict.js
|
|
7
|
+
// applySeverityGatePolicy) downgrades a worker's sub-threshold 'fail' to
|
|
8
|
+
// 'concerns' when no major/blocker finding backs it. The worker keeps its low
|
|
9
|
+
// score, so the grade still read "FAIL 5.5" — and the card said "Fix the issues
|
|
10
|
+
// above" over an empty list. Three defects made that shape:
|
|
11
|
+
// 1. runWorker rebuilt the worker row field by field and dropped the policy's
|
|
12
|
+
// marker, so the stored grade could not say which low score was unbacked;
|
|
13
|
+
// 2. an ADVISORY Narc's unbacked low ff made the panel report PASS while the
|
|
14
|
+
// server's ff gate failed the ship — a PASS verdict inside a FAIL grade;
|
|
15
|
+
// 3. the card had no branch for it, so it printed the ordinary FAIL block.
|
|
16
|
+
// The constraint the task set: close it WITHOUT letting an unbacked low score
|
|
17
|
+
// pass and WITHOUT adding a gate (ADR 0162). Every case below that has a low
|
|
18
|
+
// score also asserts the server still refuses it.
|
|
19
|
+
//
|
|
20
|
+
// Run: node tests/grader_unbacked_score.mjs
|
|
21
|
+
|
|
22
|
+
import { strict as assert } from 'node:assert';
|
|
23
|
+
import { createRequire } from 'node:module';
|
|
24
|
+
|
|
25
|
+
const require = createRequire(import.meta.url);
|
|
26
|
+
const grader = require('../modules/grading/grader.js');
|
|
27
|
+
const { aggregatePanel, validateAndScore, serverSideGuards, THRESHOLD, FALLTHROUGH_CONFIDENCE } = grader;
|
|
28
|
+
const { unbackedLowScores, UNBACKED_SCORE_DOWNGRADE, failResult } = require('../modules/grading/grader-workers/worker-verdict.js');
|
|
29
|
+
const { runWorker, quality, narc, hacker, efficiency } = require('../modules/grading/grader-workers/index.js');
|
|
30
|
+
const { buildGradePayload, formatGradeOutcome } = require('../scripts/gds/ship-grade-steps.js');
|
|
31
|
+
const { CARDS } = require('../scripts/gds/plain-cards.js');
|
|
32
|
+
|
|
33
|
+
let passed = 0;
|
|
34
|
+
let failed = 0;
|
|
35
|
+
async function test(name, fn) {
|
|
36
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
37
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// A REAL worker row: the real worker module's prompt, parser and verdict, run
|
|
41
|
+
// through the real runWorker, with only the subprocess replaced by its output.
|
|
42
|
+
// That is the point — defect 1 lived in runWorker's row assembly, so a
|
|
43
|
+
// hand-built row would have hidden it.
|
|
44
|
+
async function row(worker, output) {
|
|
45
|
+
return runWorker(worker, {
|
|
46
|
+
task: { id: 1002903, title: 't', description: 'd', kind: 'bug', discipline: 'engineer' },
|
|
47
|
+
valueSummary: 'v', diff: '+ const x = 1;', changedFiles: ['a.js'], diffStats: {},
|
|
48
|
+
model: 'claude-sonnet-5', runOpts: {},
|
|
49
|
+
runSubagentFn: async () => ({ stdout: JSON.stringify(output), run_ms: 5, cost_usd: 0 }),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const nit = { severity: 'nit', file: 'a.js', summary: 'comment style' };
|
|
53
|
+
const major = { severity: 'major', file: 'a.js', summary: 'breaks the caller' };
|
|
54
|
+
const ac = [{ ac: 'does the thing', status: 'satisfied', evidence: 'a.js:1' }];
|
|
55
|
+
|
|
56
|
+
const OUT = {
|
|
57
|
+
narcPass: { functional_fidelity: 9, confidence: 0.9, issues: [], notes_md: 'ok', ac_checklist: ac },
|
|
58
|
+
narcUnbacked: { functional_fidelity: 4, confidence: 0.9, issues: [nit], notes_md: 'hm', ac_checklist: ac },
|
|
59
|
+
narcLowConf: { functional_fidelity: 7, confidence: 0.2, issues: [], notes_md: 'unsure', ac_checklist: ac },
|
|
60
|
+
qualityPass: { code_quality: 9, economy: 9, confidence: 0.9, issues: [], notes_md: 'fine' },
|
|
61
|
+
qualityUnbacked: { code_quality: 2, economy: 8, confidence: 0.9, issues: [nit], notes_md: 'meh' },
|
|
62
|
+
qualityBacked: { code_quality: 2, economy: 8, confidence: 0.9, issues: [major], notes_md: 'broken' },
|
|
63
|
+
hackerPass: { security_score: 9, confidence: 0.9, issues: [], notes_md: '', surfaces_checked: ['a.js'] },
|
|
64
|
+
hackerUnbacked: { security_score: 4, confidence: 0.9, issues: [nit], notes_md: '', surfaces_checked: ['a.js'] },
|
|
65
|
+
effPass: { efficiency_score: 9, confidence: 0.9, issues: [], notes_md: '', expensive_patterns_checked: ['loop'] },
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// The server's half: what POST /tasks/:id/grade does to the body ship.js sends —
|
|
69
|
+
// the same two calls the route makes, in the same order.
|
|
70
|
+
function serverGrade(panelResult) {
|
|
71
|
+
const body = buildGradePayload(panelResult, ['a.js']);
|
|
72
|
+
const scored = validateAndScore(body.subagent_output, body.signals);
|
|
73
|
+
return serverSideGuards(scored, { committedFiles: ['a.js'], builderRank: 'archon', diffStats: null });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---- 1. the helper reads only the policy's own marker ---------------------
|
|
77
|
+
|
|
78
|
+
await test('an unbacked low cq is listed; the axes that were fine are not', async () => {
|
|
79
|
+
const q = await row(quality, OUT.qualityUnbacked);
|
|
80
|
+
assert.equal(q.verdict, 'concerns');
|
|
81
|
+
assert.deepEqual(unbackedLowScores([q], { threshold: THRESHOLD }), [{ kind: 'quality', axis: 'code_quality', score: 2 }]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await test('a low score BACKED by a major finding is not unbacked (the worker stays fail)', async () => {
|
|
85
|
+
const q = await row(quality, OUT.qualityBacked);
|
|
86
|
+
assert.equal(q.verdict, 'fail');
|
|
87
|
+
assert.deepEqual(unbackedLowScores([q], { threshold: THRESHOLD }), []);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await test('the exempt Hacker never appears: its unbacked low score stays a hard fail', async () => {
|
|
91
|
+
const h = await row(hacker, OUT.hackerUnbacked);
|
|
92
|
+
assert.equal(h.verdict, 'fail', 'the security lens is fail-closed (task 1002663)');
|
|
93
|
+
assert.deepEqual(unbackedLowScores([h], { threshold: THRESHOLD }), []);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await test('an errored worker and junk input yield []', () => {
|
|
97
|
+
assert.deepEqual(unbackedLowScores([failResult({ notesMd: 'x', error: 'parse_error' })], { threshold: THRESHOLD }), []);
|
|
98
|
+
assert.deepEqual(unbackedLowScores(null, { threshold: THRESHOLD }), []);
|
|
99
|
+
assert.deepEqual(unbackedLowScores('workers', { threshold: THRESHOLD }), []);
|
|
100
|
+
assert.deepEqual(unbackedLowScores([null, 'x', { policy_downgrade: UNBACKED_SCORE_DOWNGRADE, scores: 'junk' }], { threshold: THRESHOLD }), []);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ---- 2. runWorker keeps the marker (defect 1) -----------------------------
|
|
104
|
+
|
|
105
|
+
await test('runWorker carries policy_downgrade onto the worker row', async () => {
|
|
106
|
+
const q = await row(quality, OUT.qualityUnbacked);
|
|
107
|
+
assert.equal(q.policy_downgrade, UNBACKED_SCORE_DOWNGRADE);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await test('an ordinary worker row keeps its exact shape (no policy_downgrade key)', async () => {
|
|
111
|
+
const q = await row(quality, OUT.qualityPass);
|
|
112
|
+
assert.equal('policy_downgrade' in q, false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ---- 3. aggregatePanel records it, and never claims a PASS the server refuses
|
|
116
|
+
|
|
117
|
+
await test('Quality unbacked: the panel records unbacked_scores and names it in the notes', async () => {
|
|
118
|
+
const r = aggregatePanel([await row(narc, OUT.narcPass), await row(quality, OUT.qualityUnbacked), await row(hacker, OUT.hackerPass), await row(efficiency, OUT.effPass)]);
|
|
119
|
+
assert.equal(r.signals.panel_outcome, 'concerns');
|
|
120
|
+
assert.equal(r.passed, false);
|
|
121
|
+
assert.deepEqual(r.signals.unbacked_scores, [{ kind: 'quality', axis: 'code_quality', score: 2 }]);
|
|
122
|
+
assert.match(r.notes_md, /unbacked score: quality code_quality 2\/10/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await test('Narc unbacked low ff: the panel says CONCERNS, not a PASS the server will fail (defect 2)', async () => {
|
|
126
|
+
const r = aggregatePanel([await row(narc, OUT.narcUnbacked), await row(quality, OUT.qualityPass), await row(hacker, OUT.hackerPass), await row(efficiency, OUT.effPass)]);
|
|
127
|
+
assert.equal(r.signals.panel_outcome, 'concerns', 'every gating worker passed, but ff 4 fails the server gate');
|
|
128
|
+
assert.equal(r.passed, false);
|
|
129
|
+
assert.equal(r.signals.confidence, FALLTHROUGH_CONFIDENCE, 'concerns forwards the fall-through confidence');
|
|
130
|
+
assert.match(r.notes_md, /functional_fidelity 4 is below 6/);
|
|
131
|
+
assert.deepEqual(r.signals.unbacked_scores, [{ kind: 'narc', axis: 'functional_fidelity', score: 4 }]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await test('negative control: a Narc concerns with ff at/above threshold still PASSES (the Narc is advisory, task 481)', async () => {
|
|
135
|
+
const r = aggregatePanel([await row(narc, OUT.narcLowConf), await row(quality, OUT.qualityPass), await row(hacker, OUT.hackerPass), await row(efficiency, OUT.effPass)]);
|
|
136
|
+
assert.equal(r.signals.panel_outcome, 'pass');
|
|
137
|
+
assert.equal(r.passed, true);
|
|
138
|
+
assert.equal('unbacked_scores' in r.signals, false, 'an ordinary grade keeps its signals shape');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ---- 4. not loosened: the server still refuses every unbacked low score ----
|
|
142
|
+
|
|
143
|
+
await test('the server still refuses both shapes — an unbacked low score never passes', async () => {
|
|
144
|
+
const qualityShape = aggregatePanel([await row(narc, OUT.narcPass), await row(quality, OUT.qualityUnbacked), await row(hacker, OUT.hackerPass), await row(efficiency, OUT.effPass)]);
|
|
145
|
+
const narcShape = aggregatePanel([await row(narc, OUT.narcUnbacked), await row(quality, OUT.qualityPass), await row(hacker, OUT.hackerPass), await row(efficiency, OUT.effPass)]);
|
|
146
|
+
for (const [label, r] of [['quality', qualityShape], ['narc', narcShape]]) {
|
|
147
|
+
const g = serverGrade(r);
|
|
148
|
+
assert.equal(g.passed, false, `${label}: the server gate must still fail it`);
|
|
149
|
+
assert.ok(Array.isArray(g.signals.unbacked_scores) && g.signals.unbacked_scores.length === 1, `${label}: the list rides into the stored signals`);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ---- 5. the card tells the truth (defect 3) --------------------------------
|
|
154
|
+
|
|
155
|
+
await test('the card reads UNBACKED LOW SCORE, names the reviewer, and never says "fix the issues"', async () => {
|
|
156
|
+
const g = serverGrade(aggregatePanel([await row(narc, OUT.narcPass), await row(quality, OUT.qualityUnbacked), await row(hacker, OUT.hackerPass), await row(efficiency, OUT.effPass)]));
|
|
157
|
+
const out = formatGradeOutcome(g, 1002903);
|
|
158
|
+
assert.match(out, /UNBACKED LOW SCORE/);
|
|
159
|
+
assert.match(out, /quality: code_quality 2\/10 — no major or blocker finding/);
|
|
160
|
+
assert.match(out, /nothing listed to fix/);
|
|
161
|
+
assert.match(out, /ship\.js 1002903 --regrade/);
|
|
162
|
+
assert.match(out, /override-request/);
|
|
163
|
+
assert.doesNotMatch(out, /Fix the issues above/);
|
|
164
|
+
assert.ok(out.includes(CARDS.grade_unbacked_score.happened), 'the plain-language card follows');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await test('the card falls back to signals.workers for a grade recorded before unbacked_scores', async () => {
|
|
168
|
+
const g = serverGrade(aggregatePanel([await row(narc, OUT.narcPass), await row(quality, OUT.qualityUnbacked), await row(hacker, OUT.hackerPass), await row(efficiency, OUT.effPass)]));
|
|
169
|
+
delete g.signals.unbacked_scores;
|
|
170
|
+
assert.match(formatGradeOutcome(g, 7), /quality: code_quality 2\/10/);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await test('the card steps aside when a finding IS substantive — the ordinary FAIL block lists it', async () => {
|
|
174
|
+
const g = serverGrade(aggregatePanel([await row(narc, OUT.narcPass), await row(quality, OUT.qualityUnbacked), await row(hacker, OUT.hackerPass), await row(efficiency, OUT.effPass)]));
|
|
175
|
+
g.issues = [...g.issues, { severity: 'blocker', file: 'b.js', summary: 'real problem', worker: 'hacker' }];
|
|
176
|
+
const out = formatGradeOutcome(g, 7);
|
|
177
|
+
assert.doesNotMatch(out, /UNBACKED LOW SCORE/);
|
|
178
|
+
assert.match(out, /Fix the issues above/);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await test('the card steps aside when a gating worker hard-failed the panel', async () => {
|
|
182
|
+
const g = serverGrade(aggregatePanel([await row(narc, OUT.narcPass), await row(quality, OUT.qualityUnbacked), await row(hacker, OUT.hackerUnbacked), await row(efficiency, OUT.effPass)]));
|
|
183
|
+
assert.equal(g.signals.panel_outcome, 'fail', 'the exempt Hacker hard-fails');
|
|
184
|
+
assert.doesNotMatch(formatGradeOutcome(g, 7), /UNBACKED LOW SCORE/);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await test('a passing grade never gets the branch', async () => {
|
|
188
|
+
const g = serverGrade(aggregatePanel([await row(narc, OUT.narcPass), await row(quality, OUT.qualityPass), await row(hacker, OUT.hackerPass), await row(efficiency, OUT.effPass)]));
|
|
189
|
+
assert.equal(g.passed, true);
|
|
190
|
+
assert.doesNotMatch(formatGradeOutcome(g, 7), /UNBACKED LOW SCORE/);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
194
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// tests/hall_preview_scrub.mjs — the hall-preview fixtures carry stand-ins, never real
|
|
2
|
+
// people (task 1003647).
|
|
3
|
+
//
|
|
4
|
+
// scripts/hall-preview/refresh-fixtures.js snapshots live API responses into COMMITTED
|
|
5
|
+
// fixtures. Before this task it wrote them verbatim, so the repo held real builders'
|
|
6
|
+
// GitHub logins, names, avatars and GitHub ids — and, in me.json, one builder's credit
|
|
7
|
+
// balance and month-to-date spend. The fix lives in the writer (scrub-identity.js, run on
|
|
8
|
+
// every snapshot before it is written), because a one-off clean-up of the output re-leaks
|
|
9
|
+
// on the next refresh. These tests pin:
|
|
10
|
+
// 1. the scheme — one stand-in per person, absence preserved, the account's own money
|
|
11
|
+
// figures fixed, everything that is not a person left alone;
|
|
12
|
+
// 2. that it is a fixed point, which is what lets (4) work;
|
|
13
|
+
// 3. that the WRITER applies it — driven with a fake API into a temp dir, no network;
|
|
14
|
+
// 4. that every fixture checked in today is already scrubbed.
|
|
15
|
+
//
|
|
16
|
+
// Every "real" identity below is invented for the test.
|
|
17
|
+
//
|
|
18
|
+
// Run: node --test tests/hall_preview_scrub.mjs
|
|
19
|
+
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import os from 'node:os';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
30
|
+
const FIXTURES = path.join(ROOT, 'scripts', 'hall-preview', 'fixtures');
|
|
31
|
+
const {
|
|
32
|
+
createScrubber, isOwnAccountFixture, highestStandIn, rewriteScalars, GHOST_AVATAR_URL,
|
|
33
|
+
} = require('../scripts/hall-preview/scrub-identity.js');
|
|
34
|
+
const { refreshFixtures, scrubFixturesOnDisk } = require('../scripts/hall-preview/refresh-fixtures.js');
|
|
35
|
+
|
|
36
|
+
const REAL_AVATAR = 'https://avatars.githubusercontent.com/u/87654321?v=4';
|
|
37
|
+
const liveMe = () => ({
|
|
38
|
+
builder: {
|
|
39
|
+
id: '95', github_id: '87654321', github_login: 'Real-Person', display_name: 'Real Person',
|
|
40
|
+
avatar_url: REAL_AVATAR, total_credits: 4321, karma: 7, rank: 'metic', monthly_budget_usd: null,
|
|
41
|
+
anthropic_email: null,
|
|
42
|
+
},
|
|
43
|
+
cost: { spend_mtd_usd: 123.45, budget_mtd_usd: null },
|
|
44
|
+
active_claims: [{ task_id: '2167', title: 'Check in the hall preview harness', worktree_name: 'frontend-redesign-ux' }],
|
|
45
|
+
});
|
|
46
|
+
const liveDirectory = () => ({
|
|
47
|
+
builders: [
|
|
48
|
+
{ id: '95', github_login: 'real-person', display_name: 'Real Person', avatar_url: REAL_AVATAR },
|
|
49
|
+
{ id: '7', github_login: 'other-human', display_name: '', avatar_url: null, total_credits: 900 },
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
const liveRequests = () => ({
|
|
53
|
+
requests: [{
|
|
54
|
+
id: '3', requested_by: '7', requested_by_login: 'other-human', requested_by_name: 'Other Human',
|
|
55
|
+
requested_by_avatar_url: 'https://avatars.githubusercontent.com/u/1234567?v=4',
|
|
56
|
+
needs_builder_login: null, needs_builder_name: null, needs_builder_avatar_url: null,
|
|
57
|
+
created_by: '1000025', worktree_name: 'keep-this-name',
|
|
58
|
+
}],
|
|
59
|
+
tasks: [{ id: '9', credits_reward: 30, sandbox_review: { by: 'Real-Person', at: '2026-09-01T00:00:00Z' } }],
|
|
60
|
+
gate: [{ number: 12, author: 'other-human', head_sha: 'abc123' }],
|
|
61
|
+
modules: [{ key: 'ideas', author: 'Cloud Bongos' }],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Every string anywhere in a value, for "does this identity survive anywhere" checks.
|
|
65
|
+
function allStrings(v, out = []) {
|
|
66
|
+
if (typeof v === 'string') out.push(v);
|
|
67
|
+
else if (Array.isArray(v)) v.forEach((x) => allStrings(x, out));
|
|
68
|
+
else if (v && typeof v === 'object') Object.values(v).forEach((x) => allStrings(x, out));
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
test('a person becomes ONE stand-in everywhere: login, name, avatar and GitHub id together', () => {
|
|
73
|
+
const s = createScrubber();
|
|
74
|
+
const me = s.scrub(liveMe(), { ownAccount: true });
|
|
75
|
+
const dir = s.scrub(liveDirectory());
|
|
76
|
+
const req = s.scrub(liveRequests());
|
|
77
|
+
|
|
78
|
+
assert.equal(me.builder.github_login, 'builder-1');
|
|
79
|
+
assert.equal(me.builder.display_name, 'Builder 1', 'the name travels with the login beside it');
|
|
80
|
+
assert.equal(me.builder.avatar_url, GHOST_AVATAR_URL);
|
|
81
|
+
assert.equal(me.builder.github_id, '1', 'a GitHub id follows its person, and keeps its type');
|
|
82
|
+
// The same person in another file, spelled with different case, is still person 1.
|
|
83
|
+
assert.equal(dir.builders[0].github_login, 'builder-1');
|
|
84
|
+
assert.equal(dir.builders[0].display_name, 'Builder 1');
|
|
85
|
+
// A second person, first met as a directory row, then as a requester.
|
|
86
|
+
assert.equal(dir.builders[1].github_login, 'builder-2');
|
|
87
|
+
assert.equal(req.requests[0].requested_by_login, 'builder-2');
|
|
88
|
+
assert.equal(req.requests[0].requested_by_name, 'Builder 2', 'a <who>_name pairs with <who>_login');
|
|
89
|
+
assert.equal(req.requests[0].requested_by_avatar_url, GHOST_AVATAR_URL);
|
|
90
|
+
// A login in a `by` field (who approved a staged change) and a PR author.
|
|
91
|
+
assert.equal(req.tasks[0].sandbox_review.by, 'builder-1');
|
|
92
|
+
assert.equal(req.gate[0].author, 'builder-2');
|
|
93
|
+
|
|
94
|
+
const leftovers = [me, dir, req].flatMap((d) => allStrings(d))
|
|
95
|
+
.filter((v) => /real-person|real person|other-human|other human|87654321|1234567/i.test(v));
|
|
96
|
+
assert.deepEqual(leftovers, [], 'no real identity survives anywhere in the output');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('absence is preserved: null stays null and "" stays ""', () => {
|
|
100
|
+
const dir = createScrubber().scrub(liveDirectory());
|
|
101
|
+
assert.equal(dir.builders[1].display_name, '', 'the pages fall back to the login when a name is empty');
|
|
102
|
+
assert.equal(dir.builders[1].avatar_url, null, 'the pages draw initials when there is no avatar');
|
|
103
|
+
const req = createScrubber().scrub(liveRequests());
|
|
104
|
+
assert.equal(req.requests[0].needs_builder_login, null);
|
|
105
|
+
assert.equal(req.requests[0].needs_builder_avatar_url, null);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("the account's own balance and spend become fixed values; other builders keep their numbers", () => {
|
|
109
|
+
const s = createScrubber();
|
|
110
|
+
const me = s.scrub(liveMe(), { ownAccount: true });
|
|
111
|
+
assert.equal(me.builder.total_credits, 1000);
|
|
112
|
+
assert.equal(me.builder.karma, 10);
|
|
113
|
+
assert.equal(me.cost.spend_mtd_usd, 42, 'month-to-date spend is private wherever it appears');
|
|
114
|
+
assert.equal(me.cost.budget_mtd_usd, null, 'an unset budget stays unset');
|
|
115
|
+
const totals = s.scrub({ handle: 'real-person', cross_project: { totals: { credits: 4890, karma: 6, projects: 2 } } }, { ownAccount: true });
|
|
116
|
+
assert.deepEqual(totals.cross_project.totals, { credits: 1000, karma: 10, projects: 2 });
|
|
117
|
+
// Not the account's own file: a stand-in's balance is nobody's, and the leaderboard
|
|
118
|
+
// preview needs real magnitudes to be worth looking at.
|
|
119
|
+
assert.equal(createScrubber().scrub(liveDirectory()).builders[1].total_credits, 900);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('what is not a person is left exactly alone', () => {
|
|
123
|
+
const s = createScrubber();
|
|
124
|
+
const me = s.scrub(liveMe(), { ownAccount: true });
|
|
125
|
+
const req = s.scrub(liveRequests());
|
|
126
|
+
assert.equal(me.builder.id, '95', 'instance-local ids stay — the harness routes on them');
|
|
127
|
+
assert.equal(me.builder.rank, 'metic');
|
|
128
|
+
assert.deepEqual(me.active_claims, liveMe().active_claims, 'task ids, titles and worktree names are project content');
|
|
129
|
+
assert.equal(req.requests[0].worktree_name, 'keep-this-name', 'a <x>_name with no <x>_login is not a person');
|
|
130
|
+
assert.equal(req.requests[0].created_by, '1000025', 'a numeric *_by is a builder id, not a login');
|
|
131
|
+
assert.equal(req.tasks[0].credits_reward, 30, "a task's bounty is not anyone's balance");
|
|
132
|
+
assert.equal(req.modules[0].author, 'Cloud Bongos', "a module's author credit names an organisation");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('scrubbing is a fixed point, and a new run numbers above the stand-ins already in use', () => {
|
|
136
|
+
const s = createScrubber();
|
|
137
|
+
const once = [s.scrub(liveMe(), { ownAccount: true }), s.scrub(liveDirectory()), s.scrub(liveRequests())];
|
|
138
|
+
const again = createScrubber();
|
|
139
|
+
const twice = [again.scrub(once[0], { ownAccount: true }), again.scrub(once[1]), again.scrub(once[2])];
|
|
140
|
+
assert.deepEqual(twice, once);
|
|
141
|
+
|
|
142
|
+
const texts = once.map((d) => JSON.stringify(d));
|
|
143
|
+
assert.equal(highestStandIn(texts), 2);
|
|
144
|
+
const later = createScrubber({ firstNumber: highestStandIn(texts) + 1 }).scrub({ github_login: 'a-newcomer' });
|
|
145
|
+
assert.equal(later.github_login, 'builder-3', 'a newcomer never collides with a stand-in on disk');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('rewriteScalars replaces only the changed values and keeps a hand-written layout', () => {
|
|
149
|
+
const text = '{\n "rows": [ { "github_login": "real-person", "tags": ["a", "b"] },\n { "github_login": null } ],\n "n": 3\n}\n';
|
|
150
|
+
const next = rewriteScalars(text, createScrubber().scrub(JSON.parse(text)));
|
|
151
|
+
assert.equal(next, text.replace('"real-person"', '"builder-1"'));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('isOwnAccountFixture names the snapshotting account\'s own files', () => {
|
|
155
|
+
assert.equal(isOwnAccountFixture('me'), true);
|
|
156
|
+
assert.equal(isOwnAccountFixture('me__credits'), true);
|
|
157
|
+
assert.equal(isOwnAccountFixture('members'), false);
|
|
158
|
+
assert.equal(isOwnAccountFixture('leaderboard'), false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('the WRITER scrubs: refreshFixtures never puts the live identity on disk', async () => {
|
|
162
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hall-fixtures-'));
|
|
163
|
+
try {
|
|
164
|
+
const responses = { '/me': liveMe(), '/builders/directory': liveDirectory(), '/help-requests': liveRequests() };
|
|
165
|
+
const api = { request: async (_m, p) => (p in responses ? { ok: true, status: 200, data: responses[p] } : { ok: false, status: 404 }) };
|
|
166
|
+
const snapshots = [['me', 'me'], ['builders/directory', 'builders__directory'], ['help-requests', 'help-requests'], ['audit-log', 'audit-log']];
|
|
167
|
+
const wrote = await refreshFixtures({ api, dir, snapshots, log: () => {} });
|
|
168
|
+
assert.equal(wrote, 3, 'the 404 is skipped, as before');
|
|
169
|
+
|
|
170
|
+
const onDisk = fs.readdirSync(dir).map((f) => fs.readFileSync(path.join(dir, f), 'utf8')).join('\n');
|
|
171
|
+
for (const real of ['Real-Person', 'real-person', 'Real Person', 'other-human', 'Other Human', '87654321', '1234567', '4321', '123.45']) {
|
|
172
|
+
assert.ok(!onDisk.includes(real), `${real} must never reach the fixture directory`);
|
|
173
|
+
}
|
|
174
|
+
const me = JSON.parse(fs.readFileSync(path.join(dir, 'me.json'), 'utf8'));
|
|
175
|
+
assert.equal(me.builder.github_login, 'builder-1');
|
|
176
|
+
assert.equal(me.builder.total_credits, 1000, 'me.json is the account\'s own file');
|
|
177
|
+
const directory = JSON.parse(fs.readFileSync(path.join(dir, 'builders__directory.json'), 'utf8'));
|
|
178
|
+
assert.equal(directory.builders[0].github_login, 'builder-1', 'one run, one registry: the same person across files');
|
|
179
|
+
} finally {
|
|
180
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('--scrub-only rewrites a hand-written fixture in place, once', () => {
|
|
185
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hall-fixtures-'));
|
|
186
|
+
try {
|
|
187
|
+
const file = path.join(dir, 'connections.json');
|
|
188
|
+
fs.writeFileSync(file, '{ "connections": [ { "handle": "other-human", "display_name": "Other Human", "avatar_url": "https://avatars.githubusercontent.com/u/1234567?v=4" } ] }\n');
|
|
189
|
+
assert.equal(scrubFixturesOnDisk({ dir, log: () => {} }), 1);
|
|
190
|
+
assert.equal(fs.readFileSync(file, 'utf8'),
|
|
191
|
+
`{ "connections": [ { "handle": "builder-1", "display_name": "Builder 1", "avatar_url": "${GHOST_AVATAR_URL}" } ] }\n`);
|
|
192
|
+
assert.equal(scrubFixturesOnDisk({ dir, log: () => {} }), 0, 'a second pass finds nothing to change');
|
|
193
|
+
} finally {
|
|
194
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('every committed fixture is already scrubbed — no real person is checked in', () => {
|
|
199
|
+
const files = fs.readdirSync(FIXTURES).filter((f) => f.endsWith('.json')).sort();
|
|
200
|
+
const texts = files.map((f) => fs.readFileSync(path.join(FIXTURES, f), 'utf8'));
|
|
201
|
+
const s = createScrubber({ firstNumber: highestStandIn(texts) + 1 });
|
|
202
|
+
const dirty = files.filter((file, i) => {
|
|
203
|
+
const data = JSON.parse(texts[i]);
|
|
204
|
+
try {
|
|
205
|
+
assert.deepEqual(s.scrub(data, { ownAccount: isOwnAccountFixture(file.slice(0, -5)) }), data);
|
|
206
|
+
return false;
|
|
207
|
+
} catch {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
assert.deepEqual(dirty, [],
|
|
212
|
+
'these fixtures carry a real login, name, avatar, GitHub id or private figure — run '
|
|
213
|
+
+ '`node scripts/hall-preview/refresh-fixtures.js --scrub-only` and commit the result');
|
|
214
|
+
});
|
|
@@ -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', () => {
|
|
@@ -41,7 +41,18 @@ const { INTEGRATION, INTEGRATION_PARKED, KNOWN_FAILING } = require('../scripts/g
|
|
|
41
41
|
// Two later readings — 2/2 locally on a clean database, and the first CI run after
|
|
42
42
|
// the parking landed — showed the pass was the anomaly. No new breakage was parked
|
|
43
43
|
// here; one straggler joined the cohort it always belonged to.
|
|
44
|
-
|
|
44
|
+
//
|
|
45
|
+
// LOWERED 24 → 23 by task 1002863: consumer_layout_boot was fixed and un-parked, and the
|
|
46
|
+
// ceiling follows it down so the freed slot cannot quietly be spent on a new failure.
|
|
47
|
+
const MAX_PARKED = 23;
|
|
48
|
+
|
|
49
|
+
// The one test that proves the PUBLISHED package works end to end: packaged, installed into
|
|
50
|
+
// a consumer, installed again from its own shipped lockfile, migrated onto EMPTY databases,
|
|
51
|
+
// and serving a database-backed route (tasks 2142 + 1002863 — "PROVE PUBLISH"). Every other
|
|
52
|
+
// test runs the source tree; none installs the package and runs it. So while this one is
|
|
53
|
+
// parked or unselected that guarantee silently does not exist — which is exactly how it sat,
|
|
54
|
+
// parked, from task 1004034 until task 1002863 found out why it failed.
|
|
55
|
+
const PUBLISH_PROOF = 'consumer_layout_boot';
|
|
45
56
|
|
|
46
57
|
test('every parked entry names a test file that exists', () => {
|
|
47
58
|
const ghosts = [...INTEGRATION_PARKED]
|
|
@@ -71,6 +82,18 @@ test('no test is parked in both INTEGRATION_PARKED and KNOWN_FAILING', () => {
|
|
|
71
82
|
'parked in two places at once — pick the mechanism that owns each test.');
|
|
72
83
|
});
|
|
73
84
|
|
|
85
|
+
test('the packaged-core proof is selected by the integration lane and parked nowhere', () => {
|
|
86
|
+
assert.ok(fs.existsSync(path.join(TESTS_DIR, `${PUBLISH_PROOF}.mjs`)),
|
|
87
|
+
`tests/${PUBLISH_PROOF}.mjs is gone — nothing proves the published package installs, migrates and serves any more.`);
|
|
88
|
+
assert.ok(INTEGRATION.has(PUBLISH_PROOF),
|
|
89
|
+
`${PUBLISH_PROOF} is not in the INTEGRATION set, so either the DB-free unit gate would try to run it `
|
|
90
|
+
+ '(it needs npm, a network and a Postgres) or no lane selects it at all.');
|
|
91
|
+
assert.ok(!INTEGRATION_PARKED.has(PUBLISH_PROOF) && !KNOWN_FAILING.has(PUBLISH_PROOF),
|
|
92
|
+
`${PUBLISH_PROOF} is parked. It is the only proof that the PUBLISHED core installs, migrates an `
|
|
93
|
+
+ 'empty database and serves from it; parking it re-opens that gap without a single red check. '
|
|
94
|
+
+ 'Fix the failure instead — or, if parking is truly the call, make it deliberately by editing this guard.');
|
|
95
|
+
});
|
|
96
|
+
|
|
74
97
|
test('the parked count is ratcheted and cannot grow silently', () => {
|
|
75
98
|
const n = INTEGRATION_PARKED.size;
|
|
76
99
|
assert.ok(n <= MAX_PARKED,
|
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)) {
|