/*
RFC-1124: tests for the git-native fleet claims registry — publish,
offline verify (self/transfer legitimacy), LWW conflict resolution, tombstone
stale verdict, sync divergence diagnostics.
- No network — remotes are local bare repos in tmp dirs.
- RFC-1124: initial coverage for claims-repo.ts (AC-1, AC-4, AC-6, AC-8, AC-9).
*/
import { execFileSync } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync, readFileSync } from "node:fs";
import { writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { generateKeyPair, toHex } from "@warpgogol/werkstatt-engine/signing";
import type { FleetClaim } from "@warpgogol/werkstatt-shared/passport/claim-sign";
import { appendBordbuchEntry } from "../bordbuch/bordbuch-io.ts";
import {
buildClaim,
claimsStatus,
loadFleetClaimsConfig,
publishClaim,
publishPendingTransferClaim,
resolveClaimConflict,
syncClaims,
verifyClaimOffline,
} from "./claims-repo.ts";
import { deriveInstanceId } from "./ownership-registry.ts";
let root: string;
let systemId: string;
beforeEach(() => {
// werkstatt root nested inside the tmpdir — resolveCacheClonePath resolves
// systems-cache as a SIBLING of the werkstatt root, so this keeps the cache
// inside the per-test tmpdir instead of leaking into shared /tmp.
root = join(mkdtempSync(join(tmpdir(), "claims-repo-")), "werkstatt");
mkdirSync(root, { recursive: true });
systemId = "test-site.example";
});
afterEach(() => {
rmSync(root, { recursive: true, force: true });
});
// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------
async function makeKey() {
const kp = await generateKeyPair();
return { privateKeyHex: toHex(kp.privateKey), publicKeyHex: toHex(kp.publicKey) };
}
function writePassportFixture(
system: string,
publicKeyHex: string,
passportHash: string,
identity = "did:web:creator",
): void {
const cacheDir = join(root, "..", "systems-cache", system);
mkdirSync(join(cacheDir, "bordbuch"), { recursive: true });
const passport = {
passportHash,
payload: {
creator: { identity, publicKey: publicKeyHex },
provenance: { bordbuchHead: null },
},
};
writeFileSync(join(cacheDir, "passport.json"), JSON.stringify(passport, null, 2));
}
async function writeFleetConfig(remotes: string[] = []): Promise {
await writeFile(
join(root, "werkstatt.fleet.json"),
JSON.stringify({ remotes, localPath: ".werkstatt/fleet-claims" }, null, 2),
);
}
function readLocalClaim(system: string): FleetClaim | null {
const p = join(root, ".werkstatt/fleet-claims/claims", `${system}.json`);
if (!existsSync(p)) return null;
return JSON.parse(readFileSync(p, "utf8")) as FleetClaim;
}
function writeLocalClaim(system: string, claim: FleetClaim): void {
const dir = join(root, ".werkstatt/fleet-claims/claims");
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, `${system}.json`), JSON.stringify(claim, null, 2));
}
function makeBareRemote(name: string): string {
const remotePath = join(root, name);
mkdirSync(remotePath, { recursive: true });
execFileSync("git", ["-C", remotePath, "init", "--bare", "--initial-branch=main"], {
stdio: "pipe",
});
return remotePath;
}
// ---------------------------------------------------------------------------
// AC-1: publish writes a signed claim verifiable against signerPublicKey
// ---------------------------------------------------------------------------
describe("RFC-1124 AC-1: publishClaim", () => {
it("writes claims/.json whose signature verifies against embedded signerPublicKey", async () => {
const key = await makeKey();
writePassportFixture(systemId, key.publicKeyHex, "p".repeat(64));
await writeFleetConfig();
const result = await publishClaim({
systemId,
werkstattRoot: root,
signerPrivateKeyHex: key.privateKeyHex,
signerPublicKey: key.publicKeyHex,
push: false,
});
const claim = readLocalClaim(systemId);
expect(claim, "claim file must exist after publish").not.toBeNull();
expect(claim!.passportHash).toBe("p".repeat(64));
expect(claim!.signerPublicKey).toBe(key.publicKeyHex);
expect(claim!.instanceId).toBe(deriveInstanceId(key.publicKeyHex));
expect(result.commit).toMatch(/^[0-9a-f]{40}$/);
const { verifyClaim } = await import("@warpgogol/werkstatt-shared/passport/claim-sign");
expect(await verifyClaim(claim!), "signature must verify against signerPublicKey").toBe(true);
});
it("verify resolves the published claim as valid offline", async () => {
const key = await makeKey();
writePassportFixture(systemId, key.publicKeyHex, "q".repeat(64));
await writeFleetConfig();
await publishClaim({
systemId,
werkstattRoot: root,
signerPrivateKeyHex: key.privateKeyHex,
signerPublicKey: key.publicKeyHex,
push: false,
});
const result = await verifyClaimOffline({ systemId, werkstattRoot: root });
expect(result.verdict).toBe("valid");
expect(result.claim?.passportHash).toBe("q".repeat(64));
});
});
// ---------------------------------------------------------------------------
// AC-4 + AC-8: invalid signature → invalid-signature, existing claim untouched
// ---------------------------------------------------------------------------
describe("RFC-1124 AC-4/AC-8: invalid signature handling", () => {
it("reports invalid-signature and leaves the file untouched", async () => {
const key = await makeKey();
writePassportFixture(systemId, key.publicKeyHex, "r".repeat(64));
await writeFleetConfig();
await publishClaim({
systemId,
werkstattRoot: root,
signerPrivateKeyHex: key.privateKeyHex,
signerPublicKey: key.publicKeyHex,
push: false,
});
// Corrupt the signature in place.
const claim = readLocalClaim(systemId)!;
const corrupted = { ...claim, signature: "z" + "1".repeat(87) };
writeLocalClaim(systemId, corrupted);
const before = readFileSync(
join(root, ".werkstatt/fleet-claims/claims", `${systemId}.json`),
"utf8",
);
const result = await verifyClaimOffline({ systemId, werkstattRoot: root });
expect(result.verdict).toBe("invalid-signature");
const after = readFileSync(
join(root, ".werkstatt/fleet-claims/claims", `${systemId}.json`),
"utf8",
);
expect(after, "verify must never write — corrupted claim stays as evidence").toBe(before);
});
});
// ---------------------------------------------------------------------------
// AC-6: fully offline verify (remote configured but unreachable)
// ---------------------------------------------------------------------------
describe("RFC-1124 AC-6: offline verify", () => {
it("resolves verdict without touching remotes", async () => {
const key = await makeKey();
writePassportFixture(systemId, key.publicKeyHex, "s".repeat(64));
// Remote points at a nonexistent path — any fetch would fail loudly.
await writeFleetConfig(["/nonexistent/remote.git"]);
await publishClaim({
systemId,
werkstattRoot: root,
signerPrivateKeyHex: key.privateKeyHex,
signerPublicKey: key.publicKeyHex,
push: false,
});
const result = await verifyClaimOffline({ systemId, werkstattRoot: root });
expect(result.verdict).toBe("valid");
});
it("reports no-claim for unknown systems", async () => {
await writeFleetConfig();
const result = await verifyClaimOffline({ systemId: "ghost.example", werkstattRoot: root });
expect(result.verdict).toBe("no-claim");
expect(result.claim).toBeNull();
});
});
// ---------------------------------------------------------------------------
// AC-9: transfer claim legitimacy via bordbuch handover event
// ---------------------------------------------------------------------------
describe("RFC-1124 AC-9: transfer claim legitimacy", () => {
it("accepts a transfer claim signed by the previous creator with matching bordbuch event", async () => {
const oldKey = await makeKey();
const newKey = await makeKey();
const oldHash = "a".repeat(64);
const newHash = "b".repeat(64);
const authHash = "c".repeat(64);
// Old owner's self-claim exists first.
writePassportFixture(systemId, oldKey.publicKeyHex, oldHash);
await writeFleetConfig();
await publishClaim({
systemId,
werkstattRoot: root,
signerPrivateKeyHex: oldKey.privateKeyHex,
signerPublicKey: oldKey.publicKeyHex,
push: false,
});
// Handover: new passport + bordbuch handover event.
writePassportFixture(systemId, newKey.publicKeyHex, newHash, "did:web:new-creator");
await appendBordbuchEntry(
root,
systemId,
"handover",
"handover to new creator",
"did:web:new-creator",
{
missionId: null,
releaseId: null,
metadata: { authorizationHash: authHash, newPassportHash: newHash },
},
);
// Sender (old key) publishes the transfer claim.
const transfer = await publishPendingTransferClaim({
systemId,
werkstattRoot: root,
signerPrivateKeyHex: oldKey.privateKeyHex,
signerPublicKey: oldKey.publicKeyHex,
push: false,
});
expect(transfer, "pending transfer claim must be detected and published").not.toBeNull();
expect(transfer!.claim.signerPublicKey).toBe(oldKey.publicKeyHex);
expect(transfer!.claim.instanceId).toBe(deriveInstanceId(newKey.publicKeyHex));
expect(transfer!.claim.authorizationHash).toBe(authHash);
const result = await verifyClaimOffline({ systemId, werkstattRoot: root });
expect(result.verdict).toBe("valid");
expect(result.claim?.passportHash).toBe(newHash);
});
it("rejects a transfer claim without a matching bordbuch handover event", async () => {
const oldKey = await makeKey();
const newKey = await makeKey();
writePassportFixture(systemId, oldKey.publicKeyHex, "a".repeat(64));
await writeFleetConfig();
await publishClaim({
systemId,
werkstattRoot: root,
signerPrivateKeyHex: oldKey.privateKeyHex,
signerPublicKey: oldKey.publicKeyHex,
push: false,
});
// Forge a transfer claim: describes new owner, signed by old key, but NO
// bordbuch handover event exists.
writePassportFixture(systemId, newKey.publicKeyHex, "b".repeat(64), "did:web:attacker");
const forged = await buildClaim({
systemId,
werkstattRoot: root,
signerPrivateKeyHex: oldKey.privateKeyHex,
signerPublicKey: oldKey.publicKeyHex,
authorizationHash: "f".repeat(64),
});
writeLocalClaim(systemId, forged);
const result = await verifyClaimOffline({ systemId, werkstattRoot: root });
expect(result.verdict).toBe("invalid-signature");
});
});
// ---------------------------------------------------------------------------
// Tombstone + LWW
// ---------------------------------------------------------------------------
describe("RFC-1124: tombstone and conflict resolution", () => {
it("reports stale for a tombstone claim (supersededBy set)", async () => {
const key = await makeKey();
writePassportFixture(systemId, key.publicKeyHex, "t".repeat(64));
await writeFleetConfig();
await publishClaim({
systemId,
werkstattRoot: root,
signerPrivateKeyHex: key.privateKeyHex,
signerPublicKey: key.publicKeyHex,
push: false,
});
// A tombstone must be SIGNED — mutating supersededBy on the existing claim
// would invalidate the signature (correctly yielding invalid-signature).
const claim = readLocalClaim(systemId)!;
const { signClaim } = await import("@warpgogol/werkstatt-shared/passport/claim-sign");
const { signature: _signature, ...data } = claim;
const tombstone: FleetClaim = {
...data,
supersededBy: "u".repeat(64),
signature: await signClaim({ ...data, supersededBy: "u".repeat(64) }, key.privateKeyHex),
};
writeLocalClaim(systemId, tombstone);
const result = await verifyClaimOffline({ systemId, werkstattRoot: root });
expect(result.verdict).toBe("stale");
});
it("resolveClaimConflict: newer claimedAt wins; equal → self-claim beats transfer", async () => {
const selfClaim = {
claimedAt: "2026-01-01T00:00:00Z",
signerPublicKey: "aa",
instanceId: deriveInstanceId("aa"),
} as FleetClaim;
const newerTransfer = {
claimedAt: "2026-01-02T00:00:00Z",
signerPublicKey: "bb",
instanceId: deriveInstanceId("cc"),
} as FleetClaim;
expect(resolveClaimConflict(selfClaim, newerTransfer).winner).toBe(newerTransfer);
const sameTimeTransfer = { ...newerTransfer, claimedAt: selfClaim.claimedAt };
expect(resolveClaimConflict(selfClaim, sameTimeTransfer).winner).toBe(selfClaim);
});
});
// ---------------------------------------------------------------------------
// Sync + status
// ---------------------------------------------------------------------------
describe("RFC-1124: sync and status", () => {
it("pushes committed claims to a local bare remote", async () => {
const key = await makeKey();
const remote = makeBareRemote("claims-remote.git");
writePassportFixture(systemId, key.publicKeyHex, "v".repeat(64));
await writeFleetConfig([remote]);
const pub = await publishClaim({
systemId,
werkstattRoot: root,
signerPrivateKeyHex: key.privateKeyHex,
signerPublicKey: key.publicKeyHex,
});
expect(pub.pushed).toBe(true);
const remoteHas = execFileSync("git", ["-C", remote, "show", `main:claims/${systemId}.json`], {
encoding: "utf8",
});
expect(JSON.parse(remoteHas).passportHash).toBe("v".repeat(64));
});
it("status reports claims count and unpushed commits", async () => {
const key = await makeKey();
writePassportFixture(systemId, key.publicKeyHex, "w".repeat(64));
await writeFleetConfig();
await publishClaim({
systemId,
werkstattRoot: root,
signerPrivateKeyHex: key.privateKeyHex,
signerPublicKey: key.publicKeyHex,
push: false,
});
const status = await claimsStatus(root);
expect(status.configured).toBe(true);
expect(status.claims).toBe(1);
expect(status.unpushedCommits).toBeGreaterThan(0);
});
it("sync fast-forwards a remote claim into the local clone", async () => {
const key = await makeKey();
const remote = makeBareRemote("claims-remote2.git");
writePassportFixture(systemId, key.publicKeyHex, "x".repeat(64));
await writeFleetConfig([remote]);
await publishClaim({
systemId,
werkstattRoot: root,
signerPrivateKeyHex: key.privateKeyHex,
signerPublicKey: key.publicKeyHex,
});
// Second workshop clones the same remote and adds another system's claim.
const other = mkdtempSync(join(tmpdir(), "claims-other-"));
try {
execFileSync("git", ["clone", remote, join(other, "clone")], { stdio: "pipe" });
const otherClaim = { ...readLocalClaim(systemId)!, systemId: "other-site.example" };
const dir = join(other, "clone/claims");
writeFileSync(join(dir, "other-site.example.json"), JSON.stringify(otherClaim, null, 2));
execFileSync("git", ["-C", join(other, "clone"), "add", "claims/other-site.example.json"], {
stdio: "pipe",
});
execFileSync(
"git",
[
"-C",
join(other, "clone"),
"-c",
"user.name=t",
"-c",
"user.email=t@t",
"commit",
"-m",
"other claim",
],
{ stdio: "pipe" },
);
execFileSync("git", ["-C", join(other, "clone"), "push", "origin", "HEAD:main"], {
stdio: "pipe",
});
const sync = await syncClaims(root);
expect(sync.fastForwarded).toBe(true);
expect(sync.diverged).toHaveLength(0);
expect(existsSync(join(root, ".werkstatt/fleet-claims/claims/other-site.example.json"))).toBe(
true,
);
} finally {
rmSync(other, { recursive: true, force: true });
}
});
});
// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------
describe("RFC-1124: config", () => {
it("loadFleetClaimsConfig returns null without werkstatt.fleet.json", async () => {
expect(await loadFleetClaimsConfig(root)).toBeNull();
});
});