@suluk/testgen 0.1.0 → 0.1.1

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/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # @suluk/testgen
2
+
3
+ **Generate a deterministic conformance test suite from a v4 "Suluk" contract — the contract's claims, made executable.**
4
+
5
+ > **CANDIDATE tooling — not official OpenAPI.** Suluk is a single-contributor candidate for
6
+ > OpenAPI Specification v4.0 ("Moonwalk"), unaffiliated with the OpenAPI Initiative and unable
7
+ > to ratify anything on the SIG's behalf.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ bun add @suluk/testgen @cfworker/json-schema
13
+ ```
14
+
15
+ `@cfworker/json-schema` is a peer dependency — the *generated* suite imports it to validate response
16
+ bodies against their declared schemas. `generateMoneyTests` additionally targets `@suluk/stripe` (the
17
+ emitted suite imports the pricing primitives from there).
18
+
19
+ ## What it does
20
+
21
+ `generateTests(doc)` walks a v4 document and emits a **self-contained test file** (a string) that runs
22
+ against a live deployment. It turns the `x-suluk-*` facets from decoration into load-bearing claims:
23
+
24
+ - **Access enforcement, on the real wire.** A non-public op must DENY anon a success; a public op must be
25
+ reachable by anon. `x-suluk-access` is only the *expectation* — the test passes iff the wire agrees. It
26
+ never asserts over a projection and never treats the facet as if it were the enforcement (C022 inv.3).
27
+ - **Status smoke + schema conformance.** A public, parameter-free GET returns a *declared* status, and its
28
+ 2xx body validates against the declared response schema (same generic JSON-Schema engine the SDK uses).
29
+ - **Error-conformance.** A denied request returns a well-formed RFC-9457 Problem Details body (the shared
30
+ `@suluk/hono` envelope: `title` string + `status` matching the HTTP status).
31
+ - **Cost declared.** Every op that prices itself declares a well-formed `x-suluk-cost` — checked statically,
32
+ never as a literal µ$ amount (which would couple tests to billing internals and go flaky).
33
+
34
+ A **pure function of the document**: same contract in, same suite out, no network at generate-time. Each op's
35
+ tests are labelled with its provenance (`x-suluk-source`) so a failure points at the authoring source.
36
+
37
+ `generateMoneyTests()` is a separate emitter for the money path — an in-process suite over the `@suluk/stripe`
38
+ pricing primitives (anti-tampering, integer-cents, never-over-discount, exact proration, deterministic
39
+ idempotency). It takes no document; the invariants live in the shared primitives, not in any one contract.
40
+
41
+ ## When to reach for it
42
+
43
+ Reach for it whenever you want the contract's facets to *mean something* — this is what makes `x-suluk-*`
44
+ enforced rather than aspirational. The common pattern is to expose the generated suite as a download endpoint
45
+ so any consumer can verify your deployment honours its own contract (see Usage). Add `generateMoneyTests` when
46
+ your app touches checkout/billing through `@suluk/stripe`.
47
+
48
+ This is a **generator**, not a runner: it produces a `bun:test` (or `vitest`) file you run yourself. It is the
49
+ testing facet of the projection model — siblings like `@suluk/scalar` / `@suluk/swagger` *render* the contract;
50
+ `@suluk/sdk` *generates a client*; this generates the *conformance proof*.
51
+
52
+ ## Usage
53
+
54
+ ### Generate a conformance suite from a v4 document
55
+
56
+ ```ts
57
+ import { generateTests } from "@suluk/testgen";
58
+ import type { OpenAPIv4Document } from "@suluk/core";
59
+
60
+ const suite = generateTests(document, { baseURL: "https://api.example.com" });
61
+ // → a self-contained test-file string. Write it next to your tests and run it:
62
+ await Bun.write("api.conformance.test.ts", suite);
63
+ ```
64
+
65
+ Run the emitted suite against a deployment — `SULUK_BASE_URL` wins over the baked-in `baseURL`:
66
+
67
+ ```bash
68
+ SULUK_BASE_URL=https://api.example.com bun test api.conformance.test.ts
69
+ ```
70
+
71
+ Optional positive-side checks (a non-public op also asserting a *valid* principal IS allowed through) are
72
+ skipped unless you provide **synthetic** principal tokens — never a production credential:
73
+
74
+ ```bash
75
+ SULUK_ADMIN_TOKEN=… SULUK_USER_TOKEN=… bun test api.conformance.test.ts
76
+ ```
77
+
78
+ ### Serve it as a download endpoint (real saasuluk usage)
79
+
80
+ ```ts
81
+ import { generateTests } from "@suluk/testgen";
82
+
83
+ app.get("/conformance.test.ts", (c) =>
84
+ new Response(generateTests(document, { baseURL: new URL(c.req.url).origin }), {
85
+ headers: {
86
+ "content-type": "application/typescript; charset=utf-8",
87
+ "content-disposition": 'attachment; filename="api.conformance.test.ts"',
88
+ },
89
+ }),
90
+ );
91
+ ```
92
+
93
+ ### Options
94
+
95
+ ```ts
96
+ generateTests(document, {
97
+ baseURL: "https://api.example.com", // baked default; SULUK_BASE_URL overrides at run-time
98
+ framework: "vitest", // "bun" (default) | "vitest" — toggles the emitted imports
99
+ });
100
+ ```
101
+
102
+ ### Money-correctness suite
103
+
104
+ ```ts
105
+ import { generateMoneyTests } from "@suluk/testgen";
106
+
107
+ const money = generateMoneyTests(); // imports primitives from "@suluk/stripe"
108
+ await Bun.write("money.conformance.test.ts", money);
109
+ // then: bun test money.conformance.test.ts (no network, in-process)
110
+
111
+ // configurable runner + import specifier:
112
+ generateMoneyTests({ framework: "vitest", stripeModule: "../pricing" });
113
+ ```
114
+
115
+ ## API
116
+
117
+ | Export | What it does |
118
+ | --- | --- |
119
+ | `generateTests(doc, opts?)` | Emit a wire-conformance suite (access / status / schema / cost) from a v4 document, as a test-file string. |
120
+ | `generateMoneyTests(opts?)` | Emit an in-process money-correctness suite over the `@suluk/stripe` pricing primitives. |
121
+ | `TestgenOptions` | `{ baseURL?: string; framework?: "bun" \| "vitest" }`. |
122
+ | `MoneyTestsOptions` | `{ framework?: "bun" \| "vitest"; stripeModule?: string }`. |
123
+
124
+ ## Boundary
125
+
126
+ This package **generates, never hosts** (L3). It is a pure string-emitter: no network at generate-time, no
127
+ deployment of its own. The seams it leaves to the app:
128
+
129
+ - **The deployment under test** is injected via `SULUK_BASE_URL` (or the baked `baseURL`) at *run*-time — the
130
+ generated suite reads it from the environment.
131
+ - **Authorization is the server's job.** The access tests assert the *wire* enforces `x-suluk-access`; this
132
+ never re-implements authz, and never asserts over a projection (C022 inv.3 — the server is the only boundary).
133
+ - **Synthetic principals** for the optional positive-side checks come from you (`SULUK_*_TOKEN`), out of band.
134
+ - **Running** the emitted file is the app's responsibility (`bun test`); you decide where to write it and when
135
+ to run it in CI.
136
+
137
+ Contributing: a new facet means a new asserted claim here — that is what keeps `x-suluk-*` load-bearing.
138
+
139
+ ## License
140
+
141
+ Apache-2.0.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suluk/testgen",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Generate a DETERMINISTIC conformance test suite from a v4 'Suluk' contract — the executable form of the contract's claims. Asserts the SERVER ENFORCES x-suluk-access on the real wire (anon rejected on non-public ops; public ops reachable), smoke-tests declared statuses, validates 2xx bodies against their declared schemas, and checks every declared cost is well-formed. A pure function of the document — same contract in, same suite out, no network at generate-time. CANDIDATE tooling.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -19,7 +19,7 @@
19
19
  ".": "./src/index.ts"
20
20
  },
21
21
  "dependencies": {
22
- "@suluk/core": "^0.1.7"
22
+ "@suluk/core": "^0.1.11"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "@cfworker/json-schema": "^4.1.1"
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/bun": "latest",
34
- "@suluk/stripe": "^0.1.2"
34
+ "@suluk/stripe": "^0.1.7"
35
35
  },
36
36
  "scripts": {
37
37
  "test": "bun test",
package/src/generate.ts CHANGED
@@ -22,10 +22,13 @@ interface RawReq {
22
22
  parameterSchema?: { path?: unknown; query?: unknown; body?: unknown };
23
23
  responses?: Record<string, { status: string | number; contentSchema?: unknown }>;
24
24
  ["x-suluk-cost"]?: CostModel; ["x-suluk-access"]?: AccessFacet; ["x-suluk-source"]?: SulukSource;
25
+ ["x-suluk-approval"]?: { required?: unknown; reason?: unknown };
25
26
  }
26
27
  interface OpInfo {
27
28
  name: string; method: string; uri: string; filledPath: string; requires: string;
28
29
  declaredStatuses: number[]; okSchema?: unknown; cost: number | null; source?: SulukSource;
30
+ /** the raw x-suluk-approval.required when the HITL facet is present (else null) — checked WELL-FORMED, statically. */
31
+ approvalRequired: unknown; hasApproval: boolean;
29
32
  hasBody: boolean; paramFree: boolean;
30
33
  }
31
34
 
@@ -52,6 +55,7 @@ function walkOps(doc: OpenAPIv4Document): OpInfo[] {
52
55
  requires: req["x-suluk-access"]?.requires ?? "anyone",
53
56
  declaredStatuses: [...new Set(statuses)].sort((a, b) => a - b),
54
57
  okSchema: ok?.contentSchema, cost: costOf(req), source: req["x-suluk-source"],
58
+ approvalRequired: req["x-suluk-approval"]?.required, hasApproval: req["x-suluk-approval"] != null,
55
59
  hasBody: req.contentSchema != null || ps.body != null,
56
60
  paramFree: method === "GET" && !hasPathParam && reqQuery.length === 0,
57
61
  });
@@ -132,6 +136,13 @@ function emitOp(doc: OpenAPIv4Document, op: OpInfo): string {
132
136
  expect(Number.isFinite(${op.cost}) && ${op.cost} >= 0, "x-suluk-cost is malformed").toBe(true);
133
137
  });`);
134
138
 
139
+ // ── L2 (static): a declared HITL gate is well-formed. x-suluk-approval is an AGENT-RUNTIME facet (it projects to the
140
+ // Agents SDK `needsApproval`), NOT wire-enforced like x-suluk-access — so, like cost, the conformance claim is the
141
+ // static well-formedness (`required` is a boolean), which keeps the facet load-bearing (it can't silently malform).
142
+ if (op.hasApproval) tests.push(` test("approval — declares a well-formed x-suluk-approval HITL gate (static; enforced by the agent runtime, not the wire)", () => {
143
+ expect(typeof ${JSON.stringify(op.approvalRequired)} === "boolean", "x-suluk-approval.required must be a boolean").toBe(true);
144
+ });`);
145
+
135
146
  return `describe("${lbl(head)}", () => {\n${tests.join("\n")}\n});`;
136
147
  }
137
148
 
@@ -10,7 +10,7 @@ const doc = {
10
10
  product: {
11
11
  requests: {
12
12
  listProduct: { method: "get", responses: { ok: { status: 200, contentSchema: { type: "array", items: { type: "object", properties: { id: { type: "integer" } }, required: ["id"], additionalProperties: false } } } }, "x-suluk-access": { requires: "anyone" }, "x-suluk-cost": { estimateMicroUsd: 10 }, "x-suluk-source": { file: "src/schema.ts", symbol: "product", kind: "drizzle-table" } },
13
- createProduct: { method: "post", contentSchema: { type: "object", properties: { name: { type: "string" } }, required: ["name"] }, responses: { created: { status: 201 } }, "x-suluk-access": { requires: "admin" }, "x-suluk-cost": { estimateMicroUsd: 145 }, "x-suluk-source": { file: "src/schema.ts", symbol: "product", kind: "drizzle-table" } },
13
+ createProduct: { method: "post", contentSchema: { type: "object", properties: { name: { type: "string" } }, required: ["name"] }, responses: { created: { status: 201 } }, "x-suluk-access": { requires: "admin" }, "x-suluk-cost": { estimateMicroUsd: 145 }, "x-suluk-approval": { required: true, reason: "creates a billable product" }, "x-suluk-source": { file: "src/schema.ts", symbol: "product", kind: "drizzle-table" } },
14
14
  },
15
15
  },
16
16
  "cart/{id}": { requests: { getCart: { method: "get", responses: { ok: { status: 200 } }, "x-suluk-access": { requires: "authenticated", scope: "owner" }, "x-suluk-source": { file: "src/ops.ts", symbol: "getCart", kind: "operation" } } } },
@@ -59,6 +59,14 @@ describe("@suluk/testgen — generate a conformance suite from a v4 contract", (
59
59
  expect(suite).not.toMatch(/toBe\(145\)/); // never asserts the literal amount
60
60
  });
61
61
 
62
+ test("x-suluk-approval (HITL) is checked as a DECLARED well-formed gate — static, NOT wire-enforced (it's an agent-runtime facet)", () => {
63
+ expect(suite).toContain("declares a well-formed x-suluk-approval HITL gate");
64
+ expect(suite).toContain("enforced by the agent runtime, not the wire"); // honest scope — unlike x-suluk-access
65
+ expect(suite).toContain('typeof true === "boolean"'); // the static well-formedness assertion on `required`
66
+ // only the op that declares it gets the claim; a non-gated op (listProduct) does not
67
+ expect(suite.match(/well-formed x-suluk-approval/g)?.length).toBe(1);
68
+ });
69
+
62
70
  test("each op group is LABELLED with its provenance (x-suluk-source) — a failure points at the source", () => {
63
71
  expect(suite).toContain("createProduct [POST product] · admin ← src/schema.ts#product");
64
72
  expect(suite).toContain("getCart [GET cart/{id}] · authenticated ← src/ops.ts#getCart");