import assert from "node:assert/strict"; import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import test from "node:test"; import { atomicWrite, finalizeDeployment, preflightDeploy, runFinalizeCli, validateManifest } from "./finalize-manifest.mjs"; import { runSelectCli, selectManifest } from "./select-manifest.mjs"; const TOKEN = "0x1000000000000000000000000000000000000001"; const PROXY = "0x2000000000000000000000000000000000000002"; const IMPLEMENTATION = "0x3000000000000000000000000000000000000003"; const OWNER = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; const IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; const EDUCATIONAL_WARNING = "Educational demo — mock token — never use real funds."; test("preflight rejects an existing target canonical manifest with the safe recovery command", async () => { await withFixture(async (root) => { await writeJson(join(root, "deployments", "anvil.json"), manifest()); await assert.rejects( () => preflightDeploy({ root, network: "anvil" }), /make reset-local/ ); await writeJson(join(root, "deployments", "base-sepolia.json"), manifest({ network: "baseSepolia", chainId: 84532 })); await assert.rejects( () => preflightDeploy({ root, network: "baseSepolia" }), /make archive-base-manifest/ ); }); }); test("direct manifest CLIs print the exact educational warning", async () => { await withFixture(async (root) => { const finalizeLogs = []; await runFinalizeCli(["preflight-deploy", "anvil"], { root, log: (line) => finalizeLogs.push(line) }); assert.deepEqual(finalizeLogs, [EDUCATIONAL_WARNING]); await writeJson(join(root, "deployments", "anvil.json"), manifest()); const selectLogs = []; await runSelectCli(["anvil"], { root, log: (line) => selectLogs.push(line) }); assert.deepEqual(selectLogs, [EDUCATIONAL_WARNING]); }); }); test("finalizer confirms a nested public actor manifest and preserves omitted Base URLs", async () => { await withFixture(async (root) => { const pending = manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 0 }); await writeJson(join(root, "deployments", "pending.json"), pending); await writeBroadcast(root, pending); const output = await finalizeDeployment({ root, rpc: fakeRpc({ chainId: "0x14a34" }) }); assert.deepEqual(output.manifest, { ...pending, deploymentBlock: 42 }); assert.equal(Object.hasOwn(output.manifest, "rpcUrl"), false); assert.equal(Object.hasOwn(output.manifest, "explorerBaseUrl"), false); assert.deepEqual(output.manifest.actors, [{ label: "owner", address: OWNER }]); }); }); test("finalizer accepts an Anvil manifest with omitted optional rpcUrl and preserves its absence", async () => { await withFixture(async (root) => { const pending = manifest({ deploymentBlock: 0 }); delete pending.rpcUrl; await writeJson(join(root, "deployments", "pending.json"), pending); await writeBroadcast(root, pending); const output = await finalizeDeployment({ root, rpc: fakeRpc() }); assert.equal(Object.hasOwn(output.manifest, "rpcUrl"), false); assert.equal(Object.hasOwn(await readJson(output.path), "rpcUrl"), false); }); }); test("validator rejects the legacy parallel actor and explorer schema", () => { assert.throws( () => validateManifest(legacyManifest()), /unknown field|missing required field/ ); }); test("finalizer writes only a receipt-confirmed manifest and preserves active until selection", async () => { await withFixture(async (root) => { const pending = manifest({ deploymentBlock: 0 }); await writeJson(join(root, "deployments", "pending.json"), pending); await writeJson(join(root, "deployments", "active.json"), manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 88 })); await writeBroadcast(root, pending, { hash: "0xaaa", contractAddress: PROXY }); const output = await finalizeDeployment({ root, rpc: fakeRpc() }); assert.equal(output.path, join(root, "deployments", "anvil.json")); assert.deepEqual(await readJson(output.path), { ...pending, deploymentBlock: 42 }); assert.deepEqual(await readJson(join(root, "deployments", "active.json")), manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 88 })); }); }); test("finalizer rejects invalid receipt, transaction, RPC, code, slot, and secret data without touching confirmed files", async () => { const cases = [ ["failed receipt", { rpc: fakeRpc({ receipt: { status: "0x0", blockNumber: "0x2a" } }) }, /not successful/], ["missing receipt", { rpc: fakeRpc({ receipt: null }) }, /missing receipt/], ["ambiguous proxy transaction", { broadcast: { extraProxy: true } }, /exactly one/], ["partial broadcast", { broadcast: { omitProxy: true } }, /exactly one/], ["non-creation proxy transaction", { broadcast: { transactionType: "CALL" } }, /exactly one/], ["wrong chain", { rpc: fakeRpc({ chainId: "0x14a34" }) }, /chain ID/], ["missing code", { rpc: fakeRpc({ missingCode: TOKEN }) }, /has no code/], ["implementation slot mismatch", { rpc: fakeRpc({ slot: TOKEN }) }, /implementation slot/], ["secret-bearing pending manifest", { pending: manifest({ rpcUrl: "https://user:password@example.invalid" }) }, /credential|secret|endpoints/i], ["mnemonic in actor label", { pending: manifest({ deploymentBlock: 0, actors: localActors("test test test test test test test test test test test junk") }) }, /prohibited/i], ["private key in actor label", { pending: manifest({ deploymentBlock: 0, actors: localActors("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") }) }, /prohibited/i], ["credential in RPC path", { pending: manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 0, rpcUrl: "https://sepolia.base.org/v1/secret-token" }) }, /public URL|prohibited/i], ["unknown manifest field", { pending: manifest({ deploymentBlock: 0, harmlessLookingField: "not allowed" }) }, /unknown field/i], ]; for (const [name, options, expected] of cases) { await withFixture(async (root) => { const pending = options.pending ?? manifest({ deploymentBlock: 0 }); await writeJson(join(root, "deployments", "pending.json"), pending); await writeJson(join(root, "deployments", "anvil.json"), manifest({ deploymentBlock: 7 })); await writeJson(join(root, "deployments", "active.json"), manifest({ deploymentBlock: 8 })); await writeBroadcast(root, pending, options.broadcast); const beforeCanonical = await readFile(join(root, "deployments", "anvil.json")); const beforeActive = await readFile(join(root, "deployments", "active.json")); await assert.rejects(() => finalizeDeployment({ root, rpc: options.rpc ?? fakeRpc() }), expected, name); assert.deepEqual(await readFile(join(root, "deployments", "anvil.json")), beforeCanonical, name); assert.deepEqual(await readFile(join(root, "deployments", "active.json")), beforeActive, name); }); } }); test("finalizer failure leaves an initially absent canonical manifest absent", async () => { await withFixture(async (root) => { const pending = manifest({ deploymentBlock: 0, actors: localActors("MNEMONIC") }); await writeJson(join(root, "deployments", "pending.json"), pending); await writeJson(join(root, "deployments", "active.json"), manifest({ deploymentBlock: 8 })); await writeBroadcast(root, pending); await assert.rejects(() => finalizeDeployment({ root, rpc: fakeRpc() }), /prohibited/i); await assert.rejects(() => access(join(root, "deployments", "anvil.json"))); }); }); test("selection atomically replaces active with only a valid named canonical manifest", async () => { await withFixture(async (root) => { const anvil = manifest({ deploymentBlock: 31 }); const base = manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 32 }); await writeJson(join(root, "deployments", "anvil.json"), anvil); await writeJson(join(root, "deployments", "base-sepolia.json"), base); await selectManifest({ root, network: "anvil" }); const anvilBytes = await readFile(join(root, "deployments", "anvil.json")); assert.deepEqual(await readFile(join(root, "deployments", "active.json")), anvilBytes); await selectManifest({ root, network: "baseSepolia" }); assert.deepEqual(await readFile(join(root, "deployments", "anvil.json")), anvilBytes); assert.deepEqual(await readJson(join(root, "deployments", "active.json")), base); const baseBytes = await readFile(join(root, "deployments", "base-sepolia.json")); await selectManifest({ root, network: "anvil" }); assert.deepEqual(await readFile(join(root, "deployments", "base-sepolia.json")), baseBytes); await writeJson(join(root, "deployments", "base-sepolia.json"), manifest({ network: "baseSepolia", chainId: 84532, deploymentBlock: 0 })); const beforeActive = await readFile(join(root, "deployments", "active.json")); await assert.rejects(() => selectManifest({ root, network: "baseSepolia" }), /deploymentBlock/); assert.deepEqual(await readFile(join(root, "deployments", "active.json")), beforeActive); }); }); test("atomic writes stage a same-directory temporary file before renaming it into place", async () => { const target = "/tmp/deployments/active.json"; const calls = []; const io = { writeFile: async (path, contents) => calls.push(["write", path, contents]), rename: async (source, destination) => calls.push(["rename", source, destination]), }; await atomicWrite(target, "confirmed", io); assert.equal(calls[0][0], "write"); assert.equal(dirname(calls[0][1]), dirname(target)); assert.notEqual(calls[0][1], target); assert.match(calls[0][1], /\.json$/); assert.deepEqual(calls[1], ["rename", calls[0][1], target]); }); test("atomic writes remove the ignored staging file when rename fails", async () => { const target = "/tmp/deployments/active.json"; const calls = []; const io = { writeFile: async (path) => calls.push(["write", path]), rename: async () => { throw new Error("rename failed"); }, rm: async (path) => calls.push(["rm", path]), }; await assert.rejects(() => atomicWrite(target, "confirmed", io), /rename failed/); assert.deepEqual(calls[1], ["rm", calls[0][1]]); }); function manifest(overrides = {}) { const baseSepolia = overrides.network === "baseSepolia"; return { schemaVersion: 1, network: baseSepolia ? "baseSepolia" : "anvil", chainId: baseSepolia ? 84532 : 31337, deploymentBlock: 1, ...(baseSepolia ? {} : { rpcUrl: "http://127.0.0.1:8545" }), token: TOKEN, proxy: PROXY, implementation: IMPLEMENTATION, owner: OWNER, actors: baseSepolia ? [{ label: "owner", address: OWNER }] : localActors(), ...overrides, }; } function legacyManifest(overrides = {}) { return { schemaVersion: 1, network: "anvil", chainId: 31337, deploymentBlock: 1, rpcUrl: "http://127.0.0.1:8545", explorerUrl: "", token: TOKEN, proxy: PROXY, implementation: IMPLEMENTATION, owner: OWNER, actorLabels: ["owner", "Alice", "Bob"], actors: [OWNER, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"], ...overrides, }; } function localActors(aliceLabel = "Alice") { return [ { label: "owner", address: OWNER }, { label: aliceLabel, address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" }, { label: "Bob", address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" }, ]; } async function withFixture(fn) { const root = await mkdtemp(join(tmpdir(), "uups-finalizer-")); try { await import("node:fs/promises").then(({ mkdir }) => mkdir(join(root, "deployments"), { recursive: true })); await fn(root); } finally { await rm(root, { recursive: true, force: true }); } } async function writeBroadcast(root, pending, options = {}) { const directory = join(root, "broadcast", "DeployV1.s.sol", String(pending.chainId)); await import("node:fs/promises").then(({ mkdir }) => mkdir(directory, { recursive: true })); const transactions = options.omitProxy ? [{ hash: "0xbbb", transactionType: "CREATE", contractAddress: pending.token }] : [{ hash: "0xaaa", transactionType: options.transactionType ?? "CREATE", contractAddress: pending.proxy }]; if (options.extraProxy) transactions.push({ hash: "0xccc", transactionType: "CREATE", contractAddress: pending.proxy }); await writeJson(join(directory, "run-latest.json"), { transactions }); } function fakeRpc(overrides = {}) { return async (method, params) => { if (method === "eth_chainId") return overrides.chainId ?? "0x7a69"; if (method === "eth_getTransactionReceipt") return Object.hasOwn(overrides, "receipt") ? overrides.receipt : { status: "0x1", blockNumber: "0x2a" }; if (method === "eth_getCode") return params[0].toLowerCase() === overrides.missingCode?.toLowerCase() ? "0x" : "0x6000"; if (method === "eth_getStorageAt") { assert.equal(params[1], IMPLEMENTATION_SLOT); return `0x000000000000000000000000${(overrides.slot ?? IMPLEMENTATION).slice(2)}`; } throw new Error(`unexpected RPC method: ${method}`); }; } async function writeJson(path, value) { await writeFile(path, `${JSON.stringify(value, null, 2)}\n`); } async function readJson(path) { return JSON.parse(await readFile(path, "utf8")); }