@astrale-os/adapter-cloudflare 0.1.9 → 0.2.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/package.json +1 -1
- package/template/.agents/skills/astrale-cli/SKILL.md +1 -1
- package/template/.agents/skills/astrale-domain/SKILL.md +5 -5
- package/template/.env.example +5 -7
- package/template/README.md +2 -2
- package/template/client/README.md +81 -62
- package/template/client/__tests__/app.test.tsx +143 -98
- package/template/client/__tests__/harness.ts +62 -12
- package/template/client/__tests__/kernel.test.ts +40 -51
- package/template/client/__tests__/seam.test.tsx +115 -0
- package/template/client/index.html +1 -1
- package/template/client/package.json +1 -0
- package/template/client/src/app.tsx +34 -83
- package/template/client/src/main.tsx +2 -2
- package/template/client/src/shell/client.ts +67 -0
- package/template/client/src/shell/index.ts +20 -0
- package/template/client/src/shell/invoke.ts +35 -0
- package/template/client/src/shell/transformers.ts +72 -0
- package/template/client/src/shell/use-async.ts +56 -0
- package/template/client/src/shell/use-capability.ts +59 -0
- package/template/client/src/shell/use-node.ts +61 -0
- package/template/client/src/shell/use-shell.ts +91 -0
- package/template/client/src/shell/view-router.tsx +97 -0
- package/template/client/src/status/components/StatusCard.tsx +50 -0
- package/template/client/src/status/components/index.ts +1 -0
- package/template/client/src/status/hooks/index.ts +3 -0
- package/template/client/src/status/hooks/useCheck.mutation.ts +16 -0
- package/template/client/src/status/hooks/useCheckable.query.ts +64 -0
- package/template/client/src/status/index.ts +7 -0
- package/template/client/src/status/status.api.ts +12 -0
- package/template/client/src/status/status.mappers.ts +19 -0
- package/template/client/src/status/status.types.ts +11 -0
- package/template/client/src/styles.css +182 -4
- package/template/client/src/ui/StatusBadge.tsx +31 -0
- package/template/client/src/ui/format.ts +24 -0
- package/template/client/src/ui/index.ts +13 -0
- package/template/client/src/ui/surface.tsx +56 -0
- package/template/client/src/ui/value.tsx +32 -0
- package/template/client/src/views/status.tsx +28 -0
- package/template/client/tsconfig.json +2 -1
- package/template/client/vite.config.ts +11 -13
- package/template/client/vitest.config.ts +11 -5
- package/template/core/monitor/health.ts +34 -0
- package/template/core/monitor/index.ts +9 -0
- package/template/core/monitor/keys.ts +41 -0
- package/template/core/monitor/node.ts +57 -0
- package/template/deps.ts +10 -9
- package/template/domain.ts +1 -1
- package/template/env.ts +2 -9
- package/template/integrations/prober/http.ts +32 -0
- package/template/integrations/prober/mock.ts +18 -0
- package/template/integrations/prober/port.ts +26 -0
- package/template/integrations/prober/registry.ts +65 -0
- package/template/package.json +1 -1
- package/template/pnpm-lock.yaml +2766 -0
- package/template/runtime/index.ts +63 -34
- package/template/runtime/monitor/check.ts +29 -0
- package/template/runtime/monitor/index.ts +9 -0
- package/template/runtime/monitor/seed.ts +95 -0
- package/template/runtime/monitor/watch.ts +31 -0
- package/template/runtime/shared.ts +21 -0
- package/template/runtime/status-page/add.ts +21 -0
- package/template/runtime/status-page/check.ts +50 -0
- package/template/runtime/status-page/create.ts +24 -0
- package/template/runtime/status-page/index.ts +8 -0
- package/template/schema/index.ts +11 -4
- package/template/schema/monitor.ts +94 -0
- package/template/views/index.ts +8 -2
- package/template/views/status-page.ts +16 -0
- package/template/client/src/lib/kernel.ts +0 -135
- package/template/client/src/lib/shell.ts +0 -197
- package/template/client/src/lib/use-node.ts +0 -66
- package/template/client/src/lib/use-shell.ts +0 -85
- package/template/core/keys.ts +0 -28
- package/template/core/note.ts +0 -148
- package/template/integrations/summary/heuristic.ts +0 -25
- package/template/integrations/summary/http.ts +0 -69
- package/template/integrations/summary/port.ts +0 -21
- package/template/integrations/summary/registry.ts +0 -52
- package/template/schema/note.ts +0 -67
- package/template/views/note.ts +0 -21
package/package.json
CHANGED
|
@@ -91,7 +91,7 @@ Examples:
|
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
93
|
astrale call /:blog.acme.com:class.Author:list
|
|
94
|
-
astrale call /:blog.acme.com:
|
|
94
|
+
astrale call /:blog.acme.com:class.Monitor:watch url=https://astrale.ai
|
|
95
95
|
astrale call /blog.acme.com/alice::deactivate
|
|
96
96
|
astrale call @f00d...::deactivate
|
|
97
97
|
```
|
|
@@ -42,8 +42,8 @@ domain.ts # THE manifest — wires it ALL: defineDomain({ schema, meth
|
|
|
42
42
|
# a renamed module is a compile error here, not a missing route.
|
|
43
43
|
astrale.config.ts # binds the domain to its deploy adapter (deploy(domain, …)) — node-only
|
|
44
44
|
schema/ # classes/interfaces/edges — the contract (zod props, fn signatures)
|
|
45
|
-
# +
|
|
46
|
-
core
|
|
45
|
+
# + index.ts exports D = compileDomain(schema) (resolved paths/keys)
|
|
46
|
+
core/<context>/ # pure, transport-agnostic logic per bounded context (e.g. core/monitor: keys/health/node)
|
|
47
47
|
integrations/ # external-API ports + adapters + a lazy registry (see §4) — what deps is built from
|
|
48
48
|
runtime/index.ts # composition root: the methods map; each execute resolves deps → calls core logic
|
|
49
49
|
functions/ # standalone remote functions (webhook-shaped endpoints)
|
|
@@ -123,7 +123,7 @@ Handler context: `kernel` (callback client bound to the composed credential —
|
|
|
123
123
|
caller's delegated authority ∪ this function's own), `self` (instance methods),
|
|
124
124
|
`params` (zod-validated), `deps` (your typed `Deps` from `deps.ts`; raw `Env` if
|
|
125
125
|
you omit the mapper), `auth` (principal, verified claims). Resolve ports from
|
|
126
|
-
`deps` per request (`deps.
|
|
126
|
+
`deps` per request (`deps.prober()` — the registry builds + caches them per
|
|
127
127
|
isolate) — NEVER construct external clients at module load (workers must be
|
|
128
128
|
import-side-effect-free).
|
|
129
129
|
|
|
@@ -167,7 +167,7 @@ example — Scaleway/WorkOS/KV):
|
|
|
167
167
|
|
|
168
168
|
1. **Port** — a narrow interface in `integrations/<feature>/port.ts` declaring
|
|
169
169
|
only what the logic needs (`WeatherClient { forecast(city) }`, the scaffold's
|
|
170
|
-
`
|
|
170
|
+
`Prober { probe(url) }`). `core/` logic depends on the port, never
|
|
171
171
|
on fetch/SDKs/env.
|
|
172
172
|
2. **Adapter(s) + registry** — `createXClient(config)` in
|
|
173
173
|
`integrations/<feature>/` (one per backend), plus a `registry.ts` that reads
|
|
@@ -177,7 +177,7 @@ example — Scaleway/WorkOS/KV):
|
|
|
177
177
|
upstream detail in `cause`. The registry builds the chosen adapter LAZILY +
|
|
178
178
|
caches it per isolate (a worker never validates an unused backend's env).
|
|
179
179
|
3. **Wiring** — `deps.ts` mounts the registry (`defineDomain({ deps })`); the
|
|
180
|
-
`execute` hook resolves the PORT from `deps` (`deps.
|
|
180
|
+
`execute` hook resolves the PORT from `deps` (`deps.prober()`) per
|
|
181
181
|
request and passes it to the `core/` logic.
|
|
182
182
|
|
|
183
183
|
Secrets & config:
|
package/template/.env.example
CHANGED
|
@@ -5,10 +5,8 @@
|
|
|
5
5
|
#
|
|
6
6
|
# EXAMPLE_API_KEY=sk-...
|
|
7
7
|
#
|
|
8
|
-
# ──
|
|
9
|
-
# The scaffold
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# SUMMARIZER_BASE_URL=https://api.openai.com/v1
|
|
14
|
-
# SUMMARIZER_MODEL=gpt-4o-mini
|
|
8
|
+
# ── Prober (the example external-API integration) ──────────────────────────
|
|
9
|
+
# The scaffold probes monitor targets with a real, KEYLESS HTTP request by
|
|
10
|
+
# default (no secret needed). These knobs are optional config, not secrets:
|
|
11
|
+
# PROBER=http # `http` (default) | `mock` (offline/deterministic)
|
|
12
|
+
# PROBE_TIMEOUT_MS=10000
|
package/template/README.md
CHANGED
|
@@ -27,7 +27,7 @@ pnpm prod # deploy prod → prints a URL
|
|
|
27
27
|
|
|
28
28
|
```
|
|
29
29
|
schema/ classes + interfaces (the data model) + compiled accessors
|
|
30
|
-
core
|
|
30
|
+
core/<context>/ pure, transport-agnostic logic per bounded context (e.g. core/monitor)
|
|
31
31
|
integrations/ external-API ports + adapters + a lazy registry (built into deps)
|
|
32
32
|
runtime/ the execute() handlers — the composition root (the methods map)
|
|
33
33
|
views/ iframe-mountable UIs
|
|
@@ -89,7 +89,7 @@ adapter-owned keys `name`, `main`, `assets`, `routes` are rejected (use
|
|
|
89
89
|
|
|
90
90
|
- **Edit a handler** (`core/` logic or `runtime/` wiring) → hot-reloads at the same URL. Nothing to reinstall.
|
|
91
91
|
- **Edit the schema** (`schema/`) → rebuilds the graph; reinstall with `astrale domain install <url> --direct`.
|
|
92
|
-
- **`postInstall`** (the static `
|
|
92
|
+
- **`postInstall`** (the static `Monitor.seed` method) runs once after install, as
|
|
93
93
|
the system identity, so the domain can seed itself and set its own grants. It
|
|
94
94
|
must be a class-hosted static addressed by a typed colon-path
|
|
95
95
|
(`/:origin:class.X:seed`) — the kernel refuses tree paths here.
|
|
@@ -1,38 +1,66 @@
|
|
|
1
1
|
# astrale-domain-client — SPA for domain views
|
|
2
2
|
|
|
3
|
-
A small React + Vite SPA that renders the domain's
|
|
4
|
-
|
|
5
|
-
its target node id and a
|
|
6
|
-
and renders
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
3
|
+
A small React + Vite SPA that renders the domain's Views. It is loaded inside an
|
|
4
|
+
iframe mounted by the Astrale shell, runs the shell handshake (via the real
|
|
5
|
+
`@astrale-os/shell`) to learn its target node id and a kernel session, fetches
|
|
6
|
+
that node from the kernel, and renders it. The bundled status view loads a
|
|
7
|
+
`Checkable` node (a Monitor or a StatusPage — both carry a `status` prop and a
|
|
8
|
+
`::check` method) and renders its status, with a "Check now" button that calls
|
|
9
|
+
the node's `check` instance method and reloads the node. Built into `../.dist/`
|
|
10
|
+
and served by the generated worker (`.astrale/`) under `/ui/*` via its `ASSETS`
|
|
11
|
+
binding.
|
|
12
|
+
|
|
13
|
+
## Built on the real shell SDK
|
|
14
|
+
|
|
15
|
+
This client consumes `@astrale-os/shell` for the child handshake AND the kernel
|
|
16
|
+
client: `src/shell/use-shell.ts` boots `createShell({ mode: 'sandboxed' })`, and
|
|
17
|
+
feature hooks call kernel methods through `shell.kernel` (token refresh, codec
|
|
18
|
+
negotiation, redirect following, and delegation are all handled by the SDK — no
|
|
19
|
+
inline wire code). `@` is aliased to `src/` (mirrors `tsconfig` paths), so feature
|
|
20
|
+
code imports `@/shell`, `@/ui`, `@/status`.
|
|
19
21
|
|
|
20
22
|
## Layout
|
|
21
23
|
|
|
24
|
+
Feature-first: each feature owns its types/api/mappers/hooks/components. The
|
|
25
|
+
`shell/` and `ui/` folders are the generic, domain-neutral seams every feature
|
|
26
|
+
builds on. The template ships ONE feature, `status/`, over a `Checkable` node —
|
|
27
|
+
one card loads the node and exposes its status + a "Check now" action.
|
|
28
|
+
|
|
22
29
|
```
|
|
23
30
|
src/
|
|
24
31
|
main.tsx # entry → createRoot(App)
|
|
25
|
-
app.tsx #
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
use-
|
|
31
|
-
|
|
32
|
+
app.tsx # path router: ROUTES { '/ui/status-page': StatusView }
|
|
33
|
+
styles.css # self-contained styles (no Tailwind), design tokens
|
|
34
|
+
shell/ # GENERIC kernel/shell adapter on @astrale-os/shell
|
|
35
|
+
client.ts # prop readers (PROP, readProp, readPropBySuffix) + errors
|
|
36
|
+
invoke.ts # callMethod / invokeNode (@<id>::<method>) / nodeAddr
|
|
37
|
+
use-shell.ts # useShell() → createShell({ mode: 'sandboxed' })
|
|
38
|
+
use-node.ts # useNode(session, id) → @<id>::get (+ reload)
|
|
39
|
+
use-async.ts # generic reloadable async resource
|
|
40
|
+
use-capability.ts # write lifecycle (idle → running → done/failed)
|
|
41
|
+
transformers.ts # qualified-prop / link / node-array shaping
|
|
42
|
+
view-router.tsx # resolveView(routes) + ViewFrame (handshake gate)
|
|
43
|
+
index.ts # barrel → @/shell
|
|
44
|
+
ui/ # PURE presentation — no kernel, no hooks
|
|
45
|
+
surface.tsx # Panel / ErrorBanner / EmptyState / Spinner
|
|
46
|
+
StatusBadge.tsx # StatusBadge (up | degraded | down | unknown)
|
|
47
|
+
value.tsx # KV / Mono / ExternalLink
|
|
48
|
+
format.ts # relativeTime
|
|
49
|
+
index.ts # barrel → @/ui
|
|
50
|
+
status/ # THE status feature — polymorphic over Checkable nodes
|
|
51
|
+
status.types.ts # CheckableRecord
|
|
52
|
+
status.api.ts # check(session, id) → @<id>::check
|
|
53
|
+
status.mappers.ts # checkableFromNode (node → record)
|
|
54
|
+
hooks/ # useCheckable.query / useCheck.mutation
|
|
55
|
+
components/ # StatusCard (container: hooks + @/ui)
|
|
56
|
+
index.ts # barrel → @/status
|
|
57
|
+
views/
|
|
58
|
+
status.tsx # StatusView = <ViewFrame>…<StatusCard/></ViewFrame>
|
|
32
59
|
__tests__/
|
|
33
|
-
harness.ts # fake shell parent + fake kernel (fetch stub)
|
|
34
|
-
app.test.tsx # handshake → render → setTarget →
|
|
35
|
-
|
|
60
|
+
harness.ts # fake shell parent (real protocol) + fake kernel (fetch stub)
|
|
61
|
+
app.test.tsx # handshake → render → check now → setTarget → fallback
|
|
62
|
+
seam.test.tsx # pure @/ui + @/status hooks in isolation (no handshake)
|
|
63
|
+
kernel.test.ts # pure helpers: prop readers, mapper, relativeTime
|
|
36
64
|
```
|
|
37
65
|
|
|
38
66
|
## Dev loops
|
|
@@ -40,7 +68,7 @@ __tests__/
|
|
|
40
68
|
```bash
|
|
41
69
|
pnpm dev # vite build --watch → ../.dist/ (worker auto-reloads, no HMR)
|
|
42
70
|
pnpm dev:hmr # vite dev on http://127.0.0.1:5173/ (React fast-refresh)
|
|
43
|
-
pnpm test # vitest run (happy-dom; fake parent
|
|
71
|
+
pnpm test # vitest run (happy-dom; fake parent speaks the real shell protocol)
|
|
44
72
|
```
|
|
45
73
|
|
|
46
74
|
For HMR, set `VIEW_DEV_URL=http://127.0.0.1:5173` in the worker's `.dev.vars`;
|
|
@@ -48,38 +76,29 @@ the generated worker forwards `/ui/*` to vite.
|
|
|
48
76
|
|
|
49
77
|
## How it connects
|
|
50
78
|
|
|
51
|
-
1. The domain declares
|
|
52
|
-
|
|
53
|
-
2. The shell mounts that iframe and completes the handshake
|
|
54
|
-
`MessagePort`
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
3. `useNode(session, nodeId)`
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
- no redirect following — a `{ redirect }` envelope (remote-domain Functions)
|
|
78
|
-
throws rather than re-minting against the target worker,
|
|
79
|
-
- no client-side credential minting / `whoami` (the handshake token is used
|
|
80
|
-
as-is), and
|
|
81
|
-
- no writes — only `@<id>::get`.
|
|
82
|
-
|
|
83
|
-
These all live in `@astrale-os/kernel-client` / `@astrale-os/shell`. To grow
|
|
84
|
-
past the template, pull those in (and `link:` them in dev) instead of extending
|
|
85
|
-
the inline subset.
|
|
79
|
+
1. The domain declares each View in `../views/` with `mount: '/ui/<path>'`. The
|
|
80
|
+
SDK points the View node's iframe at `<serving url>/ui/<path>`.
|
|
81
|
+
2. The shell mounts that iframe and completes the handshake (a transferred
|
|
82
|
+
`MessagePort` + a `ctrl:handshake` carrying `kernelUrl`, the delegation token,
|
|
83
|
+
and the `targetNodeId`). `useShell()` boots `@astrale-os/shell`, surfaces the
|
|
84
|
+
kernel session, and tracks `setTarget` hot-swaps.
|
|
85
|
+
3. A feature loads its node through the session: `useNode(session, nodeId)` calls
|
|
86
|
+
`@<id>::get` over `shell.kernel`, and a mapper (`checkableFromNode`) projects
|
|
87
|
+
it to a typed record. Domain props are read by key SUFFIX (`.property.status`,
|
|
88
|
+
…) because the domain origin is unknown at build time; the name uses the kernel
|
|
89
|
+
`Named.name` key.
|
|
90
|
+
4. Writes go through `useCapability`: "Check now" runs `@<id>::check`, then calls
|
|
91
|
+
`useCheckable`'s `reload()` so the fresh status re-renders. The shell's kernel
|
|
92
|
+
client owns the credential and refreshes it before expiry — features never
|
|
93
|
+
touch the token.
|
|
94
|
+
|
|
95
|
+
## Adding a view
|
|
96
|
+
|
|
97
|
+
1. Write a `ViewComponent` (usually wrapping `ViewFrame`) under `src/views/`.
|
|
98
|
+
2. Add a `ROUTES` entry keyed by its mount path in `src/app.tsx`.
|
|
99
|
+
3. Register a matching `defineView({ mount: '/ui/<path>' })` in the domain's
|
|
100
|
+
`views/` so the shell mounts an iframe there.
|
|
101
|
+
|
|
102
|
+
For a NEW feature, mirror `src/status/`: a `*.types.ts`, `*.api.ts`,
|
|
103
|
+
`*.mappers.ts`, a `hooks/` folder of `.query`/`.mutation` hooks, and a
|
|
104
|
+
`components/` folder of containers that compose `@/ui`.
|
|
@@ -1,21 +1,33 @@
|
|
|
1
|
-
import { act, cleanup, render, screen } from '@testing-library/react'
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
1
|
+
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
3
3
|
|
|
4
|
-
import { App } from '
|
|
5
|
-
|
|
4
|
+
import { App } from '@/app'
|
|
5
|
+
|
|
6
|
+
import { checkableNode, installFakeKernel, installFakeParent, ok } from './harness'
|
|
6
7
|
|
|
7
8
|
const KERNEL_URL = 'https://k.example.test/api'
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The router picks a view from `window.location.pathname`, so every test mounts
|
|
12
|
+
* at the StatusPage view's path. `replaceState` rewrites the happy-dom location
|
|
13
|
+
* without a reload; the fallback test overrides it.
|
|
14
|
+
*/
|
|
15
|
+
function mountAt(path: string) {
|
|
16
|
+
window.history.replaceState({}, '', path)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
mountAt('/ui/status-page')
|
|
21
|
+
})
|
|
9
22
|
|
|
10
23
|
afterEach(() => {
|
|
11
24
|
cleanup()
|
|
12
25
|
})
|
|
13
26
|
|
|
14
27
|
/**
|
|
15
|
-
* Let the handshake + any kernel
|
|
28
|
+
* Let the handshake + any kernel fetches settle, flushing the resulting React
|
|
16
29
|
* state updates inside `act`. The handshake completes over a real MessagePort
|
|
17
|
-
* (async hops), so a microtask isn't enough — a short timer pump is.
|
|
18
|
-
* it in `act` keeps the updates warning-free and committed before we assert.
|
|
30
|
+
* (async hops), so a microtask isn't enough — a short timer pump is.
|
|
19
31
|
*/
|
|
20
32
|
async function flush(ms = 20) {
|
|
21
33
|
await act(async () => {
|
|
@@ -23,35 +35,28 @@ async function flush(ms = 20) {
|
|
|
23
35
|
})
|
|
24
36
|
}
|
|
25
37
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
expect(req.headers['accept']).toBe(KERNEL_TYPE)
|
|
36
|
-
// Envelope body shape.
|
|
37
|
-
expect(req.body.method).toBe(`@${nodeId}::get`)
|
|
38
|
-
expect(req.body.params).toEqual({})
|
|
39
|
-
expect(req.body.id).toBeTruthy()
|
|
38
|
+
function handshake(targetNodeId?: string) {
|
|
39
|
+
return installFakeParent({
|
|
40
|
+
windowId: 'win-1',
|
|
41
|
+
kernelUrl: KERNEL_URL,
|
|
42
|
+
functionId: 'ui-status-page',
|
|
43
|
+
delegationToken: 'tok-A',
|
|
44
|
+
tokenExpiresAt: Date.now() + 3_600_000,
|
|
45
|
+
targetNodeId,
|
|
46
|
+
})
|
|
40
47
|
}
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
/** A StatusPage node fixture — its rolled-up `status`, no `url`. */
|
|
50
|
+
const page = (id: string, status: string, name = 'Public status') =>
|
|
51
|
+
checkableNode({ id, name, status, className: 'StatusPage' })
|
|
52
|
+
|
|
53
|
+
describe('status view (/ui/status-page)', () => {
|
|
54
|
+
it('loads the StatusPage node and renders its name + rolled-up status', async () => {
|
|
55
|
+
const parent = handshake('page-1')
|
|
56
|
+
const kernel = installFakeKernel((body) => {
|
|
57
|
+
if (body.method === '@page-1::get') return ok(page('page-1', 'degraded'))
|
|
58
|
+
return { error: { code: 3002, message: `unexpected: ${body.method}` } }
|
|
51
59
|
})
|
|
52
|
-
const kernel = installFakeKernel(() =>
|
|
53
|
-
ok(noteNode({ id: 'note-1', name: 'My first note', body: 'Hello from the kernel.' })),
|
|
54
|
-
)
|
|
55
60
|
|
|
56
61
|
try {
|
|
57
62
|
render(<App />)
|
|
@@ -60,113 +65,139 @@ describe('ui-note view — handshake + real node render', () => {
|
|
|
60
65
|
// Parent observed the child's handshakeAck.
|
|
61
66
|
await expect(parent.ack).resolves.toBeUndefined()
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
expect(
|
|
68
|
+
expect(screen.getByText('Public status')).toBeTruthy()
|
|
69
|
+
const badge = screen.getByText('DEGRADED')
|
|
70
|
+
expect(badge.className).toContain('status-degraded')
|
|
71
|
+
// A StatusPage carries no `url`, so there's no url row.
|
|
72
|
+
expect(screen.queryByText('url')).toBeNull()
|
|
66
73
|
|
|
67
|
-
// Exactly one kernel call,
|
|
74
|
+
// Exactly one kernel call, for the node load.
|
|
68
75
|
expect(kernel.calls).toHaveLength(1)
|
|
69
|
-
|
|
76
|
+
expect(kernel.calls[0]!.body.method).toBe('@page-1::get')
|
|
70
77
|
} finally {
|
|
71
78
|
kernel.restore()
|
|
72
79
|
parent.restore()
|
|
73
80
|
}
|
|
74
81
|
})
|
|
75
82
|
|
|
76
|
-
it('
|
|
77
|
-
const parent =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
it('clicks "Check now", issues @<id>::check, and reflects the refreshed status', async () => {
|
|
84
|
+
const parent = handshake('page-1')
|
|
85
|
+
// First ::get is down; ::check rolls up server-side; the post-check ::get is up.
|
|
86
|
+
let checked = false
|
|
87
|
+
const kernel = installFakeKernel((body) => {
|
|
88
|
+
if (body.method === '@page-1::check') {
|
|
89
|
+
checked = true
|
|
90
|
+
return ok(null)
|
|
91
|
+
}
|
|
92
|
+
return ok(page('page-1', checked ? 'up' : 'down'))
|
|
84
93
|
})
|
|
85
|
-
const kernel = installFakeKernel(() => ({
|
|
86
|
-
error: { code: 3002, message: 'Path not found' },
|
|
87
|
-
}))
|
|
88
94
|
|
|
89
95
|
try {
|
|
90
96
|
render(<App />)
|
|
91
97
|
await flush()
|
|
92
98
|
|
|
93
|
-
expect(screen.getByText(
|
|
94
|
-
// The "<code>: <message>" surfaces from kernelCall.
|
|
95
|
-
expect(screen.getByText(/3002: Path not found/)).toBeTruthy()
|
|
99
|
+
expect(screen.getByText('DOWN')).toBeTruthy()
|
|
96
100
|
expect(kernel.calls).toHaveLength(1)
|
|
101
|
+
expect(kernel.calls[0]!.body.method).toBe('@page-1::get')
|
|
102
|
+
|
|
103
|
+
await act(async () => {
|
|
104
|
+
fireEvent.click(screen.getByText('Check now'))
|
|
105
|
+
})
|
|
106
|
+
await flush()
|
|
107
|
+
|
|
108
|
+
// A ::check call was issued, then the node was re-fetched.
|
|
109
|
+
expect(kernel.calls.map((c) => c.body.method)).toEqual([
|
|
110
|
+
'@page-1::get',
|
|
111
|
+
'@page-1::check',
|
|
112
|
+
'@page-1::get',
|
|
113
|
+
])
|
|
114
|
+
expect(screen.getByText('UP')).toBeTruthy()
|
|
97
115
|
} finally {
|
|
98
116
|
kernel.restore()
|
|
99
117
|
parent.restore()
|
|
100
118
|
}
|
|
101
119
|
})
|
|
102
120
|
|
|
103
|
-
it('
|
|
104
|
-
const parent =
|
|
105
|
-
windowId: 'win-3',
|
|
106
|
-
kernelUrl: KERNEL_URL,
|
|
107
|
-
functionId: 'ui-note',
|
|
108
|
-
delegationToken: 'tok-A',
|
|
109
|
-
tokenExpiresAt: Date.now() + 3_600_000,
|
|
110
|
-
targetNodeId: 'note-1',
|
|
111
|
-
})
|
|
121
|
+
it('surfaces a check failure without losing the loaded record', async () => {
|
|
122
|
+
const parent = handshake('page-1')
|
|
112
123
|
const kernel = installFakeKernel((body) =>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
: ok(noteNode({ id: 'note-1', name: 'First note', body: 'Original.' })),
|
|
124
|
+
body.method === '@page-1::check'
|
|
125
|
+
? { error: { code: 2004, message: 'Permission denied' } }
|
|
126
|
+
: ok(page('page-1', 'up')),
|
|
117
127
|
)
|
|
118
128
|
|
|
119
129
|
try {
|
|
120
130
|
render(<App />)
|
|
121
131
|
await flush()
|
|
122
|
-
expect(screen.getByText('First note')).toBeTruthy()
|
|
123
|
-
expect(kernel.calls).toHaveLength(1)
|
|
124
132
|
|
|
125
|
-
|
|
126
|
-
|
|
133
|
+
await act(async () => {
|
|
134
|
+
fireEvent.click(screen.getByText('Check now'))
|
|
135
|
+
})
|
|
127
136
|
await flush()
|
|
128
137
|
|
|
129
|
-
|
|
130
|
-
|
|
138
|
+
// The proper client maps the error code to a typed error, so the operator
|
|
139
|
+
// sees the clean server message (no `<code>:` prefix).
|
|
140
|
+
expect(screen.getByText(/Check failed:/)).toBeTruthy()
|
|
141
|
+
expect(screen.getByText(/Permission denied/)).toBeTruthy()
|
|
142
|
+
// The record is still shown.
|
|
143
|
+
expect(screen.getByText('Public status')).toBeTruthy()
|
|
144
|
+
} finally {
|
|
145
|
+
kernel.restore()
|
|
146
|
+
parent.restore()
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('renders the kernel error on the node-load error path', async () => {
|
|
151
|
+
const parent = handshake('missing-1')
|
|
152
|
+
const kernel = installFakeKernel(() => ({ error: { code: 3002, message: 'Path not found' } }))
|
|
131
153
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
154
|
+
try {
|
|
155
|
+
render(<App />)
|
|
156
|
+
await flush()
|
|
157
|
+
|
|
158
|
+
expect(screen.getByText(/Failed to load/)).toBeTruthy()
|
|
159
|
+
expect(screen.getByText(/Path not found/)).toBeTruthy()
|
|
135
160
|
} finally {
|
|
136
161
|
kernel.restore()
|
|
137
162
|
parent.restore()
|
|
138
163
|
}
|
|
139
164
|
})
|
|
140
165
|
|
|
141
|
-
it('
|
|
142
|
-
const parent =
|
|
143
|
-
windowId: 'win-4',
|
|
144
|
-
kernelUrl: KERNEL_URL,
|
|
145
|
-
functionId: 'ui-note',
|
|
146
|
-
delegationToken: 'tok-A',
|
|
147
|
-
tokenExpiresAt: Date.now() + 1_000,
|
|
148
|
-
targetNodeId: 'note-1',
|
|
149
|
-
})
|
|
166
|
+
it('re-fetches on a setTarget hot-swap', async () => {
|
|
167
|
+
const parent = handshake('page-1')
|
|
150
168
|
const kernel = installFakeKernel((body) =>
|
|
151
|
-
|
|
169
|
+
body.method === '@page-2::get'
|
|
170
|
+
? ok(page('page-2', 'down', 'Internal status'))
|
|
171
|
+
: ok(page('page-1', 'up', 'Public status')),
|
|
152
172
|
)
|
|
153
173
|
|
|
154
174
|
try {
|
|
155
175
|
render(<App />)
|
|
156
176
|
await flush()
|
|
157
|
-
expect(
|
|
158
|
-
|
|
159
|
-
expect(kernel.calls[0]!.headers['authorization']).toBe('tok-A')
|
|
177
|
+
expect(screen.getByText('Public status')).toBeTruthy()
|
|
178
|
+
expect(screen.getByText('UP')).toBeTruthy()
|
|
160
179
|
|
|
161
|
-
// Parent pushes a
|
|
162
|
-
parent.
|
|
163
|
-
parent.setTarget('note-9')
|
|
180
|
+
// Parent pushes a new target node.
|
|
181
|
+
parent.setTarget('page-2')
|
|
164
182
|
await flush()
|
|
165
183
|
|
|
166
|
-
|
|
167
|
-
expect(
|
|
168
|
-
|
|
169
|
-
|
|
184
|
+
expect(screen.getByText('Internal status')).toBeTruthy()
|
|
185
|
+
expect(screen.getByText('DOWN')).toBeTruthy()
|
|
186
|
+
} finally {
|
|
187
|
+
kernel.restore()
|
|
188
|
+
parent.restore()
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('explains a missing target instead of a blank screen', async () => {
|
|
193
|
+
const parent = handshake(undefined)
|
|
194
|
+
const kernel = installFakeKernel(() => ok(null))
|
|
195
|
+
try {
|
|
196
|
+
render(<App />)
|
|
197
|
+
await flush()
|
|
198
|
+
|
|
199
|
+
expect(screen.getByText(/No target node/)).toBeTruthy()
|
|
200
|
+
expect(kernel.calls).toHaveLength(0)
|
|
170
201
|
} finally {
|
|
171
202
|
kernel.restore()
|
|
172
203
|
parent.restore()
|
|
@@ -174,15 +205,29 @@ describe('ui-note view — handshake + real node render', () => {
|
|
|
174
205
|
})
|
|
175
206
|
|
|
176
207
|
it('falls back to a standalone preview with no parent', async () => {
|
|
177
|
-
// No fake parent
|
|
178
|
-
// window.parent === window in this realm.
|
|
208
|
+
// No fake parent → the shell sees window.parent === window and stands alone.
|
|
179
209
|
const kernel = installFakeKernel(() => ok(null))
|
|
180
210
|
try {
|
|
181
211
|
render(<App />)
|
|
182
212
|
await flush()
|
|
183
213
|
|
|
184
214
|
expect(screen.getByText(/No parent shell/)).toBeTruthy()
|
|
185
|
-
|
|
215
|
+
expect(kernel.calls).toHaveLength(0)
|
|
216
|
+
} finally {
|
|
217
|
+
kernel.restore()
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
describe('router fallback', () => {
|
|
223
|
+
it('renders "No view registered" for an unregistered path', async () => {
|
|
224
|
+
mountAt('/ui/nope')
|
|
225
|
+
const kernel = installFakeKernel(() => ok(null))
|
|
226
|
+
try {
|
|
227
|
+
render(<App />)
|
|
228
|
+
await flush()
|
|
229
|
+
|
|
230
|
+
expect(screen.getByText(/No view registered for \/ui\/nope/)).toBeTruthy()
|
|
186
231
|
expect(kernel.calls).toHaveLength(0)
|
|
187
232
|
} finally {
|
|
188
233
|
kernel.restore()
|