@bongos/core 1.24.79 → 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 +495 -350
- 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 +2 -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 +70 -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/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/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/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
|
@@ -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
|
});
|
|
@@ -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', () => {
|
|
@@ -52,11 +52,11 @@ const account = (id, handle, extra = {}) => ({
|
|
|
52
52
|
|
|
53
53
|
// Deliberately NOT in rank order — the module ranks, the fake does not.
|
|
54
54
|
const BOARD = [
|
|
55
|
-
{ acct: account('205', 'third-builder'), credits: 50, tasks_shipped: 1, karma: 0, disciplines: ['artist', '
|
|
55
|
+
{ acct: account('205', 'third-builder'), credits: 50, tasks_shipped: 1, karma: 0, disciplines: ['artist', 'artisan'] },
|
|
56
56
|
{ acct: account('201', 'hidden-builder', { hide_stats: true }), credits: 250, tasks_shipped: 8, karma: 1, disciplines: ['artist'] },
|
|
57
|
-
{ acct: account('200', 'top-builder'), credits: 300, tasks_shipped: 9, karma: 4, disciplines: ['
|
|
58
|
-
{ acct: account('202', 'private-builder', { account_visibility: 'private' }), credits: 240, tasks_shipped: 7, karma: 1, disciplines: ['
|
|
59
|
-
{ acct: account('203', 'prov-builder', { profile_state: 'provisional', terms_accepted_at: null }), credits: 230, tasks_shipped: 7, karma: 1, disciplines: ['
|
|
57
|
+
{ acct: account('200', 'top-builder'), credits: 300, tasks_shipped: 9, karma: 4, disciplines: ['artisan'] },
|
|
58
|
+
{ acct: account('202', 'private-builder', { account_visibility: 'private' }), credits: 240, tasks_shipped: 7, karma: 1, disciplines: ['artisan'] },
|
|
59
|
+
{ acct: account('203', 'prov-builder', { profile_state: 'provisional', terms_accepted_at: null }), credits: 230, tasks_shipped: 7, karma: 1, disciplines: ['artisan'] },
|
|
60
60
|
// Opted out of RECRUITING only — spec D3: that opt-out does not touch a leaderboard.
|
|
61
61
|
{ acct: account('204', 'optout-builder', { recruiter_discoverable: false }), credits: 120, tasks_shipped: 3, karma: 0, disciplines: ['ui'] },
|
|
62
62
|
// Ties top-builder on every number: the handle decides, so it ranks second.
|
|
@@ -99,6 +99,7 @@ const moduleApiPath = require.resolve('../src/module-api.js');
|
|
|
99
99
|
require.cache[moduleApiPath] = {
|
|
100
100
|
id: moduleApiPath, filename: moduleApiPath, loaded: true,
|
|
101
101
|
exports: {
|
|
102
|
+
crafts: require('../src/crafts.js'), // the real vocabulary (task 1005141)
|
|
102
103
|
pool: engine,
|
|
103
104
|
corsPublicGet: () => (_req, res, next) => { res.set('Access-Control-Allow-Origin', '*'); next(); },
|
|
104
105
|
logger: () => ({ error: () => {}, info: () => {}, warn: () => {} }),
|
|
@@ -168,7 +169,7 @@ await t('all-time binds no window start; a window binds exactly its start', asyn
|
|
|
168
169
|
await t('ranked credits → ships → karma → handle, with a trimmed discipline filter and clamped paging', async () => {
|
|
169
170
|
const all = await lb.listLeaderboard({ window: 'all', pool: engine });
|
|
170
171
|
assert.deepEqual(all.results.map((r) => r.handle), ['top-builder', 'twin-builder', 'optout-builder', 'third-builder']);
|
|
171
|
-
const eng = await lb.listLeaderboard({ window: 'all', discipline: '
|
|
172
|
+
const eng = await lb.listLeaderboard({ window: 'all', discipline: ' artisan ', limit: 5000, offset: 1, pool: engine });
|
|
172
173
|
assert.deepEqual(eng.results.map((r) => [r.place, r.handle]), [[2, 'third-builder']]);
|
|
173
174
|
assert.equal(eng.limit, lb.MAX_LIMIT);
|
|
174
175
|
});
|
|
@@ -280,6 +281,13 @@ await t('a discipline filter narrows the board and is echoed', async () => {
|
|
|
280
281
|
'the hidden builder holds artist too, and still does not appear');
|
|
281
282
|
});
|
|
282
283
|
|
|
284
|
+
await t('an old link\'s ideator reads as today\'s artist (task 1005141)', async () => {
|
|
285
|
+
const r = await get(port, `${PATH}?window=30d&discipline=ideator`);
|
|
286
|
+
assert.equal(r.status, 200);
|
|
287
|
+
assert.equal(r.json.discipline, 'artist');
|
|
288
|
+
assert.deepEqual(r.json.results.map((x) => x.handle), ['third-builder']);
|
|
289
|
+
});
|
|
290
|
+
|
|
283
291
|
await t('place counts from the page offset, and page echoes the clamped values', async () => {
|
|
284
292
|
const r = await get(port, `${PATH}?limit=1&offset=1`);
|
|
285
293
|
assert.equal(r.status, 200);
|
|
@@ -137,6 +137,9 @@ require.cache[moduleApiPath] = {
|
|
|
137
137
|
pool,
|
|
138
138
|
corsPublicGet: () => (_req, _res, next) => next(),
|
|
139
139
|
logger: () => ({ error: (o, m) => console.error(`route error: ${m}`, o), info: () => {}, warn: () => {} }),
|
|
140
|
+
// The routes fold an old craft name in ?discipline= through it (task 1005141); the real
|
|
141
|
+
// doorway's getter, so this stand-in cannot drift from it (task 1005193).
|
|
142
|
+
crafts: require('../src/crafts.js'),
|
|
140
143
|
},
|
|
141
144
|
};
|
|
142
145
|
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
@@ -102,7 +102,7 @@ const ACCOUNTS = [
|
|
|
102
102
|
// active + public + NOT hidden — the plain control.
|
|
103
103
|
{ github_id: '100', handle: 'plain-vis', github_login: 'plainvis', display_name: 'Plain Visible',
|
|
104
104
|
avatar_url: null, profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'public', hide_stats: false,
|
|
105
|
-
disciplines: ['
|
|
105
|
+
disciplines: ['artisan'] },
|
|
106
106
|
// active + public + HIDDEN — findable by name, not by discipline.
|
|
107
107
|
{ github_id: '101', handle: 'hidden-builder', github_login: 'hiddenbuilder', display_name: 'Hidden Builder',
|
|
108
108
|
avatar_url: null, profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'public', hide_stats: true,
|
|
@@ -110,11 +110,11 @@ const ACCOUNTS = [
|
|
|
110
110
|
// active + PRIVATE — absent entirely, findable or not.
|
|
111
111
|
{ github_id: '102', handle: 'private-builder', github_login: 'privatebuilder', display_name: 'Private Builder',
|
|
112
112
|
avatar_url: null, profile_state: 'active', terms_accepted_at: ACCEPTED_AT, account_visibility: 'private', hide_stats: false,
|
|
113
|
-
disciplines: ['
|
|
113
|
+
disciplines: ['artisan'] },
|
|
114
114
|
// PROVISIONAL — absent entirely.
|
|
115
115
|
{ github_id: '103', handle: 'provisional-builder', github_login: 'provbuilder', display_name: 'Provisional Builder',
|
|
116
116
|
avatar_url: null, profile_state: 'provisional', terms_accepted_at: null, account_visibility: 'public', hide_stats: false,
|
|
117
|
-
disciplines: ['
|
|
117
|
+
disciplines: ['artisan'] },
|
|
118
118
|
// active + public + NOT hidden, but 'octocat' sits mid-handle — a query for
|
|
119
119
|
// it is NOT a prefix match, so this fixture exercises the FUZZY (`%`) leg
|
|
120
120
|
// specifically, not the LIKE prefix leg.
|
|
@@ -185,6 +185,7 @@ const moduleApiPath = require.resolve('../src/module-api.js');
|
|
|
185
185
|
require.cache[moduleApiPath] = {
|
|
186
186
|
id: moduleApiPath, filename: moduleApiPath, loaded: true,
|
|
187
187
|
exports: {
|
|
188
|
+
crafts: require('../src/crafts.js'), // the real vocabulary (task 1005141)
|
|
188
189
|
pool: engine,
|
|
189
190
|
corsPublicGet: () => (_req, _res, next) => next(),
|
|
190
191
|
logger: () => ({ error: () => {}, info: () => {}, warn: () => {} }),
|
|
@@ -252,7 +253,7 @@ await t('the SAME hide_stats account drops out of a DISCIPLINE-filtered search',
|
|
|
252
253
|
await t('a private account never appears — by name or by its real discipline', async () => {
|
|
253
254
|
const byName = await get(port, '/api/bongos/community/search?q=private-builder');
|
|
254
255
|
assert.ok(!byName.json.results.some((x) => x.github_login === 'privatebuilder'));
|
|
255
|
-
const byDiscipline = await get(port, '/api/bongos/community/search?q=private&discipline=
|
|
256
|
+
const byDiscipline = await get(port, '/api/bongos/community/search?q=private&discipline=artisan');
|
|
256
257
|
assert.ok(!byDiscipline.json.results.some((x) => x.github_login === 'privatebuilder'));
|
|
257
258
|
});
|
|
258
259
|
|
|
@@ -271,8 +272,8 @@ await t('a case-differing, NON-PREFIX query hits the fuzzy leg (not just the LIK
|
|
|
271
272
|
});
|
|
272
273
|
|
|
273
274
|
await t('the plain visible account IS found under its real discipline, and ABSENT under another', async () => {
|
|
274
|
-
const hit = await get(port, '/api/bongos/community/search?q=plain-vis&discipline=
|
|
275
|
-
assert.ok(hit.json.results.some((x) => x.github_login === 'plainvis'), '
|
|
275
|
+
const hit = await get(port, '/api/bongos/community/search?q=plain-vis&discipline=artisan');
|
|
276
|
+
assert.ok(hit.json.results.some((x) => x.github_login === 'plainvis'), 'artisan is its real discipline');
|
|
276
277
|
const miss = await get(port, '/api/bongos/community/search?q=plain-vis&discipline=artist');
|
|
277
278
|
assert.ok(!miss.json.results.some((x) => x.github_login === 'plainvis'), 'artist is not its discipline');
|
|
278
279
|
});
|