@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
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// tests/speciality_skills.mjs — a speciality OFFERS skills; each adopter's
|
|
2
|
+
// ENABLED set starts empty and holds only offered skills this instance has
|
|
3
|
+
// (task 1004072, part 1 of 3 of ADR 0310).
|
|
4
|
+
//
|
|
5
|
+
// Three altitudes: the disk reader against a temp tree, the pure rules, and the
|
|
6
|
+
// real routes/specialities.js router with the db functions stubbed at the
|
|
7
|
+
// module boundary, so the wiring (which set is checked where) is exercised and
|
|
8
|
+
// not only the rule.
|
|
9
|
+
//
|
|
10
|
+
// Run: node tests/speciality_skills.mjs
|
|
11
|
+
|
|
12
|
+
import { strict as assert } from 'node:assert';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import http from 'node:http';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { makeRunner } from './helpers.mjs';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
const S = require('../modules/specialities/specialities.js');
|
|
24
|
+
const skills = require('../modules/specialities/skills.js');
|
|
25
|
+
const db = require('../modules/specialities/db.js');
|
|
26
|
+
const api = require('../src/module-api.js');
|
|
27
|
+
const { test, summary } = makeRunner();
|
|
28
|
+
|
|
29
|
+
// ---- 1. the skills this instance has, read from disk -----------------------
|
|
30
|
+
function tree() {
|
|
31
|
+
const t = fs.mkdtempSync(path.join(os.tmpdir(), 'spec-skills-'));
|
|
32
|
+
const put = (rel, body) => { const p = path.join(t, rel); fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, body); };
|
|
33
|
+
put('core/.claude/skills/recall/SKILL.md', '---\nname: recall\ndescription: Search what the project already knows.\n---\nbody');
|
|
34
|
+
put('core/.claude/skills/status/SKILL.md', '---\ndescription: "Roll up a version."\n---\n');
|
|
35
|
+
put('core/.claude/skills/not-a-skill/README.md', 'no SKILL.md here');
|
|
36
|
+
put('core/.claude/skills/Bad_Name/SKILL.md', '---\ndescription: x\n---\n');
|
|
37
|
+
put('inst/.claude/skills/house-style/SKILL.md', '---\ndescription: The instance\'s own.\n---\n');
|
|
38
|
+
put('core/modules/on-mod/skills/costing/SKILL.md', '---\ndescription: Price a thing.\n---\n');
|
|
39
|
+
put('core/modules/off-mod/skills/secret/SKILL.md', '---\ndescription: Disabled module.\n---\n');
|
|
40
|
+
return t;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await test('the instance\'s skills are the core\'s, the instance\'s and ENABLED modules\' — nothing else', () => {
|
|
44
|
+
const t = tree();
|
|
45
|
+
try {
|
|
46
|
+
const got = skills.installedSkills({ coreRoot: path.join(t, 'core'), instanceRoot: path.join(t, 'inst'), isModuleEnabled: (k) => k === 'on-mod' });
|
|
47
|
+
assert.deepEqual([...got.keys()].sort(), ['costing', 'house-style', 'recall', 'status']);
|
|
48
|
+
assert.equal(got.get('recall').description, 'Search what the project already knows.');
|
|
49
|
+
assert.equal(got.get('status').description, 'Roll up a version.', 'a quoted description is unquoted');
|
|
50
|
+
assert.equal(got.get('costing').source, 'module:on-mod');
|
|
51
|
+
assert.ok(!got.has('secret'), 'a disabled module\'s skill is not this instance\'s');
|
|
52
|
+
assert.ok(!got.has('not-a-skill'), 'a directory with no SKILL.md is not a skill');
|
|
53
|
+
} finally { fs.rmSync(t, { recursive: true, force: true }); }
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await test('the real tree resolves: the core ships skills a speciality can offer', () => {
|
|
57
|
+
const got = skills.installedSkills({ coreRoot: ROOT, isModuleEnabled: () => false });
|
|
58
|
+
assert.ok(got.has('recall') && got.has('status'), 'core skills are found in this checkout');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await test('an unreadable root contributes nothing rather than throwing', () => {
|
|
62
|
+
const got = skills.installedSkills({ coreRoot: path.join(os.tmpdir(), 'no-such-root-' + Date.now()), isModuleEnabled: () => { throw new Error('boom'); } });
|
|
63
|
+
assert.equal(got.size, 0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ---- 2. the pure rules ------------------------------------------------------
|
|
67
|
+
const INSTALLED = new Set(['recall', 'status', 'costing']);
|
|
68
|
+
const draft = (over = {}) => ({ name: 'Economics', discipline: 'ideator', owner_kind: 'builder', owner_builder_id: 1, ...over });
|
|
69
|
+
const codes = (r) => r.errors.map((e) => e.code);
|
|
70
|
+
|
|
71
|
+
await test('a speciality may offer skills this instance has', () => {
|
|
72
|
+
const r = S.validateDraft(draft({ skills: ['recall', 'costing'] }), { disciplines: ['ideator'], installedSkills: INSTALLED });
|
|
73
|
+
assert.equal(r.ok, true, JSON.stringify(r.errors));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await test('a skill this instance does NOT have is refused, by name', () => {
|
|
77
|
+
const r = S.validateDraft(draft({ skills: ['recall', 'teleport'] }), { disciplines: ['ideator'], installedSkills: INSTALLED });
|
|
78
|
+
assert.equal(r.ok, false);
|
|
79
|
+
const e = r.errors.find((x) => x.code === 'unknown_skill');
|
|
80
|
+
assert.ok(e, JSON.stringify(r.errors));
|
|
81
|
+
assert.deepEqual(e.names, ['teleport']);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await test('a non-empty offered set nothing could check is refused, never admitted', () => {
|
|
85
|
+
const r = S.validateDraft(draft({ skills: ['recall'] }), { disciplines: ['ideator'] });
|
|
86
|
+
assert.ok(codes(r).includes('uncheckable'));
|
|
87
|
+
// and an empty set needs no check
|
|
88
|
+
assert.equal(S.validateDraft(draft({ skills: [] }), { disciplines: ['ideator'] }).ok, true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await test('shape: not a list, a bad name, a duplicate, too many', () => {
|
|
92
|
+
const v = (skillsList) => codes(S.validateDraft(draft({ skills: skillsList }), { disciplines: ['ideator'], installedSkills: INSTALLED }));
|
|
93
|
+
assert.ok(v('recall').includes('invalid'));
|
|
94
|
+
assert.ok(v(['Recall']).includes('invalid_name'));
|
|
95
|
+
assert.ok(v(['recall', 'recall']).includes('duplicate'));
|
|
96
|
+
const many = Array.from({ length: S.LIMITS.SKILLS + 1 }, (_, i) => `s${i}`);
|
|
97
|
+
assert.ok(v(many).includes('too_many'));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await test('an adopter may enable only skills the speciality OFFERS and the instance HAS', () => {
|
|
101
|
+
const sp = { skills: ['recall', 'status'] };
|
|
102
|
+
assert.deepEqual(S.enabledSkillsErrors(['recall'], sp, INSTALLED), []);
|
|
103
|
+
assert.deepEqual(S.enabledSkillsErrors([], sp, INSTALLED), [], 'turning everything off is a legal choice');
|
|
104
|
+
const notOffered = S.enabledSkillsErrors(['costing'], sp, INSTALLED);
|
|
105
|
+
assert.ok(notOffered.some((e) => e.code === 'not_offered' && e.names.includes('costing')), 'installed but not offered');
|
|
106
|
+
const gone = S.enabledSkillsErrors(['recall'], { skills: ['recall'] }, new Set(['status']));
|
|
107
|
+
assert.ok(gone.some((e) => e.code === 'unknown_skill'), 'offered but removed from the instance since');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ---- 3. the storage: a NEW adoption's enabled set is EMPTY, as stored --------
|
|
111
|
+
await test('the migration stores an EMPTY enabled set by default, not a UI default', () => {
|
|
112
|
+
const sql = fs.readFileSync(path.join(ROOT, 'modules/specialities/migrations/specialities_003_skills.sql'), 'utf8');
|
|
113
|
+
const ddl = sql.split('\n').filter((l) => !/^\s*--/.test(l)).join('\n');
|
|
114
|
+
assert.match(ddl, /ALTER TABLE specialities_adoptions\s+ADD COLUMN IF NOT EXISTS enabled_skills TEXT\[\] NOT NULL DEFAULT '\{\}'/);
|
|
115
|
+
assert.match(ddl, /ALTER TABLE specialities_catalog\s+ADD COLUMN IF NOT EXISTS skills TEXT\[\] NOT NULL DEFAULT '\{\}'/);
|
|
116
|
+
assert.ok(!/\bDROP (TABLE|COLUMN)\b/i.test(ddl), 'additive');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await test('adopting never writes the enabled set, so every new adoption takes the empty default', async () => {
|
|
120
|
+
const seen = [];
|
|
121
|
+
const exec = async (text, params) => { seen.push({ text, params }); return { rows: [{ builder_id: 1, speciality_id: 9, enabled_skills: [] }], rowCount: 1 }; };
|
|
122
|
+
const row = await db.adopt({ builderId: 1, specialityId: 9, discipline: 'ideator' }, exec);
|
|
123
|
+
const insert = seen.find((q) => /INSERT INTO specialities_adoptions/.test(q.text));
|
|
124
|
+
assert.ok(insert, 'the adopt INSERT ran');
|
|
125
|
+
assert.ok(!/enabled_skills\s*[,)]/.test(insert.text.split('VALUES')[0]), 'the INSERT column list does not name enabled_skills');
|
|
126
|
+
assert.ok(!/DO UPDATE SET((?!RETURNING)[\s\S])*enabled_skills/.test(insert.text), 're-adopting does not overwrite a person\'s earlier choices either');
|
|
127
|
+
assert.deepEqual(row.enabled_skills, []);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await test('the enabled set is written only on the caller\'s own ACTIVE adoption', async () => {
|
|
131
|
+
let q;
|
|
132
|
+
const exec = async (text, params) => { q = { text, params }; return { rows: [], rowCount: 0 }; };
|
|
133
|
+
const r = await db.setEnabledSkills(1, 9, ['recall'], exec);
|
|
134
|
+
assert.equal(r, null, 'no active adoption, nothing written');
|
|
135
|
+
assert.match(q.text, /WHERE builder_id = \$1 AND speciality_id = \$2 AND active/);
|
|
136
|
+
assert.deepEqual(q.params, [1, 9, ['recall']]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ---- 4. the real router -----------------------------------------------------
|
|
140
|
+
const store = {
|
|
141
|
+
speciality: { id: 9, name: 'Economics', discipline: 'ideator', owner_kind: 'builder', owner_builder_id: 1, visibility: 'public', sellable: false, skills: ['recall', 'status'] },
|
|
142
|
+
adoption: null, created: null, updated: null,
|
|
143
|
+
};
|
|
144
|
+
db.getSpeciality = async () => ({ ...store.speciality });
|
|
145
|
+
db.setEnabledSkills = async (b, s, list) => (store.adoption ? { ...store.adoption, enabled_skills: list } : null);
|
|
146
|
+
db.createSpeciality = async (row) => { store.created = row; return { id: 10, ...row }; };
|
|
147
|
+
db.updateSpeciality = async (id, patch) => { store.updated = patch; return { ...store.speciality, ...patch }; };
|
|
148
|
+
db.countTraining = async () => ({ documentCount: 0, learningCount: 0 });
|
|
149
|
+
skills.installedSkillsCached = () => new Map([['recall', {}], ['status', {}], ['costing', {}]]);
|
|
150
|
+
api.requireBuilder = (req, res, next) => { req.builder = { id: 1, rank: 'metic' }; next(); };
|
|
151
|
+
api.enabledDisciplines = () => ['ideator', 'engineer', 'artist'];
|
|
152
|
+
|
|
153
|
+
const express = require('express');
|
|
154
|
+
const app = express();
|
|
155
|
+
app.use(express.json());
|
|
156
|
+
app.use((req, res, next) => { res.fail = (code, st, extra) => res.status(typeof st === 'number' ? st : (st && st.status) || 500).json({ error: code, ...(st && typeof st === 'object' ? st : {}), ...(extra || {}) }); next(); });
|
|
157
|
+
app.use(require('../modules/specialities/routes/specialities.js')());
|
|
158
|
+
const server = http.createServer(app);
|
|
159
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
160
|
+
const base = `http://127.0.0.1:${server.address().port}`;
|
|
161
|
+
const call = async (method, url, body) => {
|
|
162
|
+
const r = await fetch(base + url, { method, headers: { 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined });
|
|
163
|
+
return { status: r.status, body: await r.json() };
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
await test('POST /specialities stores an offered set of installed skills', async () => {
|
|
168
|
+
const r = await call('POST', '/specialities', { name: 'Economics', discipline: 'ideator', skills: ['recall', 'costing'] });
|
|
169
|
+
assert.equal(r.status, 201, JSON.stringify(r.body));
|
|
170
|
+
assert.deepEqual(store.created.skills, ['recall', 'costing']);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await test('POST /specialities refuses a skill the instance does not have', async () => {
|
|
174
|
+
store.created = null;
|
|
175
|
+
const r = await call('POST', '/specialities', { name: 'Econ2', discipline: 'ideator', skills: ['teleport'] });
|
|
176
|
+
assert.equal(r.status, 400);
|
|
177
|
+
assert.ok(JSON.stringify(r.body).includes('unknown_skill'));
|
|
178
|
+
assert.equal(store.created, null, 'nothing was written');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await test('PATCH that does not send skills is not blocked by a skill removed since', async () => {
|
|
182
|
+
store.speciality.skills = ['recall', 'gone-now'];
|
|
183
|
+
const r = await call('PATCH', '/specialities/9', { name: 'Economics renamed' });
|
|
184
|
+
assert.equal(r.status, 200, JSON.stringify(r.body));
|
|
185
|
+
const bad = await call('PATCH', '/specialities/9', { skills: ['recall', 'gone-now'] });
|
|
186
|
+
assert.equal(bad.status, 400, 'but sending a list is held to what exists now');
|
|
187
|
+
store.speciality.skills = ['recall', 'status'];
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
await test('PUT /specialities/:id/skills needs an active adoption', async () => {
|
|
191
|
+
store.adoption = null;
|
|
192
|
+
const r = await call('PUT', '/specialities/9/skills', { enabled_skills: ['recall'] });
|
|
193
|
+
assert.equal(r.status, 409);
|
|
194
|
+
assert.equal(r.body.error, 'not_adopted');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await test('PUT /specialities/:id/skills stores offered, installed skills, and refuses others', async () => {
|
|
198
|
+
store.adoption = { builder_id: 1, speciality_id: 9, active: true, enabled_skills: [] };
|
|
199
|
+
const ok = await call('PUT', '/specialities/9/skills', { enabled_skills: ['status'] });
|
|
200
|
+
assert.equal(ok.status, 200, JSON.stringify(ok.body));
|
|
201
|
+
assert.deepEqual(ok.body.adoption.enabled_skills, ['status']);
|
|
202
|
+
const off = await call('PUT', '/specialities/9/skills', { enabled_skills: [] });
|
|
203
|
+
assert.equal(off.status, 200, 'everything off is a choice');
|
|
204
|
+
const notOffered = await call('PUT', '/specialities/9/skills', { enabled_skills: ['costing'] });
|
|
205
|
+
assert.equal(notOffered.status, 400);
|
|
206
|
+
assert.ok(JSON.stringify(notOffered.body).includes('not_offered'));
|
|
207
|
+
const missing = await call('PUT', '/specialities/9/skills', {});
|
|
208
|
+
assert.equal(missing.status, 400, 'the set is required: a PUT replaces it whole');
|
|
209
|
+
});
|
|
210
|
+
} finally {
|
|
211
|
+
await new Promise((r) => server.close(r));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
summary();
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// tests/studio_room_height.mjs — the studio's night room stays composed on a tall
|
|
2
|
+
// screen (task 1004344).
|
|
3
|
+
//
|
|
4
|
+
// The room is laid out on the artboard's ~900px height: the window at 86px, the
|
|
5
|
+
// floor from 700px, the desk objects just above it. On a taller screen every
|
|
6
|
+
// piece kept those tops, so the scene sat in the top 700px and the rest of the
|
|
7
|
+
// screen was bare floor, which read as a blank strip at the bottom (544px of it
|
|
8
|
+
// at 1300 tall). The fix moves the WHOLE scene down by half the height beyond
|
|
9
|
+
// the artboard, one variable, so it stays centred and keeps its own proportions.
|
|
10
|
+
// The phone layout anchors the desk to the bottom already and must not move.
|
|
11
|
+
//
|
|
12
|
+
// Run: node tests/studio_room_height.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
|
+
import { makeRunner } from './helpers.mjs';
|
|
19
|
+
|
|
20
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
const CSS = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'studio.css'), 'utf8');
|
|
22
|
+
const { test, summary } = makeRunner();
|
|
23
|
+
|
|
24
|
+
// The sheet up to the phone block: the desktop room.
|
|
25
|
+
const phoneAt = CSS.indexOf('@media (max-width: 760px)');
|
|
26
|
+
assert.ok(phoneAt > 0, 'the phone block is still where the room is re-laid for a phone');
|
|
27
|
+
const DESKTOP = CSS.slice(0, phoneAt);
|
|
28
|
+
const PHONE = CSS.slice(phoneAt, CSS.indexOf('\n}\n', phoneAt));
|
|
29
|
+
|
|
30
|
+
const ruleOf = (sheet, sel) => {
|
|
31
|
+
const esc = sel.replace(/[.*+?^${}()|[\]\\-]/g, '\\$&');
|
|
32
|
+
const m = new RegExp(`(^|\\n)${esc} \\{([^}]*)\\}`).exec(sheet);
|
|
33
|
+
return m ? m[2] : null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Every piece of the room that is placed by a pixel `top`.
|
|
37
|
+
const PIECES = ['.st-window', '.st-floor', '.st-glow', '.st-lamp-stem', '.st-lamp-shade', '.st-mug', '.st-leaf--1', '.st-leaf--2'];
|
|
38
|
+
|
|
39
|
+
await test('the room declares one drop: half the height beyond the 900px artboard, never negative', () => {
|
|
40
|
+
const room = ruleOf(DESKTOP, '.st-room');
|
|
41
|
+
assert.ok(room, 'the .st-room rule');
|
|
42
|
+
assert.match(room, /--room-drop: max\(0px, calc\(\(100% - 900px\) \/ 2\)\);/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await test('every piece of the scene moves by that drop, so it moves as ONE picture', () => {
|
|
46
|
+
for (const sel of PIECES) {
|
|
47
|
+
const rule = ruleOf(DESKTOP, sel);
|
|
48
|
+
assert.ok(rule, `the ${sel} rule`);
|
|
49
|
+
const top = /(?:^|[\s;])top: ([^;]+);/.exec(rule);
|
|
50
|
+
assert.ok(top, `${sel} is placed by a top`);
|
|
51
|
+
assert.match(top[1], /var\(--room-drop\)/, `${sel} top moves with the room: ${top[1]}`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await test('the drop keeps the original tops: at the artboard height the room is exactly where it was', () => {
|
|
56
|
+
// calc(Npx + var(--room-drop)) with the drop at 0px is Npx: the tops the
|
|
57
|
+
// design of record placed. Pinned by value so a refactor cannot move them.
|
|
58
|
+
const expected = { '.st-window': 86, '.st-floor': 700, '.st-glow': 470, '.st-lamp-stem': 560, '.st-lamp-shade': 540, '.st-mug': 650, '.st-leaf--1': 610, '.st-leaf--2': 590 };
|
|
59
|
+
for (const [sel, px] of Object.entries(expected)) {
|
|
60
|
+
const top = /(?:^|[\s;])top: calc\((\d+)px \+ var\(--room-drop\)\);/.exec(ruleOf(DESKTOP, sel));
|
|
61
|
+
assert.ok(top, `${sel} top is calc(<px> + var(--room-drop))`);
|
|
62
|
+
assert.equal(Number(top[1]), px, `${sel} keeps its design top`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await test('the phone room does not move: the drop is zero there', () => {
|
|
67
|
+
assert.match(PHONE, /\.st-room \{ --room-drop: 0px; \}/, 'the phone block zeroes the drop');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
summary();
|
|
@@ -106,3 +106,13 @@ test('an empty ledger is an empty array, never null — the page tells "none" fr
|
|
|
106
106
|
assert.deepEqual(await reads.getTaskClaims(1), []);
|
|
107
107
|
});
|
|
108
108
|
});
|
|
109
|
+
|
|
110
|
+
// ---- task 1004292: ?include=grade carries every round's per-worker verdicts ----
|
|
111
|
+
|
|
112
|
+
test('?include=grade,grade_attempts serves grade.attempts; the read runs only when asked, inside the Promise.all', () => {
|
|
113
|
+
for (const frag of ["includes.includes('grade_attempts')", 'wantAttempts ? db.gradeAttemptVerdicts(id) : Promise.resolve(null)', 'if (wantAttempts && body.grade) body.grade = { ...body.grade, attempts }']) {
|
|
114
|
+
assert.ok(handler.includes(frag), `handler has: ${frag}`);
|
|
115
|
+
}
|
|
116
|
+
const db = require('../modules/lifecycle/db.js');
|
|
117
|
+
assert.equal(typeof db.gradeAttemptVerdicts, 'function');
|
|
118
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// tests/task_visuals_instance_root.mjs
|
|
2
|
+
//
|
|
3
|
+
// Task 1004381: task visuals must be stored under the INSTANCE root, never inside
|
|
4
|
+
// the installed core package, so they survive a core upgrade.
|
|
5
|
+
//
|
|
6
|
+
// The live failure: in the consumer layout the core runs from
|
|
7
|
+
// node_modules/@bongos/core, and visualsDir() resolved from __dirname, so every
|
|
8
|
+
// render was written into the package and erased by the next upgrade.
|
|
9
|
+
//
|
|
10
|
+
// This builds a real consumer tree in a temp dir: a fake installed core package
|
|
11
|
+
// carrying the REAL task-visuals.js plus a doorway stub that re-exports the real
|
|
12
|
+
// resolveInstanceRoot, then proves, through that installed copy:
|
|
13
|
+
// 1. the store is the instance's var/task-visuals, not inside the package;
|
|
14
|
+
// 2. a stored visual survives the package being deleted and reinstalled;
|
|
15
|
+
// 3. the boot move carries files from the old in-package path, once, without
|
|
16
|
+
// overwriting, and is a no-op in a single checkout or under an override;
|
|
17
|
+
// 4. the store keeps the instance's git tree clean (upgrade pre-flight).
|
|
18
|
+
//
|
|
19
|
+
// Run: node tests/task_visuals_instance_root.mjs
|
|
20
|
+
|
|
21
|
+
import { strict as assert } from 'node:assert';
|
|
22
|
+
import { createRequire } from 'node:module';
|
|
23
|
+
import { spawnSync } from 'node:child_process';
|
|
24
|
+
import os from 'node:os';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
31
|
+
const REAL_LIB = path.join(REPO, 'modules', 'lifecycle', 'task-visuals.js');
|
|
32
|
+
const REAL_IC = path.join(REPO, 'src', 'instance-config.js');
|
|
33
|
+
|
|
34
|
+
let passed = 0, failed = 0;
|
|
35
|
+
async function t(name, fn) {
|
|
36
|
+
try { await fn(); passed++; console.log(` PASS ${name}`); }
|
|
37
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e && e.stack ? e.stack.split('\n').slice(0, 3).join('\n ') : e}`); }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const PNG = Buffer.from('89504e470d0a1a0a0000000d49484452', 'hex');
|
|
41
|
+
|
|
42
|
+
const WORK = fs.mkdtempSync(path.join(os.tmpdir(), 'tv-instance-root-'));
|
|
43
|
+
const CONSUMER = path.join(WORK, 'consumer');
|
|
44
|
+
const CORE = path.join(CONSUMER, 'node_modules', '@bongos', 'core');
|
|
45
|
+
|
|
46
|
+
// "npm ci": (re)install the fake core package — the real lib, and a doorway stub.
|
|
47
|
+
function installCore() {
|
|
48
|
+
fs.rmSync(CORE, { recursive: true, force: true });
|
|
49
|
+
fs.mkdirSync(path.join(CORE, 'modules', 'lifecycle'), { recursive: true });
|
|
50
|
+
fs.mkdirSync(path.join(CORE, 'src'), { recursive: true });
|
|
51
|
+
fs.copyFileSync(REAL_LIB, path.join(CORE, 'modules', 'lifecycle', 'task-visuals.js'));
|
|
52
|
+
fs.writeFileSync(path.join(CORE, 'src', 'module-api.js'),
|
|
53
|
+
`module.exports = { resolveInstanceRoot: require(${JSON.stringify(REAL_IC)}).resolveInstanceRoot };\n`);
|
|
54
|
+
}
|
|
55
|
+
// A fresh require of the INSTALLED copy (a restarted server).
|
|
56
|
+
function loadInstalled() {
|
|
57
|
+
const p = path.join(CORE, 'modules', 'lifecycle', 'task-visuals.js');
|
|
58
|
+
delete require.cache[p];
|
|
59
|
+
return require(p);
|
|
60
|
+
}
|
|
61
|
+
const inside = (child, parent) => {
|
|
62
|
+
const rel = path.relative(path.resolve(parent), path.resolve(child));
|
|
63
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
fs.mkdirSync(CONSUMER, { recursive: true });
|
|
67
|
+
installCore();
|
|
68
|
+
delete process.env.TASK_VISUALS_DIR;
|
|
69
|
+
process.env.BONGOS_INSTANCE_ROOT = CONSUMER;
|
|
70
|
+
|
|
71
|
+
await t('consumer layout: the store is <instance>/var/task-visuals, NOT inside the core package', () => {
|
|
72
|
+
const tv = loadInstalled();
|
|
73
|
+
const dir = tv.visualsDir();
|
|
74
|
+
assert.equal(path.resolve(dir), path.join(CONSUMER, 'var', 'task-visuals'));
|
|
75
|
+
assert.ok(!inside(dir, CORE), `store ${dir} is inside the core package ${CORE}`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await t('TASK_VISUALS_DIR still overrides', () => {
|
|
79
|
+
const tv = loadInstalled();
|
|
80
|
+
assert.equal(tv.visualsDirFor({ env: { TASK_VISUALS_DIR: '/elsewhere/x' } }), '/elsewhere/x');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
let storedName;
|
|
84
|
+
await t('a stored visual survives a core upgrade (package deleted and reinstalled)', async () => {
|
|
85
|
+
const tv = loadInstalled();
|
|
86
|
+
const r = await tv.persistImageBuffer(PNG, { taskId: 1004381, contentType: 'image/png' });
|
|
87
|
+
assert.equal(r.ok, true);
|
|
88
|
+
storedName = r.file;
|
|
89
|
+
installCore(); // the upgrade
|
|
90
|
+
const after = loadInstalled();
|
|
91
|
+
const p = after.resolveSafe(storedName);
|
|
92
|
+
assert.ok(p && fs.existsSync(p), 'the visual is gone after the upgrade');
|
|
93
|
+
assert.deepEqual(fs.readFileSync(p), PNG);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await t('the default store carries a * .gitignore, so the instance git tree stays clean', () => {
|
|
97
|
+
const gi = fs.readFileSync(path.join(CONSUMER, 'var', 'task-visuals', '.gitignore'), 'utf8');
|
|
98
|
+
assert.match(gi, /^\*$/m);
|
|
99
|
+
const init = spawnSync('git', ['init', '-q'], { cwd: CONSUMER, encoding: 'utf8' });
|
|
100
|
+
if (init.status !== 0) { console.log(' (git unavailable: .gitignore content checked only)'); return; }
|
|
101
|
+
fs.writeFileSync(path.join(CONSUMER, '.gitignore'), 'node_modules/\n');
|
|
102
|
+
const st = spawnSync('git', ['status', '--porcelain', '--untracked-files=all', '--', 'var'], { cwd: CONSUMER, encoding: 'utf8' });
|
|
103
|
+
assert.equal(st.status, 0);
|
|
104
|
+
assert.equal(st.stdout.trim(), '', `var/ shows in git status:\n${st.stdout}`);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await t('boot move: files at the old in-package path move into the instance store, once', async () => {
|
|
108
|
+
const tv = loadInstalled();
|
|
109
|
+
const legacy = path.join(CORE, 'var', 'task-visuals');
|
|
110
|
+
fs.mkdirSync(legacy, { recursive: true });
|
|
111
|
+
const a = 'task-1004345-0123456789abcdef.png';
|
|
112
|
+
const b = 'task-1004347-fedcba9876543210.png';
|
|
113
|
+
fs.writeFileSync(path.join(legacy, a), PNG);
|
|
114
|
+
fs.writeFileSync(path.join(legacy, b), Buffer.from('legacy copy'));
|
|
115
|
+
fs.writeFileSync(path.join(legacy, 'not-a-visual.txt'), 'x');
|
|
116
|
+
// b already exists in the new store: it must be kept, not overwritten.
|
|
117
|
+
const store = tv.visualsDir();
|
|
118
|
+
fs.writeFileSync(path.join(store, b), PNG);
|
|
119
|
+
|
|
120
|
+
const r = await tv.migrateLegacyVisuals();
|
|
121
|
+
assert.deepEqual(r, { moved: 1, skipped: 1, failed: 0 });
|
|
122
|
+
assert.deepEqual(fs.readFileSync(path.join(store, a)), PNG);
|
|
123
|
+
assert.ok(!fs.existsSync(path.join(legacy, a)), 'the moved file is still in the package');
|
|
124
|
+
assert.deepEqual(fs.readFileSync(path.join(store, b)), PNG, 'an existing file was overwritten');
|
|
125
|
+
assert.ok(fs.existsSync(path.join(legacy, 'not-a-visual.txt')), 'a foreign file was touched');
|
|
126
|
+
|
|
127
|
+
const again = await tv.migrateLegacyVisuals();
|
|
128
|
+
assert.deepEqual(again, { moved: 0, skipped: 1, failed: 0 });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await t('boot move is a no-op in a single checkout (same dir) and under TASK_VISUALS_DIR', async () => {
|
|
132
|
+
const tv = loadInstalled();
|
|
133
|
+
const same = path.join(WORK, 'same');
|
|
134
|
+
fs.mkdirSync(same, { recursive: true });
|
|
135
|
+
fs.writeFileSync(path.join(same, 'task-1-0123456789abcdef.png'), PNG);
|
|
136
|
+
assert.deepEqual(await tv.migrateLegacyVisuals({ from: same, to: same }), { moved: 0, skipped: 0, failed: 0 });
|
|
137
|
+
assert.deepEqual(await tv.migrateLegacyVisuals({ env: { TASK_VISUALS_DIR: path.join(WORK, 'o') } }), { moved: 0, skipped: 0, failed: 0 });
|
|
138
|
+
assert.deepEqual(await tv.migrateLegacyVisuals({ from: path.join(WORK, 'absent') }), { moved: 0, skipped: 0, failed: 0 });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await t('the served lib in THIS checkout resolves from the instance root, not __dirname', () => {
|
|
142
|
+
const src = fs.readFileSync(REAL_LIB, 'utf8');
|
|
143
|
+
const fn = src.slice(src.indexOf('function visualsDirFor'), src.indexOf('function visualsDir()'));
|
|
144
|
+
assert.match(fn, /instanceRoot\(\)/);
|
|
145
|
+
assert.doesNotMatch(fn, /__dirname/);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
fs.rmSync(WORK, { recursive: true, force: true });
|
|
149
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
150
|
+
if (failed) process.exit(1);
|
|
@@ -95,6 +95,27 @@ await test('a multi-line batch applies every line, across files, holes put back'
|
|
|
95
95
|
assert.ok(!out.sources.has(OTHER), 'the same sentence in ANOTHER file is left alone: the line names its own file');
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
+
await test('a {{token}} brand hole is written back verbatim, and a rewrite that drops or adds one is refused (task 1004381)', () => {
|
|
99
|
+
const rel = 'modules/public-landing/public/fixture-contact.html';
|
|
100
|
+
const tree = { [rel]: '<!doctype html>\n<html><body>\n<p>Both are how {{productName}} itself works.</p>\n</body></html>\n' };
|
|
101
|
+
const rows = ci.fromHtml({ source: tree[rel], rel }).map(({ start, end, ...r }) => ({ id: ci.stableId('landing', r.text), surface: 'landing', ...r }));
|
|
102
|
+
const reg = { entries: rows, counts: { strings: rows.length } };
|
|
103
|
+
const r = rows.find((e) => e.text === 'Both are how {{productName}} itself works.');
|
|
104
|
+
assert.ok(r, 'the registry keeps the token by name');
|
|
105
|
+
const mk = (key, after) => ({ key, section: 'contact', before: r.text, after, placement: 'placed', file: rel, line: r.line, string_id: r.id });
|
|
106
|
+
const batch = (lines) => ({ page_id: 'landing:contact', reading_hash: 'h', submitted_at: '2026-09-29T00:00:00Z', lines });
|
|
107
|
+
|
|
108
|
+
const kept = apply.applyBatchToSources({ batch: batch([mk('L0001', 'This project, {{productName}}, has two doors.')]), registry: reg, readSource: readerOf(tree) });
|
|
109
|
+
assert.deepEqual(kept.refused, []);
|
|
110
|
+
assert.match(kept.sources.get(rel), /<p>This project, \{\{productName\}\}, has two doors\.<\/p>/, 'the token, and the spaces around it, land in the source as typed');
|
|
111
|
+
assert.doesNotMatch(kept.sources.get(rel), /Cloud Bongos/, 'no brand value is baked into the source');
|
|
112
|
+
|
|
113
|
+
for (const [key, after] of [['L0002', 'This project, Cloud Bongos, has two doors.'], ['L0003', '{{productName}} and {{productName}} again.']]) {
|
|
114
|
+
const out = apply.applyBatchToSources({ batch: batch([mk(key, after)]), registry: reg, readSource: readerOf(tree) });
|
|
115
|
+
assert.deepEqual(out.refused.map((x) => [x.key, x.code]), [[key, 'placeholder_mismatch']], `${after} is refused`);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
98
119
|
await test('a refused line is NAMED, with its code, and the rest of the batch still lands', () => {
|
|
99
120
|
const b = BATCH();
|
|
100
121
|
b.lines.push(
|
package/tests/ui_design_kit.mjs
CHANGED
|
@@ -29,12 +29,12 @@ const readJson = (rel) => JSON.parse(fs.readFileSync(path.join(ROOT, rel), 'utf8
|
|
|
29
29
|
test('the platform floors are a pinned list — a floor cannot vanish quietly', () => {
|
|
30
30
|
assert.deepEqual([...lib.FLOORS], ['console-errors', 'failed-requests', 'horizontal-overflow', 'text-fit', 'sibling-overlap', 'one-header-main-h1', 'controls-24px', 'top-bar-80px', 'reduced-motion']);
|
|
31
31
|
assert.deepEqual(lib.EXIT, { CLEAN: 0, PROBLEMS: 1, USAGE: 2, NO_BROWSER: 3 });
|
|
32
|
-
assert.deepEqual(Object.keys(lib.SIZES), ['320', '390', '1280', '1440']);
|
|
32
|
+
assert.deepEqual(Object.keys(lib.SIZES), ['320', '390', '1024', '1280', '1440']);
|
|
33
33
|
assert.deepEqual([...lib.DEFAULT_SIZES], [1440, 390, 320], 'render.js renders these unless --sizes names 1280');
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
test('the shipped states files validate and sit beside the page they name', () => {
|
|
37
|
-
for (const rel of ['modules/public-landing/public/projects.states.json', 'modules/public-landing/public/projects-empty.states.json', 'modules/public-landing/public/index.states.json', 'modules/hall-ui/public/ranks.states.json']) {
|
|
37
|
+
for (const rel of ['modules/public-landing/public/projects.states.json', 'modules/public-landing/public/projects-empty.states.json', 'modules/public-landing/public/index.states.json', 'modules/public-landing/public/account.states.json', 'modules/public-landing/public/account-private.states.json', 'modules/hall-ui/public/ranks.states.json']) {
|
|
38
38
|
const json = readJson(rel);
|
|
39
39
|
const v = lib.validateStatesFile(json);
|
|
40
40
|
assert.deepEqual(v.errors, [], `${rel}: ${v.errors.join('; ')}`);
|
|
@@ -43,14 +43,14 @@ test('the shipped states files validate and sit beside the page they name', () =
|
|
|
43
43
|
}
|
|
44
44
|
// the apex is dark only (task 1003336): its states files name the one mode, and
|
|
45
45
|
// render.js reads it as the default so a plain `--page /` never renders light
|
|
46
|
-
for (const rel of ['modules/public-landing/public/projects.states.json', 'modules/public-landing/public/index.states.json']) {
|
|
46
|
+
for (const rel of ['modules/public-landing/public/projects.states.json', 'modules/public-landing/public/index.states.json', 'modules/public-landing/public/account.states.json']) {
|
|
47
47
|
assert.deepEqual(readJson(rel).modes, ['dark'], `${rel}: the apex renders dark only`);
|
|
48
48
|
assert.equal(readJson(rel).modeQuery, false, `${rel}: no query walks a second mode`);
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
test('the shipped probes files validate', () => {
|
|
53
|
-
for (const rel of ['modules/public-landing/public/projects.probes.json', 'modules/public-landing/public/index.probes.json']) {
|
|
53
|
+
for (const rel of ['modules/public-landing/public/projects.probes.json', 'modules/public-landing/public/index.probes.json', 'modules/public-landing/public/account.probes.json']) {
|
|
54
54
|
const v = lib.validateProbesFile(readJson(rel));
|
|
55
55
|
assert.deepEqual(v.errors, [], `${rel}: ${v.errors.join('; ')}`);
|
|
56
56
|
}
|
|
@@ -181,6 +181,38 @@ test('serve.js answers the manage flows with the routes\' receipts, signed in on
|
|
|
181
181
|
} finally { out.stop(); }
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
+
test('serve.js holds one account for the run and refuses what the platform-identity routes refuse (task 1004259)', async () => {
|
|
185
|
+
const s = await lib.startStub({ surface: 'public-landing', auth: true });
|
|
186
|
+
try {
|
|
187
|
+
const call = (p, method, body) => fetch(s.base + '/api/bongos' + p, { method, headers: { 'content-type': 'application/json' }, body: body && JSON.stringify(body) });
|
|
188
|
+
const read = async () => (await call('/me/cross-project', 'GET')).json();
|
|
189
|
+
assert.equal((await read()).handle, 'builder', 'the fixture account answers the read');
|
|
190
|
+
// the refusals the real routes make — a stub that takes any body is how task 1003352 shipped a key the route 400s on
|
|
191
|
+
assert.equal((await call('/me/cross-project', 'PATCH', { stealth: true })).status, 400, 'an unknown key');
|
|
192
|
+
assert.equal((await call('/me/cross-project', 'PATCH', { account_visibility: 'hidden' })).status, 400, 'an off-vocabulary visibility');
|
|
193
|
+
assert.equal((await call('/me/profile', 'PATCH', { bio: 'x'.repeat(281) })).status, 400, 'a bio over its cap');
|
|
194
|
+
assert.equal((await (await call('/me/profile', 'PATCH', { links: { linkedin: 'https://evil.example/in/b' } })).json()).error.code, 'invalid_link', 'a typed link off its pinned host');
|
|
195
|
+
assert.equal((await call('/me/profile', 'PATCH', { links: { website: 'http://plain.example/' } })).status, 400, 'http is not https');
|
|
196
|
+
assert.equal((await call('/me/handle', 'PATCH', { handle: '-bad-' })).status, 400, 'a handle with a hyphen at the end');
|
|
197
|
+
assert.equal((await call('/me/handle', 'PATCH', { handle: 'taken' })).status, 409, 'the one taken handle');
|
|
198
|
+
// and what they accept comes back on the next read
|
|
199
|
+
assert.equal((await call('/me/profile', 'PATCH', { bio: ' Hello. ' })).status, 200);
|
|
200
|
+
assert.equal((await call('/me/handle', 'PATCH', { handle: 'New-Name' })).status, 200);
|
|
201
|
+
assert.equal((await call('/me/cross-project', 'PATCH', { account_visibility: 'private' })).status, 200);
|
|
202
|
+
const after = await read();
|
|
203
|
+
assert.equal(after.bio, 'Hello.');
|
|
204
|
+
assert.equal(after.handle, 'new-name');
|
|
205
|
+
assert.equal(after.account_visibility, 'private');
|
|
206
|
+
assert.equal(after.recruiter_discoverable, null, 'going private without naming reach resets it (setAccountPrivacy)');
|
|
207
|
+
assert.equal(after.recruiter_discoverable_effective, false, 'so a private account following it is out of recruiting');
|
|
208
|
+
} finally { s.stop(); }
|
|
209
|
+
const out = await lib.startStub({ surface: 'public-landing', auth: false });
|
|
210
|
+
try {
|
|
211
|
+
assert.equal((await fetch(out.base + '/api/bongos/me/cross-project')).status, 401, 'signed out, the account is not read');
|
|
212
|
+
assert.equal((await fetch(out.base + '/api/bongos/me/profile', { method: 'PATCH', body: '{}' })).status, 401);
|
|
213
|
+
} finally { out.stop(); }
|
|
214
|
+
});
|
|
215
|
+
|
|
184
216
|
test('checkMock: a taste ban applies only when the instance declared it', () => {
|
|
185
217
|
const dashed = GOOD_MOCK.replace('Plain copy.', 'Plain — copy.');
|
|
186
218
|
const withBan = mock.checkMock(dashed, { bans: new Set(['em-dash']) });
|
package/tests/upgrade.mjs
CHANGED
|
@@ -279,7 +279,7 @@ t('full: pre-flight → pin → install → migrate → materialize → restart
|
|
|
279
279
|
// The trailing `git status` is persistPin's read-only look at whether the new
|
|
280
280
|
// pin is committed (task 1002712). It runs LAST — after verify — and without
|
|
281
281
|
// --commit-pin it only warns, so no git add/commit/push appears here.
|
|
282
|
-
['git status --porcelain', 'npm install --no-audit --no-fund', 'npm run migrate', 'systemctl restart emersonian.service',
|
|
282
|
+
['git status --porcelain -uall', 'npm install --no-audit --no-fund', 'npm run migrate', 'systemctl restart emersonian.service',
|
|
283
283
|
'git status --porcelain -- package.json package-lock.json',
|
|
284
284
|
// task 1003926: both generators ran, so the files they rewrote are the pin commit's too
|
|
285
285
|
`git status --porcelain -- ${Object.values(u.API_ARTIFACT_FILES).flat().join(' ')}`]
|
|
@@ -518,6 +518,30 @@ test('gitTreeClean: ignoring the materialized paths never waives anything else',
|
|
|
518
518
|
assert.equal(t.ignored, 1);
|
|
519
519
|
});
|
|
520
520
|
|
|
521
|
+
// task 1004188: a bare `git status --porcelain` collapses a wholly-untracked directory into ONE
|
|
522
|
+
// entry (`?? .claude/skills/new-in-785/`), so a waiver of FILE paths missed every file of a skill
|
|
523
|
+
// directory the newer core shipped for the first time — the commonest way an upgrade dirties an
|
|
524
|
+
// instance. This must be real git: a hand-written porcelain fixture lists the files you meant.
|
|
525
|
+
test('gitTreeClean: a skill directory that is NEW (wholly untracked) is waived file by file', () => {
|
|
526
|
+
const dir = scratchCoTenant();
|
|
527
|
+
put(dir, '.claude/skills/new-in-785/SKILL.md', 'shipped by a newer core\n');
|
|
528
|
+
put(dir, '.claude/skills/new-in-785/helper.js', '// also new\n');
|
|
529
|
+
const t = gitTreeClean(dir, spawnSync, ['.claude/skills/new-in-785/SKILL.md', '.claude/skills/new-in-785/helper.js']);
|
|
530
|
+
assert.equal(t.dirty, '', 'no collapsed `?? dir/` entry survives the waiver');
|
|
531
|
+
assert.equal(t.ok, true);
|
|
532
|
+
assert.equal(t.ignored, 2, 'each file is counted, not the directory');
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test('gitTreeClean: a new directory is NOT waived wholesale — an unlisted file in it still halts', () => {
|
|
536
|
+
const dir = scratchCoTenant();
|
|
537
|
+
put(dir, '.claude/skills/new-in-785/SKILL.md', 'shipped by a newer core\n');
|
|
538
|
+
put(dir, '.claude/skills/new-in-785/notes.md', 'a human dropped this here\n');
|
|
539
|
+
const t = gitTreeClean(dir, spawnSync, ['.claude/skills/new-in-785/SKILL.md']);
|
|
540
|
+
assert.equal(t.ok, false);
|
|
541
|
+
assert.match(t.dirty, /new-in-785\/notes\.md/);
|
|
542
|
+
assert.equal(t.ignored, 1);
|
|
543
|
+
});
|
|
544
|
+
|
|
521
545
|
test('materializeWillRewrite: the set comes from the core the instance actually has', () => {
|
|
522
546
|
const dir = scratchCoTenant();
|
|
523
547
|
installCore(dir, '1.19.785');
|