* feat(web): boot past failed optional client plugins The Web client boot audit now follows Host startup strictness. An inactive entry on the global required list (module system, Session transport, renderer, application frame, Session view, and sidebar) still rejects startup and the report marks the required entries. Other inactive entries produce one console warning and the application mounts without them. The boot page no longer draws a failed entry before the audit decides. * fix(web): audit boot entries whose creation rejects and name activation errors ClientEntries.start() now logs a row whose Loader creation rejects, such as a module that exports no plugin, and leaves it inactive for the boot audit, as the Host Loader group does. The audit awaits only failed fibers to report their recorded activation error. Notes, READMEs, and the Web client subsystem page describe the browser boot audit; tests cover invalid exports, carrier presentation of optional failures, and required names absent from the boot graph. * test(web): fail test carriers and e2e on inactive client entries The product boot only warns about inactive entries outside its required list, but tests must still surface every plugin that fails. TestClient and createClientTest now require every roster entry to activate unless a case passes allowInactiveEntries. The watchConsole tripwire, the real-host smoke, and the assembled jsdom boot treat a web boot warning as a failure with no opt-out. Docs: the Web README names Session control, notes that an entry without a fiber is reported as an import failure like the Host audit's "failed to import", and the startup strictness note lists the browser tests.
61 lines
2.6 KiB
TypeScript
61 lines
2.6 KiB
TypeScript
/** Test-scoped Remote mock and lazy client boot, owned by native Vitest fixtures. */
|
|
import { test, type TestAPI } from 'vitest'
|
|
import { RemoteMock, type MockedRemote } from '@deepseek-ai/dsh-remote-mock'
|
|
import type { AssemblyPlan } from './roster.ts'
|
|
import { remoteDefaultResponses } from './remote-default-responses.ts'
|
|
import { TestClient, type TestClientOptions } from './test-client.ts'
|
|
|
|
/** Per-test controls; configure the mock before awaiting `start()`. */
|
|
export interface ClientTestFixtures {
|
|
/** Fresh mock with the assembly's default responses already loaded. */
|
|
mock: RemoteMock
|
|
/** Namespace proxy backed by the same native mocks used by the Connection carrier. */
|
|
remote: MockedRemote
|
|
/** Await the one client owned by this test; rejects after the fixture closes. */
|
|
start: () => Promise<TestClient>
|
|
}
|
|
|
|
/**
|
|
* Extend Vitest with a fresh mock and a lazy, automatically disposed client. Repeated `start()` calls share one
|
|
* promise. Await it to observe startup failures; cleanup waits for startup but does not rethrow its rejection.
|
|
* Missing mock responses still fail teardown even when no client was started. Page globals remain environment-owned.
|
|
* @param plan - roster and replacement modules, shared as configuration rather than as a running client.
|
|
* @param options - mount, readiness, and strictness settings passed to `TestClient.start`.
|
|
* @returns Vitest's test function with test-scoped `mock` and `start` fixtures.
|
|
*/
|
|
export function createClientTest(plan: AssemblyPlan, options: TestClientOptions = {}): TestAPI<ClientTestFixtures> {
|
|
return test.extend<ClientTestFixtures>({
|
|
remote: async ({ mock }, use) => {
|
|
await use(mock.remote)
|
|
},
|
|
mock: async ({}, use) => {
|
|
const mock = RemoteMock.create().load(remoteDefaultResponses)
|
|
try {
|
|
await use(mock)
|
|
} finally {
|
|
mock.assertNoUnmatched()
|
|
}
|
|
},
|
|
start: async ({ mock }, use) => {
|
|
let pending: Promise<TestClient> | undefined
|
|
let closed = false
|
|
const start = (): Promise<TestClient> => {
|
|
if (closed) return Promise.reject(new Error('client-test-runtime: start() called after its test fixture closed'))
|
|
if (pending === undefined) {
|
|
pending = TestClient.start(plan, mock, options)
|
|
// Startup belongs to the caller; keep a rejection handler attached until teardown can await it.
|
|
void pending.catch(() => undefined)
|
|
}
|
|
return pending
|
|
}
|
|
try {
|
|
await use(start)
|
|
} finally {
|
|
closed = true
|
|
const client = await pending?.catch(() => undefined)
|
|
await client?.dispose()
|
|
}
|
|
},
|
|
})
|
|
}
|