// @vitest-environment jsdom import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import { afterEach, describe, expect, it, vi } from 'vitest' import { apply as provideModules, createClientModuleSystem } from '../src/client/index.ts' import type { ClientBundleRegistration, ClientModuleLoaderTarget, WebBootEntry, WebBootGraph } from '../src/client/index.ts' const contexts: Context[] = [] afterEach(async () => { for (const ctx of contexts.splice(0)) { await ctx.fiber.dispose() await ctx.fiber.await() } document.head.querySelectorAll('style').forEach((el) => { el.remove() }) document.body.replaceChildren() vi.restoreAllMocks() }) const row = (id: string, rev = 'r0', extra: Partial = {}): WebBootEntry => ({ id, rev, url: `/plugins/??${id}/client.js&rev=${rev}`, ...extra, }) const graph = (...entries: WebBootEntry[]): WebBootGraph => ({ rev: JSON.stringify(entries), entries, batches: entries.length === 0 ? [] : [{ phase: 'application', url: '/batch', rev: 'batch', entries: entries.map(row => row.id) }], }) const deferred = () => { let resolve!: () => void const promise = new Promise((done) => { resolve = done }) return { promise, resolve } } async function bench(initial: WebBootGraph, factories: Record, start = true) { const ctx = new Context() contexts.push(ctx) const fetched: string[] = [] const target: ClientModuleLoaderTarget = { mode: 'queue', pendingQueue: [], load: () => {}, create: options => createClientModuleSystem(target, { id: 'bootstrap', exports: { inject: ['loader'], apply: provideModules } }, options), } let arrival: (url: string) => Promise = async () => {} const modules = target.create({ boot: initial, staticModules: {}, loadBundle: async (url) => { fetched.push(url) const ids = url === '/batch' ? initial.entries.map(row => row.id).filter(id => id !== 'bootstrap') : [url.split('??')[1]!.split('/client.js')[0]!] const registrations = ids.map(id => ({ id, factory: factories[id]! })) await arrival(url) for (const registration of registrations) target.load(registration) }, }) await ctx.plugin(Loader) ctx.loader.internal = modules as never if (start) await modules.entries.start(ctx.loader, modules.manifest) return { ctx, modules, fetched, target, arrival: (fn: typeof arrival) => { arrival = fn } } } /** A visible plugin whose style and listener are owned by its factory and fiber respectively. */ function visible(id: string, effects: { mounted: number; disposed: number; hits: number }, cleanup?: () => Promise) { return () => { const style = document.createElement('style') style.dataset.plugin = id style.textContent = `[data-live="${id}"] { color: rgb(12, 34, 56); }` document.head.append(style) return { apply(ctx: Context) { ctx.effect(() => { effects.mounted++ const el = document.createElement('div') el.dataset.live = id document.body.append(el) const listener = () => { effects.hits++ } window.addEventListener('live-test', listener) return async () => { window.removeEventListener('live-test', listener) el.remove() await cleanup?.() effects.disposed++ } }) } } } } describe('client manifest entries', () => { it('adds, drains removal and re-enables one instance with styles; unrelated entries survive', async () => { const effects = { mounted: 0, disposed: 0, hits: 0 } const cleanup = deferred() const b = await bench(graph(), { pet: visible('pet', effects, () => cleanup.promise) }) b.target.load({ id: 'local', factory: () => ({ apply() {} }) }) const localId = await b.ctx.loader.create({ name: 'local' }) await b.modules.entries.sync(graph(row('pet'))) expect(b.fetched).toEqual([row('pet').url]) expect(document.querySelectorAll('[data-live=pet]')).toHaveLength(1) expect(document.querySelectorAll('style[data-plugin=pet]')).toHaveLength(1) window.dispatchEvent(new Event('live-test')) expect(effects.hits).toBe(1) const removing = b.modules.entries.sync(graph()) await vi.waitFor(() => { expect(document.querySelector('[data-live=pet]')).toBeNull() }) const readding = b.modules.entries.sync(graph(row('pet'))) expect(effects.disposed).toBe(0) expect(effects.mounted).toBe(1) cleanup.resolve() await Promise.all([removing, readding]) expect(effects).toEqual({ mounted: 2, disposed: 1, hits: 1 }) expect(document.querySelectorAll('style[data-plugin=pet]')).toHaveLength(1) expect(b.ctx.loader.resolve(localId).fiber?.state).toBe(2) await b.modules.entries.sync(graph()) window.dispatchEvent(new Event('live-test')) expect(effects).toEqual({ mounted: 2, disposed: 2, hits: 1 }) expect(document.querySelectorAll('style[data-plugin=pet]')).toHaveLength(0) expect(b.modules.loadCache.has('pet')).toBe(false) expect(b.modules.entries.state.getSnapshot()).toEqual({ syncing: false, failures: [] }) }) it('does not mount an obsolete download and can load it again later', async () => { const effects = { mounted: 0, disposed: 0, hits: 0 } const b = await bench(graph(), { pet: visible('pet', effects) }) const arrival = deferred() const started = deferred() b.arrival(async () => { started.resolve(); await arrival.promise }) const enabling = b.modules.entries.sync(graph(row('pet'))) await started.promise const disabling = b.modules.entries.sync(graph()) arrival.resolve() await Promise.all([enabling, disabling]) expect(effects.mounted).toBe(0) expect(b.modules.loadCache.has('pet')).toBe(false) await b.modules.entries.sync(graph(row('pet'))) expect(effects.mounted).toBe(1) expect(b.fetched).toEqual([row('pet').url, row('pet').url]) }) it('registers dynamic dependencies first and retains one still required by an unmanaged entry', async () => { const materialized = vi.fn<(id: string) => void>() const b = await bench(graph(row('existing')), { existing: () => { materialized('existing'); return { apply() {} } }, dependency: () => { materialized('dependency'); return { apply() {}, value: 42 } }, consumer: (require) => { materialized('consumer') expect(require('dependency/client')).toHaveProperty('value', 42) return { apply() {} } }, }) await b.modules.entries.sync(graph(row('existing'), row('consumer', 'r0', { external: ['dependency/client'] }), row('dependency'))) expect(b.fetched.slice(1)).toEqual([row('dependency').url, row('consumer').url]) expect(materialized.mock.calls.map(([id]) => id)).toEqual(['existing', 'consumer', 'dependency']) b.target.load({ id: 'local', factory: require => ({ apply() {}, dep: require('dependency/client') }) }) const localId = await b.ctx.loader.create({ name: 'local' }) await b.modules.entries.sync(graph(row('existing'))) expect(b.modules.loadCache.has('consumer')).toBe(false) expect(b.modules.loadCache.has('dependency')).toBe(true) b.ctx.loader.remove(localId) await b.modules.entries.retry() expect(b.modules.loadCache.has('dependency')).toBe(false) expect(materialized.mock.calls.filter(([id]) => id === 'existing')).toHaveLength(1) }) it('diagnoses download and apply failures, retries an identical graph and leaves healthy plugins running', async () => { const effects = { mounted: 0, disposed: 0, hits: 0 } let broken = true const b = await bench(graph(row('healthy')), { healthy: visible('healthy', effects), bad: () => ({ apply() { if (broken) throw new Error('apply unavailable') } }), }) b.arrival(async () => { throw new Error('download unavailable') }) await b.modules.entries.sync(graph(row('healthy'), row('bad'))) expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('download unavailable') b.arrival(async () => {}) await b.modules.entries.retry() expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('apply unavailable') broken = false await b.modules.entries.retry() expect(b.modules.entries.state.getSnapshot().failures).toEqual([]) expect(effects.mounted).toBe(1) expect(effects.disposed).toBe(0) }) it('deduplicates rebuilt and graph revisions and retries after failed code arrival', async () => { const effects = { mounted: 0, disposed: 0, hits: 0 } const b = await bench(graph(row('pet')), { pet: visible('pet', effects) }) await b.modules.entries.reload('pet', 'r1') await b.modules.entries.sync(graph(row('pet', 'r1'))) expect(effects).toEqual({ mounted: 2, disposed: 1, hits: 0 }) b.arrival(async () => { throw new Error('offline') }) await expect(b.modules.entries.reload('pet', 'r2')).rejects.toThrow('offline') expect(effects.mounted).toBe(2) b.arrival(async () => {}) await b.modules.entries.retry() expect(effects).toEqual({ mounted: 3, disposed: 2, hits: 0 }) expect(document.querySelectorAll('style[data-plugin=pet]')).toHaveLength(1) }) it('rejects malformed graphs before mutating active entries', async () => { const b = await bench(graph(row('a')), { a: () => ({ apply() {} }) }) expect(() => b.modules.entries.sync({ rev: 'bad', entries: [{ id: 'a' }], batches: [] })).toThrow('string id/url/rev') expect([...b.ctx.loader.entries()].map(entry => entry.options.name)).toEqual(['a']) expect(() => b.modules.entries.start(b.ctx.loader, b.modules.manifest)).toThrow('already started') }) }) it('publishes stable local snapshots and contains a failing subscriber', async () => { const b = await bench(graph(), {}) const error = vi.spyOn(console, 'error').mockImplementation(() => {}) const listener = vi.fn() const removeBad = b.modules.entries.state.subscribe(() => { throw new Error('subscriber') }) const remove = b.modules.entries.state.subscribe(listener) expect(b.modules.entries.state.getSnapshot()).toBe(b.modules.entries.state.getSnapshot()) await b.modules.entries.retry() expect(listener).toHaveBeenCalledTimes(2) expect(error).toHaveBeenCalled() removeBad() remove() await b.modules.entries.retry() expect(listener).toHaveBeenCalledTimes(2) }) it('ignores superseded queued snapshots and rebuilds of absent or unchanged entries', async () => { const b = await bench(graph(), { a: () => ({ apply() {} }) }) const first = b.modules.entries.sync(graph(row('a'))) const second = b.modules.entries.sync(graph()) await Promise.all([first, second]) expect(b.fetched).toEqual([]) await b.modules.entries.reload('a', 'r1') await b.modules.entries.sync(graph(row('a'))) await b.modules.entries.reload('a', 'r0') await b.ctx.fiber.dispose() await b.modules.entries.reload('a', 'r2') await b.modules.entries.sync(graph()) expect(b.fetched).toHaveLength(1) }) it('reports reconciliation before startup and missing service activation without losing later retries', async () => { const b = await bench(graph(), { pending: () => ({ inject: ['missing'], apply() {} }) }, false) await expect(b.modules.entries.sync(graph())).rejects.toThrow('have not started') await b.modules.entries.start(b.ctx.loader, b.modules.manifest) await b.modules.entries.sync(graph(row('pending'))) expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('waiting for activation') b.ctx.provide('missing', {}) await b.modules.entries.retry() expect(b.modules.entries.state.getSnapshot().failures).toEqual([]) }) it('retries a materialization failure with the same graph and cleans its partial styles', async () => { let broken = true const b = await bench(graph(), { a: () => { const style = document.createElement('style') style.dataset.plugin = 'a' document.head.append(style) if (broken) throw new Error('factory failed') return { apply() {} } } }) await b.modules.entries.sync(graph(row('a'))) expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('factory failed') expect(document.querySelectorAll('style[data-plugin=a]')).toHaveLength(0) await b.modules.entries.retry() expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('factory failed') await b.modules.entries.reload('a', 'r1').catch(() => {}) expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('factory failed') broken = false await b.modules.entries.retry() expect(b.modules.entries.state.getSnapshot().failures).toEqual([]) expect(document.querySelectorAll('style[data-plugin=a]')).toHaveLength(1) }) it('does not finish a stale code replacement after download or asynchronous teardown', async () => { const effects = { mounted: 0, disposed: 0, hits: 0 } const cleanup = deferred() const b = await bench(graph(row('a')), { a: visible('a', effects, () => cleanup.promise) }) const download = deferred() const started = deferred() b.arrival(async () => { started.resolve(); await download.promise }) const rebuilding = b.modules.entries.reload('a', 'r1') await started.promise const snapshot = b.modules.entries.sync(graph(row('a'))) download.resolve() await Promise.all([rebuilding, snapshot]) expect(effects.mounted).toBe(1) const swapping = b.modules.entries.reload('a', 'r2') await vi.waitFor(() => { expect(document.querySelector('[data-live=a]')).toBeNull() }) const disabling = b.modules.entries.sync(graph()) cleanup.resolve() await Promise.all([swapping, disabling]) expect(effects.mounted).toBe(1) expect(b.modules.loadCache.has('a')).toBe(false) }) it('stops an obsolete multi-entry application after awaiting removal', async () => { const cleanup = deferred() const effects = { mounted: 0, disposed: 0, hits: 0 } const b = await bench(graph(row('a')), { a: visible('a', effects, () => cleanup.promise), b: () => ({ apply() {} }) }) const first = b.modules.entries.sync(graph(row('b'))) await vi.waitFor(() => { expect(document.querySelector('[data-live=a]')).toBeNull() }) const latest = b.modules.entries.sync(graph()) cleanup.resolve() await Promise.all([first, latest]) expect(b.fetched).toEqual(['/batch']) }) it('keeps bootstrap ownership explicit and diagnoses removal without changing entries', async () => { const b = await bench(graph(row('bootstrap')), {}) await expect(b.modules.entries.sync(graph())).rejects.toThrow('removing bootstrap module') expect([...b.ctx.loader.entries()].map(entry => entry.options.name)).toEqual(['bootstrap']) expect(b.modules.entries.state.getSnapshot().syncing).toBe(false) await b.modules.entries.sync(graph(row('bootstrap'))) expect(b.modules.entries.state.getSnapshot().failures).toEqual([]) }) it('retains unrelated style tags while reporting a failed replacement', async () => { let broken = false const effects = { mounted: 0, disposed: 0, hits: 0 } const b = await bench(graph(row('a')), { a: () => { if (broken) throw new Error('changed factory failed') return visible('a', effects)() } }) const unrelated = document.createElement('style') unrelated.dataset.plugin = 'unrelated' document.head.append(unrelated) broken = true await b.modules.entries.sync(graph(row('a', 'r1'))) expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('changed factory failed') expect(unrelated.isConnected).toBe(true) }) it('keeps unrelated page failures visible when a rebuilt download fails', async () => { const b = await bench(graph(row('a'), row('bad')), { a: () => ({ apply() {} }), bad: () => ({ apply() { throw new Error('bad apply') } }), }) await b.modules.entries.retry() b.arrival(async () => { throw new Error('a download') }) await expect(b.modules.entries.reload('a', 'r1')).rejects.toThrow('a download') expect(b.modules.entries.state.getSnapshot().failures.map(failure => failure.id)).toEqual(['bad', 'a']) }) it('retains a rejected Loader entry for retry instead of creating an orphan sibling', async () => { let broken = true const b = await bench(graph(), { a: () => broken ? { default: 'invalid' } : { apply() {} } }) await b.modules.entries.sync(graph(row('a'))) const entry = [...b.ctx.loader.entries()][0]! expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('invalid plugin') expect([...b.ctx.loader.entries()]).toHaveLength(1) broken = false await b.modules.entries.retry() expect([...b.ctx.loader.entries()]).toEqual([entry]) expect(entry.fiber?.state).toBe(2) }) it('reports Loader import failures and recovers the same entry after the importer recovers', async () => { const b = await bench(graph(), { a: () => ({ apply() {} }) }) let broken = true b.ctx.loader.internal = { version: 'client', import: async (id: string) => { if (broken) throw new Error('Loader import failed') return b.modules.import(id) }, } as never await b.modules.entries.sync(graph(row('a'))) expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('import failed') const entry = [...b.ctx.loader.entries()][0]! broken = false await b.modules.entries.retry() expect(entry.fiber?.state).toBe(2) broken = true await expect(b.modules.entries.reload('a', 'r1')).rejects.toThrow('import failed') expect(b.modules.entries.state.getSnapshot().failures[0]?.id).toBe('a') }) it.each([false, true])('does not mount a materialized factory after a newer removal (rebuild: %s)', async (rebuild) => { let stop: Promise | undefined let materializations = 0 let mounted = 0 const factories = { a: () => { materializations++ if (materializations === (rebuild ? 2 : 1)) { queueMicrotask(() => { stop = b.modules.entries.sync(graph()) }) } return { apply() { mounted++ } } } } const b = await bench(graph(), factories) if (rebuild) { // The first materialization remains live until the replacement reaches the import barrier. materializations = -1 await b.modules.entries.sync(graph(row('a'))) materializations = 1 await b.modules.entries.reload('a', 'r1') } else { await b.modules.entries.sync(graph(row('a'))) } await stop expect(mounted).toBe(rebuild ? 1 : 0) expect([...b.ctx.loader.entries()]).toHaveLength(0) }) it('coalesces an overlapping graph snapshot with the same rebuilt artifact', async () => { const effects = { mounted: 0, disposed: 0, hits: 0 } const b = await bench(graph(row('a')), { a: visible('a', effects) }) const download = deferred() const started = deferred() b.arrival(async () => { started.resolve(); await download.promise }) const rebuilding = b.modules.entries.reload('a', 'r1') await started.promise const syncing = b.modules.entries.sync(graph(row('a', 'r1'))) download.resolve() await Promise.all([rebuilding, syncing]) expect(b.fetched).toEqual(['/batch', row('a', 'r1').url]) expect(effects).toEqual({ mounted: 2, disposed: 1, hits: 0 }) }) it.each(['graph', 'rebuilt'])('replaces a failed factory before entry creation on a new %s revision', async (source) => { const effects = { mounted: 0, disposed: 0, hits: 0 } const factories: Record = { a: () => { throw new Error('broken r0 factory') } } const b = await bench(graph(), factories) await b.modules.entries.sync(graph(row('a'))) expect([...b.ctx.loader.entries()]).toHaveLength(0) expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('broken r0 factory') factories.a = () => ({ ...visible('a', effects)(), revision: 'r1' }) if (source === 'graph') await b.modules.entries.sync(graph(row('a', 'r1'))) else await b.modules.entries.reload('a', 'r1') await b.modules.entries.retry() expect(b.fetched).toEqual([row('a').url, row('a', 'r1').url]) expect(await b.modules.import('a')).toHaveProperty('revision', 'r1') expect(document.querySelectorAll('[data-live=a]')).toHaveLength(1) expect(effects.mounted).toBe(1) expect(b.modules.entries.state.getSnapshot().failures).toEqual([]) }) it('replaces a superseded arrival and its cached dependency before mounting the latest code', async () => { const dependency = row('dependency') const consumer = row('consumer', 'r0', { external: ['dependency/client'] }) const factories: Record = { dependency: () => ({ apply() {}, revision: 'r0' }), consumer: require => ({ apply() {}, dependency: require('dependency/client'), revision: 'r0' }), } const b = await bench(graph(), factories) const started = deferred() const download = deferred() b.arrival(async (url) => { if (url === consumer.url) { started.resolve(); await download.promise } }) const old = b.modules.entries.sync(graph(consumer, dependency)) try { await started.promise factories.dependency = () => ({ apply() {}, revision: 'r1' }) factories.consumer = require => ({ apply() {}, dependency: require('dependency/client'), revision: 'r1' }) const latest = b.modules.entries.sync(graph(row('consumer', 'r1', { external: ['dependency/client'] }), row('dependency', 'r1'))) download.resolve() await Promise.all([old, latest]) expect(await b.modules.import('consumer')).toMatchObject({ revision: 'r1', dependency: { revision: 'r1' } }) expect(b.fetched).toEqual([dependency.url, consumer.url, row('dependency', 'r1').url, row('consumer', 'r1').url]) expect(b.modules.entries.state.getSnapshot().failures).toEqual([]) } finally { download.resolve() await old } }) it.each(['graph', 'rebuilt'])('preserves bootstrap and dependent fibers when a %s requests new bootstrap code', async (source) => { const effects = { mounted: 0, disposed: 0, hits: 0 } const b = await bench(graph(row('bootstrap'), row('consumer')), { consumer: () => ({ ...visible('consumer', effects)(), inject: ['modules'] }), }) const fibers = [...b.ctx.loader.entries()].map(entry => entry.fiber) const exports = await b.modules.import('bootstrap') if (source === 'graph') await b.modules.entries.sync(graph(row('bootstrap', 'r1'), row('consumer'))) else await expect(b.modules.entries.reload('bootstrap', 'r1')).rejects.toThrow('requires a page reload') for (let retry = 0; retry < 2; retry++) { await b.modules.entries.retry() expect(b.modules.entries.state.getSnapshot().failures).toEqual([ { id: 'bootstrap', message: 'Error: client-modules: replacing bootstrap module bootstrap requires a page reload' }, ]) } expect([...b.ctx.loader.entries()].map(entry => entry.fiber)).toEqual(fibers) expect(await b.modules.import('bootstrap')).toBe(exports) expect(effects).toEqual({ mounted: 1, disposed: 0, hits: 0 }) expect(b.fetched).toEqual(['/batch']) }) it('discards a failed arrival target when an uncreated entry receives a newer graph', async () => { const b = await bench(graph(), { a: () => { throw new Error('r0 factory') } }) await b.modules.entries.sync(graph(row('a'))) b.arrival(async () => { throw new Error('offline r1') }) await b.modules.entries.reload('a', 'r1') expect(b.modules.entries.state.getSnapshot().failures[0]?.message).toContain('offline r1') b.arrival(async () => {}) await b.modules.entries.sync(graph(row('a', 'r2'))) expect(b.fetched).toEqual([row('a').url, row('a', 'r1').url, row('a', 'r2').url]) }) it('uses the latest desired revision when a rebuild queues before entry creation', async () => { const b = await bench(graph(), { a: () => ({ apply() {} }) }) const adding = b.modules.entries.sync(graph(row('a'))) const rebuilding = b.modules.entries.reload('a', 'r1') const latest = b.modules.entries.sync(graph(row('a', 'r2'))) await Promise.all([adding, rebuilding, latest]) expect(b.fetched).toEqual([row('a', 'r2').url]) expect([...b.ctx.loader.entries()]).toHaveLength(1) }) it('cleans styles from a materialized factory superseded before its entry is created', async () => { const effects = { mounted: 0, disposed: 0, hits: 0 } let latest: Promise | undefined const factories: Record = { a: () => { const old = visible('a', effects)() queueMicrotask(() => { factories.a = visible('a', effects) latest = b.modules.entries.sync(graph(row('a', 'r1'))) }) return old } } const b = await bench(graph(), factories) await b.modules.entries.sync(graph(row('a'))) await latest expect(b.fetched).toEqual([row('a').url, row('a', 'r1').url]) expect(effects).toEqual({ mounted: 1, disposed: 0, hits: 0 }) expect(document.querySelectorAll('style[data-plugin=a]')).toHaveLength(1) })