dsh-aris-panel 0.2.6 → 0.2.8
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.md +1 -1
- package/README_CN.md +1 -1
- package/dsh/client.js +1 -1
- package/dsh/http-rpc.mjs +25 -15
- package/package.json +2 -2
- package/test/client-render-smoke.mjs +131 -0
- package/test/transport-smoke.mjs +69 -7
package/README.md
CHANGED
|
@@ -100,7 +100,7 @@ This overlay is **not** equivalent to the installed bundle: it cannot restore Co
|
|
|
100
100
|
## Known limits
|
|
101
101
|
|
|
102
102
|
- **Tracks a Harness window, declared as a peer dependency.** DeepSeek Harness is a developer preview; this bundle declares `@deepseek-ai/dsh >=0.1.7-rc.2 <0.3.0-0` as an optional peer. The transport test passes against `0.2.0-rc.2`; the `HostConnectionFetch` registry it uses is present and identical in `0.1.7-rc.2`, so the range covers both. The `dsh` peer-dependency gate reads that range: an incompatible runtime skips the whole bundle with a line in the boot log instead of loading a panel whose transport may have moved. On a newer runtime, re-run `node node_modules/dsh-aris-panel/test/transport-smoke.mjs`, then widen the range, or grant the exact-version exemption with `dsh plugin allow-version`.
|
|
103
|
-
- **The panel's browser transport is HTTP, not an RPC channel.** dsh's generic channel registry (`connection.rpc.handle`) resolves `webServer` from Connection's own context, which
|
|
103
|
+
- **The panel's browser transport is HTTP, not an RPC channel.** dsh's generic channel registry (`connection.rpc.handle`) resolves `webServer` from Connection's own context, which dsh `0.1.7-rc.2` and `0.2.0-rc.2` do not inject there, so an out-of-tree channel registration throws and every call dies as HTTP 405 from the SPA fallback. ARIS mounts exact Fetch routes on the `/api` prefix instead (documented as `HostConnectionFetch.register`), which dsh already serves behind its Host/Origin fence and browser-session cookie. `test/transport-smoke.mjs` pins both halves of that contract against the installed dsh; it cannot cover the carrier's own auth path, so verify a live host with `POST /api/aris-run` and `/api/aris-wb` — 400 means mounted, 404 means not.
|
|
104
104
|
- **No dsh packages are declared as npm dependencies.** In-box packages are host-provided and resolve from the Harness installation through the profile module fallback. The Harness's own rule keeps `@deepseek-ai/dsh-*` out of `dependencies`, and the packages version in lockstep with the CLI, so any range this bundle pinned would fight the version the user already has installed. The peer range above is a compatibility declaration, marked optional so it never pulls a second copy of the runtime.
|
|
105
105
|
- **`web_fetch` is off.** Stock dsh ships it disabled and this bundle does not enable it, which would mean depending on a provider package. Skills that reach the web use `web_search`, or `bash` with `curl`.
|
|
106
106
|
- **Reviewer thread continuity is process-local.** A Harness restart, or any MCP reconnect that replaces the Codex child, loses saved `threadId`s. Rounds after that start fresh; `review-stage/REVIEWER_MEMORY.md` remains the durable record either way.
|
package/README_CN.md
CHANGED
|
@@ -81,7 +81,7 @@ ARIS_REPO=/absolute/path/to/aris NODE_USE_ENV_PROXY=1 \
|
|
|
81
81
|
## 已知限制
|
|
82
82
|
|
|
83
83
|
- **跟随一个 Harness 版本窗口,并以 peerDependency 声明。** DeepSeek Harness 处于技术预览;本 bundle 把 `@deepseek-ai/dsh >=0.1.7-rc.2 <0.3.0-0` 声明为 optional peerDependency。传输层测试在 `0.2.0-rc.2` 上通过;它所用的 `HostConnectionFetch` 注册表在 `0.1.7-rc.2` 里存在且一致,所以该范围覆盖两者。dsh 的 peerDependency 门禁会读这个范围:不兼容的运行时会在启动日志里**整体跳过本 bundle**,而不是加载一个传输层可能已经变了的面板。升到更新版本后:先跑 `node node_modules/dsh-aris-panel/test/transport-smoke.mjs`,再放宽范围,或用 `dsh plugin allow-version` 授予精确版本豁免。
|
|
84
|
-
- **面板的浏览器传输走 HTTP,不再走 RPC channel。** dsh 的通用 channel 注册表(`connection.rpc.handle`)是从 Connection 自己的上下文里解析 `webServer`
|
|
84
|
+
- **面板的浏览器传输走 HTTP,不再走 RPC channel。** dsh 的通用 channel 注册表(`connection.rpc.handle`)是从 Connection 自己的上下文里解析 `webServer` 的,而 dsh `0.1.7-rc.2` / `0.2.0-rc.2` 并不在那里注入它,于是树外插件的 channel 注册直接抛错,每次调用都变成 SPA 兜底返回的 HTTP 405。ARIS 改为在 `/api` 前缀上挂精确 Fetch 路由(即文档中的 `HostConnectionFetch.register`),dsh 本来就为 `/api` 提供了 Host/Origin 信任围栏与浏览器会话 Cookie。`test/transport-smoke.mjs` 针对已安装的 dsh 钉住这两半契约;它覆盖不到载体自身的鉴权链路,所以请用真实 host 复核:`POST /api/aris-run` 与 `/api/aris-wb` —— 400 = 已挂载,404 = 未挂载。
|
|
85
85
|
- **不把任何 dsh 包声明为 npm 依赖。** 内置包由宿主提供,通过 profile 的模块回退从 Harness 安装目录解析。Harness 自己的规约就要求 `@deepseek-ai/dsh-*` 不进 `dependencies`;而且这些子包与 CLI **锁步发版**,本 bundle 无论钉哪个范围,都会和用户已装的版本打架。上面的 peer 范围只是兼容性声明,且标为 optional,因此永远不会拉进第二份运行时副本。
|
|
86
86
|
- **`web_fetch` 关闭。** 原版 dsh 默认关闭它,本 bundle 也不打开——打开就意味着依赖一个 provider 包。需要联网的技能改用 `web_search`,或用 `bash` 跑 `curl`。
|
|
87
87
|
- **审稿线程连续性是进程内的。** Harness 重启、或任何一次 MCP 重连替换了 Codex 子进程,已保存的 `threadId` 都会失效,之后的轮次从新线程开始;无论如何 `review-stage/REVIEWER_MEMORY.md` 都是持久记录。
|
package/dsh/client.js
CHANGED
|
@@ -1009,7 +1009,7 @@ function ArisWorkbenchView(props) {
|
|
|
1009
1009
|
setError(String(cause))
|
|
1010
1010
|
setData(undefined)
|
|
1011
1011
|
}).finally(function () { if (!silent) setBusy(false) })
|
|
1012
|
-
}, [
|
|
1012
|
+
}, [sessionId])
|
|
1013
1013
|
|
|
1014
1014
|
React.useEffect(function () { load() }, [load])
|
|
1015
1015
|
|
package/dsh/http-rpc.mjs
CHANGED
|
@@ -3,21 +3,27 @@
|
|
|
3
3
|
*
|
|
4
4
|
* dsh exposes two registries on the Connection service:
|
|
5
5
|
*
|
|
6
|
-
* - `connection.rpc`
|
|
7
|
-
* `webServer`
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
6
|
+
* - `connection.rpc` — generic channel registry. `handle()` calls
|
|
7
|
+
* `owner.webServer.register(route)` where `owner` is the Connection
|
|
8
|
+
* service's own context, not the caller's: Cordis resolves a service's
|
|
9
|
+
* methods against the context that constructed it, so injecting
|
|
10
|
+
* `webServer` on the *consumer* does not help. In dsh `0.1.7-rc.2` and
|
|
11
|
+
* `0.2.0-rc.2` that context injects `credentials` only while `webServer` is
|
|
12
|
+
* provided by a sibling plugin fiber, so the lookup walks past the
|
|
13
|
+
* provider to the root and throws `cannot get property "webServer" without
|
|
14
|
+
* inject`. The channel then never mounts and the browser gets HTTP 405 from
|
|
15
|
+
* the SPA fallback. Making that path work from outside means editing dsh's
|
|
16
|
+
* own package (the workaround this package used until 0.2.6), which every
|
|
17
|
+
* dsh upgrade reverts; a build whose Connection context does inject
|
|
18
|
+
* `webServer` would not need it.
|
|
14
19
|
*
|
|
15
20
|
* - `connection.fetch` — exact-path Fetch registry under `/api`
|
|
16
21
|
* (documented as `HostConnectionFetch.register(ConnectionFetchRoute)`).
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
22
|
+
* Registration resolves nothing beyond the Connection service itself, so it
|
|
23
|
+
* mounts under whatever Connection context the host provides. Serving is
|
|
24
|
+
* dsh's own `/api` prefix route, which applies the Host/Origin trust fence
|
|
25
|
+
* and the browser-session cookie before dispatching to any exact fetch
|
|
26
|
+
* route — the same authority the RPC channel used to carry.
|
|
21
27
|
*
|
|
22
28
|
* ARIS therefore mounts one JSON endpoint per view on the Fetch registry. The
|
|
23
29
|
* wire contract is deliberately tiny and mirrored in `dsh/client.js`:
|
|
@@ -26,10 +32,13 @@
|
|
|
26
32
|
* 200 {"ok": true, "value": unknown}
|
|
27
33
|
* 200 {"ok": false, "error": {"code": string, "message": string}}
|
|
28
34
|
* 400 malformed envelope
|
|
29
|
-
* 405 non-POST method
|
|
30
35
|
*
|
|
31
36
|
* Application failures travel as HTTP 200 with `ok: false`, so the view shows
|
|
32
37
|
* the handler's own message instead of a transport error.
|
|
38
|
+
*
|
|
39
|
+
* The route also answers 405 to a direct non-POST dispatch, but dsh's carrier
|
|
40
|
+
* only hands over methods the route declares, so a browser request with another
|
|
41
|
+
* method is answered 404 by the carrier before this handler runs.
|
|
33
42
|
*/
|
|
34
43
|
|
|
35
44
|
/** Method set every ARIS endpoint accepts. */
|
|
@@ -112,8 +121,9 @@ export function httpRpcRoute(path, handler) {
|
|
|
112
121
|
*
|
|
113
122
|
* The caller supplies the effect and the injected services, because the two
|
|
114
123
|
* ARIS views need different ones (`sessions` for both, `agents` and
|
|
115
|
-
* `workspaceRegistry` for the workbench only).
|
|
116
|
-
* `webServer
|
|
124
|
+
* `workspaceRegistry` for the workbench only). Registration resolves no
|
|
125
|
+
* `webServer`, which is what keeps the mount independent of the Connection
|
|
126
|
+
* context's inject list.
|
|
117
127
|
*
|
|
118
128
|
* @param ctx - the ARIS plugin context, which must have `connection` injected.
|
|
119
129
|
* @param path - exact `/api/<segment>` route path the browser POSTs to.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-aris-panel",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "ARIS research-workflow skills + interactive workbench panel for DeepSeek Harness (user fork of dsh-aris: 83 skills, bundled upstream dsh-aris 0.1.1 payload, ppt-master PPTX backend for the talk pipeline, cross-model Codex review, idea/workflow launcher, research status board).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"node": "^22.19.0 || >=24.0.0"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "node test/transport-smoke.mjs"
|
|
11
|
+
"test": "node test/transport-smoke.mjs && node test/client-render-smoke.mjs"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
14
|
"dsh-plugin",
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARIS browser-half render smoke test.
|
|
3
|
+
*
|
|
4
|
+
* node test/client-render-smoke.mjs
|
|
5
|
+
*
|
|
6
|
+
* Why this exists: `test/transport-smoke.mjs` proves the host half mounts, but
|
|
7
|
+
* dsh/client.js is only ever executed by a browser, so a client-side error
|
|
8
|
+
* (an undefined identifier left behind by a refactor, say) reaches the user as
|
|
9
|
+
* "the panel does not open" with no failing test. This file executes the real
|
|
10
|
+
* bundle in Node against a minimal React/element renderer and walks every
|
|
11
|
+
* returned component, so ReferenceErrors and typos in component bodies or hook
|
|
12
|
+
* dependency arrays fail here instead.
|
|
13
|
+
*
|
|
14
|
+
* It is deliberately a stub renderer, not React: components are called and
|
|
15
|
+
* their element trees walked, hooks run eagerly, and network calls are answered
|
|
16
|
+
* with empty payloads. That catches crashes, not behaviour.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { pathToFileURL } from 'node:url'
|
|
20
|
+
|
|
21
|
+
let failures = 0
|
|
22
|
+
function check(label, condition, detail) {
|
|
23
|
+
if (condition) {
|
|
24
|
+
console.log(` PASS ${label}`)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
failures += 1
|
|
28
|
+
console.log(` FAIL ${label}${detail === undefined ? '' : ` — ${detail}`}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Collect anything a hook or effect throws instead of letting it escape. */
|
|
32
|
+
const crashes = []
|
|
33
|
+
process.on('unhandledRejection', (reason) => { crashes.push(reason) })
|
|
34
|
+
|
|
35
|
+
console.log('\n1. the bundle loads the way dsh client-modules loads it')
|
|
36
|
+
let registration
|
|
37
|
+
let documentState
|
|
38
|
+
{
|
|
39
|
+
const styleNodes = []
|
|
40
|
+
documentState = { styleNodes }
|
|
41
|
+
globalThis.window = {
|
|
42
|
+
__ModuleLoader__: {
|
|
43
|
+
load(module) { registration = module },
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
globalThis.document = {
|
|
47
|
+
createElement() {
|
|
48
|
+
const node = { textContent: '', removed: false, remove() { node.removed = true } }
|
|
49
|
+
styleNodes.push(node)
|
|
50
|
+
return node
|
|
51
|
+
},
|
|
52
|
+
head: { append() {} },
|
|
53
|
+
}
|
|
54
|
+
// The views answer every call with an empty successful payload.
|
|
55
|
+
globalThis.fetch = async () => ({ status: 200, json: async () => ({ ok: true, value: {} }) })
|
|
56
|
+
|
|
57
|
+
await import(new URL('../dsh/client.js', import.meta.url).href)
|
|
58
|
+
check('the bundle registers one module', registration !== undefined && registration.id === 'dsh-aris-panel',
|
|
59
|
+
registration === undefined ? 'window.__ModuleLoader__.load was never called' : String(registration.id))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log('\n2. every component renders without crashing')
|
|
63
|
+
let rendered
|
|
64
|
+
{
|
|
65
|
+
/** Minimal React: enough for createElement + the hooks this bundle uses. */
|
|
66
|
+
const React = {
|
|
67
|
+
createElement(type, props, ...children) {
|
|
68
|
+
return { type, props: { ...(props ?? {}), children: children.flat() } }
|
|
69
|
+
},
|
|
70
|
+
useState(initial) { return [typeof initial === 'function' ? initial() : initial, () => {}] },
|
|
71
|
+
useEffect(run) { run() },
|
|
72
|
+
useCallback(fn) { return fn },
|
|
73
|
+
useRef(value) { return { current: value } },
|
|
74
|
+
}
|
|
75
|
+
const requireStub = (name) => {
|
|
76
|
+
if (name === 'react') return React
|
|
77
|
+
throw new Error(`client bundle required an unexpected module: ${name}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const plugin = registration.factory(requireStub)
|
|
81
|
+
check('the factory returns a plugin with an inject list', Array.isArray(plugin.inject), JSON.stringify(plugin.inject))
|
|
82
|
+
check('the plugin declares the slot service', plugin.inject.includes('slots'))
|
|
83
|
+
|
|
84
|
+
let render
|
|
85
|
+
const ctx = {
|
|
86
|
+
connection: {},
|
|
87
|
+
effect(run) { return run() },
|
|
88
|
+
slots: {
|
|
89
|
+
inject(name, registerCall) {
|
|
90
|
+
check('the plugin injects the conversation view slot', name === 'conversation.view', name)
|
|
91
|
+
registerCall()
|
|
92
|
+
},
|
|
93
|
+
register(spec, renderCall) {
|
|
94
|
+
check('the ARIS slot registers itself', spec.id === 'aris' && spec.name === 'conversation.view', JSON.stringify(spec))
|
|
95
|
+
render = renderCall
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
plugin.apply(ctx)
|
|
100
|
+
check('the slot exposes a render function', typeof render === 'function')
|
|
101
|
+
|
|
102
|
+
/** Call function components and walk host elements, like React would. */
|
|
103
|
+
let elements = 0
|
|
104
|
+
const walk = (node, depth = 0) => {
|
|
105
|
+
if (node === null || node === undefined || typeof node === 'boolean' || typeof node === 'string' || typeof node === 'number') return
|
|
106
|
+
if (Array.isArray(node)) { for (const child of node) walk(child, depth); return }
|
|
107
|
+
if (depth > 40) throw new Error('render tree did not settle')
|
|
108
|
+
elements += 1
|
|
109
|
+
if (typeof node.type === 'function') { walk(node.type(node.props ?? {}), depth + 1); return }
|
|
110
|
+
walk(node.props?.children, depth + 1)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
rendered = render({ sessionId: 'render-smoke', openView: undefined })
|
|
115
|
+
walk(rendered)
|
|
116
|
+
check('the ARIS tab renders a tree', elements > 5, `${elements} elements`)
|
|
117
|
+
} catch (error) {
|
|
118
|
+
check('the ARIS tab renders a tree', false, `${error.name}: ${error.message}`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log('\n3. the stylesheet effect is wired')
|
|
123
|
+
check('one style node was created', documentState.styleNodes.length === 1, String(documentState.styleNodes.length))
|
|
124
|
+
|
|
125
|
+
const asyncCrashes = crashes.filter((reason) => reason !== undefined)
|
|
126
|
+
check('no asynchronous crash escaped the views', asyncCrashes.length === 0, asyncCrashes.map(String).join('; '))
|
|
127
|
+
check('the module left no uncaught error', rendered !== undefined)
|
|
128
|
+
|
|
129
|
+
console.log(failures === 0 ? '\nOK — the browser half renders' : `\n${failures} check(s) failed`)
|
|
130
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
131
|
+
process.exit(failures === 0 && crashes.length === 0 ? 0 : 1)
|
package/test/transport-smoke.mjs
CHANGED
|
@@ -9,15 +9,23 @@
|
|
|
9
9
|
* It pins the two facts this package's transport choice rests on:
|
|
10
10
|
*
|
|
11
11
|
* 1. `connection.rpc.handle()` cannot mount a channel from an out-of-tree
|
|
12
|
-
* plugin
|
|
13
|
-
* resolves `webServer` from Connection's own context, and
|
|
14
|
-
*
|
|
15
|
-
* that provides it and throws. The channel then never mounts, and
|
|
16
|
-
* browser gets HTTP 405 from the SPA fallback (the failure this
|
|
17
|
-
* used to work around by editing dsh's own node_modules).
|
|
12
|
+
* plugin against dsh `0.1.7-rc.2` / `0.2.0-rc.2`. `HostConnectionService
|
|
13
|
+
* .register()` resolves `webServer` from Connection's own context, and
|
|
14
|
+
* those builds inject `credentials` only, so the lookup walks past the
|
|
15
|
+
* plugin that provides it and throws. The channel then never mounts, and
|
|
16
|
+
* the browser gets HTTP 405 from the SPA fallback (the failure this
|
|
17
|
+
* package used to work around by editing dsh's own node_modules).
|
|
18
18
|
* 2. The Connection Fetch registry route this package uses instead mounts
|
|
19
19
|
* without any `webServer` lookup and round-trips every envelope shape the
|
|
20
|
-
* views depend on.
|
|
20
|
+
* views depend on, including both real view registrations.
|
|
21
|
+
*
|
|
22
|
+
* What this test deliberately does NOT cover: the shipped Connection plugin's
|
|
23
|
+
* own `apply()` (so `/api` prefix mounting, the Host/Origin fence, cookie auth
|
|
24
|
+
* and the node↔fetch body bridge are assumed, not exercised), the browser half
|
|
25
|
+
* (`dsh/client.js` is never executed here), and effect disposal/reload. Those
|
|
26
|
+
* need a live host: POST `/api/aris-run` and `/api/aris-wb` with an
|
|
27
|
+
* authenticated browser session after a restart — 400 means the route is
|
|
28
|
+
* mounted and the envelope was rejected, 404 means it never mounted.
|
|
21
29
|
*
|
|
22
30
|
* Set DSH_ROOT to test another install.
|
|
23
31
|
*/
|
|
@@ -54,6 +62,8 @@ const cordisEntry = requireFromConnection.resolve('@deepseek-ai/cordis')
|
|
|
54
62
|
const { Context } = await import(pathToFileURL(cordisEntry).href)
|
|
55
63
|
const { HostConnectionService, API_PATH } = await import(pathToFileURL(connectionEntry).href)
|
|
56
64
|
const { httpRpcRoute, registerHttpRpc } = await import(new URL('../dsh/http-rpc.mjs', import.meta.url).href)
|
|
65
|
+
const { registerWorkbench } = await import(new URL('../dsh/workbench.mjs', import.meta.url).href)
|
|
66
|
+
const { registerRunStatus } = await import(new URL('../dsh/run-status.mjs', import.meta.url).href)
|
|
57
67
|
|
|
58
68
|
let failures = 0
|
|
59
69
|
function check(label, condition, detail) {
|
|
@@ -198,6 +208,58 @@ console.log('\n2. the Fetch registry route ARIS mounts instead')
|
|
|
198
208
|
|
|
199
209
|
const unknownPath = await handler.fetch(new Request('http://dsh.internal/api/not-aris', { method: 'POST' }))
|
|
200
210
|
check('an unclaimed /api path is HTTP 404', unknownPath.status === 404)
|
|
211
|
+
|
|
212
|
+
console.log('\n3. the real ARIS view registrations, over the real carrier')
|
|
213
|
+
// The host halves inject these; stubs are enough because registration only
|
|
214
|
+
// stores a handler, and the two endpoints below read workspace state lazily.
|
|
215
|
+
await root.plugin({
|
|
216
|
+
name: 'fake-session-services',
|
|
217
|
+
apply(ctx) {
|
|
218
|
+
ctx.provide('sessions', { get: () => undefined })
|
|
219
|
+
ctx.provide('agents', { create: async () => undefined })
|
|
220
|
+
ctx.provide('workspaceRegistry', {})
|
|
221
|
+
ctx.provide('sessionPersistence', {})
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
await root.plugin({
|
|
225
|
+
name: 'aris-views',
|
|
226
|
+
apply(ctx) {
|
|
227
|
+
registerWorkbench(ctx)
|
|
228
|
+
registerRunStatus(ctx)
|
|
229
|
+
},
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const view = (path, body) => handler.fetch(new Request('http://dsh.internal' + path, {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
headers: { 'content-type': 'application/json' },
|
|
235
|
+
body: JSON.stringify(body),
|
|
236
|
+
}))
|
|
237
|
+
// A nested `ctx.inject(...)` fiber activates asynchronously; a 400 (envelope
|
|
238
|
+
// rejected) means the route exists, a 404 means it never mounted.
|
|
239
|
+
const waitsForRoute = async (path) => {
|
|
240
|
+
const deadline = Date.now() + 3000
|
|
241
|
+
while (Date.now() < deadline) {
|
|
242
|
+
const probe = await view(path, {})
|
|
243
|
+
if (probe.status !== 404) return true
|
|
244
|
+
await new Promise(resolve => setTimeout(resolve, 25))
|
|
245
|
+
}
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
check('/api/aris-wb mounts through the real registerWorkbench()', await waitsForRoute('/api/aris-wb'))
|
|
249
|
+
check('/api/aris-run mounts through the real registerRunStatus()', await waitsForRoute('/api/aris-run'))
|
|
250
|
+
|
|
251
|
+
const skills = await (await view('/api/aris-wb', { endpoint: 'skills', payload: {} })).json()
|
|
252
|
+
check('the workbench skills endpoint answers over HTTP',
|
|
253
|
+
skills.ok === true && Array.isArray(skills.value.skills) && skills.value.skills.length > 20,
|
|
254
|
+
JSON.stringify(skills).slice(0, 200))
|
|
255
|
+
|
|
256
|
+
const state = await (await view('/api/aris-run', { endpoint: 'state', payload: { sessionId: 'no-such-session' } })).json()
|
|
257
|
+
check('the run-status endpoint answers over HTTP',
|
|
258
|
+
state.ok === true && Array.isArray(state.value.artifacts), JSON.stringify(state).slice(0, 200))
|
|
259
|
+
|
|
260
|
+
const unknown = await (await view('/api/aris-run', { endpoint: 'nope', payload: {} })).json()
|
|
261
|
+
check('an unknown endpoint reports through the envelope',
|
|
262
|
+
unknown.ok === false && unknown.error.code === 'UNKNOWN_ENDPOINT', JSON.stringify(unknown).slice(0, 200))
|
|
201
263
|
}
|
|
202
264
|
|
|
203
265
|
console.log(failures === 0 ? '\nOK — transport contract holds' : `\n${failures} check(s) failed`)
|