Files
paseo/packages/workspace-runtime-contract/test/contract.test.mjs

588 lines
22 KiB
JavaScript

import { execFile, spawn } from "node:child_process";
import { mkdir, mkdtemp, readFile, rm, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import test from "node:test";
import assert from "node:assert/strict";
test("the public contract contains no host or provider authority fields", async () => {
const source = await readFile(new URL("../src/index.ts", import.meta.url), "utf8");
assert.doesNotMatch(source, /host-directory|hostVisiblePath|provider-probe|provider:/u);
});
import { promisify } from "node:util";
import {
CommandRuntimeControlSchema,
CommandRuntimeDescribeResponseSchema,
CommandRuntimeLifecycleRequestSchema,
CommandRuntimeLifecycleResponseSchema,
CommandRuntimeProcessEventSchema,
CommandRuntimeProjectSourceSchema,
CommandRuntimePlacementIntentSchema,
CommandRuntimeCreateInputSchema,
CommandRuntimeInspectionSchema,
CommandRuntimePlacementSchema,
CommandRuntimeProcessPurposeSchema,
CommandRuntimeSpawnEnvelopeSchema,
CommandRuntimeStdioSchema,
CommandRuntimeStateSchema,
createCommandRuntimeMessageDecoder,
createCommandRuntimeProcessEventDecoder,
encodeCommandRuntimeMessage,
} from "../dist/index.js";
const run = promisify(execFile);
const npmCommand = process.platform === "win32" ? process.execPath : "npm";
const npmPrefixArgs =
process.platform === "win32" ? [requireEnvironmentVariable("npm_execpath")] : [];
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const examples = JSON.parse(await readFile(path.join(packageRoot, "examples/v1.json"), "utf8"));
function requireEnvironmentVariable(name) {
const value = process.env[name];
if (!value) throw new Error(`${name} is required to invoke npm on Windows`);
return value;
}
function runNpm(args, options) {
return run(npmCommand, [...npmPrefixArgs, ...args], options);
}
test("documented lifecycle, fd3, control, and fd4 examples are exact newline-terminated bytes", () => {
for (const [name, schema] of [
["describeResponse", CommandRuntimeDescribeResponseSchema],
["lifecycleRequest", CommandRuntimeLifecycleRequestSchema],
["lifecycleResponse", CommandRuntimeLifecycleResponseSchema],
["pipeSpawnControl", CommandRuntimeControlSchema],
["ptySpawnControl", CommandRuntimeControlSchema],
["resizeControl", CommandRuntimeControlSchema],
["signalControl", CommandRuntimeControlSchema],
["startedEvent", CommandRuntimeProcessEventSchema],
["eofEvent", CommandRuntimeProcessEventSchema],
["resizedEvent", CommandRuntimeProcessEventSchema],
["exitEvent", CommandRuntimeProcessEventSchema],
["errorEvent", CommandRuntimeProcessEventSchema],
]) {
const bytes = encodeCommandRuntimeMessage(schema, examples[name]);
assert.equal(bytes, examples.bytes[name]);
assert.equal(bytes.endsWith("\n"), true);
assert.equal(bytes.endsWith("\n\n"), false);
}
});
test("lifecycle uses one exact stdin request and one exact stdout response", async () => {
assert.equal(examples.lifecycleRequest.input.purpose, "discovery");
assert.deepEqual(examples.lifecycleResponse.state, {
workspaceId: "workspace-01",
lifecycle: "ready",
});
assert.equal(examples.lifecycleResponse.materializedFreshContent, true);
assert.doesNotMatch(examples.bytes.lifecycleResponse, /root|revision|executionDomainId/);
const child = spawn(
process.execPath,
[path.join(packageRoot, "test/pipe-fixture.mjs"), "lifecycle"],
{
stdio: ["pipe", "pipe", "pipe"],
},
);
child.stdin.end(examples.bytes.lifecycleRequest);
const [stdout, stderr, exit] = await Promise.all([
collect(child.stdout),
collect(child.stderr),
exited(child),
]);
assert.deepEqual(exit, { code: 0, signal: null });
assert.equal(stdout.toString(), examples.bytes.lifecycleResponse);
assert.equal(stderr.length, 0);
});
test("pipes use one fd3 spawn then started/eof/exit on fd4 with raw fd0/1/2", async () => {
const child = spawn(process.execPath, [path.join(packageRoot, "test/pipe-fixture.mjs")], {
stdio: ["pipe", "pipe", "pipe", "pipe", "pipe"],
});
child.stdin.end(Buffer.from("raw\u0000stdin\n"));
child.stdio[3].end(examples.bytes.pipeSpawnControl);
const [stdout, stderr, events, exit] = await Promise.all([
collect(child.stdout),
collect(child.stderr),
collect(child.stdio[4]),
exited(child),
]);
assert.deepEqual(exit, { code: 0, signal: null });
assert.deepEqual(stdout, Buffer.from([0x00, 0x6f, 0x75, 0x74, 0xff]));
assert.deepEqual(stderr, Buffer.from([0x65, 0x72, 0x72, 0x00]));
assert.equal(
events.toString(),
examples.bytes.startedEvent + examples.bytes.eofEvent + examples.bytes.exitEvent,
);
assert.deepEqual(decodeEvents(events), [
examples.startedEvent,
examples.eofEvent,
examples.exitEvent,
]);
});
test("PTY keeps fd3 open for resize and signal through EOF and emits a valid fd4 sequence", async () => {
const child = spawn(process.execPath, [path.join(packageRoot, "test/pty-fixture.mjs")], {
stdio: ["pipe", "pipe", "pipe", "pipe", "pipe"],
});
child.stdin.end(Buffer.from([0x74, 0x65, 0x72, 0x6d, 0x00, 0xff]));
child.stdio[3].write(examples.bytes.ptySpawnControl);
child.stdio[3].write(examples.bytes.resizeControl);
child.stdio[3].end(examples.bytes.signalControl);
const [stdout, stderr, events, exit] = await Promise.all([
collect(child.stdout),
collect(child.stderr),
collect(child.stdio[4]),
exited(child),
]);
assert.deepEqual(exit, { code: 0, signal: null });
assert.deepEqual(stdout, Buffer.from([0x74, 0x65, 0x72, 0x6d, 0x00, 0xff]));
assert.deepEqual(stderr, Buffer.from([0x70, 0x74, 0x79, 0x00]));
assert.equal(
events.toString(),
examples.bytes.startedEvent +
examples.bytes.resizedEvent +
examples.bytes.eofEvent +
'{"type":"exit","protocolVersion":1,"code":null,"signal":"SIGTERM"}\n',
);
assert.deepEqual(
decodeEvents(events).map((event) => event.type),
["started", "resized", "eof", "exit"],
);
});
test("actual fd4 rejects malformed and wrong-version frames and detects early wrapper exit", async () => {
for (const [mode, pattern] of [
["malformed", SyntaxError],
["wrong-version", /expected 1/],
]) {
const child = spawn(process.execPath, [path.join(packageRoot, "test/pipe-fixture.mjs"), mode], {
stdio: ["ignore", "ignore", "ignore", "ignore", "pipe"],
});
const [events, exit] = await Promise.all([collect(child.stdio[4]), exited(child)]);
assert.deepEqual(exit, { code: 0, signal: null });
assert.throws(() => decodeEvents(events), pattern);
}
const early = spawn(
process.execPath,
[path.join(packageRoot, "test/pipe-fixture.mjs"), "early-exit"],
{ stdio: ["ignore", "ignore", "ignore", "ignore", "pipe"] },
);
const [events, exit] = await Promise.all([collect(early.stdio[4]), exited(early)]);
assert.equal(events.length, 0);
assert.deepEqual(exit, { code: 47, signal: null });
});
test("framing accepts partial bytes and rejects malformed, early EOF, and wrong versions", () => {
const partial = createCommandRuntimeMessageDecoder(CommandRuntimeProcessEventSchema);
assert.deepEqual(partial.push(examples.bytes.exitEvent.slice(0, 11)), []);
assert.deepEqual(partial.push(examples.bytes.exitEvent.slice(11)), [examples.exitEvent]);
partial.finish();
const malformed = createCommandRuntimeMessageDecoder(CommandRuntimeProcessEventSchema);
assert.throws(() => malformed.push("{nope}\n"), SyntaxError);
const early = createCommandRuntimeMessageDecoder(CommandRuntimeProcessEventSchema);
early.push(examples.bytes.exitEvent.slice(0, -1));
assert.throws(() => early.finish(), /ended before newline/);
const wrongVersion = createCommandRuntimeMessageDecoder(CommandRuntimeProcessEventSchema);
assert.throws(
() => wrongVersion.push('{"type":"exit","protocolVersion":2,"code":0,"signal":null}\n'),
/expected 1/,
);
});
test("process event conversations enforce the complete mode-specific lifecycle", () => {
for (const [mode, events] of [
["pipes", [examples.startedEvent, examples.eofEvent, examples.exitEvent]],
["pty", [examples.startedEvent, examples.resizedEvent, examples.eofEvent, examples.exitEvent]],
]) {
const decoder = createCommandRuntimeProcessEventDecoder(mode);
assert.deepEqual(
decoder.push(
events
.map((event) => encodeCommandRuntimeMessage(CommandRuntimeProcessEventSchema, event))
.join(""),
),
events,
);
decoder.finish();
}
for (const [name, mode, events, pattern] of [
["exit before eof", "pipes", [examples.startedEvent, examples.exitEvent], /exit before eof/],
["eof before started", "pipes", [examples.eofEvent], /eof before started/],
["exit before started", "pipes", [examples.exitEvent], /exit before started/],
[
"duplicate started",
"pipes",
[examples.startedEvent, examples.startedEvent],
/duplicate started/,
],
[
"duplicate eof",
"pipes",
[examples.startedEvent, examples.eofEvent, examples.eofEvent],
/duplicate eof/,
],
[
"duplicate exit",
"pipes",
[examples.startedEvent, examples.eofEvent, examples.exitEvent, examples.exitEvent],
/duplicate exit/,
],
[
"post-exit lifecycle",
"pipes",
[examples.startedEvent, examples.eofEvent, examples.exitEvent, examples.eofEvent],
/event after exit/,
],
[
"wrong-mode resize",
"pipes",
[examples.startedEvent, examples.resizedEvent],
/pipes.*resized/,
],
["PTY resize before started", "pty", [examples.resizedEvent], /resized before started/],
[
"PTY resize after eof",
"pty",
[examples.startedEvent, examples.eofEvent, examples.resizedEvent],
/resized after eof/,
],
[
"PTY post-exit acknowledgement",
"pty",
[examples.startedEvent, examples.eofEvent, examples.exitEvent, examples.resizedEvent],
/event after exit/,
],
]) {
const decoder = createCommandRuntimeProcessEventDecoder(mode);
assert.throws(
() =>
decoder.push(
events
.map((event) => encodeCommandRuntimeMessage(CommandRuntimeProcessEventSchema, event))
.join(""),
),
pattern,
name,
);
}
});
test("every public v1 object rejects unknown authority at every nesting level", () => {
const createInput = examples.lifecycleRequest.input;
const gitSource = {
kind: "git",
url: "https://example.invalid/repository.git",
revision: "main",
subdirectory: "project",
};
const resolvedPlacement = {
kind: "resolved-worktree",
worktreeSlug: "change-7",
source: {
kind: "checkout-change-request",
headRef: "feature",
baseRefName: "main",
checkoutRefs: [{ remoteName: "origin", remoteRef: "refs/heads/feature" }],
},
};
const readyInspection = {
status: "ready",
state: examples.lifecycleResponse.state,
placement: examples.lifecycleResponse.placement,
};
const pausedInspection = {
status: "paused",
state: { ...examples.lifecycleResponse.state, lifecycle: "paused" },
placement: examples.lifecycleResponse.placement,
};
const strictCases = [
["describe", CommandRuntimeDescribeResponseSchema, examples.describeResponse, []],
["create request", CommandRuntimeLifecycleRequestSchema, examples.lifecycleRequest, []],
[
"create request input",
CommandRuntimeLifecycleRequestSchema,
examples.lifecycleRequest,
["input"],
],
[
"inspect request",
CommandRuntimeLifecycleRequestSchema,
{ protocolVersion: 1, options: {} },
[],
],
[
"pause request",
CommandRuntimeLifecycleRequestSchema,
{ protocolVersion: 1, options: {} },
[],
],
[
"resume request",
CommandRuntimeLifecycleRequestSchema,
{ protocolVersion: 1, options: {} },
[],
],
[
"destroy request",
CommandRuntimeLifecycleRequestSchema,
{ protocolVersion: 1, options: {} },
[],
],
["create input", CommandRuntimeCreateInputSchema, createInput, []],
["create project", CommandRuntimeCreateInputSchema, createInput, ["project"]],
["create source", CommandRuntimeCreateInputSchema, createInput, ["project", "source"]],
["create placement", CommandRuntimeCreateInputSchema, createInput, ["placement"]],
["host source", CommandRuntimeProjectSourceSchema, createInput.project.source, []],
["git source", CommandRuntimeProjectSourceSchema, gitSource, []],
["existing placement", CommandRuntimePlacementIntentSchema, createInput.placement, []],
[
"branch placement",
CommandRuntimePlacementIntentSchema,
{ kind: "branch", branchName: "feature", baseRef: "main" },
[],
],
[
"checkout placement",
CommandRuntimePlacementIntentSchema,
{ kind: "checkout", ref: "feature" },
[],
],
["resolved placement", CommandRuntimePlacementIntentSchema, resolvedPlacement, []],
["resolved source", CommandRuntimePlacementIntentSchema, resolvedPlacement, ["source"]],
[
"resolved checkout ref",
CommandRuntimePlacementIntentSchema,
resolvedPlacement,
["source", "checkoutRefs", 0],
],
["state", CommandRuntimeStateSchema, examples.lifecycleResponse.state, []],
["placement", CommandRuntimePlacementSchema, examples.lifecycleResponse.placement, []],
[
"create state response",
CommandRuntimeLifecycleResponseSchema,
examples.lifecycleResponse,
[],
],
[
"resume state response",
CommandRuntimeLifecycleResponseSchema,
examples.lifecycleResponse,
[],
],
[
"state response state",
CommandRuntimeLifecycleResponseSchema,
examples.lifecycleResponse,
["state"],
],
[
"state response placement",
CommandRuntimeLifecycleResponseSchema,
examples.lifecycleResponse,
["placement"],
],
["ready inspection", CommandRuntimeInspectionSchema, readyInspection, []],
["ready inspection state", CommandRuntimeInspectionSchema, readyInspection, ["state"]],
["ready inspection placement", CommandRuntimeInspectionSchema, readyInspection, ["placement"]],
["paused inspection", CommandRuntimeInspectionSchema, pausedInspection, []],
["paused inspection state", CommandRuntimeInspectionSchema, pausedInspection, ["state"]],
[
"paused inspection placement",
CommandRuntimeInspectionSchema,
pausedInspection,
["placement"],
],
["missing inspection", CommandRuntimeInspectionSchema, { status: "missing" }, []],
[
"error inspection",
CommandRuntimeInspectionSchema,
{ status: "error", message: "failure" },
[],
],
[
"inspection response",
CommandRuntimeLifecycleResponseSchema,
{ protocolVersion: 1, type: "inspection", inspection: { status: "missing" } },
[],
],
[
"ready inspection response state",
CommandRuntimeLifecycleResponseSchema,
{ protocolVersion: 1, type: "inspection", inspection: readyInspection },
["inspection", "state"],
],
[
"ready inspection response placement",
CommandRuntimeLifecycleResponseSchema,
{ protocolVersion: 1, type: "inspection", inspection: readyInspection },
["inspection", "placement"],
],
[
"nested inspection",
CommandRuntimeLifecycleResponseSchema,
{ protocolVersion: 1, type: "inspection", inspection: { status: "missing" } },
["inspection"],
],
[
"pause ok response",
CommandRuntimeLifecycleResponseSchema,
{ protocolVersion: 1, type: "ok" },
[],
],
[
"destroy ok response",
CommandRuntimeLifecycleResponseSchema,
{ protocolVersion: 1, type: "ok" },
[],
],
["agent purpose", CommandRuntimeProcessPurposeSchema, { kind: "agent" }, []],
["terminal purpose", CommandRuntimeProcessPurposeSchema, { kind: "terminal" }, []],
["Git purpose", CommandRuntimeProcessPurposeSchema, { kind: "git" }, []],
["discovery purpose", CommandRuntimeProcessPurposeSchema, { kind: "discovery" }, []],
["helper purpose", CommandRuntimeProcessPurposeSchema, { kind: "workspace-helper" }, []],
["script purpose", CommandRuntimeProcessPurposeSchema, examples.pipeSpawnControl.purpose, []],
["setup purpose", CommandRuntimeProcessPurposeSchema, { kind: "setup" }, []],
["archive purpose", CommandRuntimeProcessPurposeSchema, { kind: "archive" }, []],
["pipes stdio", CommandRuntimeStdioSchema, { kind: "pipes" }, []],
["PTY stdio", CommandRuntimeStdioSchema, examples.ptySpawnControl.stdio, []],
["spawn", CommandRuntimeSpawnEnvelopeSchema, examples.pipeSpawnControl, []],
["spawn purpose", CommandRuntimeSpawnEnvelopeSchema, examples.pipeSpawnControl, ["purpose"]],
["spawn stdio", CommandRuntimeSpawnEnvelopeSchema, examples.pipeSpawnControl, ["stdio"]],
["resize control", CommandRuntimeControlSchema, examples.resizeControl, []],
["signal control", CommandRuntimeControlSchema, examples.signalControl, []],
["started event", CommandRuntimeProcessEventSchema, examples.startedEvent, []],
["eof event", CommandRuntimeProcessEventSchema, examples.eofEvent, []],
["resized event", CommandRuntimeProcessEventSchema, examples.resizedEvent, []],
["exit event", CommandRuntimeProcessEventSchema, examples.exitEvent, []],
["error event", CommandRuntimeProcessEventSchema, examples.errorEvent, []],
];
for (const [name, schema, value, nestingPath] of strictCases) {
for (const field of ["root", "unknownV1Field"]) {
assert.throws(
() => schema.parse(inject(value, nestingPath, field, "/private")),
/unrecognized key/i,
`${name} accepted ${field}`,
);
}
}
});
test("v1 extension maps stay explicit and future versions fail by protocol version", () => {
assert.deepEqual(
CommandRuntimeLifecycleRequestSchema.parse({
protocolVersion: 1,
runtimeInstanceId: examples.lifecycleRequest.runtimeInstanceId,
options: { root: "runtime-private-option", vendorExtension: { nested: true } },
}).options,
{ root: "runtime-private-option", vendorExtension: { nested: true } },
);
assert.deepEqual(
CommandRuntimeStateSchema.parse({
...examples.lifecycleResponse.state,
lifecycleEnvironment: { root: "environment-value", VENDOR_VALUE: "accepted" },
}).lifecycleEnvironment,
{ root: "environment-value", VENDOR_VALUE: "accepted" },
);
assert.deepEqual(
CommandRuntimeSpawnEnvelopeSchema.parse({
...examples.pipeSpawnControl,
env: { root: "workload-environment-value" },
options: { vendorExtension: true },
}),
{
...examples.pipeSpawnControl,
env: { root: "workload-environment-value" },
options: { vendorExtension: true },
},
);
for (const [schema, value] of [
[CommandRuntimeDescribeResponseSchema, examples.describeResponse],
[CommandRuntimeLifecycleRequestSchema, examples.lifecycleRequest],
[CommandRuntimeLifecycleResponseSchema, examples.lifecycleResponse],
[CommandRuntimeControlSchema, examples.pipeSpawnControl],
[CommandRuntimeProcessEventSchema, examples.startedEvent],
]) {
assert.throws(() => schema.parse({ ...value, protocolVersion: 2 }), /expected 1/);
}
});
test("public state schemas reject physical authority under computed and aliased spellings", () => {
const root = ["ro", "ot"].join("");
const state = { workspaceId: "workspace-01", lifecycle: "ready", [root]: "/private" };
const schemaAlias = CommandRuntimeStateSchema;
assert.throws(() => schemaAlias.parse(state), /unrecognized key/i);
assert.throws(
() =>
CommandRuntimeLifecycleResponseSchema.parse({
...examples.lifecycleResponse,
state: { ...examples.lifecycleResponse.state, [root]: "/private" },
}),
/unrecognized key/i,
);
});
test("every export condition resolves from a packed standalone install", async (t) => {
const root = await mkdtemp(path.join(tmpdir(), "workspace-runtime-contract-pack-"));
t.after(() => rm(root, { recursive: true, force: true }));
const pack = JSON.parse(
(
await runNpm(["pack", "--json", "--ignore-scripts", "--pack-destination", root], {
cwd: packageRoot,
})
).stdout,
);
const archive = path.join(root, pack[0].filename);
const installRoot = path.join(root, "standalone");
await mkdir(installRoot);
await runNpm(["install", "--ignore-scripts", "--no-package-lock", archive], {
cwd: installRoot,
});
const installed = path.join(installRoot, "node_modules/@getpaseo/workspace-runtime-contract");
const manifest = JSON.parse(await readFile(path.join(installed, "package.json"), "utf8"));
for (const target of Object.values(manifest.exports["."])) {
await stat(path.join(installed, target));
}
const imported = await import(pathToFileURL(path.join(installed, manifest.exports["."].default)));
assert.equal(imported.COMMAND_RUNTIME_PROTOCOL_VERSION, 1);
});
async function collect(stream) {
const chunks = [];
for await (const chunk of stream) chunks.push(Buffer.from(chunk));
return Buffer.concat(chunks);
}
function exited(child) {
return new Promise((resolve, reject) => {
child.once("error", reject);
child.once("exit", (code, signal) => resolve({ code, signal }));
});
}
function decodeEvents(bytes) {
const decoder = createCommandRuntimeMessageDecoder(CommandRuntimeProcessEventSchema);
const split = Math.min(13, bytes.length);
const events = [
...decoder.push(bytes.subarray(0, split)),
...decoder.push(bytes.subarray(split)),
];
decoder.finish();
return events;
}
function inject(value, nestingPath, field, injected) {
const copy = structuredClone(value);
let target = copy;
for (const segment of nestingPath) target = target[segment];
target[field] = injected;
return copy;
}