master
John Lauer docs: first-run contract in the Rust plan (arav-rog findings), changelog 2.0.397-2.0.399, ship.sh publishes pkg before… 47808a0 2mo ago
// Gate: the consent registry (pup-bridge#92, ab SDK blocked_on_user rule). Pure, no ab, no OS.
// Proves: in-verb cap returns blocked_on_user (pending) while the toast keeps collecting;
// a re-call with the same key re-attaches (no second toast); the decision is handed over exactly
// once; a toast timeout retracts the toast (notify_dismiss) and records consent_timeout.
const assert = require('assert');
const { createRegistry, blockedOnUser } = require('../src/consent.js');

const calls = [];
let resolveToast = null;
const abCommand = (verb, args) => {
  calls.push({ verb, args });
  if (verb === 'notify_user') return new Promise((r) => { resolveToast = r; });
  if (verb === 'notify_dismiss') return Promise.resolve({ dismissed: true });
  return Promise.resolve({});
};
const reg = createRegistry({ abCommand, abPayload: (r) => r, inVerbMs: 40, totalSec: 120, pickupMs: 10000 });

(async () => {
  const toast = { idPrefix: 'credreveal', title: 'Reveal your example.com login?', buttons: [{ label: 'Reveal' }, { label: 'Deny' }] };
  // 1. first call: no decision within the in-verb cap -> pending
  const a = await reg.ask({ key: 'pup_credential_reveal|example.com|thread-a', verb: 'pup_credential_reveal', what: 'Waiting for the user', toast });
  assert.strictEqual(a.pending, true, 'first call is pending after the in-verb cap');
  assert.ok(a.consentId.startsWith('credreveal-'), 'consent id carries the prefix');
  assert.strictEqual(calls.filter((c) => c.verb === 'notify_user').length, 1, 'exactly one toast shown');
  assert.strictEqual(calls[0].args.wait, true); assert.strictEqual(calls[0].args.timeoutSec, 120);
  const rows1 = reg.rows();
  assert.strictEqual(rows1.length, 1); assert.strictEqual(rows1[0].phase, 'consent'); assert.strictEqual(rows1[0].blockedOnUser, true);

  // 2. re-call with the same key: re-attaches, still one toast
  const b = await reg.ask({ key: 'pup_credential_reveal|example.com|thread-a', verb: 'pup_credential_reveal', what: 'Waiting for the user', toast });
  assert.strictEqual(b.pending, true); assert.strictEqual(b.consentId, a.consentId, 'same consent id on re-attach'); assert.strictEqual(b.attached, 1);
  assert.strictEqual(calls.filter((c) => c.verb === 'notify_user').length, 1, 'no second toast on re-attach');

  // 3. the user clicks while nobody is attached: the row flips to consent_decided; the next
  //    re-call collects the decision exactly once and the record is gone afterwards
  resolveToast({ action: 'Reveal' });
  await new Promise((r) => setTimeout(r, 5));
  const rows2 = reg.rows();
  assert.strictEqual(rows2[0].phase, 'consent_decided'); assert.strictEqual(rows2[0].blockedOnUser, false);
  const c = await reg.ask({ key: 'pup_credential_reveal|example.com|thread-a', verb: 'pup_credential_reveal', what: 'Waiting for the user', toast });
  assert.strictEqual(c.pending, false); assert.strictEqual(c.decision, 'Reveal'); assert.strictEqual(c.consentId, a.consentId);
  assert.strictEqual(reg.map.size, 0, 'picked-up record removed');
  assert.strictEqual(calls.filter((c2) => c2.verb === 'notify_dismiss').length, 0, 'a clicked toast is not retracted');

  // 4. a different key is a different toast
  const d = await reg.ask({ key: 'pup_credential_reveal|other.com|thread-a', verb: 'pup_credential_reveal', what: 'Waiting for the user', toast });
  assert.strictEqual(d.pending, true); assert.notStrictEqual(d.consentId, a.consentId);
  assert.strictEqual(calls.filter((c2) => c2.verb === 'notify_user').length, 2);

  // 5. toast timeout (ab resolves with no action): retract via notify_dismiss, decision null
  resolveToast({});
  await new Promise((r) => setTimeout(r, 5));
  assert.strictEqual(calls.filter((c2) => c2.verb === 'notify_dismiss').length, 1, 'timed-out toast retracted');
  const e = await reg.ask({ key: 'pup_credential_reveal|other.com|thread-a', verb: 'pup_credential_reveal', what: 'Waiting for the user', toast });
  assert.strictEqual(e.pending, false); assert.strictEqual(e.decision, null, 'timeout hands over null (consent_timeout)');

  // 6. the response shape
  const shape = blockedOnUser({ stillRunning: true, blockedBy: 'click Reveal', _hint: 'wait' });
  assert.deepStrictEqual(Object.keys(shape).sort(), ['_hint', 'blockedBy', 'errorCode', 'status', 'stillRunning', 'success'].sort());
  assert.strictEqual(shape.errorCode, 'blocked_on_user'); assert.strictEqual(shape.success, false); assert.strictEqual(shape.stillRunning, true);
  console.log('check_consent: OK');
})().catch((err) => { console.error('check_consent FAILED:', err && err.stack || err); process.exit(1); });