@bongos/core 1.24.79 → 1.24.81
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 +554 -394
- 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/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md +142 -0
- package/docs/adr/README.md +4 -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 +393 -378
- package/docs/copy-registry.json +722 -578
- 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 +16 -7
- package/docs/page-readings.json +3184 -3338
- 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 +6 -4
- package/modules/hall-ui/public/ai-studio.states.json +1 -0
- package/modules/hall-ui/public/atlas.html +5 -5
- package/modules/hall-ui/public/atlas.js +3 -1
- package/modules/hall-ui/public/atlas.states.json +1 -0
- package/modules/hall-ui/public/blockers.html +5 -5
- package/modules/hall-ui/public/board-room.html +4 -4
- package/modules/hall-ui/public/board-room.states.json +1 -0
- 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 +3 -4
- package/modules/hall-ui/public/city.html +9 -9
- package/modules/hall-ui/public/city.js +17 -56
- package/modules/hall-ui/public/city.states.json +7 -32
- package/modules/hall-ui/public/collab.html +4 -4
- package/modules/hall-ui/public/collab.states.json +1 -0
- package/modules/hall-ui/public/commands.html +4 -4
- package/modules/hall-ui/public/commands.states.json +1 -0
- package/modules/hall-ui/public/controls.css +63 -11
- package/modules/hall-ui/public/copy-desk.html +4 -4
- package/modules/hall-ui/public/copy-desk.states.json +1 -0
- package/modules/hall-ui/public/craft-mode.js +258 -64
- package/modules/hall-ui/public/deploy.html +4 -4
- package/modules/hall-ui/public/deploy.states.json +1 -0
- 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/goals.states.json +1 -0
- package/modules/hall-ui/public/government.html +4 -4
- package/modules/hall-ui/public/government.states.json +1 -0
- 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/idea.states.json +1 -0
- package/modules/hall-ui/public/ideas.html +5 -5
- package/modules/hall-ui/public/ideas.states.json +1 -0
- package/modules/hall-ui/public/index-founding.states.json +1 -0
- package/modules/hall-ui/public/index-genesis-decide.states.json +1 -0
- package/modules/hall-ui/public/index-genesis-demo.states.json +1 -0
- package/modules/hall-ui/public/index-genesis.states.json +1 -0
- package/modules/hall-ui/public/index.html +11 -7
- package/modules/hall-ui/public/index.states.json +1 -0
- package/modules/hall-ui/public/market-watch-item.html +4 -4
- package/modules/hall-ui/public/market-watch-item.states.json +1 -0
- package/modules/hall-ui/public/market-watch-rules.html +4 -4
- package/modules/hall-ui/public/market-watch-rules.states.json +1 -0
- 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/market-watch.states.json +1 -0
- package/modules/hall-ui/public/modules.html +4 -4
- package/modules/hall-ui/public/modules.states.json +1 -0
- package/modules/hall-ui/public/money.html +4 -4
- package/modules/hall-ui/public/money.states.json +1 -0
- package/modules/hall-ui/public/my-agents.html +4 -4
- package/modules/hall-ui/public/my-agents.states.json +1 -0
- 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/primer.states.json +1 -0
- 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 +5 -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/roster.states.json +1 -0
- 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/settings.states.json +1 -0
- package/modules/hall-ui/public/shell-game.css +110 -0
- package/modules/hall-ui/public/shell-game.js +316 -3
- package/modules/hall-ui/public/shell.js +15 -66
- 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-card.states.json +1 -0
- package/modules/hall-ui/public/street-inside.js +3 -4
- package/modules/hall-ui/public/street.css +1 -5
- package/modules/hall-ui/public/street.html +6 -10
- package/modules/hall-ui/public/street.js +8 -59
- package/modules/hall-ui/public/street.states.json +7 -32
- package/modules/hall-ui/public/studio.html +5 -5
- package/modules/hall-ui/public/studio.states.json +1 -0
- package/modules/hall-ui/public/style.css +197 -112
- package/modules/hall-ui/public/task-detail.js +5 -4
- package/modules/hall-ui/public/task.html +5 -5
- package/modules/hall-ui/public/task.states.json +1 -0
- package/modules/hall-ui/public/thinking.css +10 -0
- package/modules/hall-ui/public/thinking.html +9 -6
- package/modules/hall-ui/public/thinking.states.json +1 -0
- 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/tweak-editor.states.json +1 -0
- 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/watch.states.json +1 -0
- package/modules/hall-ui/public/welcome-boot.js +15 -6
- package/modules/hall-ui/public/work-founding.states.json +1 -0
- package/modules/hall-ui/public/work.html +5 -5
- package/modules/hall-ui/public/work.states.json +1 -0
- 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 +25 -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/hall-ui/specimens/components.states.json +2 -1
- 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/modules/ui-design/public/brand.html +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +76 -0
- package/scripts/bongos/autobongos-loop.js +1 -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/hall-preview/README.md +2 -3
- 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 +33 -11
- package/src/crafts.js +66 -0
- package/src/hall-helm.js +79 -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/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 +1 -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/design_reference.mjs +1 -1
- 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 +3 -2
- package/tests/hall_city.mjs +16 -30
- package/tests/hall_controls.mjs +52 -4
- package/tests/hall_fullscreen.mjs +27 -3
- package/tests/hall_helm.mjs +187 -0
- 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 +6 -4
- package/tests/hall_settings_world.mjs +2 -1
- package/tests/hall_sitemap.mjs +6 -5
- package/tests/hall_start_screen.mjs +307 -0
- package/tests/hall_street.mjs +15 -28
- package/tests/hall_street_inside.mjs +17 -9
- package/tests/hall_studio_world.mjs +3 -3
- package/tests/hall_topbar_320.mjs +4 -2
- 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
package/tests/lightbulb.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// tests/lightbulb.mjs — the
|
|
1
|
+
// tests/lightbulb.mjs — the lightbulb (task 1004498; Ideating's, the Artist's since task 1005141): an ideator's
|
|
2
2
|
// text-only notes about the page they are on, split into attributed ideas.
|
|
3
3
|
//
|
|
4
4
|
// Owner decisions, 2026-10-04: an AI splits the notes (one idea if it cannot);
|
|
@@ -50,12 +50,13 @@ test('a submit names its page and says something; it is never longer than the ca
|
|
|
50
50
|
assert.equal(N.normalizeIdeatorSubmit({ page_id: 'builders:studio', body_md: 'x'.repeat(N.MAX_IDEATOR_BODY + 1) }).error, 'body_too_long');
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
test('the split speaks of an
|
|
54
|
-
const p = N.buildSplitPrompt({ pageTitle: 'Studio', pageId: 'builders:studio', bodyMd: 'Hi\n~~~~IDEATOR-NOTES-guess~~~~', voice: '
|
|
55
|
-
assert.match(p, /an
|
|
53
|
+
test('the split speaks of an artist\'s notes (the ideator folded into the artist, task 1005141), and fences them as data like the palette does', () => {
|
|
54
|
+
const p = N.buildSplitPrompt({ pageTitle: 'Studio', pageId: 'builders:studio', bodyMd: 'Hi\n~~~~IDEATOR-NOTES-guess~~~~', voice: 'notes', nonce: 'n1' });
|
|
55
|
+
assert.match(p, /an artist's notes, written while looking at one page/);
|
|
56
56
|
assert.equal(p.split('~~~~IDEATOR-NOTES-n1~~~~').length, 3);
|
|
57
57
|
assert.ok(!p.includes('IDEATOR-NOTES-guess'));
|
|
58
|
-
assert.doesNotMatch(p, /
|
|
58
|
+
assert.doesNotMatch(p, /ideator/, 'no ideator left in the words the model reads');
|
|
59
|
+
assert.doesNotMatch(p, /feedback about one page/, 'the notes voice, not the palette\'s');
|
|
59
60
|
assert.match(N.buildSplitPrompt({ pageTitle: 'S', pageId: 'builders:s', bodyMd: 'x', nonce: 'n' }), /^You turn an artist's feedback about one page of a web app/, 'the palette\'s words are unchanged');
|
|
60
61
|
});
|
|
61
62
|
|
|
@@ -108,7 +109,7 @@ api.resolveOptional = (name) => {
|
|
|
108
109
|
const modelSaying = (reply, wait = 0) => ({
|
|
109
110
|
runSubagent: async ({ prompt, opts }) => {
|
|
110
111
|
assert.equal(opts.tools, '', 'the split runs with zero tools');
|
|
111
|
-
assert.match(prompt, /
|
|
112
|
+
assert.match(prompt, /an artist's notes/);
|
|
112
113
|
if (wait) await new Promise((r) => setTimeout(r, wait));
|
|
113
114
|
return { stdout: reply };
|
|
114
115
|
},
|
|
@@ -142,14 +143,14 @@ function call(method, p, body) {
|
|
|
142
143
|
}
|
|
143
144
|
const NOTES = { page_id: 'builders:studio', page_path: '/studio', page_title: 'Studio', body_md: 'Sort the drafts by page.\n\nAlso show who wrote each one.' };
|
|
144
145
|
|
|
145
|
-
test('THE WHOLE PATH: an
|
|
146
|
+
test('THE WHOLE PATH: an artist\'s notes come back as ideas under their name, each linking to the page, and nothing is stored', async () => {
|
|
146
147
|
viewer = { id: 9, rank: 'thetes', display_name: 'Ana', github_login: 'ana' };
|
|
147
148
|
captured = []; captureFails = false; captureOn = true;
|
|
148
149
|
grade = modelSaying('{"ideas":[{"title":"Sort drafts by page","body":"Group them."},{"title":"Show each draft\'s writer","body":"Name them."}]}');
|
|
149
150
|
const r = await call('POST', '/page-notes/ideas', NOTES);
|
|
150
151
|
assert.equal(r.status, 201);
|
|
151
152
|
assert.deepEqual(r.body, { ideas: [{ id: '701', title: 'Sort drafts by page' }, { id: '702', title: 'Show each draft\'s writer' }], split_by: 'ai' });
|
|
152
|
-
assert.deepEqual(captured.map((c) => [c.capturedBy, c.filedVia]), [[9, 'hall'], [9, 'hall']], 'attributed to the
|
|
153
|
+
assert.deepEqual(captured.map((c) => [c.capturedBy, c.filedVia]), [[9, 'hall'], [9, 'hall']], 'attributed to the artist, filed through the hall');
|
|
153
154
|
for (const c of captured) assert.ok(c.bodyMd.endsWith("From Ana's ideas on Studio. Page: [Studio](/studio)"), 'each idea links back to the page');
|
|
154
155
|
});
|
|
155
156
|
|
|
@@ -372,11 +373,14 @@ test('storage that throws keeps nothing, and the panel still files', async () =>
|
|
|
372
373
|
assert.match(p.q('#lb-body').innerHTML, /Filed as 2 ideas/);
|
|
373
374
|
});
|
|
374
375
|
|
|
375
|
-
test('craft-mode.js gives the tool its OWN icon,
|
|
376
|
+
test('craft-mode.js gives the tool its OWN icon, and makes it the artist\'s since Ideating folded in (owner, 2026-10-04; task 1005141)', () => {
|
|
376
377
|
const craft = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/craft-mode.js'), 'utf8');
|
|
377
378
|
assert.match(craft, /\{ id: 'lightbulb', icon: 'note', short: 'Ideas', label: 'Write ideas about this page', script: 'lightbulb\.js', api: 'OTBLightbulb' \}/);
|
|
378
379
|
assert.match(craft, /^ {4}note: '<path /m, 'the note glyph is drawn');
|
|
379
|
-
|
|
380
|
+
const tools = /var TOOLS = \{([\s\S]*?)\n {2}\};/.exec(craft)[1];
|
|
381
|
+
assert.doesNotMatch(tools, /ideating:/, 'no Ideating tool list: the mode is gone');
|
|
382
|
+
assert.match(/artist: \[([\s\S]*?)\n {4}\]/.exec(tools)[1], /id: 'lightbulb'/, 'the lightbulb is one of the artist\'s tools');
|
|
383
|
+
assert.doesNotMatch(tools, /icon: 'ideating'/, 'never the old Ideating pill glyph');
|
|
380
384
|
});
|
|
381
385
|
|
|
382
386
|
test('the only route it calls is the lightbulb\'s own, and it rides the palette\'s styles', () => {
|
|
@@ -38,33 +38,33 @@ const offered = enabledDisciplines();
|
|
|
38
38
|
const last = () => calls[calls.length - 1];
|
|
39
39
|
|
|
40
40
|
await t('the write goes through the one exported statement', async () => {
|
|
41
|
-
await db.updateBuilderDisciplines(7, ['
|
|
41
|
+
await db.updateBuilderDisciplines(7, ['artisan']);
|
|
42
42
|
assert.equal(last().sql, db.UPDATE_BUILDER_DISCIPLINES_SQL);
|
|
43
43
|
assert.match(db.UPDATE_BUILDER_DISCIPLINES_SQL, /main_discipline = CASE WHEN \$3::boolean THEN \$4::text/);
|
|
44
44
|
assert.match(db.UPDATE_BUILDER_DISCIPLINES_SQL, /WHEN main_discipline = ANY\(\$2::text\[\]\) THEN main_discipline\s+ELSE NULL END/);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
await t('not mentioned: the flag is false, so the stored main role is left to the statement to keep or clear', async () => {
|
|
48
|
-
await db.updateBuilderDisciplines(7, ['
|
|
48
|
+
await db.updateBuilderDisciplines(7, ['artisan', 'artist']);
|
|
49
49
|
assert.deepEqual(last().params.slice(2), [false, null]);
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
await t('named: the flag is true and the role travels', async () => {
|
|
53
|
-
await db.updateBuilderDisciplines(7, ['
|
|
54
|
-
assert.deepEqual(last().params.slice(2), [true, '
|
|
55
|
-
assert.deepEqual(last().params[1], offered.filter((d) => ['
|
|
53
|
+
await db.updateBuilderDisciplines(7, ['artist', 'artisan'], { mainDiscipline: 'artist' });
|
|
54
|
+
assert.deepEqual(last().params.slice(2), [true, 'artist']);
|
|
55
|
+
assert.deepEqual(last().params[1], offered.filter((d) => ['artist', 'artisan'].includes(d)),
|
|
56
56
|
'the set is still stored in canonical order: the main role is what carries the choice');
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
await t('null clears it on purpose', async () => {
|
|
60
|
-
await db.updateBuilderDisciplines(7, ['
|
|
60
|
+
await db.updateBuilderDisciplines(7, ['artisan'], { mainDiscipline: null });
|
|
61
61
|
assert.deepEqual(last().params.slice(2), [true, null]);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
await t('a main role that is not one of the chosen crafts is refused, and nothing is written', async () => {
|
|
65
65
|
const before = calls.length;
|
|
66
|
-
await assert.rejects(() => db.updateBuilderDisciplines(7, ['
|
|
67
|
-
(err) => err.code === 'invalid_main_discipline' && err.bad[0] === '
|
|
66
|
+
await assert.rejects(() => db.updateBuilderDisciplines(7, ['artisan'], { mainDiscipline: 'artist' }),
|
|
67
|
+
(err) => err.code === 'invalid_main_discipline' && err.bad[0] === 'artist');
|
|
68
68
|
assert.equal(calls.length, before);
|
|
69
69
|
});
|
|
70
70
|
|
|
@@ -41,7 +41,7 @@ test('every tracked .md resolves to eol=lf, at any depth', () => {
|
|
|
41
41
|
'README.md',
|
|
42
42
|
'CLAUDE.md',
|
|
43
43
|
'docs/architecture.md',
|
|
44
|
-
'docs/packs/
|
|
44
|
+
'docs/packs/artisan.md',
|
|
45
45
|
'docs/adr/README.md',
|
|
46
46
|
'.claude/skills/merge-mode/SKILL.md',
|
|
47
47
|
'modules/hall-ui/CLAUDE.md',
|
|
@@ -442,28 +442,29 @@ test('MARKET WATCH OFF: both pages show the quiet off line, not an error, and as
|
|
|
442
442
|
}
|
|
443
443
|
});
|
|
444
444
|
|
|
445
|
-
// ---- the owner's
|
|
445
|
+
// ---- the owner's settings, an options menu entry (task 1005144; before it task 1005045) ----
|
|
446
446
|
//
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
// (
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
|
|
453
|
-
|
|
447
|
+
// BONGOS-V3 R04 (ADR 0401): every setting is reached from the ONE options menu. The
|
|
448
|
+
// settings that had their own page (task 1004938) and then an owner-only Settings tab here
|
|
449
|
+
// (task 1005045) are the menu's "Market Watch settings" entry: the menu opens this page in
|
|
450
|
+
// its frame (?embed=1, which shell-game.js reads), and the page then shows the settings
|
|
451
|
+
// panel ALONE. A plain visit is the watch, with no tab strip and no settings read, and an
|
|
452
|
+
// old #settings link goes on to the menu. These boot the real market-watch.js with the
|
|
453
|
+
// settings script stood in. The four settings routes stay the wall (ADR 0016).
|
|
454
|
+
|
|
455
|
+
function bootDesk({ rank = 'archon', homeStatus = 200, modules = { enabled: { 'market-watch': true } }, embed = false, hash = '' } = {}) {
|
|
454
456
|
const els = {};
|
|
455
457
|
const el = (id) => (els[id] = els[id] || { id, hidden: true, innerHTML: '', textContent: '', style: {}, value: '', dataset: {}, title: '', addEventListener() {}, querySelectorAll: () => [], setAttribute() {} });
|
|
456
458
|
const listeners = {};
|
|
457
459
|
const fetches = [];
|
|
458
|
-
const tabsMade = [];
|
|
459
460
|
const settingsLoads = [];
|
|
461
|
+
const replaced = [];
|
|
460
462
|
const window = {
|
|
461
463
|
__MODULES__: modules,
|
|
462
464
|
OTBMarketWatch: MW,
|
|
463
|
-
OTBKit: { makeTabs: (opts) => { tabsMade.push(opts); return { activate() {}, active: () => opts.initial || opts.tabs[0].id }; } },
|
|
464
465
|
OTBMarketWatchSettings: { load: () => { settingsLoads.push(1); } },
|
|
465
|
-
|
|
466
|
-
location: { pathname: '/market-watch', search: ''
|
|
466
|
+
OTBShellGame: { embedded: () => embed, menuPath: () => '/settings' },
|
|
467
|
+
location: { pathname: '/market-watch', search: embed ? '?embed=1' : '', hash, assign() {}, replace(u) { replaced.push(u); } },
|
|
467
468
|
localStorage: { getItem: () => null, setItem() {} },
|
|
468
469
|
};
|
|
469
470
|
const document = {
|
|
@@ -477,61 +478,61 @@ function bootDesk({ rank, homeStatus = 200, modules = { enabled: { 'market-watch
|
|
|
477
478
|
window, document, URLSearchParams, console,
|
|
478
479
|
fetch: async (url) => {
|
|
479
480
|
fetches.push(url);
|
|
480
|
-
if (/\/me$/.test(url)) return answer(
|
|
481
|
+
if (/\/me$/.test(url)) return answer(200, { builder: { id: '1', rank } });
|
|
481
482
|
if (/market-watch\/home$/.test(url)) return answer(homeStatus, homeStatus === 200 ? home() : { error: { code: 'not_found' } });
|
|
482
483
|
return answer(404, {});
|
|
483
484
|
},
|
|
484
485
|
});
|
|
485
486
|
vm.runInContext(readFileSync(new URL('../modules/hall-ui/public/market-watch.js', import.meta.url), 'utf8'), ctx);
|
|
486
|
-
return { els: new Proxy(els, { get: (_, id) => el(id) }), fetches,
|
|
487
|
+
return { els: new Proxy(els, { get: (_, id) => el(id) }), fetches, settingsLoads, replaced, ready: () => listeners.DOMContentLoaded() };
|
|
487
488
|
}
|
|
488
489
|
|
|
489
|
-
test('SETTINGS
|
|
490
|
-
const p = bootDesk({
|
|
490
|
+
test('SETTINGS IN THE MENU: inside the options menu frame the page is the settings panel alone', async () => {
|
|
491
|
+
const p = bootDesk({ embed: true });
|
|
491
492
|
await p.ready();
|
|
492
|
-
assert.equal(p.els['mw-
|
|
493
|
-
assert.equal(p.
|
|
494
|
-
const t = JSON.parse(JSON.stringify(p.tabsMade[0]));
|
|
495
|
-
assert.deepEqual(t.tabs.map((x) => [x.id, x.label, x.panel]), [['watch', 'Watch', 'mw-panel-watch'], ['settings', 'Settings', 'mw-panel-settings']]);
|
|
496
|
-
assert.equal(t.hash, true, '#settings is the address the old URL redirects to');
|
|
497
|
-
assert.equal(t.initial, 'watch', 'the owner lands on the watch, like everyone else');
|
|
498
|
-
assert.equal(p.els['mw-tabs'].hidden, false, 'the strip shows');
|
|
493
|
+
assert.equal(p.els['mw-panel-settings'].hidden, false, 'the settings panel shows');
|
|
494
|
+
assert.equal(p.els['mw-panel-watch'].hidden, true, 'the watch does not');
|
|
499
495
|
assert.equal(p.settingsLoads.length, 1, 'the settings read is made once');
|
|
496
|
+
assert.deepEqual(p.fetches, [], 'no watch read and no /me: the settings routes answer who may see them');
|
|
497
|
+
// even when the watch would fail to read, the switch and key stay reachable
|
|
498
|
+
const failing = bootDesk({ embed: true, homeStatus: 500 });
|
|
499
|
+
await failing.ready();
|
|
500
|
+
assert.equal(failing.settingsLoads.length, 1);
|
|
500
501
|
});
|
|
501
502
|
|
|
502
|
-
test('SETTINGS
|
|
503
|
-
for (const rank of ['metic', 'thetes', 'xenos', null]) {
|
|
503
|
+
test('SETTINGS IN THE MENU: a plain visit is the watch, with no tab strip and no settings read, for every rank', async () => {
|
|
504
|
+
for (const rank of ['archon', 'metic', 'thetes', 'xenos', null]) {
|
|
504
505
|
const p = bootDesk({ rank });
|
|
505
506
|
await p.ready();
|
|
506
507
|
assert.equal(p.els['mw-desk'].hidden, false, `${rank}: the desk draws`);
|
|
507
|
-
assert.equal(p.tabsMade.length, 0, `${rank}: no tab strip`);
|
|
508
|
-
assert.equal(p.els['mw-tabs'].hidden, true, `${rank}: the strip stays hidden`);
|
|
509
508
|
assert.equal(p.els['mw-panel-settings'].hidden, true, `${rank}: the settings panel stays hidden`);
|
|
510
|
-
assert.equal(p.settingsLoads.length, 0, `${rank}: the
|
|
509
|
+
assert.equal(p.settingsLoads.length, 0, `${rank}: the settings are not read`);
|
|
510
|
+
assert.ok(!p.fetches.some((u) => /\/me$/.test(u)), `${rank}: and nobody is asked who they are`);
|
|
511
511
|
}
|
|
512
|
-
// An unreadable /me is not an owner.
|
|
513
|
-
const blind = bootDesk({ rank: 'archon', meStatus: 500 });
|
|
514
|
-
await blind.ready();
|
|
515
|
-
assert.equal(blind.tabsMade.length, 0, 'an unreadable identity builds no settings tab');
|
|
516
512
|
});
|
|
517
513
|
|
|
518
|
-
test('SETTINGS
|
|
519
|
-
const
|
|
514
|
+
test('SETTINGS IN THE MENU: an old #settings link opens the menu entry instead, and reads nothing', async () => {
|
|
515
|
+
const p = bootDesk({ hash: '#settings' });
|
|
516
|
+
await p.ready();
|
|
517
|
+
assert.deepEqual(p.replaced, ['/settings?open=market-watch'], 'a replace, so Back does not bounce through the watch');
|
|
518
|
+
assert.deepEqual(p.fetches, []);
|
|
519
|
+
assert.equal(p.settingsLoads.length, 0);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test('SETTINGS IN THE MENU: where Market Watch is off there is nothing to set (and nothing asked)', async () => {
|
|
523
|
+
const off = bootDesk({ embed: true, modules: { enabled: { 'market-watch': false } } });
|
|
520
524
|
await off.ready();
|
|
521
|
-
assert.equal(off.
|
|
522
|
-
assert.equal(off.
|
|
523
|
-
|
|
524
|
-
await unmounted.ready();
|
|
525
|
-
assert.equal(unmounted.els['mw-off'].hidden, false);
|
|
526
|
-
assert.equal(unmounted.tabsMade.length, 0, 'unmounted routes have no settings either');
|
|
525
|
+
assert.equal(off.els['mw-off'].hidden, false, 'the off state shows in the frame');
|
|
526
|
+
assert.equal(off.settingsLoads.length, 0);
|
|
527
|
+
assert.equal(off.fetches.length, 0);
|
|
527
528
|
});
|
|
528
529
|
|
|
529
|
-
test('SETTINGS
|
|
530
|
+
test('SETTINGS PANEL: the page carries every setting the old page did, in a hidden panel, and the old page is gone', () => {
|
|
530
531
|
const pub = (f) => new URL(`../modules/hall-ui/public/${f}`, import.meta.url);
|
|
531
532
|
const html = readFileSync(pub('market-watch.html'), 'utf8');
|
|
532
533
|
const panel = html.match(/<section[^>]*id="mw-panel-settings"[^>]*>[\s\S]*?<\/section>\s*<!-- \/mw-panel-settings -->/);
|
|
533
534
|
assert.ok(panel, 'one settings panel, closed by its marker comment');
|
|
534
|
-
assert.match(panel[0].split('>')[0], /\bhidden\b/, 'the panel starts hidden:
|
|
535
|
+
assert.match(panel[0].split('>')[0], /\bhidden\b/, 'the panel starts hidden: only the options menu frame shows it');
|
|
535
536
|
for (const id of ['mws-power', 'mws-cap', 'mws-cad', 'mws-weeks', 'mws-key-in', 'mws-key-save', 'mws-key-clear', 'mws-sealed', 'mws-off', 'mws-error']) {
|
|
536
537
|
assert.match(panel[0], new RegExp(`id="${id}"`), `${id} is on the tab`);
|
|
537
538
|
}
|
|
@@ -539,12 +540,12 @@ test('SETTINGS TAB: the page carries every setting the old page did, in a hidden
|
|
|
539
540
|
// While GET /market-watch/settings is in flight every section below is [hidden], so the
|
|
540
541
|
// panel opens on the self-hiding page loader (records/loader.md) rather than a blank tab.
|
|
541
542
|
const inner = panel[0].slice(panel[0].indexOf('>') + 1).replace(/^\s*(<!--[\s\S]*?-->\s*)*/, '');
|
|
542
|
-
assert.match(inner, /^<p class="hall-loading hall-loading--page hall-loading--wait" role="status">Loading…<\/p>/, 'the settings
|
|
543
|
+
assert.match(inner, /^<p class="hall-loading hall-loading--page hall-loading--wait" role="status">Loading…<\/p>/, 'the settings panel opens on the page loader');
|
|
543
544
|
const after = inner.slice(inner.indexOf('</p>') + 4);
|
|
544
545
|
const kids = [...after.matchAll(/^ {10}<(section|div|p)\b([^>]*)>/gm)];
|
|
545
546
|
assert.ok(kids.length >= 4, `the loader's siblings were read (${kids.length})`);
|
|
546
547
|
for (const [, tag, attrs] of kids) assert.match(attrs, /\bhidden\b/, `a <${tag}> after the loader starts hidden, or the loader would never show`);
|
|
547
|
-
assert.
|
|
548
|
+
assert.doesNotMatch(html, /id="mw-tabs"/, 'no tab strip: the settings are an options menu entry (task 1005144)');
|
|
548
549
|
assert.match(html, /<div[^>]*id="mw-panel-watch"/, 'the watch is its own panel');
|
|
549
550
|
assert.match(html, /market-watch-settings\.css/);
|
|
550
551
|
assert.match(html, /<script src="\/builders\/market-watch-settings\.js"><\/script>\s*<script src="\/builders\/market-watch\.js"><\/script>/,
|
|
@@ -558,48 +559,25 @@ test('SETTINGS TAB: the page carries every setting the old page did, in a hidden
|
|
|
558
559
|
assert.match(js, /window\.OTBMarketWatchSettings = \{ load \}/);
|
|
559
560
|
});
|
|
560
561
|
|
|
561
|
-
test('SETTINGS
|
|
562
|
+
test('SETTINGS IN THE MENU: the old settings URL opens the menu entry, even framed, and no gate dispatches to it', () => {
|
|
562
563
|
const serve = readFileSync(new URL('../src/bongos/serve-internal.js', import.meta.url), 'utf8');
|
|
563
|
-
const decl = 'const
|
|
564
|
+
const decl = 'const RETIRED_OPTIONS_RE = ';
|
|
564
565
|
const i = serve.indexOf(decl);
|
|
565
566
|
assert.ok(i > 0, 'the old URL keeps a matcher, so a bookmark still lands');
|
|
566
|
-
const block = serve.slice(i, i + 700);
|
|
567
|
-
assert.ok(block.includes('res.redirect(302,'), 'a temporary redirect, never cached past a re-point');
|
|
568
|
-
assert.ok(block.includes('/market-watch${qs}#settings'), 'landing on the Settings tab, query string kept');
|
|
569
567
|
const rx = eval(serve.slice(i + decl.length, serve.indexOf(';', i))); // eslint-disable-line no-eval
|
|
570
568
|
for (const p of ['/builders/market-watch-settings', '/builders/market-watch-settings/', '/builders/market-watch-settings.html']) {
|
|
571
569
|
assert.ok(rx.test(p), `${p} reaches the redirect`);
|
|
572
570
|
}
|
|
573
|
-
for (const p of ['/builders/market-watch', '/builders/market-watch-
|
|
571
|
+
for (const p of ['/builders/market-watch', '/builders/market-watch-item', '/api/bongos/market-watch/settings', '/builders/market-watch-settingsx']) {
|
|
574
572
|
assert.ok(!rx.test(p), `${p} is not redirected`);
|
|
575
573
|
}
|
|
574
|
+
const block = serve.slice(i, i + 900);
|
|
575
|
+
assert.ok(block.includes("embed === '1' && m[1] !== 'market-watch-settings'"), 'the retired page has no page left, so even a framed request is sent to the menu');
|
|
576
|
+
assert.ok(block.includes('res.redirect(302,'), 'a temporary redirect, never cached past a re-point');
|
|
577
|
+
assert.match(serve, /'market-watch-settings': 'market-watch'/, 'it opens the Market Watch settings entry');
|
|
576
578
|
const mwRe = serve.match(/const MARKET_WATCH_PAGE_RE = (\/[^\n]*?\/);/);
|
|
577
579
|
assert.ok(mwRe, 'the Market Watch page gate is still a literal');
|
|
578
580
|
const gate = eval(mwRe[1]); // eslint-disable-line no-eval
|
|
579
581
|
assert.ok(!gate.test('/builders/market-watch-settings'), 'the page gate no longer names the retired page');
|
|
580
582
|
for (const p of ['/builders/market-watch', '/builders/market-watch-item', '/builders/market-watch-rules']) assert.ok(gate.test(p), p);
|
|
581
583
|
});
|
|
582
|
-
|
|
583
|
-
test('SETTINGS TAB: the page adopts the shell\'s one /me read instead of asking again', async () => {
|
|
584
|
-
const owner = bootDesk({ rank: 'archon', shared: Promise.resolve({ builder: { id: '1', rank: 'archon' } }) });
|
|
585
|
-
await owner.ready();
|
|
586
|
-
assert.equal(owner.tabsMade.length, 1, 'the shared answer is enough to know the owner');
|
|
587
|
-
assert.ok(!owner.fetches.some((u) => /\/me$/.test(u)), 'no second /me request');
|
|
588
|
-
for (const answer of [{ builder: { id: '2', rank: 'metic' } }, false, null]) {
|
|
589
|
-
const p = bootDesk({ rank: 'archon', shared: Promise.resolve(answer) });
|
|
590
|
-
await p.ready();
|
|
591
|
-
assert.equal(p.tabsMade.length, 0, `${JSON.stringify(answer)}: no tab`);
|
|
592
|
-
assert.ok(!p.fetches.some((u) => /\/me$/.test(u)), 'and no fallback request either');
|
|
593
|
-
}
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
test('SETTINGS TAB: a watch that fails to read keeps the owner\'s tab, so the switch and key stay reachable', async () => {
|
|
597
|
-
const p = bootDesk({ rank: 'archon', homeStatus: 500 });
|
|
598
|
-
await p.ready();
|
|
599
|
-
assert.equal(p.els['mw-fail'].hidden, false, 'the failure line shows on the Watch tab');
|
|
600
|
-
assert.equal(p.tabsMade.length, 1, 'the settings are their own read, so the owner still gets them');
|
|
601
|
-
assert.equal(p.settingsLoads.length, 1);
|
|
602
|
-
const member = bootDesk({ rank: 'metic', homeStatus: 500 });
|
|
603
|
-
await member.ready();
|
|
604
|
-
assert.equal(member.tabsMade.length, 0, 'and a non-owner still gets none');
|
|
605
|
-
});
|
|
@@ -64,7 +64,7 @@ const TASKS = {
|
|
|
64
64
|
104: { kind: 'refactor', est_minutes: 90 },
|
|
65
65
|
105: { kind: 'bug', est_minutes: 20 },
|
|
66
66
|
};
|
|
67
|
-
const claimsFor = (ids) => ids.map((id) => ({ task_id: String(id), discipline: '
|
|
67
|
+
const claimsFor = (ids) => ids.map((id) => ({ task_id: String(id), discipline: 'artisan' }));
|
|
68
68
|
|
|
69
69
|
await test('/me sends the same number of streak queries for 1 claim as for 5', async () => {
|
|
70
70
|
const counts = [];
|
|
@@ -117,7 +117,7 @@ await test('each streak_preview is exactly what wouldExtendStreak returns, in ev
|
|
|
117
117
|
await test('the claim keeps its own fields and gains craft_mode beside the preview', async () => {
|
|
118
118
|
const db = fakeDb({ tasks: TASKS });
|
|
119
119
|
const cur = await streak.computeStreak(1, db);
|
|
120
|
-
const [c] = await claimsWithStreakPreview(rewardOn(db), 1, [{ task_id: '101', discipline: '
|
|
120
|
+
const [c] = await claimsWithStreakPreview(rewardOn(db), 1, [{ task_id: '101', discipline: 'artisan', id: 7 }], cur, null);
|
|
121
121
|
assert.equal(c.id, 7);
|
|
122
122
|
assert.equal(c.task_id, '101');
|
|
123
123
|
assert.ok('craft_mode' in c);
|
|
@@ -149,7 +149,7 @@ await test('two leaderboard reads within the window hit the database once', asyn
|
|
|
149
149
|
await test('a caller editing its rows does not edit the cached answer', async () => {
|
|
150
150
|
achievements.clearLeaderboardCache();
|
|
151
151
|
const a = await achievements.getLeaderboardWithAchievements(100, 5);
|
|
152
|
-
a[0].roles = ['
|
|
152
|
+
a[0].roles = ['artisan'];
|
|
153
153
|
const b = await achievements.getLeaderboardWithAchievements(100, 5);
|
|
154
154
|
assert.equal(b[0].roles, undefined);
|
|
155
155
|
});
|
|
@@ -266,7 +266,7 @@ function shouldReserveMigrationNumber(task) {
|
|
|
266
266
|
const db = require('../modules/lifecycle/db.js');
|
|
267
267
|
const setters = [
|
|
268
268
|
[() => db.updateTaskKind(1, 'bug'), 'kind'],
|
|
269
|
-
[() => db.updateTaskDiscipline(1, '
|
|
269
|
+
[() => db.updateTaskDiscipline(1, 'artisan'), 'discipline'],
|
|
270
270
|
[() => db.updateTaskNewcomerFriendly(1, true), 'newcomer_friendly'],
|
|
271
271
|
[() => db.updateTaskParallelSafe(1, true), 'parallel_safe'],
|
|
272
272
|
[() => db.updateTaskAutomationTag(1, 'live-only'), 'automation_tag'],
|
package/tests/mingle.mjs
CHANGED
|
@@ -76,13 +76,28 @@ const off = () => async () => null;
|
|
|
76
76
|
// ── 1. A PAIR IS ONLY EVER CROSS-CRAFT ──────────────────────────────────────
|
|
77
77
|
|
|
78
78
|
test('THE RULE: crafts must be DISJOINT, not merely different', () => {
|
|
79
|
-
assert.equal(mingle.isCrossCraft(['
|
|
80
|
-
assert.equal(mingle.isCrossCraft(['
|
|
79
|
+
assert.equal(mingle.isCrossCraft(['artisan'], ['artist']), true);
|
|
80
|
+
assert.equal(mingle.isCrossCraft(['artisan'], ['artisan']), false);
|
|
81
81
|
// The case a "different primary craft" rule would get wrong: someone who is
|
|
82
|
-
// both shares a craft with a pure
|
|
82
|
+
// both shares a craft with a pure artisan, so pairing them puts two of the
|
|
83
83
|
// same in a room and calls it cross-craft.
|
|
84
|
-
assert.equal(mingle.isCrossCraft(['artist', '
|
|
85
|
-
assert.equal(mingle.isCrossCraft(['artist', '
|
|
84
|
+
assert.equal(mingle.isCrossCraft(['artist', 'artisan'], ['artisan']), false);
|
|
85
|
+
assert.equal(mingle.isCrossCraft(['artist', 'artisan'], ['artist']), false);
|
|
86
|
+
assert.equal(mingle.isCrossCraft(['artist', 'artisan'], ['ui']), true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('since task 1005141 an artist and an (old) ideator are the SAME craft, so never a pair', () => {
|
|
90
|
+
// The fold: a builder still carrying 'ideator' in preferred_disciplines maps to
|
|
91
|
+
// the artist craft through help-requests' map, so artist × ideator is no longer
|
|
92
|
+
// cross-craft.
|
|
93
|
+
const help = require(path.join(ROOT, 'modules', 'lifecycle', 'help-requests.js'));
|
|
94
|
+
const ideator = [help.craftForDiscipline('ideator')];
|
|
95
|
+
const artist = [help.craftForDiscipline('artist')];
|
|
96
|
+
assert.deepEqual(ideator, ['artist']);
|
|
97
|
+
assert.equal(mingle.isCrossCraft(artist, ideator), false);
|
|
98
|
+
const engineer = [help.craftForDiscipline('engineer')];
|
|
99
|
+
assert.deepEqual(engineer, ['artisan'], 'a stored engineer discipline is the artisan craft');
|
|
100
|
+
assert.equal(mingle.isCrossCraft(engineer, artist), true);
|
|
86
101
|
});
|
|
87
102
|
|
|
88
103
|
test('a builder with NO craft is never pairable', () => {
|
|
@@ -99,11 +114,11 @@ test('a builder with NO craft is never pairable', () => {
|
|
|
99
114
|
|
|
100
115
|
test('EVERY pair the planner emits is cross-craft', () => {
|
|
101
116
|
const eligible = [
|
|
102
|
-
{ builder_id: '10', crafts: ['
|
|
103
|
-
{ builder_id: '20', crafts: ['
|
|
117
|
+
{ builder_id: '10', crafts: ['artisan'] },
|
|
118
|
+
{ builder_id: '20', crafts: ['artisan'] },
|
|
104
119
|
{ builder_id: '30', crafts: ['artist'] },
|
|
105
|
-
{ builder_id: '40', crafts: ['
|
|
106
|
-
{ builder_id: '50', crafts: ['artist', '
|
|
120
|
+
{ builder_id: '40', crafts: ['artist'] },
|
|
121
|
+
{ builder_id: '50', crafts: ['artist', 'artisan'] },
|
|
107
122
|
];
|
|
108
123
|
const byId = new Map(eligible.map((b) => [b.builder_id, b.crafts]));
|
|
109
124
|
const pairs = mingle.planPairs(eligible);
|
|
@@ -117,9 +132,9 @@ test('EVERY pair the planner emits is cross-craft', () => {
|
|
|
117
132
|
|
|
118
133
|
test('nobody is put in two pairs in one round', () => {
|
|
119
134
|
const pairs = mingle.planPairs([
|
|
120
|
-
{ builder_id: '1', crafts: ['
|
|
135
|
+
{ builder_id: '1', crafts: ['artisan'] },
|
|
121
136
|
{ builder_id: '2', crafts: ['artist'] },
|
|
122
|
-
{ builder_id: '3', crafts: ['
|
|
137
|
+
{ builder_id: '3', crafts: ['artist'] },
|
|
123
138
|
{ builder_id: '4', crafts: ['ui'] },
|
|
124
139
|
]);
|
|
125
140
|
const seen = new Set();
|
|
@@ -154,7 +169,7 @@ test('the matching rule is RECORDED, and the route serves it verbatim', () => {
|
|
|
154
169
|
|
|
155
170
|
test('SWITCH 1 OFF: the project disabled means no pairing, and no write at all', async () => {
|
|
156
171
|
const pool = recordingPool({ eligible: [
|
|
157
|
-
{ builder_id: '1', crafts: ['
|
|
172
|
+
{ builder_id: '1', crafts: ['artisan'] },
|
|
158
173
|
{ builder_id: '2', crafts: ['artist'] },
|
|
159
174
|
] });
|
|
160
175
|
const out = await mingle.runMingleRound({}, { pool, getSetting: off() });
|
|
@@ -174,7 +189,7 @@ test('SWITCH 2 OFF: eligibility is opted-in only, enforced in SQL', async () =>
|
|
|
174
189
|
|
|
175
190
|
test('BOTH ON is the only state that pairs', async () => {
|
|
176
191
|
const pool = recordingPool({ eligible: [
|
|
177
|
-
{ builder_id: '1', crafts: ['
|
|
192
|
+
{ builder_id: '1', crafts: ['artisan'] },
|
|
178
193
|
{ builder_id: '2', crafts: ['artist'] },
|
|
179
194
|
] });
|
|
180
195
|
const out = await mingle.runMingleRound({}, { pool, getSetting: on() });
|
|
@@ -185,9 +200,9 @@ test('BOTH ON is the only state that pairs', async () => {
|
|
|
185
200
|
|
|
186
201
|
test('a round is ONE insert, so a partial round cannot be written', () => {
|
|
187
202
|
const pool = recordingPool({ eligible: [
|
|
188
|
-
{ builder_id: '1', crafts: ['
|
|
203
|
+
{ builder_id: '1', crafts: ['artisan'] },
|
|
189
204
|
{ builder_id: '2', crafts: ['artist'] },
|
|
190
|
-
{ builder_id: '3', crafts: ['
|
|
205
|
+
{ builder_id: '3', crafts: ['artist'] },
|
|
191
206
|
{ builder_id: '4', crafts: ['ui'] },
|
|
192
207
|
] });
|
|
193
208
|
return mingle.runMingleRound({}, { pool, getSetting: on() }).then((out) => {
|
|
@@ -202,8 +217,8 @@ test('an empty plan writes nothing at all', async () => {
|
|
|
202
217
|
// Two builders of the SAME craft are not a pair, so there is nothing to insert
|
|
203
218
|
// — and no empty INSERT should be issued to discover that.
|
|
204
219
|
const pool = recordingPool({ eligible: [
|
|
205
|
-
{ builder_id: '1', crafts: ['
|
|
206
|
-
{ builder_id: '2', crafts: ['
|
|
220
|
+
{ builder_id: '1', crafts: ['artisan'] },
|
|
221
|
+
{ builder_id: '2', crafts: ['artisan'] },
|
|
207
222
|
] });
|
|
208
223
|
const out = await mingle.runMingleRound({}, { pool, getSetting: on() });
|
|
209
224
|
assert.deepEqual(out.paired, []);
|
|
@@ -243,7 +258,7 @@ test('the two switches are reported SEPARATELY, so a builder can see which is of
|
|
|
243
258
|
const PENALTY_SURFACES = /credit|karma|rank|streak|achievement|reward|score|penalt|cooldown|strike/i;
|
|
244
259
|
|
|
245
260
|
test('THE NO-PENALTY PROPERTY: a decline writes ONE row in ONE table', async () => {
|
|
246
|
-
const pool = recordingPool({ pairRow: { id: 5, builder_lo: '1', builder_hi: '2', craft_lo: '
|
|
261
|
+
const pool = recordingPool({ pairRow: { id: 5, builder_lo: '1', builder_hi: '2', craft_lo: 'artisan', craft_hi: 'artist', status: 'declined', decided_by: '1', decided_at: 'now' } });
|
|
247
262
|
const out = await mingle.decidePair(5, 1, 'declined', { pool });
|
|
248
263
|
assert.equal(out.pair.status, 'declined');
|
|
249
264
|
|
|
@@ -274,7 +289,7 @@ test('a declined pair may be introduced again — a decline is not a permanent s
|
|
|
274
289
|
// is free.
|
|
275
290
|
assert.match(MIGRATION, /CREATE UNIQUE INDEX[\s\S]{0,200}lifecycle_mingle_pairs_open_uniq[\s\S]{0,120}WHERE status = 'proposed'/);
|
|
276
291
|
const eligible = [
|
|
277
|
-
{ builder_id: '1', crafts: ['
|
|
292
|
+
{ builder_id: '1', crafts: ['artisan'] },
|
|
278
293
|
{ builder_id: '2', crafts: ['artist'] },
|
|
279
294
|
];
|
|
280
295
|
// With the pair already OPEN, it is skipped...
|
|
@@ -388,8 +403,8 @@ test('the route is registered in the module manifest', () => {
|
|
|
388
403
|
});
|
|
389
404
|
|
|
390
405
|
test('the craft vocabulary is CONSUMED from help-requests, never redefined', () => {
|
|
391
|
-
// Two spellings of the same craft ('builder' the craft
|
|
392
|
-
// discipline) would silently match nobody.
|
|
406
|
+
// Two spellings of the same craft (before task 1005141, 'builder' the craft was
|
|
407
|
+
// 'engineer' the discipline) would silently match nobody.
|
|
393
408
|
assert.match(SRC, /require\('\.\/help-requests'\)/);
|
|
394
409
|
assert.ok(!/CRAFT_TO_DISCIPLINE\s*=|DISCIPLINE_TO_CRAFT\s*=/.test(SRC),
|
|
395
410
|
'mingle must not declare its own craft map');
|
|
@@ -407,9 +407,9 @@ test('methodology joined the roster (task 1002989) — the instruction surfaces
|
|
|
407
407
|
const covers = (touch) => gsc.scopeSubset(['methodology'], [touch]).ok;
|
|
408
408
|
for (const touch of [
|
|
409
409
|
'CLAUDE.md', // the kernel half of the split (1002990)
|
|
410
|
-
'docs/packs/
|
|
410
|
+
'docs/packs/artisan.md', // the three role packs the split authored (1002990)
|
|
411
|
+
'docs/packs/artist.md',
|
|
411
412
|
'docs/packs/artist.md',
|
|
412
|
-
'docs/packs/ideator.md',
|
|
413
413
|
'.claude/skills/status/SKILL.md', // the skills floor no module claims
|
|
414
414
|
'.claude/rules/content-charter.md', // pre-carved for task 1003687
|
|
415
415
|
'docs/adr/0264-the-ten-working-areas.md', // the ADR library
|
|
@@ -767,3 +767,23 @@ test('every file under src/bongos/routes/ is protected by the blanket prefix AND
|
|
|
767
767
|
);
|
|
768
768
|
}
|
|
769
769
|
});
|
|
770
|
+
|
|
771
|
+
// Task 1004793: the module store's code (src/bongos/module-*.js + its scripts/bongos/
|
|
772
|
+
// module-*.js CLIs) was owned by no key, so no goal could author a change to it. Every
|
|
773
|
+
// such file must resolve to a key, and the store files to the 'module-store' key. The
|
|
774
|
+
// directory is read live, so a NEW store file added without an owner fails here.
|
|
775
|
+
test('every module-* store file resolves to a key, the store files to module-store', () => {
|
|
776
|
+
const all = Object.values(scope.MODULE_GLOBS);
|
|
777
|
+
const files = [
|
|
778
|
+
...fs.readdirSync(path.join(ROOT, 'src', 'bongos')).filter((f) => /^module-.*\.js$/.test(f)).map((f) => 'src/bongos/' + f),
|
|
779
|
+
...fs.readdirSync(path.join(ROOT, 'scripts', 'bongos')).filter((f) => /^module-.*\.js$/.test(f)).map((f) => 'scripts/bongos/' + f),
|
|
780
|
+
];
|
|
781
|
+
assert.ok(files.length >= 20, 'expected the store files to still exist, saw ' + files.length);
|
|
782
|
+
const store = scope.MODULE_GLOBS['module-store'];
|
|
783
|
+
for (const f of files) {
|
|
784
|
+
assert.ok(all.some((g) => matchOne(f, g)), f + ' is owned by no key in MODULE_GLOBS');
|
|
785
|
+
if (f === 'src/bongos/module-scope-map.js') assert.ok(matchOne(f, scope.MODULE_GLOBS.kernel), f + ' stays kernel');
|
|
786
|
+
else assert.ok(matchOne(f, store), f + ' should be owned by module-store');
|
|
787
|
+
}
|
|
788
|
+
assert.equal(gsc.scopeSubset(['module-store'], ['src/bongos/module-store.js']).ok, true, 'a module-store goal may touch module-store.js');
|
|
789
|
+
});
|
package/tests/module_api.mjs
CHANGED
|
@@ -65,6 +65,7 @@ const PUBLISHED_SURFACE = [
|
|
|
65
65
|
'buildInfo', 'validateOrRespond', 'LIMITS', 'parseId', 'asyncHandler', 'corsPublicGet', // parseId added 1.8.0 (BV1.R78); asyncHandler + corsPublicGet added 1.12.0 (BV1.R86; lifecycle routes)
|
|
66
66
|
'parsePagination', 'pageMeta', 'PAGINATION', // R14 (#2001, 1.17.0): shared pagination contract for module list routes (ADR 0119)
|
|
67
67
|
'responsibilityFor', 'ROLE_RESPONSIBILITIES', // added by task 1003732 (ADR 0275; criterion wa6-written-responsibilities): the ONE source for the sentence each craft is answerable for. It rides the doorway because two modules need the same text — grading judges role-shaped work against it, the hall shows it to the person being judged — and modules never import each other. Declared here deliberately: this list is what makes adding to the doorway a decision rather than a side effect
|
|
68
|
+
'crafts', // added by task 1005141 (BONGOS-V3 C1): the craft vocabulary (src/crafts.js) — artisan · artist · patron and normalizeCraft() for the old spellings (engineer/ideator/builder). On the doorway because every module input boundary that takes a craft (help requests, task discipline, specialities) must read an old word as today's the same way, and modules never import each other
|
|
68
69
|
'readPackageRegistry', // added by task 1004296 (ADR 0340 D5): the npm registry reader moved into the core, because Settings → Software update needs it in every hall; the module that publishes a package reads the same answer and cache through here rather than keeping a second copy
|
|
69
70
|
'logger',
|
|
70
71
|
];
|
|
@@ -169,26 +169,26 @@ test('craft picker options: OTB.disciplines() mirrors the server list, and falls
|
|
|
169
169
|
const OTB = global.window.OTB;
|
|
170
170
|
|
|
171
171
|
// the server's list wins, in the server's order
|
|
172
|
-
global.window.__MODULES__ = { enabled: {}, disciplines: ['
|
|
173
|
-
assert.deepEqual(OTB.disciplines(), ['
|
|
172
|
+
global.window.__MODULES__ = { enabled: {}, disciplines: ['artisan', 'artist', 'ui'] };
|
|
173
|
+
assert.deepEqual(OTB.disciplines(), ['artisan', 'artist', 'ui']);
|
|
174
174
|
// a module-contributed craft is offered without any per-craft moduleOn() branch
|
|
175
|
-
global.window.__MODULES__ = { enabled: { 'art-pipeline': true }, disciplines: ['
|
|
175
|
+
global.window.__MODULES__ = { enabled: { 'art-pipeline': true }, disciplines: ['artisan', 'artist', 'artist', 'ui'] };
|
|
176
176
|
assert.ok(OTB.disciplines().includes('ui') && OTB.disciplines().includes('artist'));
|
|
177
177
|
// withdrawn with its module
|
|
178
|
-
global.window.__MODULES__ = { enabled: {}, disciplines: ['
|
|
179
|
-
assert.deepEqual(OTB.disciplines(), ['
|
|
178
|
+
global.window.__MODULES__ = { enabled: {}, disciplines: ['artisan', 'artist'] };
|
|
179
|
+
assert.deepEqual(OTB.disciplines(), ['artisan', 'artist']);
|
|
180
180
|
|
|
181
181
|
// an older server that sends no list degrades to the core two, never to an
|
|
182
182
|
// empty picker (which would strand a builder with no way to pick a craft)
|
|
183
183
|
for (const m of [undefined, {}, { enabled: {} }, { disciplines: [] }, { disciplines: 'nope' }]) {
|
|
184
184
|
global.window.__MODULES__ = m;
|
|
185
|
-
assert.deepEqual(OTB.disciplines(), ['
|
|
185
|
+
assert.deepEqual(OTB.disciplines(), ['artisan', 'artist'], `fallback for ${JSON.stringify(m)}`);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
// returns a COPY — a picker mutating its options cannot corrupt the global
|
|
189
|
-
global.window.__MODULES__ = { disciplines: ['
|
|
189
|
+
global.window.__MODULES__ = { disciplines: ['artisan', 'artist', 'ui'] };
|
|
190
190
|
OTB.disciplines().push('bogus');
|
|
191
|
-
assert.deepEqual(OTB.disciplines(), ['
|
|
191
|
+
assert.deepEqual(OTB.disciplines(), ['artisan', 'artist', 'ui']);
|
|
192
192
|
});
|
|
193
193
|
|
|
194
194
|
test('craft picker options: roleLabel names an acronym correctly', () => {
|
|
@@ -199,9 +199,9 @@ test('craft picker options: roleLabel names an acronym correctly', () => {
|
|
|
199
199
|
require(domUtils);
|
|
200
200
|
const OTB = global.window.OTB;
|
|
201
201
|
assert.equal(OTB.roleLabel('ui'), 'UI', 'not "Ui"');
|
|
202
|
-
assert.equal(OTB.roleLabel('
|
|
202
|
+
assert.equal(OTB.roleLabel('artisan'), 'Artisan');
|
|
203
|
+
assert.equal(OTB.roleLabel('artist'), 'Artist');
|
|
203
204
|
assert.equal(OTB.roleLabel('artist'), 'Artist');
|
|
204
|
-
assert.equal(OTB.roleLabel('ideator'), 'Ideator');
|
|
205
205
|
// a craft a module adds later needs no new entry here
|
|
206
206
|
assert.equal(OTB.roleLabel('archivist'), 'Archivist');
|
|
207
207
|
for (const bad of ['', null, undefined]) assert.equal(OTB.roleLabel(bad), '');
|
|
@@ -253,7 +253,7 @@ test('an unset role is never shown to a person as the database default', () => {
|
|
|
253
253
|
assert.equal(OTB.ROLE_UNSET, 'unclassified', 'the stored value is unchanged');
|
|
254
254
|
assert.equal(OTB.roleLabel('unclassified'), 'No role yet');
|
|
255
255
|
assert.ok(OTB.roleIsUnset('unclassified') && OTB.roleIsUnset(null) && OTB.roleIsUnset(''));
|
|
256
|
-
assert.ok(!OTB.roleIsUnset('
|
|
256
|
+
assert.ok(!OTB.roleIsUnset('artisan'));
|
|
257
257
|
|
|
258
258
|
const readSrc = (p) => readFileSync(path.join(ROOT, p), 'utf8');
|
|
259
259
|
// The role-bearing render sites must not interpolate the raw default into markup.
|
|
@@ -271,8 +271,8 @@ test('craft picker options: neither picker hardcodes the craft list any more', (
|
|
|
271
271
|
for (const p of ['modules/hall-ui/public/settings.js', 'modules/hall-ui/public/setup-gate.js']) {
|
|
272
272
|
const src = read(p);
|
|
273
273
|
assert.ok(/OTB\.disciplines\(\)/.test(src), `${p} builds its options from OTB.disciplines()`);
|
|
274
|
-
assert.ok(!/\['
|
|
275
|
-
assert.ok(!/discBtn\('
|
|
274
|
+
assert.ok(!/\['artisan',\s*'artist',\s*'artist'\]/.test(src), `${p} no longer hardcodes the craft triple`);
|
|
275
|
+
assert.ok(!/discBtn\('artisan'/.test(src), `${p} no longer names a craft literally`);
|
|
276
276
|
}
|
|
277
277
|
});
|
|
278
278
|
|