mirror of
https://github.com/stablyai/orca.git
synced 2026-09-27 16:02:35 +00:00
fix(crash-reporting): record a pending minidump status so an unresolved pairing is visible
Crash report 8d4a8d01 (released v1.4.200, win32 renderer, exit -1) reached triage carrying no minidumpStatus at all. attachMinidumpSignature writes 'absent' or 'captured' on every path, but it runs asynchronously off recorded.then(...), and store.record persisted no value of its own. A session that ends before that chain settles therefore leaves the field missing -- which reads identically to a pairing that ran and found nothing, and is why that whole Windows crashed/-1 family cannot be told apart from a genuinely dumpless crash. Seed 'pending' at record time. attachDetails merges and is queued strictly after record's write resolves, so the terminal value still wins and no diagnostic recorded alongside it is dropped. Two tests, each pinning a different half. The first drives a capture that never settles and asserts the seed reaches the store, and fails when this change is reverted. The second runs the recorder against a real CrashReportStore on a tmpdir and asserts both that the seed resolves to 'absent' in the persisted report and that a sibling diagnostic survives -- it fails when attachDetails is mutated from a merge into a replace, which the first test cannot see because its store is a mock.
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { appMetricsMock } = vi.hoisted(() => ({
|
||||
@@ -16,6 +19,7 @@ import {
|
||||
getCrashBreadcrumbSnapshot,
|
||||
recordCrashBreadcrumb
|
||||
} from './crash-breadcrumb-store'
|
||||
import { CrashReportStore } from './crash-report-store'
|
||||
import { ProcessGoneDedupe } from './process-gone-dedupe'
|
||||
import { recordProcessGoneCrash, type ProcessGoneCrashEvent } from './process-gone-recorder'
|
||||
import { resetProcessGoneSiblingCorrelationForTest } from './process-gone-sibling-correlation'
|
||||
@@ -52,6 +56,7 @@ function event(overrides: Partial<ProcessGoneCrashEvent> = {}): ProcessGoneCrash
|
||||
}
|
||||
|
||||
let sink: CapturingSink
|
||||
const tempDirs: string[] = []
|
||||
|
||||
beforeEach(() => {
|
||||
sink = capturingSink()
|
||||
@@ -60,12 +65,13 @@ beforeEach(() => {
|
||||
resetProcessGoneSiblingCorrelationForTest()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
vi.useRealTimers()
|
||||
vi.restoreAllMocks()
|
||||
_resetTracerForTests()
|
||||
clearCrashBreadcrumbsForTest()
|
||||
resetProcessGoneSiblingCorrelationForTest()
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })))
|
||||
})
|
||||
|
||||
describe('recordProcessGoneCrash', () => {
|
||||
@@ -714,6 +720,43 @@ describe('minidump signature attachment', () => {
|
||||
expect(JSON.stringify(sink.records)).not.toContain('abc123')
|
||||
})
|
||||
|
||||
// Crash 8d4a8d01 (v1.4.200, win32) reached triage with no minidumpStatus at all, which reads
|
||||
// identically to "pairing ran and found nothing". Only the async attach below ever wrote the
|
||||
// field, so a session that ends first leaves it absent.
|
||||
it('records a pending minidump status so an unfinished pairing stays distinguishable', async () => {
|
||||
const record = vi.fn().mockResolvedValue({ id: 'report-1' })
|
||||
const neverSettles = () => new Promise<never>(() => {})
|
||||
|
||||
recordProcessGoneCrash(
|
||||
{ record, attachDetails } as never,
|
||||
event(),
|
||||
new ProcessGoneDedupe(),
|
||||
neverSettles
|
||||
)
|
||||
|
||||
await vi.waitFor(() => expect(record).toHaveBeenCalledOnce())
|
||||
expect(record.mock.calls[0]?.[0]).toMatchObject({
|
||||
details: expect.objectContaining({ minidumpStatus: 'pending' })
|
||||
})
|
||||
})
|
||||
|
||||
it('resolves the seeded pending status in the persisted report, keeping the diagnostics', async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'orca-process-gone-'))
|
||||
tempDirs.push(dir)
|
||||
const store = new CrashReportStore(path.join(dir, 'crash-reports.json'))
|
||||
|
||||
recordProcessGoneCrash(store, event(), new ProcessGoneDedupe(), noMinidump)
|
||||
|
||||
await vi.waitFor(async () => {
|
||||
const [persisted] = await store.listRecent()
|
||||
expect(persisted?.details.minidumpStatus).toBe('absent')
|
||||
})
|
||||
// Why: the seed only stays safe while attachDetails merges -- a replacing write would
|
||||
// resolve the status and silently drop every diagnostic recorded alongside it.
|
||||
const [persisted] = await store.listRecent()
|
||||
expect(persisted?.details.mainProcessPid).toBe(process.pid)
|
||||
})
|
||||
|
||||
it('marks the report when no dump was produced, so absence is visible', async () => {
|
||||
const record = vi.fn().mockResolvedValue({ id: 'report-1' })
|
||||
const attach = vi.fn().mockResolvedValue(null)
|
||||
|
||||
@@ -297,7 +297,10 @@ export function recordProcessGoneCrash(
|
||||
arch: process.arch,
|
||||
electronVersion: process.versions.electron ?? 'unknown',
|
||||
chromeVersion: process.versions.chrome ?? 'unknown',
|
||||
details: crashDetails,
|
||||
// Why: attachMinidumpSignature below is the only writer of minidumpStatus, so a session that
|
||||
// ends before it settles left the field absent -- indistinguishable from a paired dump that
|
||||
// found nothing. Seed the unresolved state so the report says which happened.
|
||||
details: { ...crashDetails, minidumpStatus: 'pending' },
|
||||
breadcrumbs: reportBreadcrumbs
|
||||
})
|
||||
trackRendererSiblingAttribution(
|
||||
|
||||
Reference in New Issue
Block a user