Files
apex-mcp-server/packages/builder/test/integration/registry-keyless.test.ts
FilipBl4gojevic 2baff726a0 test(builder): hermetic pin for script-witness preservation; tighten non-owner and submit assertions
Closes an Opus review round on Task 5's registry integration work.

Adds packages/builder/test/sign-helper.test.ts (top-level test/, picked up
by the unit glob) as the first hermetic gate that would fail if
signWithMnemonic's witness-preservation fix regressed. Before this, the
fix had zero coverage under any required gate: sign-helper.ts lives under
test/integration/, outside the unit glob; run.test.ts's
assertSuccessOrKnownError swallows the resulting "Failed to submit
transaction" as an indistinguishable known error; and the E2E's
hash-equality check can't catch it either, since dropping witnesses never
changes the transaction body hash - only the ledger tells the two apart,
and this test never talks to it. The new test builds a synthetic,
fully-offline script-bearing transaction directly via CML (one input/
output, a witness set carrying one PlutusV3 script, one map-form redeemer,
one datum), signs it with a public, well-known, never-funded BIP39 test
vector (computed from the installed bip39 package rather than transcribed
from memory - the first guess had the wrong checksum word and failed
loudly), and asserts every witness category plus the vkey witness survive
signing with the body hash unchanged. Proved it's load-bearing: reverted
the fix to the buggy from-scratch witness set, watched the test fail
naming the exact lost witness, reverted back (byte-identical, zero diff),
re-ran green.

registry-keyless.test.ts: the non-owner-update assertion drops its second
regex alternative (`Failed to build agent update`), which was a superset
match able to hide a real, unrelated build failure behind the same
"ownership was correctly refused" label. Now asserts the specific
`Ownership check failed` message only.

run.test.ts: buildSignSubmit now hard-asserts `Transaction Submitted` on
the submit leg when walletHasAda is true, instead of routing through the
tolerant assertSuccessOrKnownError - on a funded wallet a submit failure
is a real regression, not indistinguishable from an unfunded wallet, and
should fail the test rather than pass silently as a "known error". The
graceful path is kept for the unfunded case. Also documents, in a comment,
the known/accepted risk that a register-succeeds/deregister-fails run in
this file leaves the 10 AP3X test deposit locked on-chain (same shape as
the pre-keyless suite).

Hermetic only this round, per instruction: no live network calls, no
transaction submissions, no real secrets. Full account in
.superpowers/sdd/task-5-report.md's Addendum section.

Gates: typecheck 0, unit 351/351 (350 + 1 new), smoke 26/26, auth 12/12,
boundary 4/4 (confirms the new test's mnemonic constant sits outside the
custody scan by design, same as the rest of test/, and is labeled
TEST_ONLY_PUBLIC_MNEMONIC regardless per public-repo hygiene).

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

96 lines
4.9 KiB
TypeScript

import { describe, test, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { CML } from '@lucid-evolution/lucid';
import { startServer, stopServer, callTool, ServerContext } from '../setup.ts';
// Tier-1 registry integration: LIVE testnet, ZERO secrets. Build-only - nothing
// is signed, nothing is submitted, no funds move. The register build runs the
// REAL registry validator server-side during .complete().
const OWN_ADDRESS = 'addr1qylasu4y34ccwt8hv55tkswa9fthtck9xtppvc4y03kwhaztd0jlxdzk72zj8mgy6x4269v9gytkgffh3k8d3l8987yqu5c35z';
let ctx: ServerContext;
before(async () => { ctx = await startServer(); });
after(async () => { await stopServer(ctx); });
function extractCbor(text: string): string {
const m = text.match(/```\n([0-9a-f]+)\n```/);
assert.ok(m, `no CBOR block in response:\n${text.slice(0, 500)}`);
return m![1];
}
function assertBuildNotSubmitted(text: string) {
assert.doesNotMatch(text, /(?<!Not )Submitted\b|Transaction Submitted/);
assert.match(text, /Unsigned|NOT been submitted|Not Submitted/i);
}
describe('registry keyless tier-1 (live testnet, no secrets)', () => {
let liveDid: string | null = null;
test('discover returns live agents and yields a DID to work with', { timeout: 120_000 }, async () => {
const text = await callTool(ctx.client, 'vector_discover_agents', { limit: 3 });
const m = text.match(/(did:vector:agent:[a-f0-9]+:[a-f0-9]+)/);
assert.ok(m, 'no DID found in discovery output - is the testnet registry empty?');
liveDid = m![1];
});
test('build_register_agent builds an evaluable unsigned registration', { timeout: 120_000 }, async () => {
const text = await callTool(ctx.client, 'vector_build_register_agent', {
changeAddress: OWN_ADDRESS, name: `Tier1Agent-${process.pid}`,
description: 'tier-1 build-only agent', capabilities: ['testing'], framework: 'custom', endpoint: '',
});
assertBuildNotSubmitted(text);
// Brief specified /Agent DID:\s*did:vector:agent:/ - the live response is
// Markdown ("**Agent DID:** did:vector:agent:..."), so two literal '*'
// sit between the label and the value that \s* alone doesn't cover.
// Verified against the actual tier-1 live response (register-agent build
// succeeded; only this assertion's regex was too strict) before widening
// it, per Task 5's task-5-report.md self-review.
assert.match(text, /Agent DID:\*{0,2}\s*did:vector:agent:/);
const tx = CML.Transaction.from_cbor_hex(extractCbor(text));
const vkeys = tx.witness_set().vkeywitnesses();
assert.ok(vkeys === undefined || vkeys.len() === 0, 'register build must be unsigned');
assert.ok(tx.body().mint(), 'register tx has no mint field');
});
test('build_message_agent builds against a live agent', { timeout: 120_000 }, async () => {
assert.ok(liveDid, 'needs the DID from discovery');
const text = await callTool(ctx.client, 'vector_build_message_agent', {
changeAddress: OWN_ADDRESS, agent_id: liveDid!, message_type: 'inquiry', payload: 'tier-1 build-only ping',
});
assertBuildNotSubmitted(text);
const tx = CML.Transaction.from_cbor_hex(extractCbor(text));
const aux = tx.auxiliary_data();
assert.ok(aux && aux.metadata() && aux.metadata()!.get(674n), 'label-674 metadata missing');
});
test('get_agent_profile resolves the live agent', { timeout: 120_000 }, async () => {
assert.ok(liveDid);
const text = await callTool(ctx.client, 'vector_get_agent_profile', { agent_id: liveDid! });
assert.match(text, /Agent Profile/);
assert.match(text, /Registry UTxO:/);
});
test('a bogus changeAddress fails fast with a clear validation error', { timeout: 30_000 }, async () => {
const t0 = Date.now();
const text = await callTool(ctx.client, 'vector_build_register_agent', {
changeAddress: 'addr1garbage', name: 'X', description: '', capabilities: [], framework: 'c', endpoint: '',
});
assert.match(text, /Invalid change address/);
assert.ok(Date.now() - t0 < 5_000, 'validation should not have hit the network');
});
test('non-owner update is refused at build time', { timeout: 120_000 }, async () => {
assert.ok(liveDid);
const text = await callTool(ctx.client, 'vector_build_update_agent', {
changeAddress: OWN_ADDRESS, agent_id: liveDid!, description: 'not mine',
});
// The discovered agent belongs to another wallet (verified during planning:
// live datum owners differ from OWN_ADDRESS's key). Expect the ownership
// fail-fast specifically, not the broader "Failed to build agent update"
// wrapper it's nested inside - that second alternative would also match
// any OTHER build failure (a real regression), so it isn't a safe
// stand-in for "ownership was checked and correctly refused." Reviewer-
// confirmed against today's live response; not re-run live for this change.
assert.match(text, /Ownership check failed/);
});
});