also looking at this
feat: add declared VTOL recovery and landing routes
#80
13 files
+558
-52
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -491,6 +491,8 @@ jobs: | ||
| 491 | 491 | ${{ runner.temp }}/execution-browser-evidence/*.png |
| 492 | 492 | ${{ runner.temp }}/execution-browser-evidence/compiled.html |
| 493 | 493 | ${{ runner.temp }}/execution-browser-evidence/browser-launch.json |
| 494 | ${{ runner.temp }}/execution-browser-evidence/failure-diagnostics.json | |
| 495 | ${{ runner.temp }}/execution-browser-evidence/admission-renewal.json | |
| 494 | 496 | ${{ runner.temp }}/execution-browser-evidence/chrome-startup-stderr.log |
| 495 | 497 | purge-namespace: |
| 496 | 498 | runs-on: ubuntu-latest |
| @@ -21,6 +21,8 @@ const { executionWords: words } = await import('../../../resources/functions/dro | ||
| 21 | 21 | let holdArmed = false, holdUsed = false, held = false, releaseCredit: (() => void) | undefined |
| 22 | 22 | let delayStart = true, releaseStart: (() => void) | undefined |
| 23 | 23 | let streamStarts = 0, startRequests = 0, admissionRequests = 0, closeStream: (() => Promise<void>) | undefined |
| 24 | let closeWorkspaceStream: (() => Promise<void>) | undefined, releaseWorkspace: (() => void) | undefined | |
| 25 | let holdWorkspace = false, workspaceHeld = false | |
| 24 | 26 | |
| 25 | 27 | // Diagnostic state belongs only to this disposable fixture, never the execution controller. |
| 26 | 28 | type RouteClass = 'execution-start' | 'execution-close' | 'execution-admission' | 'execution-events' | 'execution-status' | 'workspace-read' |
| @@ -51,7 +53,7 @@ function diagnosticSnapshot() { | ||
| 51 | 53 | } |
| 52 | 54 | return { |
| 53 | 55 | available: true, pendingChecks: scalar(owner.pendingChecks), |
| 54 | fixture: { startRequests, admissionRequests, streamStarts, holdArmed, held, holdUsed }, lastStart, lastAdmission, responses: responses.slice(), | |
| 56 | fixture: { startRequests, admissionRequests, streamStarts, holdArmed, held, holdUsed, holdWorkspace, workspaceHeld }, lastStart, lastAdmission, responses: responses.slice(), | |
| 55 | 57 | slot: slot === null ? null : { |
| 56 | 58 | ordinal: ordinal(slot), |
| 57 | 59 | phase: choice(field(slot, 'phase'), ['waiting-for-consumer', 'running', 'stopping', 'completed', 'canceled', 'failed', 'discarded'] satisfies ExecutionView['phase'][]), |
| @@ -138,8 +140,15 @@ const control = async (request: Request) => { | ||
| 138 | 140 | assert(rows.some(row => row.digest !== seeded.source.digest)) |
| 139 | 141 | const committed = await fixture.snapshot(seeded.key) |
| 140 | 142 | assert.equal(committed.audits.filter(audit => audit.action === 'source-registered').length, 2) |
| 143 | holdWorkspace = true | |
| 144 | assert(closeWorkspaceStream, 'The real workspace stream must exist before renewal') | |
| 145 | await closeWorkspaceStream() | |
| 146 | const until = Date.now() + 5000 | |
| 147 | while (!workspaceHeld && Date.now() < until) await Bun.sleep(5) | |
| 148 | assert(workspaceHeld, 'Native workspace renewal must request a fresh authorized snapshot') | |
| 141 | 149 | return Response.json({ ok: true }) |
| 142 | 150 | } |
| 151 | if (action === 'release-workspace') { assert(workspaceHeld, 'Release only the held native renewal'); releaseWorkspace?.(); return Response.json({ ok: true }) } | |
| 143 | 152 | if (action === 'read-only') return Response.json({ token: await fixture.credential(fixture.sender.id, ['public-safety:read']) }) |
| 144 | 153 | if (action === 'revoke') { await fixture.tokens.revokeToken(fixture.senderToken); return Response.json({ ok: true }) } |
| 145 | 154 | return new Response(null, { status: 404 }) |
| @@ -153,15 +162,25 @@ route.use(async (request: Request, next: () => Promise<Response | null> | Respon | ||
| 153 | 162 | if (admissionOrdinal !== null) lastAdmission = { ordinal: admissionOrdinal, settled: false, status: null, elapsedMs: null } |
| 154 | 163 | let status: number | null = null |
| 155 | 164 | try { |
| 165 | if (kind === 'workspace-read' && holdWorkspace) { | |
| 166 | holdWorkspace = false; workspaceHeld = true | |
| 167 | await new Promise<void>(resolve => { | |
| 168 | const release = () => { workspaceHeld = false; resolve() } | |
| 169 | const timeout = setTimeout(release, 5000) | |
| 170 | releaseWorkspace = () => { clearTimeout(timeout); release() } | |
| 171 | }) | |
| 172 | } | |
| 156 | 173 | const response = await next() |
| 157 | 174 | status = response?.status ?? null |
| 158 | 175 | if (delayStart && request.method === 'POST' && pathname.endsWith('/workspace/execution') && response?.ok) { delayStart = false; await new Promise<void>(resolve => { releaseStart = resolve }) } |
| 159 | if (request.method !== 'GET' || !/\/workspace\/execution\/[a-f0-9-]+\/events$/.test(pathname) || !response?.ok || !response.body) return response | |
| 160 | streamStarts++ | |
| 176 | const executionEvents = /\/workspace\/execution\/[a-f0-9-]+\/events$/.test(pathname), workspaceEvents = pathname.endsWith('/workspace/events') | |
| 177 | if (request.method !== 'GET' || !(executionEvents || workspaceEvents) || !response?.ok || !response.body) return response | |
| 178 | if (executionEvents) streamStarts++ | |
| 161 | 179 | const reader = response.body.getReader() |
| 162 | 180 | let closed = false, output: ReadableStreamDefaultController<Uint8Array> | undefined |
| 163 | 181 | const close = async () => { if (closed) return; closed = true; output?.close(); await reader.cancel().catch(() => {}) } |
| 164 | closeStream = close | |
| 182 | if (executionEvents) closeStream = close | |
| 183 | else closeWorkspaceStream = close | |
| 165 | 184 | // Opaque native chunks pass unchanged. Only the private fixture can end a connection. |
| 166 | 185 | const body = new ReadableStream<Uint8Array>({ |
| 167 | 186 | start(controller) { output = controller }, |
| @@ -188,7 +207,7 @@ route.use(async (request: Request, next: () => Promise<Response | null> | Respon | ||
| 188 | 207 | }) |
| 189 | 208 | const page = () => new Response(html, { headers: { 'content-type': 'text/html', 'cache-control': 'no-store' } }) |
| 190 | 209 | route.get('/incidents', page) |
| 191 | for (const action of ['status', 'diagnostics', 'release-start', 'arm-before-return', 'before-return', 'release-and-renew', 'before-admission', 'verify-admitted', 'read-only', 'revoke']) route.get(`/__fixture/${action}`, control) | |
| 210 | for (const action of ['status', 'diagnostics', 'release-start', 'arm-before-return', 'before-return', 'release-and-renew', 'before-admission', 'verify-admitted', 'release-workspace', 'read-only', 'revoke']) route.get(`/__fixture/${action}`, control) | |
| 192 | 211 | const server = await route.serve({ hostname: '127.0.0.1', port: 0, nativeRoutes: false }) |
| 193 | 212 | if (process.env.EXECUTION_BROWSER_EVIDENCE) { await mkdir(process.env.EXECUTION_BROWSER_EVIDENCE, { recursive: true }); await writeFile(join(process.env.EXECUTION_BROWSER_EVIDENCE, 'compiled.html'), html) } |
| 194 | 213 | await writeFile(join(root, 'execution-credentials.json'), JSON.stringify({ url: `http://127.0.0.1:${server.port}`, key: seeded.key, sourceDigest: seeded.source.digest, token: fixture.senderToken, control: secret }), { mode: 0o600 }) |
| @@ -197,8 +216,8 @@ try { | ||
| 197 | 216 | assert.equal(await child.exited, 0, 'Isolated real execution browser driver must pass') |
| 198 | 217 | } |
| 199 | 218 | finally { |
| 200 | releaseStart?.(); releaseCredit?.() | |
| 201 | await closeStream?.(); await server.stop(true) | |
| 219 | releaseStart?.(); releaseCredit?.(); releaseWorkspace?.() | |
| 220 | await closeStream?.(); await closeWorkspaceStream?.(); await server.stop(true) | |
| 202 | 221 | const slot = executionOwner().slot as { execution: string, binding: { scope: string }, producer: string, worker: Worker | null } | null |
| 203 | 222 | if (slot?.producer === 'pending') { |
| 204 | 223 | const core = await import('../../../resources/functions/drone/public-safety/execution/service') |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | import assert from 'node:assert/strict' |
| 2 | import { mkdir, readFile } from 'node:fs/promises' | |
| 2 | import { mkdir, readFile, writeFile } from 'node:fs/promises' | |
| 3 | 3 | import { basename, join } from 'node:path' |
| 4 | 4 | import { startWorkspaceBrowser } from './workspace-browser' |
| 5 | 5 | import { settleExecutionDiagnostics } from './execution-diagnostics' |
| @@ -41,6 +41,7 @@ async function failureDiagnostics() { | ||
| 41 | 41 | workspaceStatus: selected('[data-workspace-status]', ['disconnected', 'connecting', 'synchronizing', 'current', 'reconnecting', 'refused']), |
| 42 | 42 | executionPhase: selected('[data-execution-phase]', ['no current run', 'waiting-for-consumer', 'running', 'stopping', 'completed', 'canceled', 'failed', 'discarded']), |
| 43 | 43 | hasExecutionId: !!document.querySelector('[data-execution-id]'), hasCompletion: !!document.querySelector('[data-execution-completion]'), |
| 44 | hasSelectedSource: !!document.querySelector('[data-workspace-source]'), | |
| 44 | 45 | startDisabled: start ? start.disabled === true : null, admitDisabled: admit ? admit.disabled === true : null, |
| 45 | 46 | hasSourceExists: !!document.querySelector('[data-execution-source-exists]'), hasExecutionError: !!document.querySelector('[data-execution-error]') |
| 46 | 47 | }; |
| @@ -53,7 +54,9 @@ async function failureDiagnostics() { | ||
| 53 | 54 | admissionRequests: diagnosticPage ? diagnosticPage.requests.filter(request => request.method === 'POST' && request.url.endsWith('/workspace/execution/admission')).length : null, |
| 54 | 55 | } |
| 55 | 56 | const text = JSON.stringify({ ...base, ...parts }) |
| 56 | console.error(Buffer.byteLength(text, 'utf8') <= 8192 ? text : JSON.stringify({ ...base, server: { available: false }, dom: null, byteBudgetExceeded: true })) | |
| 57 | const bounded = Buffer.byteLength(text, 'utf8') <= 8192 ? text : JSON.stringify({ ...base, server: { available: false }, dom: null, byteBudgetExceeded: true }) | |
| 58 | console.error(bounded) | |
| 59 | await writeFile(join(evidence, 'failure-diagnostics.json'), `${bounded}\n`, { mode: 0o600, flag: 'wx' }) | |
| 57 | 60 | } |
| 58 | 61 | catch { console.error(JSON.stringify({ label: 'execution-browser-failure-diagnostics', phase, available: false })) } |
| 59 | 62 | finally { |
| @@ -130,10 +133,32 @@ try { | ||
| 130 | 133 | await first.screenshot(join(evidence, 'execution-completed-mobile.png'), 375, 900) |
| 131 | 134 | assert.equal(await first.evaluate('Math.max(0, document.documentElement.scrollWidth - innerWidth)'), 0, 'Execution UUID and digest wrap at375px') |
| 132 | 135 | mark('explicit source admission') |
| 136 | await first.evaluate(`(() => { | |
| 137 | const probe = { samples: [], overflow: false }; | |
| 138 | const sample = () => { | |
| 139 | const value = document.querySelector('[data-workspace-source]')?.textContent.trim(); | |
| 140 | const selected = !value ? 'absent' : value === ${JSON.stringify(data.sourceDigest)} ? 'original' : 'other'; | |
| 141 | if (probe.samples.at(-1) === selected) return; | |
| 142 | if (probe.samples.length >= 64) { probe.overflow = true; return; } | |
| 143 | probe.samples.push(selected); | |
| 144 | }; | |
| 145 | const observer = new MutationObserver(sample); | |
| 146 | observer.observe(document.body, { childList: true, characterData: true, subtree: true }); | |
| 147 | sample(); globalThis.executionSourceProbe = { probe, stop: () => observer.disconnect() }; | |
| 148 | })()`) | |
| 133 | 149 | await first.click('Admit completed source') |
| 134 | 150 | await first.wait("!!document.querySelector('[data-execution-source-exists]')", 'Explicit admission must refresh actual source catalog', 15000) |
| 135 | 151 | await control('verify-admitted') |
| 136 | assert.equal(await first.evaluate("document.querySelector('[data-workspace-source]')?.textContent.trim()"), data.sourceDigest, 'Admission cannot select its new source') | |
| 152 | mark('fresh workspace after admission renewal') | |
| 153 | assert.equal(await first.evaluate("!!document.querySelector('[data-workspace-source]') || !!document.querySelector('[data-execution-completion]')"), false, 'Native renewal must clear private panes before fresh authorization') | |
| 154 | await control('release-workspace') | |
| 155 | await first.wait(`document.querySelector('[data-workspace-status]')?.textContent.trim() === 'current' && !!document.querySelector('[data-execution-source-exists]') && document.querySelector('[data-workspace-source]')?.textContent.trim() === ${JSON.stringify(data.sourceDigest)}`, 'Fresh authorization must restore the original selection after admission', 15000) | |
| 156 | const selections = await first.evaluate<{ samples: string[], overflow: boolean }>('(() => { globalThis.executionSourceProbe.stop(); return globalThis.executionSourceProbe.probe; })()') | |
| 157 | assert.equal(selections.overflow, false, 'The complete bounded selection history must be available') | |
| 158 | assert(selections.samples.includes('absent'), 'The actual native renewal must hide selected imagery') | |
| 159 | assert.equal(selections.samples.at(-1), 'original') | |
| 160 | assert(selections.samples.every(value => value === 'original' || value === 'absent'), 'Admission must never select the new source, including during renewal') | |
| 161 | await writeFile(join(evidence, 'admission-renewal.json'), `${JSON.stringify(selections)}\n`, { mode: 0o600, flag: 'wx' }) | |
| 137 | 162 | assert.equal(await first.evaluate("document.querySelector('#incident-note')?.value"), 'Later unsent observation survives execution success') |
| 138 | 163 | mark('read-only credential') |
| 139 | 164 | const { token } = await control('read-only') |