Files
modelgrid/test/ui-server.smoke.ts
jkunz 3b2a16b151
CI / Type Check & Lint (push) Successful in 8s
CI / Build Test (Current Platform) (push) Successful in 9s
CI / Build All Platforms (push) Successful in 39s
feat(ui): add browser console served by the daemon
Introduce a minimal operations console reachable on a dedicated UI port
(default 8081), kept separate from the OpenAI-compatible API port.

- ts_web/ holds the SPA shell (index.html, app.css, vanilla app.js) with
  sidebar navigation for all views from readme.ui.md and a working
  Overview page backed by a new /_ui/overview JSON endpoint.
- scripts/bundle-ui.ts walks ts_web/ and emits ts_bundled/bundle.ts, a
  single generated module exporting every asset as base64. Mirrors the
  @stack.gallery/registry pattern so deno compile binaries embed the
  entire UI with no external filesystem dependency at runtime.
- ts/ui/server.ts (UiServer) serves assets from either the bundled map
  (default, prod) or directly from ts_web/ on disk (dev). The source is
  chosen per-config and can be overridden by UI_ASSET_SOURCE=disk|bundle.
  SPA fallback routes unknown extensionless paths to index.html.
- IModelGridConfig.ui block with enabled/port/host/assetSource defaults;
  config init writes the block, the normalizer fills in defaults on
  load, and the daemon starts/stops the UI server alongside the API.
- deno.json gains a bundle:ui task; compile:all now depends on it so
  released binaries always contain an up-to-date bundle. dev task sets
  UI_ASSET_SOURCE=disk for hot edits.
- ts_bundled/ is gitignored (generated on build).
- test/ui-server.smoke.ts exercises bundle and disk modes end to end
  (index, app.js, SPA fallback, /_ui/overview, 404).
2026-04-21 10:01:44 +00:00

68 lines
2.4 KiB
TypeScript

// Smoke test for the UI server: bundle mode serves /index.html,
// disk mode serves /app.js, /_ui/overview returns structured JSON.
// Run with: deno run --allow-all test/ui-server.smoke.ts
import { UiServer } from '../ts/ui/server.ts';
import { ContainerManager } from '../ts/containers/container-manager.ts';
import { ClusterManager } from '../ts/cluster/cluster-manager.ts';
async function probe(source: 'bundle' | 'disk', port: number): Promise<void> {
const cm = new ContainerManager();
const cluster = new ClusterManager();
cluster.configure({
enabled: false,
nodeName: 'test-node',
role: 'standalone',
bindHost: '127.0.0.1',
gossipPort: 7946,
heartbeatIntervalMs: 5000,
seedNodes: [],
});
const server = new UiServer(
{ enabled: true, port, host: '127.0.0.1', assetSource: source },
cm,
cluster,
);
await server.start();
try {
const index = await fetch(`http://127.0.0.1:${port}/`);
const indexBody = await index.text();
if (!index.ok || !indexBody.includes('ModelGrid')) {
throw new Error(`[${source}] index.html missing expected content (status=${index.status})`);
}
const app = await fetch(`http://127.0.0.1:${port}/app.js`);
const appBody = await app.text();
if (!app.ok || !appBody.includes('ModelGrid UI')) {
throw new Error(`[${source}] app.js missing expected content (status=${app.status})`);
}
const spa = await fetch(`http://127.0.0.1:${port}/cluster/nodes`);
const spaBody = await spa.text();
if (!spa.ok || !spaBody.includes('ModelGrid')) {
throw new Error(`[${source}] SPA fallback did not return index.html (status=${spa.status})`);
}
const overview = await fetch(`http://127.0.0.1:${port}/_ui/overview`);
const data = await overview.json();
if (!overview.ok || data.node?.name !== 'test-node' || !data.health?.status) {
throw new Error(`[${source}] /_ui/overview unexpected: ${JSON.stringify(data)}`);
}
const missing = await fetch(`http://127.0.0.1:${port}/nope.png`);
if (missing.status !== 404) {
throw new Error(`[${source}] expected 404 for missing asset, got ${missing.status}`);
}
console.log(`ok: ${source} mode — index, app.js, SPA fallback, /_ui/overview, 404`);
} finally {
await server.stop();
}
}
await probe('bundle', 18081);
await probe('disk', 18082);
console.log('UI server smoke test passed');