diff --git a/packages/builder/test/boundary.test.ts b/packages/builder/test/boundary.test.ts index d8d30de..a7f3ec3 100644 --- a/packages/builder/test/boundary.test.ts +++ b/packages/builder/test/boundary.test.ts @@ -12,6 +12,7 @@ import { resolve, join, relative } from 'node:path'; * non-custodial split; growing it should be impossible to do accidentally. */ const SRC_ROOT = resolve(import.meta.dirname!, '../src'); +const SCRIPTS_ROOT = resolve(import.meta.dirname!, '../scripts'); const CUSTODIAL_UNTIL_LATER_PR = new Set([ 'vector/agent-network.ts', // family 2 — goes keyless in spec PR 7 'vector/self-improvement.ts', // family 3 — goes keyless in spec PR 8 @@ -46,6 +47,16 @@ describe('builder custody boundary', () => { if (FORBIDDEN.test(line)) offenders.push(`${rel}:${i + 1}: ${line.trim()}`); }); } + // scripts/ is dev-tooling, not shipped builder source - it has no + // custodial carve-out, so nothing here is ever checked against the + // allowlist (unlike SRC_ROOT files above). + for (const file of sourceFiles(SCRIPTS_ROOT)) { + const rel = relative(SCRIPTS_ROOT, file).replace(/\\/g, '/'); + const lines = readFileSync(file, 'utf-8').split('\n'); + lines.forEach((line, i) => { + if (FORBIDDEN.test(line)) offenders.push(`scripts/${rel}:${i + 1}: ${line.trim()}`); + }); + } assert.deepEqual(offenders, [], `key-material vocabulary found in keyless builder source:\n${offenders.join('\n')}`); }); diff --git a/packages/builder/test/build-contracts.test.ts b/packages/builder/test/build-contracts.test.ts index e41a579..2e7ce7e 100644 --- a/packages/builder/test/build-contracts.test.ts +++ b/packages/builder/test/build-contracts.test.ts @@ -66,6 +66,14 @@ describe('buildDeployContract', () => { const vkeys = CML.Transaction.from_cbor_hex(r.txCbor).witness_set().vkeywitnesses(); assert.ok(vkeys === undefined || vkeys.len() === 0); }); + + test("deploy with initialDatum '' fails loudly instead of silently voiding", async () => { + // '' is falsy but caller-supplied; ?? must let it flow through and fail + // loudly downstream rather than defaulting to Data.void(). + await assert.rejects(buildDeployContract(lucid, { + scriptCbor: ALWAYS_SUCCEEDS_V2, scriptType: 'PlutusV2', initialDatum: '', + })); + }); }); describe('buildInteractContract', () => { @@ -89,6 +97,24 @@ describe('buildInteractContract', () => { })); }); + test('lock datum is inline (kind), not a hash reference', async () => { + const r = await buildInteractContract(lucid, { + scriptCbor: ALWAYS_SUCCEEDS_V2, scriptType: 'PlutusV2', action: 'lock', + changeAddress: OWN_ADDRESS, lovelaceAmount: 2_000_000, datum: 'd87980', + }); + const tx = CML.Transaction.from_cbor_hex(r.txCbor); + const outs = tx.body().outputs(); + let inlineSeen = false; + for (let i = 0; i < outs.len(); i++) { + const d = outs.get(i).datum(); + // CML DatumOptionKind: Hash = 0, Datum = 1 (verified against the + // package's own .d.ts) - kind() === 1 is an inline datum. + if (d && d.kind() === 1) inlineSeen = true; + if (d && d.kind() === 0) assert.fail('lock produced a datum HASH output - must be inline'); + } + assert.ok(inlineSeen, 'no inline-datum output found on the lock tx'); + }); + test('rejects an invalid action', async () => { await assert.rejects( () => buildInteractContract(lucid, { @@ -172,4 +198,19 @@ describe('buildInteractContract', () => { /not at script address/, ); }); + + test("spend: rejects an empty-string redeemer instead of silently treating it as void", async () => { + // '' is falsy but caller-supplied; ?? must let it flow through and fail + // loudly downstream rather than defaulting to Data.void(). + const scriptUtxo: UTxO = { + txHash: FIXTURE_TXHASH, outputIndex: 7, address: SCRIPT_ADDRESS, + assets: { lovelace: 2_000_000n }, datum: 'd87980', + }; + const withScript = new FixtureProvider([...FIXTURE_UTXOS, scriptUtxo]); + const l = await lucidForAddress(withScript, OWN_ADDRESS); + await assert.rejects(buildInteractContract(l, { + scriptCbor: ALWAYS_SUCCEEDS_V2, scriptType: 'PlutusV2', action: 'spend', + changeAddress: OWN_ADDRESS, redeemer: '', + })); + }); });