Merge remote-tracking branch 'origin/master' into worktree/drop-create-by-name
# Conflicts: # .agents/notes/implemented/feature/2026-07-25-session-list-browsing-and-manual-order.i18n.yaml # .agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.i18n.yaml # .agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.zh.md # .agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.i18n.yaml # .agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.md # .agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.zh.md # apps/cli/reference/README.i18n.yaml # docs/config-catalog.md # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/README.md # packages/host/apiproxy/README.zh.md # packages/host/apiproxy/src/api-proxy.ts # packages/host/apiproxy/src/index.ts # packages/host/apiproxy/tests/api-proxy-approval.spec.ts # packages/host/apiproxy/tests/api-proxy-blank.spec.ts # packages/host/apiproxy/tests/api-proxy-cold.spec.ts # packages/host/apiproxy/tests/api-proxy-commands.spec.ts # packages/host/apiproxy/tests/api-proxy-config.spec.ts # packages/host/apiproxy/tests/api-proxy-models.spec.ts # packages/host/apiproxy/tests/api-proxy-projections.spec.ts # packages/host/apiproxy/tests/api-proxy-question.spec.ts # packages/host/apiproxy/tests/api-proxy-rename.spec.ts # packages/host/apiproxy/tests/api-proxy-search.spec.ts # packages/host/apiproxy/tests/api-proxy-subagents.spec.ts # packages/host/apiproxy/tests/api-proxy-view.spec.ts # packages/host/apiproxy/tests/api-proxy-workspace.spec.ts # packages/todo/tool-todo/tests/projection.spec.ts # scripts/hero-composer-dom-continuity.mjs
This commit is contained in:
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/test-runtime/README.md
|
||||
README.md: 74da8fde7fd9cc3733d2d1ae03dd3d213e4d553e
|
||||
README.zh.md: a86b9e469a5632886891628267002a14588afeaa
|
||||
README.zh.md: 1df28f7b25c35333e91476e10480c22a728cdab3
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
面向 client feature 测试的 jsdom slot 测试运行时:真实 Cordis `Context`、生产 `SlotsService` 与 web-react 渲染器,围绕带类型的 session/workspace 测试替身组装。feature 套件无需逐套件手搭机器即可测遍声明、注册、scope、store、inject、渲染、更新与销毁——且不存在任何生产逻辑的第二份实现。
|
||||
面向客户端功能测试的 jsdom slot 测试运行时:真实 Cordis `Context`、生产 `SlotsService` 与 web-react 渲染器,围绕带类型的 session/workspace 测试替身组装。功能套件无需逐套件手搭机器即可测遍声明、注册、scope、store、inject、渲染、更新与销毁——且不存在任何生产逻辑的第二份实现。
|
||||
|
||||
替身实现的正是 feature 经 ctx 拿到的对外面(`TestSessions implements ISessions`、`TestWorkspaces implements IWorkspaces`;每个 fixture session 是 `FixtureSession implements SessionFace`),生产面一旦改形,测试台在编译期即断,而非静默漂移。provide bundle 材料化直接运行生产 `SessionProvideChannel`——与 `SessionsService` 共用同一份实现。fixture 灌入的是普通数据:列表行、会话快照(经 `updateSnapshot` 以 immer 补丁改写)、projection 值,以及按 `ISession` 取型的行为桩——spec 调用未打桩的动词时报错自明。带类型的 `provide()` 将已声明服务名的 fake 约束为该服务对外面的 `Partial` 子集。
|
||||
替身实现的正是功能通过 ctx 获得的对外接口(`TestSessions implements ISessions`、`TestWorkspaces implements IWorkspaces`;每个 fixture session 是 `FixtureSession implements SessionFace`),生产面一旦改形,测试台在编译期即断,而非静默漂移。provide bundle 材料化直接运行生产 `SessionProvideChannel`——与 `SessionsService` 共用同一份实现。fixture 灌入的是普通数据:列表行、会话快照(经 `updateSnapshot` 以 immer 补丁改写)、projection 值,以及按 `ISession` 取型的行为桩——spec 调用未打桩的动词时报错自明。带类型的 `provide()` 将已声明服务名的 fake 约束为该服务对外面的 `Partial` 子集。
|
||||
|
||||
局部 DOM 快照:`declare(children)` 注册自动 frame,逐 key 的 `<div data-slot>` 包裹层即快照根;`renderSlot(key, owner)` 返回该 slot 的局部视图(container、限定范围的 Testing Library 查询、原位 `update(owner)`);注册的快照序列化器把 CSS-module 哈希类名折回语义名(`_frame_a1b2c3` → `frame`)保持 `.snap` 只含结构,并把 `<svg>` 内部折叠为 `data-content` 指纹。需要自定义页面 frame 的套件改用 `root.declare(children, Frame)`;`mount(plugin)` 在真实 fiber 上运行并对缺失服务先行报错;`dispose()` 沿单一轴拆除视图、feature fiber、已铸 scope 与持久化 store 状态。
|
||||
|
||||
不属于产品插件图(无 `dshClient`);feature 包仅以 `devDependencies` 依赖之。
|
||||
|
||||
## Model Experience
|
||||
## 模型体验
|
||||
|
||||
无;本包是浏览器侧测试基础设施,无一物到达模型请求。
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
无;本包既不组装也不发送 provider 请求。
|
||||
无;本包既不组装也不发送提供方请求。
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
## 已知限制与延期工作
|
||||
|
||||
- **仅可经仓内源码别名消费。** spec 通过 tsconfig `paths` 解析到 `src`;构建产物 `lib/` 再导出 `@deepseek-ai/dsh-client-runtime/client`,而该 bundle 是无 Node ESM 导出的浏览器 loader 脚本,故 `lib/index.js` 在纯 Node 下不可导入。所有消费方都是仓内 Vitest 套件;不存在 Node 兼容的运行时入口。
|
||||
- **会话快照是 fixture 数据,不是重放历史。** `updateSnapshot` 直写快照 store;wire 到快照的运算仍由 runtime 包自身测试与 replay e2e 把守。因此 fixture 可以表达生产投影永不产出的状态。
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type {
|
||||
ConversationSnapshot, ISession, SessionId, SessionSummary, WorkspaceListState,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { EMPTY_CHAT_SNAPSHOT } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
/**
|
||||
* Fixture overrides for the session behavior face: any subset of the
|
||||
@@ -45,12 +46,12 @@ export interface SessionFixture {
|
||||
export function conversationSnapshot(sessionId: SessionId): ConversationSnapshot {
|
||||
return {
|
||||
sessionId,
|
||||
chat: EMPTY_CHAT_SNAPSHOT,
|
||||
nodes: [],
|
||||
turnTimings: new Map(),
|
||||
turnEnds: new Map(),
|
||||
partial: null,
|
||||
runningCalls: [],
|
||||
codeDispatches: new Map(),
|
||||
pending: [],
|
||||
queue: [],
|
||||
running: false,
|
||||
|
||||
@@ -22,7 +22,9 @@ import { act, render, within } from '@testing-library/react'
|
||||
import type { RenderResult } from '@testing-library/react'
|
||||
import type { queries } from '@testing-library/dom'
|
||||
import type { BoundFunctions } from '@testing-library/dom'
|
||||
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import {
|
||||
ConversationEventRegistry, ConversationViewRegistry, SlotsService,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { createSlotRenderer } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type {
|
||||
ChildrenDecl, ComposedProps, OwnerOf, SlotComponent, SlotMap, SlotRendererHost, StoreInstanceLike,
|
||||
@@ -40,7 +42,7 @@ export type { SessionBehaviorOverrides, SessionFixture, Stabilizer } from './fix
|
||||
export { makeTranslate } from './translate.ts'
|
||||
export { usePinnedBrowserLanguages } from './locale-env.ts'
|
||||
|
||||
/** Erased register face for the internal root call (the public declare seam holds the typing). */
|
||||
/** Erased register face for the internal root call (the public declaration contract holds the typing). */
|
||||
type ErasedRegister = (options: object, component: unknown) => () => void
|
||||
|
||||
/**
|
||||
@@ -80,7 +82,7 @@ export interface FeatureHandle {
|
||||
/**
|
||||
* Owner-props cell behind the auto frame: one external store the frame
|
||||
* subscribes to, so {@link SlotTestRuntime.renderSlot} and
|
||||
* {@link SlotView.update} drive React through the standard uSES seam.
|
||||
* {@link SlotView.update} drive React through the standard uSES boundary.
|
||||
*/
|
||||
class OwnerPropsCell {
|
||||
private readonly owners = new Map<string, object>()
|
||||
@@ -142,11 +144,11 @@ export class TestRoot {
|
||||
*/
|
||||
async declare<const D extends ChildrenDecl>(
|
||||
children: D,
|
||||
frame: SlotComponent<ComposedProps<'root', keyof NoInfer<D> & keyof SlotMap & string, undefined, object>>,
|
||||
frame: SlotComponent<ComposedProps<'root', never, keyof NoInfer<D> & keyof SlotMap & string, undefined, object>>,
|
||||
): Promise<void> {
|
||||
await this.stabilize(() => {
|
||||
// Erased hop (same pattern as SlotsService's own implementation arm);
|
||||
// the declare signature above is the typed seam.
|
||||
// the declaration signature above is the typed contract.
|
||||
this.disposeEntry = (this.slots.register as unknown as ErasedRegister)({ name: 'root', children }, frame)
|
||||
})
|
||||
}
|
||||
@@ -218,6 +220,8 @@ export class SlotTestRuntime {
|
||||
const ctx = new Context()
|
||||
const fiber = ctx.plugin(SlotsService)
|
||||
await fiber.await()
|
||||
await ctx.plugin(ConversationEventRegistry).await()
|
||||
await ctx.plugin(ConversationViewRegistry).await()
|
||||
return new SlotTestRuntime(ctx, ctx.get('slots') as SlotsService)
|
||||
}
|
||||
|
||||
@@ -267,7 +271,7 @@ export class SlotTestRuntime {
|
||||
|
||||
/**
|
||||
* Render the root slot tree through the ctx-level entry (the shell's own
|
||||
* seam): `ctx.slots.renderSlot('root', {})` under Testing Library.
|
||||
* entry point): `ctx.slots.renderSlot('root', {})` under Testing Library.
|
||||
* @returns the Testing Library view.
|
||||
*/
|
||||
renderRoot(): RenderResult {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Context } from 'cordis'
|
||||
import { createScope, scopeOf, SessionProvideChannel } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type {
|
||||
ConversationSnapshot, ISessions, ObservableSnapshot, ProjectionsFace, SessionFace, SessionId,
|
||||
AgentContext, ConversationSnapshot, ISessions, ObservableSnapshot, ProjectionsFace, SessionFace, SessionId,
|
||||
SessionListState, SessionProvideDescriptor, SessionSearchResultItem, SessionSummary, SnapshotStore,
|
||||
SubagentAddress,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
@@ -134,7 +134,7 @@ interface SessionRecord {
|
||||
summary: SessionSummary
|
||||
snapshot: SnapshotStore<ConversationSnapshot>
|
||||
session: FixtureSession
|
||||
scope: Context | undefined
|
||||
scope: AgentContext | undefined
|
||||
scopeFiber: { dispose(): Promise<void> } | undefined
|
||||
/** Materialized standard-props bundle (identity-stable per session; invalidated on roster change). */
|
||||
provideInfo: SessionProvideInfo | undefined
|
||||
@@ -144,7 +144,7 @@ interface SessionRecord {
|
||||
export interface TestSessionBinding {
|
||||
readonly sessionId: SessionId
|
||||
readonly session: FixtureSession
|
||||
readonly ctx: Context
|
||||
readonly ctx: AgentContext
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -345,7 +345,7 @@ export class TestSessions implements ISessions {
|
||||
* @param id - session id.
|
||||
* @returns the scoped context, or undefined for unknown sessions.
|
||||
*/
|
||||
scope(id: string): Context | undefined {
|
||||
scope(id: string): AgentContext | undefined {
|
||||
const record = this.records.get(id as SessionId)
|
||||
if (record === undefined) return undefined
|
||||
if (record.scope === undefined) {
|
||||
@@ -368,7 +368,7 @@ export class TestSessions implements ISessions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the session scope tag off a context (service-method seam mirror).
|
||||
* Read the session scope tag off a context (service-method boundary mirror).
|
||||
* @param ctx - any client context.
|
||||
* @returns the session id, or undefined on root contexts.
|
||||
*/
|
||||
@@ -430,6 +430,14 @@ export class TestSessions implements ISessions {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
/** Apply a confirmed preset switch into the fixture list, as production does. */
|
||||
noteAgentPreset(sessionId: SessionId, agentPreset: string): void {
|
||||
this.list.update((draft) => {
|
||||
const summary = draft.byId[sessionId]
|
||||
if (summary !== undefined) draft.byId[sessionId] = { ...summary, agentPreset }
|
||||
})
|
||||
}
|
||||
|
||||
/** Clear the current selection (recorded; the production no-session flow). */
|
||||
clear(): void {
|
||||
this.calls.push({ method: 'clear', args: [] })
|
||||
|
||||
@@ -18,6 +18,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
'trt.panel': { kind: 'single'; scope: 'root'; owner: { label?: string } }
|
||||
'trt.chat': { kind: 'single'; scope: 'session' }
|
||||
'trt.rows': { kind: 'list'; scope: 'root' }
|
||||
'trt.rows.hole': { kind: 'single'; scope: 'root' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +221,13 @@ describe('sessions', () => {
|
||||
.toMatchObject({ displayTitle: 'renamed', running: true })
|
||||
runtime.sessions.setSubagentCatalogOpen('s2' as SessionId, true)
|
||||
await runtime.sessions.refreshSubagents('s2' as SessionId)
|
||||
// The confirmed-switch write-back lands on the row it names and ignores
|
||||
// one the fixture never added, exactly as production's list upsert does.
|
||||
runtime.sessions.noteAgentPreset('s1' as SessionId, 'minimal')
|
||||
runtime.sessions.noteAgentPreset('missing' as SessionId, 'minimal')
|
||||
await runtime.flush()
|
||||
expect(runtime.sessions.list.getSnapshot().byId['s1' as SessionId])
|
||||
.toMatchObject({ agentPreset: 'minimal' })
|
||||
runtime.sessions.open('s1' as SessionId)
|
||||
await runtime.flush()
|
||||
expect(runtime.sessions.list.getSnapshot().current).toBe('s1')
|
||||
@@ -419,7 +427,7 @@ describe('feature mount and disposal', () => {
|
||||
await feature.dispose()
|
||||
await feature.dispose() // idempotent
|
||||
expect(runtime.slots.entries('trt.rows')).toHaveLength(0)
|
||||
expect(runtime.slots.spec('trt.rows.hole' as never)).toBeUndefined()
|
||||
expect(runtime.slots.spec('trt.rows.hole')).toBeUndefined()
|
||||
expect(runtime.ctx.get('feature-service')).toBeUndefined()
|
||||
expect(view.queryByTestId('row')).toBeNull()
|
||||
await runtime.dispose()
|
||||
|
||||
6
packages/client/test-runtime/tsdown.config.ts
Normal file
6
packages/client/test-runtime/tsdown.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clientLibrary } from '../tsdown.client.ts'
|
||||
|
||||
export default clientLibrary(
|
||||
'@deepseek-ai/dsh-client-test-runtime',
|
||||
['lib/types/index.js', 'lib/types/invariant.js'],
|
||||
)
|
||||
Reference in New Issue
Block a user