Files
apex-mcp-server/packages/builder/test/integration/keyless-e2e.test.ts
FilipBl4gojevic 3edb9afd3c test(builder): registry tier-1 + gated lifecycle E2E; shared sign helper; legacy suite goes keyless
Adds the three remaining integration layers for spec PR 7 (agent-registry
keyless): a tier-1 suite that exercises the five build_* tools against the
live testnet registry with zero secrets (build-only, nothing signed or
submitted), a gated full lifecycle E2E (register -> update -> deregister)
that actually submits, and a shared signWithMnemonic() helper extracted
from keyless-e2e.test.ts so registry-e2e.test.ts and run.test.ts's
registry section don't each re-inline the same CML signing sequence.
run.test.ts's "Agent Network Tools" section is rewritten: each of the five
old custodial calls is now build_* -> signWithMnemonic -> submit, via a
new buildSignSubmit() helper that reuses the file's existing
assertSuccessOrKnownError convention at both the build and submit legs.

Found and fixed one load-bearing bug while proving this live, not just in
review: the inline signing block moved verbatim per the brief's Step 1
built the signed tx's witness set from scratch, which is lossless only for
a script-less payment (the one case it had been proven against). The
registry family's build_* tools attach a minting policy and/or spending
validator, so their unsigned CBOR already carries scripts/redeemers that
the from-scratch witness set silently dropped - the ledger rejected the
first live E2E attempt with MissingScriptWitnessesUTXOW /
PPViewHashesDontMatch. Verified zero funds were lost (balance identical
before/after the rejected submission - a mempool-level rejection never
reaches a block) before fixing it by merging into the tx's existing
witness set via CML's add_all_witnesses instead of discarding it. Re-run
clean: all 3 E2E transactions confirmed on-chain, plus all 5 of the legacy
suite's registry operations submitted successfully too (independent
confirmation across both the mint and spend redeemer paths). Also widened
one tier-1 regex that didn't tolerate the live tool's Markdown-bold label
formatting.

Live evidence (testnet, funded wallet, 8 real transactions total across
the gated E2E and the legacy suite): balance 986.689510 -> 982.280960
AP3X, fully reconciled against transaction fees plus one message delivery
to an owner-derived enterprise address. All tx hashes, explorer links, and
the full reconciliation are in task-5-report.md. mnemonic.txt and the two
temporary evidence-capture scripts were removed and confirmed absent
before this commit.

Gates: typecheck 0, unit 350/350, smoke 26/26, auth 12/12, boundary 4/4 -
all re-verified after cleanup. E2E skip-path (VECTOR_E2E_SUBMIT unset)
exits 0 with tests 0 for both keyless-e2e.test.ts and registry-e2e.test.ts,
confirmed with mnemonic.txt genuinely absent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:17:47 +02:00

59 lines
2.6 KiB
TypeScript

// packages/builder/test/integration/keyless-e2e.test.ts
import { describe, test, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { walletFromSeed } from '@lucid-evolution/lucid';
import { startServer, stopServer, callTool, getMnemonic, ServerContext } from '../setup.ts';
import { signWithMnemonic } from './sign-helper.ts';
// Full non-custodial pipeline against the live testnet:
// keyless build (hosted tool) -> inline CML sign (this process, standing in
// for the in-tree local signer (packages/signer) — same CML primitives,
// without the signer's decode/policy/audit path) -> submit tool -> await
// tool. A self-send, so the run costs only the fee (~0.16 AP3X).
//
// Gated: set VECTOR_E2E_SUBMIT=1 to run. Requires packages/builder/mnemonic.txt
// (the funded testnet wallet). NEVER runs in CI.
const RUN = process.env.VECTOR_E2E_SUBMIT === '1';
function extractCbor(text: string): string {
const m = text.match(/```\n([0-9a-fA-F]+)\n```/);
assert.ok(m, `no CBOR block in tool response:\n${text.slice(0, 500)}`);
return m[1];
}
function extractTxHash(text: string): string {
const m = text.match(/Transaction Hash: ([0-9a-f]{64})/);
assert.ok(m, `no tx hash in tool response:\n${text.slice(0, 500)}`);
return m[1];
}
describe('keyless E2E: build -> sign -> submit -> await', { skip: !RUN }, () => {
let ctx: ServerContext;
before(async () => { ctx = await startServer(); });
after(async () => { await stopServer(ctx); });
test('the four-call non-custodial flow lands a self-send on-chain', { timeout: 300_000 }, async () => {
const mnemonic = getMnemonic();
const { address } = walletFromSeed(mnemonic, { network: 'Mainnet', accountIndex: 0 });
// 1. keyless build (hosted)
const buildText = await callTool(ctx.client, 'vector_build_send_apex', {
changeAddress: address, recipientAddress: address, amount: 3,
});
const unsignedCbor = extractCbor(buildText);
// 2. sign locally (stand-in for vector_signer_sign)
const { signedCborHex: signedCbor, txHash } = signWithMnemonic(unsignedCbor, mnemonic);
// 3. submit (hosted)
const submitText = await callTool(ctx.client, 'vector_submit_transaction', { signedTxCbor: signedCbor });
const submittedHash = extractTxHash(submitText);
assert.equal(submittedHash, txHash, 'submitted hash differs from the locally computed body hash');
// 4. await confirmation (hosted)
const awaitText = await callTool(ctx.client, 'vector_await_transaction', {
txHash: submittedHash, timeoutSeconds: 240,
});
assert.match(awaitText, /Transaction Confirmed/);
});
});