@nifrajs/testing 3.1.0 → 3.2.0
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 +8 -0
- package/dist/agent-certification.d.ts +41 -0
- package/dist/agent-certification.d.ts.map +1 -0
- package/dist/agent-certification.js +242 -0
- package/dist/agent-certification.js.map +1 -0
- package/dist/agent-eval.d.ts +192 -0
- package/dist/agent-eval.d.ts.map +1 -0
- package/dist/agent-eval.js +410 -0
- package/dist/agent-eval.js.map +1 -0
- package/dist/contract-lab.d.ts +43 -0
- package/dist/contract-lab.d.ts.map +1 -0
- package/dist/contract-lab.js +139 -0
- package/dist/contract-lab.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +11 -6
- package/src/agent-certification.ts +296 -0
- package/src/agent-eval.ts +660 -0
- package/src/contract-lab.ts +196 -0
- package/src/index.ts +52 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-neutral HTTP contract witnesses.
|
|
3
|
+
*
|
|
4
|
+
* The witness set is deliberately made only of Web `Request`/`Response` values. Runtime suites provide
|
|
5
|
+
* the handler under test - the full Bun server, the compact edge server, or a native Node/Deno adapter -
|
|
6
|
+
* and this module compares the wire contract without importing any runtime-specific implementation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface ContractLabHandler {
|
|
10
|
+
fetch(request: Request): Response | Promise<Response>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ContractLabServer {
|
|
14
|
+
readonly origin: string
|
|
15
|
+
stop(): void | Promise<void>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ContractLabRuntimeAdapter {
|
|
19
|
+
start(app: ContractLabHandler): ContractLabServer | Promise<ContractLabServer>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ContractLabWitness {
|
|
23
|
+
readonly id: string
|
|
24
|
+
readonly request: {
|
|
25
|
+
readonly method: string
|
|
26
|
+
readonly path: string
|
|
27
|
+
readonly headers?: Readonly<Record<string, string>>
|
|
28
|
+
readonly body?: string
|
|
29
|
+
}
|
|
30
|
+
readonly expected: {
|
|
31
|
+
readonly status: number
|
|
32
|
+
readonly contentType: "json" | "text"
|
|
33
|
+
readonly headers?: Readonly<Record<string, string>>
|
|
34
|
+
readonly body: unknown
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The shared cross-runtime wire contract. Keep this list small and stable: it is evidence, not a load test. */
|
|
39
|
+
export const contractLabWitnesses: readonly ContractLabWitness[] = Object.freeze([
|
|
40
|
+
{
|
|
41
|
+
id: "params-query-header",
|
|
42
|
+
request: {
|
|
43
|
+
method: "GET",
|
|
44
|
+
path: "/lab/users/alice?tag=one&tag=two",
|
|
45
|
+
headers: { "x-lab": "yes" },
|
|
46
|
+
},
|
|
47
|
+
expected: {
|
|
48
|
+
status: 200,
|
|
49
|
+
contentType: "json",
|
|
50
|
+
body: { id: "alice", tags: ["one", "two"], lab: "yes" },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "json-body",
|
|
55
|
+
request: {
|
|
56
|
+
method: "POST",
|
|
57
|
+
path: "/lab/echo",
|
|
58
|
+
headers: { "content-type": "application/json" },
|
|
59
|
+
body: JSON.stringify({ message: "hello", count: 2 }),
|
|
60
|
+
},
|
|
61
|
+
expected: {
|
|
62
|
+
status: 200,
|
|
63
|
+
contentType: "json",
|
|
64
|
+
body: { message: "hello", count: 2 },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "response-bridge",
|
|
69
|
+
request: { method: "GET", path: "/lab/created" },
|
|
70
|
+
expected: { status: 201, contentType: "text", body: "created" },
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "not-found",
|
|
74
|
+
request: { method: "GET", path: "/lab/missing" },
|
|
75
|
+
expected: { status: 404, contentType: "json", body: { ok: false, error: "not_found" } },
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "method-not-allowed",
|
|
79
|
+
request: { method: "POST", path: "/lab/users/alice" },
|
|
80
|
+
expected: {
|
|
81
|
+
status: 405,
|
|
82
|
+
contentType: "json",
|
|
83
|
+
headers: { allow: "GET, HEAD" },
|
|
84
|
+
body: { ok: false, error: "method_not_allowed" },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
])
|
|
88
|
+
|
|
89
|
+
function canonical(value: unknown): string {
|
|
90
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`
|
|
91
|
+
if (value !== null && typeof value === "object") {
|
|
92
|
+
const record = value as Record<string, unknown>
|
|
93
|
+
return `{${Object.keys(record)
|
|
94
|
+
.sort()
|
|
95
|
+
.map((key) => `${JSON.stringify(key)}:${canonical(record[key])}`)
|
|
96
|
+
.join(",")}}`
|
|
97
|
+
}
|
|
98
|
+
return JSON.stringify(value)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function witnessRequest(origin: string, witness: ContractLabWitness): Request {
|
|
102
|
+
return new Request(`${origin}${witness.request.path}`, {
|
|
103
|
+
method: witness.request.method,
|
|
104
|
+
headers: witness.request.headers,
|
|
105
|
+
...(witness.request.body === undefined ? {} : { body: witness.request.body }),
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function assertWitnessResponse(
|
|
110
|
+
witness: ContractLabWitness,
|
|
111
|
+
response: Response,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
const contentType = response.headers.get("content-type")?.split(";", 1)[0] ?? ""
|
|
114
|
+
const actual =
|
|
115
|
+
witness.expected.contentType === "json" ? await response.json() : await response.text()
|
|
116
|
+
const expectedContentType =
|
|
117
|
+
witness.expected.contentType === "json" ? "application/json" : "text/plain"
|
|
118
|
+
const headersMatch = Object.entries(witness.expected.headers ?? {}).every(
|
|
119
|
+
([name, value]) => response.headers.get(name) === value,
|
|
120
|
+
)
|
|
121
|
+
if (
|
|
122
|
+
response.status !== witness.expected.status ||
|
|
123
|
+
contentType !== expectedContentType ||
|
|
124
|
+
!headersMatch ||
|
|
125
|
+
canonical(actual) !== canonical(witness.expected.body)
|
|
126
|
+
) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`contract witness ${witness.id} failed: expected ${witness.expected.status} ` +
|
|
129
|
+
`${expectedContentType} ${canonical(witness.expected.body)}, got ${response.status} ` +
|
|
130
|
+
`${contentType || "(missing content type)"} ${canonical(actual)}`,
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Execute every witness against one runtime and throw a bounded, replayable mismatch. */
|
|
136
|
+
export async function runContractLab(
|
|
137
|
+
handler: ContractLabHandler,
|
|
138
|
+
origin = "http://nifra-contract-lab.invalid",
|
|
139
|
+
): Promise<void> {
|
|
140
|
+
for (const witness of contractLabWitnesses) {
|
|
141
|
+
await assertWitnessResponse(witness, await handler.fetch(witnessRequest(origin, witness)))
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Execute the same shared witnesses through a real HTTP origin. */
|
|
146
|
+
export async function runContractLabOverHttp(
|
|
147
|
+
origin: string,
|
|
148
|
+
fetcher: typeof fetch = globalThis.fetch,
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
for (const witness of contractLabWitnesses) {
|
|
151
|
+
await assertWitnessResponse(witness, await fetcher(witnessRequest(origin, witness)))
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Run the shared witnesses through a real HTTP adapter and always release its server. */
|
|
156
|
+
export async function runContractLabThroughAdapter(
|
|
157
|
+
adapter: ContractLabRuntimeAdapter,
|
|
158
|
+
app: ContractLabHandler = createReferenceContractLabHandler(),
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
const server = await adapter.start(app)
|
|
161
|
+
try {
|
|
162
|
+
await runContractLabOverHttp(server.origin)
|
|
163
|
+
} finally {
|
|
164
|
+
await server.stop()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** A reference Web handler for adapter-only suites. Core and edge suites use their real routers instead. */
|
|
169
|
+
export function createReferenceContractLabHandler(): ContractLabHandler {
|
|
170
|
+
return {
|
|
171
|
+
async fetch(request) {
|
|
172
|
+
const url = new URL(request.url)
|
|
173
|
+
const user = /^\/lab\/users\/([^/]+)$/.exec(url.pathname)
|
|
174
|
+
if (request.method === "GET" && user !== null) {
|
|
175
|
+
return Response.json({
|
|
176
|
+
id: decodeURIComponent(user[1] ?? ""),
|
|
177
|
+
tags: url.searchParams.getAll("tag"),
|
|
178
|
+
lab: request.headers.get("x-lab"),
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
if (request.method === "POST" && url.pathname === "/lab/echo") {
|
|
182
|
+
return Response.json(await request.json())
|
|
183
|
+
}
|
|
184
|
+
if (request.method === "GET" && url.pathname === "/lab/created") {
|
|
185
|
+
return new Response("created", { status: 201, headers: { "content-type": "text/plain" } })
|
|
186
|
+
}
|
|
187
|
+
if (url.pathname === "/lab/users/alice") {
|
|
188
|
+
return Response.json(
|
|
189
|
+
{ ok: false, error: "method_not_allowed" },
|
|
190
|
+
{ status: 405, headers: { allow: "GET, HEAD" } },
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
return Response.json({ ok: false, error: "not_found" }, { status: 404 })
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,47 @@ export {
|
|
|
25
25
|
type ContractWitness,
|
|
26
26
|
runAdversarialContract,
|
|
27
27
|
} from "./adversarial.ts"
|
|
28
|
+
export {
|
|
29
|
+
type CertifiableDeploymentAdapter,
|
|
30
|
+
type CertifiableDescriptorAdapter,
|
|
31
|
+
type CertifiableModelGateway,
|
|
32
|
+
deploymentAdapterCertificationProfile,
|
|
33
|
+
deploymentCertificationProfile,
|
|
34
|
+
gatewayCertificationProfile,
|
|
35
|
+
modelGatewayCertificationProfile,
|
|
36
|
+
registryCertificationProfile,
|
|
37
|
+
} from "./agent-certification.ts"
|
|
38
|
+
export {
|
|
39
|
+
type AgentEvalCase,
|
|
40
|
+
type AgentEvalComponentEvidence,
|
|
41
|
+
type AgentEvalCompositionOptions,
|
|
42
|
+
type AgentEvalCompositionReport,
|
|
43
|
+
AgentEvalRegressionError,
|
|
44
|
+
type AgentEvalReport,
|
|
45
|
+
type AgentEvalSuite,
|
|
46
|
+
type AgentEvalSuiteSpec,
|
|
47
|
+
type AgentFailureMatrixCase,
|
|
48
|
+
AgentFailureMatrixError,
|
|
49
|
+
type AgentFailureMatrixKind,
|
|
50
|
+
type AgentFailureMatrixReport,
|
|
51
|
+
assertAgentEval,
|
|
52
|
+
assertAgentEvalBaseline,
|
|
53
|
+
assertAgentFailureMatrix,
|
|
54
|
+
type BaselineComparison,
|
|
55
|
+
type BaselineOptions,
|
|
56
|
+
type CaseComparison,
|
|
57
|
+
type CaseResult,
|
|
58
|
+
type ComparisonCode,
|
|
59
|
+
compareAgentEvalBaseline,
|
|
60
|
+
defineAgentEvalSuite,
|
|
61
|
+
parseRubricVerdict,
|
|
62
|
+
type RubricShape,
|
|
63
|
+
type RubricSpec,
|
|
64
|
+
type RubricVerdict,
|
|
65
|
+
runAgentEvalComposition,
|
|
66
|
+
runAgentFailureMatrix,
|
|
67
|
+
type ScoreTolerance,
|
|
68
|
+
} from "./agent-eval.ts"
|
|
28
69
|
export {
|
|
29
70
|
AdapterCertificationError,
|
|
30
71
|
type AdapterCertificationProfile,
|
|
@@ -51,6 +92,17 @@ export {
|
|
|
51
92
|
storageAdapterCertificationProfile,
|
|
52
93
|
verifyAdapterCertification,
|
|
53
94
|
} from "./certification.ts"
|
|
95
|
+
export {
|
|
96
|
+
type ContractLabHandler,
|
|
97
|
+
type ContractLabRuntimeAdapter,
|
|
98
|
+
type ContractLabServer,
|
|
99
|
+
type ContractLabWitness,
|
|
100
|
+
contractLabWitnesses,
|
|
101
|
+
createReferenceContractLabHandler,
|
|
102
|
+
runContractLab,
|
|
103
|
+
runContractLabOverHttp,
|
|
104
|
+
runContractLabThroughAdapter,
|
|
105
|
+
} from "./contract-lab.ts"
|
|
54
106
|
export { type CookieJar, cookieJar } from "./cookies.ts"
|
|
55
107
|
export {
|
|
56
108
|
createFailureLab,
|