@crewhaus/hangar-ui 0.5.0
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 +309 -0
- package/assets/hangar.css +1726 -0
- package/assets/index.html +21 -0
- package/assets/js/api.js +368 -0
- package/assets/js/app.js +734 -0
- package/assets/js/dom.js +228 -0
- package/assets/js/keys.js +159 -0
- package/assets/js/markdown.js +131 -0
- package/assets/js/notify.js +152 -0
- package/assets/js/omnibox.js +307 -0
- package/assets/js/router.js +280 -0
- package/assets/js/routes.js +793 -0
- package/assets/js/shapes.js +36 -0
- package/assets/js/supervision.js +1334 -0
- package/assets/js/util.js +426 -0
- package/assets/js/views/activity.js +140 -0
- package/assets/js/views/approvals.js +155 -0
- package/assets/js/views/channels.js +455 -0
- package/assets/js/views/control.js +253 -0
- package/assets/js/views/costs.js +103 -0
- package/assets/js/views/creds.js +610 -0
- package/assets/js/views/data.js +456 -0
- package/assets/js/views/deploy.js +106 -0
- package/assets/js/views/evals-lab.js +1029 -0
- package/assets/js/views/evals.js +342 -0
- package/assets/js/views/feedback.js +555 -0
- package/assets/js/views/health.js +176 -0
- package/assets/js/views/inbox.js +103 -0
- package/assets/js/views/inspect.js +751 -0
- package/assets/js/views/jobs.js +110 -0
- package/assets/js/views/library.js +605 -0
- package/assets/js/views/memory-fabric.js +2026 -0
- package/assets/js/views/memory.js +228 -0
- package/assets/js/views/onboarding.js +253 -0
- package/assets/js/views/overview.js +196 -0
- package/assets/js/views/panes.js +132 -0
- package/assets/js/views/proc.js +280 -0
- package/assets/js/views/review.js +164 -0
- package/assets/js/views/runs.js +452 -0
- package/assets/js/views/runtime.js +450 -0
- package/assets/js/views/schedulers.js +133 -0
- package/assets/js/views/security.js +925 -0
- package/assets/js/views/sessions.js +265 -0
- package/assets/js/views/settings.js +489 -0
- package/assets/js/views/spec-edit.js +1289 -0
- package/assets/js/views/spec.js +66 -0
- package/assets/js/views/thredz.js +1853 -0
- package/assets/js/views/token.js +49 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +145 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# @crewhaus/hangar-ui
|
|
2
|
+
|
|
3
|
+
The Hangar console's static UI — the browser side of the harness manager.
|
|
4
|
+
One HTML shell, one CSS file, and a set of hand-written browser ES modules,
|
|
5
|
+
embedded as text and exported as a serve-path map for the hangar server to
|
|
6
|
+
host.
|
|
7
|
+
|
|
8
|
+
M1 was read-only over harness state. **M2 makes the console a driver**: it
|
|
9
|
+
starts, stops, restarts and drains supervised harnesses, watches a run live,
|
|
10
|
+
pokes scheduler lanes through `crewhaus.control.v1`, and settles parked
|
|
11
|
+
approvals and review items — and every one of those actions shows the exact
|
|
12
|
+
CLI command it runs. **M3 adds the detail surface**: eight more harness tabs
|
|
13
|
+
and three more fleet screens over a 178-route contract, covering the spec's
|
|
14
|
+
write side, the memory fabric's write side, the eval/dataset/feedback loops,
|
|
15
|
+
credentials + channels + security, Thredz, and the raw inspectors.
|
|
16
|
+
|
|
17
|
+
## Zero-build by design
|
|
18
|
+
|
|
19
|
+
There is no build step, no framework, and no npm dependency. The `assets/`
|
|
20
|
+
tree IS the app: plain ES modules loaded by the browser exactly as checked
|
|
21
|
+
in. `src/index.ts` imports every asset `with { type: "text" }` — static
|
|
22
|
+
imports only, because `bun build --compile` embeds only what the import
|
|
23
|
+
graph references statically; a runtime `readFileSync` of a
|
|
24
|
+
package-relative path resolves inside the compiled binary's virtual
|
|
25
|
+
filesystem where the files were never placed, and bricks the CLI at boot.
|
|
26
|
+
A unit test enforces that the embed map and the `assets/` tree stay in
|
|
27
|
+
lockstep (every file present, every relative import resolving inside the
|
|
28
|
+
map, every module transpiler-clean), so the zero-build path still has a
|
|
29
|
+
compile-shaped safety net.
|
|
30
|
+
|
|
31
|
+
## Exported API
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { hangarAssets, CONTENT_TYPES, contentTypeFor } from "@crewhaus/hangar-ui";
|
|
35
|
+
|
|
36
|
+
hangarAssets; // Readonly<Record<string, { body: string; contentType: string }>>
|
|
37
|
+
contentTypeFor("/assets/js/app.js"); // "text/javascript; charset=utf-8"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`hangarAssets` is keyed by serve path — `"/"` is the SPA shell (the app is
|
|
41
|
+
a hash router, so `/` is the only HTML route) and everything else lives
|
|
42
|
+
under `/assets/…` exactly as laid out on disk. Hand the map to the hangar
|
|
43
|
+
server's static-asset option; a server route for `path` responds with
|
|
44
|
+
`body` and `Content-Type: contentType`.
|
|
45
|
+
|
|
46
|
+
## The app
|
|
47
|
+
|
|
48
|
+
- **Auth bootstrap** — reads `#t=<token>` from the URL fragment into
|
|
49
|
+
sessionStorage (and strips it via `history.replaceState`), then sends
|
|
50
|
+
`Authorization: Bearer` on every request. Fragments never reach server
|
|
51
|
+
logs; no cookies means no CSRF surface. Any 401 swaps in a token-paste
|
|
52
|
+
screen that points at the token file (`~/.crewhaus/hangar/token` by
|
|
53
|
+
default). The live run feed is hand-rolled over `fetch` for the same
|
|
54
|
+
reason `EventSource` cannot be used: it carries no bearer header.
|
|
55
|
+
- **Hash router, deep-linkable** — `#/` (Library), the fleet screens
|
|
56
|
+
`#/{runs, approvals, review, activity, credentials, feedback, thredz}`,
|
|
57
|
+
`#/h/<id>` (Overview), and `#/h/<id>/{spec, runs[/<runId>], schedulers,
|
|
58
|
+
sessions[/<sess>], evals[/<runId>[/<sampleId>]], memory[/wiki/<slug>],
|
|
59
|
+
data, feedback, costs, creds, channels, security, thredz, deploy, inspect,
|
|
60
|
+
dev}`. The eight M3 tabs capture their trailing segments GENERICALLY
|
|
61
|
+
(`M3_TABS` → a `rest` array), so a sub-screen inside one of them never
|
|
62
|
+
needs a new `parseRoute` case.
|
|
63
|
+
- **Library** — dense sortable table (name + dir tail, shape badge, model,
|
|
64
|
+
supervision state + parked-approval count, eval health dot + text,
|
|
65
|
+
sessions, spend 7d, capability badges, group chips); a sub-rail of stored
|
|
66
|
+
groups plus client-computed smart groups (Failing evals, Unbudgeted, Has
|
|
67
|
+
Thredz, Recently active, Ungrouped, Missing); missing-dir cards with
|
|
68
|
+
relocate/remove; Scan and Add-harness actions; a one-line fleet rollup.
|
|
69
|
+
- **Detail** — Overview (header with click-to-copy dir and capability
|
|
70
|
+
chips, health checklist, eval-trend SVG, memory mini-cards, cost
|
|
71
|
+
mini-chart, expandable preflight report), Spec (masked YAML with line numbers,
|
|
72
|
+
env-ref presence checklist, parse issues), Sessions (TTL countdowns,
|
|
73
|
+
evicted rows fall through to summaries, per-kind transcript rendering
|
|
74
|
+
with a metadata gutter and a Raw toggle), Evals (history with
|
|
75
|
+
flaky/partial/replayed/baseline badges, run + sample drill-down),
|
|
76
|
+
Memory (facts with tombstones folded, wiki with a minimal safe-subset
|
|
77
|
+
markdown reader, continuity, dream, watchme), Costs (per-model table +
|
|
78
|
+
7-day bars).
|
|
79
|
+
|
|
80
|
+
### M2 — the driving surfaces
|
|
81
|
+
|
|
82
|
+
- **Runs & daemons** (`#/runs`) — every registered harness's supervision
|
|
83
|
+
state in one board: state pill, pid, uptime, adopted badge, restarts in
|
|
84
|
+
the window with the next-restart countdown, last exit + failure class,
|
|
85
|
+
control availability. Row actions Start / Stop / Restart / Drain, each
|
|
86
|
+
disabled WITH ITS REASON; a blocked start opens the refusal modal. Under
|
|
87
|
+
it: the global job queue (running / pending / interrupted) and a failure
|
|
88
|
+
board grouping the fleet by how it died ("3 harnesses exited 31 —
|
|
89
|
+
provider funding").
|
|
90
|
+
- **Run console** (`#/h/<id>/runs/<runId>`) — the live SSE feed rendered in
|
|
91
|
+
the M1 transcript vocabulary, with interleaved (scrubbed) prose, a
|
|
92
|
+
stats/cost HUD folded from the frames, and the terminal exit banner with
|
|
93
|
+
its failure class and remediation. The `replay`-first / `done`-last
|
|
94
|
+
grammar means a finished run and a live one take the same code path.
|
|
95
|
+
- **Schedulers** (`#/h/<id>/schedulers`) — the four-lane timeline with
|
|
96
|
+
cadence + its source, last outcome, and next due — or an honest "cadence
|
|
97
|
+
only" naming why the phase is unknown. Wake respects `pokeable`; a
|
|
98
|
+
`tick_in_flight` refusal retries, a `draining` one does not.
|
|
99
|
+
- **Approvals** (`#/approvals`) and **Review** (`#/review`) — cross-harness
|
|
100
|
+
triage with the verbatim (masked) tool input, keyboard-first (j/k to
|
|
101
|
+
move, one key per verdict, `.` row menu, `?` bindings). Review offers
|
|
102
|
+
thumbs only where they can land on a session turn.
|
|
103
|
+
- **Activity** (`#/activity`) — what changed since yesterday, grouped by
|
|
104
|
+
kind with per-group filters, every row deep-linking into the screen that
|
|
105
|
+
owns it.
|
|
106
|
+
- **Deployments** (`#/h/<id>/deploy`) — the records with class (local /
|
|
107
|
+
PaaS / cf-worker) and health, and an honest empty state naming the file
|
|
108
|
+
that will hold them.
|
|
109
|
+
- **Action faces on the M1 screens** — session retention pin, eval baseline
|
|
110
|
+
re-pin, dream "Run now" (through the job queue), plus the supervision
|
|
111
|
+
pill, bundle-freshness badge and control availability on the detail
|
|
112
|
+
header, where a plan that cannot be built renders its remedy as a button.
|
|
113
|
+
|
|
114
|
+
### M3 — the detail surface
|
|
115
|
+
|
|
116
|
+
Eleven new view modules (`views/{spec-edit, memory-fabric, evals-lab, data,
|
|
117
|
+
feedback, creds, channels, security, thredz, inspect, runtime}.js`) draw
|
|
118
|
+
eight new harness tabs and three new fleet screens:
|
|
119
|
+
|
|
120
|
+
- **Spec** (`#/h/<id>/spec`) gains the write side — the trust-tier table
|
|
121
|
+
(auto-tunable rows edit inline; human-owned rows demand the typed spec-name
|
|
122
|
+
confirm or route to `crewhaus propose`), the diff interstitial, version
|
|
123
|
+
history with pin/rollback, and the builders (templates, graders, dataset,
|
|
124
|
+
MCP connectors).
|
|
125
|
+
- **Memory** (`#/h/<id>/memory`) gains the fabric's write side — facts with
|
|
126
|
+
forget/sweep (tombstones, never a delete), continuity trash + restore, the
|
|
127
|
+
wiki editor with `expectedVersion` and its stale retry, watch-me analytics,
|
|
128
|
+
reports, intents and the synthesize review.
|
|
129
|
+
- **Datasets** (`#/h/<id>/data`), **Feedback** (`#/h/<id>/feedback`) and the
|
|
130
|
+
eval lab inside **Evals** cover the quality loops: registry + hygiene +
|
|
131
|
+
growth, the fewshot/faq/lessons/advice feeds, and the matrix / trends /
|
|
132
|
+
judge / graders / coverage / sentinel screens.
|
|
133
|
+
- **Credentials**, **Channels** and **Security** (`#/h/<id>/{creds,channels,
|
|
134
|
+
security}`) show env-key PRESENCE (never a value), the channel doctor and
|
|
135
|
+
provisioning, and the egress / PII / compliance / on-chain / retention
|
|
136
|
+
panels.
|
|
137
|
+
- **Thredz** (`#/h/<id>/thredz`) is proxied server-side — the workspace key
|
|
138
|
+
never reaches the browser.
|
|
139
|
+
- **Inspect** (`#/h/<id>/inspect`) is the raw store browser, and **Dev & MCP**
|
|
140
|
+
(`#/h/<id>/dev`) drives the dev server and the MCP servers.
|
|
141
|
+
- Fleet-wide: **Credentials** (`#/credentials`, the provider × harness
|
|
142
|
+
matrix), **Feedback** (`#/feedback`) and **Thredz** (`#/thredz`).
|
|
143
|
+
|
|
144
|
+
Two conventions make that surface cheap to extend:
|
|
145
|
+
|
|
146
|
+
1. **The client wrappers are GENERATED.** `routes.js` carries all 178 M3
|
|
147
|
+
routes as pure data tagged with a `group`, and `api.js` builds one wrapper
|
|
148
|
+
per key from the map rather than hand-writing them. A hand-written name
|
|
149
|
+
WINS a collision, so an M1/M2 verb keeps its bespoke wrapper.
|
|
150
|
+
2. **Every M3 read answers `{present, note, verb, …}`** — is there anything
|
|
151
|
+
to show, why is it empty, and which CLI verb creates it. Most of this
|
|
152
|
+
surface is normally empty on a given harness, so the views render that
|
|
153
|
+
triple directly through `emptyState(message, verb)`; an empty panel is
|
|
154
|
+
never indistinguishable from a broken one.
|
|
155
|
+
|
|
156
|
+
### UX invariants
|
|
157
|
+
|
|
158
|
+
Dark theme default with an explicit light toggle; traffic-light dots are
|
|
159
|
+
always paired with text; every empty state names the CLI verb that creates
|
|
160
|
+
its data; 404s render as "nothing yet", never as errors; loading skeletons
|
|
161
|
+
everywhere; cached figures show their as-of time; the footer shows the
|
|
162
|
+
hangar version + wire-protocol pair. No animation beyond opacity.
|
|
163
|
+
|
|
164
|
+
Three more that M2 adds, because a console that drives things can lie in
|
|
165
|
+
new ways:
|
|
166
|
+
|
|
167
|
+
1. **Every action shows its CLI twin** — the exact command, with a copy
|
|
168
|
+
button. The CLI and the console drive the same harness-local state tree,
|
|
169
|
+
so the twin is both a trust affordance and a fallback. Where no CLI verb
|
|
170
|
+
exists yet (the retention pin), the UI says so instead of inventing one.
|
|
171
|
+
2. **A disabled control always says why.** `expected: true` control
|
|
172
|
+
refusals (`no_control_port`, `lane_not_armed`, `draining`) render
|
|
173
|
+
disabled-with-reason, never as an error toast.
|
|
174
|
+
3. **Say which answer you got.** Bundle freshness distinguishes the
|
|
175
|
+
hash-exact verdict from the mtime approximation; a last exit read off
|
|
176
|
+
the run ledger rather than observed by this manager is badged as such.
|
|
177
|
+
|
|
178
|
+
### Injection safety
|
|
179
|
+
|
|
180
|
+
Rendering is 100% `createElement`/`textContent` via a tiny `el()`/`text()`
|
|
181
|
+
toolkit — markup-string assignment is banned and a unit test scans every
|
|
182
|
+
embedded module for it. The wiki renderer parses a minimal markdown subset
|
|
183
|
+
(headings, bold, inline code, lists, fenced code) into tokens and renders
|
|
184
|
+
them as text nodes; raw HTML in an article body stays literal text.
|
|
185
|
+
|
|
186
|
+
## Server routes consumed
|
|
187
|
+
|
|
188
|
+
This package is the client of record for the whole route surface, and the
|
|
189
|
+
contract is EXECUTABLE: `assets/js/routes.js` holds every route as pure
|
|
190
|
+
data (method, path template, body-shape name), `api.js` builds every
|
|
191
|
+
request from that map, and the hangar server's `contract.test.ts` imports
|
|
192
|
+
the same map and drives each route against a live fixture server — writes
|
|
193
|
+
must take effect, reads must carry every field the views dereference. A
|
|
194
|
+
unit test here drives every `api.js` wrapper against the same map, so the
|
|
195
|
+
client cannot drift from it either. The two sides cannot drift silently.
|
|
196
|
+
|
|
197
|
+
Reads: `/api/harnesses[?hydrate=1]`, `/api/registry/groups`, `/api/h/:id`,
|
|
198
|
+
and per-harness `spec`, `preflight`, `sessions[/:sess]`,
|
|
199
|
+
`evals[/:runId[/:sampleId]]`,
|
|
200
|
+
`memory/{facts,wiki[/:slug],state,dream,watchme}`, `costs`, plus
|
|
201
|
+
`/api/version` — and for M2 `proc`, `runs[/:runId]`,
|
|
202
|
+
`runs/:runId/events` (SSE), `control/status`, `schedulers`, `deployments`,
|
|
203
|
+
`/api/{approvals,review,activity,jobs}`. Writes: registry CRUD
|
|
204
|
+
(`POST /api/harnesses` `{dir}`, `POST /api/scan`,
|
|
205
|
+
`POST /api/registry/groups` `{name}`, `POST /api/registry/scan-roots`
|
|
206
|
+
`{dir}`, `PUT /api/h/:id/{groups,tags,pin,notes}`,
|
|
207
|
+
`POST /api/h/:id/relocate` `{newDir}`, `DELETE /api/h/:id`) plus the M2
|
|
208
|
+
verbs: `POST /api/h/:id/proc/{start,stop,restart,drain}`,
|
|
209
|
+
`POST /api/h/:id/control/{wake,drain}`,
|
|
210
|
+
`POST /api/h/:id/approvals/:apprId/{grant,deny}`,
|
|
211
|
+
`POST /api/h/:id/review/:itemId`, `POST /api/h/:id/jobs`,
|
|
212
|
+
`POST /api/h/:id/sessions/:sess/pin`, `POST /api/h/:id/evals/baseline`.
|
|
213
|
+
|
|
214
|
+
M3 adds 178 more, grouped `spec | memory | evals | data | feedback | creds |
|
|
215
|
+
channels | security | thredz | inspect | runtime` — enumerated in
|
|
216
|
+
`assets/js/routes.js` and mirrored route-for-route by the server's
|
|
217
|
+
`M3_ROUTES`, which the contract test asserts is the same set (key, method and
|
|
218
|
+
path). They are not re-listed here: the map is the inventory, and a README
|
|
219
|
+
copy of it would be the thing that goes stale.
|
|
220
|
+
|
|
221
|
+
All payload readers are tolerant: unknown fields are ignored, missing
|
|
222
|
+
fields degrade to "—", and unknown transcript/trace kinds still render. A
|
|
223
|
+
failed write is never silent — non-2xx surfaces as a toast, and a zero-root
|
|
224
|
+
Scan says "no scan roots configured" and offers the add-scan-root input
|
|
225
|
+
instead of pretending success. Two refusals are deliberately NOT errors:
|
|
226
|
+
`POST /proc/start`'s 409 becomes the preflight modal, and a control
|
|
227
|
+
envelope with `expected: true` disables its control with the server's own
|
|
228
|
+
sentence.
|
|
229
|
+
|
|
230
|
+
The Library paints twice by design: instantly from the cache-only feed
|
|
231
|
+
(figures labeled with their `cachedAt` as-of time), then again from a
|
|
232
|
+
background `?hydrate=1` refetch with freshly computed rollups.
|
|
233
|
+
|
|
234
|
+
## M4 — the polish layer
|
|
235
|
+
|
|
236
|
+
| Module | What it owns |
|
|
237
|
+
|---|---|
|
|
238
|
+
| `assets/js/keys.js` | the app-level keyboard reducer: who owns ⌘K, Escape and `?` (HM-190) |
|
|
239
|
+
| `assets/js/omnibox.js` | the ⌘K overlay — navigation rows are links, ACTION rows are proposals |
|
|
240
|
+
| `assets/js/notify.js` | the ONE notification poll: the evaluating GET, shared (HM-183) |
|
|
241
|
+
| `assets/js/views/health.js` | the explained score, per harness and as the fleet board |
|
|
242
|
+
| `assets/js/views/onboarding.js` | first boot: scan-root picker, demo mode, direct registration |
|
|
243
|
+
| `assets/js/views/settings.js` | notification rules, read-only mode, the plugin inventory |
|
|
244
|
+
| `assets/js/views/panes.js` | sandboxed plugin panes |
|
|
245
|
+
|
|
246
|
+
**Keyboard ownership is a reducer, not a race.** M2 shipped the inbox half of
|
|
247
|
+
HM-190 (`triageKey` in `supervision.js`: j/k, one-key verdicts, `.`, `?`).
|
|
248
|
+
`keys.js` adds the layer above it and decides between them: a field owns its
|
|
249
|
+
keys (including ⌘K), the open omnibox owns everything but Escape, and `?`
|
|
250
|
+
goes to the app sheet only when no inbox is mounted — two sheets opening on
|
|
251
|
+
one keypress is the bug that rule exists to prevent. The reducer claims
|
|
252
|
+
nothing else, so the inbox keeps receiving j/k/g/d untouched.
|
|
253
|
+
|
|
254
|
+
The field rule has exactly one exception, and the omnibox is it: the overlay
|
|
255
|
+
focuses its own input, so from the moment it opens every key arrives from a
|
|
256
|
+
field. Escape and the ⌘K toggle are therefore claimed even in a field while
|
|
257
|
+
the omnibox is open — otherwise the overlay swallows both of its own exits
|
|
258
|
+
and the only way out is a mouse click, on the feature whose whole point is
|
|
259
|
+
the keyboard. Nothing else is taken: the query still types.
|
|
260
|
+
|
|
261
|
+
**One evaluating poll, shared.** `GET /api/notifications` IS the rules
|
|
262
|
+
evaluation (the manager runs no timer of its own), and every delivery it
|
|
263
|
+
returns is deduped away server-side and never returned again — so a second
|
|
264
|
+
caller does not read that state, it consumes it. `notify.js` owns the only
|
|
265
|
+
call: it polls on an interval so a console left open actually notifies,
|
|
266
|
+
toasts what each pass delivered, keeps the nav badge honest, and hands the
|
|
267
|
+
same snapshot to the Settings screen rather than letting it issue a GET that
|
|
268
|
+
would eat a toast. The PUT that saves the rules answers with the same
|
|
269
|
+
evaluated view, so its answer is folded back in through the same path. The
|
|
270
|
+
asset-hygiene suite pins the single caller.
|
|
271
|
+
|
|
272
|
+
**The omnibox proposes; it never acts.** Enter on a navigation row goes
|
|
273
|
+
somewhere. Enter on an ACTION row opens a confirm, showing the exact CLI
|
|
274
|
+
command it will run, and only the four process verbs are executable at all —
|
|
275
|
+
anything else the server proposes renders with a reason instead.
|
|
276
|
+
|
|
277
|
+
**The score is never rendered alone.** Every deduction row is a link to the
|
|
278
|
+
tab that fixes it; signals the server could not read are shown as unknowns
|
|
279
|
+
rather than folded into the number.
|
|
280
|
+
|
|
281
|
+
**Panes are the one deliberate exception to the markup-string ban.** A pane
|
|
282
|
+
document is handed to an `<iframe>` via `srcdoc` with `sandbox="allow-scripts"`
|
|
283
|
+
and no `allow-same-origin`, so it becomes a SANDBOXED document with an opaque
|
|
284
|
+
origin — never part of this one. The asset-hygiene suite pins the exception
|
|
285
|
+
to that single module, and `paneSandbox` strips `allow-same-origin` even if a
|
|
286
|
+
payload asks for it.
|
|
287
|
+
|
|
288
|
+
## Testing
|
|
289
|
+
|
|
290
|
+
`bun test src` — the embed-map completeness/hygiene suite plus unit tests
|
|
291
|
+
for the pure browser modules (`util.js`, `supervision.js`, `markdown.js`,
|
|
292
|
+
`router.js`, `routes.js`, `shapes.js`) and the `api.js` client under a
|
|
293
|
+
stubbed `fetch` (every wrapper asserted against the route map's
|
|
294
|
+
method/path/body). All DOM-free at import time and imported directly by
|
|
295
|
+
bun. Deterministic: clocks are injected, no network, no subprocesses. The
|
|
296
|
+
server side of the same contract runs in `@crewhaus/hangar-server`'s
|
|
297
|
+
`contract.test.ts`.
|
|
298
|
+
|
|
299
|
+
The M2 screens stay testable without a browser by keeping the decisions —
|
|
300
|
+
state → row, envelope → disabled-with-reason, SSE frame → feed item,
|
|
301
|
+
refusal → modal model, keystroke → next state — as pure functions in
|
|
302
|
+
`supervision.js`; `views/*.js` are thin DOM builders over them.
|
|
303
|
+
|
|
304
|
+
The M3 suites follow the same shape: `routes.test.ts` pins the route map's
|
|
305
|
+
grammar, `api.test.ts` drives every generated M3 wrapper against it under a
|
|
306
|
+
stubbed `fetch`, and `creds-views.test.ts` / `memory-fabric.test.ts` unit-test
|
|
307
|
+
the pure decisions those tabs make. The embed-map suite additionally asserts
|
|
308
|
+
that no module is ORPHANED — reachable from `app.js`'s import graph — so a
|
|
309
|
+
view that loses its last importer cannot keep shipping to the browser.
|