Files
agents-marketplace/tests/fixtures/supplier-side/advert-datum-builders.ts
david 2aa1688a17 fix(ci): green the test suite — pnpm 10 build scripts, libsodium ESM, stale time fixtures
CI has been red since the pnpm 10 migration. Four independent causes:

- better-sqlite3: pnpm 10 blocks dependency build scripts by default, so the
  native binding was never compiled on CI. Allowlist it via
  onlyBuiltDependencies in pnpm-workspace.yaml.
- libsodium-wrappers-sumo 0.7.16: the published ESM wrapper does
  `import "./libsodium-sumo.mjs"` but the tarball does not ship that file
  (it lives in the libsodium-sumo package). Patch the import to the bare
  specifier; local runs only worked because of a hand-copied file in
  node_modules.
- tx-accept/claim [live] tests: the live path checks deadlines against
  Date.now(), and the fixed fixture timestamps expired in June. Freeze the
  clock inside the fixture windows with vi.useFakeTimers.
- CML set_exunits panic ("RuntimeError: unreachable"): with
  localUPLCEval: false the provider asks Ogmios for ex-units; the test mocks
  answered `{}`, so the provider fell back to a spend redeemer at index 0
  while the escrow input sorts to index 1 — CML panics on the unknown
  redeemer key. Mock evaluateTransaction with the true index like real
  Ogmios.

Also regenerate the advert-datum golden: the fixture builders passed plain JS
numbers to cbor-x (minimal ints, float64 for the timestamp) while the
production codec — whose output is live on mainnet — emits full-width uint64.
Builders now encode integer fields as BigInt so builders, codec, and golden
agree byte-for-byte.

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

133 lines
5.0 KiB
TypeScript

/**
* Supplier-side AdvertDatum fixture builder — M0-B RED phase
*
* Constructs AdvertDatum INDEPENDENTLY from the spec (ARCHITECTURE.md §4.1).
* MUST NOT import from buyer-side builders or any shared fixture helper.
* This is the "no same source of truth" discipline from ARCHITECTURE.md §7.2.
*
* Golden file consumer: This side reads tests/fixtures/golden/advert-datum-sample.hex
* (produced by buyer-side) and independently encodes the same datum to assert hex equality.
* If the two sides diverge, there is a spec interpretation error in one of them.
*
* cbor-x Plutus Tag convention (from apex-dashboard cbor-decoder.ts):
* new Tag(tagNumber, fieldsArray) — tagNumber in .value, fields in .tag
* This is the INVERSE of standard cbor-x but matches Plutus on-chain encoding.
*/
import { Tag } from "cbor-x";
import { readFileSync } from "fs";
import type { AdvertDatum } from "../../../packages/shared/src/cbor/types.js";
// NOTE: Shared low-level CBOR encoder. The "no shared source of truth" rule
// (ARCHITECTURE.md §7.2) applies to fixture-builder LOGIC — datum construction
// from spec — not to byte-level CBOR machinery (tagUint8Array=false + Plutus
// Tag extension). Both sides must agree at the byte level or hex equality is
// meaningless.
import { encodePlutus } from "../../../packages/shared/src/cbor/plutus-encoder.js";
// ─── Helpers (no import from buyer-side) ────────────────────────────────────
function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
}
return bytes;
}
function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
function plutusTag(tagNumber: number, value: unknown): Tag {
return new (Tag as unknown as new (a: unknown, b: unknown) => Tag)(
tagNumber,
value,
);
}
function utf8Bytes(s: string): Uint8Array {
return new TextEncoder().encode(s);
}
// ─── Sample datum values — derived independently from ARCHITECTURE.md §4.1 ──
// These values are identical to buyer-side because BOTH derive from the same spec,
// not because they share any code or import.
const SAMPLE_SUPPLIER_PKH = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef01";
const SAMPLE_CAPABILITY_ID = "llm.text.generate.v1";
const SAMPLE_MODEL = "qwen2.5:0.5b";
const SAMPLE_MAX_OUTPUT_TOKENS = 512;
const SAMPLE_MAX_PROCESSING_MS = 60_000;
const SAMPLE_PRICE_LOVELACE = 2_000_000n;
const SAMPLE_SUPPLIER_BOND_LOVELACE = 1_000_000n;
const SAMPLE_BUYER_BOND_LOVELACE = 1_000_000n;
const SAMPLE_ENDPOINT_URL = "https://supplier.example.com/v1";
const SAMPLE_DETAIL_URI = "ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG";
const SAMPLE_DETAIL_HASH = "a".repeat(64);
const SAMPLE_ADVERTISED_AT = 1745500000000;
const SAMPLE_STATUS: "Active" = "Active";
/**
* buildSampleAdvertDatum — returns the canonical sample AdvertDatum.
* Derived independently from ARCHITECTURE.md §4.1; shares NO code with buyer-side.
*/
export function buildSampleAdvertDatum(): AdvertDatum {
return {
supplier_pkh: SAMPLE_SUPPLIER_PKH,
capability_id: SAMPLE_CAPABILITY_ID,
model: SAMPLE_MODEL,
max_output_tokens: SAMPLE_MAX_OUTPUT_TOKENS,
max_processing_ms: SAMPLE_MAX_PROCESSING_MS,
price_lovelace: SAMPLE_PRICE_LOVELACE,
supplier_bond_lovelace: SAMPLE_SUPPLIER_BOND_LOVELACE,
buyer_bond_lovelace: SAMPLE_BUYER_BOND_LOVELACE,
endpoint_url: SAMPLE_ENDPOINT_URL,
detail_uri: SAMPLE_DETAIL_URI,
detail_hash: SAMPLE_DETAIL_HASH,
advertised_at: SAMPLE_ADVERTISED_AT,
status: SAMPLE_STATUS,
};
}
/**
* encodeSampleAdvertDatumHex — CBOR-encodes the sample datum using cbor-x.
*
* Field order per §4.1 (13 fields) — written independently from buyer-side.
* If this produces a different hex than buyer-side, one side has misread the spec.
*/
export function encodeSampleAdvertDatumHex(): string {
const datum = buildSampleAdvertDatum();
const statusTag =
datum.status === "Active" ? plutusTag(121, []) : plutusTag(122, []);
const cborDatum = plutusTag(121, [
hexToBytes(datum.supplier_pkh),
utf8Bytes(datum.capability_id),
utf8Bytes(datum.model),
// BigInt so cbor-x emits uint64 (0x1b) like the production codec — plain
// numbers encode minimally (or as float64 past 2^32) and break golden parity.
BigInt(datum.max_output_tokens),
BigInt(datum.max_processing_ms),
datum.price_lovelace,
datum.supplier_bond_lovelace,
datum.buyer_bond_lovelace,
utf8Bytes(datum.endpoint_url),
utf8Bytes(datum.detail_uri),
hexToBytes(datum.detail_hash),
BigInt(datum.advertised_at),
statusTag,
]);
return bytesToHex(encodePlutus(cborDatum));
}
/**
* readGoldenAdvertDatum — reads the buyer-side golden hex from file.
*/
export function readGoldenAdvertDatum(goldenPath: string): string {
return readFileSync(goldenPath, "utf8").trim();
}