@aikoql/client 0.0.0-stage → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/tools.ts ADDED
@@ -0,0 +1,72 @@
1
+ // Typed parameter/result shapes for the canonical Database API surface
2
+ // over the MCP tools. The server's tool registry is the schema source
3
+ // (crates/sdk/go/tools.go mirrors it); everything is snake_case on the
4
+ // wire, matching the frozen vectors.
5
+
6
+ /** Establishes session identity (MRFC-0040). */
7
+ export interface SessionParams {
8
+ agent_id?: string;
9
+ run_id?: string;
10
+ tenant?: string;
11
+ roles?: string[];
12
+ }
13
+
14
+ /** One knowledge-object commit (create or new version). */
15
+ export interface RememberParams {
16
+ subject?: string;
17
+ type_name: string;
18
+ koid?: string;
19
+ properties?: Record<string, unknown>;
20
+ note?: string;
21
+ idempotency_key?: string;
22
+ embed?: boolean;
23
+ expected_version?: number;
24
+ retention_ms?: number;
25
+ semantic?: Record<string, unknown>;
26
+ }
27
+
28
+ /** The server's answer to remember. */
29
+ export interface Remembered {
30
+ koid: string;
31
+ version: number;
32
+ commit_ts: number;
33
+ /** P3-008: per-op journal marker; 0 on servers that predate it. */
34
+ journal_seq?: number;
35
+ }
36
+
37
+ /** A fetched KO (the wire shape — the canonical subset of the kernel's). */
38
+ export interface KnowledgeObject {
39
+ koid: string;
40
+ version: number;
41
+ state?: string;
42
+ properties: Record<string, unknown>;
43
+ }
44
+
45
+ /** A hybrid recall query (vector + text + filters, RRF/weighted fusion). */
46
+ export interface FindSimilarParams {
47
+ subject?: string;
48
+ text?: string;
49
+ vector?: number[];
50
+ type_name?: string;
51
+ k?: number;
52
+ fusion?: string;
53
+ embedding_model?: string;
54
+ wait_for_freshness_ms?: number;
55
+ }
56
+
57
+ /** One recall hit. */
58
+ export interface ScoredKO {
59
+ koid: string;
60
+ score: number;
61
+ type_name: string;
62
+ }
63
+
64
+ /** The server's metrics payload. */
65
+ export interface Metrics {
66
+ journal_seq: number;
67
+ total_objects: number;
68
+ active_objects: number;
69
+ uptime_seconds: number;
70
+ by_lifecycle?: Record<string, unknown>;
71
+ by_type?: Record<string, unknown>;
72
+ }
package/src/tx.ts ADDED
@@ -0,0 +1,83 @@
1
+ // Tx implements the §3.5 transaction handle: the txn_id is a first-class
2
+ // field here — it never leaks as a bare tool argument that a caller
3
+ // threads between call sites. The four txn_* tools are reachable only
4
+ // through Tx methods. Mirrors the Go SDK's transactions.go and the Rust
5
+ // reference (crates/sdk/rust/src/tx.rs).
6
+
7
+ import type { Client } from "./client.ts";
8
+ import { McpError } from "./error.ts";
9
+
10
+ /** One staged write (the txn_stage op shape). */
11
+ export interface StagedOp {
12
+ action: string;
13
+ type_name?: string;
14
+ koid?: string;
15
+ properties?: Record<string, unknown>;
16
+ }
17
+
18
+ /** The txn_commit outcome: the staged write results and whether this
19
+ * commit was a retry of an already-committed txn_id. */
20
+ export interface CommitResult {
21
+ results?: unknown[];
22
+ deduped?: boolean;
23
+ }
24
+
25
+ /**
26
+ * A staged write handle: begin on the Client, execute stages ops, commit
27
+ * or rollback closes it. A closed handle refuses further use with
28
+ * INVALID_ARGUMENT.
29
+ */
30
+ export class Tx {
31
+ private c: Client;
32
+ private txId: string;
33
+ private closed = false;
34
+
35
+ constructor(c: Client, txnId: string) {
36
+ this.c = c;
37
+ this.txId = txnId;
38
+ }
39
+
40
+ /** The txn_id (for retries — commit dedupes by it, P5-M20). */
41
+ id(): string {
42
+ return this.txId;
43
+ }
44
+
45
+ /** True once commit or rollback closed the handle (the pool's session
46
+ * reset checks this). */
47
+ done(): boolean {
48
+ return this.closed;
49
+ }
50
+
51
+ /** Stages one write. */
52
+ async execute(op: StagedOp, opts: { signal?: AbortSignal } = {}): Promise<void> {
53
+ await this.step("txn_stage", { txn_id: this.txId, op }, opts);
54
+ }
55
+
56
+ /** Applies the staged writes and closes the handle. A commit that errors
57
+ * leaves the handle open: the server dedupes by txn_id, so the caller
58
+ * may retry commit or begin with the same id (P5-M20). */
59
+ async commit(opts: { signal?: AbortSignal } = {}): Promise<CommitResult> {
60
+ const raw = await this.step("txn_commit", { txn_id: this.txId }, opts);
61
+ return (raw ?? {}) as CommitResult;
62
+ }
63
+
64
+ /** Discards the staged writes and closes the handle. */
65
+ async rollback(opts: { signal?: AbortSignal } = {}): Promise<void> {
66
+ await this.step("txn_rollback", { txn_id: this.txId }, opts);
67
+ }
68
+
69
+ private async step(
70
+ name: string,
71
+ args: Record<string, unknown>,
72
+ opts: { signal?: AbortSignal },
73
+ ): Promise<unknown> {
74
+ if (this.closed) {
75
+ throw McpError.invalidArgument(`transaction ${this.txId} is closed`);
76
+ }
77
+ const raw = await this.c.callTool(name, args, opts);
78
+ if (name === "txn_commit" || name === "txn_rollback") {
79
+ this.closed = true;
80
+ }
81
+ return raw;
82
+ }
83
+ }
@@ -0,0 +1,60 @@
1
+ // D-13: the shared conformance runner, typescript arm (§7, §23). This
2
+ // test pins only the CLI contract — the vectors carry the semantics.
3
+ // Mirrors crates/sdk/rust/tests/conformance.rs.
4
+
5
+ import assert from "node:assert/strict";
6
+ import { execFile } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+ import path from "node:path";
9
+ import test from "node:test";
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+
13
+ // Native→"bash" spawns the WSL shim on Windows (its PATH has no cargo/
14
+ // node); where.exe is a plain PATH walk and finds Git's bash. Prefer its
15
+ // Git hit; plain "bash" elsewhere (CI Linux).
16
+ async function bashExe(): Promise<string> {
17
+ if (process.platform !== "win32") return "bash";
18
+ try {
19
+ const { stdout } = await new Promise<{ stdout: string }>((resolve, reject) =>
20
+ execFile("where.exe", ["bash"], { encoding: "utf8" }, (err, stdout) =>
21
+ err ? reject(err) : resolve({ stdout }),
22
+ ),
23
+ );
24
+ const hits = stdout
25
+ .split(/\r?\n/)
26
+ .map((l) => l.trim())
27
+ .filter(Boolean);
28
+ return hits.find((h) => h.toLowerCase().includes("git")) ?? hits[0] ?? "bash";
29
+ } catch {
30
+ return "bash";
31
+ }
32
+ }
33
+
34
+ function run(args: string[], env: Record<string, string>): Promise<{ code: number; out: string }> {
35
+ return new Promise((resolve, reject) => {
36
+ execFile(args[0]!, args.slice(1), { encoding: "utf8", env }, (err, stdout, stderr) => {
37
+ if (err && (err as { code?: number }).code === undefined) reject(err);
38
+ else resolve({ code: (err as { code?: number } | null)?.code ?? 0, out: `${stdout}${stderr}` });
39
+ });
40
+ });
41
+ }
42
+
43
+ test("sdk-conformance --language typescript", async () => {
44
+ const script = path.resolve(__dirname, "..", "..", "..", "..", "scripts", "sdk-conformance.sh");
45
+ // crates/sdk/typescript/tests → repo root is four levels up.
46
+ assert.ok(
47
+ (await import("node:fs")).existsSync(script),
48
+ `sdk-conformance runner missing at ${script} — D-13 RED`,
49
+ );
50
+ const bin = process.env.AIKOQL_MCP_BIN;
51
+ if (!bin) {
52
+ process.stderr.write("AIKOQL_MCP_BIN not set — real-server conformance skipped\n");
53
+ return;
54
+ }
55
+ const { code, out } = await run(
56
+ [await bashExe(), script, "--language", "typescript"],
57
+ { ...process.env, AIKOQL_MCP_BIN: bin },
58
+ );
59
+ assert.equal(code, 0, `sdk-conformance --language typescript failed (exit ${code}):\n${out}`);
60
+ });
@@ -0,0 +1,96 @@
1
+ // The §16 pin: the cross-language golden corpus spec exists at
2
+ // sdk-fuzz-corpus/corpus.json and this SDK's column holds for every case.
3
+ // Removing a case id is a detected coverage loss; a column mismatch is a
4
+ // wire-behavior drift (or an undocumented divergence — document it in the
5
+ // spec's note and re-stamp). Real primitives run where they exist
6
+ // (parseVersion/classifyID/rpcError/streamNotify); the request verdicts
7
+ // are restated inline at their client.ts lines.
8
+ import { test } from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { readFileSync } from "node:fs";
11
+ import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { classifyID, parseVersion, rpcError, streamNotify } from "../src/client.ts";
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const CORPUS = path.resolve(__dirname, "..", "..", "..", "..", "sdk-fuzz-corpus", "corpus.json");
17
+
18
+ const CASE_IDS = [
19
+ "corp-v01", "corp-v02", "corp-v03", "corp-v04", "corp-v05",
20
+ "corp-e01", "corp-e02", "corp-e03", "corp-e04", "corp-e05",
21
+ "corp-r01", "corp-r02", "corp-n01",
22
+ "corp-s01", "corp-s02", "corp-s03",
23
+ "corp-c01", "corp-c02", "corp-c03", "corp-c04",
24
+ ];
25
+ const SURFACES = ["version", "rpc_error", "response_id", "nonfinite", "notify", "correlation"];
26
+
27
+ function load(): { cases: Array<{ id: string; surface: string; input: unknown; expected: Record<string, unknown> }> } {
28
+ let raw: string;
29
+ try {
30
+ raw = readFileSync(CORPUS, "utf8");
31
+ } catch (err) {
32
+ throw new Error(
33
+ `the §16 corpus is absent (${CORPUS}): the D-16 golden-corpus slice is missing — ${String(err)}`,
34
+ );
35
+ }
36
+ return JSON.parse(raw) as never;
37
+ }
38
+
39
+ function classify(surface: string, input: unknown): unknown {
40
+ switch (surface) {
41
+ case "version":
42
+ return parseVersion(input as string);
43
+ case "rpc_error": {
44
+ const frame = JSON.parse(input as string) as { error?: { code?: unknown; message?: string } };
45
+ // The real error renderer: rpcError + the McpError ctor decoration.
46
+ const e = rpcError(frame.error as { code: unknown; message: string });
47
+ return { code: e.code, message: e.message };
48
+ }
49
+ case "response_id":
50
+ case "nonfinite": {
51
+ // The request loop's verdict, restated (client.ts ~:300-315, want=1):
52
+ // non-numeric ids skip (JSON.parse throws on garbage → skip), and a
53
+ // numeric id goes through classifyID.
54
+ let frame: { id?: unknown };
55
+ try {
56
+ frame = JSON.parse(input as string) as { id?: unknown };
57
+ } catch {
58
+ return "skip";
59
+ }
60
+ const rid = frame.id;
61
+ if (typeof rid !== "number") return "skip";
62
+ return classifyID(1, rid);
63
+ }
64
+ case "notify": {
65
+ // The real primitive: streamNotify (client.ts) — the same verdict
66
+ // the aikoqlStream loop runs; ts yields the raw params object as-is.
67
+ const frame = JSON.parse(input as string) as Record<string, unknown>;
68
+ const p = streamNotify(frame, "s1");
69
+ if (p === null) return { verdict: "skip", pair: null };
70
+ return { verdict: "yield", pair: p };
71
+ }
72
+ case "correlation": {
73
+ const pair = input as { want: number; got: number };
74
+ return classifyID(pair.want, pair.got);
75
+ }
76
+ }
77
+ throw new Error(`unknown surface ${surface}`);
78
+ }
79
+
80
+ test("the §16 corpus pin", () => {
81
+ const spec = load();
82
+ const byId = new Map(spec.cases.map((c) => [c.id, c]));
83
+ for (const id of CASE_IDS) {
84
+ assert.ok(byId.has(id), `case ${id} is gone from the §16 corpus — a removed case is a coverage loss`);
85
+ }
86
+ const haveSurfaces = new Set(spec.cases.map((c) => c.surface));
87
+ for (const s of SURFACES) {
88
+ assert.ok(haveSurfaces.has(s), `surface ${s} has no cases in the §16 corpus`);
89
+ }
90
+ for (const c of spec.cases) {
91
+ assert.ok("typescript" in c.expected, `case ${c.id} has no typescript column in the §16 corpus`);
92
+ const got = classify(c.surface, c.input);
93
+ const want = c.expected.typescript;
94
+ assert.deepEqual(got, want, `case ${c.id} (${c.surface}): typescript column drift — got ${JSON.stringify(got)}, want ${JSON.stringify(want)}`);
95
+ }
96
+ });
@@ -0,0 +1,298 @@
1
+ // D-16 fault matrix (line wire): the §18 fault proxy sits between the SDK
2
+ // and a real server and mangles the newline-delimited JSON-RPC wire (§7 of
3
+ // the testing plan: every SDK passes the same matrix against the same
4
+ // misbehaving server). Frame accounting: client line #1 = initialize, #2 =
5
+ // the victim call, #3 = the follow-up (no HELLO/AUTH frames on the MCP
6
+ // wire). Mirrors crates/sdk/rust/tests/fault.rs on the line wire.
7
+ //
8
+ // The contracts GREEN must make hold (the proxy --wire line arm + the
9
+ // client's MAX_FRAME cap and closed latch):
10
+ // drop-request/drop-response → victim TIMEOUT (retryable), follow-up ok
11
+ // delay-response → tight deadline TIMEOUT; generous deadline ok
12
+ // duplicate-response → both ok (id correlation skips the duplicate)
13
+ // reorder-response → victim TIMEOUT (response held), follow-up ok
14
+ // truncate-frame → victim TIMEOUT (the missing bytes never arrive),
15
+ // follow-up ok (the wire is self-delimiting)
16
+ // corrupt-frame → the line is noise per the frozen §3.3 semantics →
17
+ // victim TIMEOUT, follow-up ok
18
+ // inject-notification → both ok (an id-less frame is never a response)
19
+ // inject-stale-response → both ok (stale id skipped)
20
+ // close/half-close → victim ok, follow-up fails fast (never TIMEOUT),
21
+ // the client latches closed → UNAVAILABLE from then on
22
+ // slow-server → tight deadline TIMEOUT
23
+ // oversized-response → FRAME_TOO_LARGE before buffering past the 1 MiB
24
+ // cap (§19: a malicious server cannot cause unbounded client memory),
25
+ // follow-up UNAVAILABLE (the stream is desynced — latched)
26
+
27
+ import assert from "node:assert/strict";
28
+ import fs from "node:fs";
29
+ import net from "node:net";
30
+ import os from "node:os";
31
+ import path from "node:path";
32
+ import { spawn, type ChildProcess } from "node:child_process";
33
+ import { test, type TestContext } from "node:test";
34
+ import { Client, McpError, withDeadline } from "../src/index.ts";
35
+
36
+ const TOKEN = "test-token";
37
+ // npm test / node --test run from the package dir → 3 ups = the repo root.
38
+ const ROOT = path.resolve(process.cwd(), "..", "..", "..");
39
+
40
+ function findBin(name: string): string | undefined {
41
+ const p = path.join(ROOT, "target", "debug", name + (process.platform === "win32" ? ".exe" : ""));
42
+ return fs.existsSync(p) ? p : undefined;
43
+ }
44
+
45
+ const MCP_BIN = process.env.AIKOQL_MCP_BIN ?? findBin("aikoql-mcp");
46
+ const PROXY_BIN = process.env.AIKOQL_FAULT_PROXY ?? findBin("aikoql-fault-proxy");
47
+
48
+ function requireBins(t: TestContext): boolean {
49
+ if (!MCP_BIN || !PROXY_BIN) {
50
+ t.skip("AIKOQL_MCP_BIN / aikoql-fault-proxy not built — real-server fault matrix skipped");
51
+ return false;
52
+ }
53
+ return true;
54
+ }
55
+
56
+ function probe(port: number): Promise<boolean> {
57
+ return new Promise((resolve) => {
58
+ const s = net.connect(port, "127.0.0.1");
59
+ s.once("connect", () => {
60
+ s.destroy();
61
+ resolve(true);
62
+ });
63
+ s.once("error", () => resolve(false));
64
+ });
65
+ }
66
+
67
+ async function waitUp(port: number, child: ChildProcess, errFile: string, what: string): Promise<void> {
68
+ for (let i = 0; i < 100; i++) {
69
+ if (child.exitCode !== null) {
70
+ const err = await fs.promises.readFile(errFile, "utf8").catch(() => "");
71
+ throw new Error(`${what} exited early (${child.exitCode}):\n${err}`);
72
+ }
73
+ if (await probe(port)) return;
74
+ await new Promise((r) => setTimeout(r, 100));
75
+ }
76
+ throw new Error(`${what} never listened on 127.0.0.1:${port}`);
77
+ }
78
+
79
+ /** A real server + one fault-proxy instance (one fault mode) behind the
80
+ * proxy address. The port-probe idiom races → 3 spawn attempts. */
81
+ async function faultEnv(
82
+ mode: string,
83
+ extra: string[],
84
+ ): Promise<{ addr: string; stop: () => void }> {
85
+ let last: Error | undefined;
86
+ for (let attempt = 0; attempt < 3; attempt++) {
87
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "aikoql-ts-fault-"));
88
+ const cleanup = async () => {
89
+ await fs.promises.rm(dir, { recursive: true, force: true }).catch(() => {});
90
+ };
91
+ let srv: ChildProcess | undefined;
92
+ let px: ChildProcess | undefined;
93
+ try {
94
+ const srvPort = await freePort();
95
+ const db = path.join(dir, "db.aikoql"); // does not exist → auto-create (aikoql-v2)
96
+ srv = spawn(MCP_BIN!, ["serve", db, "--listen", `127.0.0.1:${srvPort}`, "--tcp-token", `${TOKEN}::admin`], {
97
+ stdio: ["ignore", "ignore", fs.openSync(path.join(dir, "srv.err"), "w")],
98
+ windowsHide: true,
99
+ });
100
+ await waitUp(srvPort, srv, path.join(dir, "srv.err"), "aikoql-mcp");
101
+
102
+ const pxPort = await freePort();
103
+ const errFile = path.join(dir, "proxy.err");
104
+ px = spawn(
105
+ PROXY_BIN!,
106
+ ["--listen", `127.0.0.1:${pxPort}`, "--target", `127.0.0.1:${srvPort}`, "--wire", "line", "--mode", mode, ...extra],
107
+ { stdio: ["ignore", "ignore", fs.openSync(errFile, "w")], windowsHide: true },
108
+ );
109
+ await waitUp(pxPort, px, errFile, "aikoql-fault-proxy");
110
+ const addr = `127.0.0.1:${pxPort}`;
111
+ return {
112
+ addr,
113
+ stop: () => {
114
+ px!.kill();
115
+ srv!.kill();
116
+ void cleanup();
117
+ },
118
+ };
119
+ } catch (e) {
120
+ last = e as Error;
121
+ px?.kill();
122
+ srv?.kill();
123
+ await cleanup();
124
+ }
125
+ }
126
+ throw last;
127
+ }
128
+
129
+ async function freePort(): Promise<number> {
130
+ const probeSrv = net.createServer();
131
+ await new Promise<void>((resolve, reject) => {
132
+ probeSrv.once("error", reject);
133
+ probeSrv.listen(0, "127.0.0.1", resolve);
134
+ });
135
+ const port = (probeSrv.address() as net.AddressInfo).port;
136
+ await new Promise<void>((resolve) => probeSrv.close(() => resolve()));
137
+ return port;
138
+ }
139
+
140
+ async function dial(t: TestContext, addr: string): Promise<Client> {
141
+ const c = (await Client.dial(addr)).withToken(TOKEN);
142
+ t.after(() => {
143
+ void c.close().catch(() => {});
144
+ });
145
+ await c.initialize();
146
+ return c;
147
+ }
148
+
149
+ /** Runs fn under the deadline and returns the error code ("" = ok). */
150
+ async function codeOf(fn: (signal: AbortSignal) => Promise<unknown>): Promise<string> {
151
+ try {
152
+ await withDeadline(5000, fn);
153
+ return "";
154
+ } catch (e) {
155
+ if (e instanceof McpError) return e.code;
156
+ return "raw:" + String(e);
157
+ }
158
+ }
159
+
160
+ const call = (c: Client) => (signal: AbortSignal) => c.callTool("health", {}, { signal });
161
+
162
+ async function victim(c: Client, ms: number): Promise<string> {
163
+ try {
164
+ await withDeadline(ms, (signal) => c.callTool("health", {}, { signal }));
165
+ return "";
166
+ } catch (e) {
167
+ if (e instanceof McpError) return e.code;
168
+ return "raw:" + String(e);
169
+ }
170
+ }
171
+
172
+ test("fault matrix: drop-request", async (t) => {
173
+ if (!requireBins(t)) return;
174
+ const { addr, stop } = await faultEnv("drop-request", ["--n", "2"]);
175
+ t.after(stop);
176
+ const c = await dial(t, addr);
177
+ const code = await victim(c, 200);
178
+ assert.equal(code, "TIMEOUT");
179
+ assert.equal(await codeOf(call(c)), ""); // the follow-up survives
180
+ });
181
+
182
+ test("fault matrix: drop-response", async (t) => {
183
+ if (!requireBins(t)) return;
184
+ const { addr, stop } = await faultEnv("drop-response", ["--n", "2"]);
185
+ t.after(stop);
186
+ const c = await dial(t, addr);
187
+ const code = await victim(c, 200);
188
+ assert.equal(code, "TIMEOUT");
189
+ assert.equal(await codeOf(call(c)), "");
190
+ });
191
+
192
+ test("fault matrix: delay-response", async (t) => {
193
+ if (!requireBins(t)) return;
194
+ const { addr, stop } = await faultEnv("delay-response", ["--from", "2", "--delay-ms", "400"]);
195
+ t.after(stop);
196
+ const c = await dial(t, addr);
197
+ const code = await victim(c, 200);
198
+ assert.equal(code, "TIMEOUT");
199
+ const { addr: addr2, stop: stop2 } = await faultEnv("delay-response", ["--from", "2", "--delay-ms", "400"]);
200
+ t.after(stop2);
201
+ const c2 = await dial(t, addr2);
202
+ assert.equal(await victim(c2, 2000), ""); // a generous deadline absorbs the delay
203
+ });
204
+
205
+ test("fault matrix: duplicate-response", async (t) => {
206
+ if (!requireBins(t)) return;
207
+ const { addr, stop } = await faultEnv("duplicate-response", ["--n", "2"]);
208
+ t.after(stop);
209
+ const c = await dial(t, addr);
210
+ assert.equal(await victim(c, 2000), "");
211
+ assert.equal(await codeOf(call(c)), ""); // the duplicate is a stale id — skipped
212
+ });
213
+
214
+ test("fault matrix: reorder-response", async (t) => {
215
+ if (!requireBins(t)) return;
216
+ const { addr, stop } = await faultEnv("reorder-response", ["--n", "2"]);
217
+ t.after(stop);
218
+ const c = await dial(t, addr);
219
+ assert.equal(await victim(c, 200), "TIMEOUT");
220
+ assert.equal(await codeOf(call(c)), ""); // request #3 releases response #2
221
+ });
222
+
223
+ test("fault matrix: truncate-response", async (t) => {
224
+ if (!requireBins(t)) return;
225
+ const { addr, stop } = await faultEnv("truncate-response", ["--n", "2", "--bytes", "8"]);
226
+ t.after(stop);
227
+ const c = await dial(t, addr);
228
+ assert.equal(await victim(c, 200), "TIMEOUT");
229
+ assert.equal(await codeOf(call(c)), ""); // the wire is self-delimiting
230
+ });
231
+
232
+ test("fault matrix: corrupt-response", async (t) => {
233
+ if (!requireBins(t)) return;
234
+ const { addr, stop } = await faultEnv("corrupt-response", ["--n", "2"]);
235
+ t.after(stop);
236
+ const c = await dial(t, addr);
237
+ assert.equal(await victim(c, 200), "TIMEOUT"); // noise-skip, never a fast error
238
+ assert.equal(await codeOf(call(c)), "");
239
+ });
240
+
241
+ test("fault matrix: inject-notification", async (t) => {
242
+ if (!requireBins(t)) return;
243
+ const { addr, stop } = await faultEnv("inject-notification", ["--after", "2"]);
244
+ t.after(stop);
245
+ const c = await dial(t, addr);
246
+ assert.equal(await victim(c, 2000), "");
247
+ assert.equal(await codeOf(call(c)), ""); // an id-less frame is never a response
248
+ });
249
+
250
+ test("fault matrix: inject-stale-response", async (t) => {
251
+ if (!requireBins(t)) return;
252
+ const { addr, stop } = await faultEnv("inject-stale-response", ["--after", "2"]);
253
+ t.after(stop);
254
+ const c = await dial(t, addr);
255
+ assert.equal(await victim(c, 2000), "");
256
+ assert.equal(await codeOf(call(c)), ""); // the replayed initialize response is stale
257
+ });
258
+
259
+ test("fault matrix: close-after", async (t) => {
260
+ if (!requireBins(t)) return;
261
+ const { addr, stop } = await faultEnv("close-after", ["--n", "2"]);
262
+ t.after(stop);
263
+ const c = await dial(t, addr);
264
+ assert.equal(await victim(c, 2000), "");
265
+ const follow = await codeOf(call(c));
266
+ assert.notEqual(follow, "");
267
+ assert.notEqual(follow, "TIMEOUT"); // fails fast, never hangs
268
+ assert.equal(await codeOf(call(c)), "UNAVAILABLE"); // latched
269
+ });
270
+
271
+ test("fault matrix: half-close-after", async (t) => {
272
+ if (!requireBins(t)) return;
273
+ const { addr, stop } = await faultEnv("half-close-after", ["--n", "2"]);
274
+ t.after(stop);
275
+ const c = await dial(t, addr);
276
+ assert.equal(await victim(c, 2000), "");
277
+ const follow = await codeOf(call(c));
278
+ assert.notEqual(follow, "");
279
+ assert.notEqual(follow, "TIMEOUT");
280
+ assert.equal(await codeOf(call(c)), "UNAVAILABLE");
281
+ });
282
+
283
+ test("fault matrix: slow-server", async (t) => {
284
+ if (!requireBins(t)) return;
285
+ const { addr, stop } = await faultEnv("slow-server", ["--from", "2", "--bytes", "4", "--delay-ms", "25"]);
286
+ t.after(stop);
287
+ const c = await dial(t, addr);
288
+ assert.equal(await victim(c, 200), "TIMEOUT");
289
+ });
290
+
291
+ test("fault matrix: oversized-response", async (t) => {
292
+ if (!requireBins(t)) return;
293
+ const { addr, stop } = await faultEnv("oversized-response", ["--n", "2", "--claim", "67108864"]);
294
+ t.after(stop);
295
+ const c = await dial(t, addr);
296
+ assert.equal(await victim(c, 5000), "FRAME_TOO_LARGE"); // the 1 MiB cap, not 64 MiB
297
+ assert.equal(await codeOf(call(c)), "UNAVAILABLE"); // the stream is desynced — latched
298
+ });
@@ -0,0 +1,32 @@
1
+ // D-16 §10: the TS fuzz estate's own pin (the F-04 pattern — a removed
2
+ // target is a detected coverage loss, never silent). The estate lives in
3
+ // tests/fuzz-estate.test.ts: fast-check property tests over the frozen
4
+ // mirrors (version parse, error mapping, id correlation, frame cap,
5
+ // envelope decode) plus the §17 command model
6
+ // (DISCONNECTED→CONNECTED→INITIALIZED→TRANSACTION→STREAMING→CLOSED).
7
+
8
+ import assert from "node:assert/strict";
9
+ import { readFileSync } from "node:fs";
10
+ import { fileURLToPath } from "node:url";
11
+ import path from "node:path";
12
+ import test from "node:test";
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const estate = path.join(__dirname, "fuzz-estate.test.ts");
16
+
17
+ // The five property tests and the §17 model, by name.
18
+ const pins = [
19
+ "the version-parse property",
20
+ "the error-mapping property",
21
+ "the id-correlation property",
22
+ "the frame-cap property",
23
+ "the envelope property",
24
+ "the §17 command model",
25
+ ];
26
+
27
+ test("the fuzz-estate pin", () => {
28
+ const src = readFileSync(estate, "utf8");
29
+ for (const pin of pins) {
30
+ assert.ok(src.includes(pin), `§10 fuzz target missing: ${pin}`);
31
+ }
32
+ });