dsh-aris-panel 0.2.7 → 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/dsh/client.js +1 -1
- package/dsh/http-rpc.mjs +8 -7
- package/package.json +2 -2
- package/test/client-render-smoke.mjs +131 -0
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
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
*
|
|
20
20
|
* - `connection.fetch` — exact-path Fetch registry under `/api`
|
|
21
21
|
* (documented as `HostConnectionFetch.register(ConnectionFetchRoute)`).
|
|
22
|
-
* Registration
|
|
23
|
-
*
|
|
24
|
-
* which applies the Host/Origin trust fence
|
|
25
|
-
* before dispatching to any exact fetch
|
|
26
|
-
* channel used to carry.
|
|
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.
|
|
27
27
|
*
|
|
28
28
|
* ARIS therefore mounts one JSON endpoint per view on the Fetch registry. The
|
|
29
29
|
* wire contract is deliberately tiny and mirrored in `dsh/client.js`:
|
|
@@ -121,8 +121,9 @@ export function httpRpcRoute(path, handler) {
|
|
|
121
121
|
*
|
|
122
122
|
* The caller supplies the effect and the injected services, because the two
|
|
123
123
|
* ARIS views need different ones (`sessions` for both, `agents` and
|
|
124
|
-
* `workspaceRegistry` for the workbench only).
|
|
125
|
-
* `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.
|
|
126
127
|
*
|
|
127
128
|
* @param ctx - the ARIS plugin context, which must have `connection` injected.
|
|
128
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)
|