@bongos/core 1.19.126 → 1.19.128

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.
@@ -1033,17 +1033,40 @@ await ta('placeManifestCreds rejects a dedicated droplet (no post-hoc exec path)
1033
1033
  assert.equal(noCode.ok, false); assert.match(noCode.error, /no github_code/);
1034
1034
  });
1035
1035
 
1036
- await ta('placeManifestCreds fails when /healthz never reports auth_configured (never claims success)', async () => {
1036
+ await ta('verification timeout reports the code as SPENT and does not disown the placement (F23)', async () => {
1037
1037
  const inst = { ...coTenant, domain: 'mercury.example.com' };
1038
1038
  const deps = {
1039
- apply: true, privileged: false, log: () => {},
1039
+ apply: true, privileged: false, log: () => {}, sleep: async () => {},
1040
1040
  convertManifestCode: async () => ({ clientId: 'Iv1.abcdef0123456789', clientSecret: 'z'.repeat(40), appId: 1, appSlug: 's' }),
1041
1041
  exec: () => ({ ok: true, stdout: '{"ok":true,"auth_configured":false}' }),
1042
1042
  writeFile: () => ({ ok: true }),
1043
1043
  };
1044
1044
  const r = await P.placeManifestCreds(inst, { github_code: 'C', attempts: 1 }, deps);
1045
1045
  assert.equal(r.ok, false);
1046
- assert.match(r.error, /auth_configured=false/);
1046
+ assert.equal(r.codeSpent, true, 'the conversion happened — the single-use code is burned');
1047
+ assert.match(r.error, /credentials are placed/, 'the error owns what DID happen');
1048
+ assert.doesNotMatch(r.error, /\b\d+s\b|minute|second/i, 'no wall-clock promises (§5.2)');
1049
+ });
1050
+
1051
+ await ta('verification PATIENCE wins the boot race — one conversion, success (F23)', async () => {
1052
+ const inst = { ...coTenant, domain: 'mercury.example.com' };
1053
+ let probes = 0, converts = 0;
1054
+ const deps = {
1055
+ apply: true, privileged: false, log: () => {}, sleep: async () => {},
1056
+ convertManifestCode: async () => { converts++; return { clientId: 'Iv1.abcdef0123456789', clientSecret: 'z'.repeat(40), appId: 1, appSlug: 's' }; },
1057
+ exec: (cmd) => {
1058
+ if (/healthz/.test(cmd)) {
1059
+ probes++;
1060
+ return { ok: true, stdout: probes > 20 ? '{"auth_configured":true}' : '{"auth_configured":false}' };
1061
+ }
1062
+ return { ok: true, stdout: '' };
1063
+ },
1064
+ writeFile: () => ({ ok: true }),
1065
+ };
1066
+ const r = await P.placeManifestCreds(inst, { github_code: 'C', attempts: 1 }, deps);
1067
+ assert.equal(r.ok, true, 'a slow child boot is waited out, not recorded as a failure');
1068
+ assert.equal(converts, 1, 'the single-use code is converted exactly once');
1069
+ assert.ok(probes > 10, 'the old 10-probe budget would have lost this race');
1047
1070
  });
1048
1071
 
1049
1072
  await ta('drainOAuthManifests (--apply): claims a code_received row, places creds, resolves done', async () => {
@@ -1064,9 +1087,57 @@ await ta('drainOAuthManifests (--apply): claims a code_received row, places cred
1064
1087
  assert.equal(resolved.length, 1);
1065
1088
  assert.equal(resolved[0].status, 'done');
1066
1089
  assert.equal(resolved[0].patch.app_slug, 'app-3');
1090
+ assert.equal(resolved[0].patch.github_code, null, 'a done row holds no re-convertible code (F23)');
1067
1091
  assert.ok(events.includes('oauth-manifest-done'));
1068
1092
  });
1069
1093
 
1094
+ await ta('a SPENT code is never re-converted — terminal error, no code_received reset (F23)', async () => {
1095
+ // A stateful one-row store: claimNext honors status, resolve writes it back —
1096
+ // exactly the reset loop that used to re-convert a burned code on the next tick.
1097
+ const row = { id: 21, instance_id: 1, github_code: 'BURNED', attempts: 1, status: 'code_received' };
1098
+ const resolves = []; let converts = 0;
1099
+ const deps = {
1100
+ apply: true, privileged: false, log: () => {}, sleep: async () => {},
1101
+ db: {}, writeFile: () => ({ ok: true }),
1102
+ exec: (cmd) => (/healthz/.test(cmd) ? { ok: true, stdout: '{"auth_configured":false}' } : { ok: true, stdout: '' }),
1103
+ convertManifestCode: async () => { converts++; return { clientId: 'Iv1.abcdef0123456789', clientSecret: 'z'.repeat(40), appId: 3, appSlug: 'a' }; },
1104
+ provisioning: {
1105
+ claimNextOAuthManifest: async () => (row.status === 'code_received' ? { ...row } : null),
1106
+ getInstanceById: async () => ({ ...coTenant, domain: 'mercury.example.com' }),
1107
+ resolveOAuthManifest: async (_db, _id, status, patch = {}) => {
1108
+ resolves.push({ status, patch });
1109
+ row.status = status;
1110
+ if ('github_code' in patch) row.github_code = patch.github_code;
1111
+ },
1112
+ recordEvent: async () => {},
1113
+ },
1114
+ };
1115
+ await P.drainOAuthManifests(deps); // tick 1: converts, placement verification times out
1116
+ await P.drainOAuthManifests(deps); // tick 2: must find nothing to claim
1117
+ assert.equal(converts, 1, 'the single-use code was converted exactly once across ticks');
1118
+ assert.equal(row.status, 'error', 'a spent code is terminal');
1119
+ assert.equal(row.github_code, null, 'the burned code is scrubbed from the row');
1120
+ assert.ok(!resolves.some((x) => x.status === 'code_received'), 'never reset for a doomed retry');
1121
+ });
1122
+
1123
+ await ta('a PRE-conversion failure stays retryable — the narrowing spared the retry path (F23)', async () => {
1124
+ let claims = 0; const resolves = [];
1125
+ const deps = {
1126
+ apply: true, privileged: false, log: () => {}, sleep: async () => {},
1127
+ db: {}, exec: () => ({ ok: true, stdout: '' }), writeFile: () => ({ ok: true }),
1128
+ provisioning: {
1129
+ claimNextOAuthManifest: async () => (claims++ === 0 ? { id: 22, instance_id: 1, github_code: 'C', attempts: 1 } : null),
1130
+ // dedicated shape → placeManifestCreds refuses BEFORE converting (codeSpent absent)
1131
+ getInstanceById: async () => ({ ...coTenant, hosting_shape: 'dedicated', domain: 'd.example.com' }),
1132
+ resolveOAuthManifest: async (_db, _id, status, patch = {}) => { resolves.push({ status, patch }); },
1133
+ recordEvent: async () => {},
1134
+ },
1135
+ };
1136
+ await P.drainOAuthManifests(deps);
1137
+ assert.equal(resolves.length, 1);
1138
+ assert.equal(resolves[0].status, 'code_received', 'an unconverted code may retry on a later tick');
1139
+ });
1140
+
1070
1141
  console.log('\nprivileged mode (PROVISION_SUDO — task 2066):');
1071
1142
 
1072
1143
  t('dbCreateCmd: privileged runs createdb as the postgres role; default is bare (peer auth)', () => {
@@ -1502,17 +1573,20 @@ console.log('\ndomain-attach leg (task 1002692 — give an ACTIVE instance its l
1502
1573
  // bodies), and recorded events. No status writes to capture — domain-attach must
1503
1574
  // never touch the instance lifecycle.
1504
1575
  function attachRecorder({ apply = false } = {}) {
1505
- const cmds = [], envs = [], writes = [], events = [];
1576
+ const cmds = [], envs = [], writes = [], events = [], dnsNotes = [];
1506
1577
  const deps = {
1507
1578
  apply, log: () => {},
1508
1579
  db: { query: async () => ({ rows: [] }) },
1509
- provisioning: { recordEvent: async (_db, e) => { events.push(e); } },
1580
+ provisioning: {
1581
+ recordEvent: async (_db, e) => { events.push(e); },
1582
+ recordDnsNote: async (_db, id, note) => { dnsNotes.push({ id, note }); },
1583
+ },
1510
1584
  exec: (cmd, opts = {}) => { cmds.push(cmd); envs.push((opts && opts.env) || null); return { ok: true, stdout: '' }; },
1511
1585
  writeFile: (p, body) => { writes.push({ path: p, body }); return { ok: true }; },
1512
1586
  doApi: null,
1513
1587
  checkDnsTokenScope: async () => ({ reachable: true, zone: 'cloudbongos.com' }),
1514
1588
  };
1515
- return { deps, cmds, envs, writes, events };
1589
+ return { deps, cmds, envs, writes, events, dnsNotes };
1516
1590
  }
1517
1591
 
1518
1592
  const activeStandalone = {
@@ -1630,5 +1704,197 @@ await ta('cmdRunIntents (--apply): an UNKNOWN intent action never falls through
1630
1704
  assert.ok(!cmds.some((c) => /systemctl (stop|disable)/.test(c)), 'no teardown commands ran');
1631
1705
  });
1632
1706
 
1707
+ console.log('\nprovisioning truthfulness (task 1002720 — QA-walk F15/F16/F19):');
1708
+
1709
+ // An apply-mode recorder for provisionInstance: every command succeeds except
1710
+ // (optionally) the healthz probe, which soft-fails the way a refused connection
1711
+ // does under allowFail.
1712
+ function truthRecorder({ healthzFails = false } = {}) {
1713
+ const cmds = [], statuses = [], patches = [], probes = [], logs = [], events = [], dnsNotes = [];
1714
+ const deps = {
1715
+ apply: true, privileged: false, log: (m) => logs.push(String(m)), sleep: async () => {},
1716
+ db: { query: async () => ({ rows: [] }) },
1717
+ provisioning: {
1718
+ setInstanceStatus: async (_db, _id, s, patch) => { statuses.push(s); patches.push(patch || {}); },
1719
+ recordLivenessProbe: async (_db, _id, up) => { probes.push(up); },
1720
+ recordDnsNote: async (_db, _id, note) => { dnsNotes.push(note); },
1721
+ recordEvent: async (_db, e) => { events.push(e); },
1722
+ getInstanceBySlug: async () => null, getInstanceById: async () => null,
1723
+ claimNextIntent: async () => null, resolveIntent: async () => {},
1724
+ enqueueIntent: async () => ({ created: true }),
1725
+ claimNextOAuthManifest: async () => null, resolveOAuthManifest: async () => {},
1726
+ },
1727
+ exec: (cmd) => {
1728
+ cmds.push(cmd);
1729
+ if (healthzFails && /healthz/.test(cmd)) return { ok: true, stdout: '', softFailed: true };
1730
+ return { ok: true, stdout: '' };
1731
+ },
1732
+ writeFile: () => ({ ok: true }),
1733
+ doApi: null,
1734
+ checkDnsTokenScope: async () => ({ reachable: true, zone: 'cloudbongos.com' }),
1735
+ getGithubToken: async () => null,
1736
+ };
1737
+ return { deps, cmds, statuses, patches, probes, logs, events, dnsNotes };
1738
+ }
1739
+
1740
+ await ta('F15: a refused /healthz is RECORDED — probe handed to the overlay, note cleared, success line qualified', async () => {
1741
+ const { deps, cmds, statuses, patches, probes, logs, events } = truthRecorder({ healthzFails: true });
1742
+ const r = await P.provisionInstance({ ...coTenant }, deps);
1743
+ assert.equal(r.ok, true, 'a failed probe must NOT fail the run — that would burn the intent retry budget');
1744
+ assert.equal(r.healthy, false);
1745
+ assert.equal(statuses[statuses.length - 1], 'active', 'the lifecycle still activates (the overlay carries the truth)');
1746
+ assert.equal(patches[patches.length - 1].error_note, null, 'a stale error_note never rides into active');
1747
+ assert.deepEqual(probes, [false], 'the runner hands its observation to the liveness overlay');
1748
+ assert.ok(cmds.filter((c) => /healthz/.test(c)).length >= 2, 'a slow boot gets a bounded retry, not one shot');
1749
+ assert.ok(logs.some((l) => /not answering/.test(l)), 'the operator line is qualified, not a bare ✓');
1750
+ const activeEvent = events.find((e) => e.event === 'active');
1751
+ assert.match(activeEvent.detail, /not answering/, 'the event detail carries the observation');
1752
+ });
1753
+
1754
+ await ta('F15: a passing /healthz records up=true immediately — no five-minute "live?" limbo', async () => {
1755
+ const { deps, cmds, probes, logs } = truthRecorder();
1756
+ const r = await P.provisionInstance({ ...coTenant }, deps);
1757
+ assert.equal(r.ok, true);
1758
+ assert.equal(r.healthy, true);
1759
+ assert.deepEqual(probes, [true]);
1760
+ assert.equal(cmds.filter((c) => /healthz/.test(c)).length, 1, 'a pass ends the loop on the first probe');
1761
+ assert.ok(logs.some((l) => /✓ mercury provisioned$/.test(l)), 'the plain success line survives');
1762
+ });
1763
+
1764
+ await ta('F16: a no-hook attach is honest — ok:true, dns_note written, event says SKIPPED', async () => {
1765
+ const savedHook = P.CONFIG.dnsHook, savedCp = P.CONFIG.controlPlaneIp, savedRemote = P.CONFIG.remoteHost;
1766
+ P.CONFIG.dnsHook = null; P.CONFIG.controlPlaneIp = '203.0.113.9'; P.CONFIG.remoteHost = null;
1767
+ try {
1768
+ const { deps, events, dnsNotes } = attachRecorder({ apply: true });
1769
+ const r = await P.domainAttachInstance({ ...activeStandalone }, deps);
1770
+ assert.equal(r.ok, true, 'an unset hook is unretryable — ok:false would burn the retry budget');
1771
+ assert.equal(dnsNotes.length, 1);
1772
+ assert.match(dnsNotes[0].note, /A record/, 'the owner is told the outstanding manual step');
1773
+ assert.match(dnsNotes[0].note, /203\.0\.113\.9/, 'the note names the target IP');
1774
+ assert.doesNotMatch(dnsNotes[0].note, /shortly|minute|second/i, 'no wall-clock promises (§5.2)');
1775
+ const detail = events.map((e) => e.detail).join('\n');
1776
+ assert.match(detail, /DNS SKIPPED/, 'the event stops claiming DNS was re-asserted');
1777
+ } finally { P.CONFIG.dnsHook = savedHook; P.CONFIG.controlPlaneIp = savedCp; P.CONFIG.remoteHost = savedRemote; }
1778
+ });
1779
+
1780
+ await ta('F16: a hooked attach CLEARS a stale dns_note', async () => {
1781
+ const savedHook = P.CONFIG.dnsHook, savedCp = P.CONFIG.controlPlaneIp, savedRemote = P.CONFIG.remoteHost;
1782
+ P.CONFIG.dnsHook = '/opt/infra/box-dns-cloudflare.sh'; P.CONFIG.controlPlaneIp = '203.0.113.9'; P.CONFIG.remoteHost = null;
1783
+ try {
1784
+ const { deps, dnsNotes } = attachRecorder({ apply: true });
1785
+ const r = await P.domainAttachInstance({ ...activeStandalone }, deps);
1786
+ assert.equal(r.ok, true);
1787
+ assert.deepEqual(dnsNotes, [{ id: activeStandalone.id, note: null }], 'a successful attach retires the note');
1788
+ } finally { P.CONFIG.dnsHook = savedHook; P.CONFIG.controlPlaneIp = savedCp; P.CONFIG.remoteHost = savedRemote; }
1789
+ });
1790
+
1791
+ await ta('F19: a RETIRED intent records its reason on the instance without changing its status', async () => {
1792
+ let claims = 0; const resolved = []; const statusWrites = [];
1793
+ const deps = {
1794
+ apply: true, log: () => {},
1795
+ db: { query: async () => ({ rows: [] }) },
1796
+ provisioning: {
1797
+ claimNextIntent: async () => (claims++ === 0
1798
+ ? { id: 9, instance_id: 18, action: 'domain-attach', attempts: P.MAX_INTENT_ATTEMPTS } : null),
1799
+ // an ACTIVE instance with no domain → the attach leg returns ok:false (unretryable)
1800
+ getInstanceById: async () => ({ ...activeStandalone, domain: null }),
1801
+ resolveIntent: async (_db, id, state, err) => { resolved.push({ id, state, err }); },
1802
+ setInstanceStatus: async (_db, _id, s, patch) => { statusWrites.push({ s, patch }); },
1803
+ recordEvent: async () => {},
1804
+ claimNextOAuthManifest: async () => null, resolveOAuthManifest: async () => {},
1805
+ },
1806
+ exec: () => ({ ok: true }), writeFile: () => ({ ok: true }), doApi: null,
1807
+ checkDnsTokenScope: async () => ({ reachable: true, zone: 'z' }),
1808
+ };
1809
+ const r = await P.cmdRunIntents(deps);
1810
+ assert.equal(r.errors, 1);
1811
+ assert.equal(resolved[0].state, 'error', 'the intent retires');
1812
+ assert.equal(statusWrites.length, 1, 'the retire is surfaced on the instance');
1813
+ assert.equal(statusWrites[0].s, 'active', 'status write is a NO-OP on status — a healthy instance is never marked broken');
1814
+ assert.match(statusWrites[0].patch.error_note, /no domain recorded/, 'the reason survives where the card reads it');
1815
+ });
1816
+
1817
+ await ta('F19: a NON-final failure re-pends the intent and writes nothing on the instance', async () => {
1818
+ let claims = 0; const resolved = []; const statusWrites = []; const updates = [];
1819
+ const deps = {
1820
+ apply: true, log: () => {},
1821
+ db: { query: async (sql) => { if (/UPDATE provisioning_intents/.test(sql)) updates.push(sql); return { rows: [] }; } },
1822
+ provisioning: {
1823
+ claimNextIntent: async () => (claims++ === 0
1824
+ ? { id: 10, instance_id: 18, action: 'domain-attach', attempts: 1 } : null),
1825
+ getInstanceById: async () => ({ ...activeStandalone, domain: null }),
1826
+ resolveIntent: async (_db, id, state, err) => { resolved.push({ id, state, err }); },
1827
+ setInstanceStatus: async (_db, _id, s, patch) => { statusWrites.push({ s, patch }); },
1828
+ recordEvent: async () => {},
1829
+ claimNextOAuthManifest: async () => null, resolveOAuthManifest: async () => {},
1830
+ },
1831
+ exec: () => ({ ok: true }), writeFile: () => ({ ok: true }), doApi: null,
1832
+ checkDnsTokenScope: async () => ({ reachable: true, zone: 'z' }),
1833
+ };
1834
+ await P.cmdRunIntents(deps);
1835
+ assert.equal(resolved.length, 0, 'not resolved — it re-pends for a later tick');
1836
+ assert.equal(updates.length, 1, 're-pended via the intents table');
1837
+ assert.equal(statusWrites.length, 0, 'no instance write until the intent actually retires');
1838
+ });
1839
+
1840
+ console.log('\nreview-panel hardening (task 1002720 — the loopback/latch/birth-note seams):');
1841
+
1842
+ await ta('a PASSING loopback probe on a DOMAIN-bearing instance records NO verdict — the address claim belongs to the sweep', async () => {
1843
+ const { deps, probes } = truthRecorder();
1844
+ const r = await P.provisionInstance({ ...coTenant, domain: 'mercury.example.com' }, deps);
1845
+ assert.equal(r.ok, true);
1846
+ assert.equal(r.healthy, true);
1847
+ assert.deepEqual(probes, [], 'loopback-up proves the process, not the domain — "Answering at" must stay unearned');
1848
+ });
1849
+
1850
+ await ta('a FAILED loopback probe is a verdict on any shape — recorded even with a domain', async () => {
1851
+ const { deps, probes } = truthRecorder({ healthzFails: true });
1852
+ const r = await P.provisionInstance({ ...coTenant, domain: 'mercury.example.com' }, deps);
1853
+ assert.equal(r.healthy, false);
1854
+ assert.deepEqual(probes, [false], 'a dead process answers nowhere — down is down');
1855
+ });
1856
+
1857
+ await ta('birth-path dns_note: written when the hook is unset, CLEARED when a later run has one', async () => {
1858
+ const savedHook = P.CONFIG.dnsHook, savedPub = P.CONFIG.publicIp;
1859
+ P.CONFIG.publicIp = '203.0.113.5';
1860
+ try {
1861
+ P.CONFIG.dnsHook = null;
1862
+ const a = truthRecorder();
1863
+ await P.provisionInstance({ ...coTenant, domain: 'mercury.example.com' }, a.deps);
1864
+ assert.equal(a.dnsNotes.length, 1, 'the no-hook birth writes the note');
1865
+ assert.match(a.dnsNotes[0], /A record/);
1866
+ P.CONFIG.dnsHook = '/opt/infra/box-dns-cloudflare.sh';
1867
+ const b = truthRecorder();
1868
+ await P.provisionInstance({ ...coTenant, domain: 'mercury.example.com' }, b.deps);
1869
+ assert.deepEqual(b.dnsNotes, [null], 'a hooked run (e.g. a revive) retires the stale note');
1870
+ } finally { P.CONFIG.dnsHook = savedHook; P.CONFIG.publicIp = savedPub; }
1871
+ });
1872
+
1873
+ await ta('a successful domain-attach releases the F19 error_note latch', async () => {
1874
+ const savedHook = P.CONFIG.dnsHook, savedCp = P.CONFIG.controlPlaneIp, savedRemote = P.CONFIG.remoteHost;
1875
+ P.CONFIG.dnsHook = '/opt/infra/box-dns-cloudflare.sh'; P.CONFIG.controlPlaneIp = '203.0.113.9'; P.CONFIG.remoteHost = null;
1876
+ try {
1877
+ const { deps } = attachRecorder({ apply: true });
1878
+ const cleared = [];
1879
+ deps.provisioning.clearErrorNote = async (_db, id) => { cleared.push(id); };
1880
+ const r = await P.domainAttachInstance({ ...activeStandalone, error_note: 'DNS preflight failed' }, deps);
1881
+ assert.equal(r.ok, true);
1882
+ assert.deepEqual(cleared, [activeStandalone.id], 'the "we stopped trying" paragraph must not outlive its recovery');
1883
+ } finally { P.CONFIG.dnsHook = savedHook; P.CONFIG.controlPlaneIp = savedCp; P.CONFIG.remoteHost = savedRemote; }
1884
+ });
1885
+
1886
+ await ta('UNUSABLE creds from a successful conversion still report codeSpent — no re-POST of a burned code', async () => {
1887
+ const inst = { ...coTenant, domain: 'mercury.example.com' };
1888
+ const deps = {
1889
+ apply: true, privileged: false, log: () => {}, sleep: async () => {},
1890
+ convertManifestCode: async () => ({ clientId: 'short', clientSecret: 'also-short' }),
1891
+ exec: () => ({ ok: true, stdout: '' }), writeFile: () => ({ ok: true }),
1892
+ };
1893
+ const r = await P.placeManifestCreds(inst, { github_code: 'C', attempts: 1 }, deps);
1894
+ assert.equal(r.ok, false);
1895
+ assert.match(r.error, /unusable creds/);
1896
+ assert.equal(r.codeSpent, true, 'the conversion RESOLVED — GitHub burned the code even though the creds are junk');
1897
+ });
1898
+
1633
1899
  console.log(`\nprovision: ${passed} passed, ${failed} failed`);
1634
1900
  process.exit(failed ? 1 : 0);
@@ -189,6 +189,46 @@ await ta('same domain again is idempotent — 200, changed:false, nothing re-que
189
189
  assert.equal(enqueueCalls.length, 0);
190
190
  });
191
191
 
192
+ await ta('same domain again RE-ENQUEUES when the wiring stopped — the owner re-kick (task 1002720 / F19)', async () => {
193
+ // A retired domain-attach intent left its error_note; re-sending the same
194
+ // address is the owner's re-kick. The idempotent no-op above still holds for
195
+ // a healthy instance — this branch needs the demonstrated failure.
196
+ reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'spike.cloudbongos.com', error_note: 'DNS preflight failed' } });
197
+ const res = await post('/provisioning/instances/18/domain', { domain: 'spike.cloudbongos.com' });
198
+ assert.equal(res.status, 200, res.raw);
199
+ assert.equal(res.body.changed, false);
200
+ assert.equal(res.body.queued, true, 'the attach intent re-queues');
201
+ assert.deepEqual(enqueueCalls.map((c) => c.action), ['domain-attach']);
202
+ assert.equal(updateCalls.length, 0, 'the domain itself is untouched');
203
+ assert.match(res.body.message, /Wiring that address again/);
204
+ });
205
+
206
+ await ta('a verified-DOWN address re-enqueues too; an open intent keeps the no-op', async () => {
207
+ reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'spike.cloudbongos.com', last_verified_up: false } });
208
+ const res = await post('/provisioning/instances/18/domain', { domain: 'spike.cloudbongos.com' });
209
+ assert.equal(res.body.queued, true, 'probed-down wiring may be re-kicked');
210
+ reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'spike.cloudbongos.com', error_note: 'x' } });
211
+ const savedGetOpen = provisioning.getOpenIntent;
212
+ provisioning.getOpenIntent = async () => ({ id: 5, action: 'domain-attach' });
213
+ try {
214
+ const res2 = await post('/provisioning/instances/18/domain', { domain: 'spike.cloudbongos.com' });
215
+ assert.equal(res2.body.queued, false, 'an in-flight intent is never doubled');
216
+ assert.equal(enqueueCalls.length, 0);
217
+ } finally { provisioning.getOpenIntent = savedGetOpen; }
218
+ });
219
+
220
+ await ta('re-kick honors enqueueIntent created:false — a racing enqueue is never double-reported', async () => {
221
+ reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'spike.cloudbongos.com', error_note: 'x' } });
222
+ const savedEnqueue = provisioning.enqueueIntent;
223
+ provisioning.enqueueIntent = async (...a) => { enqueueCalls.push({ action: a[2] }); return { intent: { id: 5 }, created: false }; };
224
+ try {
225
+ const res = await post('/provisioning/instances/18/domain', { domain: 'spike.cloudbongos.com' });
226
+ assert.equal(res.status, 200, res.raw);
227
+ assert.equal(res.body.queued, false, 'queued mirrors created, like the sibling attach path');
228
+ assert.match(res.body.message, /already being wired/);
229
+ } finally { provisioning.enqueueIntent = savedEnqueue; }
230
+ });
231
+
192
232
  await ta('a DIFFERENT domain already set is a truthful 409 domain_already_set (re-pointing is not this route)', async () => {
193
233
  reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'old.cloudbongos.com' } });
194
234
  const res = await post('/provisioning/instances/18/domain', { domain: 'new.cloudbongos.com' });
@@ -117,6 +117,39 @@ test('the empty state points a new owner at the wizard view — never at a hall'
117
117
  assert.doesNotMatch(body, /Enter the Hall/i);
118
118
  });
119
119
 
120
+ test('teardown copy states what the runner actually does, per hosting shape (F17)', async () => {
121
+ const { body } = await request('/projects', 'game.test.local');
122
+ // the DECIDED policy: shared shapes keep the database; a dedicated machine
123
+ // takes its database with it — the copy branches on the shape field
124
+ assert.match(body, /The database is kept/, 'the kept-shape promise is present');
125
+ assert.match(body, /database goes with (it|the machine)/, 'the dedicated branch exists');
126
+ assert.match(body, /hosting_shape === 'dedicated'/, 'shape-driven, not flat prose');
127
+ assert.match(body, /repository is untouched|repository stays yours|repository is still yours/i);
128
+ // the pre-walk over-claim can never come back
129
+ assert.doesNotMatch(body, /server and database go away/i);
130
+ assert.doesNotMatch(body, /database (is|will be) (deleted|dropped|destroyed|removed)/i);
131
+ });
132
+
133
+ test('the done panel reads the open intent — a queued revive is never told to start another (F22)', async () => {
134
+ const { body } = await request('/projects', 'game.test.local');
135
+ const fn = (body.match(/function renderProgress\(inst\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
136
+ assert.ok(fn, 'renderProgress() must be extractable');
137
+ assert.match(fn, /open_intent/, 'the strip is chosen from the open intent, not status alone');
138
+ assert.match(fn, /revive/, 'a queued revive on a torn-down instance gets the revive vocabulary');
139
+ // a requested instance ALWAYS has its provision intent open — the status
140
+ // branch must win there or it goes dead and diverges from the mine card
141
+ assert.match(fn, /!== 'requested'/, 'requested speaks its status, not its (always-present) intent');
142
+ assert.match(body, /inst\.open_intent = data\.open_intent \|\| null/, 'the watch hands the intent through');
143
+ // teardown speaks its own stage names in the done panel, in its own palette
144
+ assert.match(fn, /'teardown'/, 'the teardown flow is used');
145
+ assert.match(body, /\.prog\.flowDown/, 'the .prog strip has the teardown palette (transcribed from .strip.flowDown)');
146
+ });
147
+
148
+ test('the address row surfaces the manual-DNS note the runner recorded (F16)', async () => {
149
+ const { body } = await request('/projects', 'game.test.local');
150
+ assert.match(body, /inst\.dns_note/, 'the card reads the dns_note the runner writes');
151
+ });
152
+
120
153
  test('the watch give-up hands the owner their projects view — "the hall has it" is dead', async () => {
121
154
  // the wizard (and its 33-minute watch cap) lives on /projects now
122
155
  const { body } = await request('/projects', 'game.test.local');