diff --git a/apps/web/tests/smoke-real.e2e.ts b/apps/web/tests/smoke-real.e2e.ts index 0bbaa2c1d3..a980c2d02d 100644 --- a/apps/web/tests/smoke-real.e2e.ts +++ b/apps/web/tests/smoke-real.e2e.ts @@ -17,7 +17,7 @@ import type { ChildProcess } from 'node:child_process' import { spawn } from 'node:child_process' import { randomUUID } from 'node:crypto' -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { createServer } from 'node:http' import { createRequire, SourceMap } from 'node:module' import { tmpdir } from 'node:os' @@ -27,7 +27,7 @@ import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' import WebSocket from 'ws' -import { REPO_ROOT, newEnglishPage, probeFreePort, requireDist, saveFailureShot } from './support.ts' +import { REPO_ROOT, connectFreshWorkspace, newEnglishPage, probeFreePort, requireDist, saveFailureShot } from './support.ts' const WEB_SURFACE_PROMPT = fileURLToPath(new URL('./expected/web-runtime-context/web-surface-prompt.expected.md', import.meta.url)) /** Framed user text that marks the auxiliary title request's own messages. */ @@ -284,18 +284,31 @@ async function screen(page: Page, name: string): Promise { /** First column track (px string) of the frame grid. */ async function firstTrack(page: Page): Promise { - return (await page.locator('[class*="frame"]').first().evaluate( + return (await page.locator('[class*="frame"]').evaluate( el => getComputedStyle(el).gridTemplateColumns)).split(' ')[0]! } /** Last column track (details) as a number of pixels. */ async function detailsTrack(page: Page): Promise { - const cols = await page.locator('[class*="frame"]').first().evaluate( + const cols = await page.locator('[class*="frame"]').evaluate( el => getComputedStyle(el).gridTemplateColumns) return Number(cols.split(' ').pop()!.replace('px', '')) } +// Readiness gate: `dsh web` serves every production manifest plugin; until every UI +// plugin's client bundle exists and exports apply, the loader fail-louds and +// the frame never appears. +const UI_PLUGIN_DIRS = [ + 'connection', 'ui-theme', 'locale', 'ui-layout', 'ui-renderer', 'ui-session', 'ui-sidebar', + 'ui-settings', 'ui-settings-general', 'ui-settings-models', 'ui-conversation', 'ui-approval', 'ui-chat', + 'ui-model-selection', 'ui-user-questions', 'ui-trajectory', '../session-query/session-log-export', +] const ROUND_DONE_MARKER = 'WEB_ROUND_DONE' +const notReady = UI_PLUGIN_DIRS.filter((dir) => { + const bundle = join(REPO_ROOT, 'packages/client', dir, 'lib/client.js') + return !existsSync(bundle) || !readFileSync(bundle, 'utf8').includes('exports.apply') +}) +if (notReady.length > 0) console.warn(`[smoke-real] skipped — client bundles not ready: ${notReady.join(', ')}`) describe('dsh web keyless CLI smoke', () => { it('serves a usable app from three immutable plugin batches', async () => { @@ -673,7 +686,7 @@ describe('dsh web keyless CLI smoke', () => { }) }) -describe.skipIf(!process.env.DEEPSEEK_API_KEY)('web smoke (real host, real key)', () => { +describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke (real host, real key)', () => { let child: ChildProcess let sessionsDir: string let baseUrl: string @@ -717,9 +730,6 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('web smoke (real host, real key)' page = await newEnglishPage(browser) page.on('pageerror', e => pageErrors.push(String(e))) await page.goto(baseUrl, { waitUntil: 'load' }) - const welcome = page.getByRole('dialog', { name: 'Preview Notice' }) - await welcome.getByRole('button', { name: 'Continue' }).click() - await welcome.waitFor({ state: 'detached' }) }, 120_000) afterAll(async () => { @@ -737,13 +747,18 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('web smoke (real host, real key)' onTestFailed(() => saveFailureShot(page, 'w5-cold-start')) await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) expect(await page.locator('text=Failed to load plugins').count()).toBe(0) - const template = await page.locator('[class*="frame"]').first().evaluate(el => getComputedStyle(el).gridTemplateColumns) + const template = await page.locator('[class*="frame"]').evaluate(el => getComputedStyle(el).gridTemplateColumns) expect(template.split(' ').length).toBe(3) await screen(page, '01-cold-start') }) it('empty-state first send completes a real model round', async () => { onTestFailed(() => saveFailureShot(page, 'w5-first-round')) + // This scenario spawns its own server against a fresh $DSH_HOME with the + // DeepSeek credential inherited from the environment, so no onboarding + // step mounts and the page is immediately interactive. + // Fresh world: connect a Workspace so the composer starts live. + await connectFreshWorkspace(page, sessionsDir) const input = page.locator('[data-composer-input]').first() await input.waitFor({ timeout: 10_000 }) const productTitle = await page.title() @@ -824,7 +839,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('web smoke (real host, real key)' it('sidebar drag widens the column and resets across reload', async () => { onTestFailed(() => saveFailureShot(page, 'w5-drag')) const before = await firstTrack(page) - const handle = page.locator('[data-side="sidebar"]') + const handle = page.locator('[class*="handle"]').first() const box = await handle.boundingBox() expect(box).not.toBeNull() await page.mouse.move(box!.x + box!.width / 2, box!.y + 300) diff --git a/packages/client/AGENTS.md b/packages/client/AGENTS.md index 80d9702f14..85d5d69354 100644 --- a/packages/client/AGENTS.md +++ b/packages/client/AGENTS.md @@ -85,7 +85,7 @@ A dynamic browser half either carries a module privately or requests the shared 3. **Silence means a private copy.** Third-party implementation libraries outside the baseline may be bundled independently. A value reached only through `import type` is erased and creates no request. 4. **A request has two possible suppliers.** A dynamic package supplies its own row; `PLATFORM_MODULES` supplies an exact static-table key. There is no `dsh.client.provide` alias protocol. 5. **Validate both sides.** The dynamic build preset externalizes the baseline and rejects undeclared workspace value imports; [`verify-client-packages`](../../scripts/verify-client-packages.ts) rejects malformed or redundant requests, missing suppliers, and synchronous request cycles. -6. **Package-local dynamic chunks are self-contained.** Source uses `import()` and the shared preset calls the factory loader's `.async` method with a literal `./client..js` path; an entry or chunk must not retain a synchronous relative `require("./client*.js")`. Keep every static relative dependency inside its owning output chunk rather than relying on a sibling-chunk graph the runtime does not support. +6. **Package-local dynamic chunks are self-contained.** Source uses `import()` and the shared preset emits `require.async("./client..js")`; an entry or chunk must not retain a synchronous relative `require("./client*.js")`. Keep every static relative dependency inside its owning output chunk rather than relying on a sibling-chunk graph the runtime does not support. ### The module graph sits below cordis DI diff --git a/packages/client/modules/README.i18n.yaml b/packages/client/modules/README.i18n.yaml index 258d5971f7..eb264da002 100644 --- a/packages/client/modules/README.i18n.yaml +++ b/packages/client/modules/README.i18n.yaml @@ -24,8 +24,8 @@ en: 529af1eb8bf1acf2 zh: 7d891a5dfa90c625 /deepseek-ai-dsh-client-modules/use-this-package/what-the-browser-loads: - en: 3c372e9d65e294a8 - zh: 00f82efab7071258 + en: 1383d68472b3c057 + zh: 94dd7d9abbdbccf3 /deepseek-ai-dsh-client-modules/use-this-package/live-plugin-composition: en: 45740a56e931d094 zh: fa44cf77c86019af @@ -36,8 +36,8 @@ en: 1778b5a63ecde5e8 zh: 537b4f8c755af8ad /deepseek-ai-dsh-client-modules/use-this-package/build-requirements: - en: 15aa99a9df72e51e - zh: 3324596fb5d3588d + en: b6a4d3b901c89efe + zh: 615b5f3eec41f281 /deepseek-ai-dsh-client-modules/understand-the-implementation: en: 7ab11d2c2bdc6eaa zh: 54c3c9091cf59711 diff --git a/packages/client/modules/README.md b/packages/client/modules/README.md index 10af92132b..6048a73939 100644 --- a/packages/client/modules/README.md +++ b/packages/client/modules/README.md @@ -40,7 +40,7 @@ The same `/plugins` route serves the images a client package publishes through i ### What the browser loads -The application combo scripts carry only each plugin's `client.js` entry and register those factories once during boot; module bodies remain lazy and run only at first import or materialization. A source `import()` split by tsdown calls the factory loader's `.async` method with a literal `./client..js` path; its versioned sibling script arrives only when that expression runs. Rows that share a combo URL share one in-flight script task. A combo whose `