File size: 5,426 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
137
138
139
140
141
// Copyright 2026 SZL Holdings
// SPDX-License-Identifier: Apache-2.0
//
// a11oyClient.test.ts — integration tests for the operator-console data layer.
// Boots a real node:http server that mimics a11oy's `serve` route surface and
// exercises every A11oyClient method over real TCP via Node's global fetch.
// No mocked transport: the request crosses a socket and the real JSON response
// is parsed back. This proves the 5 console routes' backing calls work against
// the a11oy serve contract.
//
// Run: node --experimental-strip-types src/a11oyClient.test.ts
//
// Authored for SZL Holdings. Signed-off per repository DCO.

import * as http from "node:http";
import { test } from "node:test";
import assert from "node:assert/strict";
import { A11oyClient, CONSOLE_ROUTES } from "./a11oyClient.ts";

function bootA11oyLike(): Promise<{ base: string; close: () => Promise<void> }> {
  const server = http.createServer((req, res) => {
    const url = new URL(req.url ?? "/", "http://localhost");
    const chunks: Buffer[] = [];
    req.on("data", (c) => chunks.push(c));
    req.on("end", () => {
      const reply = (status: number, body: unknown) => {
        res.writeHead(status, { "content-type": "application/json" });
        res.end(JSON.stringify(body));
      };
      if (req.method === "GET" && url.pathname === "/healthz") {
        return reply(200, { status: "ok", sha: "abc123", ts: "2026-05-31T00:00:00Z" });
      }
      if (req.method === "GET" && url.pathname === "/readyz") {
        return reply(200, { status: "ready", ledger: "/tmp/x.jsonl" });
      }
      if (req.method === "GET" && url.pathname === "/v1/ledger") {
        const limit = Number(url.searchParams.get("limit") ?? "20");
        return reply(200, { count: 1, total: 1, receipts: [{ receipt_id: "r-1", merkle_root: "m-1", limit }] });
      }
      if (req.method === "GET" && url.pathname.startsWith("/v1/ledger/")) {
        const hash = decodeURIComponent(url.pathname.slice("/v1/ledger/".length));
        return reply(200, { receipt: { receipt_id: hash } });
      }
      if (req.method === "POST" && url.pathname === "/v1/verify") {
        const body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
        const valid = Array.isArray(body.ledger) && body.ledger.length > 0;
        return reply(200, { valid, broken_at: valid ? null : 0, errors: valid ? [] : ["position 0: empty"] });
      }
      if (req.method === "POST" && url.pathname === "/v1/policy/evaluate") {
        const body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
        const sev = body.action?.severity ?? "medium";
        const allow = sev === "low" || sev === "medium";
        return reply(200, {
          decision: allow ? "allow" : "deny",
          gate: "thresholdPolicySeverity",
          receipt_hash: allow ? "sig-xyz" : "",
          rationale: `severity=${sev}`,
        });
      }
      reply(404, { error: "not found", path: url.pathname });
    });
  });
  return new Promise((resolve) => {
    server.listen(0, "127.0.0.1", () => {
      const addr = server.address();
      const port = typeof addr === "object" && addr ? addr.port : 0;
      resolve({ base: `http://127.0.0.1:${port}`, close: () => new Promise<void>((r) => server.close(() => r())) });
    });
  });
}

test("there are exactly 5 console routes, each citing its a11oy source endpoint", () => {
  assert.equal(CONSOLE_ROUTES.length, 5);
  for (const r of CONSOLE_ROUTES) assert.ok(r.source.length > 0);
});

test("health + readiness routes read /healthz and /readyz", async () => {
  const srv = await bootA11oyLike();
  try {
    const c = new A11oyClient(srv.base);
    const h = await c.health();
    assert.equal(h.status, "ok");
    assert.equal(h.sha, "abc123");
    const r = await c.readiness();
    assert.equal(r.status, "ready");
  } finally {
    await srv.close();
  }
});

test("proof-ledger route reads /v1/ledger with the limit", async () => {
  const srv = await bootA11oyLike();
  try {
    const c = new A11oyClient(srv.base);
    const page = await c.ledger(5);
    assert.equal(page.count, 1);
    assert.equal(page.receipts[0].limit, 5);
  } finally {
    await srv.close();
  }
});

test("receipt route reads /v1/ledger/{hash}", async () => {
  const srv = await bootA11oyLike();
  try {
    const c = new A11oyClient(srv.base);
    const out = await c.receipt("m-1");
    assert.equal(out.receipt.receipt_id, "m-1");
  } finally {
    await srv.close();
  }
});

test("verify route POSTs the ledger and returns the real verdict", async () => {
  const srv = await bootA11oyLike();
  try {
    const c = new A11oyClient(srv.base);
    const ok = await c.verify([{ receipt_id: "r-1" }]);
    assert.equal(ok.valid, true);
    const empty = await c.verify([]);
    assert.equal(empty.valid, false);
    assert.equal(empty.broken_at, 0);
  } finally {
    await srv.close();
  }
});

test("policy route POSTs the action and returns allow/deny", async () => {
  const srv = await bootA11oyLike();
  try {
    const c = new A11oyClient(srv.base);
    const allow = await c.evaluatePolicy({ actionId: "a", severity: "low", confidence: 0.9 });
    assert.equal(allow.decision, "allow");
    assert.equal(allow.gate, "thresholdPolicySeverity");
    const deny = await c.evaluatePolicy({ actionId: "b", severity: "critical" });
    assert.equal(deny.decision, "deny");
  } finally {
    await srv.close();
  }
});