Merge remote-tracking branch 'origin/master' into worktree/code-diff-card
This commit is contained in:
commit
3c632ebf66
318 files changed
+9061
-3253
No files matched your search
@@ -101,7 +101,7 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn
|
||||
// Skip the resident fixture's three questions, then resolve its approval so
|
||||
// the ordinary composer bar (which owns ContextMeter) resumes.
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Skip this question' }))
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Skip' }))
|
||||
}
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Allow once' }))
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/** Historical Messages provider replay preserves its recorded identity and DeepSeek model group. */
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
import { chromium, type Browser, type Page } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, selectedSessionFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspaceZh, saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/deepseek-messages-chat', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.v3.jsonl')
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: DeepSeek Messages conversation', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
let replayFixture: string
|
||||
|
||||
beforeAll(async () => {
|
||||
replayFixture = await selectedSessionFixture(FIXTURE, false)
|
||||
scaffold = await launchWebScaffold({
|
||||
deepSeekMessages: true,
|
||||
replayFixture,
|
||||
paceMs: 5,
|
||||
replayProviders: [{
|
||||
id: 'deepseek-messages', name: 'DeepSeek',
|
||||
models: [{
|
||||
id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash',
|
||||
contextWindow: 1_000_000, defaultMaxTokens: 256_000,
|
||||
reasoningEfforts: ['off', 'low', 'high', 'max'], defaultReasoningEffort: 'high',
|
||||
}],
|
||||
}],
|
||||
})
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
await connectFreshWorkspaceZh(page, scaffold.workspaceCwd)
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
try { await browser?.close() } finally { await scaffold?.close() }
|
||||
})
|
||||
|
||||
it('replays the historical Messages provider while displaying DeepSeek in the model selector', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-deepseek-messages-chat'))
|
||||
const prompts = fixtureUserPrompts(await readFile(replayFixture, 'utf8'))
|
||||
expect(prompts).toHaveLength(1)
|
||||
expect(scaffold.ctx.agentDefaultModel.currentSelection()).toEqual({ provider: 'deepseek-messages', model: 'deepseek-v4-flash' })
|
||||
await page.getByRole('button', { name: /^选择模型/ }).click()
|
||||
await page.getByRole('menuitem', { name: /模型/ }).click()
|
||||
await page.getByText('DeepSeek', { exact: true }).waitFor()
|
||||
await page.getByRole('button', { name: /^选择模型/ }).click()
|
||||
const input = page.locator('[data-composer-input]').first()
|
||||
const settled = scaffold.whenTurnSettled()
|
||||
await input.fill(prompts[0]!)
|
||||
await input.press('Enter')
|
||||
const sessionId = await settled
|
||||
const session = scaffold.ctx.sessions.get(sessionId)!
|
||||
expect(session.requestHeader()?.config.provider).toBe('deepseek-messages')
|
||||
await page.getByText('MESSAGES_WEB_READY', { exact: true }).waitFor()
|
||||
await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'ui.expected.md'),
|
||||
await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd), MODE)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps the recorded-session inventory closed', async () => {
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['session.v3.jsonl', 'ui.expected.md'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,103 @@
|
||||
/** Opt-in Web Messages configuration, credential reuse, and recovery from a saved Chat Completions selection. */
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium, type Browser, type Page } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import {
|
||||
captureStableAria, compareOrRefreshGolden, launchWebScaffold,
|
||||
watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspaceZh, saveFailureShot, ZH_BROWSER_LOCALE } from './support.ts'
|
||||
|
||||
const EXPECTED = fileURLToPath(new URL('./expected/deepseek-messages-settings/', import.meta.url))
|
||||
|
||||
describe.skipIf(webSnapshotMode() === 'record')('web e2e: DeepSeek Messages opt-in', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({ deepSeekMissingCredential: true, deepSeekMessages: true })
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
try {
|
||||
await browser?.close()
|
||||
} finally {
|
||||
await scaffold?.close()
|
||||
}
|
||||
})
|
||||
|
||||
it('offers one DeepSeek card and saves Messages settings using the existing credential reference', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-deepseek-messages-settings'))
|
||||
expect(scaffold.ctx.llm.listProviders()).toContainEqual({ id: 'deepseek-official', name: 'DeepSeek' })
|
||||
expect(scaffold.ctx.llm.listProviders().filter(provider => provider.id === 'deepseek-official')).toHaveLength(1)
|
||||
expect(scaffold.ctx.agentDefaultModel.currentSelection()).toEqual({ provider: 'deepseek-official', model: 'deepseek-flash' })
|
||||
const onboarding = page.getByRole('dialog', { name: '添加一个 API Key 开始使用' })
|
||||
await onboarding.getByLabel('API 密钥', { exact: true }).fill('sk-messages-onboarding')
|
||||
await onboarding.getByRole('button', { name: '保存并继续' }).click()
|
||||
await onboarding.waitFor({ state: 'detached' })
|
||||
await page.getByRole('button', { name: '设置', exact: true }).click()
|
||||
const dialog = page.getByRole('dialog', { name: '设置', exact: true })
|
||||
await dialog.getByRole('button', { name: '模型', exact: true }).click()
|
||||
await dialog.getByText('DeepSeek', { exact: true }).waitFor()
|
||||
expect(await dialog.getByText('DeepSeek', { exact: true }).count()).toBe(1)
|
||||
await dialog.getByText('DeepSeek', { exact: true }).locator('xpath=ancestor::li').getByRole('button', { name: '编辑' }).click()
|
||||
const messages = dialog
|
||||
await messages.getByText('自定义设置', { exact: true }).click()
|
||||
expect(await messages.getByLabel('API 地址', { exact: true }).getAttribute('placeholder'))
|
||||
.toBe('https://api.deepseek.com/anthropic')
|
||||
await compareOrRefreshGolden(join(EXPECTED, 'cards.expected.md'),
|
||||
await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd), webSnapshotMode())
|
||||
await messages.getByLabel('API 密钥', { exact: true }).fill('sk-e2e-messages')
|
||||
await messages.getByLabel('API 地址', { exact: true }).fill('https://messages.example/anthropic')
|
||||
expect(await messages.getByLabel('模型 ID 1').inputValue()).toBe('deepseek-flash')
|
||||
await messages.getByLabel('显示名称 1', { exact: true }).fill('Messages Flash')
|
||||
await messages.getByRole('button', { name: '保存', exact: true }).click()
|
||||
await dialog.getByText('已保存 DeepSeek (deepseek-official)。', { exact: true }).waitFor()
|
||||
|
||||
const settings = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
|
||||
expect(settings).toContain('https://messages.example/anthropic')
|
||||
expect(settings).toContain('llm-deepseek:')
|
||||
await expect(scaffold.ctx.llm.resolveModelInfo('deepseek-official', 'deepseek-flash')).resolves.toMatchObject({
|
||||
name: 'Messages Flash', inputModalities: ['text', 'image'], systemPromptUpdate: 'in-history',
|
||||
})
|
||||
expect(scaffold.ctx.settings.get('llm-deepseek')).toMatchObject({ protocol: 'messages' })
|
||||
expect(settings).not.toContain('sk-e2e-')
|
||||
const credentials = await readFile(join(scaffold.harnessHome, '.credentials.yaml'), 'utf8')
|
||||
expect(credentials).toContain('DEEPSEEK_API_KEY: sk-e2e-messages')
|
||||
expect(credentials).not.toContain('DEEPSEEK_MESSAGES_API_KEY')
|
||||
expect(await page.locator('body').innerText()).not.toContain('sk-e2e-')
|
||||
await page.keyboard.press('Escape')
|
||||
await connectFreshWorkspaceZh(page, scaffold.workspaceCwd, 'messages-settings-e2e')
|
||||
await page.getByRole('button', { name: /^选择模型/ }).click()
|
||||
await page.getByRole('menuitem', { name: /模型/ }).click()
|
||||
await page.getByRole('menuitemradio', { name: 'Messages Flash', exact: true }).waitFor()
|
||||
await compareOrRefreshGolden(join(EXPECTED, 'picker.expected.md'),
|
||||
await captureStableAria(page, '[role="menu"]', scaffold.workspaceCwd), webSnapshotMode())
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('keeps a saved Chat Completions selection available after the YAML protocol switch', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-deepseek-messages-default'))
|
||||
await page.keyboard.press('Escape')
|
||||
await scaffold.ctx.agentDefaultModel.saveSelection({ provider: 'deepseek-official', model: 'deepseek-v4-flash' })
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
const input = page.locator('[data-composer-input]').first()
|
||||
await expect.poll(() => input.isEnabled()).toBe(true)
|
||||
await page.getByRole('button', { name: /^选择模型/ }).click()
|
||||
await page.getByRole('menuitem', { name: /模型/ }).click()
|
||||
await page.getByRole('menuitemradio', { name: 'Messages Flash', exact: true }).click()
|
||||
await expect.poll(() => input.isEnabled()).toBe(true)
|
||||
await expect.poll(() => scaffold.ctx.agentDefaultModel.currentSelection().provider).toBe('deepseek-official')
|
||||
const settings = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
|
||||
expect(settings).toContain('provider: deepseek-official')
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
})
|
||||
@@ -0,0 +1,87 @@
|
||||
- dialog "设置":
|
||||
- navigation:
|
||||
- text: 设置
|
||||
- button "通用设置":
|
||||
- img
|
||||
- text: 通用设置
|
||||
- button "模型":
|
||||
- img
|
||||
- text: 模型
|
||||
- button "插件":
|
||||
- img
|
||||
- text: 插件
|
||||
- button "Agent 预设":
|
||||
- img
|
||||
- text: Agent 预设
|
||||
- button "打开配置文件"
|
||||
- button "关闭":
|
||||
- img
|
||||
- text: 关闭
|
||||
- heading "模型" [level=2]
|
||||
- paragraph: 填入各提供方的 API 密钥即可使用其模型。
|
||||
- list:
|
||||
- listitem:
|
||||
- text: DeepSeek
|
||||
- img "API 密钥已配置"
|
||||
- button "编辑 DeepSeek (deepseek-official)": 编辑
|
||||
- text: DeepSeek deepseek-official API 密钥
|
||||
- textbox "API 密钥":
|
||||
- /placeholder: 已配置——输入新值可替换
|
||||
- group:
|
||||
- text: 自定义设置 API 地址
|
||||
- textbox "API 地址":
|
||||
- /placeholder: https://api.deepseek.com/anthropic
|
||||
- text: 请填写与当前连接配置兼容的 API 地址。
|
||||
- region "模型目录":
|
||||
- text: 模型目录 正在使用适配器默认模型
|
||||
- textbox "模型 ID 1":
|
||||
- /placeholder: 模型 ID
|
||||
- text: deepseek-flash
|
||||
- textbox "显示名称 1":
|
||||
- /placeholder: 显示名称
|
||||
- text: DeepSeek-V41-Flash
|
||||
- button "容量 1":
|
||||
- img
|
||||
- button "删除模型 1":
|
||||
- img
|
||||
- textbox "模型 ID 2":
|
||||
- /placeholder: 模型 ID
|
||||
- text: deepseek-v4-flash
|
||||
- textbox "显示名称 2":
|
||||
- /placeholder: 显示名称
|
||||
- text: DeepSeek-V4-Flash
|
||||
- button "容量 2":
|
||||
- img
|
||||
- button "删除模型 2":
|
||||
- img
|
||||
- textbox "模型 ID 3":
|
||||
- /placeholder: 模型 ID
|
||||
- text: deepseek-v4-pro
|
||||
- textbox "显示名称 3":
|
||||
- /placeholder: 显示名称
|
||||
- text: DeepSeek-V4-Pro
|
||||
- button "容量 3":
|
||||
- img
|
||||
- button "删除模型 3":
|
||||
- img
|
||||
- textbox "模型 ID 4":
|
||||
- /placeholder: 模型 ID
|
||||
- text: deepseek-v4-flash-vision-exp
|
||||
- textbox "显示名称 4":
|
||||
- /placeholder: 显示名称
|
||||
- text: DeepSeek-V4-Flash-Vision-Exp
|
||||
- button "容量 4":
|
||||
- img
|
||||
- button "删除模型 4":
|
||||
- img
|
||||
- button "添加模型":
|
||||
- img
|
||||
- text: 添加模型
|
||||
- button "取消"
|
||||
- button "保存"
|
||||
- button "添加提供方":
|
||||
- img
|
||||
- text: 添加提供方
|
||||
- button "添加自定义提供方":
|
||||
- img
|
||||
- text: 添加自定义提供方
|
||||
@@ -0,0 +1,9 @@
|
||||
- menu "模型与推理等级":
|
||||
- group "DeepSeek":
|
||||
- text: DeepSeek
|
||||
- menuitemradio "Messages Flash" [checked]:
|
||||
- text: Messages Flash
|
||||
- img
|
||||
- menuitemradio "DeepSeek-V4-Flash"
|
||||
- menuitemradio "DeepSeek-V4-Pro"
|
||||
- menuitemradio "DeepSeek-V4-Flash-Vision-Exp"
|
||||
@@ -31,6 +31,7 @@
|
||||
- text: 自定义设置 API 地址
|
||||
- textbox "API 地址":
|
||||
- /placeholder: https://api.deepseek.com
|
||||
- text: 请填写与当前连接配置兼容的 API 地址。
|
||||
- region "模型目录":
|
||||
- text: 模型目录 正在使用适配器默认模型
|
||||
- textbox "模型 ID 1":
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
- text: 自定义设置 API 地址
|
||||
- textbox "API 地址":
|
||||
- /placeholder: https://api.deepseek.com
|
||||
- text: 请填写与当前连接配置兼容的 API 地址。
|
||||
- region "模型目录":
|
||||
- text: 模型目录 已自定义模型目录
|
||||
- button "恢复默认模型"
|
||||
|
||||
@@ -428,22 +428,15 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
|
||||
await recoveryPage.context().setOffline(false)
|
||||
await expect.poll(() => recoveryPage.evaluate(() => navigator.onLine)).toBe(true)
|
||||
const connecting = recoveryPage.getByRole('button', {
|
||||
name: 'Reconnecting automatically, reconnect now', exact: true,
|
||||
name: 'Reconnecting, reconnect now', exact: true,
|
||||
})
|
||||
await connecting.waitFor({ timeout: 10_000 })
|
||||
expect(await connecting.innerText()).toMatch(/^Reconnecting\.{1,3}$/)
|
||||
const connectingGeometry = await connectionIndicatorGeometry(connecting)
|
||||
expect(await connectionIndicatorTextAlignment(connecting)).toBe('left')
|
||||
// Animated dots must remain hidden with their state label during hover.
|
||||
await connecting.evaluate((element) => {
|
||||
for (const animation of element.getAnimations({ subtree: true })) {
|
||||
if (!(animation instanceof CSSAnimation)) continue
|
||||
animation.pause()
|
||||
animation.currentTime = 1_250
|
||||
}
|
||||
})
|
||||
// Hover keeps the state label; the pill never swaps copy or resizes.
|
||||
await connecting.hover()
|
||||
expect(await connecting.innerText()).toBe('Reconnect now')
|
||||
expect(await connecting.innerText()).toMatch(/^Reconnecting\.{1,3}$/)
|
||||
expect(await connectionIndicatorGeometry(connecting)).toEqual(connectingGeometry)
|
||||
await recoveryPage.mouse.move(0, 0)
|
||||
|
||||
@@ -461,7 +454,6 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
|
||||
const indicator = connecting
|
||||
expect(await connectionIndicatorGeometry(indicator)).toEqual(connectingGeometry)
|
||||
expect(await connectionIndicatorTextAlignment(indicator)).toBe('left')
|
||||
await indicator.hover()
|
||||
const snapshot = await captureStableAria(recoveryPage, '[class*="footArea"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(CONNECTION_ERROR_EXPECTED, snapshot, MODE)
|
||||
const style = await indicator.evaluate((element) => {
|
||||
@@ -498,11 +490,9 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
|
||||
await connecting.waitFor()
|
||||
await recoveryPage.clock.fastForward(500)
|
||||
await expect.poll(() => sockets.length).toBe(11)
|
||||
const idleBackground = await indicator.evaluate(element => getComputedStyle(element).backgroundColor)
|
||||
await indicator.hover()
|
||||
expect(await indicator.innerText()).toBe('Reconnect now')
|
||||
expect(await indicator.innerText()).toMatch(/^Reconnecting\.{1,3}$/)
|
||||
const hoverBackground = await indicator.evaluate(element => getComputedStyle(element).backgroundColor)
|
||||
expect(hoverBackground).toBe(idleBackground)
|
||||
await recoveryPage.mouse.down()
|
||||
await expect.poll(() => indicator.evaluate(element => getComputedStyle(element).backgroundColor))
|
||||
.not.toBe(hoverBackground)
|
||||
@@ -513,7 +503,10 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
|
||||
const recovered = recoveryPage.getByRole('status')
|
||||
await recovered.waitFor({ timeout: 10_000 })
|
||||
expect(await recovered.innerText()).toBe('Connected')
|
||||
expect(await connectionIndicatorGeometry(recovered)).toEqual(connectingGeometry)
|
||||
// The pill sizes to its current label; chrome height and icon box stay fixed.
|
||||
const recoveredGeometry = await connectionIndicatorGeometry(recovered)
|
||||
expect(recoveredGeometry.outer[3]).toBe(connectingGeometry.outer[3])
|
||||
expect(recoveredGeometry.icon).toEqual(connectingGeometry.icon)
|
||||
expect(await connectionIndicatorTextAlignment(recovered)).toBe('left')
|
||||
await recoveryPage.clock.fastForward(2_000)
|
||||
await recovered.waitFor({ state: 'detached', timeout: 5_000 })
|
||||
|
||||
@@ -345,7 +345,7 @@ describe('web e2e: resident question composer round trip', () => {
|
||||
expect(await capMetrics(field)).toEqual({ textLines: CAP_LINES, scrolls: true })
|
||||
|
||||
// Settle the wait so teardown is not racing a pending question.
|
||||
await composer.getByRole('button', { name: 'Skip this question' }).click()
|
||||
await composer.getByRole('button', { name: 'Skip' }).click()
|
||||
expect(await asked).toEqual({ answers: [{ id: 'free', selected: [] }] })
|
||||
await expect.poll(() => page.locator('[data-question-key]').count(), { timeout: 10_000 }).toBe(0)
|
||||
}, 60_000)
|
||||
|
||||
+30
-20
@@ -5,7 +5,7 @@
|
||||
// layer stack the profile boot composes), patched the
|
||||
// snapshot way — so a real chromium exercises the real HTTP uplink/WebSocket
|
||||
// downlink, api-gateway, agent loop, tools, and persistence. Modes ride $DSH_SNAPSHOT:
|
||||
// replay (default, keyless: normally disables the llm-deepseek row and
|
||||
// replay (default, keyless: normally disables the direct DeepSeek rows and
|
||||
// inserts dsh-llm-replay in providers mode), record (real adapter + key,
|
||||
// harvests fixtures from live session memory), refresh (keyless replay that
|
||||
// rewrites goldens). A first-run option keeps the real adapter mounted while
|
||||
@@ -18,7 +18,7 @@
|
||||
// disabled (recorded fixtures must not embed this repo's AGENTS.md);
|
||||
// session-title-llm disabled (its fire-and-forget title call would race the
|
||||
// loop for the session's replay cursor); webserver pinned to port 0 with the
|
||||
// built dist; ordinary keyless modes disable llm-deepseek and fill the open
|
||||
// built dist; ordinary keyless modes disable both direct adapters and fill the open
|
||||
// llm seam post-boot with installLlmReplay on the settled root ctx
|
||||
// (the plugin-row path discards the ReplayHandle; the direct install keeps
|
||||
// assertConsumed for the teardown fixture-consumption check).
|
||||
@@ -57,7 +57,7 @@ import {
|
||||
type NormalizeContext,
|
||||
} from '@deepseek-ai/dsh-session-snapshot'
|
||||
import {
|
||||
assertEntriesLoaded,
|
||||
auditStartupEntries,
|
||||
composeEntries,
|
||||
healProfilesModuleFallback,
|
||||
loadOverlayPatches,
|
||||
@@ -187,7 +187,7 @@ const WEB_PATCH_PATH = join(REPO_ROOT, 'packages/bundle/web-app/cordis.patch.yml
|
||||
const INSTALL_ANCHOR = join(REPO_ROOT, 'apps/cli/package.json')
|
||||
|
||||
// Replay publishes the provider catalog the gateway routes to (providers
|
||||
// mode, never catch-all: with llm-deepseek disabled no adapter exists, so a
|
||||
// mode, never catch-all: with both direct adapters disabled no adapter exists, so a
|
||||
// catch-all would leave resolveModelInfo unroutable and compaction-basic's
|
||||
// post-step pressure check would warn every step). The published
|
||||
// contextWindow keeps that pressure path provably inert for small fixtures.
|
||||
@@ -248,11 +248,14 @@ class RouteOnlyAdapter extends LlmAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
function replayProviders(contextWindow: number | undefined): typeof REPLAY_PROVIDERS {
|
||||
if (contextWindow === undefined) return REPLAY_PROVIDERS
|
||||
function replayProviders(contextWindow: number | undefined, messages: boolean): typeof REPLAY_PROVIDERS {
|
||||
return REPLAY_PROVIDERS.map(provider => ({
|
||||
...provider,
|
||||
models: provider.models.map(model => ({ ...model, contextWindow })),
|
||||
id: messages ? 'deepseek-messages' : provider.id,
|
||||
models: provider.models.map(model => ({
|
||||
...model,
|
||||
...contextWindow === undefined ? {} : { contextWindow },
|
||||
})),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -305,7 +308,7 @@ export interface LaunchOptions {
|
||||
* Replay fixture (session.jsonl) served by the inserted dsh-llm-replay row
|
||||
* in replay/refresh modes; ignored in record mode (the real adapter
|
||||
* answers). Omit for scenarios issuing no model calls — a stray stream then
|
||||
* fails loud with NO_ADAPTER (llm-deepseek is disabled and no replay row
|
||||
* fails loud with NO_ADAPTER (both direct adapters are disabled and no replay row
|
||||
* mounts). With {@link replayProvidersOnly}, the fixture must record no
|
||||
* model calls (its header alone mounts the catalog).
|
||||
*/
|
||||
@@ -362,6 +365,8 @@ export interface LaunchOptions {
|
||||
* keyless first-run configuration lane; the default disables the adapter.
|
||||
*/
|
||||
deepSeekMissingCredential?: boolean
|
||||
/** Record or replay a Messages scenario; older scenarios explicitly retain their recorded Chat Completions route. */
|
||||
deepSeekMessages?: boolean
|
||||
/** Leave the current welcome notice pending; ordinary scenarios pre-acknowledge it before browser boot. */
|
||||
welcomeNoticePending?: boolean
|
||||
/**
|
||||
@@ -443,6 +448,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
throw new Error('deepSeekMissingCredential is a keyless replay/refresh option')
|
||||
}
|
||||
const maskDeepSeekCredential = mode !== 'record' && options.deepSeekMissingCredential === true
|
||||
const messages = options.deepSeekMessages === true
|
||||
const originalDeepSeekCredential = process.env.DEEPSEEK_API_KEY
|
||||
let credentialEnvironmentRestored = false
|
||||
const restoreCredentialEnvironment = (): void => {
|
||||
@@ -516,10 +522,13 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
const patches: PatchOptions[] = [
|
||||
...basePatches,
|
||||
...surfacePatches,
|
||||
// Keyless scenarios retain the recorded default; explicit scenario overlays win.
|
||||
...mode === 'record' || options.deepSeekMissingCredential === true
|
||||
? []
|
||||
: [{ id: 'agent-default-model', config: { provider: 'deepseek-official', model: 'deepseek-v4-flash' } }],
|
||||
// The historical Messages fixture retains its recorded route during replay;
|
||||
// live configuration uses the shared DeepSeek route. Explicit overlays win.
|
||||
...messages
|
||||
? [{ id: 'agent-default-model', config: { provider: mode === 'record' || maskDeepSeekCredential ? 'deepseek-official' : 'deepseek-messages', model: maskDeepSeekCredential ? 'deepseek-flash' : 'deepseek-v4-flash' } }]
|
||||
: mode === 'record' || options.deepSeekMissingCredential === true
|
||||
? []
|
||||
: [{ id: 'agent-default-model', config: { provider: 'deepseek-official', model: 'deepseek-v4-flash' } }],
|
||||
...extraOverlayPatches,
|
||||
// The roster's shipped presets are the plugin's own, bundled inside
|
||||
// `dsh-agent-presets` and prepended by it. Pin only the machine-local
|
||||
@@ -644,9 +653,10 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
baseURL: options.deepSeekSearch.baseURL,
|
||||
},
|
||||
}],
|
||||
...mode === 'record' || options.deepSeekMissingCredential === true
|
||||
? []
|
||||
: [{ id: 'llm-deepseek', disabled: true }],
|
||||
...maskDeepSeekCredential && !messages ? [] : [
|
||||
{ id: 'llm-deepseek', disabled: mode !== 'record' && !maskDeepSeekCredential,
|
||||
config: { protocol: messages ? 'messages' : 'chat-completions' } },
|
||||
],
|
||||
]
|
||||
|
||||
// Sessions inherit the gateway's process.cwd() default; run the boot from
|
||||
@@ -722,7 +732,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
config: { path: pathToFileURL(rootConfig).href, patches },
|
||||
})
|
||||
await ctx.loader.await()
|
||||
assertEntriesLoaded(ctx, 'web e2e scaffold')
|
||||
await auditStartupEntries(ctx, 'web e2e scaffold')
|
||||
if (options.welcomeNoticePending !== true) {
|
||||
await ctx.settings.mutate(WELCOME_NOTICE_SETTINGS_NAMESPACE, [{
|
||||
op: 'set', path: [WELCOME_NOTICE_ACK_FIELD], value: WELCOME_NOTICE_VERSION,
|
||||
@@ -735,7 +745,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
port = boundPort
|
||||
|
||||
// Fill the open llm seam on the settled root ctx. Ordinary keyless modes
|
||||
// disable llm-deepseek; the first-run lane keeps it mounted but has no
|
||||
// disable the direct adapter; the first-run lane keeps the selected adapter but has no
|
||||
// replay fixture and never streams. The direct install, unlike the plugin
|
||||
// row, returns the ReplayHandle for the teardown consumption check.
|
||||
if (options.replayProvidersOnly) {
|
||||
@@ -772,7 +782,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
if (mode !== 'record' && replayFixture !== undefined) {
|
||||
replayHandle = installLlmReplay(ctx, {
|
||||
file: replayFixture,
|
||||
providers: (options.replayProviders ?? replayProviders(options.replayContextWindow)).map(provider => ({
|
||||
providers: (options.replayProviders ?? replayProviders(options.replayContextWindow, messages)).map(provider => ({
|
||||
...provider,
|
||||
...(options.replayRetryPolicy === undefined ? {} : { retryPolicy: options.replayRetryPolicy }),
|
||||
})),
|
||||
@@ -787,8 +797,8 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
// a fixture would, with streaming that still fails loud: the scenario
|
||||
// issues no model calls, and one that slipped in must not pass quietly.
|
||||
ctx.effect(() => ctx.llm.registerAdapter(
|
||||
replayProviders(options.replayContextWindow).map(provider => provider.id),
|
||||
new RouteOnlyAdapter(replayProviders(options.replayContextWindow)),
|
||||
replayProviders(options.replayContextWindow, messages).map(provider => provider.id),
|
||||
new RouteOnlyAdapter(replayProviders(options.replayContextWindow, messages)),
|
||||
), 'web e2e scaffold: route-only adapter')
|
||||
}
|
||||
baseUrl = `http://${browserHost}:${String(port)}`
|
||||
|
||||
@@ -79,6 +79,8 @@ afterEach(async () => {
|
||||
it('assembles the shipped Web transport, catalog, guidance, and defaults', async () => {
|
||||
scaffold = await launchWebScaffold({ deepSeekMissingCredential: true })
|
||||
const ctx = scaffold.ctx
|
||||
expect(ctx.llm.listProviders().some(provider => provider.id === 'deepseek-messages')).toBe(false)
|
||||
expect(ctx.agentDefaultModel.currentSelection()).toEqual({ provider: 'deepseek-official', model: 'deepseek-flash' })
|
||||
const index = await fetch(`http://127.0.0.1:${String(ctx.webServer.port)}`, {
|
||||
headers: { 'accept-encoding': 'gzip' },
|
||||
})
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
"tests/plugin-config.e2e.ts",
|
||||
"tests/settings-chrome.e2e.ts",
|
||||
"tests/models-settings.e2e.ts",
|
||||
"tests/deepseek-messages-settings.e2e.ts",
|
||||
"tests/deepseek-messages-chat.e2e.ts",
|
||||
"tests/models-settings-recovery.e2e.ts",
|
||||
"tests/default-model.e2e.ts",
|
||||
"tests/github-ready-review.e2e.ts",
|
||||
|
||||
Reference in new issue
Block a user