|
|
@@ -8,52 +8,117 @@
|
|
|
// Keyless and deterministic: the fixture is the fake server, so nothing here
|
|
|
// reaches a model or the network.
|
|
|
import { readFileSync } from 'node:fs'
|
|
|
-import { join } from 'node:path'
|
|
|
+import { createRequire } from 'node:module'
|
|
|
+import { dirname, join, resolve } from 'node:path'
|
|
|
+import { pathToFileURL } from 'node:url'
|
|
|
import { act, cleanup } from '@testing-library/react'
|
|
|
import { afterEach, beforeEach, vi } from 'vitest'
|
|
|
-import { injectBootManifest } from '@deepseek-ai/dsh-client-modules'
|
|
|
+import { injectBootManifest, orderByModuleGraph } from '@deepseek-ai/dsh-client-modules'
|
|
|
import type { ClientModuleLoaderTarget, WebBootEntry } from '@deepseek-ai/dsh-client-modules/client'
|
|
|
import { AppWebEntry } from '@deepseek-ai/dsh-client-web'
|
|
|
|
|
|
-/** Boot entries for the minimal assembled graph, each carrying the workspace bundle it loads. */
|
|
|
-const PLUGINS: readonly (WebBootEntry & { bundlePath: string })[] = [
|
|
|
- { id: '@deepseek-ai/dsh-client-modules', bundlePath: 'packages/client/modules/lib/client.js', url: '/plugins/modules.js', rev: 'fx', inject: [], immediately: true },
|
|
|
- { id: '@deepseek-ai/dsh-typert-registry', bundlePath: 'packages/typert/registry/lib/client.js', url: '/plugins/typert-registry.js', rev: 'fx', inject: [], immediately: true },
|
|
|
- { id: '@deepseek-ai/dsh-client-connection', bundlePath: 'packages/client/connection/lib/client.js', url: '/plugins/connection.js', rev: 'fx', inject: [], immediately: true },
|
|
|
- { id: '@deepseek-ai/dsh-api-gateway', bundlePath: 'packages/api/gateway/lib/client.js', url: '/plugins/api-gateway.js', rev: 'fx', inject: ['@deepseek-ai/dsh-typert-registry', '@deepseek-ai/dsh-client-connection'], immediately: true },
|
|
|
- { id: '@deepseek-ai/dsh-api-remotes', bundlePath: 'packages/api/remotes/lib/client.js', url: '/plugins/api-remotes.js', rev: 'fx', inject: ['@deepseek-ai/dsh-api-gateway'], immediately: true },
|
|
|
- // The settings domain base: the only provider of ctx.settingsScope, which the
|
|
|
- // locale and ui-theme rows below inject for their preference rows. Without it
|
|
|
- // both stay pending and ui-layout never activates, so nothing renders.
|
|
|
- { id: '@deepseek-ai/dsh-client-ui-settings', bundlePath: 'packages/client/ui-settings/lib/client.js', url: '/plugins/ui-settings.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection', '@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-api-remotes'], immediately: true },
|
|
|
- { id: '@deepseek-ai/dsh-client-runtime', bundlePath: 'packages/client/runtime/lib/client.js', url: '/plugins/runtime.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection', '@deepseek-ai/dsh-typert-registry', '@deepseek-ai/dsh-api-gateway'], immediately: true },
|
|
|
- { id: '@deepseek-ai/dsh-client-ui-theme', bundlePath: 'packages/client/ui-theme/lib/client.js', url: '/plugins/ui-theme.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection', '@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-locale', '@deepseek-ai/dsh-client-ui-settings', '@deepseek-ai/dsh-api-remotes'], immediately: true },
|
|
|
- { id: '@deepseek-ai/dsh-client-locale', bundlePath: 'packages/client/locale/lib/client.js', url: '/plugins/locale.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection', '@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-settings', '@deepseek-ai/dsh-api-remotes'], immediately: true },
|
|
|
- { id: '@deepseek-ai/dsh-client-ui-layout', bundlePath: 'packages/client/ui-layout/lib/client.js', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
|
|
|
- { id: '@deepseek-ai/dsh-client-ui-renderer', bundlePath: 'packages/client/ui-renderer/lib/client.js', url: '/plugins/ui-renderer.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-layout'], immediately: true },
|
|
|
- { id: '@deepseek-ai/dsh-client-ui-sidebar', bundlePath: 'packages/client/ui-sidebar/lib/client.js', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
|
|
|
- { id: '@deepseek-ai/dsh-client-ui-conversation', bundlePath: 'packages/client/ui-conversation/lib/client.js', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
|
|
|
- { id: '@deepseek-ai/dsh-client-ui-attachment', bundlePath: 'packages/client/ui-attachment/lib/client.js', url: '/plugins/ui-attachment.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
|
|
|
- { id: '@deepseek-ai/dsh-client-ui-tool', bundlePath: 'packages/client/ui-tool/lib/client.js', url: '/plugins/ui-tool.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-locale', '@deepseek-ai/dsh-client-ui-conversation'] },
|
|
|
- { id: '@deepseek-ai/dsh-client-ui-workflow-run', bundlePath: 'packages/client/ui-workflow-run/lib/client.js', url: '/plugins/ui-workflow-run.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-locale', '@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-conversation'] },
|
|
|
+interface AssembledPlugin extends WebBootEntry {
|
|
|
+ /** Absolute path to the built client artifact declared by this package. */
|
|
|
+ bundlePath: string
|
|
|
+}
|
|
|
+
|
|
|
+interface ClientPackageManifest {
|
|
|
+ name?: string
|
|
|
+ exports?: Record<string, string | { default?: string }>
|
|
|
+ dsh?: {
|
|
|
+ client?: {
|
|
|
+ platform?: string
|
|
|
+ inject?: string[]
|
|
|
+ external?: string[]
|
|
|
+ immediately?: boolean
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+interface ComposedEntry {
|
|
|
+ name?: unknown
|
|
|
+ disabled?: unknown
|
|
|
+}
|
|
|
+
|
|
|
+interface BootComposition {
|
|
|
+ loadOverlayPatches(binName: string, file: string): unknown[]
|
|
|
+ composeEntries(layers: readonly unknown[][]): ComposedEntry[]
|
|
|
+}
|
|
|
+
|
|
|
+const REPO_ROOT = process.cwd()
|
|
|
+const BUNDLE_LAYERS = [
|
|
|
+ {
|
|
|
+ manifest: join(REPO_ROOT, 'packages/bundle/base/package.json'),
|
|
|
+ patch: join(REPO_ROOT, 'packages/bundle/base/cordis.patch.yml'),
|
|
|
+ },
|
|
|
{
|
|
|
- id: '@deepseek-ai/dsh-client-ui-workspace',
|
|
|
- bundlePath: 'packages/client/ui-workspace/lib/client.js',
|
|
|
- url: '/plugins/ui-workspace.js',
|
|
|
- rev: 'fx',
|
|
|
- inject: [
|
|
|
- '@deepseek-ai/dsh-client-runtime',
|
|
|
- '@deepseek-ai/dsh-client-ui-conversation',
|
|
|
- '@deepseek-ai/dsh-client-ui-sidebar',
|
|
|
- ],
|
|
|
+ manifest: join(REPO_ROOT, 'packages/bundle/web-app/package.json'),
|
|
|
+ patch: join(REPO_ROOT, 'packages/bundle/web-app/cordis.patch.yml'),
|
|
|
},
|
|
|
- { id: '@deepseek-ai/dsh-session-log-export', bundlePath: 'packages/session-query/session-log-export/lib/client.js', url: '/plugins/session-log-download.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-commands', '@deepseek-ai/dsh-client-ui-conversation'] },
|
|
|
- { id: '@deepseek-ai/dsh-client-ui-trajectory', bundlePath: 'packages/client/ui-trajectory/lib/client.js', url: '/plugins/ui-trajectory.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
|
|
|
-]
|
|
|
+] as const
|
|
|
+const bundleResolvers = BUNDLE_LAYERS.map(layer => createRequire(layer.manifest))
|
|
|
+const webBundleResolver = bundleResolvers[1]
|
|
|
+if (webBundleResolver === undefined) throw new Error('assembled boot: web bundle resolver missing')
|
|
|
+const appBoot = await import(pathToFileURL(webBundleResolver.resolve('@deepseek-ai/dsh-app-boot')).href) as unknown as BootComposition
|
|
|
+
|
|
|
+function resolvePackageManifest(specifier: string): string | undefined {
|
|
|
+ for (const require of bundleResolvers) {
|
|
|
+ try {
|
|
|
+ return require.resolve(`${specifier}/package.json`)
|
|
|
+ } catch {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return undefined
|
|
|
+}
|
|
|
+
|
|
|
+function resolveClientExport(packagePath: string, pkg: ClientPackageManifest): string {
|
|
|
+ const declared = pkg.exports?.['./client']
|
|
|
+ const relative = typeof declared === 'string' ? declared : declared?.default
|
|
|
+ if (relative === undefined) {
|
|
|
+ throw new Error(`assembled boot: ${pkg.name ?? packagePath} declares dsh.client without a ./client export`)
|
|
|
+ }
|
|
|
+ return resolve(dirname(packagePath), relative)
|
|
|
+}
|
|
|
+
|
|
|
+/** Derive the assembled browser graph from the same bundle patches and package declarations as `dsh web`. */
|
|
|
+function loadAssembledPlugins(): readonly AssembledPlugin[] {
|
|
|
+ const entries = appBoot.composeEntries(BUNDLE_LAYERS.map(layer =>
|
|
|
+ appBoot.loadOverlayPatches('assembled boot', layer.patch)))
|
|
|
+ const plugins = new Map<string, AssembledPlugin>()
|
|
|
+ for (const entry of entries) {
|
|
|
+ if (entry.disabled === true || typeof entry.name !== 'string') continue
|
|
|
+ const packagePath = resolvePackageManifest(entry.name)
|
|
|
+ if (packagePath === undefined) continue
|
|
|
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf8')) as ClientPackageManifest
|
|
|
+ const declaration = pkg.dsh?.client
|
|
|
+ if (declaration?.platform !== 'web') continue
|
|
|
+ if (pkg.name !== entry.name) {
|
|
|
+ throw new Error(`assembled boot: ${entry.name} resolved package ${pkg.name ?? '<unnamed>'}`)
|
|
|
+ }
|
|
|
+ plugins.set(entry.name, {
|
|
|
+ id: entry.name,
|
|
|
+ bundlePath: resolveClientExport(packagePath, pkg),
|
|
|
+ url: `/plugins/${entry.name}/client.js?rev=fx`,
|
|
|
+ rev: 'fx',
|
|
|
+ ...(declaration.inject === undefined ? {} : { inject: declaration.inject }),
|
|
|
+ ...(declaration.external === undefined ? {} : { external: declaration.external }),
|
|
|
+ ...(declaration.immediately === true ? { immediately: true } : {}),
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return orderByModuleGraph([...plugins.values()]).map(({ id }) => {
|
|
|
+ const plugin = plugins.get(id)
|
|
|
+ /* v8 ignore next -- orderByModuleGraph returns the input row identities */
|
|
|
+ if (plugin === undefined) throw new Error(`assembled boot: ordered unknown client package ${id}`)
|
|
|
+ return plugin
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const PLUGINS = loadAssembledPlugins()
|
|
|
|
|
|
const bundles = new Map(PLUGINS.map(plugin => [
|
|
|
plugin.url,
|
|
|
- readFileSync(join(process.cwd(), plugin.bundlePath), 'utf8'),
|
|
|
+ readFileSync(plugin.bundlePath, 'utf8'),
|
|
|
]))
|
|
|
|
|
|
interface FixtureWindow extends Window {
|
|
|
@@ -67,6 +132,11 @@ class ResizeObserverStub {
|
|
|
unobserve(): void {}
|
|
|
}
|
|
|
|
|
|
+class EventSourceStub {
|
|
|
+ addEventListener(): void {}
|
|
|
+ close(): void {}
|
|
|
+}
|
|
|
+
|
|
|
const win = window as FixtureWindow
|
|
|
let unmount: (() => Promise<void>) | undefined
|
|
|
|
|
|
@@ -88,6 +158,7 @@ export function installAssembledBootEnv(): void {
|
|
|
Object.defineProperty(navigator, 'language', { value: 'en-US', configurable: true })
|
|
|
document.title = 'DeepSeek Harness'
|
|
|
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
|
|
+ vi.stubGlobal('EventSource', EventSourceStub)
|
|
|
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
|
|
|
setTimeout(() => { callback(0) }, 0) as unknown as number)
|
|
|
vi.stubGlobal('cancelAnimationFrame', (id: number) => { clearTimeout(id) })
|