@bongos/core 1.19.1081 → 1.20.2
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 +528 -193
- package/.claude/skills/planning-session/SKILL.md +6 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +32 -0
- package/clients/bongos-client/index.cjs +32 -0
- package/clients/bongos-client/index.d.ts +51 -6
- package/clients/bongos-client/index.mjs +32 -0
- package/docs/adr/0099-delayed-redacted-mirror-export.md +5 -1
- package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +17 -0
- package/docs/adr/0176-private-repo-deploy-keys.md +2 -0
- package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
- package/docs/adr/0348-the-web-tier-may-look-read-only-at-what-an-owners-render-key-can-see.md +53 -0
- package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +106 -0
- package/docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md +103 -0
- package/docs/adr/README.md +3 -0
- package/docs/api/openapi.json +851 -19
- package/docs/api-reference.md +33 -9
- package/docs/architecture.md +6 -2
- package/docs/copy-inventory.md +620 -553
- package/docs/copy-registry.json +1467 -820
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +8 -0
- package/docs/modules-contract.md +1 -0
- package/docs/page-inventory.json +38 -4
- package/docs/page-readings.json +1666 -1558
- package/migrations/core_260_goals_working_area.sql +63 -0
- package/migrations/core_261_grade_attempts_verdicts.sql +24 -0
- package/migrations/core_262_drop_builder_box_blocked.sql +33 -0
- package/modules/agents/lib/validate.js +43 -0
- package/modules/autonomy/gauge.js +38 -2
- package/modules/grading/grader-subagent.js +37 -2
- package/modules/grading/grader-workers/reader.js +65 -5
- package/modules/hall-ui/public/approval-queue.css +33 -7
- package/modules/hall-ui/public/approval-queue.js +9 -2
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/brand-holes.js +121 -0
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/collab.js +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +6 -1
- package/modules/hall-ui/public/deploy.js +22 -8
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/dom-utils.js +20 -0
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-inbox.js +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/idea.js +28 -0
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/ideas.js +19 -3
- package/modules/hall-ui/public/index.html +5 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile-nudge.js +21 -4
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/profile.js +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +19 -30
- package/modules/hall-ui/public/settings.js +24 -177
- package/modules/hall-ui/public/settings.states.json +1 -1
- package/modules/hall-ui/public/shell.js +4 -2
- package/modules/hall-ui/public/studio.css +14 -8
- package/modules/hall-ui/public/studio.html +5 -4
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.css +6 -3
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +82 -1
- package/modules/hall-ui/public/tweak-editor.css +142 -11
- package/modules/hall-ui/public/tweak-editor.html +25 -9
- package/modules/hall-ui/public/tweak-editor.js +243 -10
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/approval-queue.md +6 -0
- package/modules/hall-ui/records/tweak-editor.md +6 -0
- package/modules/ideas/ratify.js +36 -8
- package/modules/ideas/routes/ratify-goal.js +6 -0
- package/modules/lifecycle/db-analytics.js +79 -10
- package/modules/lifecycle/db-claims.js +48 -1
- package/modules/lifecycle/db-deps-criteria.js +94 -40
- package/modules/lifecycle/db-goals.js +4 -1
- package/modules/lifecycle/db-grade.js +9 -1
- package/modules/lifecycle/db-relevance-flags.js +183 -0
- package/modules/lifecycle/db-versions.js +4 -3
- package/modules/lifecycle/db.js +19 -0
- package/modules/lifecycle/done-when.js +5 -3
- package/modules/lifecycle/est-advisory.js +31 -4
- package/modules/lifecycle/goal-task-relevance-advisory.js +153 -0
- package/modules/lifecycle/goal-task-relevance-judge.js +226 -0
- package/modules/lifecycle/migrations/lifecycle_014_goal_task_relevance_flags.sql +82 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/routes/claims.js +8 -1
- package/modules/lifecycle/routes/goal-task-relevance.js +72 -0
- package/modules/lifecycle/routes/goals.js +1 -1
- package/modules/lifecycle/routes/tasks.js +27 -24
- package/modules/lifecycle/routes/visuals.js +9 -0
- package/modules/lifecycle/ship-preflight.js +7 -11
- package/modules/lifecycle/task-visuals.js +109 -5
- package/modules/npm-release/module.json +2 -1
- package/modules/npm-release/preview/commands.js +81 -0
- package/modules/npm-release/preview/divert.js +95 -0
- package/modules/npm-release/preview/env.js +78 -0
- package/modules/npm-release/preview/proxy.js +107 -0
- package/modules/npm-release/preview/runtime.js +83 -0
- package/modules/npm-release/preview/supervisor.js +175 -0
- package/modules/npm-release/public/work.js +65 -1
- package/modules/npm-release/routes/preview.js +133 -0
- package/modules/provisioning/app-status.js +83 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/pollers/app-liveness.js +110 -0
- package/modules/provisioning/render-lookup.js +68 -0
- package/modules/provisioning/render-standup.js +44 -1
- package/modules/provisioning/routes/render-standup.js +81 -6
- package/modules/provisioning/starter-bundles.js +4 -1
- package/modules/public-landing/public/account-private.states.json +14 -0
- package/modules/public-landing/public/account.html +513 -0
- package/modules/public-landing/public/account.probes.json +37 -0
- package/modules/public-landing/public/account.states.json +16 -0
- package/modules/public-landing/public/index.html +5 -2
- package/modules/public-landing/public/projects.html +661 -18
- package/modules/public-landing/public/projects.states.json +8 -1
- package/modules/render-deploy/deploys.js +203 -0
- package/modules/render-deploy/migrations/render_deploy_001_app.sql +22 -0
- package/modules/render-deploy/module.json +25 -0
- package/modules/render-deploy/public/deploy.css +10 -0
- package/modules/render-deploy/public/deploy.js +249 -0
- package/modules/render-deploy/render.js +56 -0
- package/modules/render-deploy/routes/act.js +88 -0
- package/modules/render-deploy/routes/app.js +65 -0
- package/modules/render-deploy/routes/history.js +38 -0
- package/modules/specialities/db.js +27 -8
- package/modules/specialities/migrations/specialities_003_skills.sql +46 -0
- package/modules/specialities/routes/specialities.js +51 -3
- package/modules/specialities/skills.js +94 -0
- package/modules/specialities/specialities.js +56 -9
- package/modules/ui-design/kit/fixtures/me-cross-project-private.json +12 -0
- package/modules/ui-design/kit/fixtures/me__cross-project.json +15 -0
- package/modules/ui-design/kit/fixtures/provisioning-instance-render.json +39 -0
- package/modules/ui-design/kit/lib.js +3 -1
- package/modules/ui-design/kit/serve.js +96 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +169 -0
- package/scripts/gds/agents-sync.js +13 -3
- package/scripts/gds/autobongos-run.js +80 -2
- package/scripts/gds/autobongos-service.cmd +12 -0
- package/scripts/gds/copy-apply.js +14 -0
- package/scripts/gds/dev-box-guard.js +3 -2
- package/scripts/gds/fitness.js +9 -0
- package/scripts/gds/grade-correlation-audit.js +42 -12
- package/scripts/gds/grade-replay.js +1 -1
- package/scripts/gds/provision-core-upgrade.js +38 -7
- package/scripts/gds/provision-pin-key.js +176 -0
- package/scripts/gds/provision-render.js +62 -31
- package/scripts/gds/provision-teardown.js +7 -2
- package/scripts/gds/provision-units.js +31 -0
- package/scripts/gds/provision.js +10 -10
- package/scripts/gds/publish-manifest.js +1 -0
- package/scripts/gds/render-api.js +19 -0
- package/scripts/gds/render-payload.js +39 -65
- package/scripts/gds/run-unit-tests.js +10 -0
- package/scripts/gds/ship-finish.js +19 -20
- package/scripts/gds/smoke-dependencies.sh +37 -8
- package/scripts/gds/status.js +12 -4
- package/scripts/gds/upgrade.js +2 -2
- package/scripts/public-mirror-export.js +7 -1
- package/src/bongos/module-scope-map.js +13 -0
- package/src/bongos/serve-internal.js +3 -1
- package/src/module-api.js +10 -1
- package/src/platform-server.js +39 -0
- package/tests/account_privacy_flags.mjs +10 -6
- package/tests/agents_authoring.mjs +15 -1
- package/tests/agents_sync.mjs +106 -5
- package/tests/agents_validate.mjs +45 -0
- package/tests/api_path_404.mjs +6 -0
- package/tests/autobongos_cadence.mjs +7 -0
- package/tests/autobongos_loop.mjs +136 -1
- package/tests/autonomy_gauge.mjs +62 -0
- package/tests/blocker_hall_live_proof.mjs +275 -0
- package/tests/claim_gate_ci_unblock.mjs +116 -0
- package/tests/claim_gate_rebase.mjs +15 -3
- package/tests/collab_page.mjs +10 -0
- package/tests/conductor_main_e2e.mjs +97 -0
- package/tests/core_262_drop_box_blocked_db.mjs +143 -0
- package/tests/core_upgrade_runner.mjs +1 -0
- package/tests/dependency_writes_atomic.mjs +191 -0
- package/tests/deploy_page_projects.mjs +2 -1
- package/tests/effective_visibility_predicate.mjs +15 -0
- package/tests/est_advisory.mjs +27 -0
- package/tests/goal_map_page.mjs +26 -0
- package/tests/goal_task_relevance.mjs +518 -0
- package/tests/goal_working_area.mjs +146 -0
- package/tests/grade_attempts.mjs +61 -0
- package/tests/grade_attribution.mjs +23 -0
- package/tests/grade_correlation_audit.mjs +54 -1
- package/tests/grader_reader_lens.mjs +55 -2
- package/tests/grader_root_outage.mjs +6 -1
- package/tests/grader_subagent_tools_arg.mjs +102 -0
- package/tests/hall_approval_queue.mjs +54 -2
- package/tests/hall_settings_world.mjs +3 -1
- package/tests/hall_tweak_editor.mjs +316 -11
- package/tests/hub_account_page.mjs +394 -0
- package/tests/idea_detail_page.mjs +64 -0
- package/tests/idea_goal_ratification.mjs +64 -0
- package/tests/idea_spark_hall.mjs +60 -9
- package/tests/ideator_full_idea_shapes_space_proof.mjs +1 -1
- package/tests/landing_page.mjs +6 -3
- package/tests/module-scope-map.mjs +31 -8
- package/tests/module_api.mjs +1 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/no_phantom_mirror_workflow.mjs +24 -0
- package/tests/npm_release_preview_commands.mjs +89 -0
- package/tests/npm_release_preview_divert.mjs +164 -0
- package/tests/npm_release_preview_env.mjs +100 -0
- package/tests/npm_release_preview_proxy.mjs +162 -0
- package/tests/npm_release_preview_routes.mjs +208 -0
- package/tests/npm_release_preview_supervisor.mjs +203 -0
- package/tests/pin_write_key.mjs +383 -0
- package/tests/planning_session_skill.mjs +27 -0
- package/tests/platform_boot.mjs +71 -0
- package/tests/profile_nudge_links.mjs +86 -0
- package/tests/profile_ui_cross_project.mjs +1 -1
- package/tests/projects_hub.mjs +2 -2
- package/tests/projects_hub_app_status.mjs +249 -0
- package/tests/projects_hub_app_step.mjs +282 -52
- package/tests/projects_hub_render_connect.mjs +295 -0
- package/tests/provision_render.mjs +121 -28
- package/tests/provision_settings_apply.mjs +62 -0
- package/tests/provisioning_app_status.mjs +204 -0
- package/tests/provisioning_render_route.mjs +126 -6
- package/tests/render_check.mjs +2 -1
- package/tests/render_deploy.mjs +495 -0
- package/tests/rot_inbox.mjs +8 -0
- package/tests/ship_preflight.mjs +61 -8
- package/tests/smoke_dependencies_witness.mjs +46 -0
- package/tests/speciality_skills.mjs +214 -0
- package/tests/studio_room_height.mjs +70 -0
- package/tests/task_detail_includes.mjs +10 -0
- package/tests/task_visuals_instance_root.mjs +150 -0
- package/tests/tweak_batch_apply.mjs +21 -0
- package/tests/ui_design_kit.mjs +36 -4
- package/tests/upgrade.mjs +1 -1
- package/tests/upgrade_persist_pin.mjs +24 -0
package/tests/grade_attempts.mjs
CHANGED
|
@@ -113,6 +113,67 @@ await test('gradeAttemptStats clamps the window (bad input → 7, oversize → 9
|
|
|
113
113
|
assert.deepEqual(seen, [7, 7, 90, 90], 'both queries share the clamped window');
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
+
// ---- task 1004292: per-worker verdicts ride the attempt row ----
|
|
117
|
+
|
|
118
|
+
const WORKERS = [
|
|
119
|
+
{ kind: 'narc', verdict: 'pass', error: null, issues: [] },
|
|
120
|
+
{ kind: 'hacker', verdict: 'concerns', error: null, issues: [{ severity: 'major', summary: 'SECRET PROSE', file: 'a.js' }, { severity: 'minor', summary: 'more prose' }] },
|
|
121
|
+
{ kind: 'quality', verdict: 'fail', error: 'SUBAGENT_TIMEOUT', issues: [], stderr_tail: 'oops' },
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
await test('workerVerdicts keeps kind, verdict, error and severities and NOTHING else', () => {
|
|
125
|
+
const v = db.workerVerdicts(WORKERS);
|
|
126
|
+
assert.deepEqual(v[1], { kind: 'hacker', verdict: 'concerns', error: null, severities: ['major', 'minor'] });
|
|
127
|
+
assert.deepEqual(v[2], { kind: 'quality', verdict: 'fail', error: 'SUBAGENT_TIMEOUT', severities: [] });
|
|
128
|
+
const text = JSON.stringify(v);
|
|
129
|
+
assert.ok(!/PROSE|a\.js|oops|summary|stderr/.test(text), 'no prose, files or stderr');
|
|
130
|
+
assert.equal(db.workerVerdicts(undefined), null);
|
|
131
|
+
assert.equal(db.workerVerdicts([]), null);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await test('workerVerdicts caps the workers and severities it keeps', () => {
|
|
135
|
+
const many = Array.from({ length: db.VERDICT_LIMITS.workers + 5 }, (_, i) => ({ kind: `w${i}`, verdict: 'pass', issues: [] }));
|
|
136
|
+
assert.equal(db.workerVerdicts(many).length, db.VERDICT_LIMITS.workers);
|
|
137
|
+
const sev = [{ kind: 'x', verdict: 'fail', issues: Array.from({ length: db.VERDICT_LIMITS.severities + 9 }, () => ({ severity: 'minor' })) }];
|
|
138
|
+
assert.equal(db.workerVerdicts(sev)[0].severities.length, db.VERDICT_LIMITS.severities);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await test('logGradeAttempt writes worker_verdicts as one jsonb param', async () => {
|
|
142
|
+
const calls = [];
|
|
143
|
+
const r = await db.logGradeAttempt({ taskId: 9, builderId: 1, passed: false, score: 5, workers: WORKERS },
|
|
144
|
+
(text, params) => { calls.push({ text, params }); return Promise.resolve({ rows: [] }); });
|
|
145
|
+
assert.equal(r, true);
|
|
146
|
+
assert.equal(calls.length, 1);
|
|
147
|
+
assert.ok(/worker_verdicts/.test(calls[0].text));
|
|
148
|
+
assert.equal(calls[0].params.length, 5);
|
|
149
|
+
assert.equal(JSON.parse(calls[0].params[4])[1].kind, 'hacker');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await test('a failing worker_verdicts write never costs the attempt row (column not migrated)', async () => {
|
|
153
|
+
const calls = [];
|
|
154
|
+
const r = await db.logGradeAttempt({ taskId: 9, builderId: 1, passed: true, score: 8, workers: WORKERS },
|
|
155
|
+
(text) => {
|
|
156
|
+
calls.push(text);
|
|
157
|
+
if (/worker_verdicts/.test(text)) return Promise.reject(new Error('column "worker_verdicts" does not exist'));
|
|
158
|
+
return Promise.resolve({ rows: [] });
|
|
159
|
+
});
|
|
160
|
+
assert.equal(r, true, 'the plain row still lands');
|
|
161
|
+
assert.equal(calls.length, 2);
|
|
162
|
+
assert.ok(!/worker_verdicts/.test(calls[1]));
|
|
163
|
+
const both = await db.logGradeAttempt({ taskId: 9, passed: true, score: 8, workers: WORKERS }, () => { throw new Error('db down'); });
|
|
164
|
+
assert.equal(both, false, 'and a total failure still never throws');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await test('gradeAttemptVerdicts reads only rounds that kept verdicts, and never throws', async () => {
|
|
168
|
+
let sql;
|
|
169
|
+
const rows = await db.gradeAttemptVerdicts('12', (text, params) => { sql = { text, params }; return Promise.resolve({ rows: [{ passed: false, workers: [] }] }); });
|
|
170
|
+
assert.equal(rows.length, 1);
|
|
171
|
+
assert.ok(/worker_verdicts IS NOT NULL/.test(sql.text) && /ORDER BY attempted_at ASC/.test(sql.text));
|
|
172
|
+
assert.ok(sql.text.includes('LIMIT $2'), 'bounded to the most recent rounds');
|
|
173
|
+
assert.deepEqual(sql.params, [12, db.VERDICT_LIMITS.rounds]);
|
|
174
|
+
assert.deepEqual(await db.gradeAttemptVerdicts(12, () => Promise.reject(new Error('no column'))), []);
|
|
175
|
+
});
|
|
176
|
+
|
|
116
177
|
console.log('');
|
|
117
178
|
console.log(`Grade-attempts (6D.6) tests: ${passed} passed, ${failed} failed`);
|
|
118
179
|
if (failed > 0) process.exit(1);
|
|
@@ -114,4 +114,27 @@ await test('grade: NO claim on the task → falls back to the caller (never refu
|
|
|
114
114
|
assert.equal(out.creditsAwarded, 0, 'still no per-task credit at grader-confirm (lands at ship)');
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
// task 1004292: applyGrade hands the panel's per-worker verdicts to the attempt
|
|
118
|
+
// log (best-effort, post-commit), verdicts only — no prose.
|
|
119
|
+
await test('grade: the attempt row carries the worker verdicts and no prose', async () => {
|
|
120
|
+
const pool = fakePool({ claimHolderId: WORKER, task: TASK });
|
|
121
|
+
const seen = [];
|
|
122
|
+
const attemptQuery = (text, params) => { seen.push({ text, params }); return Promise.resolve({ rows: [] }); };
|
|
123
|
+
const grade = { ...passingGrade, signals: { workers: [
|
|
124
|
+
{ kind: 'hacker', verdict: 'concerns', error: null, issues: [{ severity: 'major', summary: 'PROSE HERE' }] },
|
|
125
|
+
] } };
|
|
126
|
+
await applyGrade({ taskId: TASK.id, builderId: ARCHON, gradeResult: grade }, { pool, attemptQuery });
|
|
127
|
+
assert.equal(seen.length, 1);
|
|
128
|
+
assert.match(seen[0].text, /INSERT INTO grade_attempts .*worker_verdicts/);
|
|
129
|
+
assert.deepEqual(JSON.parse(seen[0].params[4]), [{ kind: 'hacker', verdict: 'concerns', error: null, severities: ['major'] }]);
|
|
130
|
+
assert.ok(!JSON.stringify(seen[0].params).includes('PROSE'));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await test('grade: a failing verdicts write never fails the grade', async () => {
|
|
134
|
+
const pool = fakePool({ claimHolderId: WORKER, task: TASK });
|
|
135
|
+
const grade = { ...passingGrade, signals: { workers: [{ kind: 'hacker', verdict: 'pass', issues: [] }] } };
|
|
136
|
+
const out = await applyGrade({ taskId: TASK.id, builderId: ARCHON, gradeResult: grade }, { pool, attemptQuery: () => Promise.reject(new Error('boom')) });
|
|
137
|
+
assert.equal(out.taskStatus, 'confirmed');
|
|
138
|
+
});
|
|
139
|
+
|
|
117
140
|
summary();
|
|
@@ -148,7 +148,7 @@ test('the CLI reports a saved dataset — refusals by name, and --json — with
|
|
|
148
148
|
fs.writeFileSync(file, JSON.stringify({ grades: [row(1), row(2, { quality: 'fail' })], overrides: [] }));
|
|
149
149
|
try {
|
|
150
150
|
const text = execFileSync(process.execPath, ['scripts/gds/grade-correlation-audit.js', '--file', file], { cwd: ROOT, encoding: 'utf8' });
|
|
151
|
-
assert.match(text, /2 recorded panel
|
|
151
|
+
assert.match(text, /2 recorded panel round\(s\) over 2 task\(s\), minimum n = 30/);
|
|
152
152
|
assert.match(text, /narc × quality\s+REFUSED — n=2 is below the minimum of 30/);
|
|
153
153
|
const json = JSON.parse(execFileSync(process.execPath, ['scripts/gds/grade-correlation-audit.js', '--file', file, '--json'], { cwd: ROOT, encoding: 'utf8' }));
|
|
154
154
|
assert.equal(json.panels, 2);
|
|
@@ -157,3 +157,56 @@ test('the CLI reports a saved dataset — refusals by name, and --json — with
|
|
|
157
157
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
158
158
|
}
|
|
159
159
|
});
|
|
160
|
+
|
|
161
|
+
// ---- task 1004292: earlier rounds ----
|
|
162
|
+
|
|
163
|
+
// A stored attempt: kind, verdict, error, severities. No prose.
|
|
164
|
+
const AW = (kind, verdict, severities = [], error = null) => ({ kind, verdict, error, severities });
|
|
165
|
+
const attempt = (o) => ({ attempted_at: '2026-09-29T00:00:00Z', passed: false, workers: [AW('narc', o.narc || 'pass'), AW('quality', o.quality || 'pass'), AW('hacker', o.hacker || 'pass', o.hs), AW('efficiency', o.efficiency || 'pass')] });
|
|
166
|
+
const gradeOf = (attempts) => ({ passed: true, signals: { workers: [W('narc', 'pass'), W('quality', 'pass'), W('hacker', 'pass'), W('efficiency', 'pass')] }, attempts });
|
|
167
|
+
|
|
168
|
+
test('a stored attempt votes from its severities alone', () => {
|
|
169
|
+
assert.equal(A.voteOf(AW('hacker', 'concerns', ['minor'])), 1);
|
|
170
|
+
assert.equal(A.voteOf(AW('hacker', 'pass')), 0);
|
|
171
|
+
assert.equal(A.voteOf(AW('hacker', 'fail', ['blocker'], 'parse_error')), 1, 'errored but substantive is still an objection');
|
|
172
|
+
assert.equal(A.voteOf(AW('hacker', 'fail', [], 'SUBAGENT_TIMEOUT')), null, 'errored with no finding casts none');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('toAuditRow keeps every round that kept its verdicts; a task with none falls back to the final grade', () => {
|
|
176
|
+
const r = A.toAuditRow({ task_id: 5, task_status: 'shipped', grade: gradeOf([attempt({ hacker: 'concerns', hs: ['major'] }), attempt({})]) });
|
|
177
|
+
assert.equal(r.rounds.length, 2);
|
|
178
|
+
assert.equal(r.rounds[0].find((w) => w.kind === 'hacker').verdict, 'concerns');
|
|
179
|
+
assert.equal(JSON.stringify(r.rounds).includes('summary'), false, 'no prose travels with a round');
|
|
180
|
+
const legacy = A.toAuditRow({ task_id: 6, task_status: 'shipped', grade: gradeOf([]) });
|
|
181
|
+
assert.equal(legacy.rounds, undefined);
|
|
182
|
+
assert.equal(A.panelsOf(legacy).length, 1);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('the audit counts earlier rounds: an objection fixed before the final grade is still a vote', () => {
|
|
186
|
+
// 40 tasks: round 1 the hacker and efficiency both object on half, round 2 (final) all clear.
|
|
187
|
+
const grades = Array.from({ length: 40 }, (_, i) => A.toAuditRow({
|
|
188
|
+
task_id: i, task_status: 'shipped',
|
|
189
|
+
grade: gradeOf([attempt({ hacker: i < 20 ? 'concerns' : 'pass', hs: ['major'], efficiency: i < 20 ? 'concerns' : 'pass' }), attempt({})]),
|
|
190
|
+
}));
|
|
191
|
+
const withRounds = A.audit({ grades, overrides: [] }, { minN: 30 });
|
|
192
|
+
assert.equal(withRounds.panels, 80);
|
|
193
|
+
assert.equal(withRounds.tasks, 40);
|
|
194
|
+
const he = withRounds.pairwise.find((p) => p.a === 'hacker' && p.b === 'efficiency');
|
|
195
|
+
assert.equal(he.refused, undefined, 'computable once earlier rounds are visible');
|
|
196
|
+
assert.equal(he.table.both_object, 20);
|
|
197
|
+
// The same tasks seen through their final grade alone: the objections are gone.
|
|
198
|
+
const finalOnly = A.audit({ grades: grades.map(({ rounds, ...g }) => g), overrides: [] }, { minN: 30 });
|
|
199
|
+
assert.notEqual(finalOnly.pairwise.find((p) => p.a === 'hacker' && p.b === 'efficiency').refused, undefined);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('owner labels compare against the FINAL round only', () => {
|
|
203
|
+
const grades = Array.from({ length: 40 }, (_, i) => A.toAuditRow({
|
|
204
|
+
task_id: i, task_status: 'shipped',
|
|
205
|
+
// round 1 objects (quality), final clears; the owner accepted the final work.
|
|
206
|
+
grade: { ...gradeOf([attempt({ quality: 'concerns' }), attempt({})]), passed: false },
|
|
207
|
+
}));
|
|
208
|
+
const overrides = grades.map((g) => ({ task_id: g.task_id, status: 'approved' }));
|
|
209
|
+
const rep = A.audit({ grades, overrides }, { minN: 30 });
|
|
210
|
+
const q = rep.vs_owner.find((w) => w.kind === 'quality');
|
|
211
|
+
assert.equal(q.n, 40, 'one labelled observation per task, not per round');
|
|
212
|
+
});
|
|
@@ -200,13 +200,66 @@ test('the prompt sends the worker to the written guide, and forbids an uncited f
|
|
|
200
200
|
assert.match(p, /cites no rule id is not a finding/i, "the guide's own rule travels with it");
|
|
201
201
|
// The hard/judgement split is what stops an advisory lens recording taste as
|
|
202
202
|
// fact, so it is pinned rather than left to the prompt's good intentions.
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
// Read from the guide since task 1004075, so the pin is on what the guide
|
|
204
|
+
// says, not on a literal range the guide may outgrow.
|
|
205
|
+
const ids = reader.ruleIdsText(reader.voiceRuleClasses(fs.readFileSync(reader.VOICE_GUIDE, 'utf8')));
|
|
206
|
+
assert.ok(p.includes(`Hard rules (${ids.hard})`), 'hard rules may be asserted: ' + ids.hard);
|
|
207
|
+
assert.ok(p.includes(`Judgement rules (${ids.judgement})`), 'judgement rules may only be flagged: ' + ids.judgement);
|
|
205
208
|
assert.match(p, /ADVISORY/, 'the worker is told it cannot block');
|
|
206
209
|
assert.ok(!/blocker/.test(p.split('## Return ONLY JSON')[1] || ''),
|
|
207
210
|
'the JSON schema offers no blocker severity');
|
|
208
211
|
});
|
|
209
212
|
|
|
213
|
+
// task 1004075: the guide grows by adding rules (its own Part D), so the lens
|
|
214
|
+
// must follow the file rather than a range typed into the prompt.
|
|
215
|
+
test('every rule id in docs/voice.md is a hard rule or a judgement rule, none left over', () => {
|
|
216
|
+
const text = fs.readFileSync(reader.VOICE_GUIDE, 'utf8');
|
|
217
|
+
const c = reader.voiceRuleClasses(text);
|
|
218
|
+
const all = [...text.matchAll(/^## (V\d+)\b/gm)].map((m) => m[1]);
|
|
219
|
+
assert.ok(all.length >= 11, 'the guide still carries its rules, saw ' + all.length);
|
|
220
|
+
assert.deepEqual(c.unclassified, [], 'a rule under neither Part A nor Part B reaches the lens unclassified');
|
|
221
|
+
assert.deepEqual([...c.hard, ...c.judgement].sort(), [...all].sort(), 'every rule is classified exactly once');
|
|
222
|
+
assert.ok(c.hard.length > 0 && c.judgement.length > 0, 'both classes are populated');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('a twelfth rule, or a split rule, reaches the prompt with no change to reader.js', () => {
|
|
226
|
+
const text = fs.readFileSync(reader.VOICE_GUIDE, 'utf8');
|
|
227
|
+
const grown = text.replace(/\n# Part C/, '\n## V12 — A new judgement rule\n\nprose\n\n# Part C');
|
|
228
|
+
assert.notEqual(grown, text, 'fixture: Part C is still where the judgement rules end');
|
|
229
|
+
const c = reader.voiceRuleClasses(grown);
|
|
230
|
+
assert.ok(c.judgement.includes('V12'), 'the new rule is a judgement rule');
|
|
231
|
+
assert.equal(reader.ruleIdsText(c).judgement.endsWith('V12'), true, reader.ruleIdsText(c).judgement);
|
|
232
|
+
// splitting V4 renumbers: the ranges must follow whatever ids the file has
|
|
233
|
+
assert.equal(reader.formatRuleIds(['V1', 'V2', 'V3', 'V4', 'V5', 'V6']), 'V1-V6');
|
|
234
|
+
assert.equal(reader.formatRuleIds(['V7', 'V8', 'V10']), 'V7-V8, V10');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('the PROMPT itself follows the guide: a grown guide reaches the header with no code change', () => {
|
|
238
|
+
// The unit checks above prove the parser; this proves the prompt uses it. A
|
|
239
|
+
// literal range typed back into the header would pass every other case today,
|
|
240
|
+
// because today's literal happens to match the guide. So load the worker
|
|
241
|
+
// against a guide with a V12 and read the header it builds.
|
|
242
|
+
const real = fs.readFileSync(reader.VOICE_GUIDE, 'utf8');
|
|
243
|
+
const grown = real.replace(/\n# Part C/, '\n## V12 — A new judgement rule\n\nprose\n\n# Part C');
|
|
244
|
+
const origRead = fs.readFileSync;
|
|
245
|
+
const key = require.resolve('../modules/grading/grader-workers/reader.js');
|
|
246
|
+
fs.readFileSync = function (p, ...rest) {
|
|
247
|
+
return String(p) === reader.VOICE_GUIDE ? grown : origRead.call(this, p, ...rest);
|
|
248
|
+
};
|
|
249
|
+
delete require.cache[key];
|
|
250
|
+
let fresh;
|
|
251
|
+
try { fresh = require(key); } finally { fs.readFileSync = origRead; delete require.cache[key]; require(key); }
|
|
252
|
+
assert.ok(fresh.PROMPT_HEADER.includes('Judgement rules (V6-V12)'), 'the header carries the grown judgement range');
|
|
253
|
+
assert.ok(fresh.PROMPT_HEADER.includes('Hard rules (V1-V5)'));
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('an unreadable guide makes the prompt name the Parts, never a stale number', () => {
|
|
257
|
+
const ids = reader.ruleIdsText(null);
|
|
258
|
+
assert.match(ids.hard, /Part A/);
|
|
259
|
+
assert.match(ids.judgement, /Part B/);
|
|
260
|
+
assert.ok(!/V\d/.test(ids.hard + ids.judgement));
|
|
261
|
+
});
|
|
262
|
+
|
|
210
263
|
test('docs/voice.md exists — the lens has a standard to judge against', () => {
|
|
211
264
|
// The hard dependency (task 1003976). A copy lens with no written standard
|
|
212
265
|
// records taste and calls it a finding.
|
|
@@ -105,5 +105,10 @@ test('the worker spawn picks a permission mode the CLI accepts as root', () => {
|
|
|
105
105
|
assert.match(src, /'--permission-mode', permissionMode/);
|
|
106
106
|
assert.doesNotMatch(src, /'--permission-mode', 'bypassPermissions'/);
|
|
107
107
|
// The tool list is what makes acceptEdits cost nothing: there are no edits.
|
|
108
|
-
|
|
108
|
+
// task 1001691 gave every caller an opt-out (`opts.tools`, for the ADR 0101
|
|
109
|
+
// judge's zero-tools request), so the literal moved into a default rather
|
|
110
|
+
// than a hardcoded --tools argument — every OTHER caller still resolves to
|
|
111
|
+
// exactly 'Read', which is the property this test cares about.
|
|
112
|
+
assert.match(src, /opts\.tools \?\? 'Read'/);
|
|
113
|
+
assert.match(src, /'--tools', tools/);
|
|
109
114
|
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// tests/grader_subagent_tools_arg.mjs — toolsArgForSpawn, the win32 empty-arg
|
|
2
|
+
// fix behind runClaudeCli's `opts.tools` (task 1001691, round-4 hacker finding).
|
|
3
|
+
//
|
|
4
|
+
// THE BUG THIS PINS. runClaudeCli spawns with `shell: true` on win32 (the
|
|
5
|
+
// claude.cmd shim needs it). Node's shell path on win32 joins argv into ONE
|
|
6
|
+
// command-line string before handing it to cmd.exe, and an empty-string
|
|
7
|
+
// element is silently DROPPED in that join — verified with a real spawn
|
|
8
|
+
// during development (`node -e` reading back its own argv via a stub
|
|
9
|
+
// script): `['--tools', '', '--strict-mcp-config']` arrived at the child as
|
|
10
|
+
// `['--tools', '--strict-mcp-config']`, so `--tools` silently swallowed
|
|
11
|
+
// `--strict-mcp-config` as its value — the opposite of "no tools", and it
|
|
12
|
+
// would have reopened the exact MCP-server exposure task 1000380 closed.
|
|
13
|
+
// `'""'` (two literal quote characters) is the standard Windows workaround:
|
|
14
|
+
// cmd.exe's own argv parsing turns that back into a genuine empty string on
|
|
15
|
+
// the far side — also verified with a real spawn before landing this file.
|
|
16
|
+
//
|
|
17
|
+
// Run: node tests/grader_subagent_tools_arg.mjs
|
|
18
|
+
|
|
19
|
+
import { strict as assert } from 'node:assert';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import os from 'node:os';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { makeRunner } from './helpers.mjs';
|
|
25
|
+
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const subagent = require('../modules/grading/grader-subagent.js');
|
|
28
|
+
const { toolsArgForSpawn } = subagent;
|
|
29
|
+
|
|
30
|
+
const { test, summary } = makeRunner();
|
|
31
|
+
|
|
32
|
+
await test('non-empty tools values pass through unchanged, on either platform', () => {
|
|
33
|
+
assert.equal(toolsArgForSpawn('Read', { shell: true }), 'Read');
|
|
34
|
+
assert.equal(toolsArgForSpawn('Read', { shell: false }), 'Read');
|
|
35
|
+
assert.equal(toolsArgForSpawn('Read,Write', { shell: true }), 'Read,Write');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await test('an empty request on a NON-shell spawn (POSIX) stays a plain empty string', () => {
|
|
39
|
+
assert.equal(toolsArgForSpawn('', { shell: false }), '', 'execve() gets the args array directly — no shell to interpret quoting');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await test('an empty request on a SHELL spawn (win32) is rewritten to the quoted-empty token', () => {
|
|
43
|
+
assert.equal(toolsArgForSpawn('', { shell: true }), '""', 'a bare empty string would be dropped by the shell join, shifting every later arg');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await test('the CLI-argv reconstruction: a bare "" survives Node\'s shell-join where a plain empty string does not', () => {
|
|
47
|
+
// The claim toolsArgForSpawn exists to make true, stated as a source fact
|
|
48
|
+
// (verified empirically during development with a real spawn — see this
|
|
49
|
+
// file's header) rather than re-run as a live child process on every CI
|
|
50
|
+
// machine, which would make this suite depend on a shell being present.
|
|
51
|
+
const rewritten = toolsArgForSpawn('', { shell: true });
|
|
52
|
+
assert.equal(rewritten.length, 2, 'exactly the two quote characters, nothing else');
|
|
53
|
+
assert.equal(rewritten, '"'.repeat(2));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ---- REAL end-to-end: a genuine spawn through runSubagent -> runClaudeCli,
|
|
57
|
+
// against a stub `claude` binary, on THIS host's real platform. The
|
|
58
|
+
// grade_model_provider.mjs precedent stubs a shebang script and SKIPS on
|
|
59
|
+
// win32 ("the fake CLI is a shebang script") — a shebang has no meaning to
|
|
60
|
+
// cmd.exe. This stub is a `.cmd` file instead, which win32 spawns natively,
|
|
61
|
+
// so this test runs WHERE THE BUG LIVED: on win32, through shell: true,
|
|
62
|
+
// through the actual argv this module hands to cmd.exe — not a mock of
|
|
63
|
+
// child_process, and not only the pure function above. ----
|
|
64
|
+
|
|
65
|
+
await test('end-to-end: --tools "" arrives as a genuine empty string, and --strict-mcp-config is never swallowed as its value', async () => {
|
|
66
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'grader-tools-arg-'));
|
|
67
|
+
try {
|
|
68
|
+
let bin;
|
|
69
|
+
if (process.platform === 'win32') {
|
|
70
|
+
bin = path.join(dir, 'claude.cmd');
|
|
71
|
+
const echoJs = path.join(dir, 'echo.js');
|
|
72
|
+
fs.writeFileSync(echoJs,
|
|
73
|
+
"process.stdin.resume();\nprocess.stdin.on('end', () => process.stdout.write(JSON.stringify({ type: 'result', result: JSON.stringify(process.argv.slice(2)), total_cost_usd: 0 })));\n");
|
|
74
|
+
fs.writeFileSync(bin, `@echo off\r\nnode "${echoJs}" %*\r\n`);
|
|
75
|
+
} else {
|
|
76
|
+
bin = path.join(dir, 'claude');
|
|
77
|
+
fs.writeFileSync(bin,
|
|
78
|
+
'#!/usr/bin/env node\nprocess.stdin.resume(); process.stdin.on("end", () => process.stdout.write(JSON.stringify({ type: "result", result: JSON.stringify(process.argv.slice(2)), total_cost_usd: 0 })));\n');
|
|
79
|
+
fs.chmodSync(bin, 0o755);
|
|
80
|
+
}
|
|
81
|
+
// Self-quote `bin` on win32 (round-6 quality finding): shell:true joins
|
|
82
|
+
// [bin, ...args] with spaces before handing the line to cmd.exe, so an
|
|
83
|
+
// unquoted path containing a space (a real risk from a bare os.tmpdir())
|
|
84
|
+
// breaks exactly the same way the trap this file exists to test does —
|
|
85
|
+
// verified empirically (a real spawn against a deliberately spacey temp
|
|
86
|
+
// dir) that wrapping the executable path in quotes fixes it, mirroring
|
|
87
|
+
// how any real ops-configured CLAUDE_BIN with a space would also need
|
|
88
|
+
// quoting. Production's own default (`bin = 'claude'`, a bare PATH
|
|
89
|
+
// lookup) never hits this, so it stays out of runClaudeCli itself.
|
|
90
|
+
const claudeBin = process.platform === 'win32' ? `"${bin}"` : bin;
|
|
91
|
+
const r = await subagent.runSubagent({ prompt: 'hi', opts: { claudeBin, tools: '', model: 'claude-sonnet-5' } });
|
|
92
|
+
const argv = JSON.parse(subagent.extractAssistantResult(r.stdout));
|
|
93
|
+
const toolsIdx = argv.indexOf('--tools');
|
|
94
|
+
assert.ok(toolsIdx >= 0, '--tools made it into argv at all');
|
|
95
|
+
assert.equal(argv[toolsIdx + 1], '', 'the value right after --tools is a genuine empty string');
|
|
96
|
+
assert.ok(argv.includes('--strict-mcp-config'), '--strict-mcp-config is its own argv element, never swallowed as --tools\' value');
|
|
97
|
+
} finally {
|
|
98
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
summary();
|
|
@@ -101,9 +101,10 @@ function bootPanel({ answers = {}, writes = null, mode = 'light' } = {}) {
|
|
|
101
101
|
return val.__status ? { ok: false, status: val.__status, data: val.data } : { ok: true, status: 200, data: val };
|
|
102
102
|
},
|
|
103
103
|
};
|
|
104
|
-
const win = { matchMedia: () => ({ matches: false }) };
|
|
104
|
+
const win = { matchMedia: () => ({ matches: false }), __BRANDING__: { identity: { productName: 'Cloud Bongos', worldName: 'the studio' } } };
|
|
105
105
|
const ctx = vm.createContext({ window: win, document: documentObj, console, setTimeout, clearTimeout, encodeURIComponent });
|
|
106
106
|
win.window = win;
|
|
107
|
+
vm.runInContext(read('brand-holes.js'), ctx, { filename: 'brand-holes.js' });
|
|
107
108
|
vm.runInContext(read('approval-queue-lib.js'), ctx, { filename: 'approval-queue-lib.js' });
|
|
108
109
|
vm.runInContext(read('approval-queue.js'), ctx, { filename: 'approval-queue.js' });
|
|
109
110
|
const panel = win.OTBApprovalQueue.mount({
|
|
@@ -320,8 +321,59 @@ await test('the panel writes only through the writes it is handed; the studio ha
|
|
|
320
321
|
assert.match(studio, /sendBack: \(pageId\) => `\$\{API\}\/copy-desk\/pages\/\$\{pageId\}\/send-back`,/);
|
|
321
322
|
assert.match(studio, /writes: \{ approve: WRITE_ROUTES\.approve, sendBack: WRITE_ROUTES\.sendBack \}/);
|
|
322
323
|
const html = read('studio.html');
|
|
323
|
-
assert.ok(html.indexOf('approval-queue-lib.js') < html.indexOf('approval-queue.js
|
|
324
|
+
assert.ok(html.indexOf('brand-holes.js') < html.indexOf('approval-queue.js?') && html.indexOf('approval-queue-lib.js') < html.indexOf('approval-queue.js?') && html.indexOf('approval-queue.js?') < html.indexOf('studio.js?'), 'the holes and the lib, the panel, then the studio');
|
|
324
325
|
assert.match(html, /id="studio-approvals"[^>]*aria-haspopup="dialog"/);
|
|
325
326
|
});
|
|
326
327
|
|
|
328
|
+
// ── task 1004381: brand holes read as the name, and the panel fits the screen ──
|
|
329
|
+
|
|
330
|
+
await test('a line holding {{productName}} reads as the product name in a chip, in the line and in its was:', async () => {
|
|
331
|
+
const tasks = [round({
|
|
332
|
+
id: 43, pageId: 'landing:index', n: 2, appliedAt: '2026-09-21T00:00:00Z',
|
|
333
|
+
lines: [{ key: 'L0005', section: 'contact', before: 'Both are how {{productName}} itself works.', after: 'This project, {{productName}}, has two doors.' }],
|
|
334
|
+
})];
|
|
335
|
+
const b = bootPanel({ answers: { [APPROVALS]: queueFor({ tasks }), [vis(43)]: slotsFor(43) } });
|
|
336
|
+
await b.panel.open();
|
|
337
|
+
await settle(); await settle();
|
|
338
|
+
// The mini-DOM's textContent does not keep a text node's place between
|
|
339
|
+
// children, so the words are read in order off the painted markup.
|
|
340
|
+
const words = (open, close) => {
|
|
341
|
+
const h = b.q('.aq-dialog').innerHTML;
|
|
342
|
+
const at = h.indexOf(open);
|
|
343
|
+
assert.ok(at !== -1, `painted: ${open}`);
|
|
344
|
+
return h.slice(at + open.length, h.indexOf(close, at)).replace(/<[^>]+>/g, '');
|
|
345
|
+
};
|
|
346
|
+
const lineWords = () => words('<span class="aq-line__text">', '</span>\n');
|
|
347
|
+
const asideWords = () => words('<span class="aq-line__aside">', '</span>\n');
|
|
348
|
+
assert.equal(lineWords(), 'This project, Cloud Bongos, has two doors.');
|
|
349
|
+
assert.equal(asideWords(), 'was: Both are how Cloud Bongos itself works.');
|
|
350
|
+
const chips = b.qa('.aq-hole');
|
|
351
|
+
assert.equal(chips.length, 2, 'one chip in the line, one in its was:');
|
|
352
|
+
assert.ok(chips.every((c) => c.attrs['data-hole'] === 'productName' && text(c) === 'Cloud Bongos'));
|
|
353
|
+
assert.ok(b.q('mark').querySelector('.aq-hole'), 'the chip sits inside the changed words\' mark');
|
|
354
|
+
assert.ok(!/\{\{/.test(b.scrim.innerHTML || '') && !/\{\{/.test(text(b.scrim)), 'no raw {{token}} anywhere in the panel');
|
|
355
|
+
b.scrim.fire('click', { target: b.q('[data-phase="before"]') }); await settle();
|
|
356
|
+
assert.equal(lineWords(), 'Both are how Cloud Bongos itself works.');
|
|
357
|
+
assert.equal(asideWords(), 'becomes: This project, Cloud Bongos, has two doors.');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
await test('the dialog fits the viewport at every width: capped by the screen, the page shrinks first, rows wrap', () => {
|
|
361
|
+
const css = read('approval-queue.css');
|
|
362
|
+
const rule = (sel) => {
|
|
363
|
+
const at = css.indexOf(`\n${sel} {`);
|
|
364
|
+
assert.ok(at !== -1, `a ${sel} rule`);
|
|
365
|
+
return css.slice(at, css.indexOf('}', at));
|
|
366
|
+
};
|
|
367
|
+
const dialog = rule('.aq-dialog');
|
|
368
|
+
assert.match(dialog, /width: min\(1180px, 100%, calc\(100vw - 48px\)\)/, 'never wider than the screen less its gutters');
|
|
369
|
+
assert.match(dialog, /height: min\(820px, 100%, calc\(100dvh - 48px\)\)/, 'never taller than the screen');
|
|
370
|
+
assert.match(dialog, /min-width: 0/);
|
|
371
|
+
assert.match(rule('.aq-head'), /flex-wrap: wrap/, 'the header wraps rather than running off the edge');
|
|
372
|
+
assert.match(rule('.aq-foot'), /flex-wrap: wrap/);
|
|
373
|
+
assert.match(rule('.aq-body'), /min-width: 0/);
|
|
374
|
+
assert.match(rule('.aq-frame'), /flex-grow: 1;[\s\S]*min-width: 0/, 'the rendered page is what shrinks first');
|
|
375
|
+
assert.match(rule('.aq-lines'), /width: clamp\(240px, 32%, 300px\)/, 'the words keep the artboard\'s 300 until the page has shrunk');
|
|
376
|
+
assert.match(rule('.aq-line__text'), /overflow-wrap: anywhere/, 'a long word breaks inside its column');
|
|
377
|
+
});
|
|
378
|
+
|
|
327
379
|
summary();
|
|
@@ -133,7 +133,9 @@ test('a toggle is a role=switch with aria-checked, and the page script keeps dat
|
|
|
133
133
|
// toggle in task 1002518 (four rungs, not on/off), so it became a radio group
|
|
134
134
|
// in settings-render.js — covered by the radio assertions and the wiring test
|
|
135
135
|
// below, not by the switch contract.
|
|
136
|
-
|
|
136
|
+
// renderCrossProject and privacyRow left with their sections (task 1004259):
|
|
137
|
+
// the hub's account page owns those settings now, and the hall links out.
|
|
138
|
+
for (const fnName of ['renderSounds']) {
|
|
137
139
|
const body = settingsJs.match(new RegExp(`function ${fnName}\\([\\s\\S]*?\\n \\}\\n`));
|
|
138
140
|
assert.ok(body && /switchHtml\(/.test(body[0]), `${fnName} renders the switch`);
|
|
139
141
|
}
|