/*
RFC-0988: Unit tests for --dry-run flag on sternsystem.handover.prepare
and sternsystem.handover.complete. Verifies side-effect-free behavior, preview data,
and error cases.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-0988: initial dry-run unit tests for handover prepare and complete.
*/
import { test, expect, describe, beforeEach, afterEach } from "vitest";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import type { KernelCommandInput, KernelRuntimeContext } from "@warpgogol/werkstatt-engine/kernel";
import { generateKeyPair, toHex, toPem } from "@warpgogol/werkstatt-engine/signing";
import { buildPassportPayload, signPassport, type SignedSitePassport } from "./passport.ts";
import { writePassport } from "./registry-io.ts";
import {
signAuthorization,
type HandoverAuthorizationV1,
type SignedHandoverAuthorization,
} from "./handover.ts";
import { runSternsystemHandoverPrepare } from "./sternsystem-handover-prepare.ts";
import { runSternsystemHandoverComplete } from "./sternsystem-handover-complete.ts";
import { resolveAuthorizationPath } from "./handover.ts";
import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
let tmpDir: string;
beforeEach(() => {
tmpDir = mkdtempSync(join(tmpdir(), "handover-dry-run-"));
});
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
});
function makeInput(flags: Record): KernelCommandInput {
return {
flags: flags as Record,
argv: [],
};
}
function makeContext(root: string): KernelRuntimeContext {
return {
io: createDefaultIO().io,
workspaceRoot: root,
logger: {
section: () => {},
info: () => {},
warn: () => {},
error: () => {},
success: () => {},
},
dryRun: false,
} as unknown as KernelRuntimeContext;
}
function setupSystem(root: string, systemId: string): void {
const cacheDir = join(root, "..", "systems-cache", systemId);
mkdirSync(cacheDir, { recursive: true });
mkdirSync(join(cacheDir, "missions"), { recursive: true });
mkdirSync(join(cacheDir, "bordbuch"), { recursive: true });
writeFileSync(
join(cacheDir, "system-config.yaml"),
[
`schemaVersion: system-config/v1`,
`id: ${systemId}`,
`cosmicStar: Vega`,
`mirrors:`,
` - path: ../systems-cache/${systemId}`,
` storageType: non-bare`,
` - path: ../systems-git/${systemId}.git`,
` storageType: bare`,
`pinnedPlatform: 6.0.0`,
`status: active`,
`registeredAt: "2026-01-01T00:00:00Z"`,
`notes: ""`,
``,
].join("\n"),
);
writeFileSync(
join(cacheDir, "system.pin.json"),
JSON.stringify({
platform: {
version: "6.0.0",
platformSemanticHash:
"sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
},
}) + "\n",
);
}
async function setupPassport(
root: string,
systemId: string,
keyPair: { publicKey: Uint8Array; privateKey: Uint8Array },
identity: string,
): Promise {
const payload = await buildPassportPayload({
systemId,
werkstattRoot: root,
creatorIdentity: identity,
creatorPublicKey: toHex(keyPair.publicKey),
});
const { passportHash, signature } = await signPassport(payload, keyPair.privateKey);
const passport: SignedSitePassport = { payload, passportHash, signature };
await writePassport(root, systemId, passport);
return passport;
}
describe("RFC-0988: handover.prepare --dry-run", () => {
test("returns SignedHandoverAuthorization without writing to disk", async () => {
const systemId = "dry-run-prepare-site";
const root = join(tmpDir, "werkstatt");
mkdirSync(root, { recursive: true });
setupSystem(root, systemId);
const senderKeyPair = await generateKeyPair();
const passport = await setupPassport(root, systemId, senderKeyPair, "human:sender");
const recipientKeyPair = await generateKeyPair();
const recipientPublicKeyHex = toHex(recipientKeyPair.publicKey);
process.env.SIGNING_PRIVATE_KEY = toPem(senderKeyPair.privateKey, "private");
const result = await runSternsystemHandoverPrepare(
makeInput({
id: systemId,
"recipient-identity": "human:recipient",
"recipient-public-key": recipientPublicKeyHex,
"dry-run": true,
}),
makeContext(root),
);
expect(result.data!.dryRun).toBe(true);
expect(result.data!.signedAuthorization).toBeDefined();
expect(result.data!.signedAuthorization!.authorizationHash).toBeDefined();
expect(result.data!.signedAuthorization!.signature).toBeDefined();
expect(result.data!.signedAuthorization!.payload.recipient.identity).toBe("human:recipient");
expect(result.data!.signedAuthorization!.payload.passportHash).toBe(passport.passportHash);
const authPath = resolveAuthorizationPath(root, systemId);
expect(existsSync(authPath)).toBe(false);
delete process.env.SIGNING_PRIVATE_KEY;
});
test("non-dry-run still writes to disk", async () => {
const systemId = "non-dry-run-prepare-site";
const root = join(tmpDir, "werkstatt");
mkdirSync(root, { recursive: true });
setupSystem(root, systemId);
const senderKeyPair = await generateKeyPair();
await setupPassport(root, systemId, senderKeyPair, "human:sender");
const recipientKeyPair = await generateKeyPair();
const recipientPublicKeyHex = toHex(recipientKeyPair.publicKey);
process.env.SIGNING_PRIVATE_KEY = toPem(senderKeyPair.privateKey, "private");
const result = await runSternsystemHandoverPrepare(
makeInput({
id: systemId,
"recipient-identity": "human:recipient",
"recipient-public-key": recipientPublicKeyHex,
}),
makeContext(root),
);
expect(result.data!.dryRun).toBe(false);
expect(result.data!.signedAuthorization).toBeUndefined();
expect(result.data!.authorizationPath).not.toBe("");
const authPath = resolveAuthorizationPath(root, systemId);
expect(existsSync(authPath)).toBe(true);
delete process.env.SIGNING_PRIVATE_KEY;
});
});
describe("RFC-0988: handover.complete --dry-run", () => {
async function setupCompleteScenario(
root: string,
systemId: string,
): Promise<{
signedAuth: SignedHandoverAuthorization;
recipientKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array };
}> {
mkdirSync(root, { recursive: true });
setupSystem(root, systemId);
const senderKeyPair = await generateKeyPair();
const passport = await setupPassport(root, systemId, senderKeyPair, "human:sender");
const recipientKeyPair = await generateKeyPair();
const recipientPublicKeyHex = toHex(recipientKeyPair.publicKey);
const now = new Date();
const expires = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
const authPayload: HandoverAuthorizationV1 = {
schema: "handover-authorization/v1",
systemId,
sender: { identity: "human:sender", publicKey: toHex(senderKeyPair.publicKey) },
recipient: { identity: "human:recipient", publicKey: recipientPublicKeyHex },
passportHash: passport.passportHash,
bordbuchHead: "",
authorizedAt: now.toISOString(),
expiresAt: expires.toISOString(),
};
const signedAuth = await signAuthorization(authPayload, senderKeyPair.privateKey);
return { signedAuth, recipientKeyPair };
}
test("returns preview data without writing to disk", async () => {
const systemId = "dry-run-complete-site";
const root = join(tmpDir, "werkstatt");
const { signedAuth, recipientKeyPair } = await setupCompleteScenario(root, systemId);
process.env.SIGNING_PRIVATE_KEY = toPem(recipientKeyPair.privateKey, "private");
const result = await runSternsystemHandoverComplete(
makeInput({
id: systemId,
"dry-run": true,
"authorization-data": JSON.stringify(signedAuth),
}),
makeContext(root),
);
expect(result.data!.dryRun).toBe(true);
expect(result.data!.newPassportHashPreview).toBeDefined();
expect(result.data!.bordbuchEventPreview).toBeDefined();
expect(result.data!.bordbuchEventPreview!.kind).toBe("handover");
expect(result.data!.bordbuchEventPreview!.metadata.authorizationHash).toBe(
signedAuth.authorizationHash,
);
expect(result.data!.registryTransferPreview).toBeDefined();
expect(result.data!.registryTransferPreview!.previousOwner).toBe("human:sender");
expect(result.data!.registryTransferPreview!.newOwner).toBe("human:recipient");
const authPath = resolveAuthorizationPath(root, systemId);
expect(existsSync(authPath)).toBe(false);
delete process.env.SIGNING_PRIVATE_KEY;
});
test("fails without --authorization-data", async () => {
const systemId = "dry-run-no-auth-data-site";
const root = join(tmpDir, "werkstatt");
const { recipientKeyPair } = await setupCompleteScenario(root, systemId);
process.env.SIGNING_PRIVATE_KEY = toPem(recipientKeyPair.privateKey, "private");
await expect(
runSternsystemHandoverComplete(
makeInput({ id: systemId, "dry-run": true }),
makeContext(root),
),
).rejects.toThrow(/authorization-data is required in dry-run mode/);
delete process.env.SIGNING_PRIVATE_KEY;
});
test("fails with --source-locator", async () => {
const systemId = "dry-run-source-locator-site";
const root = join(tmpDir, "werkstatt");
const { signedAuth, recipientKeyPair } = await setupCompleteScenario(root, systemId);
process.env.SIGNING_PRIVATE_KEY = toPem(recipientKeyPair.privateKey, "private");
await expect(
runSternsystemHandoverComplete(
makeInput({
id: systemId,
"dry-run": true,
"authorization-data": JSON.stringify(signedAuth),
"source-locator": "some-remote",
}),
makeContext(root),
),
).rejects.toThrow(/source-locator is incompatible with --dry-run/);
delete process.env.SIGNING_PRIVATE_KEY;
});
test("fails with invalid JSON in --authorization-data", async () => {
const systemId = "dry-run-invalid-json-site";
const root = join(tmpDir, "werkstatt");
const { recipientKeyPair } = await setupCompleteScenario(root, systemId);
process.env.SIGNING_PRIVATE_KEY = toPem(recipientKeyPair.privateKey, "private");
await expect(
runSternsystemHandoverComplete(
makeInput({ id: systemId, "dry-run": true, "authorization-data": "not-valid-json" }),
makeContext(root),
),
).rejects.toThrow(/invalid authorization-data JSON/);
delete process.env.SIGNING_PRIVATE_KEY;
});
test("fails with expired authorization (HANDOVER-02)", async () => {
const systemId = "dry-run-expired-site";
const root = join(tmpDir, "werkstatt");
const { signedAuth, recipientKeyPair } = await setupCompleteScenario(root, systemId);
const expiredAuth: SignedHandoverAuthorization = {
...signedAuth,
payload: {
...signedAuth.payload,
expiresAt: new Date(Date.now() - 1000).toISOString(),
},
};
process.env.SIGNING_PRIVATE_KEY = toPem(recipientKeyPair.privateKey, "private");
await expect(
runSternsystemHandoverComplete(
makeInput({
id: systemId,
"dry-run": true,
"authorization-data": JSON.stringify(expiredAuth),
}),
makeContext(root),
),
).rejects.toThrow(/HANDOVER-02/);
delete process.env.SIGNING_PRIVATE_KEY;
});
test("fails with wrong recipient key (HANDOVER-04)", async () => {
const systemId = "dry-run-wrong-key-site";
const root = join(tmpDir, "werkstatt");
const { signedAuth } = await setupCompleteScenario(root, systemId);
const wrongKeyPair = await generateKeyPair();
process.env.SIGNING_PRIVATE_KEY = toPem(wrongKeyPair.privateKey, "private");
await expect(
runSternsystemHandoverComplete(
makeInput({
id: systemId,
"dry-run": true,
"authorization-data": JSON.stringify(signedAuth),
}),
makeContext(root),
),
).rejects.toThrow(/HANDOVER-04/);
delete process.env.SIGNING_PRIVATE_KEY;
});
});