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>
96 lines
4.9 KiB
TypeScript
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/);
|
|
});
|
|
});
|