@bongos/core 1.19.142 → 1.19.144

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.
@@ -251,6 +251,124 @@ test('the create-repo response keeps the html_url it already returned', async ()
251
251
  assert.match(fn, /saveDraft\(\)/, 'the draft is persisted after the URL lands on state');
252
252
  });
253
253
 
254
+ /* ── The address step's three choices (task 1002898 N2 / N8 / N10) ───────────
255
+ One root: the create path asserted things it had not verified. The step offered
256
+ an address it had not checked was free, hid the no-address opt-out under a fold
257
+ labelled the OPPOSITE choice, and re-sent a client-derived default the server
258
+ already owns. ── */
259
+
260
+ test('the no-address opt-out is visible at the step, not folded under "a domain I own" (N8)', async () => {
261
+ const { body } = await request('/projects', 'game.test.local');
262
+ const panel = (body.match(/<div class="addrConfirm" id="addrConfirm">([\s\S]*?)\n {14}<\/div>/) || [, ''])[1];
263
+ assert.ok(panel, 'the address confirmation box must be extractable');
264
+ const fold = (panel.match(/<details id="addrCustom">[\s\S]*?<\/details>/) || [''])[0];
265
+ assert.ok(fold, 'the bring-your-own-domain disclosure still exists');
266
+ assert.match(fold, /id="f-domain"/, 'the domain field is what the fold is FOR');
267
+ assert.doesNotMatch(fold, /f-noaddr/,
268
+ 'the opt-out for NO address may not live under a fold labelled "use a domain I own"');
269
+ assert.match(panel, /<\/details>[\s\S]*id="f-noaddr"/,
270
+ 'the opt-out sits after the disclosure, in the open');
271
+ // all three choices legible at once: assigned address, own domain, no address
272
+ assert.match(panel, /id="addrAssigned"/);
273
+ assert.match(fold, /Use a domain I own instead/);
274
+ assert.match(panel, /Start without an address/);
275
+ });
276
+
277
+ test('the ownership promise is gated on the availability answer, never on format (N10)', async () => {
278
+ const { body } = await request('/projects', 'game.test.local');
279
+ const fn = (body.match(/function renderAddrPanel\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
280
+ assert.ok(fn, 'renderAddrPanel() must be extractable');
281
+ // the promise now rides a branch that requires the server's verdict
282
+ assert.match(fn, /avail === 'free'[\s\S]{0,200}yours from the moment it stands up/,
283
+ "'yours from the moment it stands up' must only render on a free verdict");
284
+ // and the honest alternatives exist rather than a single optimistic string
285
+ assert.match(fn, /already taken/, 'a taken handle is said, not confirmed');
286
+ assert.match(fn, /you already have that one/i, "the caller's own handle reads as theirs, not as a stranger's");
287
+ assert.match(fn, /reserved by the platform/, 'a platform-reserved address is said');
288
+ assert.match(fn, /checking that it’s free/, 'the in-flight state has its own calm rung');
289
+ // the degrade: an unanswered check names the address WITHOUT promising it
290
+ assert.match(fn, /the address it would take/, 'no answer degrades to naming, not promising');
291
+ assert.match(fn, /couldn’t check whether that handle is free/, 'and says so honestly');
292
+ });
293
+
294
+ test('the availability check is debounced, generation-guarded, and fails to "unknown" (N10)', async () => {
295
+ const { body } = await request('/projects', 'game.test.local');
296
+ const fn = (body.match(/function checkSlugAvailable\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
297
+ assert.ok(fn, 'checkSlugAvailable() must be extractable');
298
+ assert.match(fn, /\/provisioning\/slug-available\?slug=/, 'it asks the real route');
299
+ assert.match(fn, /encodeURIComponent\(slug\)/, 'the handle is encoded into the query');
300
+ assert.match(fn, /clearTimeout\(slugCheckT\)[\s\S]*setTimeout\(/, 'debounced on a timer');
301
+ assert.match(fn, /myGen !== slugCheckGen/, 'a stale in-flight answer cannot overwrite a newer one');
302
+ assert.match(fn, /catch[\s\S]*state: 'unknown'/, 'a failed check is never read as free');
303
+ // a verdict only counts for the handle it was asked about
304
+ const guard = (body.match(/function slugAvailState\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
305
+ assert.match(guard, /slugAvail\.slug === state\.slug/, 'a verdict for another handle is not trusted');
306
+ });
307
+
308
+ test('only a DEFINITE negative blocks the step — unknown and checking never gate it (N10)', async () => {
309
+ const { body } = await request('/projects', 'game.test.local');
310
+ const fn = (body.match(/function validateStep\(step\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
311
+ assert.ok(fn, 'validateStep() must be extractable');
312
+ const gate = (fn.match(/if \(!state\.domain && !state\.noAddress\) \{[\s\S]*?\n {8}\}/) || [''])[0];
313
+ assert.ok(gate, 'the availability gate must be extractable');
314
+ assert.match(gate, /av === 'taken' \|\| av === 'yours' \|\| av === 'reserved'/,
315
+ 'exactly the three definite negatives block');
316
+ assert.doesNotMatch(gate, /'unknown'|'checking'/,
317
+ 'an unanswered or in-flight check must never trap the owner on the step');
318
+ });
319
+
320
+ test("the wizard's validSlug() agrees with the server's isValidSlug(), handle for handle (N10)", async () => {
321
+ // The whole availability contract is "answer what create would answer" — but the
322
+ // page gates on its OWN hand-copied validSlug() before it even asks. If the two
323
+ // drift, the wizard promises (or refuses) an address the server would not, which is
324
+ // the exact class of dishonesty N10 exists to close. Run both over the same corpus.
325
+ const { body } = await request('/projects', 'game.test.local');
326
+ const src = (body.match(/(function validSlug\(s\)\s*\{[\s\S]*?\n \})/) || [, ''])[1];
327
+ assert.ok(src, 'validSlug() must be extractable');
328
+ const clientValid = new Function('s', `${src}\nreturn validSlug(s);`);
329
+ const { createRequire } = await import('node:module');
330
+ const require = createRequire(import.meta.url);
331
+ const serverValid = require('../modules/provisioning/provisioning.js').isValidSlug;
332
+
333
+ const corpus = [
334
+ 'mercury', 'a1', 'x'.repeat(40), 'has-dash', 'a-b-c', '0start', 'end0',
335
+ 'a', '', 'x'.repeat(41), '-lead', 'trail-', 'do--uble', 'UPPER', 'sp ace',
336
+ 'dot.ted', 'under_score', 'bang!', 'uni✦code', '--', '-', 'a--b',
337
+ ];
338
+ for (const s of corpus) {
339
+ assert.equal(clientValid(s), serverValid(s),
340
+ `validSlug(${JSON.stringify(s)}) disagrees: page=${clientValid(s)} server=${serverValid(s)}`);
341
+ }
342
+ });
343
+
344
+ test('the create POST lets the SERVER assign the default address (N2)', async () => {
345
+ const { body } = await request('/projects', 'game.test.local');
346
+ // the wizard may only carry a bring-your-own domain; the <slug>.<apex> default is
347
+ // the server's (task 1002704), and re-asserting a client-derived copy could only
348
+ // disagree with it
349
+ assert.match(body, /domain: byoDomain\(\) \|\| undefined/,
350
+ 'only a bring-your-own domain travels in the create body');
351
+ assert.doesNotMatch(body, /domain: resolvedDomain\(\) \|\| undefined/,
352
+ 'the client-derived assigned default must no longer be re-sent');
353
+ const fn = (body.match(/function byoDomain\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
354
+ assert.ok(fn, 'byoDomain() must be extractable');
355
+ assert.match(fn, /looksLikeDomain\(state\.domain\)/, 'and only a domain that parses');
356
+ // the opt-out is still the only thing that means "no address"
357
+ assert.match(body, /no_address: state\.noAddress \? true : undefined/,
358
+ 'the explicit opt-out shape must survive — an absent domain now means "assign one"');
359
+ // resolvedDomain() still drives what the page DISPLAYS; it just stops driving the wire
360
+ assert.match(body, /resolvedDomain\(\)/, 'the display path keeps its own resolver');
361
+ });
362
+
363
+ test('the done panel promises the address the SERVER assigned, not our preview (N2)', async () => {
364
+ const { body } = await request('/projects', 'game.test.local');
365
+ assert.match(body, /var madeDomain = \(data\.instance && data\.instance\.domain\) \|\| '';/,
366
+ 'the created-instance stash reads the address off the create response');
367
+ assert.match(body, /domain: madeDomain/, 'and stashes that, not resolvedDomain()');
368
+ assert.match(body, /noaddr: !madeDomain/,
369
+ 'no address on the row IS the no-address state — derived, not re-asserted');
370
+ });
371
+
254
372
  test('the address row surfaces the manual-DNS note the runner recorded (F16)', async () => {
255
373
  const { body } = await request('/projects', 'game.test.local');
256
374
  assert.match(body, /inst\.dns_note/, 'the card reads the dns_note the runner writes');