/* RFC-0968: End-to-end handover test simulating two werkstatt instances (sender + recipient) performing a full prepare → complete cycle. Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. RFC-0968: initial E2E handover test. */ import { test, expect, describe, beforeEach, afterEach } from "vitest"; import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { signAuthorization, writeAuthorization, readAuthorization, removeAuthorization, verifyAuthorization, isAuthorizationExpired, type HandoverAuthorizationV1, } from "./handover.ts"; import { buildPassportPayload, signPassport, verifyPassport, derivePublicKey, type SignedSitePassport, } from "./passport.ts"; import { writePassport, readPassport } from "./registry-io.ts"; import { appendBordbuchEntry, readBordbuch } from "../bordbuch/bordbuch-io.ts"; import { generateKeyPair, toHex } from "@warpgogol/werkstatt-engine/signing"; let tmpDir: string; beforeEach(() => { tmpDir = mkdtempSync(join(tmpdir(), "handover-e2e-")); }); afterEach(() => { rmSync(tmpDir, { recursive: true, force: true }); }); 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", ); writeFileSync( join(cacheDir, ".env.example"), ["# Example env", "API_KEY=your-key-here", ""].join("\n"), ); } describe("RFC-0968: E2E handover cycle", () => { test("full prepare → complete cycle: sender prepares, recipient completes", async () => { const systemId = "transfer-site"; const senderRoot = join(tmpDir, "sender", "werkstatt"); const recipientRoot = join(tmpDir, "recipient", "werkstatt"); mkdirSync(senderRoot, { recursive: true }); mkdirSync(recipientRoot, { recursive: true }); // Both instances share the same systems-cache (simulating mirror sync) setupSystem(senderRoot, systemId); setupSystem(recipientRoot, systemId); // --- Sender side: generate passport + prepare handover --- const senderKeyPair = await generateKeyPair(); const senderPublicKeyHex = toHex(senderKeyPair.publicKey); const senderPayload = await buildPassportPayload({ systemId, werkstattRoot: senderRoot, creatorIdentity: "human:sender", creatorPublicKey: senderPublicKeyHex, }); const senderSigned = await signPassport(senderPayload, senderKeyPair.privateKey); const senderPassport: SignedSitePassport = { payload: senderPayload, passportHash: senderSigned.passportHash, signature: senderSigned.signature, }; await writePassport(senderRoot, systemId, senderPassport); // Recipient generates their key pair const recipientKeyPair = await generateKeyPair(); const recipientPublicKeyHex = toHex(recipientKeyPair.publicKey); // Sender prepares handover authorization 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: senderPublicKeyHex, }, recipient: { identity: "human:recipient", publicKey: recipientPublicKeyHex, }, passportHash: senderPassport.passportHash, bordbuchHead: "", authorizedAt: now.toISOString(), expiresAt: expires.toISOString(), }; const signedAuth = await signAuthorization(authPayload, senderKeyPair.privateKey); await writeAuthorization(senderRoot, systemId, signedAuth); // --- Simulate mirror sync: copy authorization + passport to recipient --- // In real life, sternsystem.sync would push from sender to bare, then pull on recipient. // Here we simulate by writing the same files to the recipient's cache clone. await writeAuthorization(recipientRoot, systemId, signedAuth); await writePassport(recipientRoot, systemId, senderPassport); // --- Recipient side: complete handover --- // 1. Read and verify authorization const recipientAuth = await readAuthorization(recipientRoot, systemId); expect(recipientAuth).not.toBeNull(); expect(recipientAuth!.authorizationHash).toBe(signedAuth.authorizationHash); // 2. Verify signature using sender's public key from passport const recipientSenderPassport = await readPassport(recipientRoot, systemId); expect(recipientSenderPassport).not.toBeNull(); const senderPubKey = recipientSenderPassport!.payload.creator.publicKey; const verification = await verifyAuthorization(recipientAuth!, senderPubKey); expect(verification.valid).toBe(true); // 3. Check expiry (HANDOVER-02) expect(isAuthorizationExpired(recipientAuth!.payload.expiresAt)).toBe(false); // 4. Check passport hash match (HANDOVER-03) expect(recipientAuth!.payload.passportHash).toBe(recipientSenderPassport!.passportHash); // 5. Verify recipient key possession (HANDOVER-04) const derivedRecipientPubKey = await derivePublicKey(recipientKeyPair.privateKey); expect(recipientAuth!.payload.recipient.publicKey).toBe(derivedRecipientPubKey); // 6. Regenerate passport with new creator identity const newPayload = await buildPassportPayload({ systemId, werkstattRoot: recipientRoot, creatorIdentity: recipientAuth!.payload.recipient.identity, creatorPublicKey: derivedRecipientPubKey, }); newPayload.provenance.createdAt = recipientSenderPassport!.payload.provenance.createdAt; const newSigned = await signPassport(newPayload, recipientKeyPair.privateKey); const newPassport: SignedSitePassport = { payload: newPayload, passportHash: newSigned.passportHash, signature: newSigned.signature, }; await writePassport(recipientRoot, systemId, newPassport); // 7. Append bordbuch handover event const metadata = { from: recipientAuth!.payload.sender.identity, to: recipientAuth!.payload.recipient.identity, authorizationHash: recipientAuth!.authorizationHash, newPassportHash: newPassport.passportHash, }; const entry = await appendBordbuchEntry( recipientRoot, systemId, "handover", `handover from ${recipientAuth!.payload.sender.identity} to ${recipientAuth!.payload.recipient.identity}`, recipientAuth!.payload.recipient.identity, { missionId: null, releaseId: null, metadata: metadata as Record }, ); // 8. Remove authorization file const removed = await removeAuthorization(recipientRoot, systemId); expect(removed).toBe(true); // --- Verify final state --- const finalPassport = await readPassport(recipientRoot, systemId); expect(finalPassport).not.toBeNull(); expect(finalPassport!.payload.creator.identity).toBe("human:recipient"); expect(finalPassport!.payload.creator.publicKey).toBe(recipientPublicKeyHex); // Verify the new passport is valid const passportVerification = await verifyPassport(finalPassport!); expect(passportVerification.valid).toBe(true); // Verify bordbuch has the handover event const entries = await readBordbuch(recipientRoot, systemId); const handoverEntries = entries.filter((e) => e.kind === "handover"); expect(handoverEntries).toHaveLength(1); expect(handoverEntries[0].hash).toBe(entry.hash); expect((handoverEntries[0].metadata as Record).from).toBe("human:sender"); expect((handoverEntries[0].metadata as Record).to).toBe("human:recipient"); expect((handoverEntries[0].metadata as Record).authorizationHash).toBe( signedAuth.authorizationHash, ); // Verify authorization file is gone const authAfter = await readAuthorization(recipientRoot, systemId); expect(authAfter).toBeNull(); }); test("crash recovery: re-running complete after bordbuch append is rejected (duplicate)", async () => { const systemId = "crash-test-site"; const root = join(tmpDir, "werkstatt"); mkdirSync(root, { recursive: true }); setupSystem(root, systemId); const senderKeyPair = await generateKeyPair(); const recipientKeyPair = await generateKeyPair(); // Setup passport const payload = await buildPassportPayload({ systemId, werkstattRoot: root, creatorIdentity: "human:sender", creatorPublicKey: toHex(senderKeyPair.publicKey), }); const { passportHash, signature } = await signPassport(payload, senderKeyPair.privateKey); await writePassport(root, systemId, { payload, passportHash, signature }); // Create authorization 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: toHex(recipientKeyPair.publicKey) }, passportHash, bordbuchHead: "", authorizedAt: now.toISOString(), expiresAt: expires.toISOString(), }; const signedAuth = await signAuthorization(authPayload, senderKeyPair.privateKey); await writeAuthorization(root, systemId, signedAuth); // Simulate crash: bordbuch entry was appended but authorization file was NOT removed const metadata = { from: "human:sender", to: "human:recipient", authorizationHash: signedAuth.authorizationHash, newPassportHash: "sha256:newpass", }; await appendBordbuchEntry( root, systemId, "handover", "handover from sender to recipient", "human:recipient", { missionId: null, releaseId: null, metadata: metadata as Record }, ); // Crash recovery: attempt to append the same handover event again — should be rejected await expect( appendBordbuchEntry( root, systemId, "handover", "handover from sender to recipient", "human:recipient", { missionId: null, releaseId: null, metadata: metadata as Record }, ), ).rejects.toThrow(/duplicate handover/); // Verify only one handover entry exists const entries = await readBordbuch(root, systemId); const handoverEntries = entries.filter((e) => e.kind === "handover"); expect(handoverEntries).toHaveLength(1); }); test("cancel: sender cancels pending handover by removing authorization file", async () => { const systemId = "cancel-test-site"; const root = join(tmpDir, "werkstatt"); mkdirSync(root, { recursive: true }); setupSystem(root, systemId); const senderKeyPair = await generateKeyPair(); const payload = await buildPassportPayload({ systemId, werkstattRoot: root, creatorIdentity: "human:sender", creatorPublicKey: toHex(senderKeyPair.publicKey), }); const { passportHash, signature } = await signPassport(payload, senderKeyPair.privateKey); await writePassport(root, systemId, { payload, passportHash, signature }); // Create authorization 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: "abcd1234" }, passportHash, bordbuchHead: "", authorizedAt: now.toISOString(), expiresAt: expires.toISOString(), }; const signedAuth = await signAuthorization(authPayload, senderKeyPair.privateKey); await writeAuthorization(root, systemId, signedAuth); // Verify it exists expect(await readAuthorization(root, systemId)).not.toBeNull(); // Cancel const removed = await removeAuthorization(root, systemId); expect(removed).toBe(true); // Verify it's gone expect(await readAuthorization(root, systemId)).toBeNull(); // Cancel again — should return false const removedAgain = await removeAuthorization(root, systemId); expect(removedAgain).toBe(false); }); });