Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/client/machine-listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,14 @@ export function startMachineListener(
? issueGuiSession(req, config, managementAuth, { trustedTailscaleIngress: false })
: null;
if (url.pathname === "/opencodex-session" && session) return serveSessionBootstrap(session);
const gui = serveGuiFile(url.pathname, undefined, session ?? undefined);
// State the role, exactly as the standalone/hub server does (src/server/index.ts).
// The GUI decides whether a machine plane exists from this tag alone
// (gui/src/api-targets.ts `isConnectedRuntime`): without it `discoverApiTargets`
// returns standalone targets and never queries /api/machine/status, so a connected
// client renders as a plain install — no hub usage scope, no "this machine" panel,
// no connected-client list. This listener only ever serves a connected client, so
// the role is a constant here rather than a config read.
const gui = serveGuiFile(url.pathname, undefined, session ?? undefined, "client");
if (gui) return gui;
if (url.pathname === "/") {
return Response.json({
Expand Down
41 changes: 40 additions & 1 deletion tests/client-machine-listener.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { Server } from "bun";
import { startMachineListener } from "../src/client/machine-listener";
import { serveGuiFile } from "../src/server/gui-static";
import type { OcxClientConnectionConfig } from "../src/types";
import type { ManagementAuthState } from "../src/server/management-auth";

Expand Down Expand Up @@ -155,3 +156,41 @@ describe("client machine listener", () => {
expect(() => startMachineListener(0, { managementAuthState: authState() })).toThrow(/requires connected client state/);
});
});

describe("the served document states the client role", () => {
// The GUI decides whether a machine plane exists from this tag alone
// (gui/src/api-targets.ts `isConnectedRuntime` / `discoverApiTargets`). A missing tag is
// not cosmetic: discovery returns standalone targets immediately and never queries
// /api/machine/status, so a connected client renders as a plain install — no hub usage
// scope, no "this machine" panel, no connected-client list.
//
// Asserted against `serveGuiFile` directly rather than over HTTP, because the listener
// falls through to a JSON payload when `gui/dist` is absent, and a checkout without a
// GUI build would make an HTTP-level assertion pass vacuously.
test("the client dashboard document carries the role tag", () => {
const dist = mkdtempSync(join(tmpdir(), "ocx-gui-dist-"));
try {
writeFileSync(join(dist, "index.html"), "<!doctype html><html><head></head><body></body></html>");
const response = serveGuiFile("/", dist, undefined, "client");
expect(response).not.toBeNull();
return response!.text().then(html => {
expect(meta(html, "opencodex-runtime-role")).toBe("client");
});
} finally {
rmSync(dist, { recursive: true, force: true });
}
});

test("the listener asks for the client role rather than leaving it undefined", () => {
// Source-level, deliberately: the call is what carries the role, and the HTTP path
// cannot show it in a checkout with no GUI build. Reading the file keeps the
// assertion honest in both cases.
const source = readFileSync(
join(import.meta.dir, "..", "src", "client", "machine-listener.ts"),
"utf8",
);
const call = /serveGuiFile\(([^)]*)\)/.exec(source);
expect(call, "machine-listener no longer calls serveGuiFile").not.toBeNull();
expect(call![1]).toContain('"client"');
});
});
Loading