@bongos/core 1.24.78 → 1.24.80
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 +498 -353
- package/.claude/skills/ask-for-help/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/design/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +11 -0
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/adr/0274-one-kernel-three-role-packs.md +2 -2
- package/docs/adr/0399-the-visor-is-glass-over-the-scene-and-owns-no-camera.md +91 -0
- package/docs/adr/0400-a-chain-reserves-work-and-each-sub-quest-is-still-claimed-alone.md +109 -0
- package/docs/adr/0401-one-options-menu-opens-every-settings-screen-in-a-frame.md +53 -0
- package/docs/adr/README.md +3 -0
- package/docs/api/openapi.json +195 -3
- package/docs/api-reference.md +8 -4
- package/docs/architecture.md +7 -3
- package/docs/copy-inventory.md +390 -370
- package/docs/copy-registry.json +719 -518
- package/docs/design/characters/character-sprites-source.webp +0 -0
- package/docs/file-map.md +7 -8
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +5 -0
- package/docs/onboarding/slash-commands.md +1 -1
- package/docs/packs/{engineer.md → artisan.md} +5 -5
- package/docs/packs/artist.md +50 -4
- package/docs/page-inventory.json +15 -4
- package/docs/page-readings.json +3182 -3336
- package/docs/{packs/ideator.md → recipes/ideation-playbook.md} +5 -48
- package/migrations/core_296_crafts_artisan_artist.sql +72 -0
- package/migrations/core_297_builder_character.sql +30 -0
- package/modules/copy-desk/proposals.js +1 -1
- package/modules/economy/credits-by-craft.js +29 -15
- package/modules/hall-ui/public/ai-studio.html +4 -4
- package/modules/hall-ui/public/atlas.html +5 -5
- package/modules/hall-ui/public/atlas.js +3 -1
- package/modules/hall-ui/public/blockers.html +5 -5
- package/modules/hall-ui/public/board-room.html +4 -4
- package/modules/hall-ui/public/characters/artisan.png +0 -0
- package/modules/hall-ui/public/characters/artist.png +0 -0
- package/modules/hall-ui/public/characters/patron.png +0 -0
- package/modules/hall-ui/public/city.css +2 -1
- package/modules/hall-ui/public/city.html +9 -6
- package/modules/hall-ui/public/city.js +7 -1
- package/modules/hall-ui/public/collab.html +4 -4
- package/modules/hall-ui/public/commands.html +4 -4
- package/modules/hall-ui/public/copy-desk.html +4 -4
- package/modules/hall-ui/public/craft-mode.js +258 -64
- package/modules/hall-ui/public/deploy.html +4 -4
- package/modules/hall-ui/public/diagram-viewer.js +2 -1
- package/modules/hall-ui/public/dom-utils.js +1 -1
- package/modules/hall-ui/public/goal-map.html +9 -6
- package/modules/hall-ui/public/goals.html +5 -5
- package/modules/hall-ui/public/government.html +4 -4
- package/modules/hall-ui/public/hall-render.js +3 -1
- package/modules/hall-ui/public/home-city-draw.js +3 -3
- package/modules/hall-ui/public/home-city.css +4 -0
- package/modules/hall-ui/public/home-city.js +17 -9
- package/modules/hall-ui/public/home-gallery.js +8 -1
- package/modules/hall-ui/public/home-idea-box.css +2 -2
- package/modules/hall-ui/public/home-idea-box.js +3 -3
- package/modules/hall-ui/public/idea.html +5 -5
- package/modules/hall-ui/public/ideas.html +5 -5
- package/modules/hall-ui/public/index.html +11 -7
- package/modules/hall-ui/public/market-watch-item.html +4 -4
- package/modules/hall-ui/public/market-watch-rules.html +4 -4
- package/modules/hall-ui/public/market-watch.html +6 -9
- package/modules/hall-ui/public/market-watch.js +22 -45
- package/modules/hall-ui/public/modules.html +4 -4
- package/modules/hall-ui/public/money.html +4 -4
- package/modules/hall-ui/public/my-agents.html +4 -4
- package/modules/hall-ui/public/options-menu.js +409 -0
- package/modules/hall-ui/public/paintbrush.js +3 -3
- package/modules/hall-ui/public/palette.js +11 -13
- package/modules/hall-ui/public/primer.html +5 -5
- package/modules/hall-ui/public/profile-crafts.js +1 -1
- package/modules/hall-ui/public/profile-roles.js +23 -19
- package/modules/hall-ui/public/profile.css +1 -1
- package/modules/hall-ui/public/profile.html +19 -18
- package/modules/hall-ui/public/profile.js +19 -18
- package/modules/hall-ui/public/profile.states.json +4 -4
- package/modules/hall-ui/public/project-settings.html +4 -4
- package/modules/hall-ui/public/roster.html +4 -4
- package/modules/hall-ui/public/roster.js +3 -3
- package/modules/hall-ui/public/settings.css +24 -4
- package/modules/hall-ui/public/settings.html +21 -17
- package/modules/hall-ui/public/settings.js +44 -44
- package/modules/hall-ui/public/shell-game.css +65 -0
- package/modules/hall-ui/public/shell-game.js +283 -3
- package/modules/hall-ui/public/shell.js +7 -26
- package/modules/hall-ui/public/sky-interaction.js +7 -0
- package/modules/hall-ui/public/speciality-tools.js +1 -2
- package/modules/hall-ui/public/street-inside.js +3 -4
- package/modules/hall-ui/public/street.html +4 -4
- package/modules/hall-ui/public/studio.html +5 -5
- package/modules/hall-ui/public/style.css +73 -47
- package/modules/hall-ui/public/task-detail.js +5 -4
- package/modules/hall-ui/public/task.html +5 -5
- package/modules/hall-ui/public/thinking.css +10 -0
- package/modules/hall-ui/public/thinking.html +9 -6
- package/modules/hall-ui/public/tweak-editor.html +5 -5
- package/modules/hall-ui/public/tweak-editor.js +5 -1
- package/modules/hall-ui/public/update-pill.js +91 -2
- package/modules/hall-ui/public/visor.css +80 -0
- package/modules/hall-ui/public/visor.js +316 -0
- package/modules/hall-ui/public/watch-repo-invite.js +159 -0
- package/modules/hall-ui/public/watch.html +6 -5
- package/modules/hall-ui/public/watch.js +17 -2
- package/modules/hall-ui/public/welcome-boot.js +15 -6
- package/modules/hall-ui/public/work.html +5 -5
- package/modules/hall-ui/records/builders-roster.md +2 -2
- package/modules/hall-ui/records/craft-mode.md +13 -11
- package/modules/hall-ui/records/game-mode.md +2 -2
- package/modules/hall-ui/records/governor-street.md +1 -1
- package/modules/hall-ui/records/home-city.md +3 -1
- package/modules/hall-ui/records/market-watch-settings.md +2 -0
- package/modules/hall-ui/records/options-menu.md +15 -0
- package/modules/hall-ui/records/profile-roles.md +11 -11
- package/modules/hall-ui/records/sky.md +3 -3
- package/modules/hall-ui/records/start-screen.md +21 -0
- package/modules/hall-ui/records/tweak-editor.md +1 -1
- package/modules/hall-ui/records/update-pill.md +12 -0
- package/modules/hall-ui/records/visor.md +23 -0
- package/modules/hall-ui/records/welcome-tower.md +5 -5
- package/modules/ideas/ideator-stats.js +3 -1
- package/modules/ideas/inbox.js +2 -2
- package/modules/lifecycle/activity.js +1 -1
- package/modules/lifecycle/cascade.js +4 -4
- package/modules/lifecycle/goal-spec-task.js +1 -1
- package/modules/lifecycle/help-requests.js +19 -15
- package/modules/lifecycle/kickoff-checklist.js +3 -3
- package/modules/lifecycle/kickoff-seed.js +1 -1
- package/modules/lifecycle/migrations/lifecycle_021_crafts_artisan_artist.sql +29 -0
- package/modules/lifecycle/mingle.js +4 -4
- package/modules/lifecycle/page-tweak-write.js +1 -1
- package/modules/lifecycle/routes/task-write-routes.js +3 -1
- package/modules/lifecycle/routes/tasks.js +3 -3
- package/modules/lifecycle/ship-card.js +2 -3
- package/modules/lifecycle/task-classifier.js +39 -29
- package/modules/lifecycle/tests/cascade.mjs +7 -7
- package/modules/onboarding/founding-mode.js +3 -0
- package/modules/onboarding/genesis-home.js +21 -1
- package/modules/onboarding/routes/onboarding.js +2 -0
- package/modules/page-notes/notes.js +3 -2
- package/modules/page-notes/split.js +1 -1
- package/modules/platform-identity/craft-rollup.js +35 -7
- package/modules/platform-identity/migrations/platform_identity_036_crafts_artisan_artist.sql +45 -0
- package/modules/platform-identity/platform-identity.js +2 -2
- package/modules/platform-identity/routes/community-leaderboard.js +2 -1
- package/modules/platform-identity/routes/community-search.js +2 -1
- package/modules/platform-identity/routes/sso.js +2 -1
- package/modules/platform-identity/tests/platform-identity.mjs +6 -6
- package/modules/public-landing/public/community.html +3 -5
- package/modules/public-landing/public/index.html +1 -1
- package/modules/public-landing/public/projects.html +1 -1
- package/modules/specialities/migrations/specialities_004_crafts_artisan_artist.sql +30 -0
- package/modules/specialities/routes/specialities.js +2 -2
- package/modules/status-ui/public/status.js +1 -1
- package/modules/ui-design/kit/fixtures/community-leaderboard.json +10 -10
- package/modules/ui-design/kit/fixtures/community-search.json +2 -2
- package/modules/ui-design/kit/fixtures/scouting-recruiter.json +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +76 -0
- package/scripts/bongos/autobongos-host.js +19 -4
- package/scripts/bongos/autobongos-loop.js +1 -1
- package/scripts/bongos/claude-materialize.js +20 -1
- package/scripts/bongos/discipline-modes.json +7 -12
- package/scripts/bongos/fitness.js +1 -1
- package/scripts/bongos/gen-hall-sitemap.js +7 -7
- package/scripts/bongos/grade-smoke.js +2 -2
- package/scripts/bongos/init.js +2 -2
- package/scripts/bongos/newcomer-floor.config.json +1 -2
- package/scripts/bongos/newcomer-floor.js +4 -3
- package/scripts/bongos/newcomer-floor.templates.json +10 -12
- package/scripts/bongos/newcomer-floor.test.js +47 -38
- package/scripts/bongos/role-pack-guard.js +5 -4
- package/scripts/bongos/seed-tasks.example.json +3 -3
- package/scripts/bongos/setup.js +20 -107
- package/scripts/bongos/start.js +2 -2
- package/scripts/bongos/task.js +5 -4
- package/scripts/bongos/triage.js +1 -1
- package/scripts/bongos/worktree.js +16 -4
- package/scripts/setup-builder.js +6 -46
- package/src/bongos/auth-admission.js +8 -8
- package/src/bongos/craft-mode.js +2 -0
- package/src/bongos/db.js +39 -1
- package/src/bongos/github-repos.js +78 -0
- package/src/bongos/module-scope-map.js +22 -0
- package/src/bongos/role-stats.js +27 -11
- package/src/bongos/routes/github.js +100 -2
- package/src/bongos/routes/me.js +33 -0
- package/src/bongos/serve-internal.js +26 -9
- package/src/crafts.js +66 -0
- package/src/module-api.js +6 -1
- package/src/module-loader/manifest-schema.js +16 -0
- package/src/modules.js +2 -2
- package/src/role-responsibilities.js +6 -5
- package/tests/acclaim_feed_evidence_db.mjs +1 -1
- package/tests/activity_rollup_as_of.mjs +1 -1
- package/tests/api_alias_caller_ratchet.mjs +3 -3
- package/tests/application_lifecycle.mjs +1 -1
- package/tests/artist_review.mjs +1 -1
- package/tests/artist_review_goal.mjs +1 -1
- package/tests/autobongos_host.mjs +19 -1
- package/tests/blocker_hall_live_proof.mjs +1 -1
- package/tests/blocker_tasks_named_db.mjs +2 -2
- package/tests/canonical_profile_url.mjs +1 -1
- package/tests/cap_newcomer_tasks.mjs +13 -13
- package/tests/character_creation.mjs +294 -0
- package/tests/claim_gate_open_blocker.mjs +2 -2
- package/tests/claim_gate_rebase.mjs +6 -6
- package/tests/claude_materialize.mjs +75 -1
- package/tests/clawback_confirmed_backlog.mjs +3 -3
- package/tests/collab_page.mjs +8 -9
- package/tests/collab_self_settling.mjs +2 -2
- package/tests/community_leaderboard_route.mjs +13 -5
- package/tests/community_proof_db.mjs +3 -0
- package/tests/community_search_route.mjs +7 -6
- package/tests/conductor.mjs +1 -1
- package/tests/conductor_main_e2e.mjs +8 -8
- package/tests/copy_desk_flags_db.mjs +1 -1
- package/tests/copy_desk_page_approve.mjs +2 -2
- package/tests/copy_desk_page_claim.mjs +2 -2
- package/tests/copy_desk_page_next.mjs +1 -1
- package/tests/copy_desk_page_release.mjs +1 -1
- package/tests/copy_desk_page_submit.mjs +2 -2
- package/tests/core_258_box_session_claims_db.mjs +1 -1
- package/tests/craft_mode.mjs +48 -34
- package/tests/craft_mode_home.mjs +20 -20
- package/tests/craft_mode_looks.mjs +16 -15
- package/tests/craft_mode_nav.mjs +31 -29
- package/tests/craft_review_mode.mjs +46 -41
- package/tests/crafts_vocabulary.mjs +70 -0
- package/tests/cross_discipline_priority_direction.mjs +5 -5
- package/tests/fitness.mjs +7 -11
- package/tests/funded_project_pays_proof_db.mjs +1 -1
- package/tests/genesis_home.mjs +21 -1
- package/tests/github_chip.mjs +3 -3
- package/tests/github_repo_invite.mjs +379 -0
- package/tests/goal_criterion_confirm_ui.mjs +1 -1
- package/tests/goal_invite_consent.mjs +1 -1
- package/tests/goal_map_page.mjs +2 -2
- package/tests/goal_sort_staleness.mjs +1 -1
- package/tests/goal_spec_task.mjs +1 -1
- package/tests/goal_spec_task_db.mjs +3 -3
- package/tests/grade_engine.mjs +2 -2
- package/tests/grader_no_artifact.mjs +1 -1
- package/tests/grader_prompt_fencing.mjs +1 -1
- package/tests/grader_prompt_window_bound.mjs +1 -1
- package/tests/grader_reader_lens.mjs +2 -2
- package/tests/grader_severity_gate.mjs +1 -1
- package/tests/grader_unbacked_score.mjs +1 -1
- package/tests/hall_approval_queue.mjs +1 -1
- package/tests/hall_board_world.mjs +2 -2
- package/tests/hall_builders_page_pass.mjs +1 -1
- package/tests/hall_city.mjs +1 -1
- package/tests/hall_fullscreen.mjs +27 -3
- package/tests/hall_kit_charts.mjs +1 -1
- package/tests/hall_landing_boot.mjs +1 -1
- package/tests/hall_my_agents.mjs +16 -16
- package/tests/hall_nav.mjs +22 -15
- package/tests/hall_options_menu.mjs +247 -0
- package/tests/hall_oversight_world.mjs +1 -1
- package/tests/hall_palette.mjs +8 -9
- package/tests/hall_profile_world.mjs +3 -3
- package/tests/hall_record_world.mjs +2 -2
- package/tests/hall_settings_world.mjs +2 -1
- package/tests/hall_sitemap.mjs +6 -5
- package/tests/hall_start_screen.mjs +244 -0
- package/tests/hall_street.mjs +2 -2
- package/tests/hall_street_inside.mjs +17 -9
- package/tests/hall_studio_world.mjs +3 -3
- package/tests/hall_visor.mjs +224 -0
- package/tests/help_requests.mjs +47 -15
- package/tests/helpers.mjs +4 -4
- package/tests/home_city.mjs +6 -4
- package/tests/home_city_widget.mjs +33 -14
- package/tests/home_gallery_widget.mjs +4 -4
- package/tests/home_idea_box.mjs +6 -6
- package/tests/hub_community_page.mjs +3 -3
- package/tests/hub_craft_rollup.mjs +75 -36
- package/tests/hub_recent_ships.mjs +3 -2
- package/tests/idea_cascade.mjs +2 -2
- package/tests/idea_credit_streams_db.mjs +1 -1
- package/tests/idea_detail_page.mjs +1 -1
- package/tests/idea_ideate_full.mjs +5 -2
- package/tests/idea_spark_hall.mjs +1 -1
- package/tests/init.mjs +3 -3
- package/tests/instance_manifest.mjs +2 -2
- package/tests/kickoff_checklist.mjs +1 -1
- package/tests/leaderboard_roles.mjs +34 -25
- package/tests/lifecycle_claim_ladder_refs.mjs +55 -0
- package/tests/lightbulb.mjs +14 -10
- package/tests/main_discipline.mjs +8 -8
- package/tests/markdown_line_endings.mjs +1 -1
- package/tests/market_watch_pages.mjs +52 -74
- package/tests/me_streak_leaderboard_queries.mjs +3 -3
- package/tests/migration_allocation.mjs +1 -1
- package/tests/mingle.mjs +36 -21
- package/tests/module-scope-map.mjs +22 -2
- package/tests/module_api.mjs +1 -0
- package/tests/module_contributions.mjs +13 -13
- package/tests/module_manifest.mjs +29 -0
- package/tests/module_skills_hook.mjs +2 -2
- package/tests/modules.mjs +14 -7
- package/tests/nav_permission_atoms.mjs +2 -1
- package/tests/needs_rebase.mjs +1 -1
- package/tests/newcomer_restock.mjs +13 -13
- package/tests/page_notes_panel.mjs +22 -22
- package/tests/page_tweak_no_block.mjs +1 -1
- package/tests/paintbrush.mjs +7 -7
- package/tests/privacy_matrix.mjs +1 -0
- package/tests/profile_activity.mjs +20 -23
- package/tests/profile_earnings.mjs +1 -1
- package/tests/profile_nudge_prefs.mjs +4 -4
- package/tests/profile_role_headline.mjs +73 -46
- package/tests/profile_role_stats.mjs +55 -45
- package/tests/profile_rollup_consent.mjs +19 -6
- package/tests/profile_route.mjs +12 -12
- package/tests/profile_ui_cross_project.mjs +4 -4
- package/tests/project_invite_suggestions.mjs +2 -2
- package/tests/promote_goal_id.mjs +1 -1
- package/tests/publish_manifest.mjs +1 -1
- package/tests/rank_floor_wiring.mjs +1 -1
- package/tests/regrade_actual_minutes.mjs +1 -1
- package/tests/removal_proposal.mjs +2 -2
- package/tests/reward_gate.mjs +4 -4
- package/tests/reward_mode_cost_plus_only.mjs +1 -1
- package/tests/role_responsibilities.mjs +18 -12
- package/tests/role_stats_db.mjs +17 -16
- package/tests/scouting_rollup_fanout.mjs +5 -5
- package/tests/seed_tasks.mjs +5 -2
- package/tests/session_earnings_mirror_db.mjs +1 -1
- package/tests/session_reward_on_land_db.mjs +1 -1
- package/tests/session_reward_reconcile_db.mjs +2 -2
- package/tests/sessions_kit_pager.mjs +1 -1
- package/tests/settings_art_key_absent.mjs +2 -2
- package/tests/settings_main_role.mjs +21 -20
- package/tests/setup_flow.mjs +3 -3
- package/tests/setup_smoke.mjs +20 -0
- package/tests/ship_card.mjs +6 -6
- package/tests/specialities.mjs +16 -4
- package/tests/speciality_db_shape.mjs +6 -6
- package/tests/speciality_injection.mjs +17 -17
- package/tests/speciality_offer_ui.mjs +5 -5
- package/tests/speciality_offered.mjs +6 -5
- package/tests/speciality_session_skills.mjs +7 -7
- package/tests/speciality_skills.mjs +11 -11
- package/tests/speciality_walkthrough_ui.mjs +2 -2
- package/tests/sso_checkin_identity_spoof.mjs +2 -1
- package/tests/task_classifier_vocab.mjs +10 -0
- package/tests/task_cli.mjs +11 -3
- package/tests/task_criteria_unlink_db.mjs +2 -2
- package/tests/task_discipline_required.mjs +38 -16
- package/tests/task_goal_required.mjs +4 -4
- package/tests/task_needs_db.mjs +1 -1
- package/tests/tweak_replaces_artist_review.mjs +1 -1
- package/tests/update_pill.mjs +153 -2
- package/tests/upkeep_home.mjs +1 -1
- package/tests/v3_c1_options_proof.mjs +145 -0
- package/tests/visual_review.mjs +3 -3
- package/tests/watch_applications_queue.mjs +8 -5
- package/tests/watch_sealed_floor.mjs +3 -1
- package/tests/welcome_boot.mjs +12 -9
- package/tests/welcome_boot_browser.mjs +13 -13
- package/tests/worktree_helper.mjs +16 -1
|
@@ -43,8 +43,8 @@ try {
|
|
|
43
43
|
await q(`INSERT INTO versions (id,name,status,track) VALUES ($1,'names','shipped','internal') ON CONFLICT (id) DO NOTHING`, [VER]);
|
|
44
44
|
// T2 first and with no name: the read must order by id and keep a null name.
|
|
45
45
|
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline,short_name)
|
|
46
|
-
VALUES ($1,$3,'Payout approvals','backlog',10,'feature','
|
|
47
|
-
($2,$3,'#12','backlog',10,'feature','
|
|
46
|
+
VALUES ($1,$3,'Payout approvals','backlog',10,'feature','artisan','payout approvals'),
|
|
47
|
+
($2,$3,'#12','backlog',10,'feature','artisan',NULL)`, [T1, T2, VER]);
|
|
48
48
|
const holding = await blockers.createBlocker({ title: 'names fixture: holds two', bodyMd: '', source: 'test', sourceRef: `${REF}-a` });
|
|
49
49
|
const empty = await blockers.createBlocker({ title: 'names fixture: holds none', bodyMd: '', source: 'test', sourceRef: `${REF}-b` });
|
|
50
50
|
await blockers.linkBlockerToTask({ blockerId: holding.id, taskId: T2 }); // linked out of id order on purpose
|
|
@@ -280,7 +280,7 @@ const BUILDER = {
|
|
|
280
280
|
total_credits: 12,
|
|
281
281
|
karma: 3,
|
|
282
282
|
created_at: '2026-01-01T00:00:00.000Z',
|
|
283
|
-
preferred_disciplines: ['
|
|
283
|
+
preferred_disciplines: ['artisan'],
|
|
284
284
|
// The private columns the whitelist exists to keep out. Sentinel values so a
|
|
285
285
|
// raw-text scan of the serialized body bites even if a key were renamed.
|
|
286
286
|
anthropic_email: 'SENTINEL-anthropic-email',
|
|
@@ -23,31 +23,31 @@ function t(name, fn) {
|
|
|
23
23
|
|
|
24
24
|
// A claimable feed row (only the fields capNewcomerTasks reads).
|
|
25
25
|
const nc = (id, discipline) => ({ id, discipline, newcomer_friendly: true });
|
|
26
|
-
const real = (id, discipline = '
|
|
26
|
+
const real = (id, discipline = 'artisan') => ({ id, discipline, newcomer_friendly: false });
|
|
27
27
|
const ids = (rows) => rows.map((r) => r.id);
|
|
28
28
|
|
|
29
|
-
// ---- the reported case: 3 artist + 2
|
|
30
|
-
t('caps an artist+
|
|
29
|
+
// ---- the reported case: 3 artist + 2 artisan newcomer seats (was ideator before task 1005141 folded it into artist) ----
|
|
30
|
+
t('caps an artist+artisan pool of 5 down to 3, both lanes represented', () => {
|
|
31
31
|
const feed = [
|
|
32
32
|
nc(1259, 'artist'), nc(1260, 'artist'), nc(1261, 'artist'),
|
|
33
|
-
nc(1176, '
|
|
33
|
+
nc(1176, 'artisan'), nc(1177, 'artisan'),
|
|
34
34
|
];
|
|
35
35
|
const out = capNewcomerTasks(feed);
|
|
36
36
|
assert.equal(out.length, 3, 'exactly 3 newcomer rows kept');
|
|
37
|
-
// Round-robin SELECTS artist[0]=1259,
|
|
37
|
+
// Round-robin SELECTS artist[0]=1259, artisan[0]=1176, artist[1]=1260; the
|
|
38
38
|
// output preserves INPUT order (we filter the ordered array), so 1260 (which
|
|
39
39
|
// came before 1176 in the feed) lands ahead of it.
|
|
40
40
|
assert.deepEqual(ids(out), [1259, 1260, 1176]);
|
|
41
41
|
// Both disciplines are represented before a duplicate fills the third slot.
|
|
42
42
|
const disciplines = new Set(out.map((r) => r.discipline));
|
|
43
|
-
assert.ok(disciplines.has('artist') && disciplines.has('
|
|
43
|
+
assert.ok(disciplines.has('artist') && disciplines.has('artisan'));
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
// ---- non-newcomer (real) rows are never dropped ----
|
|
47
47
|
t('real version tasks pass through untouched and unbounded', () => {
|
|
48
48
|
const feed = [
|
|
49
49
|
real(900), real(901), real(902), real(903),
|
|
50
|
-
nc(1259, 'artist'), nc(1260, 'artist'), nc(1261, 'artist'), nc(1176, '
|
|
50
|
+
nc(1259, 'artist'), nc(1260, 'artist'), nc(1261, 'artist'), nc(1176, 'artisan'),
|
|
51
51
|
];
|
|
52
52
|
const out = capNewcomerTasks(feed);
|
|
53
53
|
const reals = out.filter((r) => r.newcomer_friendly !== true);
|
|
@@ -59,19 +59,19 @@ t('real version tasks pass through untouched and unbounded', () => {
|
|
|
59
59
|
// ---- ordering is preserved (we filter the input array, not re-sort) ----
|
|
60
60
|
t('preserves original feed ordering across mixed rows', () => {
|
|
61
61
|
const feed = [
|
|
62
|
-
real(900), nc(1259, 'artist'), real(901), nc(1176, '
|
|
63
|
-
nc(1260, 'artist'), nc(1261, 'artist'), nc(1177, '
|
|
62
|
+
real(900), nc(1259, 'artist'), real(901), nc(1176, 'artisan'),
|
|
63
|
+
nc(1260, 'artist'), nc(1261, 'artist'), nc(1177, 'artisan'),
|
|
64
64
|
];
|
|
65
65
|
const out = capNewcomerTasks(feed);
|
|
66
|
-
// Kept newcomer ids (round-robin over [artist,
|
|
67
|
-
// artist[0]=1259,
|
|
66
|
+
// Kept newcomer ids (round-robin over [artist, artisan] groups by appearance):
|
|
67
|
+
// artist[0]=1259, artisan[0]=1176, artist[1]=1260.
|
|
68
68
|
// Output keeps input order, dropping only un-kept newcomer rows (1261, 1177).
|
|
69
69
|
assert.deepEqual(ids(out), [900, 1259, 901, 1176, 1260]);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
// ---- no-op when at or under the cap ----
|
|
73
73
|
t('returns the list unchanged when newcomer rows <= cap', () => {
|
|
74
|
-
const feed = [nc(1, 'artist'), nc(2, '
|
|
74
|
+
const feed = [nc(1, 'artist'), nc(2, 'artisan'), real(3)];
|
|
75
75
|
const out = capNewcomerTasks(feed);
|
|
76
76
|
assert.equal(out.length, 3);
|
|
77
77
|
assert.deepEqual(ids(out), [1, 2, 3]);
|
|
@@ -106,7 +106,7 @@ t('tolerates non-array input', () => {
|
|
|
106
106
|
|
|
107
107
|
// ---- custom cap argument ----
|
|
108
108
|
t('honors a custom cap', () => {
|
|
109
|
-
const feed = [nc(1, 'artist'), nc(2, '
|
|
109
|
+
const feed = [nc(1, 'artist'), nc(2, 'artisan'), nc(3, 'artist'), nc(4, 'artisan')];
|
|
110
110
|
assert.equal(capNewcomerTasks(feed, 2).length, 2);
|
|
111
111
|
assert.deepEqual(ids(capNewcomerTasks(feed, 2)), [1, 2]);
|
|
112
112
|
});
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// tests/character_creation.mjs — character creation and the three sprites (task 1005147,
|
|
2
|
+
// BONGOS-V3 G2, criterion you-are-a-character-in-first-person).
|
|
3
|
+
//
|
|
4
|
+
// A builder picks one of three characters when they join, and the pick lives on their
|
|
5
|
+
// ACCOUNT (core_297, PUT /me/character). These cases run the REAL craft-mode.js in a
|
|
6
|
+
// small DOM stub and drive it the way the shell does once /me answers: no character
|
|
7
|
+
// opens character creation; a pick is saved and worn; a held character is the only one
|
|
8
|
+
// offered and the top bar wears its sprite; changing it is signing out. The server half
|
|
9
|
+
// (migration, route, db write) is pinned beside them.
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { test } from 'node:test';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import vm from 'node:vm';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { realOTB } from './helpers.mjs';
|
|
18
|
+
|
|
19
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
const HALL = path.join(ROOT, 'modules/hall-ui/public');
|
|
21
|
+
const CRAFT = fs.readFileSync(path.join(HALL, 'craft-mode.js'), 'utf8');
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
|
|
24
|
+
function el(tag) {
|
|
25
|
+
const attrs = {}, listeners = {};
|
|
26
|
+
const e = {
|
|
27
|
+
tagName: tag, id: '', className: '', dataset: {}, children: [], parentNode: null, _html: '', textContent: '',
|
|
28
|
+
classList: { _s: new Set(), add(c) { this._s.add(c); }, remove(c) { this._s.delete(c); }, toggle(c, on) { if (on) this._s.add(c); else this._s.delete(c); }, contains(c) { return this._s.has(c); } },
|
|
29
|
+
setAttribute(k, v) { attrs[k] = String(v); }, getAttribute(k) { return k in attrs ? attrs[k] : null; },
|
|
30
|
+
hasAttribute(k) { return k in attrs; }, removeAttribute(k) { delete attrs[k]; },
|
|
31
|
+
addEventListener(t, fn) { (listeners[t] ||= []).push(fn); },
|
|
32
|
+
fire(t, ev) { for (const fn of listeners[t] || []) fn(ev); },
|
|
33
|
+
appendChild(c) { c.parentNode = e; e.children.push(c); return c; },
|
|
34
|
+
removeChild(c) { e.children = e.children.filter((x) => x !== c); c.parentNode = null; },
|
|
35
|
+
contains() { return false; },
|
|
36
|
+
focus() {},
|
|
37
|
+
querySelector(sel) { return e._q(sel); },
|
|
38
|
+
querySelectorAll() { return []; },
|
|
39
|
+
set innerHTML(v) { e._html = String(v); e._err = null; }, get innerHTML() { return e._html; },
|
|
40
|
+
_q(sel) {
|
|
41
|
+
if (sel === '[data-char-error]' && /data-char-error/.test(e._html)) return (e._err ||= el('P'));
|
|
42
|
+
return null;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
return e;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function boot({ store = {}, session = {}, put = () => ({ ok: true, status: 200, data: null }) } = {}) {
|
|
49
|
+
const root = el('HTML');
|
|
50
|
+
const body = el('BODY');
|
|
51
|
+
const requests = [];
|
|
52
|
+
const keyListeners = [];
|
|
53
|
+
const docObj = {
|
|
54
|
+
body, documentElement: root, activeElement: null, readyState: 'complete',
|
|
55
|
+
getElementById(id) {
|
|
56
|
+
if (id === 'craft-picker') return body.children.find((c) => c.id === 'craft-picker') || null;
|
|
57
|
+
return null;
|
|
58
|
+
},
|
|
59
|
+
querySelector: () => null, querySelectorAll: () => [],
|
|
60
|
+
createElement: (t) => el(t.toUpperCase()),
|
|
61
|
+
addEventListener(t, fn) { if (t === 'keydown') keyListeners.push(fn); },
|
|
62
|
+
removeEventListener(t, fn) { const i = keyListeners.indexOf(fn); if (i >= 0) keyListeners.splice(i, 1); },
|
|
63
|
+
dispatchEvent() { return true; },
|
|
64
|
+
};
|
|
65
|
+
const mk = (obj) => ({
|
|
66
|
+
getItem: (k) => (k in obj ? obj[k] : null),
|
|
67
|
+
setItem: (k, v) => { obj[k] = String(v); },
|
|
68
|
+
removeItem: (k) => { delete obj[k]; },
|
|
69
|
+
});
|
|
70
|
+
const win = {
|
|
71
|
+
OTB: realOTB(),
|
|
72
|
+
localStorage: mk(store), sessionStorage: mk(session),
|
|
73
|
+
location: { search: '', assign() {} },
|
|
74
|
+
matchMedia: () => ({ matches: false }),
|
|
75
|
+
BongosClient: {
|
|
76
|
+
createClient: () => ({
|
|
77
|
+
request(method, url, opts) {
|
|
78
|
+
requests.push({ method, url, body: opts && opts.body });
|
|
79
|
+
return Promise.resolve(put(method, url, opts));
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
const sandbox = {
|
|
85
|
+
window: win, document: docObj, console, Promise,
|
|
86
|
+
CustomEvent: class { constructor(t, i) { this.type = t; Object.assign(this, i); } },
|
|
87
|
+
};
|
|
88
|
+
sandbox.globalThis = sandbox;
|
|
89
|
+
vm.createContext(sandbox);
|
|
90
|
+
vm.runInContext(CRAFT, sandbox, { filename: 'craft-mode.js' });
|
|
91
|
+
const picker = () => body.children.find((c) => c.id === 'craft-picker') || null;
|
|
92
|
+
const signedIn = { authed: true };
|
|
93
|
+
return { win, root, body, store, session, requests, keyListeners, picker, signedIn, C: win.OTBCraftMode };
|
|
94
|
+
}
|
|
95
|
+
const builder = { id: 7, github_login: 'ada', main_discipline: null };
|
|
96
|
+
const flush = () => new Promise((r) => setImmediate(r));
|
|
97
|
+
|
|
98
|
+
test('no character yet: character creation opens, three characters, nothing to dismiss', () => {
|
|
99
|
+
const t = boot();
|
|
100
|
+
assert.equal(t.C.characterGate(t.signedIn, { builder, character: null }), true, 'the gate holds the welcome and the picker back');
|
|
101
|
+
const p = t.picker();
|
|
102
|
+
assert.ok(p, 'the creation screen is up');
|
|
103
|
+
assert.match(p.innerHTML, /Choose your character/);
|
|
104
|
+
const doors = [...p.innerHTML.matchAll(/data-char-pick="([a-z]+)"/g)].map((m) => m[1]);
|
|
105
|
+
assert.deepEqual(doors, ['artisan', 'artist', 'patron'], 'Artisan, Artist, Patron — three, in that order');
|
|
106
|
+
for (const id of doors) assert.ok(p.innerHTML.includes(`/builders/characters/${id}.png`), `${id} stands on its sprite`);
|
|
107
|
+
assert.ok(!/data-craft-close/.test(p.innerHTML), 'no close button: the pick is the way through');
|
|
108
|
+
// Escape does not dismiss it.
|
|
109
|
+
let prevented = false;
|
|
110
|
+
for (const fn of t.keyListeners.slice()) fn({ key: 'Escape', preventDefault() { prevented = true; } });
|
|
111
|
+
assert.ok(prevented && t.picker(), 'Escape is swallowed and the screen stays');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('an artist-led builder starts on the Artist door', () => {
|
|
115
|
+
const t = boot();
|
|
116
|
+
t.C.characterGate(t.signedIn, { builder: { ...builder, main_discipline: 'artist' }, character: null });
|
|
117
|
+
assert.match(t.picker().innerHTML, /craft-door--pre[^"]*" type="button" data-char-pick="artist"/);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('picking saves the character to the account and the hall wears it', async () => {
|
|
121
|
+
const t = boot({ put: (m, u, o) => ({ ok: true, status: 200, data: { character: o.body.character } }) });
|
|
122
|
+
t.C.characterGate(t.signedIn, { builder, character: null });
|
|
123
|
+
const target = { closest: (sel) => (sel === 'button[data-char-pick]' ? { getAttribute: () => 'artist' } : null) };
|
|
124
|
+
t.picker().fire('click', { target });
|
|
125
|
+
await flush();
|
|
126
|
+
assert.deepEqual(JSON.parse(JSON.stringify(t.requests[0])), { method: 'PUT', url: '/api/bongos/me/character', body: { character: 'artist' } });
|
|
127
|
+
assert.equal(t.picker(), null, 'the screen closes');
|
|
128
|
+
assert.equal(t.root.dataset.craft, 'artist', 'the hall wears the character');
|
|
129
|
+
assert.equal(t.store['hall-character'], 'artist', 'remembered for the next first paint');
|
|
130
|
+
assert.equal(t.C.character(), 'artist');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('a failed save keeps the screen up and says so', async () => {
|
|
134
|
+
const t = boot({ put: () => ({ ok: false, status: 500, data: null }) });
|
|
135
|
+
t.C.characterGate(t.signedIn, { builder, character: null });
|
|
136
|
+
const target = { closest: () => ({ getAttribute: () => 'artisan' }) };
|
|
137
|
+
t.picker().fire('click', { target });
|
|
138
|
+
await flush(); await flush();
|
|
139
|
+
assert.ok(t.picker(), 'still choosing');
|
|
140
|
+
const err = t.picker().querySelector('[data-char-error]');
|
|
141
|
+
assert.match(err.textContent, /could not be saved/);
|
|
142
|
+
assert.equal(err.hasAttribute('hidden'), false);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('a held character is the only one offered, and the top bar wears its sprite', () => {
|
|
146
|
+
const t = boot();
|
|
147
|
+
assert.equal(t.C.characterGate(t.signedIn, { builder, character: 'artisan' }), false, 'nothing to create: the welcome may show');
|
|
148
|
+
assert.equal(t.picker(), null);
|
|
149
|
+
assert.equal(t.root.dataset.craft, 'artisan');
|
|
150
|
+
assert.equal(t.C.offered('artisan'), true);
|
|
151
|
+
assert.equal(t.C.offered('artist'), false, 'another character cannot be worn without signing out');
|
|
152
|
+
assert.equal(t.C.offered('patron'), false);
|
|
153
|
+
const sw = t.C.switchHtml();
|
|
154
|
+
assert.match(sw, /craft-switch--character/);
|
|
155
|
+
assert.match(sw, /data-craft-me/);
|
|
156
|
+
assert.match(sw, /\/builders\/characters\/artisan\.png/);
|
|
157
|
+
assert.ok(!/data-craft-opt=/.test(sw), 'the three-way switch is gone');
|
|
158
|
+
// A session's stored mode from before the character cannot override it.
|
|
159
|
+
t.session['hall-craft'] = 'patron';
|
|
160
|
+
assert.equal(t.C.current(), 'artisan');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('the sprite is drawn on first paint from the last held character, before /me answers', () => {
|
|
164
|
+
const t = boot({ store: { 'hall-character': 'patron' } });
|
|
165
|
+
assert.match(t.C.switchHtml(), /\/builders\/characters\/patron\.png/);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('an older core that says nothing about characters changes nothing', () => {
|
|
169
|
+
const t = boot();
|
|
170
|
+
assert.equal(t.C.characterGate(t.signedIn, { builder }), false);
|
|
171
|
+
assert.equal(t.picker(), null);
|
|
172
|
+
assert.equal(t.C.character(), undefined);
|
|
173
|
+
assert.equal(t.C.offered('artist'), true, 'the three crafts stay offered as before');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('a visitor who is not signed in is never asked', () => {
|
|
177
|
+
const t = boot();
|
|
178
|
+
assert.equal(t.C.characterGate({ authed: false }, { character: null }), false);
|
|
179
|
+
assert.equal(t.picker(), null);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('the character section never writes `hidden` onto the nav: a character only marks [data-craft-off]', () => {
|
|
183
|
+
const start = CRAFT.indexOf('// ---------- character creation (task 1005147');
|
|
184
|
+
const end = CRAFT.indexOf('// ---------- the shell\'s three calls');
|
|
185
|
+
const section = CRAFT.slice(start, end);
|
|
186
|
+
assert.ok(start > 0 && end > start);
|
|
187
|
+
assert.ok(!/nav-item|app-sidebar/.test(section), 'the character code does not touch the nav itself; applyNav does');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('the owner\'s three sprites are on disk, one per character', () => {
|
|
191
|
+
for (const id of ['artisan', 'artist', 'patron']) {
|
|
192
|
+
const f = path.join(HALL, 'characters', id + '.png');
|
|
193
|
+
assert.ok(fs.existsSync(f), `${id}.png`);
|
|
194
|
+
const head = fs.readFileSync(f).subarray(0, 8);
|
|
195
|
+
assert.deepEqual([...head], [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], `${id}.png is a PNG`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ---------- the server half ----------
|
|
200
|
+
test('core_297 adds the account column, and the CHECK names exactly the three characters', () => {
|
|
201
|
+
const sql = fs.readFileSync(path.join(ROOT, 'migrations/core_297_builder_character.sql'), 'utf8');
|
|
202
|
+
assert.match(sql, /ADD COLUMN IF NOT EXISTS game_character text;/);
|
|
203
|
+
assert.match(sql, /game_character IN \('artisan', 'artist', 'patron'\)/);
|
|
204
|
+
assert.match(sql, /INSERT INTO schema_migrations \(version\) VALUES \('core_297_builder_character'\)/);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('PUT /me/character is a self-service write behind requireBuilder, and /me reports the character fail-open', () => {
|
|
208
|
+
const src = fs.readFileSync(path.join(ROOT, 'src/bongos/routes/me.js'), 'utf8');
|
|
209
|
+
assert.match(src, /router\.put\('\/me\/character', auth\.requireBuilder,/);
|
|
210
|
+
assert.match(src, /db\.setBuilderCharacter\(req\.builder\.id, raw\)/, 'only ever the caller\'s own row');
|
|
211
|
+
assert.match(src, /character: await characterP,/);
|
|
212
|
+
assert.match(src, /try \{ return await db\.getBuilderCharacter\(req\.builder\.id\); \} catch \(_\) \{ return null; \}/);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// The real route over HTTP: the real me.js router and error envelope, with only the
|
|
216
|
+
// sign-in (auth.requireBuilder) and the two db calls stood in for.
|
|
217
|
+
test('PUT /me/character over HTTP: sets, clears, and refuses with stable error codes', async () => {
|
|
218
|
+
const express = require('express');
|
|
219
|
+
const authPath = require.resolve('../src/bongos/auth.js');
|
|
220
|
+
const dbPath = require.resolve('../src/bongos/db.js');
|
|
221
|
+
const mePath = require.resolve('../src/bongos/routes/me.js');
|
|
222
|
+
const auth = require(authPath);
|
|
223
|
+
const db = require(dbPath);
|
|
224
|
+
const saved = { requireBuilder: auth.requireBuilder, setBuilderCharacter: db.setBuilderCharacter };
|
|
225
|
+
const writes = [];
|
|
226
|
+
auth.requireBuilder = (req, res, next) => {
|
|
227
|
+
if (req.get('x-test-signed-out')) return res.status(401).json({ error: { code: 'unauthorized' } });
|
|
228
|
+
req.builder = { id: 41, rank: 'xenos' };
|
|
229
|
+
next();
|
|
230
|
+
};
|
|
231
|
+
db.setBuilderCharacter = async (id, c) => {
|
|
232
|
+
writes.push([id, c]);
|
|
233
|
+
if (c !== null && !['artisan', 'artist', 'patron'].includes(c)) { const e = new Error('x'); e.code = 'invalid_character'; throw e; }
|
|
234
|
+
return c;
|
|
235
|
+
};
|
|
236
|
+
delete require.cache[mePath];
|
|
237
|
+
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
238
|
+
const app = express();
|
|
239
|
+
app.use(express.json());
|
|
240
|
+
app.use(attachFail);
|
|
241
|
+
app.use(require(mePath)());
|
|
242
|
+
const server = app.listen(0);
|
|
243
|
+
const base = `http://127.0.0.1:${server.address().port}`;
|
|
244
|
+
const put = (body, headers = {}) => fetch(base + '/me/character', {
|
|
245
|
+
method: 'PUT', headers: { 'content-type': 'application/json', ...headers }, body: JSON.stringify(body),
|
|
246
|
+
}).then(async (r) => ({ status: r.status, body: await r.json() }));
|
|
247
|
+
try {
|
|
248
|
+
let r = await put({ character: 'artist' });
|
|
249
|
+
assert.equal(r.status, 200);
|
|
250
|
+
assert.equal(r.body.character, 'artist');
|
|
251
|
+
r = await put({ character: null });
|
|
252
|
+
assert.equal(r.status, 200, 'null is the deliberate clear (change character)');
|
|
253
|
+
assert.equal(r.body.character, null);
|
|
254
|
+
r = await put({});
|
|
255
|
+
assert.equal(r.status, 400);
|
|
256
|
+
assert.equal(r.body.error.code, 'character_required', 'a stable slug, not prose');
|
|
257
|
+
r = await put({ character: 'emperor' });
|
|
258
|
+
assert.equal(r.status, 422);
|
|
259
|
+
assert.equal(r.body.error.code, 'invalid_character');
|
|
260
|
+
r = await put({ character: 7 });
|
|
261
|
+
assert.equal(r.status, 400, 'a non-string is refused by the body schema');
|
|
262
|
+
r = await put({ character: 'artist', rank: 'archon' });
|
|
263
|
+
assert.equal(r.status, 400, 'strict: an unknown field (an attempt to write anything else) is refused');
|
|
264
|
+
r = await put({ character: 'artist' }, { 'x-test-signed-out': '1' });
|
|
265
|
+
assert.equal(r.status, 401, 'signed out never reaches the write');
|
|
266
|
+
assert.deepEqual(writes.map((w) => w[0]), [41, 41, 41], 'every write that ran was the caller\'s own row');
|
|
267
|
+
} finally {
|
|
268
|
+
server.close();
|
|
269
|
+
auth.requireBuilder = saved.requireBuilder;
|
|
270
|
+
db.setBuilderCharacter = saved.setBuilderCharacter;
|
|
271
|
+
delete require.cache[mePath];
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('setBuilderCharacter writes a character, clears with null, normalizes an old spelling, refuses anything else', async () => {
|
|
276
|
+
const poolPath = require.resolve('../src/bongos/pool.js');
|
|
277
|
+
const dbPath = require.resolve('../src/bongos/db.js');
|
|
278
|
+
const seen = [];
|
|
279
|
+
const real = require.cache[poolPath];
|
|
280
|
+
require.cache[poolPath] = { id: poolPath, filename: poolPath, loaded: true, exports: { pool: { query: async (sql, params) => { seen.push(params); return { rows: [{ game_character: params[1] }] }; } } } };
|
|
281
|
+
delete require.cache[dbPath];
|
|
282
|
+
try {
|
|
283
|
+
const db = require(dbPath);
|
|
284
|
+
assert.equal(await db.setBuilderCharacter(7, 'artist'), 'artist');
|
|
285
|
+
assert.equal(await db.setBuilderCharacter(7, null), null);
|
|
286
|
+
assert.equal(await db.setBuilderCharacter(7, 'ruler'), 'patron', 'the sprite\'s old name lands as Patron');
|
|
287
|
+
await assert.rejects(db.setBuilderCharacter(7, 'governor-general'), { code: 'invalid_character' });
|
|
288
|
+
await assert.rejects(db.setBuilderCharacter(7, 'ui'), { code: 'invalid_character' }, 'a discipline is not a character');
|
|
289
|
+
assert.deepEqual(seen.map((p) => p[0]), [7, 7, 7], 'every write is the one builder\'s own row');
|
|
290
|
+
} finally {
|
|
291
|
+
if (real) require.cache[poolPath] = real; else delete require.cache[poolPath];
|
|
292
|
+
delete require.cache[dbPath];
|
|
293
|
+
}
|
|
294
|
+
});
|
|
@@ -53,8 +53,8 @@ try {
|
|
|
53
53
|
// Archon so no rank/newcomer gate interferes; the gate under test is OPEN_BLOCKER.
|
|
54
54
|
await q(`INSERT INTO builders (id,github_id,github_login,display_name,rank) VALUES ($1,'ob-w','ob-worker','OB Worker','archon')`, [WORKER]);
|
|
55
55
|
// TRAP: ready (feed-hidden) with an open blocker linked. CONTROL: ready, clean.
|
|
56
|
-
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'trap','ready',10,'feature','
|
|
57
|
-
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'control','ready',10,'feature','
|
|
56
|
+
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'trap','ready',10,'feature','artisan')`, [TRAP, VER]);
|
|
57
|
+
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'control','ready',10,'feature','artisan')`, [CONTROL, VER]);
|
|
58
58
|
await q(`INSERT INTO blockers (id,title,status) VALUES ($1,'Owner must flip the switch','open')`, [BLOCKER]);
|
|
59
59
|
// blocker_ref is NOT NULL and part of the key; linkBlockerToTask writes 'blocker:<id>'.
|
|
60
60
|
await q(`INSERT INTO task_blockers (task_id,blocker_id,blocker_ref) VALUES ($1,$2,$3)`, [TRAP, BLOCKER, `blocker:${BLOCKER}`]);
|
|
@@ -60,11 +60,11 @@ try {
|
|
|
60
60
|
// Archon so no rank/newcomer gate interferes; the gate under test is REBASE_REQUIRED.
|
|
61
61
|
await q(`INSERT INTO builders (id,github_id,github_login,display_name,rank) VALUES ($1,'cg-w','cg-worker','CG Worker','archon')`, [WORKER]);
|
|
62
62
|
// STALLED: a confirmed task owned by WORKER, flagged needs_rebase.
|
|
63
|
-
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,shipped_by,discipline) VALUES ($1,$2,'stalled','confirmed',10,'feature',$3,'
|
|
63
|
+
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,shipped_by,discipline) VALUES ($1,$2,'stalled','confirmed',10,'feature',$3,'artisan')`, [STALLED, VER, WORKER]);
|
|
64
64
|
await q(`INSERT INTO claims (task_id,builder_id,claimed_at) VALUES ($1,$2, now())`, [STALLED, WORKER]);
|
|
65
65
|
await db.flagTaskNeedsRebase(String(STALLED), 'strand:no_branch_no_tip');
|
|
66
66
|
// FRESH: a ready, claimable task the worker will try to pick up.
|
|
67
|
-
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'fresh','ready',10,'feature','
|
|
67
|
+
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'fresh','ready',10,'feature','artisan')`, [FRESH, VER]);
|
|
68
68
|
|
|
69
69
|
// 1) With STALLED flagged, claiming FRESH is refused with REBASE_REQUIRED.
|
|
70
70
|
let threw = null;
|
|
@@ -116,13 +116,13 @@ try {
|
|
|
116
116
|
await q(`UPDATE claims SET released_at = now() WHERE task_id = $1 AND released_at IS NULL`, [FRESH]);
|
|
117
117
|
|
|
118
118
|
const seedStrand = async (id, reason, ageHours) => {
|
|
119
|
-
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,shipped_by,discipline) VALUES ($1,$2,'aged','confirmed',10,'feature',$3,'
|
|
119
|
+
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,shipped_by,discipline) VALUES ($1,$2,'aged','confirmed',10,'feature',$3,'artisan')`, [id, VER, WORKER]);
|
|
120
120
|
await q(`INSERT INTO claims (task_id,builder_id,claimed_at) VALUES ($1,$2, now())`, [id, WORKER]);
|
|
121
121
|
await db.flagTaskNeedsRebase(String(id), reason);
|
|
122
122
|
await q(`UPDATE tasks SET needs_rebase_at = now() - ($2::bigint * interval '1 hour') WHERE id = $1`, [id, ageHours]);
|
|
123
123
|
};
|
|
124
124
|
|
|
125
|
-
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'fresh2','ready',10,'feature','
|
|
125
|
+
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'fresh2','ready',10,'feature','artisan')`, [FRESH2, VER]);
|
|
126
126
|
|
|
127
127
|
// 4) An unprovable strand AGED past the grace no longer refuses a claim.
|
|
128
128
|
await seedStrand(AGED, 'strand:no_branch_no_tip', 48);
|
|
@@ -154,7 +154,7 @@ try {
|
|
|
154
154
|
// 7) task 1004370 — the one task the gate lets through: a P1 kind=infra fix,
|
|
155
155
|
// the shape a CI-unblock is filed in. With AGED_OTHER still gating, claiming
|
|
156
156
|
// it succeeds; FRESH2 (a P1 feature) was refused in 6), so both halves count.
|
|
157
|
-
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,priority,discipline) VALUES ($1,$2,'ci fix','ready',10,'infra',1,'
|
|
157
|
+
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,priority,discipline) VALUES ($1,$2,'ci fix','ready',10,'infra',1,'artisan')`, [CIFIX, VER]);
|
|
158
158
|
const ciClaim = await db.claimTask({ taskId: String(CIFIX), builderId: String(WORKER), worktreeName: 'cg-cifix' });
|
|
159
159
|
ok(ciClaim && Number(ciClaim.task_id) === CIFIX,
|
|
160
160
|
'a P1 infra task is claimable while a rebase is owed — the CI-unblock fix is never gated out');
|
|
@@ -171,7 +171,7 @@ try {
|
|
|
171
171
|
await q(`UPDATE tasks SET status='shipped', needs_rebase=false WHERE id=$1`, [AGED_OTHER]);
|
|
172
172
|
await q(`UPDATE claims SET released_at = now() WHERE task_id = ANY($1::bigint[]) AND released_at IS NULL`, [[CIFIX]]);
|
|
173
173
|
await seedStrand(GUARDED, 'strand:branch_modifies_executed_code:.gitattributes', 1);
|
|
174
|
-
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'fresh3','ready',10,'feature','
|
|
174
|
+
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,discipline) VALUES ($1,$2,'fresh3','ready',10,'feature','artisan')`, [FRESH3, VER]);
|
|
175
175
|
const claim3 = await db.claimTask({ taskId: String(FRESH3), builderId: String(WORKER), worktreeName: 'cg-fresh3' });
|
|
176
176
|
ok(claim3 && Number(claim3.task_id) === FRESH3, 'a confirmed task parked on a server-side guard does not refuse a new claim');
|
|
177
177
|
ok((await db.needsRebaseTasksForBuilder(String(WORKER))).some((t) => Number(t.id) === GUARDED),
|
|
@@ -764,7 +764,7 @@ t('the materialise-at-claim trigger is DATA: the ui mode opts in, every other di
|
|
|
764
764
|
const modes = JSON.parse(readFileSync(join(ROOT, 'scripts', 'bongos', 'discipline-modes.json'), 'utf8')).modes;
|
|
765
765
|
assert.equal(modes.ui.materializeModuleSkills, true, 'a ui claim brings the skills /design names');
|
|
766
766
|
assert.equal(modes.ui.skill, 'design', 'and still routes to /design');
|
|
767
|
-
for (const d of ['
|
|
767
|
+
for (const d of ['artisan', 'artist']) {
|
|
768
768
|
assert.ok(!modes[d].materializeModuleSkills, `${d} did not opt in, so its claim output is unchanged`);
|
|
769
769
|
}
|
|
770
770
|
});
|
|
@@ -951,6 +951,69 @@ t("the core's OWN deny list covers every file the CLI reads a session from, and
|
|
|
951
951
|
}
|
|
952
952
|
});
|
|
953
953
|
|
|
954
|
+
// ---- task 1005186: the runner's own login file is denied like the builder's ----
|
|
955
|
+
// Task 1005095 moved an Autobongos runner's session, heartbeat and run log under a folder of its
|
|
956
|
+
// own, so every deny rule above (all under ~/.config) stopped short of it and a worker on a
|
|
957
|
+
// runner device could read the runner's login.
|
|
958
|
+
const hostProgram = require('../scripts/bongos/autobongos-host.js');
|
|
959
|
+
const RUNNER_RULE = 'Read(~/.cloudbongos-runner/config/**)';
|
|
960
|
+
|
|
961
|
+
t('the runner deny rule names the folder the device program actually uses, however the runner was installed', () => {
|
|
962
|
+
// Asked of the device program with a stand-in home, so a renamed folder fails HERE rather
|
|
963
|
+
// than leaving a rule that guards a place nothing writes to.
|
|
964
|
+
const home = join(tmpdir(), 'runner-deny-home');
|
|
965
|
+
const ruleFor = (env) => `Read(~/${relative(home, hostProgram.hostPaths(env, home).configRoot).split(sep).join('/')}/**)`;
|
|
966
|
+
assert.equal(ruleFor({}), RUNNER_RULE, 'the default install');
|
|
967
|
+
// AUTOBONGOS_HOME moves the runner's copy, logs and lock. It must NOT move the login:
|
|
968
|
+
// a login an environment variable can relocate is one the rule does not cover.
|
|
969
|
+
for (const custom of [join(home, 'elsewhere'), join(tmpdir(), 'not-even-under-home'), join(home, '.cloudbongos-runner')]) {
|
|
970
|
+
const env = { AUTOBONGOS_HOME: custom };
|
|
971
|
+
assert.equal(ruleFor(env), RUNNER_RULE, `AUTOBONGOS_HOME=${custom}`);
|
|
972
|
+
assert.equal(hostProgram.hostPaths(env, home).root, custom, 'while the rest of the runner does move');
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
t('the runner deny rule covers the files a runner keeps there: its session, the store, its heartbeat', () => {
|
|
977
|
+
// The same cli-lib and supervisor a runner runs, under the runner's config root.
|
|
978
|
+
const home = join(tmpdir(), 'runner-deny-home');
|
|
979
|
+
const root = hostProgram.hostPaths({}, home).configRoot;
|
|
980
|
+
const probe = `
|
|
981
|
+
const cli = require(${JSON.stringify(join(ROOT, 'scripts', 'bongos', 'cli-lib.js'))});
|
|
982
|
+
const run = require(${JSON.stringify(join(ROOT, 'scripts', 'bongos', 'autobongos-run.js'))});
|
|
983
|
+
process.stdout.write(JSON.stringify([...cli.SESSION_READ_PATHS, cli.sessionStoreDir(), run.heartbeatPath(), run.logPath()]));`;
|
|
984
|
+
const r = require('node:child_process').spawnSync(process.execPath, ['-e', probe], { encoding: 'utf8', env: { ...process.env, BONGOS_CONFIG_ROOT: root, AUTOBONGOS_HEARTBEAT_FILE: '', AUTOBONGOS_LOG_FILE: '' } });
|
|
985
|
+
assert.equal(r.status, 0, r.stderr);
|
|
986
|
+
const files = JSON.parse(r.stdout);
|
|
987
|
+
assert.ok(files.length >= 5);
|
|
988
|
+
const guarded = `${root}${sep}`; // what Read(<root>/**) matches
|
|
989
|
+
for (const f of files) assert.ok(f.startsWith(guarded), `${f} sits outside the denied folder`);
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
t("the core's own deny list and the starter both carry the runner rule", () => {
|
|
993
|
+
const core = JSON.parse(readFileSync(join(ROOT, '.claude', 'settings.json'), 'utf8')).permissions.deny;
|
|
994
|
+
assert.ok(core.includes(RUNNER_RULE), 'the core checkout is itself where a runner works');
|
|
995
|
+
assert.ok(starterDenyFor({}).includes(RUNNER_RULE));
|
|
996
|
+
assert.ok(starterDenyFor({ configDir: 'acme', envPrefix: 'ACME' }).includes(RUNNER_RULE), 'at the same fixed place whatever the instance is called');
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
t('a checkout-materialized instance keeps the runner rule at its fixed place through the rebrand', () => {
|
|
1000
|
+
// An instance whose own name is the word in the folder's name: the rebrand must not
|
|
1001
|
+
// carry the rule off to a folder no runner uses.
|
|
1002
|
+
for (const branding of [{ configDir: 'acme', envPrefix: 'ACME' }, { configDir: 'cloudbongos' }, { configDir: 'otb', envPrefix: 'OTB' }]) {
|
|
1003
|
+
const core = mkdtempSync(join(tmpdir(), 'w3-runner-core-'));
|
|
1004
|
+
const inst = mkdtempSync(join(tmpdir(), 'w3-runner-inst-'));
|
|
1005
|
+
mkdirSync(join(core, '.claude', 'skills'), { recursive: true });
|
|
1006
|
+
writeFileSync(join(core, '.claude', 'settings.json'), JSON.stringify({ permissions: { deny: [RUNNER_RULE] } }));
|
|
1007
|
+
mkdirSync(join(inst, 'config'), { recursive: true });
|
|
1008
|
+
writeFileSync(join(inst, 'config', 'branding.json'), JSON.stringify(branding));
|
|
1009
|
+
m.materializeClaude({ coreRoot: core, instanceDir: inst, dryRun: false });
|
|
1010
|
+
const deny = JSON.parse(readFileSync(join(inst, '.claude', 'settings.json'), 'utf8')).permissions.deny;
|
|
1011
|
+
assert.deepEqual(deny, [RUNNER_RULE], JSON.stringify(branding));
|
|
1012
|
+
rmSync(core, { recursive: true, force: true });
|
|
1013
|
+
rmSync(inst, { recursive: true, force: true });
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
|
|
954
1017
|
t('a checkout-materialized instance keeps the session-store rule at its fixed anchor', () => {
|
|
955
1018
|
// The rebrand rewrites every `.config/<core dir>` literal, and the store's dir is one of them —
|
|
956
1019
|
// but the store does not move with the brand, so a rebranded rule guards a dir nothing reads.
|
|
@@ -1011,6 +1074,17 @@ t('an instance still on the allow-only starter GAINS the deny rules on its next
|
|
|
1011
1074
|
f.cleanup();
|
|
1012
1075
|
});
|
|
1013
1076
|
|
|
1077
|
+
t('an instance that already has a deny list gains the runner rule at its next upgrade (task 1005186)', () => {
|
|
1078
|
+
// Every rule it had before this task, and not the new one.
|
|
1079
|
+
const had = topUpDenyFor({ configDir: 'acme' }).filter((r) => r !== RUNNER_RULE);
|
|
1080
|
+
const f = topUpFixture({ permissions: { allow: m.STARTER_ALLOW, deny: had } });
|
|
1081
|
+
const res = m.materializeClaude({ coreRoot: f.core, instanceDir: f.inst, dryRun: false });
|
|
1082
|
+
const after = JSON.parse(f.read());
|
|
1083
|
+
assert.deepEqual(res.settingsDenyAdded, [RUNNER_RULE], 'the one rule it was missing, and it is named');
|
|
1084
|
+
assert.deepEqual(after.permissions.deny, [...had, RUNNER_RULE], 'appended; nothing it had is moved or dropped');
|
|
1085
|
+
f.cleanup();
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1014
1088
|
t("an owner-edited settings.json keeps every rule of its own and only gains what's missing", () => {
|
|
1015
1089
|
const f = topUpFixture({
|
|
1016
1090
|
permissions: { allow: ['Bash(npm test:*)'], deny: ['Read(secrets/**)', 'Read(**/*.pem)'], ask: ['Bash(rm:*)'] },
|
|
@@ -60,9 +60,9 @@ try {
|
|
|
60
60
|
// Two still-confirmed tasks (unlanded) + one legitimately shipped task.
|
|
61
61
|
// discipline is required since core_247 (the column default 'unclassified' fails its CHECK).
|
|
62
62
|
await q(`INSERT INTO tasks (id,version_id,title,status,credits_reward,kind,shipped_by,discipline) VALUES
|
|
63
|
-
($1,$4,'confirmed A','confirmed',20,'feature',$2,'
|
|
64
|
-
($5,$4,'confirmed B','confirmed',15,'feature',$3,'
|
|
65
|
-
($6,$4,'shipped legit','shipped',30,'feature',$2,'
|
|
63
|
+
($1,$4,'confirmed A','confirmed',20,'feature',$2,'artisan'),
|
|
64
|
+
($5,$4,'confirmed B','confirmed',15,'feature',$3,'artisan'),
|
|
65
|
+
($6,$4,'shipped legit','shipped',30,'feature',$2,'artisan')`, [T_A, A, B, VER, T_B, T_S]);
|
|
66
66
|
// Seed the credits the OLD confirm-time rule would have paid (credit_log INSERT
|
|
67
67
|
// fires the total_credits trigger, exactly like a real award).
|
|
68
68
|
await q(`INSERT INTO credit_log (builder_id,task_id,delta,reason,description) VALUES
|
package/tests/collab_page.mjs
CHANGED
|
@@ -481,8 +481,8 @@ test('roleWord and roleLabel disagree exactly where they should', () => {
|
|
|
481
481
|
// Executed, not grepped: the acronym exception has to survive both ways, or
|
|
482
482
|
// "You asked uis" ships the next time a ui task files a help request.
|
|
483
483
|
const { roleLabel, roleWord } = domUtils();
|
|
484
|
-
assert.equal(roleLabel('
|
|
485
|
-
assert.equal(roleWord('
|
|
484
|
+
assert.equal(roleLabel('artisan'), 'Artisan');
|
|
485
|
+
assert.equal(roleWord('artisan'), 'artisan');
|
|
486
486
|
assert.equal(roleLabel('ui'), 'UI');
|
|
487
487
|
assert.equal(roleWord('ui'), 'UI', 'an acronym is not a common noun in any sentence');
|
|
488
488
|
assert.equal(roleLabel('unclassified'), 'No role yet');
|
|
@@ -524,13 +524,13 @@ test('the mingle sub-line reads as English for every pair, and never shows the c
|
|
|
524
524
|
const word = (d) => (OTB.roleIsUnset(d) ? '' : OTB.roleWord(d));
|
|
525
525
|
const line = (a, b) => L.pairSentence(word(a), word(b));
|
|
526
526
|
|
|
527
|
-
assert.equal(line('
|
|
528
|
-
assert.equal(line('artist', '
|
|
527
|
+
assert.equal(line('artisan', 'artist'), 'An artisan — you are an artist. Mingle pairs across roles.');
|
|
528
|
+
assert.equal(line('artist', 'artisan'), 'An artist — you are an artisan. Mingle pairs across roles.');
|
|
529
529
|
|
|
530
530
|
// 'ui' is the acronym exception roleWord() already makes, and it is why the article
|
|
531
531
|
// cannot be a plain vowel test — "an UI" is how that shortcut fails.
|
|
532
|
-
assert.equal(line('ui', '
|
|
533
|
-
assert.equal(line('
|
|
532
|
+
assert.equal(line('ui', 'artisan'), 'A UI — you are an artisan. Mingle pairs across roles.');
|
|
533
|
+
assert.equal(line('artisan', 'ui'), 'An artisan — you are a UI. Mingle pairs across roles.');
|
|
534
534
|
});
|
|
535
535
|
|
|
536
536
|
test('a pair with an unset role drops the comparison rather than naming the default', () => {
|
|
@@ -541,7 +541,7 @@ test('a pair with an unset role drops the comparison rather than naming the defa
|
|
|
541
541
|
// The defect this replaced rendered the database's own word — "A unclassified — you are
|
|
542
542
|
// a engineer" — which is the example V2 names. Nothing a person did not choose may reach
|
|
543
543
|
// the sentence, whichever side is missing.
|
|
544
|
-
for (const pair of [[OTB.ROLE_UNSET, '
|
|
544
|
+
for (const pair of [[OTB.ROLE_UNSET, 'artisan'], ['artisan', OTB.ROLE_UNSET], [null, 'artist'], ['', '']]) {
|
|
545
545
|
const out = L.pairSentence(word(pair[0]), word(pair[1]));
|
|
546
546
|
assert.equal(out, 'Mingle pairs across roles.', `pair ${JSON.stringify(pair)}`);
|
|
547
547
|
assert.doesNotMatch(out, /unclassified/);
|
|
@@ -550,9 +550,8 @@ test('a pair with an unset role drops the comparison rather than naming the defa
|
|
|
550
550
|
|
|
551
551
|
test('aOrAn is the article rule, stated once', () => {
|
|
552
552
|
const L = lib();
|
|
553
|
-
assert.equal(L.aOrAn('
|
|
553
|
+
assert.equal(L.aOrAn('artisan'), 'an');
|
|
554
554
|
assert.equal(L.aOrAn('artist'), 'an');
|
|
555
|
-
assert.equal(L.aOrAn('ideator'), 'an');
|
|
556
555
|
assert.equal(L.aOrAn('UI'), 'a', 'the acronym is spoken "you-eye", so it takes "a"');
|
|
557
556
|
assert.equal(L.aOrAn('ui'), 'a');
|
|
558
557
|
});
|
|
@@ -257,8 +257,8 @@ test('a craft the caller does not hold is dropped before it binds', async () =>
|
|
|
257
257
|
// The C01 hole, one surface over: a craft value that did not come from the
|
|
258
258
|
// caller's own profile must never widen a scope.
|
|
259
259
|
const r = recorder();
|
|
260
|
-
await help.listArchivedForBuilder({ builderId: 7, crafts: ['wizard', '
|
|
261
|
-
assert.deepEqual(r.calls[0].params[1], ['
|
|
260
|
+
await help.listArchivedForBuilder({ builderId: 7, crafts: ['wizard', 'artist'] }, { pool: r.pool });
|
|
261
|
+
assert.deepEqual(r.calls[0].params[1], ['artist']);
|
|
262
262
|
});
|
|
263
263
|
|
|
264
264
|
test('the archive ROUTE reads crafts from the profile and takes nothing from the query', () => {
|