@bongos/core 1.19.438 → 1.19.440
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 +21 -21
- package/.claude/skills/strand-fix/SKILL.md +1 -1
- package/docs/copy-inventory.md +136 -132
- package/docs/copy-registry.json +207 -170
- package/docs/design/projects-hub-direction-v2.md +20 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/lifecycle/ship-assurance.js +12 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +17 -22
- package/public-landing/projects.html +210 -309
- package/public-landing/projects.probes.json +16 -19
- package/public-landing/projects.states.json +5 -4
- package/src/module-api.js +1 -1
- package/tests/projects_hub.mjs +17 -7
- package/tests/projects_hub_pre_uat.mjs +111 -69
- package/tests/ship_assurance.mjs +52 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_": "The hub's interaction contract for modules/ui-design/kit/probe.js (task 1003320): the disclosure bands, the views, the card, the fold under 480, signed out. A probe with `continue` keeps the previous page — the contract is a sequence. The manage flows
|
|
2
|
+
"_": "The hub's interaction contract for modules/ui-design/kit/probe.js (task 1003320): the disclosure bands, the views, the card, the fold under 480, signed out. A probe with `continue` keeps the previous page — the contract is a sequence. The manage flows: since task 1003355 a row carries only its two doors, so attach, restart, revive, retry, teardown (the ender), join and the planet all drive the stub's receipts from the project's OWN page — and one probe holds the row itself to two doors and nothing that acts. The catalog is featured-hub.json (stub.fixtures): the signed-in stub owns Demo, and an owned orb's plain click opens its manage view, never its card — the card probes open the landing's other rows and Tidewater, the un-owned floor row.",
|
|
3
3
|
"page": "/projects",
|
|
4
4
|
"surface": "public-landing",
|
|
5
5
|
"stub": { "fixtures": { "featured": "featured-hub" } },
|
|
@@ -108,24 +108,21 @@
|
|
|
108
108
|
{ "name": "390: a solo Shift tap opens the fold and focuses the field, without overflow", "continue": true,
|
|
109
109
|
"steps": [["press", "Escape"], ["wait", 100], ["press", "Shift"], ["wait", 150]],
|
|
110
110
|
"expect": { "hasClass": [["#qWrap", "is-open", true]], "activeId": "mapQ", "noOverflow": true } },
|
|
111
|
-
{ "name": "
|
|
112
|
-
"steps": [["wait",600]
|
|
113
|
-
"expect": {"
|
|
114
|
-
{ "name": "manage flows
|
|
115
|
-
"steps": [["wait",
|
|
116
|
-
"expect": {"text":[["
|
|
117
|
-
{ "name": "manage flows:
|
|
118
|
-
"steps": [["wait",
|
|
119
|
-
"expect": {"
|
|
120
|
-
{ "name": "manage flows:
|
|
121
|
-
"steps": [["click","[data-
|
|
122
|
-
"expect": {"text":[["
|
|
123
|
-
{ "name": "manage flows:
|
|
124
|
-
"steps": [["wait",
|
|
125
|
-
"expect": {"text":[["#
|
|
126
|
-
{ "name": "manage flows: try again on a snagged row re-files it too", "auth": true, "width": 1440, "url": "/projects?view=mine",
|
|
127
|
-
"steps": [["wait",600],["click","[data-retry=\"103\"]"],["wait",600]],
|
|
128
|
-
"expect": {"text":[["#msg-103","Queued. The platform picks it up from here."]],"hasClass":[["#msg-103","ok",true]]} },
|
|
111
|
+
{ "name": "the row is two doors and nothing that acts (task 1003355): view where a project is live and addressed, manage on every row, no button anywhere in the list", "auth": true, "width": 1440, "url": "/projects?view=mine",
|
|
112
|
+
"steps": [["wait",600]],
|
|
113
|
+
"expect": {"count":[["#projectsList .proj .actions a",7],["#projectsList .proj .actions a[data-manage]",5],["#projectsList .proj .actions button",0],["li[data-inst=\"104\"] .actions a",1]],"text":[["li[data-inst=\"104\"] .actions","Manage"]]} },
|
|
114
|
+
{ "name": "manage flows (task 1003355): the address control on the project's own page records a domain", "auth": true, "width": 1440, "url": "/projects?view=manage&id=101",
|
|
115
|
+
"steps": [["wait",900],["fill","#mDomName","demo.example.com"],["click","[data-m-attach]"],["wait",900]],
|
|
116
|
+
"expect": {"text":[["#mDomMsg","Address recorded."],["#mName","demo"]],"hasClass":[["#mDomMsg","ok",true]],"visible":["#mAddress","#mDomWhat summary"]} },
|
|
117
|
+
{ "name": "manage flows: restart from the unreachable project's own page; the page re-reads it, the rail says queued and the door leaves while the intent is open", "auth": true, "width": 1440, "url": "/projects?view=manage&id=105",
|
|
118
|
+
"steps": [["wait",900],["click","[data-m-restart]"],["wait",900]],
|
|
119
|
+
"expect": {"text":[["#mMsg","Restart queued."],["#mStrip .strip .stages li.now .slbl","Restart queued"]],"hasClass":[["#mMsg","ok",true]],"visible":["#mStrip .strip","#mOrb .worldOrb.ashen"],"hidden":["[data-m-restart]"]} },
|
|
120
|
+
{ "name": "manage flows: bring it back from the torn-down project's own page re-files it", "auth": true, "width": 1440, "url": "/projects?view=manage&id=104",
|
|
121
|
+
"steps": [["wait",900],["click","[data-m-revive]"],["wait",900]],
|
|
122
|
+
"expect": {"text":[["#mMsg","Queued. The platform picks it up from here."]],"hasClass":[["#mMsg","ok",true]],"visible":["#mOrb .worldOrb.ghost"],"hidden":["#mAddress"]} },
|
|
123
|
+
{ "name": "manage flows: try again from the snagged project's own page re-files it too", "auth": true, "width": 1440, "url": "/projects?view=manage&id=103",
|
|
124
|
+
"steps": [["wait",900],["click","[data-m-retry]"],["wait",900]],
|
|
125
|
+
"expect": {"text":[["#mMsg","Queued. The platform picks it up from here."]],"hasClass":[["#mMsg","ok",true]],"hidden":["#mAddress"]} },
|
|
129
126
|
{ "name": "manage flows: how people join — pick a cell, save, the receipt lands under the choice", "auth": true, "width": 1440, "url": "/projects?view=manage&id=101",
|
|
130
127
|
"steps": [["wait",900],["check","input[name=\"mJoinPick\"][value=\"apply\"]"],["click","[data-join-save]"],["wait",600]],
|
|
131
128
|
"expect": {"text":[["#mJoinMsg","Saved. The project restarts to apply it."],["#mName","demo"]],"checked":"input[name=\"mJoinPick\"][value=\"apply\"]","visible":["#mOrb .worldOrb","#mFacts .ledger .k"]} },
|
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
"in-map-search": { "auth": true, "actions": [["clickIfVisible", "#qToggle"], ["wait", 200], ["fill", "#mapQ", "emer"], ["wait", 300]], "expect": { "value": [["#mapQ", "emer"]] } },
|
|
17
17
|
"in-map-mine-on": { "auth": true, "actions": [["check", "#onlyMine"], ["wait", 300]], "expect": { "checked": "#onlyMine" } },
|
|
18
18
|
"in-table": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 250], ["click", "a[data-sub=\"table\"]"], ["wait", 400]], "expect": { "visible": ["#sub-table-body", ".tblMast", ".mastObj", "#selfHall"], "closed": "#menuProjects", "count": [["#mapRows tr[data-generated] .rowHall", 1]] } },
|
|
19
|
-
"in-mine": { "auth": true, "url": "/projects?view=mine", "expect": { "hidden": ["#qWrap", "#empty"], "visible": ["#mineBar", "#projectsList", "#projectsList .proj .worldOrb .disc"], "count": [["#projectsList .proj", 5], ["#projectsList .proj.gone", 1], ["#projectsList .proj .worldOrb[data-shape][data-material][data-style]", 5], ["#projectsList .proj .worldOrb[data-template=\"blob-frost-tint\"]", 1], ["#projectsList .proj .strip", 1], ["#projectsList .proj .worldOrb.ashen", 2], ["#projectsList [
|
|
20
|
-
"in-manage": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mMain", "#mChip .chip", "#mOrb .worldOrb", "#mFacts .ledger .k", "#mActs"], "hidden": ["#qWrap", "#mGate", "#mMissing", "#mErr"], "text": [["#mName", "demo"], ["#mTitle", "demo"]] } },
|
|
19
|
+
"in-mine": { "auth": true, "url": "/projects?view=mine", "expect": { "hidden": ["#qWrap", "#empty"], "visible": ["#mineBar", "#projectsList", "#projectsList .proj .worldOrb .disc"], "count": [["#projectsList .proj", 5], ["#projectsList .proj.gone", 1], ["#projectsList .proj .worldOrb[data-shape][data-material][data-style]", 5], ["#projectsList .proj .worldOrb[data-template=\"blob-frost-tint\"]", 1], ["#projectsList .proj .strip", 1], ["#projectsList .proj .worldOrb.ashen", 2], ["#projectsList .proj .actions a", 7], ["#projectsList .proj .actions button", 0]] } },
|
|
20
|
+
"in-manage": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mMain", "#mChip .chip", "#mOrb .worldOrb", "#mFacts .ledger .k", "#mActs", "#mAddress"], "hidden": ["#qWrap", "#mGate", "#mMissing", "#mErr"], "text": [["#mName", "demo"], ["#mTitle", "demo"]] } },
|
|
21
|
+
"in-manage-address": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mAddress", "#mAddress h2", "#mDom .plain", "#mDomName", "#mDom [data-m-attach]", "#mDomWhat summary"], "count": [["#mDom .attach input[type=text]", 1], ["#mDom .attach button", 1]] } },
|
|
21
22
|
"in-manage-planet": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mPlanetPick", "#mPick .pick .pickPreview .worldOrb .disc", "#mPick [data-pick-save]", "#mPick [data-pick-reset]", "#mPickWhat summary"], "count": [["#mPick .pickCells[role=\"radiogroup\"]", 3], ["#mPick .pickCell", 10], ["#mPick .pickCell.on", 3], ["#mPick .pickSw", 5], ["#mPick .pickSw.on", 1]], "attr": [["#mPick .pickPreview .worldOrb", "data-template", "pair-obsidian-tint"], ["#mOrb .worldOrb", "data-template", "pair-obsidian-tint"]], "checked": "#mPick input[name=\"mPickAccent\"][value=\"sea\"]", "text": [["#mPlanet .v small", "your choice."]] } },
|
|
22
|
-
"in-manage-snag": { "auth": true, "url": "/projects?view=manage&id=103", "expect": { "visible": ["#mMain", "#mErr", "#mActs"], "text": [["#mName", "tidepool"]] } },
|
|
23
|
-
"in-manage-gone": { "auth": true, "url": "/projects?view=manage&id=104", "expect": { "visible": ["#mMain", "#mActs"], "hidden": ["#mErr"], "text": [["#mName", "old-lantern"]] } },
|
|
23
|
+
"in-manage-snag": { "auth": true, "url": "/projects?view=manage&id=103", "expect": { "visible": ["#mMain", "#mErr", "#mActs"], "hidden": ["#mAddress"], "text": [["#mName", "tidepool"]] } },
|
|
24
|
+
"in-manage-gone": { "auth": true, "url": "/projects?view=manage&id=104", "expect": { "visible": ["#mMain", "#mActs"], "hidden": ["#mErr", "#mAddress"], "text": [["#mName", "old-lantern"]] } },
|
|
24
25
|
"in-manage-missing": { "auth": true, "url": "/projects?view=manage&id=999", "expect": { "visible": ["#mMissing"], "hidden": ["#mMain", "#mGate"] } },
|
|
25
26
|
"out-manage": { "auth": false, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mGate"], "hidden": ["#mMain", "#mMissing"] } },
|
|
26
27
|
"in-new": { "auth": true, "url": "/projects?view=new", "expect": { "hidden": ["#qWrap"] } },
|
package/src/module-api.js
CHANGED
|
@@ -33,7 +33,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
33
33
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
34
34
|
// the entry to that file. Look for a version's history there, not here.
|
|
35
35
|
// ---------------------------------------------------------------------------
|
|
36
|
-
const CORE_VERSION = '1.19.
|
|
36
|
+
const CORE_VERSION = '1.19.440'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
37
37
|
|
|
38
38
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
39
39
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -1161,7 +1161,8 @@ test('the manage chip defers to chipFor — one site, no second story (C6)', ()
|
|
|
1161
1161
|
assert.ok(fn, 'renderManage() must be extractable');
|
|
1162
1162
|
assert.equal((fn.match(/chipFor\(inst\)/g) || []).length, 1,
|
|
1163
1163
|
'renderManage must have exactly one chip call site');
|
|
1164
|
-
assert.match(fn, /addressRow\(inst\)/,
|
|
1164
|
+
assert.match(fn, /addressRow\(inst, true\)/,
|
|
1165
|
+
'the address sentence is the shared, verified one — read on THIS surface, so its pointers name the controls this page has (task 1003355)');
|
|
1165
1166
|
assert.match(fn, /SHAPE_LABEL/, 'the hosting shape speaks the card\'s vocabulary');
|
|
1166
1167
|
});
|
|
1167
1168
|
|
|
@@ -1304,12 +1305,11 @@ test('tearing a project down asks for its NAME — the page\'s heaviest control'
|
|
|
1304
1305
|
});
|
|
1305
1306
|
|
|
1306
1307
|
test('the delete section mirrors the server gate — every state but torn_down', () => {
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
//
|
|
1310
|
-
assert.doesNotMatch(
|
|
1311
|
-
'the
|
|
1312
|
-
assert.match(fn, /torn_down/, 'torn_down is the only lifecycle exclusion');
|
|
1308
|
+
// the ender is the ONLY teardown door since task 1003355 took the row's away,
|
|
1309
|
+
// so renderDelete IS the gate: it carries each exclusion as its own honest
|
|
1310
|
+
// state rather than deferring to a predicate nothing else reads
|
|
1311
|
+
assert.doesNotMatch(SCRIPT, /function canTeardown\(/,
|
|
1312
|
+
'the client mirror went with the row button that was its only caller');
|
|
1313
1313
|
const render = (SCRIPT.match(/function renderDelete\(inst\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
1314
1314
|
assert.ok(render, 'renderDelete() must be extractable');
|
|
1315
1315
|
// keyed by PROJECT, not a bare flag: a boolean would also suppress the repaint
|
|
@@ -1320,6 +1320,16 @@ test('the delete section mirrors the server gate — every state but torn_down',
|
|
|
1320
1320
|
for (const s of ['torn_down', 'tearing_down']) {
|
|
1321
1321
|
assert.match(render, new RegExp(s), `${s} gets its own honest state, not the action`);
|
|
1322
1322
|
}
|
|
1323
|
+
assert.match(render, /open_intent\.action === 'teardown'/, 'and a queued teardown is the third');
|
|
1324
|
+
// the old whitelist is what made a snagged project a dead end (N5): every
|
|
1325
|
+
// other state — 'requested' and 'error' included — still reaches the ask
|
|
1326
|
+
assert.match(render, /teardownTruth\(inst\)[\s\S]*?data-del-ask/,
|
|
1327
|
+
'a project that never finished standing up still reaches the ask');
|
|
1328
|
+
// the pointer out of a torn-down project must name a door THIS page has
|
|
1329
|
+
assert.doesNotMatch(render, /view=mine/,
|
|
1330
|
+
'the revive door moved here in task 1003355 — a pointer at the list would be a dead end');
|
|
1331
|
+
assert.match(render, /canRevive\(inst\) \? ' Bring it back with the button at the top of this page\.' : ''/,
|
|
1332
|
+
'and the sentence is dropped where no such button rendered');
|
|
1323
1333
|
});
|
|
1324
1334
|
|
|
1325
1335
|
test('a teardown does not silence the address — the copy says what the runner does', () => {
|
|
@@ -79,16 +79,17 @@ test('the restart door appears ONLY where it has a reason to exist, and mirrors
|
|
|
79
79
|
assert.match(body, /!inst\.open_intent/, 'the strip tells the story while work is in flight');
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
test('the restart door
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
test('the restart door lives on the project’s own page, and the address row points at it from wherever it is read', () => {
|
|
83
|
+
// task 1003355: a list row carries only view and manage, so the door has ONE
|
|
84
|
+
// home — a second copy on the row would be the thing the owner asked to remove
|
|
85
|
+
assert.doesNotMatch(fn('renderCard'), /data-restart/, 'the row acts on nothing');
|
|
86
|
+
assert.match(fn('renderManage'), /canRestart\(inst\)[\s\S]*?data-m-restart/, 'the project’s own page');
|
|
85
87
|
// the door and the sentence that sends the owner to it must ship together —
|
|
86
88
|
// and the sentence must be gated by the SAME predicate, or a project with no
|
|
87
|
-
// button (dedicated shape, or work already queued) is told to
|
|
89
|
+
// button (dedicated shape, or work already queued) is told to go looking for one
|
|
88
90
|
const addr = fn('addressRow');
|
|
89
|
-
assert.match(addr, /or restart it yourself below
|
|
90
|
-
|
|
91
|
-
'the pointer is conditioned on canRestart(), not on last_verified_up alone (ADR 0199 §8)');
|
|
91
|
+
assert.match(addr, /canRestart\(inst\) \? \(onManage \? ' — or restart it yourself below\.' : ' — or restart it from its own page\.'\) : '\.'/,
|
|
92
|
+
'the pointer is conditioned on canRestart() and names a door the READING surface has (ADR 0199 §8)');
|
|
92
93
|
});
|
|
93
94
|
|
|
94
95
|
test('a queued restart has its own strip — never the generic "a <action> run is queued" fallthrough', () => {
|
|
@@ -102,11 +103,12 @@ test('a queued restart has its own strip — never the generic "a <action> run i
|
|
|
102
103
|
});
|
|
103
104
|
|
|
104
105
|
test('the restart sender posts to the restart ROUTE — re-POSTing the slug would be refused on every press', () => {
|
|
105
|
-
const body = fn('
|
|
106
|
+
const body = fn('mRestart');
|
|
106
107
|
assert.match(body, /\/restart'/, 'its own endpoint');
|
|
107
108
|
assert.doesNotMatch(body, /provisioning\/instances',/, 'NOT the create/re-provision POST — PROVISION_ENQUEUE_FROM excludes active');
|
|
108
|
-
assert.match(body,
|
|
109
|
-
|
|
109
|
+
assert.match(body, /\$\('mActs'\)\.querySelectorAll\('button'\)\.forEach\(function \(b\) \{ b\.disabled = true; \}\);/,
|
|
110
|
+
'the doors go quiet while it is in flight');
|
|
111
|
+
assert.doesNotMatch(SCRIPT, /function doRestart\(/, 'and there is no second sender since the row lost its door (task 1003355)');
|
|
110
112
|
});
|
|
111
113
|
/* ── the moment a standup lands (task 1003243) ── */
|
|
112
114
|
test('the landed moment is earned by a VERIFIED probe inside a window, not invented state', () => {
|
|
@@ -319,11 +321,14 @@ test('card metadata is span-wrapped so a separator can never strand; the manage
|
|
|
319
321
|
assert.match(fn('repaintPlanets'), /if \(manageInst\) paintManagePlanet\(manageInst\);/, 'a catalog that lands after the manage page rendered repaints its head too');
|
|
320
322
|
});
|
|
321
323
|
|
|
322
|
-
test('
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
assert.
|
|
326
|
-
assert.match(SCRIPT, /
|
|
324
|
+
test('the one open teardown confirm — the ender’s — survives the minute re-render', () => {
|
|
325
|
+
// task 1003355: the row's two-press confirm is gone with the row's door, so
|
|
326
|
+
// there is exactly one confirmation on the hub and it is the typed-back name
|
|
327
|
+
assert.doesNotMatch(SCRIPT, /cardAsking/, 'the row holds no confirmation to protect');
|
|
328
|
+
assert.match(SCRIPT, /var mDelAsking = null;/);
|
|
329
|
+
assert.match(fn('renderDelete'), /if \(mDelAsking && mDelAsking === inst\.slug\) return;/,
|
|
330
|
+
'and it is keyed by PROJECT, so a different project always repaints');
|
|
331
|
+
assert.match(fn('askDelete'), /mDelAsking = inst\.slug;/);
|
|
327
332
|
});
|
|
328
333
|
|
|
329
334
|
test('the inputs a stranger types into are labelled', () => {
|
|
@@ -332,48 +337,54 @@ test('the inputs a stranger types into are labelled', () => {
|
|
|
332
337
|
assert.match(SCRIPT, /aria-label="A domain you own, for ' \+ esc\(inst\.slug\) \+ '"/);
|
|
333
338
|
});
|
|
334
339
|
|
|
335
|
-
/* ── the custom-domain field
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
assert.match(fold, /<details class="attachFold"/, 'the wizard’s address-step disclosure shape');
|
|
339
|
-
assert.match(fold, /Use a domain I own/, 'labelled the way the wizard labels it');
|
|
340
|
-
assert.match(fold, /inst\.status !== 'active' && inst\.status !== 'requested'\) return ''/, 'live + queued only — snagged and torn-down cards carry no fold');
|
|
341
|
-
assert.match(fold, /if \(!canAttachDomain\(inst\)\) return ''/, 'the server-mirror gate still comes first');
|
|
342
|
-
assert.match(fold, /inst\.open_intent\.action === 'domain-attach'\) return ''/, 'no second attach control while one is wiring');
|
|
343
|
-
assert.doesNotMatch(fold, /Use your own domain/, 'the old filled-button label is gone');
|
|
340
|
+
/* ── the custom-domain field is a section of the project's own page (owner,
|
|
341
|
+
2026-08-29 — task 1003355; it sat behind a fold on the list card until then) ── */
|
|
342
|
+
test('the row carries two doors and nothing that acts; the address field is a section of the project’s own page', () => {
|
|
344
343
|
const card = fn('renderCard');
|
|
345
|
-
assert.doesNotMatch(card, /<
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
assert.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
//
|
|
344
|
+
assert.doesNotMatch(card, /<button/, 'no sender survives on a row');
|
|
345
|
+
assert.doesNotMatch(card, /attachFold|<div class="attach">/, 'and no field either');
|
|
346
|
+
assert.match(card, /'<div class="actions">' \+ acts\.join\(''\)/, 'one doors strip, always rendered');
|
|
347
|
+
assert.match(card, /inst\.status === 'active' && inst\.domain\) acts\.push\([\s\S]{0,180}>View<\/a>'\)/,
|
|
348
|
+
'View only where the project is live and has an address to open');
|
|
349
|
+
assert.match(card, /acts\.push\('<a class="btn small ghost" data-manage="' \+ iid \+ '" href="\?view=manage&id=' \+ iid \+ '">Manage<\/a>'\);/,
|
|
350
|
+
'Manage on every row, as a real link');
|
|
351
|
+
// the section on the manage page: the heading is the disclosure, so the field
|
|
352
|
+
// needs no fold of its own, and the ender's hairline shapes are reused
|
|
353
|
+
assert.match(HUB, /<div class="card mSec" id="mAddress" hidden>/, 'a settings section like the three above it');
|
|
354
|
+
assert.match(HUB, /<h2>Its address<\/h2>/);
|
|
355
|
+
for (const id of ['mDom', 'mDomWhat']) assert.match(HUB, new RegExp('id="' + id + '"'), '#' + id + ' is real markup');
|
|
356
|
+
const sec = fn('renderAddress');
|
|
357
|
+
assert.match(sec, /canAttachDomain\(inst\) && \(inst\.status === 'active' \|\| inst\.status === 'requested'\)/,
|
|
358
|
+
'the server-mirror gate first, then live + queued only — a snagged or torn-down project has nothing to point an address at');
|
|
359
|
+
assert.match(sec, /\$\('mAddress'\)\.hidden = !offer;/, 'and the whole section goes, not just the button');
|
|
360
|
+
assert.match(sec, /inst\.open_intent\.action === 'domain-attach'/, 'no second attach control while one is wiring');
|
|
361
|
+
assert.match(sec, /<div class="attach">/, 'the ender’s hairline field');
|
|
362
|
+
assert.match(sec, /data-m-attach/, 'and the manage page’s own marker');
|
|
363
|
+
assert.doesNotMatch(SCRIPT, /function doAttach\(/, 'the row’s sender is gone with the row’s field');
|
|
364
|
+
// the 390px capture: the row wraps and the field's minimum (220px) is
|
|
365
|
+
// narrower than a phone's body, so it can never be the thing that overflows
|
|
353
366
|
assert.match(COSMOS, /\.attach\{display:flex;gap:10px;margin-top:10px;flex-wrap:wrap;\}/, 'the row wraps at phone width');
|
|
354
|
-
assert.match(COSMOS, /\.attach input\[type=text\]\{flex:1;min-width:220px;/, 'the field fits a 390px
|
|
355
|
-
assert.doesNotMatch(COSMOS,
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
assert.match(
|
|
363
|
-
assert.match(
|
|
364
|
-
assert.match(
|
|
365
|
-
assert.match(fold, /var prefill = rekick \|\| repoint;/, 'a stalled address and a correctable one both arrive in the field');
|
|
366
|
-
assert.match(fold, /rekick \? 'Try the address again' : repoint \? 'Change the address' : 'Use this domain'/, 'and the button says what it does');
|
|
367
|
+
assert.match(COSMOS, /\.attach input\[type=text\]\{flex:1;min-width:220px;/, 'the field fits a 390px page and grows on a laptop');
|
|
368
|
+
assert.doesNotMatch(COSMOS, /attachFold/, 'the fold’s rules went with the fold');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('the F19 re-kick door survives the move: pre-filled, one button, its own sentence', () => {
|
|
372
|
+
const sec = fn('renderAddress');
|
|
373
|
+
assert.match(sec, /var rekick = inst\.status === 'active' && inst\.domain && inst\.error_note;/);
|
|
374
|
+
assert.match(sec, /\(prefill \? ' value="' \+ esc\(inst\.domain\) \+ '"' : ''\)/, 'pre-filled with the address in play');
|
|
375
|
+
assert.match(sec, /var prefill = rekick \|\| repoint;/, 'a stalled address and a correctable one both arrive in the field');
|
|
376
|
+
assert.match(sec, /rekick \? 'Try the address again' : repoint \? 'Change the address' : 'Use this domain'/, 'and the button says what it does');
|
|
377
|
+
assert.match(sec, /rekick \? 'The wiring for this address stopped\./, 'the section says WHY the field is pre-filled — the fold said it by opening itself');
|
|
367
378
|
assert.match(fn('canAttachDomain'), /inst\.status === 'active' && inst\.domain && inst\.error_note && !inst\.open_intent\) return true;/, 'the route-side re-kick gate is untouched');
|
|
368
379
|
});
|
|
369
380
|
|
|
370
|
-
test('the correction door: a bring-your-own address can be changed, and the
|
|
371
|
-
const fold = fn('
|
|
381
|
+
test('the correction door: a bring-your-own address can be changed, and the section says what that costs (task 1003237)', () => {
|
|
382
|
+
const fold = fn('renderAddress');
|
|
372
383
|
// the three doors are told apart by what the project is actually addressed as
|
|
373
384
|
assert.match(fold, /var byo = inst\.domain && inst\.domain !== assignedDefaultFor\(inst\);/,
|
|
374
|
-
'the platform-assigned <slug>.<apex> is NOT a correction — moving off it is the upgrade
|
|
385
|
+
'the platform-assigned <slug>.<apex> is NOT a correction — moving off it is the upgrade this control already offered');
|
|
375
386
|
assert.match(fold, /var repoint = !rekick && byo;/, 'a stalled address keeps the F19 door, which is louder');
|
|
376
|
-
assert.match(fold, /'
|
|
387
|
+
assert.match(fold, /repoint \? 'It answers on an address you brought\./, 'the lead names the correction');
|
|
377
388
|
assert.match(fold, /attachNote/, 'and the consequence is stated where the button is');
|
|
378
389
|
assert.match(fold, /won’t use that address any more/, 'letting the old address go is SAID, not discovered');
|
|
379
390
|
// the door itself: an addressed, live project is no longer excluded
|
|
@@ -392,13 +403,23 @@ test('the retired refusal is gone from the card: domain_already_set can no longe
|
|
|
392
403
|
'the server stopped sending it (task 1003237) — a branch for it would be unreachable copy');
|
|
393
404
|
});
|
|
394
405
|
|
|
395
|
-
test('
|
|
406
|
+
test('the minute tick keeps a half-typed domain typed, and its receipt on the page', () => {
|
|
407
|
+
// the rule moved with the field (task 1003355): renderManage repaints every
|
|
408
|
+
// section once a minute, so the address section must survive its own repaint
|
|
409
|
+
const sec = fn('renderAddress');
|
|
410
|
+
assert.match(sec, /old\.value !== old\.defaultValue \? old\.value : ''/, 'typing is told apart from the pre-fill');
|
|
411
|
+
assert.match(sec, /document\.activeElement === old/, 'the caret comes back too');
|
|
412
|
+
// lastIndexOf: the in-flight branch above writes the same slot, and returns
|
|
413
|
+
const set = sec.lastIndexOf("$('mDom').innerHTML =");
|
|
414
|
+
assert.ok(sec.indexOf('var typed =') < set && sec.indexOf('if (field && typed) field.value = typed;') > set,
|
|
415
|
+
'read before the swap, restore after it');
|
|
416
|
+
assert.match(sec, /if \(mDomFor !== inst\.id\) \{ mDomFor = inst\.id; mDomNote = null; \}/,
|
|
417
|
+
'and the receipt is keyed by PROJECT, so a different project never wears the last one’s');
|
|
418
|
+
assert.match(sec, /\(mDomNote \? mDomNote\.html : ''\)/, 'a repaint re-renders the receipt rather than swallowing it');
|
|
419
|
+
// the row keeps nothing to restore but the door that had focus
|
|
396
420
|
const card = fn('renderCard');
|
|
397
|
-
assert.
|
|
398
|
-
assert.match(card, /
|
|
399
|
-
assert.match(card, /document\.activeElement === oldInput/, 'the caret comes back too');
|
|
400
|
-
const set = card.indexOf('li.innerHTML = orbFor(inst)');
|
|
401
|
-
assert.ok(card.indexOf('var keepOpen') < set && card.indexOf('if (fold && keepOpen) fold.open = true;') > set, 'read before the swap, restore after it');
|
|
421
|
+
assert.doesNotMatch(card, /oldFold|oldInput|keepOpen/, 'the row holds no field or fold to carry across the swap');
|
|
422
|
+
assert.match(card, /if \(wasInside\) focusCardControl\(inst\.id, prefer\);/);
|
|
402
423
|
});
|
|
403
424
|
|
|
404
425
|
/* ── the owner's five calls of 2026-08-24 (task 1003250) ── */
|
|
@@ -464,7 +485,18 @@ test('an unmapped create failure gets a plain sentence with the raw text folded
|
|
|
464
485
|
test('a manifest error says a plain sentence, folds the route text, and sends a domainless project to its address', () => {
|
|
465
486
|
const steps = fn('renderStepOauthApp');
|
|
466
487
|
assert.doesNotMatch(steps, /'GitHub setup hit a snag' \+ \(live\.manifestMsg \? ': ' \+ esc\(live\.manifestMsg\)/, 'the raw route text is no longer the headline');
|
|
467
|
-
assert.match(steps, /else if \(live\.manifestCode === 'domain_required'\) snag = 'This project needs an address first — attach one from
|
|
488
|
+
assert.match(steps, /else if \(live\.manifestCode === 'domain_required'\) snag = 'This project needs an address first — attach one from ' \+ projectPageLink\('its own page'\)/,
|
|
489
|
+
'the address lives on the project’s own page since task 1003355 — the list would be a dead end');
|
|
490
|
+
assert.match(fn('projectPageLink'), /instanceId \? '\?view=manage&id=' \+ encodeURIComponent\(instanceId\) : '\?view=mine'/,
|
|
491
|
+
'and it is named by id whenever the flow has one');
|
|
492
|
+
// every done-panel pointer that names a CONTROL goes the same way; the ones
|
|
493
|
+
// that only say "watch it there" still name the list, which does show it
|
|
494
|
+
assert.equal((SCRIPT.match(/projectPageLink\(/g) || []).length, 6,
|
|
495
|
+
'the helper and its five callers: the locked step, the domain_required snag, the domainless standup, the snagged retry, the torn-down revive');
|
|
496
|
+
for (const dead of ['Retry it \\(and see the technical details\\) from <a href="\\?view=mine">',
|
|
497
|
+
'Bring it back any time from <a href="\\?view=mine">']) {
|
|
498
|
+
assert.doesNotMatch(SCRIPT, new RegExp(dead), 'a control the list no longer has must not be pointed at there');
|
|
499
|
+
}
|
|
468
500
|
assert.match(steps, /\(raw \? '<details><summary>Technical details<\/summary><p>' \+ esc\(raw\) \+ '<\/p><\/details>' : ''\)/);
|
|
469
501
|
assert.match(SCRIPT, /e\.code = \(d\.error && d\.error\.code\) \|\| '';/, 'the route code travels with the error');
|
|
470
502
|
assert.match(SCRIPT, /live\.manifestCode = String\(\(err && err\.code\) \|\| ''\);/);
|
|
@@ -486,21 +518,24 @@ test('the rail at the done panel keeps its marker on Create until the project an
|
|
|
486
518
|
assert.match(HUB, /<li class="now" data-step="1" aria-current="step">/, 'the static rail starts with the same marker');
|
|
487
519
|
});
|
|
488
520
|
|
|
489
|
-
test('
|
|
490
|
-
|
|
521
|
+
test('every action result announces, nothing under the pointer is deleted, and focus stays where it was', () => {
|
|
522
|
+
// task 1003355 moved every sender to the project's own page, so the live
|
|
523
|
+
// regions that matter are that page's; the row keeps its own for the watch's
|
|
524
|
+
// "can't see this project any more"
|
|
525
|
+
assert.match(fn('renderCard'), /id="msg-' \+ iid \+ '" role="status" aria-live="polite">/, 'the row slot is a live region');
|
|
491
526
|
assert.match(HUB, /<div class="projMsg" id="mMsg" role="status" aria-live="polite"><\/div>/, 'so is the manage page’s');
|
|
492
|
-
assert.
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
assert.match(fn('
|
|
498
|
-
assert.match(fn('
|
|
527
|
+
assert.match(fn('renderAddress'), /id="mDomMsg" role="status" aria-live="polite"/, 'and the address section’s own');
|
|
528
|
+
for (const sender of ['mReProvision', 'mRestart']) {
|
|
529
|
+
assert.match(fn(sender), /\$\('mActs'\)\.querySelectorAll\('button'\)\.forEach\(function \(b\) \{ b\.disabled = true; \}\);/,
|
|
530
|
+
sender + ' quiets the doors while it is in flight');
|
|
531
|
+
}
|
|
532
|
+
assert.match(fn('mAttach'), /if \(btn\) btn\.disabled = true;/, 'and so does the address control');
|
|
533
|
+
assert.match(fn('askDelete'), /if \(field\) field\.focus\(\);/, 'the typed-back name takes focus when the ask opens');
|
|
499
534
|
const card = fn('renderCard');
|
|
500
535
|
assert.match(card, /var wasInside = li\.contains\(document\.activeElement\) && document\.activeElement !== li;/);
|
|
501
|
-
assert.match(card, /
|
|
502
|
-
assert.match(fn('focusKeyOf'), /
|
|
503
|
-
assert.match(fn('focusCardControl'), /li\.querySelector\('\.actions a
|
|
536
|
+
assert.match(card, /if \(wasInside\) focusCardControl\(inst\.id, prefer\);/);
|
|
537
|
+
assert.match(fn('focusKeyOf'), /el\.hasAttribute\('data-manage'\) \? '\[data-manage\]' : null/, 'Manage lands back on Manage');
|
|
538
|
+
assert.match(fn('focusCardControl'), /li\.querySelector\('\.actions a'\)/, 'nearest door as the fallback');
|
|
504
539
|
});
|
|
505
540
|
|
|
506
541
|
/* ── copy follow-ups (task 1003245) ── */
|
|
@@ -539,7 +574,14 @@ test('repo visibility: the refusal is one line, the mechanism lives in the fold'
|
|
|
539
574
|
test('a failed manage read offers a retry; the no-address sentence offers a door', () => {
|
|
540
575
|
assert.match(SCRIPT, /a\.innerHTML = 'Couldn’t read this project right now\. <button type="button" class="btn small ghost" id="mAlertRetry">Try again<\/button>';/);
|
|
541
576
|
assert.match(SCRIPT, /\$\('mAlert'\)\.addEventListener\('click', function \(ev\) \{\n\s*if \(ev\.target\.closest && ev\.target\.closest\('#mAlertRetry'\)\) loadManage\(\);/, 'delegated — the catch rewrites the alert on every failure');
|
|
542
|
-
|
|
577
|
+
// the door the sentence offers must be one the READING surface has: on the
|
|
578
|
+
// project's own page it is the section below, on a row it is that project's
|
|
579
|
+
// page — never the list, which since task 1003355 carries no field at all
|
|
580
|
+
const addr = fn('addressRow');
|
|
581
|
+
assert.match(addr, /No address yet<\/span> — sign-in on this project can’t be set up until it has one\. '/);
|
|
582
|
+
assert.match(addr, /onManage \? 'Give it an address below\.'/);
|
|
583
|
+
assert.match(addr, /'<a href="\?view=manage&id=' \+ esc\(String\(inst\.id\)\) \+ '" data-manage="' \+ esc\(String\(inst\.id\)\) \+ '">Give it an address on its own page<\/a>\.'/,
|
|
584
|
+
'and it carries data-manage, so the list’s delegated handler opens it in place');
|
|
543
585
|
assert.match(SCRIPT, /var a = ev\.target\.closest && ev\.target\.closest\('\[data-nav-to\]'\);\n\s*if \(!a \|\| !isPlainClick\(ev\)\) return;/, 'data-nav-to is delegated, so a link injected after boot switches views in place');
|
|
544
586
|
assert.doesNotMatch(SCRIPT, /querySelectorAll\('\[data-nav-to\]'\)/, 'the boot-time sweep is gone');
|
|
545
587
|
});
|
package/tests/ship_assurance.mjs
CHANGED
|
@@ -186,5 +186,57 @@ await t('the CLI carries the declaration through its deliberate-empty path', ()
|
|
|
186
186
|
'and the deliberate-empty land sites must mark themselves as such');
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
+
// task 1003434 — a recorded tip proves the land even when the claim has no branch.
|
|
190
|
+
// Strands 1003285/1003286: commits on main, no branch on the claim. Once the tip was
|
|
191
|
+
// recorded, POST /ship still refused, because getPublishStatus threw BAD_BRANCH before
|
|
192
|
+
// landProvenForTask ever saw the tip. The status probe is a witness, not a
|
|
193
|
+
// prerequisite: its failure must degrade to status=null, never discard the proof.
|
|
194
|
+
const { deriveShipAssurance } = require(path.join(ROOT, 'modules/lifecycle/ship-assurance.js'));
|
|
195
|
+
const TIP = 'a'.repeat(40);
|
|
196
|
+
console.log('\nthe tip proof does not depend on the branch/PR probe');
|
|
197
|
+
await t('tip in main + getPublishStatus throwing (no branch on the claim) → proof', async () => {
|
|
198
|
+
const seen = [];
|
|
199
|
+
const r = await deriveShipAssurance({
|
|
200
|
+
taskId: 1003285,
|
|
201
|
+
body: {},
|
|
202
|
+
db: { getClaimPublishedSha: async () => TIP },
|
|
203
|
+
githubPush: {
|
|
204
|
+
getPublishStatus: async () => { const e = new Error('BAD_BRANCH'); e.code = 'BAD_BRANCH'; throw e; },
|
|
205
|
+
landProvenForTask: async ({ publishedSha, status }) => {
|
|
206
|
+
seen.push({ publishedSha, status });
|
|
207
|
+
return publishedSha === TIP ? { proven: true, reason: 'published tip in main' } : { proven: false };
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
onProbeError: (e) => seen.push({ probeError: e.code }),
|
|
211
|
+
});
|
|
212
|
+
assert.equal(seen.some((s) => s.publishedSha === TIP && s.status === null), true, 'the proof ran with status=null');
|
|
213
|
+
assert.equal(seen.some((s) => s.probeError === 'BAD_BRANCH'), true, 'the probe failure was still reported');
|
|
214
|
+
assert.equal(r.assurance && r.assurance.kind, 'proof');
|
|
215
|
+
});
|
|
216
|
+
await t('no tip + getPublishStatus throwing → still the 409 refusal (nothing to prove)', async () => {
|
|
217
|
+
const r = await deriveShipAssurance({
|
|
218
|
+
taskId: 1003253,
|
|
219
|
+
body: {},
|
|
220
|
+
db: { getClaimPublishedSha: async () => null },
|
|
221
|
+
githubPush: {
|
|
222
|
+
getPublishStatus: async () => { throw new Error('BAD_BRANCH'); },
|
|
223
|
+
landProvenForTask: async () => ({ proven: false, reason: 'no tip, no PR' }),
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
assert.equal(r.refusal && r.refusal.code, 'ship_land_not_proven');
|
|
227
|
+
});
|
|
228
|
+
await t('a proof failure (compare API down) still falls through to the refusal, not a throw', async () => {
|
|
229
|
+
const r = await deriveShipAssurance({
|
|
230
|
+
taskId: 1,
|
|
231
|
+
body: {},
|
|
232
|
+
db: { getClaimPublishedSha: async () => TIP },
|
|
233
|
+
githubPush: {
|
|
234
|
+
getPublishStatus: async () => null,
|
|
235
|
+
landProvenForTask: async () => { throw new Error('STATUS_FETCH_FAILED'); },
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
assert.equal(r.refusal && r.refusal.code, 'ship_land_not_proven');
|
|
239
|
+
});
|
|
240
|
+
|
|
189
241
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
190
242
|
process.exit(failed ? 1 : 0);
|