@bongos/core 1.20.49 → 1.20.50
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 +55 -35
- package/.claude/skills/new-project/SKILL.md +5 -0
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +3 -0
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +314 -294
- package/docs/copy-registry.json +560 -378
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +5 -1
- package/docs/page-readings.json +538 -521
- package/modules/platform-identity/routes/sso.js +5 -0
- package/modules/provisioning/migrations/provisioning_033_terms_ack.sql +33 -0
- package/modules/provisioning/onboard-plan.js +5 -0
- package/modules/provisioning/provisioning.js +7 -4
- package/modules/provisioning/routes/provisioning.js +3 -3
- package/modules/provisioning/terms-ack.js +36 -0
- package/modules/public-landing/public/projects.html +226 -77
- package/modules/public-landing/public/projects.probes.json +8 -2
- package/modules/public-landing/public/projects.states.json +16 -12
- package/modules/ui-design/kit/serve.js +6 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/projects_hub.mjs +4 -2
- package/tests/projects_hub_app_status.mjs +1 -1
- package/tests/projects_hub_app_step.mjs +9 -9
- package/tests/projects_hub_module_picker.mjs +7 -7
- package/tests/projects_hub_pre_uat.mjs +4 -1
- package/tests/provisioning_terms_ack.mjs +216 -0
- package/tests/wizard_draft_resume.mjs +4 -4
- package/tests/wizard_front_door.mjs +271 -0
- package/tests/wizard_intent_resume.mjs +31 -19
|
@@ -159,10 +159,16 @@
|
|
|
159
159
|
"steps": [["wait",900],["click","[data-disc-ask]"],["wait",200],["fill","#mDiscName","old-lantern"],["wait",100],["click","[data-disc-yes]"],["wait",900]],
|
|
160
160
|
"expect": {"text":[["#mDisc .addr","This project is being disconnected."],["#mName","old-lantern"]],"hidden":["#mDiscName","[data-disc-ask]"]} },
|
|
161
161
|
{ "name": "the wizard driver (task 1003346): name, type, about, home (fresh; the stub grants repo access, so the new repository is named from the project), address (the handle's verdict), the review as a ledger, then the filing — the done panel with the standup rail and the human steps", "auth": true, "width": 1440, "url": "/projects?view=new",
|
|
162
|
-
"steps": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#doCreate"],["wait",1500]],
|
|
162
|
+
"steps": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#doCreate"],["wait",1500]],
|
|
163
163
|
"expect": {"visible":["#panel-done","#liveProgress .prog","#liveProgress .prog .stages li.now","#humanSteps li"],"hidden":["#panel-review","#panel-name"],"text":[["#done-h","The drums are rolling"]],"attr":[["#panel-done","class","panel on"]]} },
|
|
164
|
+
{ "name": "the front door (task 1004417): Proceed is refused until the terms are ticked, the fork's demo door says the demo is not open yet, and Begin planet creation goes on to the name step with the rail back", "auth": true, "width": 1440, "url": "/projects?view=new",
|
|
165
|
+
"steps": [["wait",600],["click","#panel-terms button[type=submit]"],["wait",300],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#wizDemo"],["wait",200],["click","#panel-fork button[type=submit]"],["wait",400]],
|
|
166
|
+
"expect": {"visible":["#panel-name","#view-new .steps","#view-new .steps li.now"],"hidden":["#panel-fork","#panel-terms"],"attr":[["#wizDemo","aria-expanded","true"],["#view-new .steps li.now","data-step","3"]]} },
|
|
167
|
+
{ "name": "the front door at 390: the fork's two doors stack inside one pill and the walk still reaches the name step", "auth": true, "width": 390, "url": "/projects?view=new",
|
|
168
|
+
"steps": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400]],
|
|
169
|
+
"expect": {"visible":["#panel-fork",".doorPill","#wizDemo","#panel-fork button[type=submit]"],"hidden":["#view-new .steps"]} },
|
|
164
170
|
{ "name": "the wizard driver at 390: the rail folds to two rows of three and the walk still reaches the review", "auth": true, "width": 390, "url": "/projects?view=new",
|
|
165
|
-
"steps": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400]],
|
|
171
|
+
"steps": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400]],
|
|
166
172
|
"expect": {"visible":["#panel-review","#reviewList li","#doCreate"],"hidden":["#panel-address"],"text":[["#rev-h","Ready to roll"]]} }
|
|
167
173
|
]
|
|
168
174
|
}
|
|
@@ -35,19 +35,23 @@
|
|
|
35
35
|
"in-manage-gone": { "auth": true, "url": "/projects?view=manage&id=104", "expect": { "visible": ["#mMain", "#mActs"], "hidden": ["#mErr", "#mAddress"], "text": [["#mName", "old-lantern"]] } },
|
|
36
36
|
"in-manage-missing": { "auth": true, "url": "/projects?view=manage&id=999", "expect": { "visible": ["#mMissing"], "hidden": ["#mMain", "#mGate"] } },
|
|
37
37
|
"out-manage": { "auth": false, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mGate"], "hidden": ["#mMain", "#mMissing"] } },
|
|
38
|
-
"in-new": { "auth": true, "url": "/projects?view=new", "expect": { "hidden": ["#qWrap"] } },
|
|
38
|
+
"in-new": { "auth": true, "url": "/projects?view=new", "expect": { "visible": ["#panel-terms", "#termsAck"], "hidden": ["#qWrap", "#view-new .steps", "#panel-name"] } },
|
|
39
|
+
"in-new-terms-refused": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#panel-terms button[type=submit]"],["wait",300]], "expect": {"visible":["#panel-terms","#h-terms"],"hidden":["#panel-fork"],"text":[["#h-terms","Tick the box to go on. It’s how your project records that you acknowledged the terms."]]} },
|
|
40
|
+
"in-new-fork": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-fork",".doorPill","#wizDemo",".forkStage",".forkRoutes"],"hidden":["#panel-terms","#view-new .steps","#demoSoon",".bongoCell"]} },
|
|
41
|
+
"in-new-fork-demo": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#wizDemo"],["wait",200]], "expect": {"visible":["#panel-fork","#demoSoon"],"attr":[["#wizDemo","aria-expanded","true"]]} },
|
|
42
|
+
"in-new-name": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-name","#newName","#view-new .steps"],"hidden":["#panel-fork","#panel-terms"]} },
|
|
39
43
|
"out-new": { "auth": false, "url": "/projects?view=new", "expect": { "hidden": ["#qWrap"] } },
|
|
40
|
-
"in-new-type": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-type"],"hidden":["#panel-name","#qWrap"]} },
|
|
41
|
-
"in-new-about": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-detail"],"hidden":["#panel-type","#qWrap"]} },
|
|
42
|
-
"in-new-modules": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900]], "expect": {"visible":["#panel-modules","#modQ","#modCards .tpill[aria-pressed=\"true\"]","#modCoreNote"],"hidden":["#panel-detail","#qWrap"]} },
|
|
43
|
-
"in-new-modules-search": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["fill","#modQ","discord"],["wait",200]], "expect": {"visible":["#panel-modules","#modQ"],"value":[["#modQ","discord"]],"count":[["#modCards .tpill",1],["#modCards .tpill[data-mod=\"discord\"]",1]],"text":[["#modFilterNote","Showing 1 of 3. 2 extras are switched on, including any this search is hiding."]]} },
|
|
44
|
-
"in-new-home": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-home"],"hidden":["#panel-detail","#qWrap"]} },
|
|
45
|
-
"in-new-address": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-address"],"hidden":["#panel-home","#qWrap"]} },
|
|
46
|
-
"in-new-app": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-app","#appOpts","#appOptRender"],"hidden":["#panel-address","#panel-review","#qWrap"]} },
|
|
47
|
-
"in-new-app-render": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#appOptRender input"],["wait",300]], "expect": {"visible":["#panel-app","#renderSetup","#f-renderKey","#renderCheck"],"hidden":["#renderWs","#renderSvc","#renderNew"]} },
|
|
48
|
-
"in-new-review": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-review","#reviewList li"],"hidden":["#panel-address","#qWrap"]} },
|
|
49
|
-
"in-new-done": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#doCreate"],["wait",1500]], "expect": {"visible":["#panel-done","#liveProgress .prog","#humanSteps li"],"hidden":["#panel-review","#qWrap"]} },
|
|
50
|
-
"in-new-resume-offer": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#doCreate"],["wait",1500],["click","#projTrig"],["wait",350],["click","#menuProjects [data-nav=\"new\"]"],["wait",500]], "expect": {"visible":["#panel-
|
|
44
|
+
"in-new-type": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-type"],"hidden":["#panel-name","#qWrap"]} },
|
|
45
|
+
"in-new-about": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-detail"],"hidden":["#panel-type","#qWrap"]} },
|
|
46
|
+
"in-new-modules": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900]], "expect": {"visible":["#panel-modules","#modQ","#modCards .tpill[aria-pressed=\"true\"]","#modCoreNote"],"hidden":["#panel-detail","#qWrap"]} },
|
|
47
|
+
"in-new-modules-search": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["fill","#modQ","discord"],["wait",200]], "expect": {"visible":["#panel-modules","#modQ"],"value":[["#modQ","discord"]],"count":[["#modCards .tpill",1],["#modCards .tpill[data-mod=\"discord\"]",1]],"text":[["#modFilterNote","Showing 1 of 3. 2 extras are switched on, including any this search is hiding."]]} },
|
|
48
|
+
"in-new-home": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-home"],"hidden":["#panel-detail","#qWrap"]} },
|
|
49
|
+
"in-new-address": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-address"],"hidden":["#panel-home","#qWrap"]} },
|
|
50
|
+
"in-new-app": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-app","#appOpts","#appOptRender"],"hidden":["#panel-address","#panel-review","#qWrap"]} },
|
|
51
|
+
"in-new-app-render": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#appOptRender input"],["wait",300]], "expect": {"visible":["#panel-app","#renderSetup","#f-renderKey","#renderCheck"],"hidden":["#renderWs","#renderSvc","#renderNew"]} },
|
|
52
|
+
"in-new-review": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-review","#reviewList li"],"hidden":["#panel-address","#qWrap"]} },
|
|
53
|
+
"in-new-done": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#doCreate"],["wait",1500]], "expect": {"visible":["#panel-done","#liveProgress .prog","#humanSteps li"],"hidden":["#panel-review","#qWrap"]} },
|
|
54
|
+
"in-new-resume-offer": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#doCreate"],["wait",1500],["click","#projTrig"],["wait",350],["click","#menuProjects [data-nav=\"new\"]"],["wait",500]], "expect": {"visible":["#panel-terms","#wizResume","#wizResumeGo"],"hidden":["#panel-done","#panel-name"],"text":[["#wizResumeName","Mercury"]]} },
|
|
51
55
|
"out-map-projects": { "auth": false, "actions": [["click", "#projTrig"], ["wait", 350]], "expect": { "expanded": true, "hidden": ["#menuProjects [data-nav=\"mine\"]"] } },
|
|
52
56
|
"card": { "auth": true, "actions": [["dispatchClick", "#orbField a.worldOrb[data-origin=\"https://example.com\"]"], ["wait", 900]], "expect": { "dialog": true, "hasClass": [["#projCard", "has-art"]], "visible": ["#cardArt", "#cardFaces", "#cardBuildersFig", "#cardShippedFig", "#cardOpen", "#cardHall"], "text": [["#cardBuilders", "11"], ["#cardShipped", "0"], ["#cardRel", "you build here"], ["#cardRuns", "example.com"], ["#cardFaces .facesMore", "+6"]], "count": [["#cardFaces img", 5]], "href": [["#cardHall", "https://builders.example.com/"]] } },
|
|
53
57
|
"card-noart": { "auth": true, "actions": [["dispatchClick", "#orbField a.worldOrb[data-origin=\"https://emersonian-circles.cloudbongos.com\"]"], ["wait", 600]], "expect": { "dialog": true, "hasClass": [["#projCard", "has-art", false]], "hidden": ["#cardArt", "#cardFaces", "#cardHall"], "visible": ["#cardBuildersFig", "#cardShippedFig", "#cardOpen"], "text": [["#cardShipped", "30+"], ["#cardRel", "you build here"]] } },
|
|
@@ -393,6 +393,12 @@ const server = http.createServer((req, res) => {
|
|
|
393
393
|
const all = JSON.parse(fs.readFileSync(path.join(FIXTURES, 'provisioning-instance-render.json'), 'utf8'));
|
|
394
394
|
return json(res, 200, all[appRead[1]] || { ok: true, app: null, env_vars: [], values_held: false });
|
|
395
395
|
}
|
|
396
|
+
// the done panel's invite step (R28, ADR 0251) asks who is worth inviting; the stub
|
|
397
|
+
// gives the route's own "nobody to look for" answer, so a render that lingers on the
|
|
398
|
+
// done panel pictures that branch instead of a 404 (found by task 1004417's render)
|
|
399
|
+
if (/^\/provisioning\/instances\/\d+\/invite-suggestions$/.test(rest) && req.method === 'GET') {
|
|
400
|
+
return AUTH ? json(res, 200, { suggested: [], seeking: null, reason: null }) : json(res, 401, UNAUTH);
|
|
401
|
+
}
|
|
396
402
|
const one = rest.match(/^\/provisioning\/instances\/(\d+)(\/settings)?$/);
|
|
397
403
|
if (one && req.method === 'GET') return AUTH ? instances(res, one[1], !!one[2]) : json(res, 401, UNAUTH);
|
|
398
404
|
// the hub's own cross-project surface (task 1003774): the requested/leave/
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.50",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.50",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.50",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -8407,5 +8407,11 @@
|
|
|
8407
8407
|
"id": "1004402",
|
|
8408
8408
|
"text": "A goal's page now shows where each checklist item really stands: Awaiting UAT, Satisfied, or Closed before UAT, with a Backend-only tag where there is nothing to record. The old Confirm button is gone. Instead, an item whose w"
|
|
8409
8409
|
}
|
|
8410
|
+
],
|
|
8411
|
+
"1.20.50": [
|
|
8412
|
+
{
|
|
8413
|
+
"id": "1004417",
|
|
8414
|
+
"text": "Starting a new project now begins with two decisions before any setup. First screen, \"anything you want to make\": the founder ticks that they can sign legally for the platform and acknowledge the terms, with a link to read the"
|
|
8415
|
+
}
|
|
8410
8416
|
]
|
|
8411
8417
|
}
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.20.
|
|
78
|
+
const CORE_VERSION = '1.20.50'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -714,7 +714,9 @@ test('a renumbering of the wizard invalidates old drafts — the key carries the
|
|
|
714
714
|
assert.match(key, /-v\d+$/, 'the draft key is versioned');
|
|
715
715
|
const panels = (SCRIPT.match(/var panels = \{([^}]*)\}/) || [, ''])[1];
|
|
716
716
|
const count = [...panels.matchAll(/\d+:\s*'panel-/g)].length;
|
|
717
|
-
|
|
717
|
+
// task 1004417 put the front door's two screens (terms, fork) in front: the sixth renumbering
|
|
718
|
+
assert.equal(count, 12, 'gate + the front door\'s two + eight numbered steps + done — bump DRAFT_KEY when this changes');
|
|
719
|
+
assert.equal(key, 'cb-create-draft-v6', 'and the key moved with the front door');
|
|
718
720
|
});
|
|
719
721
|
|
|
720
722
|
test('every row lays the same five parts — the ledger has no ragged row', () => {
|
|
@@ -2182,7 +2184,7 @@ test('the wizard\'s own structures wear the world: hairline fields, cells of one
|
|
|
2182
2184
|
assert.match(block, /\.addrBig code\{display:block;font-family:var\(--font-display\);/, 'the address stands in the display voice');
|
|
2183
2185
|
assert.doesNotMatch(block, /linear-gradient/, 'no tinted plates');
|
|
2184
2186
|
assert.doesNotMatch(block, /box-shadow:(?!none)/, 'no box shadow in the wizard block');
|
|
2185
|
-
assert.match(HUB, /<div class="nav">\s*<button type="button" class="btn ghost" data-back="
|
|
2187
|
+
assert.match(HUB, /<div class="nav">\s*<button type="button" class="btn ghost" data-back="9">← Back<\/button>\s*<button type="button" class="btn big" id="doCreate">Create my project<\/button>/, 'the review\'s doors sit on one row, back then the primary');
|
|
2186
2188
|
assert.match(HUB, /<h2 class="stepsHead" id="doneStepsHead">Your steps — the parts only you can play<\/h2>/, 'the steps heading is the label face by class, not an inline style (the id is where a skipped invite step hands focus on, task 1002346)');
|
|
2187
2189
|
// what is enqueued is unchanged (ADR 0111): the filing body the control plane reads
|
|
2188
2190
|
assert.match(SCRIPT, /body: JSON\.stringify\(\{\s*slug: state\.slug,/, 'the filing still carries the slug first');
|
|
@@ -240,7 +240,7 @@ test('a repaint never lands on top of an open Retry — from this watch or a fre
|
|
|
240
240
|
});
|
|
241
241
|
|
|
242
242
|
test('both surfaces use it: the done panel and the project page', () => {
|
|
243
|
-
assert.match(HUB, /if \(box\) watchApp\(instanceId, box, function \(\) \{ return state\.step ===
|
|
243
|
+
assert.match(HUB, /if \(box\) watchApp\(instanceId, box, function \(\) \{ return state\.step === 11; \}, 5000\);/);
|
|
244
244
|
assert.match(HUB, /<div id="doneApp" hidden><\/div>/);
|
|
245
245
|
assert.match(HUB, /<div class="card mSec" id="mAppCard" hidden>\n\s*<h2>Where its app runs<\/h2>\n\s*<div id="mApp"><\/div>/);
|
|
246
246
|
assert.match(HUB, /loadAppStatus\(inst\.id \|\| asked, inst\);/);
|
|
@@ -142,17 +142,17 @@ test('the panel is real markup: Render and Decide later, Decide later the defaul
|
|
|
142
142
|
assert.doesNotMatch(PANEL, /Coming soon/, 'and is no longer marked soon');
|
|
143
143
|
assert.match(PANEL, /<input type="radio" name="appPlatform" value="later" checked>/, 'Decide later is the default');
|
|
144
144
|
assert.match(PANEL, /We host your Builders' hall/, 'the step says the hall is ours to host (ADR 0345 D1), in the page\'s one spelling of it');
|
|
145
|
-
assert.match(PANEL, /<form data-next="
|
|
146
|
-
assert.match(PANEL, /data-back="
|
|
145
|
+
assert.match(PANEL, /<form data-next="10">/, 'Next leads to the review');
|
|
146
|
+
assert.match(PANEL, /data-back="8"/, 'Back returns to the address step');
|
|
147
147
|
});
|
|
148
148
|
|
|
149
149
|
test('the step sits between Address and Review in the panels map and on the rail', () => {
|
|
150
|
-
assert.match(HUB, /
|
|
151
|
-
assert.match(HUB, /<li data-step="
|
|
152
|
-
assert.match(HUB, /<li data-step="
|
|
153
|
-
assert.match(HUB, /data-back="
|
|
154
|
-
assert.match(HUB, /if \(step ===
|
|
155
|
-
assert.match(HUB, /if \(step ===
|
|
150
|
+
assert.match(HUB, /8: 'panel-address', 9: 'panel-app', 10: 'panel-review', 11: 'panel-done'/);
|
|
151
|
+
assert.match(HUB, /<li data-step="9"><span class="snum">7<\/span><span class="slbl">App<\/span><\/li>/);
|
|
152
|
+
assert.match(HUB, /<li data-step="10"><span class="snum">8<\/span><span class="slbl">Create<\/span><\/li>/);
|
|
153
|
+
assert.match(HUB, /data-back="9">← Back<\/button>\n\s*<button type="button" class="btn big" id="doCreate">/, "the review's Back returns to the app step");
|
|
154
|
+
assert.match(HUB, /if \(step === 9\) \{ syncAppStep\(\); syncRenderSetup\(\); \}\n\s*if \(step === 10\) renderReview\(\);/, 'entering the step repaints it, Render setup included');
|
|
155
|
+
assert.match(HUB, /if \(step === 9\) return validateRender\(\);/, 'Next on the step runs the Render check');
|
|
156
156
|
});
|
|
157
157
|
|
|
158
158
|
test('a fresh draft reads "decide later", and the Render setup stays hidden', () => {
|
|
@@ -455,5 +455,5 @@ test('the review names exactly what was chosen, prices included', () => {
|
|
|
455
455
|
});
|
|
456
456
|
|
|
457
457
|
test('Create hands the app to Render straight after the project is filed', () => {
|
|
458
|
-
assert.match(HUB, /show\(
|
|
458
|
+
assert.match(HUB, /show\(11, true\);\n\s*sendRenderSetup\(data\.instance && data\.instance\.id\);/);
|
|
459
459
|
});
|
|
@@ -41,7 +41,7 @@ const LANDING = path.join(ROOT, 'modules', 'public-landing', 'public');
|
|
|
41
41
|
const HUB = fs.readFileSync(path.join(LANDING, 'projects.html'), 'utf8');
|
|
42
42
|
|
|
43
43
|
const START = '/* ── the module picker (task 1002339, BV1.R20) ──';
|
|
44
|
-
const END = "$('skipModules').addEventListener('click', function () { show(
|
|
44
|
+
const END = "$('skipModules').addEventListener('click', function () { show(7, true); });";
|
|
45
45
|
const from = HUB.indexOf(START);
|
|
46
46
|
const to = HUB.indexOf(END);
|
|
47
47
|
assert.ok(from > -1, 'the picker block opens with its sentinel comment');
|
|
@@ -160,7 +160,7 @@ function boot({ state: seed = {}, fetchOk = true, payload = PAYLOAD } = {}) {
|
|
|
160
160
|
const els = new Map();
|
|
161
161
|
const $ = (id) => { if (!els.has(id)) els.set(id, makeEl(id)); return els.get(id); };
|
|
162
162
|
const form = makeEl('panel-modules-form');
|
|
163
|
-
const state = { step:
|
|
163
|
+
const state = { step: 6, projType: null, teamShape: null, modules: null, ...seed };
|
|
164
164
|
const shown = [];
|
|
165
165
|
let saves = 0;
|
|
166
166
|
let fetches = 0;
|
|
@@ -312,7 +312,7 @@ test('Skip walks past WITHOUT answering, so the server applies the bundle the pa
|
|
|
312
312
|
|
|
313
313
|
// Assert — skipping is the one path that must leave nothing said, because the
|
|
314
314
|
// preselect and the server's fallback are the same declaration (ADR 0237)
|
|
315
|
-
assert.deepEqual(h.shown, [
|
|
315
|
+
assert.deepEqual(h.shown, [7], 'it advances to the Home step (7 since task 1004417)');
|
|
316
316
|
assert.equal(h.state.modules, null, 'and answers nothing');
|
|
317
317
|
assert.equal(h.forCreate(), undefined);
|
|
318
318
|
assert.deepEqual(h.picked(), ['npm-release', 'discord'], 'while the set they were shown is still the set they get');
|
|
@@ -377,7 +377,7 @@ test('a catalog that will not load degrades to a clause — it never blocks a pr
|
|
|
377
377
|
assert.match(h.coreNote(), /couldn’t load/, 'the panel says so in the owner\'s words');
|
|
378
378
|
assert.equal(h.state.modules, null, 'nothing is answered on their behalf');
|
|
379
379
|
h.skip();
|
|
380
|
-
assert.deepEqual(h.shown, [
|
|
380
|
+
assert.deepEqual(h.shown, [7], 'and the wizard still walks on — the server applies the bundle');
|
|
381
381
|
});
|
|
382
382
|
|
|
383
383
|
test('every rendered value goes through the page escaper', async () => {
|
|
@@ -415,7 +415,7 @@ test('the review names the set even when the picker was never opened (task 10023
|
|
|
415
415
|
// and the module NAMES are not known. "None — just the core" would be a false
|
|
416
416
|
// promise on the last screen before Create: the server is about to apply the
|
|
417
417
|
// type's bundle. The row says that instead, then repaints when the read lands.
|
|
418
|
-
const h = boot({ state: { projType: 'game', step:
|
|
418
|
+
const h = boot({ state: { projType: 'game', step: 10 } }); // 10 = the review since task 1004417 (8 since 1004351)
|
|
419
419
|
|
|
420
420
|
// Arrange + Act — the review asks for the value before the read can answer
|
|
421
421
|
const first = h.extrasRow();
|
|
@@ -510,7 +510,7 @@ test('Skip after an answered About step still yields the set the panel showed',
|
|
|
510
510
|
|
|
511
511
|
h.skip();
|
|
512
512
|
|
|
513
|
-
assert.deepEqual(h.shown, [
|
|
513
|
+
assert.deepEqual(h.shown, [7]);
|
|
514
514
|
assert.equal(h.state.modules, null, 'skipping answers nothing, tailored or not');
|
|
515
515
|
assert.equal(h.forCreate(), undefined, 'so the create request still carries no modules key');
|
|
516
516
|
assert.deepEqual(h.picked(), ['discord'], 'while the set they were shown is still the set they get');
|
|
@@ -821,7 +821,7 @@ test('with nothing switched on the count says only what is shown', async () => {
|
|
|
821
821
|
});
|
|
822
822
|
|
|
823
823
|
test('the review names the hidden picks too — a filter never reaches the create request', async () => {
|
|
824
|
-
const h = boot({ state: { projType: 'game', step:
|
|
824
|
+
const h = boot({ state: { projType: 'game', step: 10 } }); // 10 = the review since task 1004417 (8 since 1004351)
|
|
825
825
|
await h.open();
|
|
826
826
|
h.click('agents');
|
|
827
827
|
assert.deepEqual(h.forCreate(), ['agents', 'npm-release', 'discord']);
|
|
@@ -550,7 +550,10 @@ test('the rail at the done panel keeps its marker on Create until the project an
|
|
|
550
550
|
assert.doesNotMatch(fn('show'), /classList\.toggle\('done'/, 'and no longer marks the rail on its own');
|
|
551
551
|
assert.ok(SCRIPT.includes(`if (state.step === ${DONE}) paintRail(${DONE});`),
|
|
552
552
|
'the done-panel poll repaints when the status moves');
|
|
553
|
-
|
|
553
|
+
// the first NUMBERED beat carries it — since task 1004417 the front door's two screens come
|
|
554
|
+
// before the rail, and the card statically wears wizFront so a no-script visitor sees no rail there
|
|
555
|
+
assert.match(HUB, new RegExp(`<li class="now" data-step="${STEP['panel-name']}" aria-current="step"><span class="snum">1</span>`), 'the static rail starts with the same marker');
|
|
556
|
+
assert.match(HUB, /<section id="view-new" hidden>\s*<div class="wrap viewWrap">\s*<div class="card wizFront">/, 'and steps aside on the static first screen');
|
|
554
557
|
});
|
|
555
558
|
|
|
556
559
|
test('every action result announces, nothing under the pointer is deleted, and focus stays where it was', () => {
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// tests/provisioning_terms_ack.mjs
|
|
2
|
+
//
|
|
3
|
+
// The create wizard's terms screen, as the server records it (task 1004417, BV2.PS05;
|
|
4
|
+
// migration provisioning_033). The founder ticks one box; the project row keeps WHEN and
|
|
5
|
+
// WHICH terms version. The owner's call (2026-09-30): the record lives on the PROJECT,
|
|
6
|
+
// not on the person's account — ADR 0213's account stamp is a different record, written
|
|
7
|
+
// only at a witnessed sign-in.
|
|
8
|
+
//
|
|
9
|
+
// The one property every case below protects: the VERSION is the server's answer. The
|
|
10
|
+
// request can say only "the box was ticked"; the version comes from platform-identity's
|
|
11
|
+
// TERMS_VERSION through the platform-identity.hub port, so a body cannot record agreement
|
|
12
|
+
// to a text it names itself.
|
|
13
|
+
//
|
|
14
|
+
// Four layers:
|
|
15
|
+
// • the port — platform-identity's hub provider answers TERMS_VERSION
|
|
16
|
+
// • createInstance — what reaches the INSERT
|
|
17
|
+
// • POST /instances — validation, and that no body field reaches the version
|
|
18
|
+
// • the migration — the columns exist and do not collide with the account's
|
|
19
|
+
//
|
|
20
|
+
// DB-free: a recording pool, stubbed data-access functions, a loopback listener.
|
|
21
|
+
//
|
|
22
|
+
// Run: node tests/provisioning_terms_ack.mjs
|
|
23
|
+
|
|
24
|
+
import { strict as assert } from 'node:assert';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
import http from 'node:http';
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
|
|
31
|
+
let passed = 0, failed = 0;
|
|
32
|
+
async function ta(name, fn) {
|
|
33
|
+
try { await fn(); passed++; console.log(` PASS ${name}`); }
|
|
34
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const api = require('../src/module-api.js');
|
|
38
|
+
const { TERMS_VERSION } = require('../modules/platform-identity/terms-acceptance.js');
|
|
39
|
+
|
|
40
|
+
// ── the port: the hub answers the version /terms states ───────────────────────
|
|
41
|
+
await ta('the platform-identity.hub port answers the current TERMS_VERSION, and it is a read', async () => {
|
|
42
|
+
// Capture what the SSO route factory registers, without touching the real registry.
|
|
43
|
+
const realHas = api.hasProvider, realReg = api.registerProvider;
|
|
44
|
+
const registered = {};
|
|
45
|
+
api.hasProvider = () => false;
|
|
46
|
+
api.registerProvider = (name, provider) => { registered[name] = provider; };
|
|
47
|
+
try {
|
|
48
|
+
require('../modules/platform-identity/routes/sso.js')();
|
|
49
|
+
} finally {
|
|
50
|
+
api.hasProvider = realHas; api.registerProvider = realReg;
|
|
51
|
+
}
|
|
52
|
+
const hub = registered['platform-identity.hub'];
|
|
53
|
+
assert.ok(hub, 'the SSO factory registers the hub port');
|
|
54
|
+
assert.equal(typeof hub.termsVersion, 'function', 'the port carries termsVersion');
|
|
55
|
+
assert.equal(hub.termsVersion(), TERMS_VERSION, 'and it is the version /terms states');
|
|
56
|
+
assert.match(TERMS_VERSION, /^\d{4}-\d{2}-\d{2}$/, 'a date-stamped version, as the page prints it');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ── createInstance: what reaches the INSERT ───────────────────────────────────
|
|
60
|
+
const provisioning = require('../modules/provisioning/provisioning.js');
|
|
61
|
+
const realCreate = provisioning.createInstance;
|
|
62
|
+
|
|
63
|
+
function recordingDb() {
|
|
64
|
+
const calls = [];
|
|
65
|
+
return {
|
|
66
|
+
calls,
|
|
67
|
+
query: async (text, params) => { calls.push({ text, params }); return { rows: [{ id: 1, owner_builder_id: 7, slug: 'demo' }] }; },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function insertOf(db) {
|
|
71
|
+
const insert = db.calls.find((c) => /INSERT INTO provisioning_instances/i.test(c.text));
|
|
72
|
+
assert.ok(insert, 'issued an INSERT');
|
|
73
|
+
return insert;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await ta('createInstance: an acknowledgement stores the time and the server-resolved version', async () => {
|
|
77
|
+
const db = recordingDb();
|
|
78
|
+
await realCreate(db, { ownerBuilderId: 7, slug: 'demo', hostingShape: 'cloud-host', termsAck: { version: '2026-08-29' } });
|
|
79
|
+
const { text, params } = insertOf(db);
|
|
80
|
+
assert.match(text, /terms_ack_at, terms_ack_version\)/, 'both columns are in the column list');
|
|
81
|
+
assert.match(text, /CASE WHEN \$15::boolean THEN now\(\) END, \$16\)/, 'the time is the database clock, never a caller value');
|
|
82
|
+
assert.equal(params[14], true);
|
|
83
|
+
assert.equal(params[15], '2026-08-29');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await ta('createInstance: no acknowledgement leaves both columns NULL — a row that never saw the screen says so', async () => {
|
|
87
|
+
for (const termsAck of [undefined, null]) {
|
|
88
|
+
const db = recordingDb();
|
|
89
|
+
await realCreate(db, { ownerBuilderId: 7, slug: 'demo', hostingShape: 'cloud-host', termsAck });
|
|
90
|
+
const { params } = insertOf(db);
|
|
91
|
+
assert.equal(params[14], false, `termsAck=${termsAck} records no time`);
|
|
92
|
+
assert.equal(params[15], null, 'and no version');
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await ta('createInstance: an acknowledgement with no hub to ask keeps the time and a NULL version', async () => {
|
|
97
|
+
const db = recordingDb();
|
|
98
|
+
await realCreate(db, { ownerBuilderId: 7, slug: 'demo', hostingShape: 'cloud-host', termsAck: { version: null } });
|
|
99
|
+
const { params } = insertOf(db);
|
|
100
|
+
assert.equal(params[14], true, 'the founder did tick the box');
|
|
101
|
+
assert.equal(params[15], null, 'but nothing names a version this instance cannot vouch for');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ── POST /instances over a mounted router ─────────────────────────────────────
|
|
105
|
+
let lastCreateArgs = null;
|
|
106
|
+
provisioning.getInstanceBySlug = async () => null;
|
|
107
|
+
provisioning.getInstanceByDomain = async () => null;
|
|
108
|
+
provisioning.createInstance = async (_db, args) => {
|
|
109
|
+
lastCreateArgs = args;
|
|
110
|
+
return { instance: { id: 41, owner_builder_id: 7, slug: args.slug, status: 'requested', hosting_shape: 'byo-host', detail: {} }, created: true };
|
|
111
|
+
};
|
|
112
|
+
provisioning.enqueueIntent = async (_db, _id, action) => ({ intent: { id: 9, action, state: 'pending' }, created: true });
|
|
113
|
+
provisioning.getOpenIntent = async () => null;
|
|
114
|
+
provisioning.recordEvent = async () => {};
|
|
115
|
+
|
|
116
|
+
api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, github_login: 'owner' }; next(); };
|
|
117
|
+
// The hub port, as the route will see it. `hubVersion === undefined` = no hub at all.
|
|
118
|
+
let hubVersion;
|
|
119
|
+
const realResolveOptional = api.resolveOptional;
|
|
120
|
+
api.resolveOptional = (name, fallback = null) => {
|
|
121
|
+
if (name !== 'platform-identity.hub') return realResolveOptional(name, fallback);
|
|
122
|
+
return hubVersion === undefined ? fallback : { termsVersion: () => hubVersion };
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const express = require('express');
|
|
126
|
+
const app = express();
|
|
127
|
+
app.use(express.json());
|
|
128
|
+
app.use((_req, res, next) => {
|
|
129
|
+
res.fail = (code, opts, details) => {
|
|
130
|
+
const status = typeof opts === 'number' ? opts : (opts && opts.status) || 400;
|
|
131
|
+
res.status(status).json({ error: { code, ...(details || (typeof opts === 'object' ? opts : {})) } });
|
|
132
|
+
};
|
|
133
|
+
next();
|
|
134
|
+
});
|
|
135
|
+
app.use('/api/bongos', require('../modules/provisioning/routes/provisioning.js')());
|
|
136
|
+
const server = app.listen(0);
|
|
137
|
+
await new Promise((r) => server.once('listening', r));
|
|
138
|
+
const base = `http://127.0.0.1:${server.address().port}/api/bongos`;
|
|
139
|
+
|
|
140
|
+
function post(path, body) {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const payload = JSON.stringify(body);
|
|
143
|
+
const req = http.request(`${base}${path}`, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload), Connection: 'close' },
|
|
146
|
+
}, (res) => {
|
|
147
|
+
let d = '';
|
|
148
|
+
res.on('data', (c) => { d += c; });
|
|
149
|
+
res.on('end', () => { let j = null; try { j = JSON.parse(d); } catch { /* non-JSON */ } resolve({ status: res.statusCode, body: j, raw: d }); });
|
|
150
|
+
});
|
|
151
|
+
req.on('error', reject);
|
|
152
|
+
req.end(payload);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await ta('POST /instances: a ticked box records the version the SERVER names', async () => {
|
|
157
|
+
lastCreateArgs = null; hubVersion = '2026-08-29';
|
|
158
|
+
const res = await post('/provisioning/instances', { slug: 'mercury', hosting_shape: 'byo-host', terms_acknowledged: true });
|
|
159
|
+
assert.equal(res.status, 201, res.raw);
|
|
160
|
+
assert.deepEqual(lastCreateArgs.termsAck, { version: '2026-08-29' });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await ta('POST /instances: a caller-named version is refused outright, never recorded', async () => {
|
|
164
|
+
// Every spelling a forger might try, beside a real tick. The schema is closed, so each
|
|
165
|
+
// one is named back as an unknown field and nothing is created at all.
|
|
166
|
+
for (const forged of ['terms_version', 'terms_ack_version', 'termsVersion']) {
|
|
167
|
+
lastCreateArgs = null; hubVersion = '2026-08-29';
|
|
168
|
+
const res = await post('/provisioning/instances', {
|
|
169
|
+
slug: 'mercury', hosting_shape: 'byo-host', terms_acknowledged: true, [forged]: '1999-01-01',
|
|
170
|
+
});
|
|
171
|
+
assert.equal(res.status, 400, res.raw);
|
|
172
|
+
assert.equal(res.body.error.code, 'validation_failed');
|
|
173
|
+
assert.match(res.raw, new RegExp(`"field":"${forged}","reason":"unknown_field"`), `${forged} is named as unknown`);
|
|
174
|
+
assert.equal(lastCreateArgs, null, `${forged}: nothing was created`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await ta('POST /instances: no tick is still a valid create — the CLI and older clients never showed the screen', async () => {
|
|
179
|
+
lastCreateArgs = null; hubVersion = '2026-08-29';
|
|
180
|
+
for (const body of [{}, { terms_acknowledged: false }]) {
|
|
181
|
+
const res = await post('/provisioning/instances', { slug: 'mercury', hosting_shape: 'byo-host', ...body });
|
|
182
|
+
assert.equal(res.status, 201, res.raw);
|
|
183
|
+
assert.equal(lastCreateArgs.termsAck, null, `${JSON.stringify(body)} records nothing`);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await ta('POST /instances: an instance with no hub keeps the tick with a NULL version', async () => {
|
|
188
|
+
lastCreateArgs = null; hubVersion = undefined;
|
|
189
|
+
const res = await post('/provisioning/instances', { slug: 'mercury', hosting_shape: 'byo-host', terms_acknowledged: true });
|
|
190
|
+
assert.equal(res.status, 201, res.raw);
|
|
191
|
+
assert.deepEqual(lastCreateArgs.termsAck, { version: null });
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await ta('POST /instances: terms_acknowledged must be a boolean — "yes" is refused, not read as a tick', async () => {
|
|
195
|
+
lastCreateArgs = null; hubVersion = '2026-08-29';
|
|
196
|
+
const res = await post('/provisioning/instances', { slug: 'mercury', hosting_shape: 'byo-host', terms_acknowledged: 'yes' });
|
|
197
|
+
assert.equal(res.status, 400, res.raw);
|
|
198
|
+
assert.equal(res.body.error.code, 'validation_failed');
|
|
199
|
+
assert.equal(lastCreateArgs, null, 'nothing was created');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
server.close();
|
|
203
|
+
api.resolveOptional = realResolveOptional;
|
|
204
|
+
|
|
205
|
+
// ── the migration ─────────────────────────────────────────────────────────────
|
|
206
|
+
await ta('provisioning_033 adds both columns, nullable, and never names the account\'s acceptance columns', async () => {
|
|
207
|
+
const sql = fs.readFileSync(new URL('../modules/provisioning/migrations/provisioning_033_terms_ack.sql', import.meta.url), 'utf8');
|
|
208
|
+
const code = sql.replace(/--.*$/gm, '');
|
|
209
|
+
assert.match(code, /ADD COLUMN IF NOT EXISTS terms_ack_at timestamptz;/);
|
|
210
|
+
assert.match(code, /ADD COLUMN IF NOT EXISTS terms_ack_version text;/);
|
|
211
|
+
assert.doesNotMatch(code, /NOT NULL|DEFAULT|UPDATE /i, 'no backfill: a project that never saw the screen stays NULL');
|
|
212
|
+
assert.doesNotMatch(code, /\bterms_(?:accepted_at|version|accepted_source)\b/, 'the account\'s columns belong to terms-acceptance.js alone');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
216
|
+
process.exit(failed ? 1 : 0);
|
|
@@ -71,7 +71,7 @@ function runStep({ authKnown, draftStep }) {
|
|
|
71
71
|
|
|
72
72
|
test('pre-auth: the placeholder renders step 1 but the draft KEEPS the real step', () => {
|
|
73
73
|
const { rendered, persisted } = runStep({ authKnown: false, draftStep: 5 });
|
|
74
|
-
assert.deepEqual(rendered, [1], 'still renders the
|
|
74
|
+
assert.deepEqual(rendered, [1], 'still renders step 1 (the terms, since task 1004417) before identity settles');
|
|
75
75
|
assert.equal(persisted.step, 5, 'the draft must not be overwritten with the placeholder');
|
|
76
76
|
});
|
|
77
77
|
|
|
@@ -90,9 +90,9 @@ test('post-auth: the real step is rendered', () => {
|
|
|
90
90
|
|
|
91
91
|
test('a draft already at step 1 is unchanged, and an out-of-range step is not resurrected', () => {
|
|
92
92
|
assert.equal(runStep({ authKnown: false, draftStep: 1 }).persisted.step, 1);
|
|
93
|
-
//
|
|
94
|
-
// is a resumable wizard position, so the placeholder is allowed to stand.
|
|
95
|
-
for (const bad of [0,
|
|
93
|
+
// 11 is the done panel (9 before task 1004417's front door, 8 before task 1004351), reached by its own branch
|
|
94
|
+
// above this slice; 0 is the gate. Neither is a resumable wizard position, so the placeholder is allowed to stand.
|
|
95
|
+
for (const bad of [0, 11, 99]) {
|
|
96
96
|
assert.equal(runStep({ authKnown: false, draftStep: bad }).persisted.step, 1, `step ${bad} is not resumed`);
|
|
97
97
|
}
|
|
98
98
|
});
|