Skip to content
Developer Preview

The shared harness

The small harness the journey and every recipe are written against: a host wrapper over the SDK’s public surface, the typed accessors that read a notification’s untyped params safely, and the segment runner that turns a sequence of steps into one pass-or-fail report.

Read this once and the recipes below read as ordinary programs. Nothing here is required to use the SDK — it is the scaffolding these examples share, and your own application would have its own.

The code below is the source that runs, not a retelling of it. It is reproduced unchanged except that comments citing the repository it is developed in — issue numbers, internal file paths, specification section numbers — are removed, because they resolve nothing for you. No executable line differs.

/**
* One owned `muse serve` host, plus the notification recorder the streaming
* assertions wait on.
*
* Everything here goes through the shipped `@muse-code/sdk` public surface —
* `spawnMspConnection`, `MspHandshake.initialize`, `Connection.command`,
* `Connection.request`, `SpawnedMspConnection.close`. The journey never
* reaches into an SDK internal, so what it proves is exactly what a consumer
* gets.
*/
import { spawnMspConnection } from "@muse-code/sdk";
import type { SpawnedMspConnection } from "@muse-code/sdk";
/** A JSON-RPC notification the host pushed at us. */
export interface RecordedNotification {
readonly method: string;
readonly params: Record<string, unknown>;
}
export interface HostOptions {
/** Absolute path to the release-built binary. */
readonly museBin: string;
/** Isolated `HOME`. Every host in one journey shares it, so state persists. */
readonly home: string;
/** The workspace a session is started in. */
readonly workspaceRoot: string;
/**
* The clientInfo the handshake announces to the host's session/audit
* attribution. REQUIRED, matching {@link HostSpawnSpec}: this used to
* default to the quickstart's identity, so a new release-host recipe that
* forgot it announced itself as the quickstart and its sessions were
* attributed to a journey it never ran (T-24225-6). Every caller names
* itself.
*/
readonly clientInfo: { readonly name: string; readonly version: string };
}
/**
* The generic spawn shape behind {@link Host.start}. Cookbook recipes use it
* to launch `muse-conformance serve-fixture` (the canned stdio host) with the
* same recorder and bounded waits the quickstart journey uses for `muse serve`.
*/
export interface HostSpawnSpec {
/** The host executable. */
readonly command: string;
readonly args: readonly string[];
readonly cwd?: string;
/** The COMPLETE child environment: nothing else is inherited. */
readonly env: Record<string, string>;
/** The clientInfo the MSP handshake announces. */
readonly clientInfo: { readonly name: string; readonly version: string };
}
/**
* This is how the JOURNEY's own child is launched; it is not reader guidance,
* and README.md says so.
*/
export const SDK_GATE_ENV = "MUSE_EXPERIMENTAL_SDK_ENABLED";
/** The pre-Seam-C placeholder cursor. A real session must never return it. */
export const STUB_VIEW_CURSOR = "pending:seam-c-session-view-fold";
/**
* The isolated child environment every journey-spawned host gets — the
* env_clear equivalent: nothing else is inherited, so a journey can never
* read the developer's credentials, telemetry settings, or real muse state.
* ONE builder, shared by `Host.start` and the release-host recipes that spawn
* through the SDK facade, so a new isolation variable lands everywhere at
* once.
*/
export function isolatedHostEnv(home: string): Record<string, string> {
return {
HOME: home,
PATH: process.env["PATH"] ?? "/usr/bin:/bin",
TBH_CREDENTIAL_BACKEND: "file",
TBH_DISABLE_TELEMETRY: "1",
[SDK_GATE_ENV]: "on",
};
}
export class TimeoutError extends Error {
constructor(what: string, budgetMs: number) {
super(`${what} did not happen within ${String(budgetMs)}ms`);
this.name = "TimeoutError";
}
}
/** A bounded wait. Never leaves a dangling timer behind. */
export async function within<T>(
what: string,
budgetMs: number,
work: Promise<T>,
): Promise<T> {
let timer: NodeJS.Timeout | undefined;
try {
return await Promise.race([
work,
new Promise<never>((_resolve, reject) => {
timer = setTimeout(() => {
reject(new TimeoutError(what, budgetMs));
}, budgetMs);
}),
]);
} finally {
if (timer !== undefined) clearTimeout(timer);
}
}
export class Host {
readonly msp: SpawnedMspConnection;
readonly stderr: string[];
readonly #seen: RecordedNotification[] = [];
readonly #waiters: Array<{
match: (notification: RecordedNotification) => boolean;
settle: (notification: RecordedNotification) => void;
}> = [];
// The SDK keeps appending to the array `start` handed the spawn, so this
// holds the REFERENCE, never a copy: a snapshot taken at handshake time is
// empty exactly when the host later dies and its stderr is the only evidence.
private constructor(msp: SpawnedMspConnection, stderr: string[]) {
this.msp = msp;
this.stderr = stderr;
}
/** Spawn the release binary and complete the MSP handshake. */
static async start(options: HostOptions, budgetMs: number): Promise<Host> {
return await Host.spawn(
{
command: options.museBin,
args: ["serve"],
cwd: options.workspaceRoot,
env: isolatedHostEnv(options.home),
clientInfo: options.clientInfo,
},
budgetMs,
);
}
/** Spawn any MSP host command and complete the handshake. */
static async spawn(spec: HostSpawnSpec, budgetMs: number): Promise<Host> {
const stderr: string[] = [];
const handshake = spawnMspConnection({
command: spec.command,
args: [...spec.args],
...(spec.cwd === undefined ? {} : { cwd: spec.cwd }),
env: spec.env,
onStderr: (chunk) => stderr.push(chunk),
});
let msp: SpawnedMspConnection;
try {
msp = await within(
"the MSP handshake",
budgetMs,
handshake.initialize({ clientInfo: spec.clientInfo }),
);
} catch (error) {
// BOUND this peek. `child.exit` only settles when the child actually
// exits, so a host that accepted the spawn and then went silent would
// swallow the TimeoutError above and hang the caller forever — past the
// journey's own cleanup, leaking the child. A missing classification is
// worth far less than a diagnosable failure.
const exit = await within("the host's exit after a failed handshake", 2_000, handshake.child.exit)
.catch(() => undefined);
throw new Error(
`handshake failed (exit ${exit === undefined ? "still running" : JSON.stringify(exit)}); stderr: ${stderr.join("")}`,
{ cause: error },
);
}
const host = new Host(msp, stderr);
msp.connection.onNotification((notification) => {
host.#record({
method: notification.method,
params: (notification.params ?? {}) as Record<string, unknown>,
});
});
msp.child.exit.catch(() => undefined);
return host;
}
#record(notification: RecordedNotification): void {
this.#seen.push(notification);
for (let index = this.#waiters.length - 1; index >= 0; index -= 1) {
const waiter = this.#waiters[index];
if (waiter !== undefined && waiter.match(notification)) {
this.#waiters.splice(index, 1);
waiter.settle(notification);
}
}
}
/** Every notification recorded so far, oldest first. */
notifications(): readonly RecordedNotification[] {
return [...this.#seen];
}
/**
* Wait for the first notification matching `match`. Already-received
* notifications count, so there is no race between sending a command and
* subscribing to its stream.
*/
async waitFor(
what: string,
budgetMs: number,
match: (notification: RecordedNotification) => boolean,
): Promise<RecordedNotification> {
const already = this.#seen.find(match);
if (already !== undefined) return already;
const arrival = new Promise<RecordedNotification>((resolve) => {
this.#waiters.push({ match, settle: resolve });
});
// Racing the process exit turns "the host died" into that sentence
// instead of an opaque timeout.
const died = this.msp.exited.then((exit) => {
throw new Error(
`the host exited (code ${String(exit.code)}, signal ${String(exit.signal)}) while waiting for ${what}; stderr: ${this.stderr.join("")}`,
);
});
died.catch(() => undefined);
return await within(what, budgetMs, Promise.race([arrival, died]));
}
/** Close stdin and wait for the orderly drain. */
async close(budgetMs: number): Promise<{ code: number | null; signal: string | null }> {
return await within("the host's orderly drain and exit", budgetMs, this.msp.close());
}
/**
* Best-effort teardown for a failure path. Never throws.
*/
async abandon(budgetMs: number): Promise<void> {
try {
await this.close(budgetMs);
} catch (error) {
process.stderr.write(
`warning: the host did not drain (${String(error)}). The SDK exposes no kill path, ` +
`so this child may outlive the journey.\n`,
);
}
}
}
/**
* The owned-host guard every journey needs: a segment that runs after an
* earlier one failed to produce a host says so, instead of dereferencing
* `undefined`. Structural on purpose — each journey has its own context shape
* and only the `host` slot is shared.
*/
export function requireHost(context: { readonly host?: Host }): Host {
if (context.host === undefined) throw new Error("no host: an earlier segment did not finish");
return context.host;
}
/** Narrow an untyped MSP result member to an object, with a useful message. */
export function objectAt(
value: Record<string, unknown>,
key: string,
where: string,
): Record<string, unknown> {
const member = value[key];
if (member === null || typeof member !== "object" || Array.isArray(member)) {
throw new Error(`${where}: "${key}" is not an object (got ${JSON.stringify(member)})`);
}
return member as Record<string, unknown>;
}
/** Narrow an untyped MSP result member to a non-empty string. */
export function stringAt(
value: Record<string, unknown>,
key: string,
where: string,
): string {
const member = value[key];
if (typeof member !== "string" || member.length === 0) {
throw new Error(`${where}: "${key}" is not a non-empty string (got ${JSON.stringify(member)})`);
}
return member;
}
export function arrayAt(
value: Record<string, unknown>,
key: string,
where: string,
): readonly unknown[] {
const member = value[key];
if (!Array.isArray(member)) {
throw new Error(`${where}: "${key}" is not an array (got ${JSON.stringify(member)})`);
}
return member;
}
export function equals(actual: unknown, expected: unknown, what: string): void {
if (actual !== expected) {
throw new Error(`${what}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
}
}
/**
* The shared journey kit, importable as `@muse-code/sdk-cookbook/kit` — the
* package's ONLY export. There is deliberately no root export: nothing
* consumes one (recipes and tests use relative paths, CI runs
* `dist/src/main.js` by path), and a root barrel would both drift as a second
* hand-maintained export list and make every recipe module a load-time
* dependency of the quickstart journey. The kit is just the expect-block
* contract and the owned-host recorder.
*/
export {
classify,
formatReport,
runJourney,
runSegment,
summarize,
} from "./segments.js";
export type {
ExpectBlock,
JourneyReport,
Segment,
SegmentOutcome,
SegmentResult,
} from "./segments.js";
export {
Host,
SDK_GATE_ENV,
STUB_VIEW_CURSOR,
TimeoutError,
arrayAt,
equals,
isolatedHostEnv,
objectAt,
requireHost,
stringAt,
within,
} from "./host.js";
export type { HostOptions, HostSpawnSpec, RecordedNotification } from "./host.js";
/**
* Segment results and the expect-block contract.
*
* The journey runs every segment for real. A segment that today cannot pass
* because of a named open issue carries an `expectBlock`. The block never
* weakens the assertion and never stubs the segment out: the segment still
* runs, still asserts the spec-correct behavior, and its real failure text is
* kept in the report.
*
* The contract has two directions, and the second one is the point:
*
* - assertion fails with the SIGNATURE the named issues cause ->
* `expectBlocked`. The journey stays green and prints the issue numbers.
* - assertion fails with anything else -> `failed`. A block excuses exactly
* the defect it names and nothing else, so an unrelated regression in a
* blocked segment still reds the journey.
* - assertion PASSES while an expect-block is declared -> `unblocked`.
* The journey FAILS. The fix landed, so the segment must be promoted to
* required by deleting its `expectBlock`. A stale block cannot rot
* silently, and the promotion happens the first time CI runs after the
* fix — that is the automatic light-up.
*/
/** What actually happened when a segment ran. */
export type SegmentOutcome =
/** Ran and asserted clean. Required for the journey to be green. */
| "passed"
/** Failed exactly as its named open issues predict. Not fatal. */
| "expectBlocked"
/** Passed while expect-blocked: the block is stale. FATAL. */
| "unblocked"
/** Failed with no expect-block, or in a way no block predicted. FATAL. */
| "failed";
/** Why a segment cannot pass yet, in the issue tracker's own numbers. */
export interface ExpectBlock {
/** Open issue numbers. Empty is never valid. */
readonly issues: readonly number[];
/** One plain sentence: what those issues do to this segment. */
readonly because: string;
/**
* The failure text those issues produce. The block excuses ONLY a failure
* matching this; anything else is a real failure. Without it a block would
* quietly absorb every future regression in its segment.
*/
readonly signature: RegExp;
}
/** One named step of the journey. */
export interface Segment<Context> {
readonly id: string;
/** Plain-words title, used verbatim in the printed report. */
readonly title: string;
/** Present only while a named open issue stops this segment passing. */
readonly expectBlock?: ExpectBlock;
/** Runs the real work and throws on any failed assertion. */
run(context: Context): Promise<void>;
}
export interface SegmentResult {
readonly id: string;
readonly title: string;
readonly outcome: SegmentOutcome;
readonly durationMs: number;
readonly expectBlock: ExpectBlock | undefined;
/** The real failure text, kept whether or not the failure was expected. */
readonly failure: string | undefined;
}
export interface JourneyReport {
readonly segments: readonly SegmentResult[];
/** True only when no segment is `failed` or `unblocked`. */
readonly ok: boolean;
}
function describe(error: unknown): string {
if (error instanceof Error) {
const cause = error.cause === undefined ? "" : `\n caused by: ${String(error.cause)}`;
return `${error.message}${cause}`;
}
return String(error);
}
export function classify(
expectBlock: ExpectBlock | undefined,
failure: string | undefined,
): SegmentOutcome {
if (expectBlock === undefined) return failure === undefined ? "passed" : "failed";
if (failure === undefined) return "unblocked";
return expectBlock.signature.test(failure) ? "expectBlocked" : "failed";
}
/**
* Runs one segment and classifies it. `now` is injected so the deterministic
* unit tests can drive duration without a clock.
*/
export async function runSegment<Context>(
segment: Segment<Context>,
context: Context,
now: () => number = () => performance.now(),
): Promise<SegmentResult> {
const started = now();
let failure: string | undefined;
try {
await segment.run(context);
} catch (error) {
failure = describe(error);
}
return {
id: segment.id,
title: segment.title,
outcome: classify(segment.expectBlock, failure),
durationMs: Math.round(now() - started),
expectBlock: segment.expectBlock,
failure,
};
}
/**
* Run a journey's segments in order, ALWAYS run `teardown`, then summarize.
*
* This is the loop every recipe would otherwise hand-copy, and the copy's
* risky part is the `finally`: the SDK has no kill path, so a recipe that
* forgets it leaks a spawned host child on the first failing segment.
* Teardown is a callback because the kit does not know what a journey's
* context owns.
*/
export async function runJourney<Context>(
segments: ReadonlyArray<Segment<Context>>,
context: Context,
teardown: (context: Context) => Promise<void>,
): Promise<JourneyReport> {
const results: SegmentResult[] = [];
try {
for (const segment of segments) {
results.push(await runSegment(segment, context));
}
} finally {
await teardown(context);
}
return summarize(results);
}
export function summarize(segments: readonly SegmentResult[]): JourneyReport {
return {
segments,
ok: !segments.some(
(segment) => segment.outcome === "failed" || segment.outcome === "unblocked",
),
};
}
const MARK: Record<SegmentOutcome, string> = {
passed: "PASS ",
expectBlocked: "BLOCK",
unblocked: "STALE",
failed: "FAIL ",
};
function issueList(block: ExpectBlock): string {
return block.issues.map((issue) => `#${String(issue)}`).join(", ");
}
/** The human-facing report. Every expect-block states its issue numbers. */
export function formatReport(report: JourneyReport): string {
const lines: string[] = [];
for (const segment of report.segments) {
lines.push(
`${MARK[segment.outcome]} ${segment.id.padEnd(26)} ${String(segment.durationMs).padStart(6)}ms ${segment.title}`,
);
const block = segment.expectBlock;
if (block !== undefined && segment.outcome === "expectBlocked") {
lines.push(` expect-blocked on ${issueList(block)}: ${block.because}`);
lines.push(` observed: ${indent(segment.failure ?? "(no detail)")}`);
}
if (block !== undefined && segment.outcome === "unblocked") {
lines.push(
` EXPECT-BLOCK IS STALE. This segment now passes, so ${issueList(block)} looks fixed.`,
);
lines.push(
` Promote it: delete this segment's expectBlock so the journey requires it from now on.`,
);
}
if (segment.outcome === "failed") {
if (block !== undefined) {
lines.push(
` This failure does NOT match the expect-block signature for ${issueList(block)}` +
` (${String(block.signature)}), so it is a real failure.`,
);
}
lines.push(` ${indent(segment.failure ?? "(no detail)")}`);
}
}
const counts = new Map<SegmentOutcome, number>();
for (const segment of report.segments) {
counts.set(segment.outcome, (counts.get(segment.outcome) ?? 0) + 1);
}
const tally = (["passed", "expectBlocked", "unblocked", "failed"] as const)
.map((outcome) => `${outcome}=${String(counts.get(outcome) ?? 0)}`)
.join(" ");
lines.push(`journey ${report.ok ? "OK" : "NOT OK"} (${tally})`);
return lines.join("\n");
}
function indent(text: string): string {
return text.split("\n").join("\n ");
}