File size: 4,682 Bytes
518343a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#!/usr/bin/env node
// Copyright 2026 SZL Holdings
// SPDX-License-Identifier: Apache-2.0
//
// selftest.ts — in-process boot self-test for the receipt-substrate CLI image.
//
// The a11oy container is a CLI image, not a long-running network service, so
// it has no HTTP readiness probe. Without a self-test, a built image can ship
// with a broken or partially-copied dist/ and still "start" (the entrypoint
// prints --help and exits 0), giving a misleading green signal.
//
// This self-test exercises the real receipt path end to end against an
// ephemeral temp file: it emits a genesis receipt, appends a second receipt,
// reads the chain back from disk, and verifies the hash chain. It writes
// solely to a unique temp file under the OS temp dir, makes no network calls,
// and cleans up after itself. A non-zero exit means the bundled substrate is
// not functional in this image.
//
// Authored for SZL Holdings. Signed-off per repository DCO.

import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import {
  appendReceiptJsonl,
  createToolEnvelope,
  emitReceipt,
  readReceiptJsonl,
  verifyChain,
  type OperationalReceipt,
} from "./index.ts";

interface SelfTestResult {
  ok: boolean;
  checks: { name: string; ok: boolean; detail?: string }[];
}

/**
 * Run the receipt-substrate boot self-test.
 *
 * Returns a structured result. Does not throw on a failed check; instead each
 * failure is recorded so the caller can report all problems at once.
 */
export function runSelfTest(): SelfTestResult {
  const checks: SelfTestResult["checks"] = [];
  const record = (name: string, ok: boolean, detail?: string) => {
    checks.push({ name, ok, detail });
  };

  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "a11oy-selftest-"));
  const out = path.join(dir, "selftest-receipts.jsonl");

  const policy = {
    algorithm: "SHA3-256" as const,
    chaining: "hash_chain" as const,
    quorum: "1-of-1",
    nodes: ["selftest-node"],
  };

  const mkEnvelope = (seq: number) =>
    createToolEnvelope({
      protocol: "mcp",
      actor_id: "did:example:selftest",
      tool_name: "receipted_retrieval",
      lambda_axes: ["Λ7"],
      payload: { selftest: true, seq },
      metadata: { source: "a11oy-receipt-substrate-selftest" },
    });

  try {
    // 1. Emit a genesis receipt and persist it.
    const genesis = emitReceipt(mkEnvelope(0), { policy });
    appendReceiptJsonl(out, genesis);
    record("emit-genesis", true);

    // 2. Append a second receipt chained to the genesis.
    const second = emitReceipt(mkEnvelope(1), { previousReceipt: genesis, policy });
    appendReceiptJsonl(out, second);
    record("append-chained", true);

    // 3. Read the chain back from disk (round-trip through JSONL).
    const chain: OperationalReceipt[] = readReceiptJsonl(out);
    const readBackOk = chain.length === 2;
    record("read-back", readBackOk, readBackOk ? undefined : `expected 2 receipts, found ${chain.length}`);

    // 4. Verify the hash chain.
    const verification = verifyChain(chain, { quorum: "1-of-1", nodes: ["selftest-node"] });
    record(
      "verify-chain",
      verification.valid,
      verification.valid ? undefined : verification.errors.join("; "),
    );

    // 5. Negative control: a tampered chain must fail verification. This
    //    guards against a verifier that always returns valid.
    const tampered: OperationalReceipt[] = [
      { ...chain[0] },
      { ...chain[1], merkle_root: "0".repeat(chain[1].merkle_root.length) },
    ];
    const tamperedResult = verifyChain(tampered, { quorum: "1-of-1", nodes: ["selftest-node"] });
    record(
      "reject-tampered",
      tamperedResult.valid === false,
      tamperedResult.valid ? "tampered chain was incorrectly accepted" : undefined,
    );
  } catch (error) {
    record("unexpected-error", false, (error as Error).message);
  } finally {
    fs.rmSync(dir, { recursive: true, force: true });
  }

  return { ok: checks.every((c) => c.ok), checks };
}

function main(): void {
  const result = runSelfTest();
  for (const c of result.checks) {
    const icon = c.ok ? "ok  " : "FAIL";
    const detail = c.detail ? `  (${c.detail})` : "";
    console.log(`  [${icon}] ${c.name}${detail}`);
  }
  if (result.ok) {
    console.log("a11oy receipt-substrate self-test: PASS");
    process.exit(0);
  }
  console.error("a11oy receipt-substrate self-test: FAIL");
  process.exit(1);
}

// Run when invoked directly (node src/selftest.ts), not when imported.
const invokedDirectly =
  process.argv[1] !== undefined && path.resolve(process.argv[1]).endsWith("selftest.ts");
if (invokedDirectly) {
  main();
}