@bongos/core 1.19.804 → 1.19.806
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 +267 -242
- package/.claude/skills/ask-for-help/SKILL.md +6 -6
- package/.claude/skills/backlog-review/SKILL.md +1 -1
- package/.claude/skills/blocker-review/SKILL.md +7 -7
- package/.claude/skills/blocker-solve/SKILL.md +5 -5
- package/.claude/skills/bug-triage/SKILL.md +7 -7
- package/.claude/skills/builder-claim/SKILL.md +3 -3
- package/.claude/skills/builder-cost/SKILL.md +2 -2
- package/.claude/skills/builder-end/SKILL.md +2 -2
- package/.claude/skills/builder-exit/SKILL.md +7 -7
- package/.claude/skills/builder-key/SKILL.md +2 -2
- package/.claude/skills/builder-reauth/SKILL.md +2 -2
- package/.claude/skills/builder-redteam/SKILL.md +7 -7
- package/.claude/skills/builder-release/SKILL.md +2 -2
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-setup/SKILL.md +3 -3
- package/.claude/skills/builder-ship/SKILL.md +1 -1
- package/.claude/skills/builder-stage/SKILL.md +1 -1
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/fix-task/SKILL.md +4 -4
- package/.claude/skills/goal-create/SKILL.md +18 -18
- package/.claude/skills/goal-review/SKILL.md +7 -7
- package/.claude/skills/grade-audit/SKILL.md +5 -5
- package/.claude/skills/grade-recover/SKILL.md +4 -4
- package/.claude/skills/grader-health/SKILL.md +6 -6
- package/.claude/skills/idea-triage/SKILL.md +14 -14
- package/.claude/skills/merge-mode/SKILL.md +3 -3
- package/.claude/skills/planning-session/SKILL.md +20 -20
- package/.claude/skills/priority-session/SKILL.md +10 -10
- package/.claude/skills/recall/SKILL.md +1 -1
- package/.claude/skills/scan-before-install/SKILL.md +3 -3
- package/.claude/skills/status/SKILL.md +2 -2
- package/.claude/skills/strand-fix/SKILL.md +2 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +2 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +45 -11
- package/docs/api-reference.md +11 -10
- package/docs/copy-inventory.md +47 -34
- package/docs/copy-registry.json +200 -72
- package/docs/module-api-changelog.md +4 -0
- package/modules/builder-settings/profile-nudge-prefs.js +1 -1
- package/modules/builder-settings/render-prefs.js +1 -1
- package/modules/dev-box/app/README.md +1 -1
- package/modules/dev-box/app/src/gds-client.js +11 -9
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/dev-box/app/test/unit.mjs +13 -13
- package/modules/dev-box/box-access.js +3 -3
- package/modules/dev-box/box-onboard.js +7 -7
- package/modules/discord/bug-attachments.js +1 -1
- package/modules/economy/cost-classify.js +6 -6
- package/modules/grading/grader-rubric.json +1 -1
- package/modules/grading/grader-score.js +1 -1
- package/modules/hall-ui/module.json +1 -1
- package/modules/hall-ui/public/drachmae.js +3 -3
- package/modules/hall-ui/public/goals-page.js +1 -1
- package/modules/hall-ui/public/goals-render.js +144 -22
- package/modules/hall-ui/public/goals.css +16 -1
- package/modules/hall-ui/public/idea.js +1 -1
- package/modules/hall-ui/public/md-reader.js +1 -1
- package/modules/hall-ui/public/people.html +1 -1
- package/modules/hall-ui/public/people.js +2 -2
- package/modules/hall-ui/public/primer.js +4 -4
- package/modules/hall-ui/public/roster.js +1 -1
- package/modules/hall-ui/public/shell.js +1 -1
- package/modules/hall-ui/public/task.js +1 -1
- package/modules/ideas/inbox.js +2 -2
- package/modules/lifecycle/db-analytics.js +1 -1
- package/modules/lifecycle/done-when.js +4 -4
- package/modules/lifecycle/version-close.js +1 -1
- package/modules/memory/learnings.js +2 -2
- package/modules/memory/memory.js +1 -1
- package/modules/onboarding/onboarding.js +1 -1
- package/modules/platform-identity/routes/my-projects.js +1 -1
- package/modules/platform-identity/routes/sso.js +2 -2
- package/modules/public-landing/public/index.html +2 -2
- package/modules/public-landing/public/projects-floor.states.json +1 -1
- package/modules/public-landing/public/projects.html +1 -1
- package/modules/public-landing/public/projects.states.json +1 -1
- package/modules/sessions/db.js +120 -0
- package/modules/sessions/module.json +2 -2
- package/modules/sessions/pollers/session-upload-health.js +111 -0
- package/modules/sessions/pulse.js +2 -2
- package/modules/sessions/routes/sessions.js +46 -0
- package/modules/sessions/upload-health.js +197 -0
- package/modules/status-ui/module.json +1 -1
- package/modules/status-ui/public/explore.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/agent-invoke.js +3 -3
- package/scripts/gds/api.js +4 -4
- package/scripts/gds/api.sh +1 -1
- package/scripts/gds/architect-audit.js +1 -1
- package/scripts/gds/artifact-audit.js +2 -2
- package/scripts/gds/audit-ships.js +2 -2
- package/scripts/gds/backfill-cost.js +1 -1
- package/scripts/gds/backfill-sessions.js +1 -1
- package/scripts/gds/backfill-subagent-rewards.js +1 -1
- package/scripts/gds/backfill-task-rewards.js +2 -2
- package/scripts/gds/box-auth-check.js +1 -1
- package/scripts/gds/box-sync.js +3 -3
- package/scripts/gds/box.js +10 -10
- package/scripts/gds/build-cli-package.js +3 -3
- package/scripts/gds/capture.js +2 -2
- package/scripts/gds/ci-grade.js +1 -1
- package/scripts/gds/claim-recall.js +2 -2
- package/scripts/gds/cli-lib.js +3 -3
- package/scripts/gds/context-pack.js +1 -1
- package/scripts/gds/copy-apply.js +2 -2
- package/scripts/gds/dev-lib.js +2 -2
- package/scripts/gds/fetch-art-key.js +2 -2
- package/scripts/gds/fetch-sound-prefs.js +3 -3
- package/scripts/gds/grade-smoke.js +1 -1
- package/scripts/gds/hooks/pre-push.js +1 -1
- package/scripts/gds/init.js +9 -9
- package/scripts/gds/key-card.js +1 -1
- package/scripts/gds/learning-capture.js +2 -2
- package/scripts/gds/linkify-refs.js +2 -2
- package/scripts/gds/local-preview-lib.js +1 -1
- package/scripts/gds/login.js +11 -10
- package/scripts/gds/main-audit.js +1 -1
- package/scripts/gds/newcomer-floor.config.json +1 -1
- package/scripts/gds/newcomer-floor.js +2 -2
- package/scripts/gds/newcomer-floor.templates.json +2 -2
- package/scripts/gds/onboarding-ship.js +1 -1
- package/scripts/gds/paste-token.js +3 -3
- package/scripts/gds/portability-smoke-memory.sh +5 -5
- package/scripts/gds/prove-strict-versioning.js +2 -2
- package/scripts/gds/provision-net.js +2 -2
- package/scripts/gds/provision-units.js +3 -3
- package/scripts/gds/pulse.js +2 -2
- package/scripts/gds/push-path-brief.js +2 -2
- package/scripts/gds/reauth.js +1 -1
- package/scripts/gds/recall.js +1 -1
- package/scripts/gds/redteam-patrol.js +3 -3
- package/scripts/gds/render-ideas.js +5 -5
- package/scripts/gds/repo-metrics.js +1 -1
- package/scripts/gds/sandbox-stage.js +3 -3
- package/scripts/gds/seed-adopt-tasks.js +3 -3
- package/scripts/gds/seed-board-room-tasks.js +1 -1
- package/scripts/gds/seed-bongos-app-tasks.js +2 -2
- package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +3 -3
- package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +4 -4
- package/scripts/gds/seed-bongos-tasks.js +5 -5
- package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +1 -1
- package/scripts/gds/seed-bongos-v1-seo-tasks.js +1 -1
- package/scripts/gds/seed-bv1-community-tasks.js +1 -1
- package/scripts/gds/seed-bv1-privacy-invites-tasks.js +1 -1
- package/scripts/gds/seed-bv1-profile-page-tasks.js +1 -1
- package/scripts/gds/seed-bv1-profiles-tasks.js +1 -1
- package/scripts/gds/seed-bv1-r01-delta-tasks.js +1 -1
- package/scripts/gds/seed-gds-v4-tasks.js +3 -3
- package/scripts/gds/seed-medusa-tasks.js +1 -1
- package/scripts/gds/seed-productization-tasks.js +1 -1
- package/scripts/gds/seed-project-admin-console-tasks.js +1 -1
- package/scripts/gds/seed-provisioning-tasks.js +2 -2
- package/scripts/gds/seed-strict-versioning-tasks.js +3 -3
- package/scripts/gds/seed-v2-world-tasks.js +2 -2
- package/scripts/gds/seed-verify-criteria.js +1 -1
- package/scripts/gds/session-digest.js +1 -1
- package/scripts/gds/session.js +2 -2
- package/scripts/gds/setup.js +7 -7
- package/scripts/gds/ship-flow.js +3 -3
- package/scripts/gds/ship-grade-steps.js +3 -3
- package/scripts/gds/ship-memory-sync.js +1 -1
- package/scripts/gds/ship-session-upload.js +3 -3
- package/scripts/gds/smoke-batch-claim.sh +10 -10
- package/scripts/gds/smoke-batch-release.sh +14 -14
- package/scripts/gds/smoke-claim-consistency.sh +19 -19
- package/scripts/gds/smoke-dependencies.sh +9 -9
- package/scripts/gds/smoke-write.sh +11 -11
- package/scripts/gds/start.js +1 -1
- package/scripts/gds/status.js +2 -2
- package/scripts/gds/strand-watch.js +1 -1
- package/scripts/gds/triage.js +4 -4
- package/scripts/gds/version-close.js +2 -2
- package/scripts/hall-preview/README.md +1 -1
- package/scripts/hall-preview/server.js +1 -1
- package/src/bongos/auth-admission.js +3 -3
- package/src/bongos/auth-idp.js +2 -2
- package/src/bongos/auth.js +3 -3
- package/src/bongos/db.js +2 -2
- package/src/bongos/llm-cache.js +2 -2
- package/src/bongos/metrics.js +1 -1
- package/src/bongos/middleware/audit.js +3 -3
- package/src/bongos/middleware/rate-limit.js +1 -1
- package/src/bongos/platform-visibility-gate.js +2 -2
- package/src/bongos/routes/_helpers.js +1 -1
- package/src/bongos/routes/audit-log.js +1 -1
- package/src/bongos/routes/auth.js +5 -5
- package/src/bongos/routes/backup.js +1 -1
- package/src/bongos/routes/builders.js +1 -1
- package/src/bongos/routes/instance.js +1 -1
- package/src/bongos/routes/live.js +1 -1
- package/src/bongos/routes/me.js +2 -2
- package/src/bongos/routes/modules.js +2 -2
- package/src/bongos/routes/public.js +1 -1
- package/src/bongos/routes/security.js +4 -4
- package/src/bongos/routes.js +1 -1
- package/src/bongos/serve-internal.js +6 -6
- package/src/branding.js +1 -1
- package/src/module-api.js +1 -1
- package/src/module-seams.js +2 -2
- package/src/platform-server.js +2 -2
- package/tests/api_alias_exceptions.mjs +67 -0
- package/tests/application_echo.mjs +12 -12
- package/tests/artifact_audit.mjs +1 -1
- package/tests/auth_page_gate.mjs +4 -4
- package/tests/auth_web_recovery.mjs +11 -11
- package/tests/bongos_login.mjs +7 -4
- package/tests/box_env.mjs +6 -6
- package/tests/bug_attachments.mjs +1 -1
- package/tests/cli_sessions.mjs +1 -1
- package/tests/dev_lib.mjs +1 -1
- package/tests/discord_bugs.mjs +2 -2
- package/tests/discord_inbound.mjs +2 -2
- package/tests/explore_visible_states.mjs +1 -1
- package/tests/goal_invite_consent.mjs +307 -0
- package/tests/government_page_gates.mjs +1 -1
- package/tests/hall_reference_world.mjs +1 -1
- package/tests/idea_ideate_full.mjs +1 -1
- package/tests/idp_federation.mjs +2 -2
- package/tests/llm-cache.mjs +1 -1
- package/tests/manage_manifest_shared_read.mjs +3 -3
- package/tests/people_directory_clip.mjs +1 -1
- package/tests/planning_session_skill.mjs +1 -1
- package/tests/platform_visibility_gate.mjs +15 -15
- package/tests/profile_route.mjs +25 -25
- package/tests/projects_hub.mjs +2 -2
- package/tests/provision.mjs +2 -2
- package/tests/provision_settings_apply.mjs +4 -4
- package/tests/sandbox_stage.mjs +8 -8
- package/tests/scouting_owner_access_and_reach.mjs +3 -3
- package/tests/session_upload_health.mjs +329 -0
- package/tests/setup_device_flow_fallback.mjs +1 -1
- package/tests/skill_ask_for_help.mjs +3 -3
- package/tests/skill_grade_audit.mjs +2 -2
- package/tests/skill_grader_health.mjs +3 -3
- package/tests/smoke_write_contract.mjs +1 -1
package/tests/auth_page_gate.mjs
CHANGED
|
@@ -74,7 +74,7 @@ await t('unauthenticated request → 302 to sign-in, next() NOT called', async (
|
|
|
74
74
|
assert.equal(nexted, false, 'next() must not run for an unauthenticated request');
|
|
75
75
|
assert.ok(res._redirect, 'a redirect must be issued');
|
|
76
76
|
assert.equal(res._redirect.code, 302);
|
|
77
|
-
assert.equal(res._redirect.loc, '/api/
|
|
77
|
+
assert.equal(res._redirect.loc, '/api/bongos/auth/web/start');
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
await t('authenticated request → next() called, no redirect', async () => {
|
|
@@ -95,7 +95,7 @@ await t('session lookup error → fails CLOSED (redirect, next() NOT called)', a
|
|
|
95
95
|
let nexted = false;
|
|
96
96
|
await auth.requireBuilderPage(req, res, () => { nexted = true; });
|
|
97
97
|
assert.equal(nexted, false, 'must not serve the page when the lookup errors');
|
|
98
|
-
assert.ok(res._redirect && res._redirect.loc === '/api/
|
|
98
|
+
assert.ok(res._redirect && res._redirect.loc === '/api/bongos/auth/web/start',
|
|
99
99
|
'must redirect to sign-in on a lookup error (fail closed)');
|
|
100
100
|
});
|
|
101
101
|
|
|
@@ -118,7 +118,7 @@ await t('box-scoped session → 302 to sign-in even though its builder holds the
|
|
|
118
118
|
let nexted = false;
|
|
119
119
|
await auth.requireBuilderPage(req, res, () => { nexted = true; });
|
|
120
120
|
assert.equal(nexted, false, 'a box session must not be served a gated page shell');
|
|
121
|
-
assert.ok(res._redirect && res._redirect.loc === '/api/
|
|
121
|
+
assert.ok(res._redirect && res._redirect.loc === '/api/bongos/auth/web/start',
|
|
122
122
|
'the remedy is a different session (/builder-reauth), so sign-in — not deniedTo');
|
|
123
123
|
});
|
|
124
124
|
|
|
@@ -129,7 +129,7 @@ await t('a source the registry does not declare → denied, never treated as uns
|
|
|
129
129
|
let nexted = false;
|
|
130
130
|
await auth.requireBuilderPage(req, res, () => { nexted = true; });
|
|
131
131
|
assert.equal(nexted, false, 'an unrecognised scope must fail closed, exactly as requireBuilder does');
|
|
132
|
-
assert.ok(res._redirect && res._redirect.loc === '/api/
|
|
132
|
+
assert.ok(res._redirect && res._redirect.loc === '/api/bongos/auth/web/start');
|
|
133
133
|
});
|
|
134
134
|
|
|
135
135
|
await t('cli and web sessions are untouched by the scope check', async () => {
|
|
@@ -21,22 +21,22 @@ const req = (cookie) => ({ headers: cookie ? { cookie } : {} });
|
|
|
21
21
|
|
|
22
22
|
test('the retry door carries the wizard return and the repo-scope opt-in while their cookies are valid', () => {
|
|
23
23
|
assert.equal(recoveryStartHref(req('gds_return=%2Fprojects%3Fview%3Dnew; gds_oauth_repos=1')),
|
|
24
|
-
'/api/
|
|
24
|
+
'/api/bongos/auth/web/start?return=%2Fprojects%3Fview%3Dnew&repos=1');
|
|
25
25
|
assert.equal(recoveryStartHref(req('gds_return=%2Fprojects%3Fview%3Dnew')),
|
|
26
|
-
'/api/
|
|
27
|
-
assert.equal(recoveryStartHref(req('gds_oauth_repos=1')), '/api/
|
|
26
|
+
'/api/bongos/auth/web/start?return=%2Fprojects%3Fview%3Dnew', 'a hall sign-in never asks for repos');
|
|
27
|
+
assert.equal(recoveryStartHref(req('gds_oauth_repos=1')), '/api/bongos/auth/web/start?repos=1');
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
test('with nothing left to rebuild from, the door is the bare start — never a guess', () => {
|
|
31
|
-
assert.equal(recoveryStartHref(req('')), '/api/
|
|
32
|
-
assert.equal(recoveryStartHref(req('gds_session=abc')), '/api/
|
|
33
|
-
assert.equal(recoveryStartHref(req('gds_oauth_repos=0')), '/api/
|
|
31
|
+
assert.equal(recoveryStartHref(req('')), '/api/bongos/auth/web/start');
|
|
32
|
+
assert.equal(recoveryStartHref(req('gds_session=abc')), '/api/bongos/auth/web/start');
|
|
33
|
+
assert.equal(recoveryStartHref(req('gds_oauth_repos=0')), '/api/bongos/auth/web/start', 'only the literal opt-in counts');
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
test('the door is under the same open-redirect allowlist as the callback', () => {
|
|
37
|
-
assert.equal(recoveryStartHref(req('gds_return=https%3A%2F%2Fevil.example%2Fx')), '/api/
|
|
38
|
-
assert.equal(recoveryStartHref(req('gds_return=%2F%2Fevil.example')), '/api/
|
|
39
|
-
assert.equal(recoveryStartHref(req('gds_return=javascript%3Aalert(1)')), '/api/
|
|
37
|
+
assert.equal(recoveryStartHref(req('gds_return=https%3A%2F%2Fevil.example%2Fx')), '/api/bongos/auth/web/start');
|
|
38
|
+
assert.equal(recoveryStartHref(req('gds_return=%2F%2Fevil.example')), '/api/bongos/auth/web/start');
|
|
39
|
+
assert.equal(recoveryStartHref(req('gds_return=javascript%3Aalert(1)')), '/api/bongos/auth/web/start');
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
test('every OAuth error page and the pending page rebuild their door from the cookies', () => {
|
|
@@ -47,8 +47,8 @@ test('every OAuth error page and the pending page rebuild their door from the co
|
|
|
47
47
|
'GitHub refused · State mismatch · handshake threw — three pages, three doors');
|
|
48
48
|
assert.match(callback, /renderAccessPendingPage\(err\.login, recoveryStartHref\(req\)\)/, 'the auto-walk-in lands where the founder was going');
|
|
49
49
|
assert.match(callback, /const onWayToProject = founderReturn\.startsWith\('\/projects'\);/, 'the founder-shaped NOT_APPROVED note');
|
|
50
|
-
assert.match(SRC, /const cta = opts\.cta \|\| \{ href: opts\.startHref \|\| '\/api\/
|
|
51
|
-
assert.match(SRC, /data-start="\$\{escapeHtml\(startHref \|\| '\/api\/
|
|
50
|
+
assert.match(SRC, /const cta = opts\.cta \|\| \{ href: opts\.startHref \|\| '\/api\/bongos\/auth\/web\/start', label: copy\.ctaLabel \};/);
|
|
51
|
+
assert.match(SRC, /data-start="\$\{escapeHtml\(startHref \|\| '\/api\/bongos\/auth\/web\/start'\)\}"/, 'the pending page reads its start from a data attribute, never string-built into script');
|
|
52
52
|
assert.match(SRC, /function land\(\) \{ window\.location\.href = start; \}/);
|
|
53
53
|
});
|
|
54
54
|
|
package/tests/bongos_login.mjs
CHANGED
|
@@ -16,12 +16,15 @@ const require = createRequire(import.meta.url);
|
|
|
16
16
|
const { normalizeInstanceBase, pollDecision } = require('../scripts/gds/login.js');
|
|
17
17
|
const LOGIN_JS = path.join(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'), 'scripts', 'gds', 'login.js');
|
|
18
18
|
|
|
19
|
-
test('normalizeInstanceBase adds https, strips trailing slashes + a pasted /api/
|
|
19
|
+
test('normalizeInstanceBase adds https, strips trailing slashes + a pasted /api/bongos', () => {
|
|
20
20
|
assert.equal(normalizeInstanceBase('example.com'), 'https://example.com');
|
|
21
21
|
assert.equal(normalizeInstanceBase('https://example.com/'), 'https://example.com');
|
|
22
22
|
assert.equal(normalizeInstanceBase('https://example.com///'), 'https://example.com');
|
|
23
|
+
assert.equal(normalizeInstanceBase('https://example.com/api/bongos'), 'https://example.com');
|
|
24
|
+
assert.equal(normalizeInstanceBase('https://example.com/api/bongos/'), 'https://example.com');
|
|
25
|
+
assert.equal(normalizeInstanceBase('https://example.com/api/bongos/v1'), 'https://example.com');
|
|
26
|
+
// the permanent alias is still a legal paste — task 1003707 moved the callers, not the door
|
|
23
27
|
assert.equal(normalizeInstanceBase('https://example.com/api/gds'), 'https://example.com');
|
|
24
|
-
assert.equal(normalizeInstanceBase('https://example.com/api/gds/'), 'https://example.com');
|
|
25
28
|
assert.equal(normalizeInstanceBase('http://localhost:3000'), 'http://localhost:3000');
|
|
26
29
|
});
|
|
27
30
|
|
|
@@ -58,10 +61,10 @@ test('login on a Device-Flow-disabled instance explains the cause and the cure',
|
|
|
58
61
|
res.writeHead(code, { 'Content-Type': 'application/json' });
|
|
59
62
|
res.end(JSON.stringify(body));
|
|
60
63
|
};
|
|
61
|
-
if (req.url.endsWith('/api/
|
|
64
|
+
if (req.url.endsWith('/api/bongos/instance')) {
|
|
62
65
|
return send(200, { schema: 'instance-manifest/v1', name: 'Stub', admission: { policy: 'invite-only' } });
|
|
63
66
|
}
|
|
64
|
-
if (req.url.endsWith('/api/
|
|
67
|
+
if (req.url.endsWith('/api/bongos/auth/device/start')) {
|
|
65
68
|
// Byte-for-byte what a real instance returned when its App had the box unticked.
|
|
66
69
|
return send(424, { error: { code: 'device_start_failed', message: 'device/code failed (400): {"error":"device_flow_disabled","error_description":"Device Flow must be explicitly enabled for this App"}' } });
|
|
67
70
|
}
|
package/tests/box_env.mjs
CHANGED
|
@@ -20,15 +20,15 @@ const box = require('../scripts/gds/box.js');
|
|
|
20
20
|
|
|
21
21
|
const { test, summary } = makeRunner();
|
|
22
22
|
|
|
23
|
-
await test('bakes GDS_BASE as the FULL api base (origin + /api/
|
|
23
|
+
await test('bakes GDS_BASE as the FULL api base (origin + /api/bongos) — matches box-report-*.sh, not box-heartbeat.sh\'s bare-origin BASE', () => {
|
|
24
24
|
const env = box.buildBoxEnv({ apiOrigin: 'https://cloudbongos.com' });
|
|
25
|
-
assert.match(env, /^GDS_BASE=https:\/\/cloudbongos\.com\/api\/
|
|
25
|
+
assert.match(env, /^GDS_BASE=https:\/\/cloudbongos\.com\/api\/bongos$/m);
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
await test('strips a trailing slash off the origin before appending /api/
|
|
28
|
+
await test('strips a trailing slash off the origin before appending /api/bongos', () => {
|
|
29
29
|
const env = box.buildBoxEnv({ apiOrigin: 'https://cloudbongos.com/' });
|
|
30
|
-
assert.match(env, /^GDS_BASE=https:\/\/cloudbongos\.com\/api\/
|
|
31
|
-
assert.ok(!env.includes('//api/
|
|
30
|
+
assert.match(env, /^GDS_BASE=https:\/\/cloudbongos\.com\/api\/bongos$/m);
|
|
31
|
+
assert.ok(!env.includes('//api/bongos'), 'must not double the slash');
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
await test('no named tunnel: box.env carries GDS_BASE and nothing else', () => {
|
|
@@ -44,7 +44,7 @@ await test('named tunnel: box.env carries GDS_BASE AND all four tunnel vars', ()
|
|
|
44
44
|
credential: 'cred456', previewHostname: 'sandbox-alice.example.com',
|
|
45
45
|
},
|
|
46
46
|
});
|
|
47
|
-
assert.match(env, /^GDS_BASE=https:\/\/cloudbongos\.com\/api\/
|
|
47
|
+
assert.match(env, /^GDS_BASE=https:\/\/cloudbongos\.com\/api\/bongos$/m);
|
|
48
48
|
assert.match(env, /^BOX_TUNNEL_TOKEN=tok123$/m);
|
|
49
49
|
assert.match(env, /^BOX_TERMINAL_HOSTNAME=term-alice\.example\.com$/m);
|
|
50
50
|
assert.match(env, /^BOX_TERMINAL_CREDENTIAL=cred456$/m);
|
|
@@ -101,7 +101,7 @@ await t('persistImageFromUrl: content-type says png but bytes are not → sniff_
|
|
|
101
101
|
await t('persistImageFromUrl: valid PNG → written to disk, returns durable url', async () => {
|
|
102
102
|
const r = await at.persistImageFromUrl('https://cdn.discordapp.com/a.png', { messageId: 'm42', fetchImpl: fakeFetch(PNG), dir: TMP });
|
|
103
103
|
assert.equal(r.ok, true);
|
|
104
|
-
assert.match(r.url, /^\/api\/
|
|
104
|
+
assert.match(r.url, /^\/api\/bongos\/bug-attachments\/bug-m42-[0-9a-f]{16}\.png$/);
|
|
105
105
|
assert.equal(at.isSafeName(r.file), true);
|
|
106
106
|
const onDisk = await fs.readFile(path.join(TMP, r.file));
|
|
107
107
|
assert.equal(onDisk.length, PNG.length);
|
package/tests/cli_sessions.mjs
CHANGED
|
@@ -259,7 +259,7 @@ test('login VERIFIES a stored token before trusting it, and can be forced past',
|
|
|
259
259
|
// A stored token may be expired or revoked. Reinstating one unchecked would leave the builder
|
|
260
260
|
// "signed in" to a session every later command then fails on.
|
|
261
261
|
assert.match(src, /loadStoredSession\(base\)/);
|
|
262
|
-
assert.match(src, /\/api\/
|
|
262
|
+
assert.match(src, /\/api\/bongos\/me`, \{ headers: \{ Authorization/);
|
|
263
263
|
assert.match(src, /if \(me\.ok && me\.data && me\.data\.builder\)/);
|
|
264
264
|
assert.match(src, /--force/, 'there must be a way to sign in as somebody else');
|
|
265
265
|
});
|
package/tests/dev_lib.mjs
CHANGED
|
@@ -211,7 +211,7 @@ t('stop: kills a live pid, reports not-running otherwise', () => {
|
|
|
211
211
|
await ta('health: up + authConfigured parsed from the two healthz routes', async () => {
|
|
212
212
|
const params = { port: 3006 };
|
|
213
213
|
const deps = { fetchImpl: async (url) => {
|
|
214
|
-
if (url.endsWith('/api/
|
|
214
|
+
if (url.endsWith('/api/bongos/healthz')) return { status: 200, json: async () => ({ ok: true, auth_configured: true }) };
|
|
215
215
|
if (url.endsWith('/healthz')) return { status: 200 };
|
|
216
216
|
return { status: 404 };
|
|
217
217
|
} };
|
package/tests/discord_bugs.mjs
CHANGED
|
@@ -99,7 +99,7 @@ function makeDeps(over = {}) {
|
|
|
99
99
|
calls.persist.push({ urls, opts });
|
|
100
100
|
if (over.persistThrows) throw new Error('persist boom');
|
|
101
101
|
// default: echo each url back as a stored ref unless overridden
|
|
102
|
-
return over.stored ?? urls.map((u, i) => ({ file: `bug-x-${i}.png`, url: `/api/
|
|
102
|
+
return over.stored ?? urls.map((u, i) => ({ file: `bug-x-${i}.png`, url: `/api/bongos/bug-attachments/bug-x-${i}.png` }));
|
|
103
103
|
},
|
|
104
104
|
};
|
|
105
105
|
return { deps, calls };
|
|
@@ -282,7 +282,7 @@ await t('recordInboundBug: text + images → persists images + embeds durable li
|
|
|
282
282
|
assert.equal(calls.persist.length, 1);
|
|
283
283
|
assert.deepEqual(calls.persist[0].urls, ['https://cdn.discordapp.com/a.png']);
|
|
284
284
|
assert.match(calls.create[0].bodyMd, /Attachments \(1\)/);
|
|
285
|
-
assert.match(calls.create[0].bodyMd, /!\[attachment 1\]\(\/api\/
|
|
285
|
+
assert.match(calls.create[0].bodyMd, /!\[attachment 1\]\(\/api\/bongos\/bug-attachments\//);
|
|
286
286
|
});
|
|
287
287
|
|
|
288
288
|
await t('recordInboundBug: image-only (empty text) → filed with screenshot title', async () => {
|
|
@@ -71,7 +71,7 @@ function makeDeps(over = {}) {
|
|
|
71
71
|
persistImages: async (urls, opts) => {
|
|
72
72
|
calls.persist.push({ urls, opts });
|
|
73
73
|
if (over.persistThrows) throw new Error('persist boom');
|
|
74
|
-
return over.stored ?? urls.map((u, i) => ({ file: `bug-x-${i}.png`, url: `/api/
|
|
74
|
+
return over.stored ?? urls.map((u, i) => ({ file: `bug-x-${i}.png`, url: `/api/bongos/bug-attachments/bug-x-${i}.png` }));
|
|
75
75
|
},
|
|
76
76
|
};
|
|
77
77
|
return { deps, calls };
|
|
@@ -224,7 +224,7 @@ await t('recordInboundIdea: text + images → persists + embeds durable links',
|
|
|
224
224
|
assert.equal(r.images, 1);
|
|
225
225
|
assert.equal(calls.persist.length, 1);
|
|
226
226
|
assert.match(calls.capture[0].bodyMd, /Attachments \(1\)/);
|
|
227
|
-
assert.match(calls.capture[0].bodyMd, /!\[attachment 1\]\(\/api\/
|
|
227
|
+
assert.match(calls.capture[0].bodyMd, /!\[attachment 1\]\(\/api\/bongos\/bug-attachments\//);
|
|
228
228
|
});
|
|
229
229
|
|
|
230
230
|
await t('recordInboundIdea: UNLINKED author with image → still re-hosted (guards, not identity)', async () => {
|
|
@@ -280,6 +280,6 @@ test('the page reads the public feed and asks for JSON', async () => {
|
|
|
280
280
|
const r = render([]);
|
|
281
281
|
await settle();
|
|
282
282
|
assert.equal(r.requests.length, 1);
|
|
283
|
-
assert.equal(r.requests[0].url, '/api/
|
|
283
|
+
assert.equal(r.requests[0].url, '/api/bongos/projects/featured');
|
|
284
284
|
assert.equal(r.requests[0].opts.headers.Accept, 'application/json');
|
|
285
285
|
});
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
// tests/goal_invite_consent.mjs — the goals page INVITES; it does not force-add
|
|
2
|
+
// behind the word "Invited." (task 1002886, goal 1000095).
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS FILE EXISTS. The '+ Invite' control posted to POST /goals/:id/members
|
|
5
|
+
// — the immediate force-add — and then toasted 'Invited.' The person was already
|
|
6
|
+
// in the goal and had never been asked. Meanwhile ADR 0112's consented flow
|
|
7
|
+
// (POST /goals/:id/invitations -> a pending row -> the invitee accepts) had its
|
|
8
|
+
// RECEIVING half fully wired in goal-inbox.js and no sender anywhere in the hall,
|
|
9
|
+
// so nothing in the product ever created an invitation.
|
|
10
|
+
//
|
|
11
|
+
// Nothing in tests/ touched the invite box before this file — which is how a
|
|
12
|
+
// control could claim one thing and do the opposite for as long as it did. The
|
|
13
|
+
// lesson generalises past this button: assert the REQUEST a control issues and
|
|
14
|
+
// the SENTENCE it then shows, because those two drifting apart is invisible to
|
|
15
|
+
// any test that only checks the control renders.
|
|
16
|
+
//
|
|
17
|
+
// It runs the REAL goals-render.js (plus the real goals-lib, task-lib and kit)
|
|
18
|
+
// in a vm against a stub DOM and a recording postJSON, and drives renderDetail —
|
|
19
|
+
// the actual entry point, not the private renderer — so what is asserted is the
|
|
20
|
+
// call the page really makes.
|
|
21
|
+
//
|
|
22
|
+
// Run: node tests/goal_invite_consent.mjs
|
|
23
|
+
import { strict as assert } from 'node:assert';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import vm from 'node:vm';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
import { makeRunner } from './helpers.mjs';
|
|
29
|
+
|
|
30
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
31
|
+
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
|
32
|
+
const P = 'modules/hall-ui/public/';
|
|
33
|
+
const RENDER_JS = read(`${P}goals-render.js`);
|
|
34
|
+
|
|
35
|
+
const { test, summary } = makeRunner();
|
|
36
|
+
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
37
|
+
|
|
38
|
+
const SELF = 7001; // the viewer: the goal's creator, so canManage is true
|
|
39
|
+
const MEMBER_ID = 7002; // already in the goal
|
|
40
|
+
const PENDING_ID = 7003; // holds a pending invitation the goal sent
|
|
41
|
+
const FREE_ID = 7004; // neither — the only genuinely invitable builder
|
|
42
|
+
|
|
43
|
+
function makeEl(id) {
|
|
44
|
+
const el = {
|
|
45
|
+
id,
|
|
46
|
+
innerHTML: '', textContent: '', value: '', className: '',
|
|
47
|
+
hidden: false, disabled: false, checked: false,
|
|
48
|
+
options: [], style: {}, dataset: {},
|
|
49
|
+
children: [],
|
|
50
|
+
classList: { add() {}, remove() {}, toggle() {}, contains: () => false },
|
|
51
|
+
appendChild(c) { el.children.push(c); return c; },
|
|
52
|
+
setAttribute() {}, removeAttribute() {}, focus() {}, scrollIntoView() {},
|
|
53
|
+
_on: new Map(),
|
|
54
|
+
addEventListener(ev, fn) {
|
|
55
|
+
if (!el._on.has(ev)) el._on.set(ev, []);
|
|
56
|
+
el._on.get(ev).push(fn);
|
|
57
|
+
},
|
|
58
|
+
fire(ev, arg) {
|
|
59
|
+
for (const fn of el._on.get(ev) || []) fn(arg || { preventDefault() {} });
|
|
60
|
+
},
|
|
61
|
+
querySelector() { return null; },
|
|
62
|
+
querySelectorAll() { return []; },
|
|
63
|
+
};
|
|
64
|
+
return el;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function harness({ pendingRequests = [] } = {}) {
|
|
68
|
+
const els = new Map();
|
|
69
|
+
// The box is re-rendered by replacing innerHTML, and the stub DOM does not
|
|
70
|
+
// parse HTML — so the controls the page writes into #goal-invite are modelled
|
|
71
|
+
// by their ids, which is exactly how the page reaches them ($('#id')).
|
|
72
|
+
// Ids the invite box writes INTO itself. Listed rather than inferred because
|
|
73
|
+
// the stub does not parse HTML — this is the seam where it has to imitate the
|
|
74
|
+
// one thing real innerHTML does that matters here: destroy what was there.
|
|
75
|
+
const INVITE_CHILD_IDS = [
|
|
76
|
+
'goal-invite-open', 'goal-invite-form', 'goal-invite-builder', 'goal-invite-note',
|
|
77
|
+
'goal-invite-cancel', 'goal-direct-open', 'goal-direct-form', 'goal-direct-builder',
|
|
78
|
+
'goal-direct-kind', 'goal-direct-cancel',
|
|
79
|
+
];
|
|
80
|
+
const getEl = (id) => {
|
|
81
|
+
const key = String(id).replace(/^#/, '');
|
|
82
|
+
if (!els.has(key)) {
|
|
83
|
+
const el = makeEl(key);
|
|
84
|
+
if (key === 'goal-invite') {
|
|
85
|
+
// THE BUG THIS MODELS IS REAL: renderInviteBox rebuilds the box on every
|
|
86
|
+
// call, and a stub that kept the old nodes alive would keep their click
|
|
87
|
+
// handlers too — so a single click would re-enter the renderer once per
|
|
88
|
+
// previous render. That is a defect in the STUB, not the page, but only
|
|
89
|
+
// because the page really does replace these nodes. Keep them in step.
|
|
90
|
+
let html = '';
|
|
91
|
+
Object.defineProperty(el, 'innerHTML', {
|
|
92
|
+
get: () => html,
|
|
93
|
+
set: (v) => { html = String(v); for (const cid of INVITE_CHILD_IDS) els.delete(cid); },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
els.set(key, el);
|
|
97
|
+
}
|
|
98
|
+
return els.get(key);
|
|
99
|
+
};
|
|
100
|
+
const documentObj = {
|
|
101
|
+
documentElement: { dataset: {}, style: {}, classList: { add() {}, remove() {} } },
|
|
102
|
+
body: makeEl('body'),
|
|
103
|
+
getElementById: getEl,
|
|
104
|
+
querySelector: (sel) => getEl(sel),
|
|
105
|
+
querySelectorAll: () => [],
|
|
106
|
+
createElement: () => makeEl(),
|
|
107
|
+
addEventListener() {},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const posts = [];
|
|
111
|
+
const toasts = [];
|
|
112
|
+
const state = {
|
|
113
|
+
current: 1,
|
|
114
|
+
view: 'detail',
|
|
115
|
+
details: new Map(),
|
|
116
|
+
directory: [
|
|
117
|
+
{ id: MEMBER_ID, display_name: 'Ada Member', github_login: 'ada' },
|
|
118
|
+
{ id: PENDING_ID, display_name: 'Ben Pending', github_login: 'ben' },
|
|
119
|
+
{ id: FREE_ID, display_name: 'Cleo Free', github_login: 'cleo' },
|
|
120
|
+
],
|
|
121
|
+
pendingRequests,
|
|
122
|
+
myPendingRequest: null,
|
|
123
|
+
inviteOpen: false,
|
|
124
|
+
addDirectOpen: false,
|
|
125
|
+
// The rest of the page state renderDetail walks through on its way to the
|
|
126
|
+
// invite box. Shapes copied from goals-page.js's initial state — the
|
|
127
|
+
// renderers read several of these unguarded, so a partial stub throws
|
|
128
|
+
// before execution ever reaches what is under test here.
|
|
129
|
+
workSort: { todo: { by: null, dir: 'desc' }, done: { by: null, dir: 'desc' } },
|
|
130
|
+
work: { todo: [], done: [] },
|
|
131
|
+
workLoaded: false,
|
|
132
|
+
claimsByTask: new Map(),
|
|
133
|
+
conflictPairs: null,
|
|
134
|
+
conflictSource: null,
|
|
135
|
+
rollups: new Map(),
|
|
136
|
+
activity: new Map(),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const windowObj = {
|
|
140
|
+
location: { assign() {}, href: '/goals' },
|
|
141
|
+
localStorage: { getItem: () => null, setItem() {}, removeItem() {} },
|
|
142
|
+
scrollTo() {}, addEventListener() {},
|
|
143
|
+
matchMedia: () => ({ matches: false, addEventListener() {}, addListener() {} }),
|
|
144
|
+
OTB: {
|
|
145
|
+
escapeHtml: (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'),
|
|
146
|
+
$: (sel) => documentObj.querySelector(sel),
|
|
147
|
+
fmtNum: (n) => String(n ?? ''),
|
|
148
|
+
fmtInt: (n) => String(n ?? ''),
|
|
149
|
+
fmtDate: (d) => String(d ?? '').slice(0, 10),
|
|
150
|
+
toast: (m) => toasts.push(String(m)),
|
|
151
|
+
rankLabel: (r) => String(r ?? ''),
|
|
152
|
+
categoryChip: () => '',
|
|
153
|
+
num: (n) => Number(n) || 0,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const sandbox = {
|
|
158
|
+
window: windowObj, document: documentObj, location: windowObj.location,
|
|
159
|
+
console: { error() {}, warn() {}, log() {} },
|
|
160
|
+
setTimeout, clearTimeout, URLSearchParams, encodeURIComponent, Date, Math, JSON, Set, Map,
|
|
161
|
+
fetch: () => Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve({}) }),
|
|
162
|
+
};
|
|
163
|
+
sandbox.globalThis = sandbox;
|
|
164
|
+
vm.createContext(sandbox);
|
|
165
|
+
for (const f of ['hall-kit.js', 'task-lib.js', 'goals-lib.js', 'goals-render.js']) {
|
|
166
|
+
vm.runInContext(read(P + f), sandbox, { filename: f });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const goal = {
|
|
170
|
+
id: 1, version_id: 'BONGOS-V2', status: 'open', visibility: 'public',
|
|
171
|
+
created_by: SELF, title: 'A goal', scope_modules: [], accepting_requests: true,
|
|
172
|
+
};
|
|
173
|
+
const members = [{ builder_id: MEMBER_ID, display_name: 'Ada Member', membership_kind: 'member' }];
|
|
174
|
+
const detail = { goal, members, criteria: [], satisfied: [], tasks: [] };
|
|
175
|
+
state.details.set('1', detail);
|
|
176
|
+
|
|
177
|
+
const api = sandbox.window.OTBGoalsRender({
|
|
178
|
+
API: '/api/bongos',
|
|
179
|
+
curLabel: () => 'Credits',
|
|
180
|
+
// URL-aware, because the renderers walk several feeds on the way to the
|
|
181
|
+
// invite box and a one-shape-fits-all reply feeds the wrong array into the
|
|
182
|
+
// wrong pager.
|
|
183
|
+
getJSON: async (url) => {
|
|
184
|
+
if (/\/goals\/\d+\/requests$/.test(url)) return { requests: pendingRequests };
|
|
185
|
+
if (/\/goals\/inbox$/.test(url)) return { myPendingRequests: [] };
|
|
186
|
+
if (/\/builders\/directory/.test(url)) return { builders: state.directory };
|
|
187
|
+
if (/\/tasks/.test(url)) return { tasks: [] };
|
|
188
|
+
if (/\/claims/.test(url)) return { claims: [] };
|
|
189
|
+
return {};
|
|
190
|
+
},
|
|
191
|
+
patchJSON: async () => ({}),
|
|
192
|
+
postJSON: async (url, body) => { posts.push({ url, body }); return { ok: true }; },
|
|
193
|
+
sameId: (a, b) => String(a ?? '') === String(b ?? ''),
|
|
194
|
+
selfId: () => SELF,
|
|
195
|
+
state,
|
|
196
|
+
get workTabs() { return { activate() {} }; },
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return { api, state, posts, toasts, getEl, els };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Open the invite box the way a user does: render the detail, then click
|
|
203
|
+
// "+ Invite". Returns the harness with the form on screen.
|
|
204
|
+
async function openInviteForm(h) {
|
|
205
|
+
h.api.renderDetail(1);
|
|
206
|
+
await tick();
|
|
207
|
+
h.getEl('goal-invite-open').fire('click');
|
|
208
|
+
await tick(); await tick();
|
|
209
|
+
return h;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
await test('the source no longer names the force-add as an "invite"', () => {
|
|
215
|
+
assert.ok(!/async function inviteMember\b/.test(RENDER_JS),
|
|
216
|
+
'inviteMember() was the force-add wearing the word invite — it is split in two');
|
|
217
|
+
assert.match(RENDER_JS, /async function sendInvitation\(/, 'the consented sender exists');
|
|
218
|
+
assert.match(RENDER_JS, /async function addMemberDirectly\(/, 'the force-add is named for what it does');
|
|
219
|
+
assert.ok(!/toast\('Invited\.'\)/.test(RENDER_JS), "the 'Invited.' toast on a force-add is gone");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await test('the invite form posts to /invitations and never claims they joined', async () => {
|
|
223
|
+
const h = await openInviteForm(harness());
|
|
224
|
+
h.getEl('goal-invite-builder').value = String(FREE_ID);
|
|
225
|
+
h.getEl('goal-invite-note').value = ' we could use your eye on this ';
|
|
226
|
+
h.getEl('goal-invite-form').fire('submit');
|
|
227
|
+
await tick(); await tick();
|
|
228
|
+
|
|
229
|
+
assert.equal(h.posts.length, 1, 'exactly one write');
|
|
230
|
+
const { url, body } = h.posts[0];
|
|
231
|
+
assert.match(url, /\/goals\/1\/invitations$/, 'the CONSENTED endpoint, not /members');
|
|
232
|
+
assert.ok(!/\/members$/.test(url), 'the force-add endpoint is not what Invite calls');
|
|
233
|
+
assert.equal(body.builder_id, FREE_ID);
|
|
234
|
+
assert.equal(body.note, 'we could use your eye on this', 'the note is sent, trimmed');
|
|
235
|
+
assert.ok(!('membership_kind' in body),
|
|
236
|
+
'the invitations route carries no membership_kind — sending one would be a choice dropped in silence');
|
|
237
|
+
|
|
238
|
+
const said = h.toasts.join(' ');
|
|
239
|
+
assert.match(said, /accept/i, 'the toast turns on their acceptance');
|
|
240
|
+
assert.ok(!/^Invited\.$/.test(h.toasts[0]), 'the old lie is not relocated');
|
|
241
|
+
assert.ok(!/\b(added|joined|is now a member)\b/i.test(said),
|
|
242
|
+
`nobody joined anything yet — got: ${JSON.stringify(h.toasts)}`);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
await test('an empty note is omitted rather than sent as an empty string', async () => {
|
|
246
|
+
const h = await openInviteForm(harness());
|
|
247
|
+
h.getEl('goal-invite-builder').value = String(FREE_ID);
|
|
248
|
+
h.getEl('goal-invite-note').value = ' ';
|
|
249
|
+
h.getEl('goal-invite-form').fire('submit');
|
|
250
|
+
await tick(); await tick();
|
|
251
|
+
assert.ok(!('note' in h.posts[0].body), 'a blank note is absent, not ""');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
await test('the invite form offers no membership-kind picker', async () => {
|
|
255
|
+
const h = await openInviteForm(harness());
|
|
256
|
+
const html = h.getEl('goal-invite').innerHTML;
|
|
257
|
+
assert.ok(!/id="goal-invite-kind"/.test(html),
|
|
258
|
+
'the picker cannot be honoured by an invitation — goal_membership_requests has no such column');
|
|
259
|
+
assert.match(html, /join as a member/i, 'and the form says what they will join as instead');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await test('the dropdown excludes members AND anyone already pending', async () => {
|
|
263
|
+
const h = await openInviteForm(harness({
|
|
264
|
+
pendingRequests: [{ id: 55, kind: 'invite', builder_id: PENDING_ID, display_name: 'Ben Pending' }],
|
|
265
|
+
}));
|
|
266
|
+
const html = h.getEl('goal-invite').innerHTML;
|
|
267
|
+
assert.ok(html.includes(`value="${FREE_ID}"`), 'the invitable builder is offered');
|
|
268
|
+
assert.ok(!html.includes(`value="${MEMBER_ID}"`), 'a member would 409 already_a_member');
|
|
269
|
+
// The one that matters: a pending invitation is INVISIBLE in the member list,
|
|
270
|
+
// so without this filter the user picks an ordinary-looking name and is handed
|
|
271
|
+
// an error they had no way to predict.
|
|
272
|
+
assert.ok(!html.includes(`value="${PENDING_ID}"`), 'a pending invitee would 409 invite_pending');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await test('a pending JOIN-REQUEST is excluded too, not just an invitation', async () => {
|
|
276
|
+
const h = await openInviteForm(harness({
|
|
277
|
+
pendingRequests: [{ id: 56, kind: 'request', builder_id: PENDING_ID }],
|
|
278
|
+
}));
|
|
279
|
+
const html = h.getEl('goal-invite').innerHTML;
|
|
280
|
+
assert.ok(!html.includes(`value="${PENDING_ID}"`),
|
|
281
|
+
'the server refuses on EITHER kind, so the filter may not look only at invites');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await test('the force-add survives, one click away, and says nobody was asked', async () => {
|
|
285
|
+
const h = await openInviteForm(harness());
|
|
286
|
+
const collapsed = h.getEl('goal-invite').innerHTML;
|
|
287
|
+
assert.match(collapsed, /without asking/i, 'the path is discoverable, not removed');
|
|
288
|
+
assert.ok(!/id="goal-direct-form"/.test(collapsed), 'but it is not the default control');
|
|
289
|
+
|
|
290
|
+
h.getEl('goal-direct-open').fire('click');
|
|
291
|
+
await tick();
|
|
292
|
+
const open = h.getEl('goal-invite').innerHTML;
|
|
293
|
+
assert.match(open, /id="goal-direct-kind"/, 'the membership kind IS honoured on this path, so it is offered here');
|
|
294
|
+
|
|
295
|
+
h.getEl('goal-direct-builder').value = String(FREE_ID);
|
|
296
|
+
h.getEl('goal-direct-kind').value = 'lead';
|
|
297
|
+
h.getEl('goal-direct-form').fire('submit');
|
|
298
|
+
await tick(); await tick();
|
|
299
|
+
|
|
300
|
+
assert.equal(h.posts.length, 1);
|
|
301
|
+
assert.match(h.posts[0].url, /\/goals\/1\/members$/, 'the force-add endpoint');
|
|
302
|
+
assert.equal(h.posts[0].body.membership_kind, 'lead', 'and the chosen kind travels');
|
|
303
|
+
const said = h.toasts.join(' ');
|
|
304
|
+
assert.match(said, /without being asked/i, 'the consequence is stated, not hidden behind "Invited."');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
summary();
|
|
@@ -98,7 +98,7 @@ const GATE_FLOOR = {
|
|
|
98
98
|
// ADR 0157: `page.view.government` floor archon→metic.
|
|
99
99
|
requireGovernmentPage: 'metic',
|
|
100
100
|
};
|
|
101
|
-
const SIGN_IN = '/api/
|
|
101
|
+
const SIGN_IN = '/api/bongos/auth/web/start';
|
|
102
102
|
|
|
103
103
|
// --- doubles ----------------------------------------------------------------
|
|
104
104
|
function fakeRes() {
|
|
@@ -241,7 +241,7 @@ const costSummaryFixture = JSON.parse(
|
|
|
241
241
|
test('the treasury reads the endpoint that already carried the series — no new route', () => {
|
|
242
242
|
// The whole point of the task: the payload had both series all along, and the
|
|
243
243
|
// route's own comment calls daily[] "the treasury chart's cumulative line".
|
|
244
|
-
assert.match(drachmaeJs, /TREASURY_URL = '\/api\/
|
|
244
|
+
assert.match(drachmaeJs, /TREASURY_URL = '\/api\/bongos\/public\/cost-summary'/);
|
|
245
245
|
assert.ok(costSummaryFixture.daily.length, 'the payload really carries daily[]');
|
|
246
246
|
assert.ok(costSummaryFixture.daily_credits.length, 'and daily_credits[]');
|
|
247
247
|
assert.ok(Number(costSummaryFixture.total_usd) > 0, 'and the totals the prose states');
|
|
@@ -93,7 +93,7 @@ test('it describes NO second write path', () => {
|
|
|
93
93
|
// ends up sending a different payload than the form does, and the two records
|
|
94
94
|
// drift while both claim to be Full Ideas.
|
|
95
95
|
assert.ok(!/curl/i.test(SKILL), 'no hand-rolled HTTP');
|
|
96
|
-
assert.ok(!/api\.js POST \/api\/
|
|
96
|
+
assert.ok(!/api\.js POST \/api\/bongos\/inbox/.test(SKILL), 'no raw POST for filing an idea');
|
|
97
97
|
const grade = [...SKILL.matchAll(/grade\s*=\s*'([a-z]+)'/g)].map((m) => m[1]);
|
|
98
98
|
assert.ok(grade.every((g) => g === 'full'), `only the full grade is named: ${JSON.stringify(grade)}`);
|
|
99
99
|
});
|
package/tests/idp_federation.mjs
CHANGED
|
@@ -29,7 +29,7 @@ t('builds the hub /sso/authorize URL with client_id, redirect_uri, state', () =>
|
|
|
29
29
|
const url = auth.idpAuthorizeUrl('https://cloudbongos.com', 'client-abc', 'st8', 'https://proj.example.com/api/gds/auth/web/callback');
|
|
30
30
|
const u = new URL(url);
|
|
31
31
|
assert.equal(u.origin, 'https://cloudbongos.com');
|
|
32
|
-
assert.equal(u.pathname, '/api/
|
|
32
|
+
assert.equal(u.pathname, '/api/bongos/sso/authorize');
|
|
33
33
|
assert.equal(u.searchParams.get('client_id'), 'client-abc');
|
|
34
34
|
assert.equal(u.searchParams.get('redirect_uri'), 'https://proj.example.com/api/gds/auth/web/callback');
|
|
35
35
|
assert.equal(u.searchParams.get('state'), 'st8');
|
|
@@ -37,7 +37,7 @@ t('builds the hub /sso/authorize URL with client_id, redirect_uri, state', () =>
|
|
|
37
37
|
|
|
38
38
|
t('trims a trailing slash on the hub origin', () => {
|
|
39
39
|
const url = auth.idpAuthorizeUrl('https://cloudbongos.com/', 'c', 's', 'https://p/cb');
|
|
40
|
-
assert.equal(new URL(url).pathname, '/api/
|
|
40
|
+
assert.equal(new URL(url).pathname, '/api/bongos/sso/authorize');
|
|
41
41
|
assert.ok(!url.includes('com//api'), 'no double slash between origin and path');
|
|
42
42
|
});
|
|
43
43
|
|
package/tests/llm-cache.mjs
CHANGED
|
@@ -88,7 +88,7 @@ await arun('lookup: routes to apiCall on a HIT (right method/path/args)', async
|
|
|
88
88
|
const r = await c.lookup(KEY, { tokensSavedEst: 12 });
|
|
89
89
|
assert.equal(r.result_stdout, 'X');
|
|
90
90
|
assert.equal(calls[0][0], 'POST');
|
|
91
|
-
assert.equal(calls[0][1], '/api/
|
|
91
|
+
assert.equal(calls[0][1], '/api/bongos/llm-cache/lookup');
|
|
92
92
|
assert.deepEqual(calls[0][2], { cache_key: KEY, tokens_saved_est: 12 });
|
|
93
93
|
assert.deepEqual(calls[0][3], { allowUnauthenticated: true });
|
|
94
94
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// manifest per manage entry, EXECUTED (task 1003554).
|
|
3
3
|
//
|
|
4
4
|
// The defect this pins shut: entering /projects?view=manage fetched
|
|
5
|
-
// https://<project>/api/
|
|
5
|
+
// https://<project>/api/bongos/instance twice, identically and cross-origin,
|
|
6
6
|
// because the two cards that render a "what it runs right now" line — how the
|
|
7
7
|
// project is reached (brand.project.platformVisibility) and how people join
|
|
8
8
|
// (brand.project.joinability) — each asked the project for themselves. Two
|
|
@@ -99,7 +99,7 @@ test('the two cards of one manage entry cost ONE round trip, not two', async ()
|
|
|
99
99
|
|
|
100
100
|
// Assert
|
|
101
101
|
assert.equal(ui.requests.length, 1, 'one cross-origin GET served both lines');
|
|
102
|
-
assert.equal(ui.requests[0].url, 'https://demo.example.com/api/
|
|
102
|
+
assert.equal(ui.requests[0].url, 'https://demo.example.com/api/bongos/instance');
|
|
103
103
|
assert.equal(ui.requests[0].mode, 'cors', 'the project is somebody else’s origin');
|
|
104
104
|
for (const a of answers) {
|
|
105
105
|
assert.equal(a.ok, true);
|
|
@@ -151,7 +151,7 @@ test('switching projects is never served the previous project’s manifest', asy
|
|
|
151
151
|
|
|
152
152
|
// Assert
|
|
153
153
|
assert.equal(ui.requests.length, 2, 'the memo is keyed on the project, not merely present');
|
|
154
|
-
assert.equal(ui.requests[1].url, 'https://two.example.com/api/
|
|
154
|
+
assert.equal(ui.requests[1].url, 'https://two.example.com/api/bongos/instance');
|
|
155
155
|
});
|
|
156
156
|
|
|
157
157
|
test('a project that answered-and-refused RESOLVES carrying its status — both lines can say so', async () => {
|
|
@@ -150,7 +150,7 @@ await test('the page asks for exactly the ceiling, and asks for it ONCE', async
|
|
|
150
150
|
const h = harness({ count: 3 });
|
|
151
151
|
await tick(); await tick();
|
|
152
152
|
assert.equal(h.requests.length, 1, 'one directory read');
|
|
153
|
-
assert.match(h.requests[0], /\/api\/
|
|
153
|
+
assert.match(h.requests[0], /\/api\/bongos\/scouting\?limit=/);
|
|
154
154
|
const limit = Number(new URLSearchParams(h.requests[0].split('?')[1]).get('limit'));
|
|
155
155
|
assert.equal(limit, CEILING, `the page must ask for the route ceiling (${CEILING})`);
|
|
156
156
|
});
|
|
@@ -31,7 +31,7 @@ function test(name, fn) {
|
|
|
31
31
|
test('the seeding contract documents the self-check convention', () => {
|
|
32
32
|
assert.ok(NORM.includes('verifySeedCriteriaLinks'), 'names the helper function');
|
|
33
33
|
assert.ok(NORM.includes('seed-verify-criteria.js'), 'names the shared helper file');
|
|
34
|
-
assert.ok(NORM.includes('GET /api/
|
|
34
|
+
assert.ok(NORM.includes('GET /api/bongos/goals/<id>'), 'names the endpoint the self-check fetches');
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
test('the self-check is described as failing loudly on any mismatch, zero included', () => {
|