@bongos/core 1.21.28 → 1.21.29
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 +28 -23
- package/docs/api/openapi.json +1 -1
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +378 -378
- package/docs/copy-registry.json +444 -444
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +233 -233
- package/modules/provisioning/routes/callback-page.js +28 -65
- package/modules/provisioning/routes/provisioning.js +6 -6
- package/modules/public-landing/public/projects.html +68 -3
- 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/provisioning_callback_page.mjs +5 -5
- package/tests/provisioning_github_app_routes.mjs +24 -25
- package/tests/wizard_auto_open.mjs +94 -0
- package/tests/wizard_intent_resume.mjs +17 -2
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
//
|
|
16
16
|
// Shape: no DB, no network, no request object — given a plain options bag it returns
|
|
17
17
|
// a string. `esc` and `callbackPage` are the whole surface; PAGE_STYLE and
|
|
18
|
-
//
|
|
18
|
+
// HANDBACK_SCRIPT stay private because they are only ever this page's own body.
|
|
19
19
|
'use strict';
|
|
20
20
|
|
|
21
21
|
const api = require('../../../src/module-api');
|
|
@@ -53,73 +53,44 @@ h1{font-family:"Baloo 2",system-ui,sans-serif;font-weight:700;font-size:1.5rem;l
|
|
|
53
53
|
.sub p{margin:.6rem 0}
|
|
54
54
|
code{font-family:"Fira Code",ui-monospace,SFMono-Regular,monospace;background:rgba(56,189,248,.14);color:var(--sky600);padding:.12em .45em;border-radius:6px;font-size:.9em;overflow-wrap:anywhere}
|
|
55
55
|
@media (prefers-color-scheme:dark){code{color:var(--sky)}}
|
|
56
|
-
.steps{display:flex;align-items:flex-start;justify-content:center;margin:1.9rem 0 1.1rem}
|
|
57
|
-
.step{flex:1 1 0;max-width:9rem;position:relative}
|
|
58
|
-
.step::before{content:"";position:absolute;top:16px;left:-50%;width:100%;height:3px;background:var(--line);z-index:0;transition:background .4s ease}
|
|
59
|
-
.step:first-child::before{display:none}
|
|
60
|
-
.dot{width:34px;height:34px;border-radius:50%;margin:0 auto;display:grid;place-items:center;background:var(--bg3);border:2px solid var(--line);color:var(--inkSoft);font-weight:700;font-size:.95rem;position:relative;z-index:1;transition:all .4s ease}
|
|
61
|
-
.lbl{margin-top:.55rem;font-size:.78rem;color:var(--inkSoft);font-weight:600;line-height:1.25;padding:0 .2rem}
|
|
62
|
-
.step.done .dot{background:var(--ok);border-color:var(--ok);color:#fff}
|
|
63
|
-
.step.done::before{background:var(--ok)}
|
|
64
|
-
.step.active .dot{border-color:var(--sun);color:var(--sun600);animation:pulse 1.5s ease-in-out infinite}
|
|
65
|
-
.step.active .lbl{color:var(--ink)}
|
|
66
|
-
.step.active .dot::after{content:"";position:absolute;inset:-3px;border-radius:50%;border:2px solid transparent;border-top-color:var(--sun);animation:spin .8s linear infinite}
|
|
67
|
-
@keyframes spin{to{transform:rotate(360deg)}}
|
|
68
|
-
@keyframes pulse{0%,100%{box-shadow:0 0 0 4px rgba(249,115,22,.15)}50%{box-shadow:0 0 0 9px rgba(249,115,22,.04)}}
|
|
69
|
-
@media (prefers-reduced-motion:reduce){.step.active .dot,.step.active .dot::after{animation:none}}
|
|
70
|
-
.status-line{min-height:1.4em;color:var(--inkSoft);font-size:.94rem;margin:.4rem auto 0;max-width:24rem;line-height:1.45}
|
|
71
56
|
.cta{margin-top:1.4rem}
|
|
72
57
|
.btn{display:inline-block;background:var(--sun);color:var(--sunInk);text-decoration:none;font-weight:700;font-family:"Baloo 2",system-ui,sans-serif;padding:.72rem 1.5rem;border-radius:999px;font-size:1.02rem;box-shadow:0 10px 22px -8px rgba(249,115,22,.7);transition:transform .15s ease,box-shadow .15s ease}
|
|
73
58
|
.btn:hover{transform:translateY(-1px);box-shadow:0 14px 28px -8px rgba(249,115,22,.8)}
|
|
74
59
|
.muted{color:var(--inkSoft);opacity:.9;font-size:.85rem;margin:1.1rem auto 0;max-width:24rem;line-height:1.45}
|
|
75
|
-
.hidden{display:none!important}
|
|
76
60
|
`;
|
|
77
61
|
|
|
78
|
-
// Client script for the
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
62
|
+
// Client script for the HAND-BACK variant (task 1004548; replaces the task 2096 progress
|
|
63
|
+
// stepper). Placing the credentials takes minutes, and the founder used to wait for it
|
|
64
|
+
// HERE, in a second tab, away from the checklist that still had steps for them. Now the
|
|
65
|
+
// page hands them straight back: as the wizard's pop-up it closes itself so the checklist
|
|
66
|
+
// tab is in front again (that tab is already polling the same status and shows the wait);
|
|
67
|
+
// if the browser refuses the close, or this was a same-tab return (pop-up blocked), this
|
|
68
|
+
// tab becomes the checklist. The one-time state rides the URL FRAGMENT — never sent to a
|
|
69
|
+
// server — so a checklist opened here can keep watching the same attempt.
|
|
70
|
+
// Deliberately contains NO backticks and NO ${...} so it survives the outer template
|
|
71
|
+
// literal verbatim; the only server data it reads is the JSON config block above it.
|
|
72
|
+
const HANDBACK_SCRIPT = `
|
|
87
73
|
(function(){
|
|
88
74
|
var cfg=window.__CB_SETUP__||{};
|
|
89
|
-
var
|
|
90
|
-
if(
|
|
91
|
-
var
|
|
92
|
-
var
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
the checklist is in the other tab, so this one may auto-open the instance. Same-tab fallback (pop-up blocked,
|
|
97
|
-
form target=_self): this IS the founder's checklist tab — never navigate it away; offer both doors (task 1003236). */
|
|
98
|
-
var popup=!!window.opener||window.name==='ghAppSetup';
|
|
99
|
-
var finished=false,tries=0,manual=false;
|
|
100
|
-
function dot(n){return steps.querySelector('[data-step="'+n+'"]');}
|
|
101
|
-
function setActive(n){for(var i=1;i<=3;i++){var el=dot(i);if(!el)continue;el.classList.remove('active','done');if(i<n){el.classList.add('done');el.querySelector('.dot').textContent='\\u2713';}else if(i===n){el.classList.add('active');}}}
|
|
102
|
-
function allDone(){for(var i=1;i<=3;i++){var el=dot(i);if(!el)continue;el.classList.remove('active');el.classList.add('done');el.querySelector('.dot').textContent='\\u2713';}}
|
|
103
|
-
function ready(){if(finished)return;finished=true;allDone();statusEl.textContent='Your instance is ready \\u2014 sign in with GitHub to claim it as owner.';cta.classList.remove('hidden');
|
|
104
|
-
if(!popup){go.textContent='Open '+domain+' \\u2192';if(back)back.classList.remove('hidden');if(foot)foot.textContent='Your setup checklist is waiting where you left it \\u2014 finish it first, or open your instance now.';return;}
|
|
105
|
-
var n=4;(function tick(){if(n<=0){location.href=target;return;}go.textContent='Opening '+domain+' in '+n+'\\u2026';n--;setTimeout(tick,1000);})();}
|
|
106
|
-
function failed(){if(finished)return;finished=true;statusEl.textContent='Setup didn\\u2019t finish. Open your instance page and start the GitHub sign-in setup again.';cta.classList.remove('hidden');go.textContent='Open '+domain+' \\u2192';}
|
|
107
|
-
function revealManual(){if(manual||finished)return;manual=true;cta.classList.remove('hidden');go.textContent='Open '+domain+' \\u2192';if(foot)foot.textContent='This is taking a little longer than usual \\u2014 you can open your instance now, or keep waiting.';}
|
|
108
|
-
function schedule(){if(!finished)setTimeout(poll,3000);}
|
|
109
|
-
function poll(){tries++;fetch(pollUrl,{headers:{accept:'application/json'},cache:'no-store'}).then(function(r){return r.ok?r.json():null;}).then(function(j){if(!j){schedule();return;}if(j.ready||j.status==='done'){ready();return;}if(j.status==='error'||j.status==='expired'){failed();return;}setActive(2);if(tries===40)revealManual();schedule();}).catch(function(){schedule();});}
|
|
110
|
-
setActive(2);poll();
|
|
75
|
+
var back=cfg.back||'/projects?view=new';
|
|
76
|
+
if(cfg.state)back+='#ghstate='+encodeURIComponent(cfg.state);
|
|
77
|
+
var link=document.getElementById('back');if(link)link.href=back;
|
|
78
|
+
var popup=false;
|
|
79
|
+
try{popup=!!window.opener&&!window.opener.closed;}catch(e){}
|
|
80
|
+
if(popup){try{window.opener.focus();}catch(e){}try{window.close();}catch(e){}}
|
|
81
|
+
setTimeout(function(){location.replace(back);},popup?700:300);
|
|
111
82
|
})();
|
|
112
83
|
`;
|
|
113
84
|
|
|
114
85
|
// The standalone landing page for the public GitHub redirect (the mothership serves it;
|
|
115
86
|
// it is NOT the hall shell). Two variants share the Cloud Bongos brand shell (task 2096):
|
|
116
|
-
// •
|
|
117
|
-
//
|
|
87
|
+
// • hand-back — ok + state + domain: a brief "GitHub app created" that returns the
|
|
88
|
+
// founder to their setup checklist, where the wait is shown (task 1004548).
|
|
118
89
|
// • message — everything else (errors + the no-domain success fallback): a calm badge
|
|
119
90
|
// + `body` (server-authored, trusted HTML).
|
|
120
91
|
function callbackPage({ ok, title, body, state, domain }) {
|
|
121
|
-
const
|
|
122
|
-
const cfgJson = JSON.stringify({ state: state || '', domain: domain || '' }).replace(/</g, '\\u003c');
|
|
92
|
+
const handback = !!(ok && state && domain);
|
|
93
|
+
const cfgJson = JSON.stringify({ state: state || '', domain: domain || '', back: '/projects?view=new' }).replace(/</g, '\\u003c');
|
|
123
94
|
const head = `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
124
95
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
125
96
|
<meta name="color-scheme" content="light dark">
|
|
@@ -127,24 +98,16 @@ function callbackPage({ ok, title, body, state, domain }) {
|
|
|
127
98
|
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
128
99
|
<link href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@600;700;800&family=Quicksand:wght@400;500;600;700&family=Fira+Code&display=swap" rel="stylesheet">
|
|
129
100
|
<style>${PAGE_STYLE}</style></head><body>`;
|
|
130
|
-
if (
|
|
101
|
+
if (handback) {
|
|
131
102
|
return `${head}
|
|
132
103
|
<main class="card">
|
|
104
|
+
<div class="badge-warn">✓</div>
|
|
133
105
|
<h1>${esc(title)}</h1>
|
|
134
|
-
<p class="sub">
|
|
135
|
-
<div class="
|
|
136
|
-
<div class="step done" data-step="1"><div class="dot">✓</div><div class="lbl">GitHub app created</div></div>
|
|
137
|
-
<div class="step active" data-step="2"><div class="dot">2</div><div class="lbl">Placing credentials</div></div>
|
|
138
|
-
<div class="step" data-step="3"><div class="dot">3</div><div class="lbl">Ready to sign in</div></div>
|
|
139
|
-
</div>
|
|
140
|
-
<p class="status-line" id="status">Hang tight — keep this tab open while we finish.</p>
|
|
141
|
-
<div class="cta hidden" id="cta"><a class="btn" id="go" href="https://${esc(domain)}">Open ${esc(domain)} →</a></div>
|
|
142
|
-
<p class="muted hidden" id="back"><a href="/projects?view=new">Finish your remaining setup steps →</a></p>
|
|
143
|
-
<p class="muted" id="foot">This page opens <code>${esc(domain)}</code> for you the moment it’s ready.</p>
|
|
144
|
-
<noscript><p class="muted">When setup finishes, open <a href="https://${esc(domain)}">${esc(domain)}</a> and sign in with GitHub — that first sign-in makes you the owner.</p></noscript>
|
|
106
|
+
<p class="sub">Taking you back to your setup checklist — it finishes setting up <code>${esc(domain)}</code> there.</p>
|
|
107
|
+
<div class="cta"><a class="btn" id="back" href="/projects?view=new">Back to my checklist →</a></div>
|
|
145
108
|
</main>
|
|
146
109
|
<script>window.__CB_SETUP__=${cfgJson};</script>
|
|
147
|
-
<script>${
|
|
110
|
+
<script>${HANDBACK_SCRIPT}</script>
|
|
148
111
|
</body></html>`;
|
|
149
112
|
}
|
|
150
113
|
return `${head}
|
|
@@ -1391,12 +1391,12 @@ module.exports = function provisioningRoutes() {
|
|
|
1391
1391
|
});
|
|
1392
1392
|
|
|
1393
1393
|
// GET /provisioning/github-app/status?state=<> — the PUBLIC progress read the onboarding
|
|
1394
|
-
//
|
|
1394
|
+
// setup checklist polls (task 1004548) to know when setup is done + where to send the owner.
|
|
1395
1395
|
// Same public-by-state model as the callback: the unguessable, single-use `state` token
|
|
1396
1396
|
// is the only key, and the response reveals only the manifest lifecycle status + the
|
|
1397
1397
|
// instance's own (already-public) domain — never a secret. Per-IP rate-limited because
|
|
1398
1398
|
// the page polls it every ~3s. `ready` is true iff the manifest reached 'done' (creds
|
|
1399
|
-
// placed + /healthz verified) — the
|
|
1399
|
+
// placed + /healthz verified) — the setup checklist then opens https://<domain>.
|
|
1400
1400
|
// rank: public — no session, no mutation, no secret; an unknown state is a 404 no-op.
|
|
1401
1401
|
router.get('/provisioning/github-app/status', statusRateLimit, async (req, res) => {
|
|
1402
1402
|
try {
|
|
@@ -1461,11 +1461,11 @@ module.exports = function provisioningRoutes() {
|
|
|
1461
1461
|
}).catch(() => {});
|
|
1462
1462
|
const inst = await provisioning.getInstanceById(pool, row.instance_id).catch(() => null);
|
|
1463
1463
|
const domain = inst && inst.domain ? inst.domain : null;
|
|
1464
|
-
//
|
|
1465
|
-
//
|
|
1466
|
-
//
|
|
1464
|
+
// Hand-back variant (task 1004548): the page returns the founder to their setup
|
|
1465
|
+
// checklist, which shows the wait for the credentials and opens the instance when
|
|
1466
|
+
// it's ready. Falls back to a static success if we have no domain to point at.
|
|
1467
1467
|
return sendPage(200, domain
|
|
1468
|
-
? callbackPage({ ok: true, title: '
|
|
1468
|
+
? callbackPage({ ok: true, title: 'GitHub app created', state, domain })
|
|
1469
1469
|
: callbackPage({ ok: true, title: 'GitHub app created',
|
|
1470
1470
|
body: '<p>Nice — GitHub created the app. We’re placing its credentials on your instance now; this finishes on its own within a few minutes.</p><p class="muted">You can close this tab.</p>' }));
|
|
1471
1471
|
} catch (err) {
|
|
@@ -423,6 +423,10 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
423
423
|
.samePath{font-size:13px;font-weight:400;color:var(--ink-faint);margin-top:18px;}
|
|
424
424
|
.samePath a{display:inline-block;padding:3px 0;min-height:24px;line-height:18px;}
|
|
425
425
|
#liveProgress{margin-top:4px;}
|
|
426
|
+
/* the sign-in wait inside its step + the "open it when ready" tick (task 1004548) */
|
|
427
|
+
.humanSteps .prog{margin-top:14px;}
|
|
428
|
+
.autoOpen{display:flex;align-items:flex-start;gap:10px;margin-top:14px;font-size:14px;line-height:1.45;color:var(--ink-soft);cursor:pointer;min-height:24px;}
|
|
429
|
+
.autoOpen input{width:18px;height:18px;margin:1px 0 0;flex:0 0 auto;accent-color:var(--accent);cursor:pointer;}
|
|
426
430
|
|
|
427
431
|
/* ── the standup strip (task 2083), drawn as the lifecycle rail (DESIGN.md):
|
|
428
432
|
three hairlines, a 7px dot, one lit step — no plate, no connector, no
|
|
@@ -6557,6 +6561,35 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6557
6561
|
'<p>The platform team places the generated credentials; this step ticks itself once they land.</p></details>';
|
|
6558
6562
|
}
|
|
6559
6563
|
|
|
6564
|
+
/* "Open it when it's ready" (task 1004548). The founder now waits for sign-in on
|
|
6565
|
+
this checklist instead of on the callback page, so the auto-open lives here: a
|
|
6566
|
+
tick, on unless they untick it (remembered per browser), that opens the project
|
|
6567
|
+
the moment it answers AND sign-in is wired. Armed only by a wait this page has
|
|
6568
|
+
actually watched, so revisiting a long-finished checklist never jumps away. */
|
|
6569
|
+
var AUTO_OPEN_KEY = 'cb-open-when-ready';
|
|
6570
|
+
var autoOpenArmed = false;
|
|
6571
|
+
var autoOpened = false;
|
|
6572
|
+
function autoOpenPref() {
|
|
6573
|
+
try { return localStorage.getItem(AUTO_OPEN_KEY) !== '0'; } catch (e) { return true; }
|
|
6574
|
+
}
|
|
6575
|
+
function autoOpenTick() {
|
|
6576
|
+
return '<label class="autoOpen"><input type="checkbox" data-action="auto-open"' + (autoOpenPref() ? ' checked' : '') + '>' +
|
|
6577
|
+
'<span>Open my project automatically when it’s ready</span></label>';
|
|
6578
|
+
}
|
|
6579
|
+
function maybeAutoOpen() {
|
|
6580
|
+
var domain = resolvedDomain();
|
|
6581
|
+
if (autoOpened || !autoOpenArmed || !domain || !autoOpenPref()) return;
|
|
6582
|
+
if (live.manifest !== 'done' || live.instStatus !== 'active' || !live.addrReady) return;
|
|
6583
|
+
autoOpened = true;
|
|
6584
|
+
var url = 'https://' + domain;
|
|
6585
|
+
/* a new tab keeps this checklist (two steps follow sign-in); a browser that
|
|
6586
|
+
blocks an unprompted tab gets this one sent instead — the founder asked */
|
|
6587
|
+
var w = null;
|
|
6588
|
+
try { w = window.open(url, '_blank'); } catch (e) {}
|
|
6589
|
+
if (w) { try { w.opener = null; } catch (e) {} }
|
|
6590
|
+
else window.location.assign(url);
|
|
6591
|
+
}
|
|
6592
|
+
|
|
6560
6593
|
/* oauth-app — the ADR 0133 one-click GitHub-App manifest flow is the primary path
|
|
6561
6594
|
(gap 3 / idea 1000676); locked until a domain exists (the App needs the real
|
|
6562
6595
|
callback host — the server refuses domainless with domain_required). */
|
|
@@ -6572,7 +6605,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6572
6605
|
return '<li class="slocked" data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>This platform is running on a local address, and GitHub refuses app setups it can’t reach over the public internet — so the one-click flow can’t complete here. On a publicly-addressed platform this is one click.</p>' + manualOauthDetails() + '</li>';
|
|
6573
6606
|
}
|
|
6574
6607
|
if (live.manifest === 'done') {
|
|
6575
|
-
|
|
6608
|
+
/* the wait ends HERE now, not on the callback page (task 1004548): once the
|
|
6609
|
+
project answers too, the step carries the door itself */
|
|
6610
|
+
var open = live.instStatus === 'active' && live.addrReady;
|
|
6611
|
+
return '<li class="sdone" data-onboard-step="oauth-app"><b>GitHub sign-in is wired</b><p>Credentials placed and verified — sign-in on <code style="font-family:var(--font-mono)">' + esc(domain) + '</code> is live.' +
|
|
6612
|
+
(open ? '' : ' The door below opens once your project is answering.') + '</p>' +
|
|
6613
|
+
(open ? '<a class="ghbtn" href="https://' + esc(domain) + '" target="_blank" rel="noopener">Open ' + esc(domain) + ' →</a>' : '') + '</li>';
|
|
6576
6614
|
}
|
|
6577
6615
|
if (live.manifest === 'error' || live.manifest === 'expired') {
|
|
6578
6616
|
/* a plain sentence first; the raw route text folds under Technical
|
|
@@ -6589,10 +6627,14 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6589
6627
|
if (live.manifest === 'code_received') {
|
|
6590
6628
|
/* GitHub's half is done — say so, rather than keep telling the founder
|
|
6591
6629
|
to approve an app they just approved */
|
|
6592
|
-
|
|
6630
|
+
autoOpenArmed = true;
|
|
6631
|
+
return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>GitHub’s part is done. The platform is placing the credentials on your project now — this usually takes a few minutes, and this step ticks itself.</p>' +
|
|
6632
|
+
progressMarkup(1, 1, 'Placing credentials and checking sign-in works…', '', 'signin') + autoOpenTick() + '</li>';
|
|
6593
6633
|
}
|
|
6594
6634
|
if (live.manifest) { /* pending — GitHub holds the manifest, nobody has approved it yet */
|
|
6635
|
+
autoOpenArmed = true;
|
|
6595
6636
|
return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>Waiting on GitHub — approve the app in the tab that opened. Once approved, the platform places the credentials and verifies sign-in on its own; this step ticks itself.</p>' +
|
|
6637
|
+
progressMarkup(0, 0, 'Waiting for your approval on GitHub…', '', 'signin') + autoOpenTick() +
|
|
6596
6638
|
(manifestStale ? '<button type="button" class="linklike" data-action="manifest-start">Didn’t get to GitHub? Start the setup again</button>' : '') + '</li>';
|
|
6597
6639
|
}
|
|
6598
6640
|
return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in — one click</b><p>Approve a pre-filled GitHub App and the platform wires sign-in by itself: no keys to copy, and the secret never leaves the server side.</p>' +
|
|
@@ -6678,6 +6720,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6678
6720
|
if (html === lastStepsHtml) return; /* change-only re-render — polls never clobber open UI */
|
|
6679
6721
|
lastStepsHtml = html;
|
|
6680
6722
|
$('humanSteps').innerHTML = html;
|
|
6723
|
+
maybeAutoOpen();
|
|
6681
6724
|
});
|
|
6682
6725
|
}
|
|
6683
6726
|
|
|
@@ -6730,6 +6773,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6730
6773
|
});
|
|
6731
6774
|
});
|
|
6732
6775
|
|
|
6776
|
+
$('humanSteps').addEventListener('change', function (ev) {
|
|
6777
|
+
var box = ev.target.closest && ev.target.closest('[data-action="auto-open"]');
|
|
6778
|
+
if (!box) return;
|
|
6779
|
+
try { localStorage.setItem(AUTO_OPEN_KEY, box.checked ? '1' : '0'); } catch (e) {}
|
|
6780
|
+
});
|
|
6781
|
+
|
|
6733
6782
|
/* ── C8: the wizard's polls run only on stage, exactly like the sky.
|
|
6734
6783
|
Off-view or hidden-tab, a due tick parks itself instead of fetching;
|
|
6735
6784
|
enter() and the visibility flip resume whatever was parked. ── */
|
|
@@ -6754,6 +6803,13 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6754
6803
|
var manifestPending = false;
|
|
6755
6804
|
var manifestStartedAt = 0; /* when the founder was sent to GitHub */
|
|
6756
6805
|
var manifestStale = false; /* ~90s with no approval → offer to start again */
|
|
6806
|
+
/* the GitHub hand-back (task 1004548): the callback page returns the founder here
|
|
6807
|
+
with the attempt's one-time state in the fragment (#ghstate=…), so a checklist
|
|
6808
|
+
that page opened — not only the tab that started the setup — keeps watching the
|
|
6809
|
+
same attempt. Read once at load, before the router's replaceState drops it. */
|
|
6810
|
+
var handbackState = (function () {
|
|
6811
|
+
try { var m = /[#&]ghstate=([0-9a-f]{16,128})/i.exec(location.hash || ''); return m ? m[1] : null; } catch (e) { return null; }
|
|
6812
|
+
})();
|
|
6757
6813
|
/* the manifest leg survives a reload: token + last status ride the CREATED
|
|
6758
6814
|
record, so enter() re-seeds the step instead of re-offering "Set up
|
|
6759
6815
|
GitHub sign-in" for an app the founder already approved (task 1003232) */
|
|
@@ -6846,7 +6902,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6846
6902
|
standup: { names: ['Request filed', 'Standing it up', 'Live'], cls: '' },
|
|
6847
6903
|
revive: { names: ['Revive queued', 'Standing it up', 'Back live'], cls: '' },
|
|
6848
6904
|
teardown: { names: ['Teardown queued', 'Winding down', 'Torn down'], cls: 'flowDown' },
|
|
6849
|
-
settings: { names: ['Change saved', 'Restarting to apply it', 'Applied'], cls: '' }
|
|
6905
|
+
settings: { names: ['Change saved', 'Restarting to apply it', 'Applied'], cls: '' },
|
|
6906
|
+
/* the GitHub sign-in wait, drawn inside its checklist step (task 1004548) */
|
|
6907
|
+
signin: { names: ['Approve on GitHub', 'Placing credentials', 'Ready to sign in'], cls: '' }
|
|
6850
6908
|
};
|
|
6851
6909
|
/* No live-region attributes here: this strip is destroyed and rebuilt on
|
|
6852
6910
|
every 5s tick, and a live region that is itself replaced never announces.
|
|
@@ -7171,6 +7229,13 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7171
7229
|
/* restore the opt-out too — without it resolvedDomain() re-derives the
|
|
7172
7230
|
assigned address and the done panel promises one that doesn't exist */
|
|
7173
7231
|
state.noAddress = !!created.noaddr;
|
|
7232
|
+
/* a hand-back from the callback page names the attempt GitHub just approved */
|
|
7233
|
+
if (handbackState) {
|
|
7234
|
+
created.manifest_state = handbackState;
|
|
7235
|
+
if (created.manifest !== 'done') created.manifest = 'code_received';
|
|
7236
|
+
rememberManifest(handbackState, created.manifest);
|
|
7237
|
+
handbackState = null;
|
|
7238
|
+
}
|
|
7174
7239
|
/* the manifest leg's last known truth, kept across a reload */
|
|
7175
7240
|
if (created.manifest) { live.manifest = created.manifest; live.appSlug = created.app_slug || null; }
|
|
7176
7241
|
if (created.manifest_state && created.manifest !== 'done' && created.manifest !== 'error' && created.manifest !== 'expired') {
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.29",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.29",
|
|
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.21.
|
|
3
|
+
"version": "1.21.29",
|
|
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
|
@@ -8924,5 +8924,11 @@
|
|
|
8924
8924
|
"id": "1004492",
|
|
8925
8925
|
"text": "The Builders Hall now asks what you are doing when you arrive: Building, Artistry, Ideating or Governing, or Legacy mode for the hall exactly as it was. It asks once per session (or never again, if you untick the box), artists"
|
|
8926
8926
|
}
|
|
8927
|
+
],
|
|
8928
|
+
"1.21.29": [
|
|
8929
|
+
{
|
|
8930
|
+
"id": "1004548",
|
|
8931
|
+
"text": "After approving GitHub sign-in, founders go straight back to their setup checklist to wait, and can tick a box to have their project open automatically when it's ready."
|
|
8932
|
+
}
|
|
8927
8933
|
]
|
|
8928
8934
|
}
|
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.21.
|
|
78
|
+
const CORE_VERSION = '1.21.29'; // 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');
|
|
@@ -38,7 +38,7 @@ test('esc renders an absent value as the empty string, never "null"/"undefined"'
|
|
|
38
38
|
assert.equal(esc(false), 'false');
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
test('the
|
|
41
|
+
test('the hand-back variant escapes the title + domain it interpolates', () => {
|
|
42
42
|
const html = callbackPage({
|
|
43
43
|
ok: true,
|
|
44
44
|
title: 'Setting up <your> instance',
|
|
@@ -49,9 +49,9 @@ test('the progress variant escapes the title + domain it interpolates', () => {
|
|
|
49
49
|
assert.match(html, /evil"\.example\.com/, 'the domain rides through esc');
|
|
50
50
|
assert.doesNotMatch(html, /Setting up <your> instance/, 'no raw title anywhere on the page');
|
|
51
51
|
assert.doesNotMatch(html, /https:\/\/evil"\.example\.com/, 'no unescaped domain in an href');
|
|
52
|
-
// it IS the
|
|
53
|
-
assert.match(html, /id="
|
|
54
|
-
assert.match(html, /__CB_SETUP__=\{"state":"GOODSTATE","domain":"evil\\"\.example\.com"\}/);
|
|
52
|
+
// it IS the hand-back variant (task 1004548): the way back + the config block the client script reads
|
|
53
|
+
assert.match(html, /id="back" href="\/projects\?view=new"/);
|
|
54
|
+
assert.match(html, /__CB_SETUP__=\{"state":"GOODSTATE","domain":"evil\\"\.example\.com","back":"\/projects\?view=new"\}/);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
test('the message variant escapes the title but passes server-authored body HTML through', () => {
|
|
@@ -63,7 +63,7 @@ test('the message variant escapes the title but passes server-authored body HTML
|
|
|
63
63
|
assert.match(html, /<h1>This setup link has <expired><\/h1>/);
|
|
64
64
|
assert.match(html, /<div class="sub"><p>Ask the owner for a fresh link\.<\/p><\/div>/,
|
|
65
65
|
'body is trusted server-authored HTML — the route composes it, a request never does');
|
|
66
|
-
assert.doesNotMatch(html, /id="
|
|
66
|
+
assert.doesNotMatch(html, /id="back"/, 'no hand-back without ok + state + domain');
|
|
67
67
|
assert.match(html, /badge-warn">⚠️</, 'a failure wears the warning badge');
|
|
68
68
|
});
|
|
69
69
|
|
|
@@ -93,44 +93,43 @@ console.log('\nGET /provisioning/github-app/callback (the public GitHub redirect
|
|
|
93
93
|
{
|
|
94
94
|
const res = await fetch(`${base}/provisioning/github-app/callback?code=GHCODE123&state=GOODSTATE`);
|
|
95
95
|
const html = await res.text();
|
|
96
|
-
t('good state → 200 text/html
|
|
96
|
+
t('good state → 200 text/html hand-back page', res.status === 200 && /text\/html/.test(res.headers.get('content-type') || '') && /GitHub app created/.test(html) && /__CB_SETUP__/.test(html) && html.includes('GOODSTATE'), `status=${res.status}`);
|
|
97
97
|
t('the page names the instance host', /mercury\.example\.com/.test(html));
|
|
98
|
-
// task
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
t('
|
|
102
|
-
t('the auto-open countdown is reachable only past the pop-up guard', /var popup=!!window\.opener\|\|window\.name==='ghAppSetup';/.test(html) && /if\(!popup\)\{[^}]*return;\}\s*var n=4;\(function tick\(\)\{if\(n<=0\)\{location\.href=target;/.test(html) && (html.match(/location\.href=target/g) || []).length === 1);
|
|
98
|
+
// task 1004548: the founder no longer waits here — the page hands them back to the
|
|
99
|
+
// checklist, which shows the wait. A manual way back is always on the page.
|
|
100
|
+
t('carries a plain "back to my checklist" link (works without script)', /<a class="btn" id="back" href="\/projects\?view=new">Back to my checklist/.test(html));
|
|
101
|
+
t('no progress stepper or countdown left on the page', !/id="steps"/.test(html) && !/Placing credentials/.test(html) && !/location\.href=target/.test(html));
|
|
103
102
|
|
|
104
103
|
// Run the served script itself (not a copy) against a minimal DOM, once as the
|
|
105
|
-
// wizard's pop-up and once as a same-tab return, and watch what it does
|
|
104
|
+
// wizard's pop-up and once as a same-tab return, and watch what it does.
|
|
106
105
|
const script = (html.match(/<script>\s*(\(function\(\)\{[\s\S]*?)<\/script>\s*<\/body>/) || [])[1];
|
|
107
|
-
t('the
|
|
108
|
-
async function runCallback({ opener
|
|
106
|
+
t('the hand-back page ships the client script', typeof script === 'string' && script.length > 100);
|
|
107
|
+
async function runCallback({ opener }) {
|
|
109
108
|
const vm = await import('node:vm');
|
|
110
|
-
const
|
|
111
|
-
const els = { steps: el(), status: el(), cta: el(), go: el(), foot: el(), back: el() };
|
|
112
|
-
els.steps.querySelector = () => el();
|
|
113
|
-
els.back.classList.add('hidden'); els.cta.classList.add('hidden');
|
|
109
|
+
const back = { href: '/projects?view=new' };
|
|
114
110
|
const timers = [];
|
|
111
|
+
const calls = { close: 0, focusOpener: 0, replaced: null };
|
|
115
112
|
const sandbox = {
|
|
116
|
-
__CB_SETUP__: { state: 'GOODSTATE', domain: 'mercury.example.com' },
|
|
117
|
-
opener,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
__CB_SETUP__: { state: 'GOODSTATE', domain: 'mercury.example.com', back: '/projects?view=new' },
|
|
114
|
+
opener: opener ? { closed: false, focus: () => { calls.focusOpener++; } } : null,
|
|
115
|
+
close: () => { calls.close++; },
|
|
116
|
+
document: { getElementById: (id) => (id === 'back' ? back : null) },
|
|
117
|
+
location: { replace: (u) => { calls.replaced = u; } },
|
|
121
118
|
setTimeout: (fn) => { timers.push(fn); return timers.length; },
|
|
122
119
|
encodeURIComponent,
|
|
123
120
|
};
|
|
124
121
|
sandbox.window = sandbox;
|
|
125
122
|
vm.runInNewContext(script, sandbox);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return {
|
|
123
|
+
const replacedBeforeTimer = calls.replaced;
|
|
124
|
+
while (timers.length) timers.shift()();
|
|
125
|
+
return { ...calls, replacedBeforeTimer, backHref: back.href };
|
|
129
126
|
}
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
t('
|
|
127
|
+
const want = '/projects?view=new#ghstate=GOODSTATE';
|
|
128
|
+
const popup = await runCallback({ opener: true });
|
|
129
|
+
t('as the wizard’s pop-up it focuses the checklist tab and closes itself', popup.focusOpener === 1 && popup.close === 1 && popup.replacedBeforeTimer === null, JSON.stringify(popup));
|
|
130
|
+
t('…and if the browser refuses the close, the pop-up becomes the checklist', popup.replaced === want, JSON.stringify(popup));
|
|
131
|
+
const sameTab = await runCallback({ opener: false });
|
|
132
|
+
t('as a same-tab return it goes straight back to the checklist, carrying the attempt in the fragment', sameTab.close === 0 && sameTab.replaced === want && sameTab.backHref === want, JSON.stringify(sameTab));
|
|
134
133
|
}
|
|
135
134
|
{
|
|
136
135
|
const res = await fetch(`${base}/provisioning/github-app/callback?code=X&state=NOPE`);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// tests/wizard_auto_open.mjs — the checklist's "Open my project automatically when it's
|
|
2
|
+
// ready" tick (task 1004548).
|
|
3
|
+
//
|
|
4
|
+
// WHY. The GitHub-App callback page used to hold the founder on its own stepper and open
|
|
5
|
+
// the instance itself. Since task 1004548 it hands them straight back to the checklist,
|
|
6
|
+
// so the auto-open moved there. The rule it must keep: open ONCE, only when sign-in is
|
|
7
|
+
// wired AND the project answers, only after a wait this page actually watched (so a
|
|
8
|
+
// revisit to a long-finished checklist never jumps away), never when the founder
|
|
9
|
+
// unticked it — and prefer a new tab, falling back to this one when the browser blocks
|
|
10
|
+
// an unprompted tab.
|
|
11
|
+
//
|
|
12
|
+
// Executed, not read: the real AUTO_OPEN_KEY…maybeAutoOpen region is sliced out of the
|
|
13
|
+
// page's inline script and run in a node:vm under stubs (the vm-slice idiom of
|
|
14
|
+
// tests/wizard_intent_resume.mjs). The slice boundaries are asserted, so a rename fails
|
|
15
|
+
// loudly instead of testing nothing.
|
|
16
|
+
//
|
|
17
|
+
// Run: node --test tests/wizard_auto_open.mjs
|
|
18
|
+
|
|
19
|
+
import assert from 'node:assert/strict';
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import vm from 'node:vm';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
|
+
const LANDING = path.join(ROOT, 'modules', 'public-landing', 'public');
|
|
28
|
+
const HUB = fs.readFileSync(path.join(LANDING, 'projects.html'), 'utf8').replace(/\r\n/g, '\n');
|
|
29
|
+
const SCRIPT = HUB.slice(HUB.lastIndexOf('<script>'), HUB.lastIndexOf('</script>'));
|
|
30
|
+
|
|
31
|
+
const START = SCRIPT.indexOf(" var AUTO_OPEN_KEY = 'cb-open-when-ready';");
|
|
32
|
+
const END = SCRIPT.indexOf(' /* oauth-app — the ADR 0133 one-click GitHub-App manifest flow', START);
|
|
33
|
+
const SLICE = SCRIPT.slice(START, END);
|
|
34
|
+
|
|
35
|
+
test('the auto-open region is still findable (a rename must fail loudly)', () => {
|
|
36
|
+
assert.ok(START > 0 && END > START, 'both slice boundaries found');
|
|
37
|
+
assert.match(SLICE, /function maybeAutoOpen\(\)/);
|
|
38
|
+
assert.match(SLICE, /function autoOpenTick\(\)/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const READY = { manifest: 'done', instStatus: 'active', addrReady: true };
|
|
42
|
+
|
|
43
|
+
function run({ live = READY, armed = true, pref = null, popupBlocked = false, ticks = 1 } = {}) {
|
|
44
|
+
const store = new Map();
|
|
45
|
+
if (pref !== null) store.set('cb-open-when-ready', pref);
|
|
46
|
+
const calls = { opened: [], assigned: [] };
|
|
47
|
+
const sandbox = {
|
|
48
|
+
live: { ...live },
|
|
49
|
+
resolvedDomain: () => 'mercury.example.com',
|
|
50
|
+
localStorage: { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, v) },
|
|
51
|
+
window: {
|
|
52
|
+
open: (u, t) => { calls.opened.push([u, t]); return popupBlocked ? null : { opener: 'checklist' }; },
|
|
53
|
+
location: { assign: (u) => { calls.assigned.push(u); } },
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
sandbox.globalThis = sandbox;
|
|
57
|
+
vm.createContext(sandbox);
|
|
58
|
+
vm.runInContext(`${SLICE}\nautoOpenArmed = ${armed ? 'true' : 'false'};\nfor (var i = 0; i < ${ticks}; i++) maybeAutoOpen();\nvar tickHtml = autoOpenTick();`, sandbox);
|
|
59
|
+
return { calls, sandbox };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
test('ready + armed + ticked (the default) → opens the project ONCE, in a new tab', () => {
|
|
63
|
+
const { calls } = run({ ticks: 3 });
|
|
64
|
+
assert.deepEqual(calls.opened, [['https://mercury.example.com', '_blank']], 'one new tab, however many renders follow');
|
|
65
|
+
assert.deepEqual(calls.assigned, [], 'this checklist stays put');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('a browser that blocks the unprompted tab sends THIS tab instead — the founder asked', () => {
|
|
69
|
+
const { calls } = run({ popupBlocked: true });
|
|
70
|
+
assert.deepEqual(calls.assigned, ['https://mercury.example.com']);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('nothing opens until sign-in is wired AND the project answers', () => {
|
|
74
|
+
for (const live of [
|
|
75
|
+
{ ...READY, manifest: 'code_received' },
|
|
76
|
+
{ ...READY, instStatus: 'provisioning' },
|
|
77
|
+
{ ...READY, addrReady: false },
|
|
78
|
+
]) {
|
|
79
|
+
const { calls } = run({ live });
|
|
80
|
+
assert.deepEqual([calls.opened, calls.assigned], [[], []], JSON.stringify(live));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('unticked → never opens, and the tick renders unticked', () => {
|
|
85
|
+
const { calls, sandbox } = run({ pref: '0' });
|
|
86
|
+
assert.deepEqual([calls.opened, calls.assigned], [[], []]);
|
|
87
|
+
assert.doesNotMatch(sandbox.tickHtml, / checked/);
|
|
88
|
+
assert.match(run().sandbox.tickHtml, / checked>/, 'the default is ticked');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('a checklist that never watched the wait (a revisit) does not jump away', () => {
|
|
92
|
+
const { calls } = run({ armed: false });
|
|
93
|
+
assert.deepEqual([calls.opened, calls.assigned], [[], []]);
|
|
94
|
+
});
|
|
@@ -70,10 +70,10 @@ test('both slices are still findable (a rename must fail loudly, not silently pa
|
|
|
70
70
|
// A faithful miniature: the stubs record what enterCreated ASKS FOR, which is the whole
|
|
71
71
|
// contract — which panel is shown, whether the answers were reset, and what the offer
|
|
72
72
|
// above step 1 was told to name.
|
|
73
|
-
function runEnter({ focus, resumeIntent = false, draft = null, record }) {
|
|
73
|
+
function runEnter({ focus, resumeIntent = false, draft = null, record, handbackState = null }) {
|
|
74
74
|
const store = new Map();
|
|
75
75
|
if (draft) store.set('draft', JSON.stringify(draft));
|
|
76
|
-
const calls = { show: [], resets: 0, offer: [], done: 0, progress: [], polls: 0, focused: 0 };
|
|
76
|
+
const calls = { show: [], resets: 0, offer: [], done: 0, progress: [], polls: 0, focused: 0, remembered: [] };
|
|
77
77
|
// state.step is the DONE step for the whole life of a finished standup — the value
|
|
78
78
|
// a draft-blind reading would have trusted
|
|
79
79
|
const state = { step: draft ? draft.step : 6, name: 'Mercury', slug: 'mercury', domain: '', mode: 'greenfield', noAddress: false, modules: null };
|
|
@@ -83,6 +83,8 @@ function runEnter({ focus, resumeIntent = false, draft = null, record }) {
|
|
|
83
83
|
live: { manifest: null, appSlug: null },
|
|
84
84
|
manifestState: null,
|
|
85
85
|
manifestPending: false,
|
|
86
|
+
handbackState, // the #ghstate=… a GitHub hand-back carries (task 1004548); null on a plain arrival
|
|
87
|
+
rememberManifest(st, status) { calls.remembered.push([st, status]); },
|
|
86
88
|
sessionStorage: { getItem: (k) => (store.has(k) ? store.get(k) : null) },
|
|
87
89
|
resetForNewProject() { calls.resets += 1; state.step = 1; state.name = ''; state.slug = ''; },
|
|
88
90
|
paintResumeOffer(c) { calls.offer.push(c ? (c.name || c.slug) : null); },
|
|
@@ -111,6 +113,19 @@ test('a reload / Back / identity settle (focus false) still resumes the done pan
|
|
|
111
113
|
assert.deepEqual(calls.offer, [null], 'the offer is down — the done panel is on stage, not step 1');
|
|
112
114
|
});
|
|
113
115
|
|
|
116
|
+
test('a hand-back from the GitHub callback page resumes watching THAT attempt (task 1004548)', () => {
|
|
117
|
+
const { calls, sandbox } = runEnter({ focus: false, record: { ...REC }, handbackState: 'ab'.repeat(32) });
|
|
118
|
+
assert.deepEqual(calls.show, [[6, false]], 'the founder lands on their checklist');
|
|
119
|
+
assert.equal(sandbox.manifestState, 'ab'.repeat(32), 'the sign-in step polls the attempt GitHub just approved');
|
|
120
|
+
assert.equal(sandbox.manifestPending, true, 'and the poll is queued for resumePolls()');
|
|
121
|
+
assert.equal(sandbox.live.manifest, 'code_received', 'it shows the wait, not "Set up GitHub sign-in" again');
|
|
122
|
+
assert.deepEqual(calls.remembered, [['ab'.repeat(32), 'code_received']], 'the attempt is kept across a reload');
|
|
123
|
+
assert.equal(sandbox.handbackState, null, 'read once — a later arrival is not a hand-back');
|
|
124
|
+
const done = runEnter({ focus: false, record: { ...REC, manifest: 'done' }, handbackState: 'cd'.repeat(32) });
|
|
125
|
+
assert.equal(done.sandbox.live.manifest, 'done', 'a finished setup is never walked back to waiting');
|
|
126
|
+
assert.equal(done.sandbox.manifestPending, false, 'and nothing is polled for it');
|
|
127
|
+
});
|
|
128
|
+
|
|
114
129
|
test('a deliberate "New project" click after a finished standup opens a FRESH step 1', () => {
|
|
115
130
|
const { calls, state } = runEnter({ focus: true, record: REC });
|
|
116
131
|
assert.equal(calls.done, 0, 'the previous project\'s done panel is not painted');
|