@qilin-agent/remote-mock 0.0.0-stage → 3.1.4
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/LICENSE +21 -0
- package/README.i18n.yaml +61 -0
- package/README.md +146 -2
- package/README.zh.md +147 -0
- package/lib/index.js +711 -0
- package/lib/types/index.d.ts +18 -0
- package/lib/types/log.d.ts +96 -0
- package/lib/types/remote-mock.d.ts +190 -0
- package/lib/types/remote-proxy.d.ts +15 -0
- package/lib/types/streams.d.ts +153 -0
- package/package.json +43 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kkutysllb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
|
2
|
+
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
|
3
|
+
# After editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/test-support/remote-mock/README.md
|
|
5
|
+
/:
|
|
6
|
+
en: 5807aeb1022af801
|
|
7
|
+
zh: 1b8715d41b159629
|
|
8
|
+
/qilin-agent-remote-mock:
|
|
9
|
+
en: 6c6a2d1a811a52cb
|
|
10
|
+
zh: d681bc5d264856f2
|
|
11
|
+
/qilin-agent-remote-mock/summary:
|
|
12
|
+
en: 72cfa12ccc0a4cf6
|
|
13
|
+
zh: c5a85ea7512be7c6
|
|
14
|
+
/qilin-agent-remote-mock/table-of-contents:
|
|
15
|
+
en: 76368b6b1da79c80
|
|
16
|
+
zh: 52384aee991ac542
|
|
17
|
+
/qilin-agent-remote-mock/use-this-package:
|
|
18
|
+
en: bbff250eba0f1b89
|
|
19
|
+
zh: 0a618f05cc88ee2b
|
|
20
|
+
/qilin-agent-remote-mock/use-this-package/when-to-use-it:
|
|
21
|
+
en: 0d7cdedee1c07e7a
|
|
22
|
+
zh: 2fef0c2109425f25
|
|
23
|
+
/qilin-agent-remote-mock/use-this-package/use-the-remote-proxy:
|
|
24
|
+
en: 5dca47c5bedcf13b
|
|
25
|
+
zh: 92cbe6b3fbb4ddfa
|
|
26
|
+
/qilin-agent-remote-mock/use-this-package/register-default-responses:
|
|
27
|
+
en: c7022d673fbbb095
|
|
28
|
+
zh: aa827878f3215388
|
|
29
|
+
/qilin-agent-remote-mock/use-this-package/drive-streams:
|
|
30
|
+
en: 0a466b4351c5ad4c
|
|
31
|
+
zh: 602d87000880166f
|
|
32
|
+
/qilin-agent-remote-mock/use-this-package/connect-a-client:
|
|
33
|
+
en: 302d3015e5d82fd8
|
|
34
|
+
zh: 2e83a236952ba390
|
|
35
|
+
/qilin-agent-remote-mock/use-this-package/observe-and-assert:
|
|
36
|
+
en: 56fcb7912f6365ec
|
|
37
|
+
zh: 299299e8afad0512
|
|
38
|
+
/qilin-agent-remote-mock/use-this-package/what-can-go-wrong:
|
|
39
|
+
en: 441ed8ae66b85a7d
|
|
40
|
+
zh: b9eea1cbaeefa086
|
|
41
|
+
/qilin-agent-remote-mock/understand-the-implementation:
|
|
42
|
+
en: 42b15ac6321706aa
|
|
43
|
+
zh: 4ed0861911abaf5d
|
|
44
|
+
/qilin-agent-remote-mock/understand-the-implementation/design:
|
|
45
|
+
en: 6385fe57822aa455
|
|
46
|
+
zh: 53eda1b6a602f357
|
|
47
|
+
/qilin-agent-remote-mock/understand-the-implementation/source-map:
|
|
48
|
+
en: 4d308c2e883b6d86
|
|
49
|
+
zh: f4eb0f86627c6e57
|
|
50
|
+
/qilin-agent-remote-mock/model-experience:
|
|
51
|
+
en: 53b590e95981d312
|
|
52
|
+
zh: 231c0245f6b6b827
|
|
53
|
+
/qilin-agent-remote-mock/model-experience/kv-cache-effect:
|
|
54
|
+
en: ca75c51c89c2c9b0
|
|
55
|
+
zh: f57cc48a1b5cbfa9
|
|
56
|
+
/qilin-agent-remote-mock/known-limitations-and-deferred-work:
|
|
57
|
+
en: 281c77a33b08da46
|
|
58
|
+
zh: 13f9efe0045b0f42
|
|
59
|
+
/qilin-agent-remote-mock/known-limitations-and-deferred-work/dev-note:
|
|
60
|
+
en: e67f633dc6fe7773
|
|
61
|
+
zh: 3bb0c20c93f16f98
|
package/README.md
CHANGED
|
@@ -1,3 +1,147 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "Endpoint-named mock for Typert Remote traffic: a table of unary answers and stream scripts, live stream control, a log, and the Connection carrier face, for test authors booting a real browser client without a Host."
|
|
3
|
+
kind: "package-library"
|
|
4
|
+
---
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
# @qilin-agent/remote-mock
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
`qilin-remote-mock` lets tests configure Host responses through `mock.remote.<namespace>.<method>` using native Vitest mock methods. The same functions answer direct calls and real Connection traffic; reusable tables supply default responses, and explicitly declared streams support test-driven frames and cancellation. Missing responses fail the call and are reported again by `assertNoUnmatched()` at teardown. The package runs without a business Host in Node or a browser page, imports no DOM, React, or Node modules, and is consumed from `devDependencies` only.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Model Experience](#model-experience)
|
|
19
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
20
|
+
- [Dev Note](#dev-note)
|
|
21
|
+
|
|
22
|
+
-----
|
|
23
|
+
|
|
24
|
+
<a id="use-this-package"></a>
|
|
25
|
+
## Use this package
|
|
26
|
+
|
|
27
|
+
### When to use it
|
|
28
|
+
|
|
29
|
+
Use it when a spec boots real client plugins that talk to `ctx.remote` and wants to script the Host side by endpoint name: whole-client specs bind `mock.rpc` to their Connection instance, while unit specs may call `mock.remote`, `dispatch`, or `open` directly. Endpoints are the Gateway's wire names (`session/page`, `settings/describe`); `args` is the caller's positional argument list with a trailing `AbortSignal` removed; a value is whatever the test registers and is answered unchanged. The only declaration is whether an endpoint is unary (`unary`) or a stream (`stream`).
|
|
30
|
+
|
|
31
|
+
<a id="remote-proxy"></a>
|
|
32
|
+
### Use the Remote proxy
|
|
33
|
+
|
|
34
|
+
`mock.remote` exposes every namespace and method without a method list or a domain-specific helper. Each accessed endpoint uses a cached native Vitest mock; `@vitest/spy.fn` is the implementation behind `vi.fn` and also works in a browser page without the Vitest runner. The same mock answers direct calls and Connection traffic, so return overrides and call assertions observe the functions the client actually invokes:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
const mock = RemoteMock.create().load(remoteDefaultResponses)
|
|
38
|
+
mock.remote.settings.describe.mockResolvedValue(ok({
|
|
39
|
+
writable: true, hasDocument: false, namespaces: [],
|
|
40
|
+
}))
|
|
41
|
+
mock.remote.settings.mutate.mockResolvedValueOnce(ok(updatedNamespace))
|
|
42
|
+
// After the client writes:
|
|
43
|
+
expect(mock.remote.settings.mutate).toHaveBeenCalledWith('locale', operations, revision)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Use `mockResolvedValue` for a persistent response, `mockResolvedValueOnce` or `mockReturnValueOnce` for queued responses, and `mockImplementation` for argument-dependent behavior. Native queued responses run in registration order, then the mock's current implementation answers. Its initial implementation reads the registered default. `mockClear()` preserves responses and queues; `mockReset()` clears overrides and restores that initial implementation, which reads the latest default. Missing defaults still fail after queued responses are exhausted, including direct calls, which can throw synchronously.
|
|
47
|
+
|
|
48
|
+
Only explicit `stream()` or table stream declarations select stream methods; everything else uses unary mocks. Accessing a method does not fabricate a successful business result. Declare stream mode before retaining a method reference: each endpoint/mode owns its own mock. Namespace and method `then` probes and symbol reads are inert.
|
|
49
|
+
|
|
50
|
+
`MockedRemote` uses Vitest's deep mock transformation over the entire generated `TypertRemoteNamespaceMap`. A non-empty map preserves its namespace and method names, parameters, results, and native spy types. An empty map makes only this test proxy `any`, allowing arbitrary namespaces and methods; it does not augment or weaken production Remote declarations. No copied method signatures, optional generated-module suppressions, or compiler-wide flags are needed. Before handing off Remote/mock changes, run `pnpm run typecheck` to generate and check the real Client types; missing, stale, or partial declarations require rebuilding first. Unbuilt test success or `any` inference is not strict type evidence.
|
|
51
|
+
|
|
52
|
+
### Register default responses
|
|
53
|
+
|
|
54
|
+
`load(table)` installs reusable `unary` values or handlers, `stream` scripts, and scriptless `streams` declarations. `unary(endpoint, value)` and `unary(endpoint, fn)` register individual defaults; handlers receive the caller's positional arguments and use existing request types. Each endpoint holds only its latest default, including an explicit `undefined`; updating defaults does not discard native overrides. `ok(value)` builds `{ ok: true, value }`; failures use `{ ok: false, error: { code, message, details } }`. Keep stateful handlers, promises, and native queues local to each test:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
const initial = { writable: true, hasDocument: false, namespaces: [] }
|
|
58
|
+
const mock = RemoteMock.create().load({
|
|
59
|
+
unary: { 'settings/describe': ok(initial) },
|
|
60
|
+
})
|
|
61
|
+
mock.remote.settings.describe.mockResolvedValueOnce(ok({ ...initial, hasDocument: true }))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Drive streams
|
|
65
|
+
|
|
66
|
+
A stream script is a function of the open `args` and a `StreamHandle` (`push`, `end`, `fail(error)`, `signal`); the stream stays open after the script returns until the handle ends or fails it. `frames(items)` builds a script that yields the items and ends; `openStream(initial)` one that yields them and stays open. `mock.streams` controls streams the client currently holds open, filtered by the args they were opened with; `opened(endpoint, count)` resolves once the endpoint has been opened that many times, and `drained(endpoint)` once the consumer of every matching stream has pulled everything pushed so far, an open stream's consumer waiting for more and a settled stream's queue empty (pulled from the queue; that equals processed only for a consumer that handles items inside its read loop):
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
mock.stream('session/follow', openStream([snapshotFrame]))
|
|
70
|
+
await mock.streams.opened('session/follow', 1)
|
|
71
|
+
mock.streams.push('session/follow', eventFrame, ([request]) => (request as { sessionId: string }).sessionId === SID)
|
|
72
|
+
mock.streams.fail('session/follow', new Error('gone'))
|
|
73
|
+
await mock.streams.drained('session/follow')
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A failed stream rejects the consumer's next read with the given `Error`. Consumer cancellation (the opening signal or an early iterator `return()`) aborts `StreamHandle.signal`, ends the iteration without throwing, and logs the stream as `cancelled`. `StreamHandle.uplink` is what the script reads as the Client's uplink: through `rpc.open` it is the carrier's iterable; for a direct `mock.remote.<namespace>.<method>(...)` call it is fed by the returned handle, which is the `RemoteStreamHandle` a generated method returns (iterate it for the downlink, `send()` and `end()` feed the uplink, `dispose()` cancels the stream); `send()` rejects a non-lossless JSON item as the real handle does, and the uplink it owns closes once the stream settles, is cancelled, or the consumer leaves, so a later `send()` throws. The mock function is called with the method's own arguments only, so the uplink never appears in call assertions or logged args.
|
|
77
|
+
|
|
78
|
+
A fake that stands in for a generated stream method returns the `RemoteStreamHandle` the generated method does. `streamHandle(source)` types an `AsyncIterable` as that handle with inert `send`, `end`, and `dispose`; `streamMethod<M>(generator)` lifts an async generator function written for the method's arguments into the method's own signature, for `vi.fn<M>()` and `mockImplementation`.
|
|
79
|
+
|
|
80
|
+
### Connect a client
|
|
81
|
+
|
|
82
|
+
`mock.rpc` is the `ClientConnectionRpc` face. Pass it as `{ transport: { rpc: mock.rpc } }` to the Connection installer, or let `TestClient` bind it to its instance, so every Remote call reaches `dispatch` and every stream reaches `open` with no envelopes in between. Payloads carry `{ args }` as the whole-client proxies send them (an array) or as the Gateway's own endpoints send them (one object, delivered as one positional arg); a call whose signal aborts rejects with the abort reason. `RemoteMock.create()` registers one stream, `$events`, that answers the Gateway client's opening with `{ type: 'ready', clientId, host: { home } }` (host from `RemoteMockOptions.host`, default `/home/mock`) and stays open, which is what lets the assembled client reach `connected`; a spec overrides or fails it like any other stream.
|
|
83
|
+
|
|
84
|
+
### Observe and assert
|
|
85
|
+
|
|
86
|
+
`mock.log.calls(endpoint?)` lists unary calls through `dispatch` or `rpc.call` (`args`, `seq`, a live `state` of `pending` / `answered` / `failed`, and the answer or thrown error as `result`), `streams(endpoint?)` lists scripted opens with their live `state` and `pushed` count, `requests(endpoint?)` lists the first positional arg of calls and opens in order (without an endpoint, excluding the Gateway's own `$`-prefixed endpoints), and `unmatched()` lists requests that found no rule. Native `.mock.calls` additionally includes direct proxy calls; carrier stream mocks receive their final cancellation signal. `assertNoUnmatched()` reports misses at teardown. `modeOf(endpoint)` reports explicit registrations; `endpoints()` also includes accessed proxy methods so assembly can provide their namespaces.
|
|
87
|
+
|
|
88
|
+
### What can go wrong
|
|
89
|
+
|
|
90
|
+
- **A request has no rule** — `dispatch` rejects and `open` throws `remote-mock: no rule for <endpoint>; registered: …`, and the log records the miss; register the endpoint.
|
|
91
|
+
- **A payload is not `{ args: unknown[] | object }`** — `rpc.call` rejects and `rpc.open` throws a `TypeError`; the whole-client proxies send the array form and the Gateway's own endpoints the object form, so a hand-written call is at fault.
|
|
92
|
+
- **A second concurrent read on one stream** — the read rejects; the Gateway reads streams sequentially, so this names a test-side misuse.
|
|
93
|
+
|
|
94
|
+
-----
|
|
95
|
+
|
|
96
|
+
<a id="understand-the-implementation"></a>
|
|
97
|
+
## Understand the implementation
|
|
98
|
+
|
|
99
|
+
<details>
|
|
100
|
+
<summary>Implementation internals — click to expand</summary>
|
|
101
|
+
|
|
102
|
+
### Design
|
|
103
|
+
|
|
104
|
+
`dispatch` and `open` take the endpoint and positional args; `rpc` exposes the same core through Connection's decoded carrier interface. Each mock owns its native functions and queued overrides; shared tables supply defaults without copying handlers or answer objects. Each scripted stream owns its queue, its single pending read, and its log entry; the first of `end`, `fail`, or consumer cancellation settles it.
|
|
105
|
+
|
|
106
|
+
### Source map
|
|
107
|
+
|
|
108
|
+
| File | Role |
|
|
109
|
+
|---|---|
|
|
110
|
+
| [`src/index.ts`](src/index.ts) | Public face re-exports |
|
|
111
|
+
| [`src/remote-mock.ts`](src/remote-mock.ts) | `RemoteMock`: default responses, native mocks, Connection dispatch, controlled streams and missing-response checks; `ok` |
|
|
112
|
+
| [`src/remote-proxy.ts`](src/remote-proxy.ts) | Namespace/method lookup and generated-map mock types |
|
|
113
|
+
| [`src/streams.ts`](src/streams.ts) | `frames` / `openStream` scripts, `streamHandle` / `streamMethod` fake typing, and `MockStream` (handle + `AsyncIterable`) |
|
|
114
|
+
| [`src/log.ts`](src/log.ts) | Log store with the shared `seq` counter |
|
|
115
|
+
|
|
116
|
+
</details>
|
|
117
|
+
|
|
118
|
+
-----
|
|
119
|
+
|
|
120
|
+
<a id="model-experience"></a>
|
|
121
|
+
## Model Experience
|
|
122
|
+
|
|
123
|
+
None, as this package is browser-side test infrastructure; nothing here reaches a model request.
|
|
124
|
+
|
|
125
|
+
#### KV Cache effect
|
|
126
|
+
|
|
127
|
+
None; this package neither assembles nor sends a provider request.
|
|
128
|
+
|
|
129
|
+
## Known Limitations and Deferred Work
|
|
130
|
+
|
|
131
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
132
|
+
|
|
133
|
+
- **In-process carrier only** — `rpc` serves a Connection instance in the same realm; no HTTP or WebSocket carrier for browser-lane specs is provided.
|
|
134
|
+
- **Values cross by reference** — answers and downlink items reach the client unserialized, so a non-JSON downlink value that the real wire would reject passes through unchanged; only the handle's `send()` applies the real uplink check.
|
|
135
|
+
- **Values are not checked** — a unary answer must be the result the caller reads (`{ ok, value }` or `{ ok: false, error }`); the mock passes it through unchanged and does not check those fields.
|
|
136
|
+
- **No payload matching** — rules match on endpoint only; discriminate on business arguments inside a handler.
|
|
137
|
+
- **Native stream overrides own their iterable** — an override returning its own iterable bypasses scripted-stream logs, `requests`, `opened`, `drained`, and `push` / `end` / `fail`; the caller also owns cancellation. Native call assertions still work. Use a registered stream script when a scenario needs those controls.
|
|
138
|
+
|
|
139
|
+
<a id="dev-note"></a>
|
|
140
|
+
### Dev Note
|
|
141
|
+
|
|
142
|
+
<details>
|
|
143
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
144
|
+
|
|
145
|
+
None.
|
|
146
|
+
|
|
147
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Typert Remote 流量的端点具名 mock:一元应答与流脚本的表、活流控制、日志与 Connection 载体面,供测试作者在没有 Host 的情况下启动真实浏览器客户端。"
|
|
3
|
+
kind: "package-library"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @qilin-agent/remote-mock
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 中文
|
|
9
|
+
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
`qilin-remote-mock` 让测试通过 `mock.remote.<namespace>.<method>`,使用原生 Vitest mock 方法配置 Host 响应。同一组函数应答直接调用与真实 Connection 流量;可复用的表提供默认响应,显式声明的流支持测试驱动的推帧与取消。缺少响应时调用失败,`assertNoUnmatched()` 会在收尾时再次报告。本包无需业务 Host 即可在 Node 或浏览器页面中运行,不导入 DOM、React 或 Node 模块,只从 `devDependencies` 消费。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [模型体验](#model-experience)
|
|
19
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
20
|
+
- [开发备注](#dev-note)
|
|
21
|
+
|
|
22
|
+
-----
|
|
23
|
+
|
|
24
|
+
<a id="use-this-package"></a>
|
|
25
|
+
## 使用本包
|
|
26
|
+
|
|
27
|
+
### 何时使用
|
|
28
|
+
|
|
29
|
+
当测试要启动与 `ctx.remote` 对话的真实客户端插件、并想按端点名脚本化 Host 侧时使用它:整体客户端测试把 `mock.rpc` 绑定到各自的 Connection 实例,单元测试也可以直接调用 `mock.remote`、`dispatch` 或 `open`。端点是 Gateway 的 wire 名(`session/page`、`settings/describe`);`args` 是调用方的位置参数列表,末尾的 `AbortSignal` 已剥掉;值就是测试登记的东西,原样应答。唯一的声明是端点是一元(`unary`)还是流(`stream`)。
|
|
30
|
+
|
|
31
|
+
<a id="remote-proxy"></a>
|
|
32
|
+
### 使用 Remote Proxy
|
|
33
|
+
|
|
34
|
+
`mock.remote` 无需方法清单或领域专属 Helper 即可提供每个命名空间和方法。每个访问过的端点使用缓存的原生 Vitest mock;`@vitest/spy.fn` 就是 `vi.fn` 背后的实现,也能在没有 Vitest runner 的浏览器页面中运行。同一个 mock 应答直接调用与 Connection 流量,因此返回值覆盖和调用断言观察的是客户端实际调用的函数:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
const mock = RemoteMock.create().load(remoteDefaultResponses)
|
|
38
|
+
mock.remote.settings.describe.mockResolvedValue(ok({
|
|
39
|
+
writable: true, hasDocument: false, namespaces: [],
|
|
40
|
+
}))
|
|
41
|
+
mock.remote.settings.mutate.mockResolvedValueOnce(ok(updatedNamespace))
|
|
42
|
+
// After the client writes:
|
|
43
|
+
expect(mock.remote.settings.mutate).toHaveBeenCalledWith('locale', operations, revision)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
使用 `mockResolvedValue` 设置持续响应,使用 `mockResolvedValueOnce` 或 `mockReturnValueOnce` 排队设置响应,使用 `mockImplementation` 按参数决定行为。原生队列按登记顺序消费响应,耗尽后由 mock 的当前实现应答;初始实现读取已登记的默认响应。`mockClear()` 保留响应与队列;`mockReset()` 清除覆盖并恢复初始实现,由它读取最新默认响应。若未配置默认响应,排队响应耗尽后仍会失败,包括可能同步抛错的直接调用。
|
|
47
|
+
|
|
48
|
+
只有显式 `stream()` 或响应表中的流声明才选择流方法;其余均使用一元 mock。访问方法不会凭空构造成功的业务结果。保存方法引用前先声明流模式:每个端点/模式拥有各自的 mock。命名空间和方法的 `then` 探测及 symbol 读取均无副作用。
|
|
49
|
+
|
|
50
|
+
`MockedRemote` 使用 Vitest 的深层 mock 类型转换,覆盖完整生成的 `TypertRemoteNamespaceMap`。非空映射保留命名空间与方法名、参数、返回值和原生 spy 类型。映射为空时只有这个测试 Proxy 变成 `any`,允许任意命名空间和方法;它不增补或弱化生产 Remote 声明。不需要复制方法签名、抑制可选生成模块错误或开启编译器级全局 Flag。交付 Remote/mock 改动前运行 `pnpm run typecheck`,生成并检查真实 Client 类型;声明缺失、陈旧或不完整时先重新构建。无构建测试通过或推断为 `any` 都不是严格类型证据。
|
|
51
|
+
|
|
52
|
+
### 登记默认响应
|
|
53
|
+
|
|
54
|
+
`load(table)` 安装可复用的 `unary` 值或 handler、`stream` 脚本以及无脚本的 `streams` 声明。`unary(endpoint, value)` 与 `unary(endpoint, fn)` 登记单个默认响应;handler 接收调用方的位置参数,并使用现有请求类型。每个端点仅保存最新默认响应,包括显式 `undefined`;更新默认响应不会清除原生覆盖。`ok(value)` 构造 `{ ok: true, value }`;失败使用 `{ ok: false, error: { code, message, details } }`。有状态 handler、promise 与原生队列均由各测试独立持有:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
const initial = { writable: true, hasDocument: false, namespaces: [] }
|
|
58
|
+
const mock = RemoteMock.create().load({
|
|
59
|
+
unary: { 'settings/describe': ok(initial) },
|
|
60
|
+
})
|
|
61
|
+
mock.remote.settings.describe.mockResolvedValueOnce(ok({ ...initial, hasDocument: true }))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 驾驭流
|
|
65
|
+
|
|
66
|
+
流脚本是一个接收打开时的 `args` 与 `StreamHandle`(`push`、`end`、`fail(error)`、`signal`)的函数;脚本返回后流保持打开,直到句柄结束或失败。`frames(items)` 构造吐完即结束的脚本,`openStream(initial)` 构造吐完后保持打开的脚本。`mock.streams` 控制客户端当前打开着的流,可按打开时的参数过滤;`opened(endpoint, count)` 在该端点被打开达到该次数时 resolve,`drained(endpoint)` 在每条匹配流的消费方都拉完了迄今推入的全部内容时 resolve——打开的流要其消费方再次等待,已定局的流要队列已空(拉完指从队列取走;只有在读循环内处理项的消费方才等于处理完):
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
mock.stream('session/follow', openStream([snapshotFrame]))
|
|
70
|
+
await mock.streams.opened('session/follow', 1)
|
|
71
|
+
mock.streams.push('session/follow', eventFrame, ([request]) => (request as { sessionId: string }).sessionId === SID)
|
|
72
|
+
mock.streams.fail('session/follow', new Error('gone'))
|
|
73
|
+
await mock.streams.drained('session/follow')
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
失败的流让消费方的下一次读取以给定的 `Error` reject。消费方取消(打开时的 signal 或 iterator 提前 `return()`)会中止 `StreamHandle.signal`、结束迭代而不抛错,并把该流记为 `cancelled`。`StreamHandle.uplink` 是脚本读到的 Client 上行:经 `rpc.open` 打开时它是载体传入的 iterable;直接调用 `mock.remote.<namespace>.<method>(...)` 时它由返回的句柄喂入——该句柄就是生成方法返回的 `RemoteStreamHandle`(迭代它读下行,`send()` 与 `end()` 喂上行,`dispose()` 取消流);`send()` 像真实句柄一样拒绝非无损 JSON 项,它拥有的上行在流结束、取消或消费方离开时关闭,之后的 `send()` 抛错。mock 函数只以方法自己的参数被调用,上行不会出现在调用断言或记录的 args 里。
|
|
77
|
+
|
|
78
|
+
替代生成流方法的假实现返回生成方法所返回的 `RemoteStreamHandle`。`streamHandle(source)` 把一个 `AsyncIterable` 标注为该句柄,其 `send`、`end`、`dispose` 为空操作;`streamMethod<M>(generator)` 把按方法参数编写的 async generator 函数提升为方法自己的签名,供 `vi.fn<M>()` 与 `mockImplementation` 使用。
|
|
79
|
+
|
|
80
|
+
### 接上客户端
|
|
81
|
+
|
|
82
|
+
`mock.rpc` 是 `ClientConnectionRpc` 面。把它作为 `{ transport: { rpc: mock.rpc } }` 传给 Connection 安装函数,或让 `TestClient` 将其绑定到自身实例,每次 Remote 调用就会直达 `dispatch`、每条流直达 `open`,中间没有信封。payload 携带 `{ args }`——整机代理发数组、Gateway 自身端点发一个对象(到达时是一个位置参数);signal 中止的调用以中止原因 reject。`RemoteMock.create()` 登记一条流 `$events`,用 `{ type: 'ready', clientId, host: { home } }`(host 来自 `RemoteMockOptions.host`,默认 `/home/mock`)应答 Gateway 客户端的打开并保持打开——这正是整机能达到 `connected` 的原因;测试可以像任何流一样覆盖或让它失败。
|
|
83
|
+
|
|
84
|
+
### 观察与断言
|
|
85
|
+
|
|
86
|
+
`mock.log.calls(endpoint?)` 列出经 `dispatch` 或 `rpc.call` 的一元调用(`args`、`seq`、实时 `state` 为 `pending` / `answered` / `failed`,以及作为 `result` 的应答值或抛出的错误),`streams(endpoint?)` 列出脚本流的打开记录及其实时 `state` 与 `pushed` 计数,`requests(endpoint?)` 按顺序列出调用与打开的首个位置参数(不带端点时去掉 Gateway 自己带 `$` 前缀的端点),`unmatched()` 列出没找到规则的请求。原生 `.mock.calls` 还包含直接 Proxy 调用;载体的流 mock 会收到末尾的取消信号。`assertNoUnmatched()` 在收尾时报告漏配。`modeOf(endpoint)` 报告显式登记;`endpoints()` 还包含访问过的 Proxy 方法,使装配能够提供它们的命名空间。
|
|
87
|
+
|
|
88
|
+
### 可能出什么问题
|
|
89
|
+
|
|
90
|
+
- **请求没有规则**——`dispatch` reject、`open` 抛出 `remote-mock: no rule for <endpoint>; registered: …`,日志记下这次漏配;请登记该端点。
|
|
91
|
+
- **payload 不是 `{ args: unknown[] | object }`**——`rpc.call` reject、`rpc.open` 抛 `TypeError`;整机代理发数组形式、Gateway 自身端点发对象形式,所以问题出在手写调用。
|
|
92
|
+
- **同一条流上有第二个并发读取**——该读取 reject;Gateway 顺序读取流,因此这指向测试侧误用。
|
|
93
|
+
|
|
94
|
+
-----
|
|
95
|
+
|
|
96
|
+
<a id="understand-the-implementation"></a>
|
|
97
|
+
## 理解实现
|
|
98
|
+
|
|
99
|
+
<details>
|
|
100
|
+
<summary>实现细节——点击展开</summary>
|
|
101
|
+
|
|
102
|
+
### 设计
|
|
103
|
+
|
|
104
|
+
`dispatch` 与 `open` 接收端点与位置参数;`rpc` 通过 Connection 的已解码载体接口暴露同一个核心。每个 mock 独立持有原生函数及排队覆盖;共享表提供默认响应,不复制 handler 或应答对象。每条脚本流拥有自己的队列、唯一挂起读取和日志条目;`end`、`fail`、消费方取消三者中最先发生者定局。
|
|
105
|
+
|
|
106
|
+
### 源码地图
|
|
107
|
+
|
|
108
|
+
| 文件 | 职责 |
|
|
109
|
+
|---|---|
|
|
110
|
+
| [`src/index.ts`](src/index.ts) | 公开面转出 |
|
|
111
|
+
| [`src/remote-mock.ts`](src/remote-mock.ts) | `RemoteMock`:默认响应、原生 mock、Connection 分发、受控流与缺失响应检查;`ok` |
|
|
112
|
+
| [`src/remote-proxy.ts`](src/remote-proxy.ts) | 命名空间/方法查找与生成映射的 mock 类型 |
|
|
113
|
+
| [`src/streams.ts`](src/streams.ts) | `frames` / `openStream` 脚本、`streamHandle` / `streamMethod` 假实现标注,以及 `MockStream`(句柄 + `AsyncIterable`) |
|
|
114
|
+
| [`src/log.ts`](src/log.ts) | 带共享 `seq` 计数器的日志 |
|
|
115
|
+
|
|
116
|
+
</details>
|
|
117
|
+
|
|
118
|
+
-----
|
|
119
|
+
|
|
120
|
+
<a id="model-experience"></a>
|
|
121
|
+
## 模型体验
|
|
122
|
+
|
|
123
|
+
无;本包是浏览器侧测试基础设施,无一物到达模型请求。
|
|
124
|
+
|
|
125
|
+
#### KV Cache 影响
|
|
126
|
+
|
|
127
|
+
无;本包既不组装也不发送提供方请求。
|
|
128
|
+
|
|
129
|
+
## 已知限制与延期工作
|
|
130
|
+
|
|
131
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
132
|
+
|
|
133
|
+
- **仅进程内载体**——`rpc` 服务同一 realm 中的 Connection 实例;不提供给浏览器车道测试用的 HTTP 或 WebSocket 载体。
|
|
134
|
+
- **值按引用传递**——应答与下行项都未经序列化就到达客户端,真实线路会拒绝的非 JSON 下行值在这里原样通过;只有句柄的 `send()` 施加真实的上行校验。
|
|
135
|
+
- **不校验值**——一元应答必须是调用方读取的结果(`{ ok, value }` 或 `{ ok: false, error }`);mock 原样传递它,不检查这些字段。
|
|
136
|
+
- **不做 payload 匹配**——规则只按端点匹配;在 handler 内按业务参数判别。
|
|
137
|
+
- **原生流覆盖自行管理 iterable**——覆盖返回自有 iterable 时,不参与脚本流日志、`requests`、`opened`、`drained` 以及 `push` / `end` / `fail`;调用方也负责取消。原生调用断言仍然有效。需要这些控制能力的场景应使用已登记的流脚本。
|
|
138
|
+
|
|
139
|
+
<a id="dev-note"></a>
|
|
140
|
+
### 开发备注
|
|
141
|
+
|
|
142
|
+
<details>
|
|
143
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
144
|
+
|
|
145
|
+
无。
|
|
146
|
+
|
|
147
|
+
</details>
|