@crazx/dsh-client-test-runtime 0.1.5-rc.2.zw.1 → 0.1.6-alpha.1.zw.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +39 -4
- package/README.zh.md +40 -5
- package/lib/index.js +21 -60
- package/lib/types/assembly/bundle-roster.d.ts +15 -0
- package/lib/types/assembly/index.d.ts +15 -0
- package/lib/types/assembly/modules.d.ts +25 -0
- package/lib/types/assembly/remote-default-responses.d.ts +12 -0
- package/lib/types/assembly/remote-proxies.d.ts +20 -0
- package/lib/types/assembly/roster.d.ts +80 -0
- package/lib/types/assembly/test-client.d.ts +83 -0
- package/lib/types/assembly/vitest.d.ts +24 -0
- package/lib/types/index.d.ts +0 -4
- package/lib/types/workspaces.d.ts +6 -0
- package/package.json +53 -32
- package/lib/types/settings-remote.d.ts +0 -83
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/test-support/client-runtime/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 014bcbf8bc0ed9be596e2a20c07f57c80aaaf7dc
|
|
6
|
+
README.zh.md: a14a5281c58a5ea4a8a49e1a826397ae91e5f115
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
|
|
9
9
|
|
|
10
10
|
## Summary
|
|
11
11
|
|
|
12
|
-
`
|
|
12
|
+
`SlotTestRuntime.create()` lets Vitest suites drive production slots, stores, typed Session and Workspace fixtures, and local DOM assertions in jsdom. For plugin activation, reload, reconnect, and cleanup tests, `createClientTest` starts the web profile's bundle roster with endpoint-named Remote mocks, without a business Host. Missing services and unstubbed calls fail explicitly. The whole-client fixture owns startup and disposal; the local runtime provides idempotent disposal. Use this package through `devDependencies` for client tests; it is not a product plugin.
|
|
13
13
|
|
|
14
14
|
## Table of Contents
|
|
15
15
|
|
|
@@ -40,7 +40,7 @@ expect(view.container).toMatchSnapshot()
|
|
|
40
40
|
await runtime.dispose()
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
`mount` prechecks required services and fails loud when one is missing — `provide(name, value)` supplies an extra service first. The runtime provides
|
|
43
|
+
`mount` prechecks required services and fails loud when one is missing — `provide(name, value)` supplies an extra service first. The runtime provides a `fileUpload` stub that rejects every call until a suite replaces `runtime.fileUpload.upload`. `storeOf(key, scopeKey)` returns the live store instance the renderer hands a slot's component for identity and action-driven-write assertions.
|
|
44
44
|
|
|
45
45
|
The optional render options select a keyed entry with `entryKey` or a list item with `only`; `view.update(owner)` retains that selection. `runtime.panelInfo` supplies the default `usePanelInfo` source with no global panel selected. Release it with `releasePanelInfoSource()` before mounting the production Layout owner. `dispose()` releases both default Workspace and panel-info root sources; early release is idempotent and does not remove replacement owners.
|
|
46
46
|
|
|
@@ -64,9 +64,34 @@ remote.goals.create.mockResolvedValue({
|
|
|
64
64
|
expect(view.getByRole('alert')).toHaveTextContent('goal/not-found')
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
### Whole-client tier
|
|
68
|
+
|
|
69
|
+
The slot tier above mounts one feature against doubles. The whole-client tier boots the real assembly: `TestClient.start(plan, mock, options)` imports every roster row's `/client` module in-process (or takes the plan's `provide` replacement), binds `{ rpc: mock.rpc }` to that client's Connection module, synthesizes the boot graph with `graphFromRoster` and hands the loaded modules to the production module system, boots through the production `bootClient`, optionally mounts `uiRenderer`, and waits for `ctx.connection.state === 'connected'`. It lives behind a deep import so slot-tier specs never load it:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
// @vitest-environment jsdom
|
|
73
|
+
import { createClientTest, webApp } from '@deepseek-ai/dsh-client-test-runtime/src/assembly/index.ts'
|
|
74
|
+
import { ok } from '@deepseek-ai/dsh-remote-mock'
|
|
75
|
+
|
|
76
|
+
const test = createClientTest({ roster: webApp }, { mount: true })
|
|
77
|
+
test('registers into the sidebar', async ({ remote, start }) => {
|
|
78
|
+
remote.settings.describe.mockResolvedValue(ok({ writable: true, hasDocument: false, namespaces: [] }))
|
|
79
|
+
const client = await start()
|
|
80
|
+
expect(client.ctx.slots.entries('sidebar.settings')).toHaveLength(1)
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`createClientTest` uses native Vitest fixtures: each test gets a fresh `mock` loaded with `remoteDefaultResponses`, a `remote` proxy equal to `mock.remote`, and a `start()` that boots only after the test configures its responses. Repeated starts share one promise; await it to observe startup errors. Fixture teardown waits for startup, disposes the client even after a failed assertion, checks missing responses, and refuses a saved `start` after the test. Use `TestClient.start` directly for independently owned clients. These fixtures isolate their own state, not page globals such as `location`.
|
|
85
|
+
|
|
86
|
+
The [generic Remote proxy](../remote-mock/README.md#remote-proxy) supports every namespace in both tiers. An assembled test uses the `remote` fixture; a local `TestRemote` can receive `{ settings: mock.remote.settings }`. Configure returned data and read native `.mock.calls` directly. A mutation response does not update later describe answers automatically: change `remote.settings.describe.mockResolvedValue(...)` explicitly when the scenario publishes new data. The proxy documentation owns unbuilt typing and the required build-backed local typecheck.
|
|
87
|
+
|
|
88
|
+
### Roster and startup behavior
|
|
89
|
+
|
|
90
|
+
`webApp` is the `web` profile's browser roster, read when the assembly entry is first imported from its bundles (`dsh-base`, then `dsh-web-app`) as the launcher composes them, except that a patch matching nothing throws here where the launcher warns: each bundle's `dsh.bundle.patch` list is parsed with the include plugin's YAML dialect and composed by its `applyEntryPatches`, and every enabled row whose package declares `dsh.client.platform === 'web'` becomes a row carrying that declaration's `inject` and `immediately`; `bundleRoster(bundles)` does the same for any bundle list. Nothing is copied from the bundles, so a bundle change is visible at the next test run. `webApp.closure(names)` keeps the named rows plus everything they inject, transitively (the rows a spec needs to boot those plugins as the bundle composes them), `webApp.pick(names)` and `webApp.without(names)` cut it down by hand, all three throw on unknown names, and `ClientRoster.of(rows)` builds one inline. `remoteDefaultResponses` holds default responses for exactly the Remote endpoints the roster calls while booting with no sessions, no workspaces, and default settings; a spec layers its own `RemoteTable` on top with `mock.load(table)`, and any call without a rule fails the test at `dispose()` through `mock.assertNoUnmatched()`. `mount` requires a roster that provides `uiRenderer`; `start` fails loud otherwise instead of returning an empty container. `client.connection` is the roster's Connection service (no `Context` augmentation declares it), and `connectTimeoutMs` bounds the readiness wait, whose failure message lists the mock log. `reload(name)` rebuilds one Loader entry the way client-hmr does (registry-first teardown, then `entry.refresh()`); each client retains its own module system and instance-bound Connection replacement, so starts and reloads can overlap without changing a page global. `unload(name)` removes an entry and waits for plugin cleanup; `flush()` settles React inside `act`. jsdom implements neither `EventSource` (client-hmr opens one at apply) nor `ResizeObserver` (layout components observe size at mount), so `start` reference-counts inert stubs for whichever global is absent and `dispose` removes exactly those — a jsdom gap, not a client identity channel. The `@deepseek-ai/dsh-api-remotes` row is dropped from every roster: its generated Remote clients exist only in built `lib/`, and `remote.<ns>` is what the tier replaces anyway. `start` instead provides one contract-free proxy per `remote.<ns>` service the roster injects (plus the namespaces the mock has rules for at that point; a namespace first registered later has no proxy); `ctx.remote.<ns>.<method>(...args)` becomes a call on the endpoint `<ns>/<method>` carrying the positional args, a stream when the mock registered a `stream()` script and a unary call otherwise, with the generated client's outcome folding (`gateway/internal` for carrier throws, `gateway/cancelled` on abort). An endpoint without a rule is still dispatched, so the mock logs it and `dispose()` fails the test.
|
|
91
|
+
|
|
67
92
|
### When to use it
|
|
68
93
|
|
|
69
|
-
Use the bench for feature suites that exercise slots, stores, rendering, and disposal under a real runtime — the production `SlotRegistry`, renderer, and provide-bundle materialization are mounted, never reimplemented. It is
|
|
94
|
+
Use the bench for feature suites that exercise slots, stores, rendering, and disposal under a real runtime — the production `SlotRegistry`, renderer, and provide-bundle materialization are mounted, never reimplemented. It is client-side test infrastructure: it never reaches a model request, and feature packages depend on it in `devDependencies` only.
|
|
70
95
|
|
|
71
96
|
### What can go wrong
|
|
72
97
|
|
|
@@ -99,6 +124,13 @@ The bench copies no production logic: it mounts the production `SlotRegistry`, p
|
|
|
99
124
|
| [`src/remote.ts`](src/remote.ts) | `TestRemote` double for host RPC, `RemoteError` value re-export |
|
|
100
125
|
| [`src/translate.ts`](src/translate.ts) + [`src/locale-env.ts`](src/locale-env.ts) | Translation and pinned-browser-language test helpers |
|
|
101
126
|
| [`src/settings-scope.ts`](src/settings-scope.ts) | `stubSettingsScope` with test-driven publications and a write spy |
|
|
127
|
+
| [`src/assembly/roster.ts`](src/assembly/roster.ts) | `ClientRosterRow`, `ClientRoster` (`of`/`closure`/`pick`/`without`), the `AssemblyPlan` it annotates, and `graphFromRoster` |
|
|
128
|
+
| [`src/assembly/modules.ts`](src/assembly/modules.ts) | Source `/client` imports and replacements, registered through the production module facade's pending factory queue |
|
|
129
|
+
| [`src/assembly/test-client.ts`](src/assembly/test-client.ts) | `TestClient`: instance-bound Connection, shared jsdom shims, `bootClient`, mount, readiness wait, `reload`/`unload`/`dispose` |
|
|
130
|
+
| [`src/assembly/vitest.ts`](src/assembly/vitest.ts) | Test-scoped `mock` and lazy `start` fixtures |
|
|
131
|
+
| [`src/assembly/remote-default-responses.ts`](src/assembly/remote-default-responses.ts) | `remoteDefaultResponses`: default responses of the Remote endpoints the roster calls at boot |
|
|
132
|
+
| [`src/assembly/remote-proxies.ts`](src/assembly/remote-proxies.ts) | Contract-free `remote.<ns>` proxies over the Connection: `remoteNamespacesOf`, `remoteProxiesPlugin` |
|
|
133
|
+
| [`src/assembly/bundle-roster.ts`](src/assembly/bundle-roster.ts) | `bundleRoster` and `webApp`: the browser roster read from bundle patch files with the include plugin's own schema and patch application |
|
|
102
134
|
| — | No runtime invariant companion is published; this test-support package owns no production event stream or mutable data — it assembles the runtime SlotRegistry and renderer (whose packages own their invariants) around test doubles; its own behavior is exercised by its package tests. |
|
|
103
135
|
|
|
104
136
|
### Lifecycle
|
|
@@ -138,7 +170,10 @@ None; this package neither assembles nor sends a provider request.
|
|
|
138
170
|
|
|
139
171
|
These limits define how the bench is consumed. They are current package constraints, not a task backlog.
|
|
140
172
|
|
|
141
|
-
- **
|
|
173
|
+
- **The whole-client tier does not run the generated Remote clients** — `remote.<ns>` proxies forward positional arguments without the generated zod validation, wire-name mapping, or scoped-identity injection; mock handlers receive those arguments directly, and the generated clients stay covered by the built-artifact e2e lanes.
|
|
174
|
+
- **Proxied calls bypass the Gateway client's `invoke` and `invokeStream`** — no `$mount` lifecycle check runs, stream failures are not re-marked through `normalizeConnectionStream`, and a unary rejection is folded by the proxy itself with the Gateway client's exported `carrierFailure` and `cancelledFailure`. `ctx.remote.$stream`, `$on`, and `$host` are the real Gateway client's.
|
|
175
|
+
- **An undeclared endpoint is dispatched as a unary call** — the proxy learns each endpoint's mode from the mock's registrations; a stream endpoint the spec neither scripts nor declares (`RemoteTable.streams`, `mock.stream(endpoint)`) is logged as a `unary` miss and product code receives a folded result rather than a failing stream. `remoteDefaultResponses` declares the roster's later-opened streams; `dispose()` fails the test either way.
|
|
176
|
+
- **The package's client compile program adds the `node` ambient types** so the roster reader can use `node:fs`; the slot-tier sources compile against them as well.
|
|
142
177
|
- **Session, Conversation, and Chat fixtures stay separate** — `sessionSnapshot` contains only Session Controller state, `conversationSnapshot` contains target-neutral Conversation state, and `chatSnapshot` contains Chat target state. Assembly tests provide Session event entries instead of adding Conversation or Chat fields to `SessionSnapshot`.
|
|
143
178
|
|
|
144
179
|
<a id="dev-note"></a>
|
package/README.zh.md
CHANGED
|
@@ -9,7 +9,7 @@ kind: "package-library"
|
|
|
9
9
|
|
|
10
10
|
## 概述
|
|
11
11
|
|
|
12
|
-
`
|
|
12
|
+
`SlotTestRuntime.create()` 让 Vitest 套件在 jsdom 中驱动生产 slot、store、带类型的 Session 与 Workspace fixture,并对局部 DOM 断言。面向插件激活、重载、重连与清理的测试,`createClientTest` 使用具名端点 Remote mock 启动 web profile 的 bundle roster,无需业务 Host。缺失服务与未打桩调用会明确失败。整机 fixture 拥有启动和销毁,局部 runtime 提供幂等销毁。通过 `devDependencies` 将本包用于客户端测试;它不是产品插件。
|
|
13
13
|
|
|
14
14
|
## 目录
|
|
15
15
|
|
|
@@ -40,7 +40,7 @@ expect(view.container).toMatchSnapshot()
|
|
|
40
40
|
await runtime.dispose()
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
`mount` 会预检必需服务,缺失时自明报错——先用 `provide(name, value)`
|
|
43
|
+
`mount` 会预检必需服务,缺失时自明报错——先用 `provide(name, value)` 提供额外服务。运行时提供的 `fileUpload` 替身会拒绝每次调用,直到测试套件替换 `runtime.fileUpload.upload`。`storeOf(key, scopeKey)` 返回渲染器交给 slot 组件的实时存储实例,用于身份与动作驱动写入断言。
|
|
44
44
|
|
|
45
45
|
可选渲染参数通过 `entryKey` 选择 keyed 条目,或通过 `only` 选择 list 条目;`view.update(owner)` 保留该选择。`runtime.panelInfo` 提供默认的 `usePanelInfo` 数据源,初始不选中全局面板。挂载生产 Layout 所有者之前,先调用 `releasePanelInfoSource()` 释放该数据源。`dispose()` 同时释放默认的工作区与面板信息根数据源;提前释放是幂等的,不会移除替代它们的所有者。
|
|
46
46
|
|
|
@@ -64,9 +64,34 @@ remote.goals.create.mockResolvedValue({
|
|
|
64
64
|
expect(view.getByRole('alert')).toHaveTextContent('goal/not-found')
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
### 整体档
|
|
68
|
+
|
|
69
|
+
上面的 slot 档把一个功能挂在替身上。整体档起真实装配:`TestClient.start(plan, mock, options)` 进程内 import 每个 roster 行的 `/client` 模块(或取计划里的 `provide` 替换),把 `{ rpc: mock.rpc }` 绑定到该客户端的 Connection 模块,用 `graphFromRoster` 合成启动图并把已加载模块交给生产模块系统,经生产 `bootClient` 启动,按需挂载 `uiRenderer`,再等 `ctx.connection.state === 'connected'`。它藏在深 import 后面,slot 档测试永不加载它:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
// @vitest-environment jsdom
|
|
73
|
+
import { createClientTest, webApp } from '@deepseek-ai/dsh-client-test-runtime/src/assembly/index.ts'
|
|
74
|
+
import { ok } from '@deepseek-ai/dsh-remote-mock'
|
|
75
|
+
|
|
76
|
+
const test = createClientTest({ roster: webApp }, { mount: true })
|
|
77
|
+
test('registers into the sidebar', async ({ remote, start }) => {
|
|
78
|
+
remote.settings.describe.mockResolvedValue(ok({ writable: true, hasDocument: false, namespaces: [] }))
|
|
79
|
+
const client = await start()
|
|
80
|
+
expect(client.ctx.slots.entries('sidebar.settings')).toHaveLength(1)
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`createClientTest` 使用原生 Vitest fixture:每个测试获得已加载 `remoteDefaultResponses` 的新 `mock`、等同于 `mock.remote` 的 `remote` Proxy,以及配置应答后才起机的 `start()`。重复启动共用一个 Promise,调用方必须 await 它来观察启动错误。fixture 收尾等待启动,即使断言失败也销毁客户端、检查漏配,并拒绝测试结束后保存的 `start` 调用。需要分别拥有多个客户端时直接用 `TestClient.start`。这些 fixture 隔离自己的状态,不隔离 `location` 等页面全局。
|
|
85
|
+
|
|
86
|
+
两档测试的所有命名空间都使用[通用 Remote Proxy](../remote-mock/README.zh.md#remote-proxy)。装配测试使用 `remote` fixture;局部 `TestRemote` 可以接收 `{ settings: mock.remote.settings }`。直接配置返回数据,并读取原生 `.mock.calls`。mutation 应答不会自动更新后续 describe 应答:场景发布新数据时,显式修改 `remote.settings.describe.mockResolvedValue(...)`。Proxy 文档拥有无构建类型说明和必需的构建后本地类型检查规则。
|
|
87
|
+
|
|
88
|
+
### Roster 与启动行为
|
|
89
|
+
|
|
90
|
+
`webApp` 是 `web` profile 的浏览器 roster,首次 import 装配入口时从它的 bundle(先 `dsh-base`、再 `dsh-web-app`)按启动器的方式现读,只是匹配不到任何行的补丁在这里抛错、启动器只警告:每个 bundle 的 `dsh.bundle.patch` 列表用 include 插件的 YAML 方言解析、用它的 `applyEntryPatches` 合成,每个未禁用且其包声明 `dsh.client.platform === 'web'` 的行成为一行,带上该声明的 `inject` 与 `immediately`;`bundleRoster(bundles)` 对任意 bundle 列表做同样的事。没有任何东西从 bundle 拷贝出来,bundle 一改下次跑测试就能看见。`webApp.closure(names)` 保留点名的行及其传递注入的全部行(即按 bundle 组合方式起这些插件所需的行),`webApp.pick(names)` 与 `webApp.without(names)` 手工裁剪,三者都对未知名字抛错,`ClientRoster.of(rows)` 内联构造一份。`remoteDefaultResponses` 是 roster 在没有 session、没有 workspace、默认设置下启动时恰好会打的那些 Remote 端点的默认响应;测试用 `mock.load(table)` 在其上叠加自己的 `RemoteTable`,任何没有规则的调用都会在 `dispose()` 时经 `mock.assertNoUnmatched()` 让测试失败。`mount` 要求 roster 提供 `uiRenderer`;否则 `start` 响亮失败而不是返回一个空容器。`client.connection` 是 roster 的 Connection 服务(没有任何 `Context` 增强声明它),`connectTimeoutMs` 限定等就绪的时长,超时消息列出 mock log。`reload(name)` 按 client-hmr 的方式重建一个 Loader entry(先拆 registry,再 `entry.refresh()`);每个客户端都保留自己的模块系统和绑定实例的 Connection 替换,因此启动与重载可以重叠而无需改写页面全局变量。`unload(name)` 移除 entry 并等待插件清理完成,`flush()` 在 `act` 内让 React 落定。jsdom 既没有 `EventSource`(client-hmr 在 apply 时打开一个)也没有 `ResizeObserver`(布局组件挂载时观察尺寸),所以 `start` 对缺失的全局惰性桩做引用计数,`dispose` 只移除它装的那些——这是 jsdom 的缺口,不是客户端身份通道。每个 roster 里的 `@deepseek-ai/dsh-api-remotes` 行都会被去掉:它生成的 Remote 客户端只存在于构建后的 `lib/`,而 `remote.<ns>` 正是本档要替掉的东西。`start` 改为给 roster 注入的每个 `remote.<ns>` 服务(加上此刻 mock 登记过规则的命名空间;之后才首次登记的命名空间没有代理)提供一个无契约代理;`ctx.remote.<ns>.<method>(...args)` 变成对端点 `<ns>/<method>` 的调用,携带位置参数,mock 登记了 `stream()` 脚本的走流、否则走一元,并沿用生成客户端的结果折叠(载体抛错折成 `gateway/internal`,中止折成 `gateway/cancelled`)。没有规则的端点照样发出,所以 mock 会记下它、`dispose()` 让测试失败。
|
|
91
|
+
|
|
67
92
|
### 何时使用
|
|
68
93
|
|
|
69
|
-
当功能套件要在真实运行时下检验 slot、存储、渲染与销毁时使用本测试台——生产 `SlotRegistry`、渲染器与 provide bundle
|
|
94
|
+
当功能套件要在真实运行时下检验 slot、存储、渲染与销毁时使用本测试台——生产 `SlotRegistry`、渲染器与 provide bundle 物化都会被挂载,绝不重实现。它是客户端测试基础设施:永远不触及模型请求,功能包仅以 `devDependencies` 依赖之。
|
|
70
95
|
|
|
71
96
|
### 可能出什么问题
|
|
72
97
|
|
|
@@ -99,7 +124,14 @@ expect(view.getByRole('alert')).toHaveTextContent('goal/not-found')
|
|
|
99
124
|
| [`src/remote.ts`](src/remote.ts) | 用于 host RPC 的 `TestRemote` 替身、`RemoteError` 值转出 |
|
|
100
125
|
| [`src/translate.ts`](src/translate.ts) + [`src/locale-env.ts`](src/locale-env.ts) | 翻译与固定浏览器语言测试辅助 |
|
|
101
126
|
| [`src/settings-scope.ts`](src/settings-scope.ts) | 带测试驱动发布与写入 spy 的 `stubSettingsScope` |
|
|
102
|
-
|
|
|
127
|
+
| [`src/assembly/roster.ts`](src/assembly/roster.ts) | `ClientRosterRow`、`ClientRoster`(`of`/`closure`/`pick`/`without`)、它所标注的 `AssemblyPlan`,以及 `graphFromRoster` |
|
|
128
|
+
| [`src/assembly/modules.ts`](src/assembly/modules.ts) | 源码 `/client` 导入及替换,通过生产模块 facade 的待注册工厂队列登记 |
|
|
129
|
+
| [`src/assembly/test-client.ts`](src/assembly/test-client.ts) | `TestClient`:绑定实例的 Connection、共享 jsdom 桩、`bootClient`、挂载、等就绪、`reload`/`unload`/`dispose` |
|
|
130
|
+
| [`src/assembly/vitest.ts`](src/assembly/vitest.ts) | 测试级 `mock` 与懒启动 `start` fixture |
|
|
131
|
+
| [`src/assembly/remote-default-responses.ts`](src/assembly/remote-default-responses.ts) | `remoteDefaultResponses`:roster 启动期 Remote 端点的默认响应 |
|
|
132
|
+
| [`src/assembly/remote-proxies.ts`](src/assembly/remote-proxies.ts) | 经 Connection 的无契约 `remote.<ns>` 代理:`remoteNamespacesOf`、`remoteProxiesPlugin` |
|
|
133
|
+
| [`src/assembly/bundle-roster.ts`](src/assembly/bundle-roster.ts) | `bundleRoster` 与 `webApp`:用 include 插件自己的 schema 与补丁应用从 bundle 补丁文件读出浏览器 roster |
|
|
134
|
+
| — | 不发布运行时不变式伴生入口;本测试支持包不拥有生产事件流或可变数据,而是围绕测试替身组装生产 SlotRegistry 与渲染器。所挂载的生产包拥有各自的不变式,本包行为由本包测试检验。 |
|
|
103
135
|
|
|
104
136
|
### 生命周期
|
|
105
137
|
|
|
@@ -138,7 +170,10 @@ expect(view.getByRole('alert')).toHaveTextContent('goal/not-found')
|
|
|
138
170
|
|
|
139
171
|
这些限制说明本测试台如何被消费。它们是当前包约束,不是任务积压。
|
|
140
172
|
|
|
141
|
-
-
|
|
173
|
+
- **整体档不运行生成的 Remote 客户端**——`remote.<ns>` 代理转发位置参数,不经过生成的 zod 校验、wire 名映射或 scoped 身份注入;mock handler 直接接收这些参数,生成客户端仍由 built-artifact e2e 车道覆盖。
|
|
174
|
+
- **代理调用绕过 Gateway 客户端的 `invoke` 与 `invokeStream`**——不做 `$mount` 生命周期检查,流失败不经 `normalizeConnectionStream` 重新标记,一元拒绝由代理自己用 Gateway 客户端导出的 `carrierFailure` 与 `cancelledFailure` 折叠。`ctx.remote.$stream`、`$on`、`$host` 是真 Gateway 客户端的。
|
|
175
|
+
- **未声明的端点按一元调用发出**——代理从 mock 的登记学到每个端点的模式;spec 既没给脚本也没声明(`RemoteTable.streams`、`mock.stream(endpoint)`)的流端点记为 `unary` 漏配,产品代码收到的是折叠结果而不是失败的流。`remoteDefaultResponses` 声明了 roster 启动后才打开的流;无论哪种,`dispose()` 都会让测试失败。
|
|
176
|
+
- **本包的 client 编译程序加了 `node` 环境类型**,好让 roster 读取器使用 `node:fs`;slot 档的源码也在这些类型下编译。
|
|
142
177
|
- **Session、Conversation 与 Chat fixture 保持分离**——`sessionSnapshot` 只包含 Session 控制器状态,`conversationSnapshot` 包含与目标无关的 Conversation 状态,`chatSnapshot` 包含 Chat 目标状态。组装测试提供 Session 事件条目,而不是向 `SessionSnapshot` 添加 Conversation 或 Chat 字段。
|
|
143
178
|
|
|
144
179
|
<a id="dev-note"></a>
|
package/lib/index.js
CHANGED
|
@@ -818,6 +818,25 @@ var TestWorkspaces = class {
|
|
|
818
818
|
draft.archivedSessionIds = [...draft.archivedSessionIds, sessionId];
|
|
819
819
|
});
|
|
820
820
|
}
|
|
821
|
+
/**
|
|
822
|
+
* Unarchive a session (recorded). The default mirrors the production face's
|
|
823
|
+
* observable effect: the id leaves the list state's archive set.
|
|
824
|
+
* @param sessionId - session to unarchive.
|
|
825
|
+
*/
|
|
826
|
+
async unarchiveSession(sessionId) {
|
|
827
|
+
this.calls.push({
|
|
828
|
+
method: "unarchiveSession",
|
|
829
|
+
args: [sessionId]
|
|
830
|
+
});
|
|
831
|
+
const stub = this.stubs.get("unarchiveSession");
|
|
832
|
+
if (stub !== void 0) {
|
|
833
|
+
await stub(sessionId);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
await this.update((draft) => {
|
|
837
|
+
draft.archivedSessionIds = draft.archivedSessionIds.filter((id) => id !== sessionId);
|
|
838
|
+
});
|
|
839
|
+
}
|
|
821
840
|
};
|
|
822
841
|
//#endregion
|
|
823
842
|
//#region lib/types/settings-scope.js
|
|
@@ -868,61 +887,6 @@ function stubSettingsScope() {
|
|
|
868
887
|
};
|
|
869
888
|
}
|
|
870
889
|
//#endregion
|
|
871
|
-
//#region lib/types/settings-remote.js
|
|
872
|
-
/** Test double for the `settings` Remote namespace a bench's plugins inject. */
|
|
873
|
-
/**
|
|
874
|
-
* Build a scripted `settings` Remote namespace for a bench. Each write answers
|
|
875
|
-
* with the addressed namespace unchanged, so a bench that only needs its
|
|
876
|
-
* plugins to activate scripts nothing; one asserting a write reads the
|
|
877
|
-
* corresponding spy or replaces the face.
|
|
878
|
-
* @param namespaces - namespace views the first describe answers with.
|
|
879
|
-
* @param options - deployment facts the describe answer reports.
|
|
880
|
-
* @returns the face and its controls.
|
|
881
|
-
*/
|
|
882
|
-
function scriptedSettingsRemote(namespaces = [], options = {}) {
|
|
883
|
-
let served = namespaces;
|
|
884
|
-
const writable = options.writable ?? true;
|
|
885
|
-
const hasDocument = options.hasDocument ?? false;
|
|
886
|
-
const answer = (ns) => {
|
|
887
|
-
const view = served.find((candidate) => candidate.ns === ns);
|
|
888
|
-
return Promise.resolve(view === void 0 ? {
|
|
889
|
-
ok: false,
|
|
890
|
-
error: {
|
|
891
|
-
code: "settings/rejected",
|
|
892
|
-
message: `no scripted namespace "${ns}"`,
|
|
893
|
-
details: { ns }
|
|
894
|
-
}
|
|
895
|
-
} : {
|
|
896
|
-
ok: true,
|
|
897
|
-
value: view
|
|
898
|
-
});
|
|
899
|
-
};
|
|
900
|
-
const update = vi.fn((ns, _patch, _expectedRevision) => answer(ns));
|
|
901
|
-
const replace = vi.fn((ns, _section, _expectedRevision) => answer(ns));
|
|
902
|
-
const mutate = vi.fn((ns, _ops, _expectedRevision) => answer(ns));
|
|
903
|
-
return {
|
|
904
|
-
settings: {
|
|
905
|
-
describe: () => Promise.resolve({
|
|
906
|
-
ok: true,
|
|
907
|
-
value: {
|
|
908
|
-
writable,
|
|
909
|
-
hasDocument,
|
|
910
|
-
namespaces: served
|
|
911
|
-
}
|
|
912
|
-
}),
|
|
913
|
-
update: (ns, patch, expectedRevision) => update(ns, patch, expectedRevision),
|
|
914
|
-
replace: (ns, section, expectedRevision) => replace(ns, section, expectedRevision),
|
|
915
|
-
mutate: (ns, ops, expectedRevision) => mutate(ns, ops, expectedRevision)
|
|
916
|
-
},
|
|
917
|
-
update,
|
|
918
|
-
replace,
|
|
919
|
-
mutate,
|
|
920
|
-
publish(next) {
|
|
921
|
-
served = next;
|
|
922
|
-
}
|
|
923
|
-
};
|
|
924
|
-
}
|
|
925
|
-
//#endregion
|
|
926
890
|
//#region lib/types/remote.js
|
|
927
891
|
/**
|
|
928
892
|
* Remote service test double for the forwarded-event path. Feature specs need
|
|
@@ -1214,10 +1178,7 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1214
1178
|
this.root = new TestRoot(slots, this.stabilizer);
|
|
1215
1179
|
this.sessions = new TestSessions(this.stabilizer, ctx);
|
|
1216
1180
|
this.workspaces = new TestWorkspaces(this.stabilizer);
|
|
1217
|
-
this.fileUpload = {
|
|
1218
|
-
available: false,
|
|
1219
|
-
upload: () => Promise.reject(/* @__PURE__ */ new Error("client test runtime: file upload is not stubbed"))
|
|
1220
|
-
};
|
|
1181
|
+
this.fileUpload = { upload: () => Promise.reject(/* @__PURE__ */ new Error("client test runtime: file upload is not stubbed")) };
|
|
1221
1182
|
ctx.provide("sessions", this.sessions);
|
|
1222
1183
|
ctx.provide("workspaces", this.workspaces);
|
|
1223
1184
|
ctx.provide("fileUpload", this.fileUpload);
|
|
@@ -1386,4 +1347,4 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1386
1347
|
}
|
|
1387
1348
|
};
|
|
1388
1349
|
//#endregion
|
|
1389
|
-
export { FixtureSession, RemoteError, SlotTestRuntime, TestRemote, TestRoot, TestSessions, TestWorkspaces, bindSnapshotSelector, chatSnapshot, conversationSnapshot, createSlotRenderer, domSnapshotSerializer, makeTranslate, registerDomSnapshotSerializer,
|
|
1350
|
+
export { FixtureSession, RemoteError, SlotTestRuntime, TestRemote, TestRoot, TestSessions, TestWorkspaces, bindSnapshotSelector, chatSnapshot, conversationSnapshot, createSlotRenderer, domSnapshotSerializer, makeTranslate, registerDomSnapshotSerializer, sessionSnapshot, stubSettingsScope, usePinnedBrowserLanguages, workspaceSnapshot };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ClientRoster } from './roster.ts';
|
|
2
|
+
/** The `web` profile's bundle layers, in the order `dsh --profile web` applies them (app-boot `PROFILE_TEMPLATES.web`). */
|
|
3
|
+
export declare const WEB_PROFILE_BUNDLES: readonly string[];
|
|
4
|
+
/**
|
|
5
|
+
* Compose the browser roster of `bundles`, applied in order.
|
|
6
|
+
* @param bundles - bundle package names in application order.
|
|
7
|
+
* @param anchor - file whose package resolution locates the bundles; default this package.
|
|
8
|
+
* @returns the roster in composition order, one row per package.
|
|
9
|
+
* @throws {Error} when a bundle, its patch file, or an enabled row's package does not resolve, when the patch list
|
|
10
|
+
* is not a list or does not apply as written, or when a browser row's `disabled` is a `!!js` expression.
|
|
11
|
+
*/
|
|
12
|
+
export declare function bundleRoster(bundles: readonly string[], anchor?: string): ClientRoster;
|
|
13
|
+
/** The `web` profile's browser roster, composed from its bundles at import. */
|
|
14
|
+
export declare const webApp: ClientRoster;
|
|
15
|
+
//# sourceMappingURL=bundle-roster.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whole-client tier entry (deep import only:
|
|
3
|
+
* `@deepseek-ai/dsh-client-test-runtime/src/assembly/index.ts`). Kept out of
|
|
4
|
+
* the package root so slot-tier specs do not load the assembly machinery.
|
|
5
|
+
* @module @deepseek-ai/dsh-client-test-runtime/src/assembly
|
|
6
|
+
*/
|
|
7
|
+
export { ClientRoster } from './roster.ts';
|
|
8
|
+
export type { AssemblyPlan, ClientPluginModule, ClientRosterRow } from './roster.ts';
|
|
9
|
+
export { TestClient } from './test-client.ts';
|
|
10
|
+
export type { TestClientOptions } from './test-client.ts';
|
|
11
|
+
export { remoteDefaultResponses } from './remote-default-responses.ts';
|
|
12
|
+
export { bundleRoster, webApp } from './bundle-roster.ts';
|
|
13
|
+
export { createClientTest } from './vitest.ts';
|
|
14
|
+
export type { ClientTestFixtures } from './vitest.ts';
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ClientModuleLoader, WebBootGraph } from '@deepseek-ai/dsh-client-modules/client';
|
|
2
|
+
import type { AssemblyPlan, ClientPluginModule } from './roster.ts';
|
|
3
|
+
/** The bootstrap row: always this process's static namespace, never a dynamic import or a `provide` replacement. */
|
|
4
|
+
export declare const MODULES_PACKAGE = "@deepseek-ai/dsh-client-modules";
|
|
5
|
+
/**
|
|
6
|
+
* Resolve each roster row to its plugin module: `plan.provide[name]` when
|
|
7
|
+
* present, otherwise a `/client` import resolved by the repository's tsconfig
|
|
8
|
+
* path aliases under Vitest. The bootstrap row is the
|
|
9
|
+
* statically imported `@deepseek-ai/dsh-client-modules/client` namespace.
|
|
10
|
+
* @param plan - validated plan.
|
|
11
|
+
* @returns package name → module, in roster order.
|
|
12
|
+
* @throws {Error} when an import fails (the package name prefixes the original message) or the bootstrap row is provided.
|
|
13
|
+
*/
|
|
14
|
+
export declare function loadPluginModules(plan: AssemblyPlan): Promise<ReadonlyMap<string, ClientPluginModule>>;
|
|
15
|
+
/**
|
|
16
|
+
* Build the production module system over queued factories returning the
|
|
17
|
+
* loaded namespaces. The bootstrap row uses `bootstrapModule`; `staticModules`
|
|
18
|
+
* is empty because namespaces already hold their own imports. Missing factories
|
|
19
|
+
* reject through `loadBundle` without fetching.
|
|
20
|
+
* @param graph - raw boot graph from `graphFromRoster`; `createClientModuleSystem` parses it.
|
|
21
|
+
* @param modules - loaded plugin modules keyed by package name.
|
|
22
|
+
* @returns module system to install as `loader.internal`; its `manifest` is the parsed graph.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createInProcessModules(graph: WebBootGraph, modules: ReadonlyMap<string, ClientPluginModule>): ClientModuleLoader;
|
|
25
|
+
//# sourceMappingURL=modules.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default responses for every Remote endpoint the web assembly calls while
|
|
3
|
+
* booting and rendering with no sessions, no workspaces, and default settings.
|
|
4
|
+
* The comment above each row names the plugin that calls it; endpoints boot
|
|
5
|
+
* never touches stay absent so a new call fails loud. `$events` is built into
|
|
6
|
+
* `RemoteMock`.
|
|
7
|
+
* @module @deepseek-ai/dsh-client-test-runtime/src/assembly/remote-default-responses
|
|
8
|
+
*/
|
|
9
|
+
import { type RemoteTable } from '@deepseek-ai/dsh-remote-mock';
|
|
10
|
+
/** Default responses of the boot-time Remote endpoints; a spec loads it first and layers its own table on top. */
|
|
11
|
+
export declare const remoteDefaultResponses: RemoteTable;
|
|
12
|
+
//# sourceMappingURL=remote-default-responses.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RemoteMock } from '@deepseek-ai/dsh-remote-mock';
|
|
2
|
+
import type { ClientPluginModule } from './roster.ts';
|
|
3
|
+
/** The assembly row the proxies stand in for; its generated clients exist only in built `lib/`. */
|
|
4
|
+
export declare const REMOTES_PACKAGE = "@deepseek-ai/dsh-api-remotes";
|
|
5
|
+
/**
|
|
6
|
+
* Namespaces to provide: every `remote.<ns>` a roster module injects, plus the
|
|
7
|
+
* namespace of every endpoint the mock has a rule for.
|
|
8
|
+
* @param modules - loaded roster modules.
|
|
9
|
+
* @param mock - the spec's mock.
|
|
10
|
+
* @returns sorted namespace names.
|
|
11
|
+
*/
|
|
12
|
+
export declare function remoteNamespacesOf(modules: Iterable<ClientPluginModule>, mock: RemoteMock): readonly string[];
|
|
13
|
+
/**
|
|
14
|
+
* Plugin providing the namespace proxies; `TestClient.start` mounts it before the Loader rows.
|
|
15
|
+
* @param namespaces - namespaces to provide.
|
|
16
|
+
* @param mock - the spec's mock, asked for each endpoint's mode.
|
|
17
|
+
* @returns the plugin.
|
|
18
|
+
*/
|
|
19
|
+
export declare function remoteProxiesPlugin(namespaces: readonly string[], mock: RemoteMock): ClientPluginModule;
|
|
20
|
+
//# sourceMappingURL=remote-proxies.d.ts.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client roster: the ordered package-name rows a whole-client test boots, and
|
|
3
|
+
* the plan that annotates one with the rows the test provides itself. `webApp`
|
|
4
|
+
* and `bundleRoster` (`./bundle-roster.ts`) read rosters from the bundle patch
|
|
5
|
+
* files; a spec may also build one inline with {@link ClientRoster.of}.
|
|
6
|
+
* @module @deepseek-ai/dsh-client-test-runtime/src/assembly/roster
|
|
7
|
+
*/
|
|
8
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
9
|
+
import type { WebBootGraph } from '@deepseek-ai/dsh-client-modules/client';
|
|
10
|
+
/** One browser plugin row as `dsh.client` declares it, keyed by package name. */
|
|
11
|
+
export interface ClientRosterRow {
|
|
12
|
+
/** Package name (== manifest entry id == Loader entry name). */
|
|
13
|
+
readonly name: string;
|
|
14
|
+
/** Package-name dependency edges from `dsh.client.inject` ([] when absent). */
|
|
15
|
+
readonly inject: readonly string[];
|
|
16
|
+
/** Stage-one prefetch mark from `dsh.client.immediately` (false when absent). */
|
|
17
|
+
readonly immediately: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Synthesize the raw `WebBootGraph` for `rows`: one `application` batch
|
|
21
|
+
* holding every row, `rev: 'local'`, placeholder `/plugins/<name>/client.js`
|
|
22
|
+
* URLs, since every module is seeded in process and never fetched. Validation
|
|
23
|
+
* stays with the production `parseBootManifest` inside the module system:
|
|
24
|
+
* duplicate names and an empty roster are rejected there, not here.
|
|
25
|
+
* @param rows - roster rows in composition order.
|
|
26
|
+
* @returns the unparsed graph, as `createClientModuleSystem` consumes it.
|
|
27
|
+
*/
|
|
28
|
+
export declare function graphFromRoster(rows: readonly ClientRosterRow[]): WebBootGraph;
|
|
29
|
+
/** Immutable, name-addressable roster. */
|
|
30
|
+
export declare class ClientRoster {
|
|
31
|
+
readonly rows: readonly ClientRosterRow[];
|
|
32
|
+
/**
|
|
33
|
+
* Build a roster from rows; duplicate names throw.
|
|
34
|
+
* @param rows - roster rows in composition order.
|
|
35
|
+
* @returns roster.
|
|
36
|
+
*/
|
|
37
|
+
static of(rows: readonly ClientRosterRow[]): ClientRoster;
|
|
38
|
+
private constructor();
|
|
39
|
+
/**
|
|
40
|
+
* Keep only `names`, preserving roster order; an unknown name throws with the roster listed.
|
|
41
|
+
* @param names - package names to keep.
|
|
42
|
+
* @returns sub-roster.
|
|
43
|
+
*/
|
|
44
|
+
pick(names: readonly string[]): ClientRoster;
|
|
45
|
+
/**
|
|
46
|
+
* The named rows plus every row they inject, transitively, in roster order: the rows a spec needs to boot the
|
|
47
|
+
* named plugins as the bundle composes them. The shell's platform modules (`PLATFORM_MODULES`, seeded statically
|
|
48
|
+
* rather than loaded as rows) end the walk. An unknown name throws with the roster listed; a row injecting any
|
|
49
|
+
* other package outside the roster throws, since the bundle itself would not boot.
|
|
50
|
+
* @param names - package names whose dependency cone to keep.
|
|
51
|
+
* @returns sub-roster.
|
|
52
|
+
*/
|
|
53
|
+
closure(names: readonly string[]): ClientRoster;
|
|
54
|
+
/**
|
|
55
|
+
* Drop `names`; an unknown name throws with the roster listed.
|
|
56
|
+
* @param names - package names to drop.
|
|
57
|
+
* @returns sub-roster.
|
|
58
|
+
*/
|
|
59
|
+
without(names: readonly string[]): ClientRoster;
|
|
60
|
+
private known;
|
|
61
|
+
}
|
|
62
|
+
/** The module face the Loader materializes for one client plugin row. */
|
|
63
|
+
export interface ClientPluginModule {
|
|
64
|
+
apply(ctx: Context, config?: unknown): unknown;
|
|
65
|
+
readonly inject?: readonly string[] | Readonly<Record<string, unknown>>;
|
|
66
|
+
readonly Config?: unknown;
|
|
67
|
+
}
|
|
68
|
+
/** What to boot and what the test supplies itself. */
|
|
69
|
+
export interface AssemblyPlan {
|
|
70
|
+
readonly roster: ClientRoster;
|
|
71
|
+
/** Row replacements by package name (the test's own implementation of that row). Names outside the roster throw. */
|
|
72
|
+
readonly provide?: Readonly<Record<string, ClientPluginModule>>;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Validate a plan against its roster.
|
|
76
|
+
* @param plan - plan to check.
|
|
77
|
+
* @throws {Error} naming any `provide` key outside the roster.
|
|
78
|
+
*/
|
|
79
|
+
export declare function assertPlan(plan: AssemblyPlan): void;
|
|
80
|
+
//# sourceMappingURL=roster.d.ts.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whole-client test carrier: boots an {@link AssemblyPlan} through the
|
|
3
|
+
* production `bootClient` over an in-process module table, with a
|
|
4
|
+
* `RemoteMock` bound to that client's Connection plugin instance.
|
|
5
|
+
* @module @deepseek-ai/dsh-client-test-runtime/src/assembly/test-client
|
|
6
|
+
*/
|
|
7
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
8
|
+
import { type ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client';
|
|
9
|
+
import type { RemoteMock } from '@deepseek-ai/dsh-remote-mock';
|
|
10
|
+
import { type AssemblyPlan } from './roster.ts';
|
|
11
|
+
/** Carrier options. */
|
|
12
|
+
export interface TestClientOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Mount `uiRenderer` into an element (a fresh `document.body` child when `true`); requires jsdom and a roster
|
|
15
|
+
* that provides `uiRenderer`. Default false.
|
|
16
|
+
*/
|
|
17
|
+
readonly mount?: boolean | HTMLElement;
|
|
18
|
+
/** Wait for `ctx.connection.state === 'connected'` before returning. Default true. */
|
|
19
|
+
readonly awaitConnected?: boolean;
|
|
20
|
+
/** Readiness budget in milliseconds before `start` rejects with the mock log summary. Default 5000. */
|
|
21
|
+
readonly connectTimeoutMs?: number;
|
|
22
|
+
}
|
|
23
|
+
/** A booted client under test. */
|
|
24
|
+
export declare class TestClient {
|
|
25
|
+
readonly ctx: Context;
|
|
26
|
+
readonly mock: RemoteMock;
|
|
27
|
+
readonly container: HTMLElement | undefined;
|
|
28
|
+
private readonly restore;
|
|
29
|
+
/**
|
|
30
|
+
* Load the roster's modules, bind this client's mock to its Connection row,
|
|
31
|
+
* hold the jsdom shims, and boot through `bootClient` over the synthesized
|
|
32
|
+
* boot graph; afterwards optionally mount and wait for the connection. The
|
|
33
|
+
* bound row replaces only the page-global input adapter: both paths call
|
|
34
|
+
* `installConnection`, while this path supplies the mock carrier, uses
|
|
35
|
+
* default recovery timings, and captures the current page hostname once for
|
|
36
|
+
* later reloads. A caller-provided Connection row remains unchanged and owns
|
|
37
|
+
* its readiness behavior. The `@deepseek-ai/dsh-api-remotes` row is
|
|
38
|
+
* dropped from the roster: its generated Remote clients exist only in built
|
|
39
|
+
* `lib/`, and the `remote.<ns>` services the roster injects (plus the
|
|
40
|
+
* namespaces the mock has rules for at this point) are provided as
|
|
41
|
+
* contract-free proxies over the same Connection instead; a `provide` entry
|
|
42
|
+
* for that row is refused. On any failure the context is disposed, an owned
|
|
43
|
+
* mount removed, and this client's hold on the shims released before the
|
|
44
|
+
* original error is rethrown.
|
|
45
|
+
* @param plan - roster and annotations.
|
|
46
|
+
* @param mock - Remote mock answering every Gateway call.
|
|
47
|
+
* @param options - mount and readiness options.
|
|
48
|
+
* @returns the booted client.
|
|
49
|
+
*/
|
|
50
|
+
static start(plan: AssemblyPlan, mock: RemoteMock, options?: TestClientOptions): Promise<TestClient>;
|
|
51
|
+
private disposing;
|
|
52
|
+
private constructor();
|
|
53
|
+
/** The roster's Connection service (no `Context` augmentation declares it); throws when the roster provides none. */
|
|
54
|
+
get connection(): ConnectionHandle;
|
|
55
|
+
/** Flush pending React work and microtasks inside `act` (plain microtask flush without a DOM). */
|
|
56
|
+
flush(): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Rebuild one Loader entry: client-hmr's registry-first fiber teardown, then
|
|
59
|
+
* `entry.refresh()`. Each client's module table retains its own instance-bound
|
|
60
|
+
* Connection plugin, so reloads do not coordinate through process globals.
|
|
61
|
+
* Requires a live client: after `dispose()` the Loader holds no entries and
|
|
62
|
+
* the lookup throws before teardown.
|
|
63
|
+
* @param name - package name of the row.
|
|
64
|
+
*/
|
|
65
|
+
reload(name: string): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Remove one Loader entry and wait for its plugin cleanup.
|
|
68
|
+
* @param name - package name of the row.
|
|
69
|
+
*/
|
|
70
|
+
unload(name: string): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Dispose the plugin tree, then drop an owned mount and release this
|
|
73
|
+
* client's hold on the shared jsdom shims even when the tree fails to dispose,
|
|
74
|
+
* then `mock.assertNoUnmatched()` last so its failure is the test's reason
|
|
75
|
+
* without skipping the cleanup; when both the tree and the check fail, one
|
|
76
|
+
* error carries both messages. The first call owns the teardown and reports
|
|
77
|
+
* its failure; every later call waits for that teardown and resolves.
|
|
78
|
+
*/
|
|
79
|
+
dispose(): Promise<void>;
|
|
80
|
+
private teardown;
|
|
81
|
+
private entryOf;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=test-client.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Test-scoped Remote mock and lazy client boot, owned by native Vitest fixtures. */
|
|
2
|
+
import { type TestAPI } from 'vitest';
|
|
3
|
+
import { RemoteMock, type MockedRemote } from '@deepseek-ai/dsh-remote-mock';
|
|
4
|
+
import type { AssemblyPlan } from './roster.ts';
|
|
5
|
+
import { TestClient, type TestClientOptions } from './test-client.ts';
|
|
6
|
+
/** Per-test controls; configure the mock before awaiting `start()`. */
|
|
7
|
+
export interface ClientTestFixtures {
|
|
8
|
+
/** Fresh mock with the assembly's default responses already loaded. */
|
|
9
|
+
mock: RemoteMock;
|
|
10
|
+
/** Namespace proxy backed by the same native mocks used by the Connection carrier. */
|
|
11
|
+
remote: MockedRemote;
|
|
12
|
+
/** Await the one client owned by this test; rejects after the fixture closes. */
|
|
13
|
+
start: () => Promise<TestClient>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Extend Vitest with a fresh mock and a lazy, automatically disposed client. Repeated `start()` calls share one
|
|
17
|
+
* promise. Await it to observe startup failures; cleanup waits for startup but does not rethrow its rejection.
|
|
18
|
+
* Missing mock responses still fail teardown even when no client was started. Page globals remain environment-owned.
|
|
19
|
+
* @param plan - roster and replacement modules, shared as configuration rather than as a running client.
|
|
20
|
+
* @param options - mount and readiness settings passed to `TestClient.start`.
|
|
21
|
+
* @returns Vitest's test function with test-scoped `mock` and `start` fixtures.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createClientTest(plan: AssemblyPlan, options?: TestClientOptions): TestAPI<ClientTestFixtures>;
|
|
24
|
+
//# sourceMappingURL=vitest.d.ts.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -27,8 +27,6 @@ export { domSnapshotSerializer, registerDomSnapshotSerializer } from './snapshot
|
|
|
27
27
|
export { FixtureSession, TestSessions } from './sessions.ts';
|
|
28
28
|
export { stubSettingsScope } from './settings-scope.ts';
|
|
29
29
|
export type { StubSettingsScope } from './settings-scope.ts';
|
|
30
|
-
export { scriptedSettingsRemote } from './settings-remote.ts';
|
|
31
|
-
export type { ScriptedNamespace, ScriptedSettingsRemote } from './settings-remote.ts';
|
|
32
30
|
export { TestWorkspaces } from './workspaces.ts';
|
|
33
31
|
export { RemoteError, TestRemote } from './remote.ts';
|
|
34
32
|
export { chatSnapshot, conversationSnapshot, sessionSnapshot, workspaceSnapshot, } from './fixtures.ts';
|
|
@@ -81,8 +79,6 @@ export interface FeatureHandle {
|
|
|
81
79
|
}
|
|
82
80
|
/** Mutable fail-loud file-upload stub installed by {@link SlotTestRuntime}. */
|
|
83
81
|
export interface TestFileUpload {
|
|
84
|
-
/** Availability reported to the feature under test. */
|
|
85
|
-
available: boolean;
|
|
86
82
|
/** Test-supplied upload behavior; the default rejects every call. */
|
|
87
83
|
upload: (sessionId: SessionId, ...args: unknown[]) => Promise<unknown>;
|
|
88
84
|
}
|
|
@@ -84,6 +84,12 @@ export declare class TestWorkspaces implements IWorkspaces {
|
|
|
84
84
|
* @param sessionId - session to archive.
|
|
85
85
|
*/
|
|
86
86
|
archiveSession(sessionId: SessionId): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Unarchive a session (recorded). The default mirrors the production face's
|
|
89
|
+
* observable effect: the id leaves the list state's archive set.
|
|
90
|
+
* @param sessionId - session to unarchive.
|
|
91
|
+
*/
|
|
92
|
+
unarchiveSession(sessionId: SessionId): Promise<void>;
|
|
87
93
|
}
|
|
88
94
|
export {};
|
|
89
95
|
//# sourceMappingURL=workspaces.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crazx/dsh-client-test-runtime",
|
|
3
|
-
"description": "jsdom slot test
|
|
4
|
-
"version": "0.1.
|
|
3
|
+
"description": "Browser test runtimes: a jsdom slot bench with test-owned Session and Workspace doubles, and a whole-client tier that boots the web roster through the production bootClient over an endpoint-named Remote mock",
|
|
4
|
+
"version": "0.1.6-alpha.1.zw.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -25,46 +25,67 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@testing-library/dom": "^10.4.1",
|
|
27
27
|
"@testing-library/react": "^16.3.2",
|
|
28
|
+
"js-yaml": "^4.2.0",
|
|
28
29
|
"vitest": "^4.1.8"
|
|
29
30
|
},
|
|
30
31
|
"peerDependencies": {
|
|
31
|
-
"@deepseek-ai/dsh-api-session-controller": "0.1.5-rc.2.zw.1",
|
|
32
|
-
"@deepseek-ai/dsh-api-workspace-controller": "^0.1.5-rc.2",
|
|
33
|
-
"@deepseek-ai/dsh-attachment": "^0.1.5-rc.2",
|
|
34
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.5-rc.2",
|
|
35
|
-
"@deepseek-ai/dsh-client-store": "^0.1.5-rc.2",
|
|
36
|
-
"@deepseek-ai/dsh-client-ui-chat": "^0.1.5-rc.2",
|
|
37
|
-
"@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.2.zw.1",
|
|
38
|
-
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.2",
|
|
39
|
-
"@deepseek-ai/dsh-client-ui-session": "^0.1.5-rc.2",
|
|
40
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.2",
|
|
41
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
|
|
42
|
-
"@deepseek-ai/dsh-session": "^0.1.5-rc.2",
|
|
43
|
-
"@deepseek-ai/dsh-subagent": "^0.1.5-rc.2",
|
|
44
|
-
"@deepseek-ai/dsh-typert-protocol": "^0.1.5-rc.2",
|
|
45
32
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
33
|
+
"@deepseek-ai/cordis-plugin-include": "^1.0.7",
|
|
34
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.3",
|
|
35
|
+
"@deepseek-ai/dsh-api-gateway": "^0.1.6-alpha.1",
|
|
36
|
+
"@deepseek-ai/dsh-api-session-controller": "0.1.6-alpha.1.zw.2",
|
|
37
|
+
"@deepseek-ai/dsh-api-workspace-controller": "^0.1.6-alpha.1",
|
|
38
|
+
"@deepseek-ai/dsh-attachment": "^0.1.6-alpha.1",
|
|
39
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.6-alpha.1",
|
|
40
|
+
"@deepseek-ai/dsh-client-hmr": "^0.1.6-alpha.1",
|
|
41
|
+
"@deepseek-ai/dsh-client-modules": "0.1.6-alpha.1.zw.2",
|
|
42
|
+
"@deepseek-ai/dsh-client-store": "^0.1.6-alpha.1",
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-chat": "^0.1.6-alpha.1",
|
|
44
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.1.6-alpha.1.zw.2",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.6-alpha.1",
|
|
46
|
+
"@deepseek-ai/dsh-client-ui-session": "^0.1.6-alpha.1",
|
|
47
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.6-alpha.1",
|
|
48
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.6-alpha.1",
|
|
49
|
+
"@deepseek-ai/dsh-client-web": "^0.1.6-alpha.1",
|
|
50
|
+
"@deepseek-ai/dsh-remote-mock": "^0.1.6-alpha.1",
|
|
51
|
+
"@deepseek-ai/dsh-session": "^0.1.6-alpha.1",
|
|
52
|
+
"@deepseek-ai/dsh-subagent": "^0.1.6-alpha.1",
|
|
53
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.6-alpha.1",
|
|
46
54
|
"react": "^18.2.0",
|
|
47
55
|
"react-dom": "^18.2.0"
|
|
48
56
|
},
|
|
49
57
|
"devDependencies": {
|
|
50
|
-
"@deepseek-ai/
|
|
51
|
-
"@deepseek-ai/
|
|
52
|
-
"@deepseek-ai/
|
|
53
|
-
"@deepseek-ai/dsh-
|
|
54
|
-
"@deepseek-ai/dsh-
|
|
55
|
-
"@deepseek-ai/dsh-
|
|
56
|
-
"@deepseek-ai/dsh-
|
|
57
|
-
"@deepseek-ai/dsh-
|
|
58
|
-
"@deepseek-ai/dsh-
|
|
59
|
-
"@deepseek-ai/dsh-client-
|
|
60
|
-
"@deepseek-ai/dsh-client-
|
|
61
|
-
"@deepseek-ai/dsh-client-
|
|
62
|
-
"@deepseek-ai/dsh-
|
|
63
|
-
"@deepseek-ai/dsh-
|
|
64
|
-
"@deepseek-ai/dsh-
|
|
58
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
59
|
+
"@deepseek-ai/cordis-plugin-include": "^1.0.7",
|
|
60
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.3",
|
|
61
|
+
"@deepseek-ai/dsh-api-gateway": "^0.1.6-alpha.1",
|
|
62
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.6-alpha.1",
|
|
63
|
+
"@deepseek-ai/dsh-api-session-controller": "npm:@crazx/dsh-api-session-controller@0.1.6-alpha.1.zw.2",
|
|
64
|
+
"@deepseek-ai/dsh-api-workspace-controller": "^0.1.6-alpha.1",
|
|
65
|
+
"@deepseek-ai/dsh-attachment": "^0.1.6-alpha.1",
|
|
66
|
+
"@deepseek-ai/dsh-base": "^0.1.6-alpha.1",
|
|
67
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.6-alpha.1",
|
|
68
|
+
"@deepseek-ai/dsh-client-hmr": "^0.1.6-alpha.1",
|
|
69
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.6-alpha.1",
|
|
70
|
+
"@deepseek-ai/dsh-client-modules": "npm:@crazx/dsh-client-modules@0.1.6-alpha.1.zw.2",
|
|
71
|
+
"@deepseek-ai/dsh-client-store": "^0.1.6-alpha.1",
|
|
72
|
+
"@deepseek-ai/dsh-client-ui-chat": "^0.1.6-alpha.1",
|
|
73
|
+
"@deepseek-ai/dsh-client-ui-conversation": "npm:@crazx/dsh-client-ui-conversation@0.1.6-alpha.1.zw.2",
|
|
74
|
+
"@deepseek-ai/dsh-client-ui-layout": "npm:@crazx/dsh-client-ui-layout@0.1.6-alpha.1.zw.2",
|
|
75
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.6-alpha.1",
|
|
76
|
+
"@deepseek-ai/dsh-client-ui-session": "^0.1.6-alpha.1",
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.6-alpha.1",
|
|
78
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.6-alpha.1",
|
|
79
|
+
"@deepseek-ai/dsh-client-web": "^0.1.6-alpha.1",
|
|
80
|
+
"@deepseek-ai/dsh-remote-mock": "^0.1.6-alpha.1",
|
|
81
|
+
"@deepseek-ai/dsh-session": "^0.1.6-alpha.1",
|
|
82
|
+
"@deepseek-ai/dsh-settings": "^0.1.6-alpha.1",
|
|
83
|
+
"@deepseek-ai/dsh-subagent": "^0.1.6-alpha.1",
|
|
84
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.6-alpha.1",
|
|
85
|
+
"@deepseek-ai/dsh-web-app": "^0.1.6-alpha.1",
|
|
86
|
+
"@types/js-yaml": "^4.0.9",
|
|
65
87
|
"@types/react": "~18.3.1",
|
|
66
88
|
"@types/react-dom": "~18.3.0",
|
|
67
|
-
"@deepseek-ai/cordis": "^4.0.2",
|
|
68
89
|
"react": "^18.2.0",
|
|
69
90
|
"react-dom": "^18.2.0"
|
|
70
91
|
},
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/** Test double for the `settings` Remote namespace a bench's plugins inject. */
|
|
2
|
-
import { vi } from 'vitest';
|
|
3
|
-
/** The minimum a scripted namespace view carries for the double's own bookkeeping. */
|
|
4
|
-
export interface ScriptedNamespace {
|
|
5
|
-
/** Namespace key the write addresses. */
|
|
6
|
-
ns: string;
|
|
7
|
-
}
|
|
8
|
-
/** One scripted `settings` namespace face plus the controls a bench drives it with. */
|
|
9
|
-
export interface ScriptedSettingsRemote<View extends ScriptedNamespace> {
|
|
10
|
-
/**
|
|
11
|
-
* The namespace face handed to `TestRemote` as `settings`. A plugin injecting
|
|
12
|
-
* `remote.settings` unparks on it, which is what most benches need; the
|
|
13
|
-
* describe answer is the same one the shared mirror would read.
|
|
14
|
-
*/
|
|
15
|
-
settings: {
|
|
16
|
-
describe(): Promise<{
|
|
17
|
-
ok: true;
|
|
18
|
-
value: {
|
|
19
|
-
writable: boolean;
|
|
20
|
-
hasDocument: boolean;
|
|
21
|
-
namespaces: readonly View[];
|
|
22
|
-
};
|
|
23
|
-
}>;
|
|
24
|
-
update(ns: string, patch: unknown, expectedRevision: number | undefined): Promise<{
|
|
25
|
-
ok: true;
|
|
26
|
-
value: View;
|
|
27
|
-
} | {
|
|
28
|
-
ok: false;
|
|
29
|
-
error: {
|
|
30
|
-
code: string;
|
|
31
|
-
message: string;
|
|
32
|
-
details: object;
|
|
33
|
-
};
|
|
34
|
-
}>;
|
|
35
|
-
replace(ns: string, section: unknown, expectedRevision: number | undefined): Promise<{
|
|
36
|
-
ok: true;
|
|
37
|
-
value: View;
|
|
38
|
-
} | {
|
|
39
|
-
ok: false;
|
|
40
|
-
error: {
|
|
41
|
-
code: string;
|
|
42
|
-
message: string;
|
|
43
|
-
details: object;
|
|
44
|
-
};
|
|
45
|
-
}>;
|
|
46
|
-
mutate(ns: string, ops: unknown, expectedRevision: number | undefined): Promise<{
|
|
47
|
-
ok: true;
|
|
48
|
-
value: View;
|
|
49
|
-
} | {
|
|
50
|
-
ok: false;
|
|
51
|
-
error: {
|
|
52
|
-
code: string;
|
|
53
|
-
message: string;
|
|
54
|
-
details: object;
|
|
55
|
-
};
|
|
56
|
-
}>;
|
|
57
|
-
};
|
|
58
|
-
/** Spy behind `settings.update`, for argument assertions. */
|
|
59
|
-
update: ReturnType<typeof vi.fn>;
|
|
60
|
-
/** Spy behind `settings.replace`, for argument assertions. */
|
|
61
|
-
replace: ReturnType<typeof vi.fn>;
|
|
62
|
-
/** Spy behind `settings.mutate`, for argument assertions. */
|
|
63
|
-
mutate: ReturnType<typeof vi.fn>;
|
|
64
|
-
/**
|
|
65
|
-
* Replace what the next describe answers with, as a Host commit would.
|
|
66
|
-
* @param namespaces - the namespace views to serve from now on.
|
|
67
|
-
*/
|
|
68
|
-
publish(namespaces: readonly View[]): void;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Build a scripted `settings` Remote namespace for a bench. Each write answers
|
|
72
|
-
* with the addressed namespace unchanged, so a bench that only needs its
|
|
73
|
-
* plugins to activate scripts nothing; one asserting a write reads the
|
|
74
|
-
* corresponding spy or replaces the face.
|
|
75
|
-
* @param namespaces - namespace views the first describe answers with.
|
|
76
|
-
* @param options - deployment facts the describe answer reports.
|
|
77
|
-
* @returns the face and its controls.
|
|
78
|
-
*/
|
|
79
|
-
export declare function scriptedSettingsRemote<View extends ScriptedNamespace>(namespaces?: readonly View[], options?: {
|
|
80
|
-
writable?: boolean;
|
|
81
|
-
hasDocument?: boolean;
|
|
82
|
-
}): ScriptedSettingsRemote<View>;
|
|
83
|
-
//# sourceMappingURL=settings-remote.d.ts.map
|