mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 03:39:51 -10:00
merge: OPR.0.3.3.04.2 guided golden path state clarity
This commit is contained in:
commit
c491827bd7
11 files changed
+325
-15
No files matched your search
@@ -0,0 +1,69 @@
|
||||
# Getting started: the guided golden path
|
||||
|
||||
This is the ordered new-operator path from a fresh machine to a running rig with
|
||||
a workspace and a workflow. It uses the **existing** `rig` verbs in order - there
|
||||
is no magic one-command onboarding; each step is a real, inspectable verb.
|
||||
|
||||
> Everything below reports **what is currently true**, never a guarantee that
|
||||
> downstream work will succeed. "Daemon up" does not mean every agent is healthy;
|
||||
> "kernel ready" does not mean every kernel agent is healthy; a workspace root
|
||||
> being *live* does not mean it is the *right* one for your project.
|
||||
|
||||
## The ordered path
|
||||
|
||||
1. **`rig setup`** - install and verify the local runtime (tmux, cmux, Claude
|
||||
Code / Codex, runtime config). Run once per machine. `rig setup` does NOT boot
|
||||
the kernel (see "Kernel framing" below).
|
||||
2. **`rig up <rig-spec>`** - launch a rig. This **auto-starts the daemon** if it
|
||||
is not already running (the four daemon-dependent verbs below do not auto-start
|
||||
it - they tell you to run `rig up` or `rig daemon start`).
|
||||
3. **`rig status`** - see what is currently true: daemon up/down + port, kernel
|
||||
readiness (distinct from daemon health), and your effective workspace root
|
||||
(default vs override).
|
||||
4. **`rig workspace doctor`** - check that your workspace is ready; it reports
|
||||
state + fix-hints. The effective workspace root defaults to
|
||||
`~/.openrig/workspace/` unless you override `workspace.root` (env or config).
|
||||
5. **`rig workflow instantiate <name>`** - start a workflow runtime instance by
|
||||
its **discovered name** (e.g. `conveyor`, `basic-loop`) - no hidden file path
|
||||
needed. `rig workflow specs` shows the seeded built-ins. Instantiating creates a
|
||||
workflow instance and its first **entry qitem**.
|
||||
6. **`rig scope ...`** - browse and manage the durable mission/slice artifacts the
|
||||
workflow coordinates (see the scope <-> workflow bridge below).
|
||||
|
||||
`rig setup` prints this ordered sequence as its next-steps output; `rig status`
|
||||
points back here. This doc is the durable reference.
|
||||
|
||||
## Kernel framing (what `rig setup` does and does not do)
|
||||
|
||||
The kernel rig **auto-boots on daemon-start** - it is daemon-start behavior, NOT
|
||||
a `rig setup` step and NOT something you boot by hand. So:
|
||||
|
||||
- `rig setup` installs/verifies the runtime; it does not start the daemon or the
|
||||
kernel.
|
||||
- Starting the daemon (`rig daemon start`, or implicitly via `rig up`) is what
|
||||
boots the kernel rig in the background.
|
||||
- **Kernel readiness is a distinct signal from daemon health.** The daemon's HTTP
|
||||
health binds early; the kernel can still be booting or a kernel agent can be
|
||||
unhealthy while the daemon is up. `rig status` surfaces kernel readiness
|
||||
separately (via `/api/kernel/status`); `rig daemon start --wait-for-kernel`
|
||||
polls it.
|
||||
|
||||
## The scope <-> workflow bridge
|
||||
|
||||
Two related primitives, often confused by new operators:
|
||||
|
||||
- **`rig scope`** manages **durable, on-disk artifacts** - missions and slices
|
||||
(markdown/YAML files in your workspace). These are the persistent record of
|
||||
*what work exists*.
|
||||
- **`rig workflow`** manages a **runtime instance** - when you
|
||||
`rig workflow instantiate <name>`, the daemon creates a workflow instance plus
|
||||
an **entry qitem** that routes the first step to an owner. This is the live
|
||||
coordination of *who does the next step*.
|
||||
|
||||
How they relate: a scope slice is the durable description of a unit of work; a
|
||||
workflow instance + its qitems are the live machinery that moves that work
|
||||
through owners (hot-potato handoffs). They are not auto-bridged - there is no
|
||||
auto-instantiate-from-scope - you instantiate a workflow by name when you want to
|
||||
run one, and you reference your scope artifacts as the work it coordinates. A
|
||||
typical loop: author/track the unit in `rig scope`, then
|
||||
`rig workflow instantiate <name>` to start the runtime that drives it.
|
||||
@@ -1,7 +1,7 @@
|
||||
import nodePath from "node:path";
|
||||
import { Command } from "commander";
|
||||
import { DaemonClient } from "../client.js";
|
||||
import { getDaemonStatus, getDaemonUrl } from "../daemon-lifecycle.js";
|
||||
import { getDaemonStatus, getDaemonUrl, printDaemonNotRunning } from "../daemon-lifecycle.js";
|
||||
import { realDeps } from "./daemon.js";
|
||||
import type { StatusDeps } from "./status.js";
|
||||
|
||||
@@ -30,7 +30,7 @@ export function bootstrapCommand(depsOverride?: StatusDeps): Command {
|
||||
async function getClient(deps: StatusDeps): Promise<DaemonClient | null> {
|
||||
const status = await getDaemonStatus(deps.lifecycleDeps);
|
||||
if (status.state !== "running" || status.healthy === false) {
|
||||
console.error("Daemon not running");
|
||||
printDaemonNotRunning();
|
||||
return null;
|
||||
}
|
||||
return deps.clientFactory(getDaemonUrl(status));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Command } from "commander";
|
||||
import { DaemonClient } from "../client.js";
|
||||
import { getDaemonStatus, getDaemonUrl } from "../daemon-lifecycle.js";
|
||||
import { getDaemonStatus, getDaemonUrl, printDaemonNotRunning } from "../daemon-lifecycle.js";
|
||||
import { realDeps } from "./daemon.js";
|
||||
import type { StatusDeps } from "./status.js";
|
||||
|
||||
@@ -11,7 +11,7 @@ export function discoverCommand(depsOverride?: StatusDeps): Command {
|
||||
async function getClient(deps: StatusDeps): Promise<DaemonClient | null> {
|
||||
const status = await getDaemonStatus(deps.lifecycleDeps);
|
||||
if (status.state !== "running" || status.healthy === false) {
|
||||
console.error("Daemon not running");
|
||||
printDaemonNotRunning();
|
||||
return null;
|
||||
}
|
||||
return deps.clientFactory(getDaemonUrl(status));
|
||||
|
||||
@@ -599,6 +599,23 @@ function buildDefaultDoctorDeps(setupDeps: SetupDeps): DoctorDeps {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* OPR.0.3.3.04.2 (AC-1): the ONE canonical ordered golden path over EXISTING
|
||||
* verbs - no magic mega-command, no hidden state. `rig setup` prints this as its
|
||||
* next-steps; `rig status`/`rig doctor` only HINT back to it; the durable
|
||||
* reference is docs/reference/getting-started.md. Returns the lines to print.
|
||||
*/
|
||||
export function goldenPathNextSteps(): string[] {
|
||||
return [
|
||||
"Next steps (the guided path; full reference: docs/reference/getting-started.md):",
|
||||
" 1. rig up <rig-spec> Launch a rig (auto-starts the daemon; the kernel boots on daemon-start)",
|
||||
" 2. rig status See daemon port, kernel readiness, and your effective workspace root",
|
||||
" 3. rig workspace doctor Check your workspace is ready",
|
||||
" 4. rig workflow instantiate <name> Start a workflow by its discovered name (e.g. conveyor); 'rig workflow specs' shows built-ins",
|
||||
" 5. rig scope ... Browse the durable mission/slice artifacts the workflow coordinates",
|
||||
];
|
||||
}
|
||||
|
||||
export function setupCommand(depsOverride?: SetupDeps): Command {
|
||||
const cmd = new Command("setup").description("Prepare the machine for OpenRig");
|
||||
|
||||
@@ -632,7 +649,16 @@ export function setupCommand(depsOverride?: SetupDeps): Command {
|
||||
if (step.fixHint) console.log(` Fix: ${step.fixHint}`);
|
||||
}
|
||||
|
||||
console.log(`\n${result.ready ? "Setup complete." : "Some steps need attention. Run `rig doctor` for detailed diagnostics."}`);
|
||||
// OPR.0.3.3.04.2 (AC-1): the canonical ordered golden path. `rig setup` is
|
||||
// the primary surface for the new-operator sequence (status/doctor only
|
||||
// HINT back to it; the durable reference is docs/reference/getting-started.md).
|
||||
if (result.ready) {
|
||||
console.log("\nSetup complete.\n");
|
||||
for (const line of goldenPathNextSteps()) console.log(line);
|
||||
} else {
|
||||
console.log("\nSome steps need attention. Run `rig doctor` for detailed diagnostics.");
|
||||
console.log("Once setup is healthy, follow the guided path: docs/reference/getting-started.md");
|
||||
}
|
||||
if (!opts.dryRun && !result.ready) process.exitCode = 1;
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type LifecycleDeps,
|
||||
} from "../daemon-lifecycle.js";
|
||||
import { realDeps } from "./daemon.js";
|
||||
import { ConfigStore } from "../config-store.js";
|
||||
|
||||
export interface StatusDeps {
|
||||
lifecycleDeps: LifecycleDeps;
|
||||
@@ -52,14 +53,39 @@ export function statusCommand(depsOverride?: StatusDeps): Command {
|
||||
|
||||
const client = deps.clientFactory(getDaemonUrl(status));
|
||||
|
||||
// Fetch summary + cmux status
|
||||
const [summaryRes, cmuxRes] = await Promise.all([
|
||||
// Fetch summary + cmux + kernel readiness
|
||||
const [summaryRes, cmuxRes, kernelRes] = await Promise.all([
|
||||
client.get<Array<{ id: string; name: string; nodeCount: number; latestSnapshotAt: string | null; latestSnapshotId: string | null }>>("/api/rigs/summary"),
|
||||
client.get<{ available: boolean }>("/api/adapters/cmux/status").catch(() => null),
|
||||
client.get<{ kernel_state?: string; error?: string }>("/api/kernel/status").catch(() => null),
|
||||
]);
|
||||
|
||||
console.log(`Daemon running on port ${status.port}`);
|
||||
|
||||
// OPR.0.3.3.04.2 (AC-2): kernel readiness is a DISTINCT signal from daemon
|
||||
// health - the kernel rig auto-boots on daemon-start, and a daemon can be up
|
||||
// while the kernel is not yet ready (or a kernel agent is unhealthy). Surface
|
||||
// it as "here's what's currently true," never as a guarantee that downstream
|
||||
// agents are healthy.
|
||||
if (kernelRes && kernelRes.status === 200 && kernelRes.data?.kernel_state) {
|
||||
console.log(`Kernel: ${kernelRes.data.kernel_state} (boots on daemon-start; distinct from daemon health)`);
|
||||
} else if (kernelRes && kernelRes.status === 503) {
|
||||
console.log("Kernel: not tracked (no kernel-boot tracker wired)");
|
||||
} else {
|
||||
console.log("Kernel: unknown (status unavailable)");
|
||||
}
|
||||
|
||||
// OPR.0.3.3.04.2 (AC-2 / gap #7): surface WHICH workspace root is effective
|
||||
// and whether it is the default or an override - the operator never guesses.
|
||||
// This reports what is currently live, NOT that any root is the right one.
|
||||
try {
|
||||
const resolved = new ConfigStore().resolveWithSource("workspace.root");
|
||||
const origin = resolved.source === "default" ? "default" : `override via ${resolved.source}`;
|
||||
console.log(`Workspace root: ${resolved.value} (${origin})`);
|
||||
} catch {
|
||||
// config resolution unavailable - omit rather than guess.
|
||||
}
|
||||
|
||||
if (summaryRes.status !== 200) {
|
||||
console.error(`Failed to fetch rig summary (HTTP ${summaryRes.status})`);
|
||||
process.exitCode = 1;
|
||||
@@ -80,6 +106,13 @@ export function statusCommand(depsOverride?: StatusDeps): Command {
|
||||
// cmux status
|
||||
const cmuxAvailable = cmuxRes?.data?.available ?? false;
|
||||
console.log(`cmux: ${cmuxAvailable ? "available" : "unavailable"}`);
|
||||
|
||||
// OPR.0.3.3.04.2 (AC-1): reinforcing HINT back to the one canonical ordered
|
||||
// path - status does not re-author the sequence (that lives in `rig setup`
|
||||
// next-steps + docs/reference/getting-started.md).
|
||||
if (rigs.length === 0) {
|
||||
console.log("\nNext: launch a rig with `rig up <rig-spec>`. Guided path: `rig setup` output or docs/reference/getting-started.md");
|
||||
}
|
||||
});
|
||||
|
||||
return cmd;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Command } from "commander";
|
||||
import { DaemonClient } from "../client.js";
|
||||
import { getDaemonStatus, getDaemonUrl } from "../daemon-lifecycle.js";
|
||||
import { getDaemonStatus, getDaemonUrl, printDaemonNotRunning } from "../daemon-lifecycle.js";
|
||||
import { realDeps } from "./daemon.js";
|
||||
import type { StatusDeps } from "./status.js";
|
||||
|
||||
@@ -24,8 +24,7 @@ async function withClient<T>(
|
||||
): Promise<T | undefined> {
|
||||
const status = await getDaemonStatus(deps.lifecycleDeps);
|
||||
if (status.state !== "running" || status.healthy === false) {
|
||||
console.error("Daemon not running. Start it with: rig daemon start");
|
||||
process.exitCode = 1;
|
||||
printDaemonNotRunning();
|
||||
return undefined;
|
||||
}
|
||||
const client = deps.clientFactory(getDaemonUrl(status));
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import { Command } from "commander";
|
||||
import { DaemonClient } from "../client.js";
|
||||
import { getDaemonStatus, getDaemonUrl } from "../daemon-lifecycle.js";
|
||||
import { getDaemonStatus, getDaemonUrl, printDaemonNotRunning } from "../daemon-lifecycle.js";
|
||||
import * as path from "node:path";
|
||||
import { realDeps } from "./daemon.js";
|
||||
import type { StatusDeps } from "./status.js";
|
||||
@@ -41,8 +41,7 @@ async function withClient<T>(
|
||||
): Promise<T | undefined> {
|
||||
const status = await getDaemonStatus(deps.lifecycleDeps);
|
||||
if (status.state !== "running" || status.healthy === false) {
|
||||
console.error("Daemon not running. Start it with: rig daemon start");
|
||||
process.exitCode = 1;
|
||||
printDaemonNotRunning();
|
||||
return undefined;
|
||||
}
|
||||
const client = deps.clientFactory(getDaemonUrl(status));
|
||||
|
||||
@@ -26,6 +26,46 @@ export function getDaemonUrl(status: DaemonStatus): string {
|
||||
return `http://${status.host ?? DEFAULT_HOST}:${status.port}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* OPR.0.3.3.04.2 (AC-4): the ONE shared honest daemon-not-running error for the
|
||||
* daemon-dependent journey verbs (bootstrap / discover / workspace / workflow).
|
||||
* Mirrors the repo's 3-part fact / consequence / action convention
|
||||
* (commands/archive.ts, commands/queue.ts) so the daemon-dependency UX is
|
||||
* consistent across the new-operator journey instead of four divergent bare
|
||||
* dead-ends. Bounded to the daemon-not-running path - NOT a general
|
||||
* error-framework (that would be its own slice).
|
||||
*/
|
||||
export interface DaemonNotRunningError {
|
||||
fact: string;
|
||||
consequence: string;
|
||||
action: string;
|
||||
}
|
||||
|
||||
export function daemonNotRunningError(): DaemonNotRunningError {
|
||||
return {
|
||||
fact: "Daemon not running.",
|
||||
consequence: "This command needs a running daemon.",
|
||||
action: "Run 'rig up' (it auto-starts the daemon), or 'rig daemon start'.",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the shared daemon-not-running error and set a failing exit code. Pass
|
||||
* `{ json: true }` for the agent-facing `{ error: { fact, consequence, action } }`
|
||||
* envelope (matching commands/queue.ts JSON output).
|
||||
*/
|
||||
export function printDaemonNotRunning(opts?: { json?: boolean }): void {
|
||||
const err = daemonNotRunningError();
|
||||
if (opts?.json) {
|
||||
console.log(JSON.stringify({ error: err }));
|
||||
} else {
|
||||
console.error(`Error: ${err.fact}`);
|
||||
console.error(` ${err.consequence}`);
|
||||
console.error(` ${err.action}`);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
export interface StartOptions {
|
||||
port?: number;
|
||||
host?: string;
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
resolveDaemonPath,
|
||||
buildDaemonEnv,
|
||||
ensureWorkspaceScaffold,
|
||||
daemonNotRunningError,
|
||||
printDaemonNotRunning,
|
||||
STATE_FILE,
|
||||
LOG_FILE,
|
||||
OPENRIG_DIR,
|
||||
@@ -1197,3 +1199,53 @@ describe("ensureWorkspaceScaffold", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// OPR.0.3.3.04.2 (AC-4): the ONE shared daemon-not-running honest error used by
|
||||
// the daemon-dependent journey verbs (bootstrap/discover/workspace/workflow).
|
||||
describe("daemonNotRunningError / printDaemonNotRunning (AC-4 shared honest error)", () => {
|
||||
it("daemonNotRunningError is the 3-part fact/consequence/action shape pointing to rig up / rig daemon start", () => {
|
||||
const err = daemonNotRunningError();
|
||||
expect(err.fact).toContain("Daemon not running");
|
||||
expect(err.consequence).toContain("needs a running daemon");
|
||||
expect(err.action).toContain("rig up");
|
||||
expect(err.action).toContain("rig daemon start");
|
||||
});
|
||||
|
||||
it("printDaemonNotRunning (human) emits all three parts on stderr and sets exit code 1", () => {
|
||||
const lines: string[] = [];
|
||||
const origErr = console.error;
|
||||
const origExit = process.exitCode;
|
||||
console.error = (...a: unknown[]) => { lines.push(a.join(" ")); };
|
||||
process.exitCode = undefined;
|
||||
try {
|
||||
printDaemonNotRunning();
|
||||
} finally {
|
||||
console.error = origErr;
|
||||
}
|
||||
const exit = process.exitCode;
|
||||
process.exitCode = origExit;
|
||||
const out = lines.join("\n");
|
||||
expect(out).toContain("Daemon not running");
|
||||
expect(out).toContain("needs a running daemon");
|
||||
expect(out).toContain("rig up");
|
||||
expect(exit).toBe(1);
|
||||
});
|
||||
|
||||
it("printDaemonNotRunning({ json: true }) emits the { error: { fact, consequence, action } } envelope", () => {
|
||||
const lines: string[] = [];
|
||||
const origLog = console.log;
|
||||
const origExit = process.exitCode;
|
||||
console.log = (...a: unknown[]) => { lines.push(a.join(" ")); };
|
||||
process.exitCode = undefined;
|
||||
try {
|
||||
printDaemonNotRunning({ json: true });
|
||||
} finally {
|
||||
console.log = origLog;
|
||||
}
|
||||
process.exitCode = origExit;
|
||||
const parsed = JSON.parse(lines.join("")) as { error: { fact: string; consequence: string; action: string } };
|
||||
expect(parsed.error.fact).toContain("Daemon not running");
|
||||
expect(parsed.error.consequence).toContain("needs a running daemon");
|
||||
expect(parsed.error.action).toContain("rig up");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { Command } from "commander";
|
||||
import { setupCommand, runSetup, type SetupDeps, type SetupResult } from "../src/commands/setup.js";
|
||||
import { setupCommand, runSetup, goldenPathNextSteps, type SetupDeps, type SetupResult } from "../src/commands/setup.js";
|
||||
import type { DoctorDeps } from "../src/commands/doctor.js";
|
||||
import { resolveCmuxSettingsPath } from "../src/cmux-config.js";
|
||||
|
||||
@@ -207,6 +207,30 @@ describe("rig setup", () => {
|
||||
expect(verify?.status).toBe("pass");
|
||||
});
|
||||
|
||||
// OPR.0.3.3.04.2 (AC-1): the ONE canonical ordered golden path over EXISTING
|
||||
// verbs (no mega-command), in sequence, with the durable doc reference.
|
||||
it("AC-1: goldenPathNextSteps is the ordered sequence over existing verbs, with the durable doc", () => {
|
||||
const out = goldenPathNextSteps().join("\n");
|
||||
const upIdx = out.indexOf("rig up");
|
||||
const statusIdx = out.indexOf("rig status");
|
||||
const wsIdx = out.indexOf("rig workspace doctor");
|
||||
const wfIdx = out.indexOf("rig workflow instantiate");
|
||||
const scopeIdx = out.indexOf("rig scope");
|
||||
expect(upIdx).toBeGreaterThan(-1);
|
||||
expect(statusIdx).toBeGreaterThan(upIdx);
|
||||
expect(wsIdx).toBeGreaterThan(statusIdx);
|
||||
expect(wfIdx).toBeGreaterThan(wsIdx);
|
||||
expect(scopeIdx).toBeGreaterThan(wfIdx);
|
||||
expect(out).toContain("docs/reference/getting-started.md");
|
||||
// no magic mega-command - the path is existing verbs only
|
||||
expect(out).not.toMatch(/rig (journey|onboarding)\b/);
|
||||
// built-in discovery surface is `rig workflow specs` (lists registered specs,
|
||||
// built-ins tagged "(built-in)"), NOT `rig workflow list` (lists instances) -
|
||||
// the path must point a fresh operator at the right discovery command.
|
||||
expect(out).toContain("rig workflow specs");
|
||||
expect(out).not.toContain("rig workflow list");
|
||||
});
|
||||
|
||||
it("fails setup honestly when tmux is installed but the default control socket is unhealthy", async () => {
|
||||
const deps = makeDeps({
|
||||
exec: (cmd: string) => {
|
||||
|
||||
@@ -43,7 +43,11 @@ function captureLogs(fn: () => Promise<void>): Promise<string[]> {
|
||||
}
|
||||
|
||||
// Echo server for daemon API
|
||||
function createDaemonServer(summaryData: unknown[], cmuxStatus: unknown) {
|
||||
function createDaemonServer(
|
||||
summaryData: unknown[],
|
||||
cmuxStatus: unknown,
|
||||
kernelStatus?: { code: number; body: unknown },
|
||||
) {
|
||||
const server = http.createServer((req, res) => {
|
||||
const url = new URL(req.url!, "http://localhost");
|
||||
|
||||
@@ -57,6 +61,12 @@ function createDaemonServer(summaryData: unknown[], cmuxStatus: unknown) {
|
||||
res.end(JSON.stringify(cmuxStatus));
|
||||
return;
|
||||
}
|
||||
if (url.pathname === "/api/kernel/status") {
|
||||
const k = kernelStatus ?? { code: 200, body: { kernel_state: "ready" } };
|
||||
res.writeHead(k.code, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify(k.body));
|
||||
return;
|
||||
}
|
||||
if (url.pathname === "/healthz") {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ status: "ok" }));
|
||||
@@ -130,6 +140,64 @@ describe("rig status", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// OPR.0.3.3.04.2 (AC-2): state clarity - daemon port, kernel readiness
|
||||
// (distinct from daemon health), effective workspace root (default vs override).
|
||||
describe("AC-2 state clarity (OPR.0.3.3.04.2)", () => {
|
||||
let srv: ReturnType<typeof createDaemonServer>;
|
||||
let port: number;
|
||||
beforeAll(async () => {
|
||||
srv = createDaemonServer([], { available: false }, { code: 200, body: { kernel_state: "ready" } });
|
||||
port = await srv.listen();
|
||||
});
|
||||
afterAll(async () => { await srv.close(); });
|
||||
|
||||
function statusDeps(): StatusDeps {
|
||||
return {
|
||||
lifecycleDeps: mockLifecycleDeps({
|
||||
exists: vi.fn((p: string) => p === STATE_FILE),
|
||||
readFile: vi.fn((p: string) => (p === STATE_FILE ? JSON.stringify(runningState(port)) : null)),
|
||||
fetch: vi.fn(async () => ({ ok: true })),
|
||||
}),
|
||||
clientFactory: (baseUrl) => new DaemonClient(baseUrl),
|
||||
};
|
||||
}
|
||||
|
||||
it("surfaces daemon port + kernel readiness (distinct from daemon health) + effective workspace root with its source", async () => {
|
||||
const prev = process.env.OPENRIG_WORKSPACE_ROOT;
|
||||
delete process.env.OPENRIG_WORKSPACE_ROOT; // not env-overridden in this case
|
||||
try {
|
||||
const program = new Command();
|
||||
program.addCommand(statusCommand(statusDeps()));
|
||||
const logs = await captureLogs(() => program.parseAsync(["node", "rig", "status"]));
|
||||
const output = logs.join("\n");
|
||||
expect(output).toMatch(/Daemon running on port \d+/);
|
||||
expect(output).toContain("Kernel: ready");
|
||||
expect(output).toContain("distinct from daemon health");
|
||||
// the effective root is surfaced WITH whether it's default vs an override
|
||||
// (host-independent: a host may legitimately carry a file-level override).
|
||||
expect(output).toMatch(/Workspace root: .+ \((default|override via (env|file))\)/);
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.OPENRIG_WORKSPACE_ROOT;
|
||||
else process.env.OPENRIG_WORKSPACE_ROOT = prev;
|
||||
}
|
||||
});
|
||||
|
||||
it("marks the workspace root as an override when OPENRIG_WORKSPACE_ROOT is set", async () => {
|
||||
const prev = process.env.OPENRIG_WORKSPACE_ROOT;
|
||||
process.env.OPENRIG_WORKSPACE_ROOT = process.cwd(); // a real existing dir
|
||||
try {
|
||||
const program = new Command();
|
||||
program.addCommand(statusCommand(statusDeps()));
|
||||
const logs = await captureLogs(() => program.parseAsync(["node", "rig", "status"]));
|
||||
const output = logs.join("\n");
|
||||
expect(output).toContain(`Workspace root: ${process.cwd()} (override via env)`);
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.OPENRIG_WORKSPACE_ROOT;
|
||||
else process.env.OPENRIG_WORKSPACE_ROOT = prev;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Test 2: Running daemon with no rigs -> 'No rigs'
|
||||
describe("with running daemon and no rigs", () => {
|
||||
let srv: ReturnType<typeof createDaemonServer>;
|
||||
|
||||
Reference in new issue
Block a user