also looking at this
test: retain execution lifetime failure observations
#62
10 files
+174
-64
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.
| @@ -58,6 +58,19 @@ jobs: | ||
| 58 | 58 | env: |
| 59 | 59 | RUNTIME_HOST_FILE_LIMIT_EVIDENCE: ${{ runner.temp }}/runtime-host-file-limit-full-evidence |
| 60 | 60 | RUNTIME_MAINTENANCE_EVIDENCE: ${{ runner.temp }}/runtime-maintenance-full-evidence |
| 61 | EXECUTION_LIFETIME_EVIDENCE: ${{ runner.temp }}/execution-lifetime-failures | |
| 62 | - uses: actions/upload-artifact@v4 | |
| 63 | if: always() | |
| 64 | with: | |
| 65 | name: execution-lifetime-failure-evidence | |
| 66 | if-no-files-found: ignore | |
| 67 | retention-days: 7 | |
| 68 | path: | | |
| 69 | ${{ runner.temp }}/execution-lifetime-failures/*/fixture.stdout | |
| 70 | ${{ runner.temp }}/execution-lifetime-failures/*/fixture.stderr | |
| 71 | ${{ runner.temp }}/execution-lifetime-failures/*/lifecycle.json | |
| 72 | ${{ runner.temp }}/execution-lifetime-failures/*/access.sqlite* | |
| 73 | ${{ runner.temp }}/execution-lifetime-failures/*/bunfig.toml | |
| 61 | 74 | - uses: actions/upload-artifact@v4 |
| 62 | 75 | if: always() |
| 63 | 76 | with: |
| @@ -123,8 +123,13 @@ exit code and signal separately from whether its own watchdog fired or cleanup | ||
| 123 | 123 | requested a kill. Elapsed time includes process work and output handling, not |
| 124 | 124 | just a native check. Failed cases retain their private temporary directory, |
| 125 | 125 | stdout, stderr and lifecycle receipt when storage permits. A fixed warning |
| 126 | reports an unavailable receipt if that write fails. Successful cases remove | |
| 127 | their temporary files as before. | |
| 126 | identifies each unavailable evidence file if its write fails; this does not | |
| 127 | replace the captured child diagnostics or its original test verdict. Successful | |
| 128 | cases remove their temporary files as before. CI sets `EXECUTION_LIFETIME_EVIDENCE` | |
| 129 | to an owned runner directory and retains the failed synthetic database, process | |
| 130 | configuration and captured diagnostics for seven days, using explicit file | |
| 131 | patterns that exclude linked dependencies. No account tokens, scope identifiers or recording contents are included | |
| 132 | in the scalar phase and lifecycle observations. | |
| 128 | 133 | |
| 129 | 134 | These observations support [the unresolved Mac failure](https://github.com/stacksjs/drone/issues/60); |
| 130 | 135 | they do not establish its cause or repair it. The production authority-check |
| @@ -1,13 +1,20 @@ | ||
| 1 | 1 | import { expect, test } from 'bun:test' |
| 2 | import { mkdtemp, rm, writeFile } from 'node:fs/promises' | |
| 2 | import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' | |
| 3 | 3 | import { tmpdir } from 'node:os' |
| 4 | import { join } from 'node:path' | |
| 4 | import { join, resolve } from 'node:path' | |
| 5 | 5 | |
| 6 | 6 | for (const mode of ['completed', 'canceled', 'quarantine']) test(`native execution lifetime ${mode}`, async () => { |
| 7 | const root = await mkdtemp(join(tmpdir(), 'drone-incident-access-execution-lifetime-')) | |
| 7 | const evidence = process.env.EXECUTION_LIFETIME_EVIDENCE | |
| 8 | if (evidence) await mkdir(resolve(evidence), { recursive: true, mode: 0o700 }) | |
| 9 | const root = await mkdtemp(join(evidence ? resolve(evidence) : tmpdir(), 'drone-incident-access-execution-lifetime-')) | |
| 8 | 10 | let child: ReturnType<typeof Bun.spawn> | undefined |
| 9 | 11 | const began = performance.now() |
| 10 | 12 | let deadlineFired = false, cleanupKill = false, completed = false |
| 13 | let stdoutWritten = false, stderrWritten = false | |
| 14 | const retain = async (artifact: 'fixture.stdout' | 'fixture.stderr' | 'lifecycle.json', value: string) => { | |
| 15 | try { await writeFile(join(root, artifact), value, { mode: 0o600, flag: 'wx' }); return true } | |
| 16 | catch { console.error('LIFETIME_FIXTURE_EVIDENCE_UNAVAILABLE', JSON.stringify({ mode, artifact })); return false } | |
| 17 | } | |
| 11 | 18 | try { |
| 12 | 19 | const config = join(root, 'bunfig.toml'); await writeFile(config, 'preload=[]\n[install]\nauto="disable"\n') |
| 13 | 20 | const running = child = Bun.spawn([process.execPath, '--no-install', `--config=${config}`, '--no-env-file', `${import.meta.dir}/../fixtures/public-safety-access/execution-lifetime.ts`, mode], { cwd: root, env: { APP_ENV: 'test', APP_DEBUG: 'false', NODE_ENV: 'test', DB_CONNECTION: 'sqlite', DB_DATABASE_PATH: join(root, 'access.sqlite') }, stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' }) |
| @@ -19,10 +26,10 @@ for (const mode of ['completed', 'canceled', 'quarantine']) test(`native executi | ||
| 19 | 26 | }, 90000) |
| 20 | 27 | try { |
| 21 | 28 | const [code, stdout, stderr] = await Promise.all([running.exited, new Response(running.stdout).text(), new Response(running.stderr).text()]) |
| 22 | await writeFile(join(root, 'fixture.stdout'), stdout, { mode: 0o600 }) | |
| 23 | await writeFile(join(root, 'fixture.stderr'), stderr, { mode: 0o600 }) | |
| 24 | 29 | // eslint-disable-next-line no-console |
| 25 | 30 | for (const line of stdout.split('\n')) if (line.startsWith('{"oracle":')) console.log(line) |
| 31 | stdoutWritten = await retain('fixture.stdout', stdout) | |
| 32 | stderrWritten = await retain('fixture.stderr', stderr) | |
| 26 | 33 | expect(code, `${stdout}\n${stderr}`).toBe(0) |
| 27 | 34 | expect(stdout).toContain(`PASS native execution lifetime ${mode}`) |
| 28 | 35 | completed = true |
| @@ -31,10 +38,9 @@ for (const mode of ['completed', 'canceled', 'quarantine']) test(`native executi | ||
| 31 | 38 | } |
| 32 | 39 | finally { |
| 33 | 40 | if (child?.exitCode === null) { cleanupKill = true; child.kill('SIGKILL'); await child.exited } |
| 34 | const lifecycle = { mode, pid: child?.pid ?? null, exitCode: child?.exitCode ?? null, signal: child?.signalCode ?? null, timeoutMs: 90000, deadlineFired, cleanupKill, elapsedMs: performance.now() - began, completed } | |
| 41 | const lifecycle = { mode, pid: child?.pid ?? null, exitCode: child?.exitCode ?? null, signal: child?.signalCode ?? null, timeoutMs: 90000, deadlineFired, cleanupKill, elapsedMs: performance.now() - began, stdoutWritten, stderrWritten, completed } | |
| 35 | 42 | console.log(JSON.stringify({ oracle: 'lifetime-fixture-exit', ...lifecycle })) |
| 36 | try { await writeFile(join(root, 'lifecycle.json'), JSON.stringify(lifecycle, null, 2), { mode: 0o600 }) } | |
| 37 | catch { console.error('LIFETIME_FIXTURE_EVIDENCE_UNAVAILABLE', JSON.stringify({ mode })) } | |
| 43 | await retain('lifecycle.json', JSON.stringify(lifecycle, null, 2)) | |
| 38 | 44 | if (completed) await rm(root, { recursive: true, force: true }) |
| 39 | 45 | else console.error('LIFETIME_FIXTURE_RETAINED', root) |
| 40 | 46 | } |