mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
518 lines
21 KiB
TypeScript
518 lines
21 KiB
TypeScript
/**
|
|
* @vitest-environment jsdom
|
|
*
|
|
* Load-engine account: what `load` answers its caller (that answer is what the
|
|
* run orchestration reports to the host), Plugin Run convergence against live
|
|
* state, per-Plugin serialization, the three-step teardown, and each failing stage.
|
|
*
|
|
* The loader is stood in by real `ctx.plugin` fibers: entry creation must run the
|
|
* guarded surface as a genuine plugin, or neither activation gating nor the
|
|
* disposal cascade under test would be real.
|
|
*/
|
|
/* oxlint-disable typescript/no-unsafe-assignment -- Vitest asymmetric matchers are typed as any. */
|
|
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
import type { Loader } from '@deepseek-ai/cordis-plugin-loader'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import type {
|
|
CordisDynamicPackageId, CordisDynamicPluginId, CordisDynamicPluginRunId,
|
|
} from '@deepseek-ai/dsh-api-remotes/client'
|
|
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client'
|
|
import type { ClientModuleSystem } from '@deepseek-ai/dsh-client-modules/client'
|
|
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
|
|
import { DYNAMIC_CLIENT_REDIRECTS } from '../src/client/evaluator.ts'
|
|
import { DynamicCordisPackageRunner } from '../src/client/runtime.ts'
|
|
import type { DynamicCordisClientHalf, DynamicCordisRenderFailure } from '../src/client/runtime.ts'
|
|
|
|
const PLUGIN = 'dyn-1' as CordisDynamicPluginId
|
|
const PACKAGE = 'pkg-1' as CordisDynamicPackageId
|
|
const RUN = 'run-1' as CordisDynamicPluginRunId
|
|
const AGENT = 's-1' as SessionId
|
|
|
|
function runId(value: number): CordisDynamicPluginRunId {
|
|
return `run-${value}` as CordisDynamicPluginRunId
|
|
}
|
|
|
|
/** One browser half as the host hands it over. */
|
|
function half(overrides: Partial<DynamicCordisClientHalf> = {}): DynamicCordisClientHalf {
|
|
return {
|
|
pluginId: PLUGIN,
|
|
packageId: PACKAGE,
|
|
pluginRunId: RUN,
|
|
agentId: AGENT,
|
|
name: 'demo',
|
|
code: 'return { apply(ctx) {} }',
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
interface Bench {
|
|
ctx: Context
|
|
slots: SlotsService
|
|
runner: DynamicCordisPackageRunner
|
|
invalidated: string[]
|
|
removed: string[]
|
|
created: string[]
|
|
invoke: ReturnType<typeof vi.fn>
|
|
/** Render failures the runner sent upstream, in order. */
|
|
reported: {
|
|
agentId: SessionId
|
|
pluginId: CordisDynamicPluginId
|
|
pluginRunId: CordisDynamicPluginRunId
|
|
failure: DynamicCordisRenderFailure
|
|
}[]
|
|
/**
|
|
* Report one entry crash the way the renderer's boundary does: the runner
|
|
* subscribed through the supervision seam, and this calls what it registered.
|
|
*/
|
|
crash: (slot: string, entry: unknown, error: unknown, abdicated?: boolean) => void
|
|
/** Whether the runner released its subscription. */
|
|
watching: () => boolean
|
|
settle: () => Promise<void>
|
|
}
|
|
|
|
/**
|
|
* Terminate the awaitable fiber handle. The runner reads activation failure
|
|
* through `fiber.await()`; without a handler on the fiber itself, a deliberately
|
|
* failing package would also surface as an unhandled rejection.
|
|
*/
|
|
function seated<T>(fiber: T): T {
|
|
void Promise.resolve(fiber).catch(() => {})
|
|
return fiber
|
|
}
|
|
|
|
async function boot(): Promise<Bench> {
|
|
const ctx = new Context()
|
|
await ctx.plugin(SlotsService)
|
|
const invalidated: string[] = []
|
|
const removed: string[] = []
|
|
const created: string[] = []
|
|
const factories = new Map<string, () => unknown>()
|
|
const fibers = new Map<string, { fiber: unknown }>()
|
|
let next = 0
|
|
|
|
;(globalThis as { __ModuleLoader__?: unknown }).__ModuleLoader__ = {
|
|
load: (handoff: { id: string; factory: () => unknown }) => { factories.set(handoff.id, handoff.factory) },
|
|
}
|
|
const loader = {
|
|
create: (options: { name: string }) => {
|
|
created.push(options.name)
|
|
const factory = factories.get(options.name)
|
|
if (factory === undefined) throw new Error(`no factory for ${options.name}`)
|
|
const entryId = `entry-${++next}`
|
|
fibers.set(entryId, { fiber: seated(ctx.plugin(factory() as Parameters<Context['plugin']>[0])) })
|
|
return Promise.resolve(entryId)
|
|
},
|
|
resolve: (entryId: string) => fibers.get(entryId) ?? { fiber: undefined },
|
|
remove: async (entryId: string) => {
|
|
removed.push(entryId)
|
|
const entry = fibers.get(entryId)
|
|
fibers.delete(entryId)
|
|
await (entry?.fiber as { dispose(): Promise<void> } | undefined)?.dispose()
|
|
},
|
|
} as unknown as Loader
|
|
|
|
const invoke = vi.fn(() => Promise.resolve(null))
|
|
const reported: Bench['reported'] = []
|
|
// The crash seam is stood in so a test can report an entry failure without a
|
|
// React render, exactly as the renderer's boundary would; registrations still
|
|
// go through the real service, so the entries are real.
|
|
type EntryErrorListener = (slot: string, entry: unknown, error: unknown, info: { abdicated: boolean }) => void
|
|
let listener: EntryErrorListener | undefined
|
|
const runner = new DynamicCordisPackageRunner({
|
|
ctx,
|
|
loader,
|
|
modules: { invalidate: (id: string) => { invalidated.push(id) } } as unknown as ClientModuleSystem,
|
|
slots: {
|
|
onEntryError: (fn: EntryErrorListener) => {
|
|
listener = fn
|
|
return () => { listener = undefined }
|
|
},
|
|
} as unknown as SlotsService,
|
|
invoke,
|
|
reportGuardFailure: () => {},
|
|
reportRenderFailure: (agentId, pluginId, pluginRunId, failure) => {
|
|
reported.push({ agentId, pluginId, pluginRunId, failure })
|
|
},
|
|
})
|
|
return {
|
|
ctx,
|
|
slots: ctx.slots,
|
|
runner,
|
|
invalidated,
|
|
removed,
|
|
created,
|
|
invoke,
|
|
reported,
|
|
crash: (slot, entry, error, abdicated = true) => {
|
|
if (listener === undefined) throw new Error('the runner is not watching the crash seam')
|
|
listener(slot, entry, error, { abdicated })
|
|
},
|
|
watching: () => listener !== undefined,
|
|
settle: async () => { await new Promise((resolve) => { setTimeout(resolve, 0) }) },
|
|
}
|
|
}
|
|
|
|
describe('load', () => {
|
|
it('mounts a browser half through the module table and the loader, then answers active', async () => {
|
|
const bench = await boot()
|
|
await expect(bench.runner.load(half())).resolves.toEqual({ ok: true, pluginRunId: RUN })
|
|
expect(bench.invalidated).toEqual(['dyn/dyn-1'])
|
|
expect(bench.created).toEqual(['dyn/dyn-1'])
|
|
expect(bench.runner.isLoaded(PLUGIN)).toBe(true)
|
|
expect(bench.runner.getSnapshot()).toEqual([
|
|
{ pluginId: PLUGIN, packageId: PACKAGE, pluginRunId: RUN, name: 'demo', slots: [], styleCount: 0 },
|
|
])
|
|
})
|
|
|
|
it('projects the contributions the package made', async () => {
|
|
const bench = await boot()
|
|
await bench.runner.load(half({
|
|
code: `return {
|
|
inject: ['slots'],
|
|
apply(ctx) {
|
|
styles.insert('.x {}')
|
|
ctx.slots.register({ name: 'root' }, () => null)
|
|
},
|
|
}`,
|
|
}))
|
|
expect(bench.runner.getSnapshot()).toEqual([
|
|
{ pluginId: PLUGIN, packageId: PACKAGE, pluginRunId: RUN, name: 'demo', slots: ['root'], styleCount: 1 },
|
|
])
|
|
})
|
|
|
|
it('answers from live state when the revision is already loaded here', async () => {
|
|
const bench = await boot()
|
|
await bench.runner.load(half())
|
|
// A replayed run must not look unacknowledged, and must not reload.
|
|
await expect(bench.runner.load(half())).resolves.toEqual({ ok: true, pluginRunId: RUN })
|
|
expect(bench.created).toEqual(['dyn/dyn-1'])
|
|
expect(bench.runner.isLoaded(PLUGIN)).toBe(true)
|
|
})
|
|
|
|
it('replays the parked services a live package still waits for', async () => {
|
|
const bench = await boot()
|
|
const parked = half({ code: "return { inject: ['absent'], apply() {} }" })
|
|
await expect(bench.runner.load(parked)).resolves.toEqual({ ok: true, pluginRunId: RUN, waitingFor: ['absent'] })
|
|
await expect(bench.runner.load(parked)).resolves.toEqual({ ok: true, pluginRunId: RUN, waitingFor: ['absent'] })
|
|
expect(bench.created).toEqual(['dyn/dyn-1'])
|
|
})
|
|
|
|
it('replaces a live load when a newer revision arrives', async () => {
|
|
const bench = await boot()
|
|
await bench.runner.load(half())
|
|
await expect(bench.runner.load(half({ pluginRunId: runId(2) }))).resolves.toEqual({ ok: true, pluginRunId: runId(2) })
|
|
expect(bench.removed).toEqual(['entry-1'])
|
|
expect(bench.invalidated).toEqual(['dyn/dyn-1', 'dyn/dyn-1', 'dyn/dyn-1'])
|
|
expect(bench.created).toEqual(['dyn/dyn-1', 'dyn/dyn-1'])
|
|
expect(bench.runner.getSnapshot()[0]?.pluginRunId).toBe(runId(2))
|
|
})
|
|
|
|
it('loads the function form, which declares no services', async () => {
|
|
const bench = await boot()
|
|
await expect(bench.runner.load(half({ code: 'return (ctx) => { globalThis.__dynFnForm = true }' })))
|
|
.resolves.toEqual({ ok: true, pluginRunId: RUN })
|
|
expect((globalThis as { __dynFnForm?: boolean }).__dynFnForm).toBe(true)
|
|
delete (globalThis as { __dynFnForm?: boolean }).__dynFnForm
|
|
})
|
|
|
|
it('serializes operations of one package id', async () => {
|
|
const bench = await boot()
|
|
const first = bench.runner.load(half())
|
|
const second = bench.runner.load(half({ pluginRunId: runId(2) }))
|
|
await expect(first).resolves.toEqual({ ok: true, pluginRunId: RUN })
|
|
await expect(second).resolves.toEqual({ ok: true, pluginRunId: runId(2) })
|
|
expect(bench.created).toEqual(['dyn/dyn-1', 'dyn/dyn-1'])
|
|
})
|
|
|
|
it('keeps the queue usable after a failed operation', async () => {
|
|
const bench = await boot()
|
|
const sink = (globalThis as { __ModuleLoader__?: unknown }).__ModuleLoader__
|
|
delete (globalThis as { __ModuleLoader__?: unknown }).__ModuleLoader__
|
|
await expect(bench.runner.load(half())).rejects.toThrow(/__ModuleLoader__ is missing/)
|
|
;(globalThis as { __ModuleLoader__?: unknown }).__ModuleLoader__ = sink
|
|
await expect(bench.runner.load(half())).resolves.toEqual({ ok: true, pluginRunId: RUN })
|
|
})
|
|
})
|
|
|
|
describe('failure stages', () => {
|
|
it('classifies a closure that will not evaluate, and leaves no styles behind', async () => {
|
|
const bench = await boot()
|
|
await expect(bench.runner.load(half({ code: 'styles.insert(".leak {}"); return 42' }))).resolves.toEqual({
|
|
ok: false,
|
|
cause: 'evaluate',
|
|
message: expect.stringContaining('must `return` a plugin') as string,
|
|
stack: expect.any(String),
|
|
error: expect.any(Error),
|
|
})
|
|
const leaked = [...document.querySelectorAll('style[data-dyn="dyn-1"]')]
|
|
.filter(tag => tag.textContent === '.leak {}')
|
|
expect(leaked).toHaveLength(0)
|
|
expect(bench.created).toEqual([])
|
|
})
|
|
|
|
it('classifies an apply that throws, and tears the entry down', async () => {
|
|
const bench = await boot()
|
|
await expect(bench.runner.load(half({ code: 'return { apply() { throw new Error("apply exploded") } }' })))
|
|
.resolves.toEqual({
|
|
ok: false,
|
|
cause: 'activate',
|
|
message: 'apply exploded',
|
|
stack: expect.any(String),
|
|
error: expect.any(Error),
|
|
})
|
|
expect(bench.removed).toEqual(['entry-1'])
|
|
expect(bench.runner.isLoaded(PLUGIN)).toBe(false)
|
|
})
|
|
|
|
it('stringifies a closure that rejects with a non-Error value', async () => {
|
|
const bench = await boot()
|
|
await expect(bench.runner.load(half({ code: 'throw "raw rejection"' })))
|
|
.resolves.toEqual({ ok: false, cause: 'evaluate', message: 'raw rejection', error: 'raw rejection' })
|
|
})
|
|
|
|
it('classifies a loader entry that produced no fiber', async () => {
|
|
const bench = await boot()
|
|
const env = bench.runner as unknown as { env: { loader: { resolve: (id: string) => unknown } } }
|
|
vi.spyOn(env.env.loader, 'resolve').mockReturnValue({ fiber: undefined })
|
|
await expect(bench.runner.load(half())).resolves.toEqual({
|
|
ok: false,
|
|
cause: 'module-import',
|
|
message: 'module import failed (see the browser console)',
|
|
})
|
|
vi.restoreAllMocks()
|
|
expect(bench.removed).toEqual(['entry-1'])
|
|
})
|
|
|
|
it('mirrors a loaded package runtime error to the console without unloading it', async () => {
|
|
const bench = await boot()
|
|
const logged = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
await bench.runner.load(half({
|
|
code: 'return { apply: (ctx) => { ctx.on("t/ping", () => console.error("after load")) } }',
|
|
}))
|
|
;(bench.ctx.emit as (type: string) => void)('t/ping')
|
|
const mirrored = logged.mock.calls.filter(call => String(call[0]).includes('logged an error'))
|
|
logged.mockRestore()
|
|
expect(mirrored).toHaveLength(1)
|
|
expect(bench.runner.isLoaded(PLUGIN)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('retract', () => {
|
|
it('unloads at the named revision', async () => {
|
|
const bench = await boot()
|
|
await bench.runner.load(half())
|
|
bench.runner.retract(PLUGIN, RUN)
|
|
await bench.settle()
|
|
expect(bench.removed).toEqual(['entry-1'])
|
|
expect(bench.invalidated).toEqual(['dyn/dyn-1', 'dyn/dyn-1'])
|
|
expect(bench.runner.isLoaded(PLUGIN)).toBe(false)
|
|
})
|
|
|
|
it('ignores a retract of a superseded revision', async () => {
|
|
const bench = await boot()
|
|
await bench.runner.load(half({ pluginRunId: runId(3) }))
|
|
bench.runner.retract(PLUGIN, runId(2))
|
|
await bench.settle()
|
|
expect(bench.runner.isLoaded(PLUGIN)).toBe(true)
|
|
})
|
|
|
|
it('ignores a retract of a package this page never loaded', async () => {
|
|
const bench = await boot()
|
|
bench.runner.retract(PLUGIN, RUN)
|
|
await bench.settle()
|
|
expect(bench.removed).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('observation and disposal', () => {
|
|
it('notifies subscribers and re-derives the snapshot after each convergence', async () => {
|
|
const bench = await boot()
|
|
let notified = 0
|
|
const unsubscribe = bench.runner.subscribe(() => { notified++ })
|
|
const empty = bench.runner.getSnapshot()
|
|
expect(bench.runner.getSnapshot()).toBe(empty) // stable between mutations
|
|
await bench.runner.load(half())
|
|
expect(notified).toBe(1)
|
|
expect(bench.runner.getSnapshot()).not.toBe(empty)
|
|
unsubscribe()
|
|
bench.runner.retract(PLUGIN, RUN)
|
|
await bench.settle()
|
|
expect(notified).toBe(1)
|
|
})
|
|
|
|
it('unloads every live package on disposal', async () => {
|
|
const bench = await boot()
|
|
await bench.runner.load(half())
|
|
await bench.runner.dispose()
|
|
expect(bench.removed).toEqual(['entry-1'])
|
|
expect(bench.runner.getSnapshot()).toEqual([])
|
|
expect(bench.slots.entries('root')).toHaveLength(0)
|
|
})
|
|
|
|
it('routes host.call through the invoke seam it was given', async () => {
|
|
const bench = await boot()
|
|
await bench.runner.load(half({ code: 'return { apply: () => host.call("ping", 1) }' }))
|
|
expect(bench.invoke).toHaveBeenCalledWith(PLUGIN, RUN, 'ping', 1)
|
|
})
|
|
})
|
|
|
|
describe('render failures', () => {
|
|
/** A package that seats one component in `root`, so a crash has something to name. */
|
|
const CONTRIBUTOR = `return {
|
|
inject: ['slots'],
|
|
apply(ctx) { ctx.slots.register({ name: 'root' }, () => null) },
|
|
}`
|
|
|
|
it('reports a crash of an entry it seated, under the session the run was for', async () => {
|
|
const bench = await boot()
|
|
await bench.runner.load(half({ code: CONTRIBUTOR }))
|
|
const [entry] = bench.slots.entries('root')
|
|
bench.crash('root', entry, new Error('Cannot read properties of undefined'))
|
|
expect(bench.reported).toEqual([{
|
|
agentId: AGENT,
|
|
pluginId: PLUGIN,
|
|
pluginRunId: RUN,
|
|
failure: {
|
|
slot: 'root',
|
|
message: 'your entry in slot "root" crashed while React rendered it: Cannot read properties of undefined',
|
|
stack: expect.any(String),
|
|
abdicated: true,
|
|
},
|
|
}])
|
|
})
|
|
|
|
it('carries the retirement bit as the seam reported it', async () => {
|
|
const bench = await boot()
|
|
await bench.runner.load(half({ code: CONTRIBUTOR }))
|
|
const [entry] = bench.slots.entries('root')
|
|
// A chain crash keeps its cell: the package's UI is broken, not gone, and the
|
|
// author needs to be able to tell those apart.
|
|
bench.crash('root', entry, new Error('boom'), false)
|
|
expect(bench.reported[0]?.failure.abdicated).toBe(false)
|
|
})
|
|
|
|
it('ignores a crash of an entry no dynamic package seated', async () => {
|
|
const bench = await boot()
|
|
await bench.runner.load(half({ code: CONTRIBUTOR }))
|
|
// Factory UI crashing is not this runner's business, and neither is an entry
|
|
// whose component cannot even be indexed by identity.
|
|
bench.crash('root', { component: () => null }, new Error('boom'))
|
|
bench.crash('root', { component: 'not-a-component' }, new Error('boom'))
|
|
bench.crash('root', { component: null }, new Error('boom'))
|
|
expect(bench.reported).toEqual([])
|
|
})
|
|
|
|
it('seats a package that registers an unindexable component without claiming it', async () => {
|
|
const bench = await boot()
|
|
// A component that is not an object has no identity to key ownership on; the
|
|
// registration still stands, and a crash on it simply goes unattributed.
|
|
await expect(bench.runner.load(half({
|
|
code: `return {
|
|
inject: ['slots'],
|
|
apply(ctx) {
|
|
ctx.slots.register({ name: 'root' }, 'not-a-component')
|
|
ctx.slots.register({ name: 'root' }, null)
|
|
},
|
|
}`,
|
|
}))).resolves.toEqual({ ok: true, pluginRunId: RUN })
|
|
for (const entry of bench.slots.entries('root')) bench.crash('root', entry, new Error('boom'))
|
|
expect(bench.reported).toEqual([])
|
|
})
|
|
|
|
it('appends the redirect a bare crash text is missing, and never twice', async () => {
|
|
const bench = await boot()
|
|
await bench.runner.load(half({ code: CONTRIBUTOR }))
|
|
const [entry] = bench.slots.entries('root')
|
|
// Reaching the global around the closure trap (window.setInterval) crashes
|
|
// with the engine's own text, which teaches nothing on its own.
|
|
bench.crash('root', entry, new TypeError('window.setInterval is not a function'))
|
|
const bare = bench.reported[0]?.failure.message ?? ''
|
|
expect(bare).toMatch(/is not a function\n/)
|
|
const timerRedirect = DYNAMIC_CLIENT_REDIRECTS.setInterval
|
|
if (timerRedirect === undefined) throw new Error('setInterval redirect is missing')
|
|
expect(bare).toContain(timerRedirect)
|
|
// The trap's own error already carries that sentence: appending it again
|
|
// would make the model read the same paragraph twice.
|
|
bench.crash('root', entry, new Error(
|
|
`setInterval is not available in a dynamic client half — ${timerRedirect}`,
|
|
))
|
|
const trapped = bench.reported[1]?.failure.message ?? ''
|
|
expect(trapped.indexOf(timerRedirect)).toBe(trapped.lastIndexOf(timerRedirect))
|
|
})
|
|
|
|
it('stops watching the seam when the engine is disposed', async () => {
|
|
const bench = await boot()
|
|
await bench.runner.load(half({ code: CONTRIBUTOR }))
|
|
expect(bench.watching()).toBe(true)
|
|
await bench.runner.dispose()
|
|
expect(bench.watching()).toBe(false)
|
|
})
|
|
|
|
it('publishes the crash on the live set\'s own notification channel', async () => {
|
|
const bench = await boot()
|
|
await bench.runner.load(half({ code: CONTRIBUTOR }))
|
|
let notified = 0
|
|
let alsoNotified = 0
|
|
const unsubscribe = bench.runner.subscribe(() => { notified++ })
|
|
const unobserve = bench.runner.renderFailures.subscribe(() => { alsoNotified++ })
|
|
const empty = bench.runner.renderFailures.getSnapshot()
|
|
expect(bench.runner.renderFailures.getSnapshot()).toBe(empty) // stable between mutations
|
|
const [entry] = bench.slots.entries('root')
|
|
bench.crash('root', entry, new Error('boom'), false)
|
|
// A surface already subscribed for load changes learns about a crash too: one
|
|
// channel, two derived snapshots — and the observable's own subscribe is that
|
|
// same channel, so a surface may take either handle.
|
|
expect(notified).toBe(1)
|
|
expect(alsoNotified).toBe(1)
|
|
const published = bench.runner.renderFailures.getSnapshot().get(PLUGIN)
|
|
expect(published?.slot).toBe('root')
|
|
expect(published?.abdicated).toBe(false)
|
|
expect(published?.message).toMatch(/boom/)
|
|
unsubscribe()
|
|
unobserve()
|
|
})
|
|
|
|
it('keeps only the latest crash per package', async () => {
|
|
const bench = await boot()
|
|
await bench.runner.load(half({ code: CONTRIBUTOR }))
|
|
const [entry] = bench.slots.entries('root')
|
|
bench.crash('root', entry, new Error('first'))
|
|
bench.crash('root', entry, new Error('second'))
|
|
expect(bench.runner.renderFailures.getSnapshot().size).toBe(1)
|
|
expect(bench.runner.renderFailures.getSnapshot().get(PLUGIN)?.message).toMatch(/second/)
|
|
})
|
|
|
|
it('clears the crash when the package is retracted', async () => {
|
|
const bench = await boot()
|
|
await bench.runner.load(half({ code: CONTRIBUTOR }))
|
|
const [entry] = bench.slots.entries('root')
|
|
bench.crash('root', entry, new Error('boom'))
|
|
bench.runner.retract(PLUGIN, RUN)
|
|
await bench.settle()
|
|
// A row must never show a failure of something that no longer renders here.
|
|
expect(bench.runner.renderFailures.getSnapshot().size).toBe(0)
|
|
})
|
|
|
|
it('clears the crash when the package loads again', async () => {
|
|
const bench = await boot()
|
|
await bench.runner.load(half({ code: CONTRIBUTOR }))
|
|
const [entry] = bench.slots.entries('root')
|
|
bench.crash('root', entry, new Error('boom'))
|
|
expect(bench.runner.renderFailures.getSnapshot().size).toBe(1)
|
|
await bench.runner.load(half({ code: CONTRIBUTOR, pluginRunId: runId(2) }))
|
|
expect(bench.runner.renderFailures.getSnapshot().size).toBe(0)
|
|
})
|
|
|
|
it('keeps the crash when a replayed run loads nothing', async () => {
|
|
const bench = await boot()
|
|
await bench.runner.load(half({ code: CONTRIBUTOR }))
|
|
const [entry] = bench.slots.entries('root')
|
|
bench.crash('root', entry, new Error('boom'))
|
|
// Same revision: nothing was re-run, so the failure the page is showing is
|
|
// still true of what is mounted.
|
|
await bench.runner.load(half({ code: CONTRIBUTOR }))
|
|
expect(bench.runner.renderFailures.getSnapshot().size).toBe(1)
|
|
})
|
|
})
|