instar 1.3.313 → 1.3.314
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/dashboard/index.html
CHANGED
|
@@ -808,11 +808,16 @@
|
|
|
808
808
|
.mnd-btn-danger { color: #f85149; }
|
|
809
809
|
.mnd-dead-note { font-size: 13px; color: var(--text-dim); font-style: italic; }
|
|
810
810
|
.mnd-notice { min-height: 20px; font-size: 14px; color: #3fb950; }
|
|
811
|
-
.mnd-
|
|
812
|
-
.mnd-issue-form { display: flex; flex-direction: column; gap:
|
|
813
|
-
.mnd-issue-form
|
|
814
|
-
.mnd-issue-form
|
|
815
|
-
.mnd-
|
|
811
|
+
.mnd-issue-form { display: flex; flex-direction: column; gap: 14px; max-width: 640px; padding: 12px 0; }
|
|
812
|
+
.mnd-issue-form label { display: flex; flex-direction: column; gap: 4px; font-size: 15px; color: var(--text-bright); }
|
|
813
|
+
.mnd-issue-form input, .mnd-issue-form textarea { font: inherit; padding: 8px 10px; border-radius: 6px; }
|
|
814
|
+
.mnd-issue-form textarea { font-family: ui-monospace, monospace; font-size: 13px; }
|
|
815
|
+
.mnd-hint { font-size: 13px; color: var(--text-dim); font-weight: 400; line-height: 1.45; }
|
|
816
|
+
.mnd-prefilled { font-size: 12px; color: var(--ph-accent, #4ade80); }
|
|
817
|
+
/* Issue-form feedback: PERSISTENT, not a transient toast. A failed issuance
|
|
818
|
+
the operator never sees (the 2026-06-05 incident) is worse than no form. */
|
|
819
|
+
.mnd-notice-err { border: 1px solid #b91c1c; background: rgba(185, 28, 28, 0.12); color: #fca5a5; padding: 12px 14px; border-radius: 8px; font-size: 15px; line-height: 1.5; white-space: pre-line; }
|
|
820
|
+
.mnd-notice-ok { border: 1px solid #15803d; background: rgba(21, 128, 61, 0.12); color: #86efac; padding: 12px 14px; border-radius: 8px; font-size: 17px; font-weight: 600; line-height: 1.5; }
|
|
816
821
|
.mnd-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
817
822
|
.mnd-table th, .mnd-table td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
|
|
818
823
|
.mnd-reason-cell { color: var(--text-dim); max-width: 420px; }
|
|
@@ -826,7 +831,13 @@
|
|
|
826
831
|
maturation "you're here" marker. */
|
|
827
832
|
.ph-root {
|
|
828
833
|
--ph-accent: #4ade80;
|
|
834
|
+
/* These panels are declared after </main>, making them direct .app grid
|
|
835
|
+
children. switchTab() hides the sidebar too, so without explicit
|
|
836
|
+
placement the lone visible panel auto-places into the 280px sidebar
|
|
837
|
+
COLUMN — the "squashed to the side" bug. Span the full grid. */
|
|
838
|
+
grid-column: 1 / -1;
|
|
829
839
|
max-width: 760px;
|
|
840
|
+
width: 100%;
|
|
830
841
|
font-size: 18px;
|
|
831
842
|
line-height: 1.55;
|
|
832
843
|
color: var(--text-bright);
|
|
@@ -3242,15 +3253,34 @@
|
|
|
3242
3253
|
<h3 class="ph-h">Issued mandates</h3>
|
|
3243
3254
|
<div id="mndList"></div>
|
|
3244
3255
|
</section>
|
|
3245
|
-
<details class="ph-detail">
|
|
3256
|
+
<details class="ph-detail" id="mndIssueDetails">
|
|
3246
3257
|
<summary class="ph-detail-summary">Issue a new mandate (PIN required)</summary>
|
|
3247
3258
|
<div class="mnd-issue-form">
|
|
3248
|
-
<label>
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
<label>
|
|
3253
|
-
|
|
3259
|
+
<label>What is this permission slip for?
|
|
3260
|
+
<span class="mnd-hint">A short name for the project the two agents may work on together, e.g. feedback-migration.</span>
|
|
3261
|
+
<input type="text" id="mndIssueScope" value="feedback-migration" />
|
|
3262
|
+
</label>
|
|
3263
|
+
<label>This agent (Agent A)
|
|
3264
|
+
<span class="mnd-hint">Pre-filled with this agent's own routing fingerprint — you normally don't need to touch it.</span>
|
|
3265
|
+
<input type="text" id="mndIssueAgentA" placeholder="filling in automatically…" />
|
|
3266
|
+
<span class="mnd-prefilled" id="mndAgentAPrefillNote"></span>
|
|
3267
|
+
</label>
|
|
3268
|
+
<label>The other agent (Agent B)
|
|
3269
|
+
<span class="mnd-hint">The peer agent's routing fingerprint — find it on that agent's Threadline tab, or ask the agent "what's your routing fingerprint?".</span>
|
|
3270
|
+
<input type="text" id="mndIssueAgentB" placeholder="e.g. 8c7928aa9f04fbda947172a2f9b2d81a" />
|
|
3271
|
+
</label>
|
|
3272
|
+
<label>What exactly may they do? (the authorities)
|
|
3273
|
+
<span class="mnd-hint">Each entry is one delegated action with its bounds. The template below covers the standard credential-handoff + code-review pair — edit only if you know you need different bounds.</span>
|
|
3274
|
+
<textarea id="mndIssueAuthorities" rows="9" spellcheck="false"></textarea>
|
|
3275
|
+
</label>
|
|
3276
|
+
<label>When does it expire?
|
|
3277
|
+
<span class="mnd-hint">Pre-filled to one week from now. Every mandate expires; expired means denied.</span>
|
|
3278
|
+
<input type="datetime-local" id="mndIssueExpires" />
|
|
3279
|
+
</label>
|
|
3280
|
+
<label>Your dashboard PIN
|
|
3281
|
+
<span class="mnd-hint">Issuing is a human action — only your PIN can do it. Sent once, never stored.</span>
|
|
3282
|
+
<input type="password" id="mndIssuePin" autocomplete="off" placeholder="typed at issuance, never stored" />
|
|
3283
|
+
</label>
|
|
3254
3284
|
<button id="mndIssueBtn" class="mnd-btn">Issue mandate</button>
|
|
3255
3285
|
</div>
|
|
3256
3286
|
</details>
|
|
@@ -8565,6 +8595,8 @@
|
|
|
8565
8595
|
issueExpires: document.getElementById('mndIssueExpires'),
|
|
8566
8596
|
issuePin: document.getElementById('mndIssuePin'),
|
|
8567
8597
|
issueBtn: document.getElementById('mndIssueBtn'),
|
|
8598
|
+
issueDetails: document.getElementById('mndIssueDetails'),
|
|
8599
|
+
agentAPrefillNote: document.getElementById('mndAgentAPrefillNote'),
|
|
8568
8600
|
};
|
|
8569
8601
|
const fetchImpl = (url, opts = {}) => fetch(url, {
|
|
8570
8602
|
...opts,
|
package/dashboard/mandates.js
CHANGED
|
@@ -106,13 +106,30 @@ export function createController({ doc, els, fetchImpl }) {
|
|
|
106
106
|
return { status: res.status, body };
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
// kind: 'error' | 'success' are PERSISTENT (cleared only by the next note or
|
|
110
|
+
// an explicit clearNote) — a transient toast the operator never sees is how
|
|
111
|
+
// the 2026-06-05 silent-issuance-failure happened. 'info' stays transient.
|
|
112
|
+
function note(msg, kind) {
|
|
110
113
|
if (!els.notice) return;
|
|
114
|
+
const isError = kind === true || kind === 'error'; // boolean kept for back-compat
|
|
111
115
|
els.notice.textContent = msg;
|
|
112
|
-
els.notice.className = isError ? 'mnd-notice mnd-notice-err'
|
|
113
|
-
|
|
116
|
+
els.notice.className = isError ? 'mnd-notice mnd-notice-err'
|
|
117
|
+
: kind === 'success' ? 'mnd-notice mnd-notice-ok'
|
|
118
|
+
: 'mnd-notice';
|
|
119
|
+
if (msg && !isError && kind !== 'success') {
|
|
120
|
+
setTimeout(() => { if (els.notice.textContent === msg) els.notice.textContent = ''; }, 8000);
|
|
121
|
+
}
|
|
114
122
|
}
|
|
115
123
|
|
|
124
|
+
function clearNote() {
|
|
125
|
+
if (!els.notice) return;
|
|
126
|
+
els.notice.textContent = '';
|
|
127
|
+
els.notice.className = 'mnd-notice';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let refreshErrorShown = false;
|
|
131
|
+
let autoOpenedIssueForm = false;
|
|
132
|
+
|
|
116
133
|
async function refresh() {
|
|
117
134
|
try {
|
|
118
135
|
const [mand, audit] = await Promise.all([
|
|
@@ -125,12 +142,22 @@ export function createController({ doc, els, fetchImpl }) {
|
|
|
125
142
|
if (els.stamp) els.stamp.textContent = '';
|
|
126
143
|
return;
|
|
127
144
|
}
|
|
128
|
-
|
|
145
|
+
const mandates = mand.body?.mandates;
|
|
146
|
+
els.list.innerHTML = renderMandates(mandates);
|
|
129
147
|
els.audit.innerHTML = renderAudit(audit.body);
|
|
130
148
|
if (els.stamp) els.stamp.textContent = 'updated ' + new Date().toLocaleTimeString();
|
|
149
|
+
// Nothing issued yet → the issue form IS the page's call to action;
|
|
150
|
+
// open it once rather than hiding it behind a collapsed <details>.
|
|
151
|
+
if (!autoOpenedIssueForm && els.issueDetails && (!Array.isArray(mandates) || mandates.length === 0)) {
|
|
152
|
+
els.issueDetails.open = true;
|
|
153
|
+
autoOpenedIssueForm = true;
|
|
154
|
+
}
|
|
155
|
+
// A persistent refresh error from a server restart-gap heals itself.
|
|
156
|
+
if (refreshErrorShown) { clearNote(); refreshErrorShown = false; }
|
|
131
157
|
wireRevokeButtons();
|
|
132
158
|
} catch (e) {
|
|
133
|
-
|
|
159
|
+
refreshErrorShown = true;
|
|
160
|
+
note('refresh failed: ' + (e?.message ?? e) + ' — retrying automatically.', 'error');
|
|
134
161
|
}
|
|
135
162
|
}
|
|
136
163
|
|
|
@@ -150,25 +177,54 @@ export function createController({ doc, els, fetchImpl }) {
|
|
|
150
177
|
body: JSON.stringify({ pin, reason: reasonEl?.value || 'operator revocation (dashboard)' }),
|
|
151
178
|
});
|
|
152
179
|
if (pinEl) pinEl.value = ''; // never retain the PIN
|
|
153
|
-
if (status === 200) { note(
|
|
154
|
-
else note(body?.error ?? `
|
|
180
|
+
if (status === 200) { note(`✓ Mandate ${id} revoked — the gate now denies its actions.`, 'success'); await refresh(); }
|
|
181
|
+
else note(`Not revoked — the server refused: ${body?.error ?? `HTTP ${status}`}`, 'error');
|
|
155
182
|
} finally { btn.disabled = false; }
|
|
156
183
|
};
|
|
157
184
|
});
|
|
158
185
|
}
|
|
159
186
|
|
|
187
|
+
// Validate EVERYTHING client-side and report every problem at once —
|
|
188
|
+
// never POST a request we know the server will refuse, and never let the
|
|
189
|
+
// operator discover a missing field one transient error at a time.
|
|
190
|
+
function validateIssueForm() {
|
|
191
|
+
const problems = [];
|
|
192
|
+
if (!els.issueScope?.value?.trim()) problems.push('• Scope is empty — give the permission slip a short name.');
|
|
193
|
+
if (!els.issueAgentA?.value?.trim()) problems.push('• Agent A is empty — this agent’s own fingerprint (normally pre-filled).');
|
|
194
|
+
if (!els.issueAgentB?.value?.trim()) problems.push('• Agent B is empty — paste the other agent’s routing fingerprint.');
|
|
195
|
+
let authorities = null;
|
|
196
|
+
const rawAuth = els.issueAuthorities?.value ?? '';
|
|
197
|
+
try {
|
|
198
|
+
authorities = JSON.parse(rawAuth || '[]');
|
|
199
|
+
if (!Array.isArray(authorities) || authorities.length === 0) {
|
|
200
|
+
problems.push('• Authorities is empty — the mandate must delegate at least one action.');
|
|
201
|
+
authorities = null;
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
problems.push('• Authorities is not valid JSON — it must be an array of { action, bounds }.');
|
|
205
|
+
}
|
|
206
|
+
const expiresRaw = els.issueExpires?.value ?? '';
|
|
207
|
+
if (!expiresRaw) {
|
|
208
|
+
problems.push('• Expiry is empty — every mandate must expire (normally pre-filled to a week out).');
|
|
209
|
+
} else if (!(Date.parse(expiresRaw) > Date.now())) {
|
|
210
|
+
problems.push('• Expiry is in the past — pick a future date.');
|
|
211
|
+
}
|
|
212
|
+
if (!els.issuePin?.value) problems.push('• Your dashboard PIN is missing — issuing is a human action; agent credentials are refused.');
|
|
213
|
+
return { problems, authorities };
|
|
214
|
+
}
|
|
215
|
+
|
|
160
216
|
async function issue() {
|
|
161
|
-
const
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
217
|
+
const { problems, authorities } = validateIssueForm();
|
|
218
|
+
if (problems.length > 0) {
|
|
219
|
+
note('Not issued — fix the following first:\n' + problems.join('\n'), 'error');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
166
222
|
const payload = {
|
|
167
|
-
pin,
|
|
223
|
+
pin: els.issuePin.value,
|
|
168
224
|
scope: els.issueScope?.value?.trim(),
|
|
169
225
|
agents: [els.issueAgentA?.value?.trim(), els.issueAgentB?.value?.trim()],
|
|
170
226
|
authorities,
|
|
171
|
-
expiresAt:
|
|
227
|
+
expiresAt: new Date(els.issueExpires.value).toISOString(),
|
|
172
228
|
};
|
|
173
229
|
els.issueBtn.disabled = true;
|
|
174
230
|
try {
|
|
@@ -179,19 +235,51 @@ export function createController({ doc, els, fetchImpl }) {
|
|
|
179
235
|
});
|
|
180
236
|
if (els.issuePin) els.issuePin.value = ''; // never retain the PIN
|
|
181
237
|
if (status === 201) {
|
|
182
|
-
note(
|
|
238
|
+
note(`✓ Mandate ${body?.mandate?.id ?? ''} issued — it is active and listed above. The agents can now act within its bounds.`, 'success');
|
|
239
|
+
if (els.issueDetails) els.issueDetails.open = false;
|
|
183
240
|
await refresh();
|
|
184
241
|
} else {
|
|
185
|
-
note(body?.error ?? `
|
|
242
|
+
note(`Not issued — the server refused: ${body?.error ?? `HTTP ${status}`}`, 'error');
|
|
186
243
|
}
|
|
187
244
|
} finally { els.issueBtn.disabled = false; }
|
|
188
245
|
}
|
|
189
246
|
|
|
247
|
+
// datetime-local wants local "YYYY-MM-DDTHH:MM" — toISOString() (UTC, with
|
|
248
|
+
// seconds + Z) renders as blank in the picker.
|
|
249
|
+
function defaultExpiryLocal() {
|
|
250
|
+
const d = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
|
251
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
252
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// The system KNOWS this agent's fingerprint — asking the operator to paste a
|
|
256
|
+
// 32-hex string the dashboard can fetch itself is exactly the UX failure
|
|
257
|
+
// Justin flagged. /threadline/health reads the canonical identity.json (works
|
|
258
|
+
// even when the relay is disconnected); /threadline/status is the fallback.
|
|
259
|
+
async function prefillAgentA() {
|
|
260
|
+
if (!els.issueAgentA || els.issueAgentA.value) return;
|
|
261
|
+
try {
|
|
262
|
+
let fp = null;
|
|
263
|
+
const health = await fetchJson('/threadline/health');
|
|
264
|
+
if (health.status === 200) fp = health.body?.fingerprint ?? null;
|
|
265
|
+
if (!fp) {
|
|
266
|
+
const status = await fetchJson('/threadline/status');
|
|
267
|
+
fp = status.body?.relay?.fingerprint ?? null;
|
|
268
|
+
}
|
|
269
|
+
if (fp && els.issueAgentA && !els.issueAgentA.value) {
|
|
270
|
+
els.issueAgentA.value = fp;
|
|
271
|
+
if (els.agentAPrefillNote) els.agentAPrefillNote.textContent = '✓ pre-filled — this agent’s own fingerprint';
|
|
272
|
+
}
|
|
273
|
+
} catch { /* leave blank; validation reports it plainly */ }
|
|
274
|
+
}
|
|
275
|
+
|
|
190
276
|
function start() {
|
|
191
277
|
if (running) return;
|
|
192
278
|
running = true;
|
|
193
279
|
if (els.issueAuthorities && !els.issueAuthorities.value) els.issueAuthorities.value = AUTHORITIES_TEMPLATE;
|
|
280
|
+
if (els.issueExpires && !els.issueExpires.value) els.issueExpires.value = defaultExpiryLocal();
|
|
194
281
|
if (els.issueBtn) els.issueBtn.onclick = issue;
|
|
282
|
+
prefillAgentA();
|
|
195
283
|
refresh();
|
|
196
284
|
timer = setInterval(refresh, REFRESH_MS);
|
|
197
285
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-05T18:
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-05T18:49:31.951Z",
|
|
5
|
+
"instarVersion": "1.3.314",
|
|
6
6
|
"entryCount": 199,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The Mandates dashboard tab was overhauled after a real operator failed to
|
|
9
|
+
issue a mandate through it (2026-06-05). Four fixes:
|
|
10
|
+
|
|
11
|
+
1. **Layout**: the panel (and the Process Health / Machines / Preferences
|
|
12
|
+
panels, which shared the bug) rendered squashed into the 280px sidebar
|
|
13
|
+
grid column. All `.ph-root` panels now span the full grid.
|
|
14
|
+
2. **Pre-fills**: Agent A is pre-filled with this agent's own routing
|
|
15
|
+
fingerprint (fetched from `/threadline/health`); expiry is pre-filled to
|
|
16
|
+
one week out. The operator no longer pastes hex strings the system
|
|
17
|
+
already knows.
|
|
18
|
+
3. **Validation**: clicking Issue with missing/invalid fields now reports
|
|
19
|
+
EVERY problem at once in plain English, client-side, before anything is
|
|
20
|
+
sent. Previously an empty form POSTed, the server refused, and the only
|
|
21
|
+
feedback was a transient toast.
|
|
22
|
+
4. **Persistent feedback**: errors and the "✓ Mandate issued" confirmation
|
|
23
|
+
are persistent banners, not 8-second toasts. A silent failure is no
|
|
24
|
+
longer possible; success is unmissable.
|
|
25
|
+
|
|
26
|
+
The last knowingly-stale entry from the 2026-06-05 test-debt triage is
|
|
27
|
+
resolved: the TunnelManager unit suite — parked in the CI skip-list because it
|
|
28
|
+
predated the tunnel provider/tier rewrite (22 of its 29 tests could never
|
|
29
|
+
pass) — has been rewritten against the current architecture and turned back
|
|
30
|
+
on. The tunnel layer's core lifecycle is now gated in CI at the manager level:
|
|
31
|
+
provider-pool fallback, the reachability probe, the owner-consent flow for
|
|
32
|
+
backup relays (including the single-use nonce and the decline cooldown), the
|
|
33
|
+
self-heal stability gate, and mandatory credential rotation after a relay
|
|
34
|
+
episode.
|
|
35
|
+
|
|
36
|
+
## What to Tell Your User
|
|
37
|
+
|
|
38
|
+
If you ever tried to issue a coordination mandate from the dashboard and
|
|
39
|
+
nothing seemed to happen, that flow is fixed: the form now fills in
|
|
40
|
+
everything the system already knows, tells you exactly what is missing
|
|
41
|
+
before submitting, and shows an unmissable confirmation when the mandate is
|
|
42
|
+
issued. The Mandates, Process Health, Machines, and Preferences tabs also
|
|
43
|
+
now use the full width of the page instead of a narrow column.
|
|
44
|
+
|
|
45
|
+
Nothing user-visible. This hardens the safety net around the tunnel/backup-
|
|
46
|
+
relay machinery your dashboard links depend on — regressions in that
|
|
47
|
+
machinery now fail PRs instead of shipping.
|
|
48
|
+
|
|
49
|
+
## Summary of New Capabilities
|
|
50
|
+
|
|
51
|
+
- Mandates tab: pre-filled Agent A fingerprint + default one-week expiry,
|
|
52
|
+
all-at-once client-side validation, persistent success/error banners,
|
|
53
|
+
plain-English field hints, and the issue form auto-opens when no mandates
|
|
54
|
+
exist yet.
|
|
55
|
+
- Layout fix for all `.ph-root` dashboard panels (Mandates, Process Health,
|
|
56
|
+
Machines, Preferences Learning): full-grid width instead of the sidebar
|
|
57
|
+
column.
|
|
58
|
+
- Maturity: stable (UI-only change over the existing PIN-gated issuance
|
|
59
|
+
API; no server surface changed).
|
|
60
|
+
|
|
61
|
+
- `tests/unit/TunnelManager.test.ts` runs in CI again (removed from the
|
|
62
|
+
vitest.push.config.ts quarantine): 51 deterministic tests driven entirely
|
|
63
|
+
through the constructor injection seams (`injections.providers`,
|
|
64
|
+
`injections.fetch`) and the public deterministic drivers
|
|
65
|
+
(`runSelfHealCheck()`, `grantConsent()`, `declineConsent()`) — no real
|
|
66
|
+
timers, processes, or network.
|
|
67
|
+
- Maturity: stable (test-only change + quarantine re-arm; no production code
|
|
68
|
+
touched).
|
|
69
|
+
|
|
70
|
+
## Evidence
|
|
71
|
+
|
|
72
|
+
Born from a live operator report (2026-06-05): "I clicked 'Issue Mandate'
|
|
73
|
+
after entering my PIN… this UI/UX is TERRIBLE. Very confusing, unclear, and
|
|
74
|
+
squashed to the side" — the mandate never registered server-side and the
|
|
75
|
+
failure was invisible. 24 tests green in the mandates tab suite (squash
|
|
76
|
+
regression pin, every-problem-at-once validation, persistence under fake
|
|
77
|
+
timers, prefill + never-overwrite, auto-open); 80 green across all dashboard
|
|
78
|
+
suites; tsc clean.
|
|
79
|
+
|
|
80
|
+
Three consecutive green runs (51/51, ~0.8s each), including under the push
|
|
81
|
+
config; the full 11-file tunnel test family passes together (165/165);
|
|
82
|
+
`npx tsc --noEmit` clean. The rewrite-era sibling suites
|
|
83
|
+
(tunnel-manager-rewrite / tunnel-self-heal / tunnel-consent-state-machine /
|
|
84
|
+
tunnel-credential-rotation) already covered parts of this surface — this
|
|
85
|
+
suite consolidates the manager-level coverage, adds the previously-untested
|
|
86
|
+
edge paths (persisted-state restore, corrupted state file, nonce replay,
|
|
87
|
+
cooldown suppression across restart, rotation-failure flag retention,
|
|
88
|
+
probe-handle release), and retires the stale file that occupied the
|
|
89
|
+
canonical TunnelManager.test.ts name.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Side-effects review — TunnelManager unit-suite rewrite + quarantine re-arm
|
|
2
|
+
|
|
3
|
+
Closes the durable commitment "Rewrite TunnelManager unit suite" opened during
|
|
4
|
+
the 2026-06-05 full-suite triage (the one knowingly-stale entry left parked by
|
|
5
|
+
the test-debt re-arm PR #837).
|
|
6
|
+
|
|
7
|
+
## 1. The change
|
|
8
|
+
|
|
9
|
+
- **`tests/unit/TunnelManager.test.ts`** — full rewrite. The old suite
|
|
10
|
+
predated the tunnel provider/tier rewrite: it mocked the `cloudflared`
|
|
11
|
+
module directly, while production drives a provider pool with a REAL
|
|
12
|
+
reachability probe (`driveTier1` → `probeReachability` fetches
|
|
13
|
+
`<url>/health` and requires 2xx), so 22/29 tests could never pass. The new
|
|
14
|
+
suite uses the constructor injection seams exclusively
|
|
15
|
+
(`injections.providers`, `injections.fetch`) plus the public deterministic
|
|
16
|
+
drivers (`runSelfHealCheck()`, `grantConsent()`, `declineConsent()`):
|
|
17
|
+
51 tests covering constructor/persisted-state restore, the happy start
|
|
18
|
+
path, provider-pool fallback (unavailable / start-throw / probe-fail /
|
|
19
|
+
fetch-throw), Tier-2-never-auto-started, exhaustion + relaysEnabled /
|
|
20
|
+
relayConsent config gates, the consent flow (nonce issuance, wrong-nonce
|
|
21
|
+
rejection, single-use replay, decline cooldown, timeout, cooldown
|
|
22
|
+
suppression across restart, post-grant probe/start failure), the self-heal
|
|
23
|
+
stability gate (reset / progress / switched, counter reset on flap,
|
|
24
|
+
throwaway-probe release, self-healed event), stop semantics (relay-stop
|
|
25
|
+
rotates credentials, plain stop doesn't, pending consent cleared), and
|
|
26
|
+
credential rotation (no-op when not pending, failure retains the flag +
|
|
27
|
+
emits rotation-failed, boot recovery, unwired-rotator loud-clear).
|
|
28
|
+
- **`vitest.push.config.ts`** — `tests/unit/TunnelManager.test.ts` REMOVED
|
|
29
|
+
from FLAKY_TESTS (re-armed) with a dated comment. No other entries touched.
|
|
30
|
+
|
|
31
|
+
## 2. Blast radius
|
|
32
|
+
|
|
33
|
+
- Zero production code changed — test + CI-config only.
|
|
34
|
+
- CI runs 51 additional deterministic tests (~0.8s, no network/timers/
|
|
35
|
+
processes). Risk is future-positive: tunnel-lifecycle regressions now fail
|
|
36
|
+
PRs instead of rotting behind the quarantine.
|
|
37
|
+
- Honest overlap note: the rewrite-era sibling suites
|
|
38
|
+
(tunnel-manager-rewrite.test.ts, tunnel-self-heal.test.ts,
|
|
39
|
+
tunnel-consent-state-machine.test.ts, tunnel-credential-rotation.test.ts —
|
|
40
|
+
39 tests, already in CI) cover parts of this surface. The new suite
|
|
41
|
+
consolidates manager-level coverage under the canonical filename and adds
|
|
42
|
+
edge paths they don't exercise (persisted-state restore + corrupted state
|
|
43
|
+
file, concurrent-start coalescing, start-after-exhaustion semantics, nonce
|
|
44
|
+
replay, cooldown suppression across construction, rotation-failure flag
|
|
45
|
+
retention, probe-handle release). Redundancy between them is cheap and
|
|
46
|
+
deliberate.
|
|
47
|
+
|
|
48
|
+
## 3. Test coverage
|
|
49
|
+
|
|
50
|
+
- New suite: 51/51 green across three consecutive runs (including under the
|
|
51
|
+
push config — proving the re-arm actually executes it).
|
|
52
|
+
- Full tunnel family (11 files incl. the 4 rewrite-era siblings): 165/165.
|
|
53
|
+
- `npx tsc --noEmit` clean.
|