@nifrajs/testing 3.2.0 → 3.5.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/dist/certification.d.ts +18 -0
- package/dist/certification.d.ts.map +1 -1
- package/dist/certification.js +34 -1
- package/dist/certification.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/prediction-lab.d.ts +65 -0
- package/dist/prediction-lab.d.ts.map +1 -0
- package/dist/prediction-lab.js +530 -0
- package/dist/prediction-lab.js.map +1 -0
- package/package.json +7 -6
- package/src/certification.ts +56 -1
- package/src/index.ts +14 -0
- package/src/prediction-lab.ts +692 -0
package/src/certification.ts
CHANGED
|
@@ -8,9 +8,24 @@ export interface CertificationContext {
|
|
|
8
8
|
readonly key: (suffix: string) => string
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/** Portable identity for the artifact, runtime, and witness that produced one check result. */
|
|
12
|
+
export interface CertificationTarget {
|
|
13
|
+
/** Published adapter/package name, for example `@nifrajs/node`. */
|
|
14
|
+
readonly adapter?: string
|
|
15
|
+
/** Runtime that executed the witness, for example `node`, `deno`, or `workerd`. */
|
|
16
|
+
readonly runtime?: string
|
|
17
|
+
/** Checked-in or built artifact path, when the witness exercises one. */
|
|
18
|
+
readonly artifact?: string
|
|
19
|
+
/** Source path that owns the behavior, when it differs from the artifact. */
|
|
20
|
+
readonly source?: string
|
|
21
|
+
/** Stable category of witness, useful when one report mixes several targets. */
|
|
22
|
+
readonly witnessKind?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
export interface CertificationCheck<Adapter> {
|
|
12
26
|
readonly id: string
|
|
13
27
|
readonly capability: string
|
|
28
|
+
readonly target?: CertificationTarget
|
|
14
29
|
readonly run: (adapter: Adapter, context: CertificationContext) => void | Promise<void>
|
|
15
30
|
}
|
|
16
31
|
|
|
@@ -25,6 +40,7 @@ export interface CertificationCheckEvidence {
|
|
|
25
40
|
readonly id: string
|
|
26
41
|
readonly capability: string
|
|
27
42
|
readonly ok: boolean
|
|
43
|
+
readonly target?: CertificationTarget
|
|
28
44
|
/** Error class only. Messages may contain credentials/provider payloads and are never evidence. */
|
|
29
45
|
readonly errorName?: string
|
|
30
46
|
}
|
|
@@ -40,6 +56,7 @@ export interface AdapterCertificationReport {
|
|
|
40
56
|
readonly ok: boolean
|
|
41
57
|
readonly profile: { readonly id: string; readonly version: number }
|
|
42
58
|
readonly adapterId: string
|
|
59
|
+
readonly target?: CertificationTarget
|
|
43
60
|
readonly checks: readonly CertificationCheckEvidence[]
|
|
44
61
|
readonly capabilities: readonly CertificationCapabilityEvidence[]
|
|
45
62
|
readonly evidenceHash: string
|
|
@@ -54,6 +71,7 @@ export class AdapterCertificationError extends Error {
|
|
|
54
71
|
}
|
|
55
72
|
|
|
56
73
|
const TOKEN = /^[a-z0-9][a-z0-9._:-]{0,127}$/
|
|
74
|
+
const TARGET_NAME = /^@?[a-z0-9][a-z0-9._:-]*(?:\/[a-z0-9][a-z0-9._:-]*)?$/
|
|
57
75
|
const MAX_CHECKS = 128
|
|
58
76
|
|
|
59
77
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
@@ -69,6 +87,28 @@ function canonical(value: unknown): string {
|
|
|
69
87
|
return JSON.stringify(value)
|
|
70
88
|
}
|
|
71
89
|
|
|
90
|
+
const boundedTargetText = (value: string): boolean =>
|
|
91
|
+
value.length >= 1 &&
|
|
92
|
+
value.length <= 256 &&
|
|
93
|
+
[...value].every((character) => {
|
|
94
|
+
const code = character.codePointAt(0) ?? 0
|
|
95
|
+
return code > 0x1f && code !== 0x7f
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
function validateTarget(target: CertificationTarget | undefined): void {
|
|
99
|
+
if (target === undefined) return
|
|
100
|
+
if (target.adapter !== undefined && !TARGET_NAME.test(target.adapter))
|
|
101
|
+
throw new TypeError("certification target adapter must be a bounded package name")
|
|
102
|
+
for (const value of [target.runtime, target.witnessKind]) {
|
|
103
|
+
if (value !== undefined && !TOKEN.test(value))
|
|
104
|
+
throw new TypeError("certification target tokens must be bounded lowercase names")
|
|
105
|
+
}
|
|
106
|
+
for (const value of [target.artifact, target.source]) {
|
|
107
|
+
if (value !== undefined && !boundedTargetText(value))
|
|
108
|
+
throw new TypeError("certification target paths must be bounded and control-free")
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
72
112
|
async function sha256(value: string): Promise<string> {
|
|
73
113
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value))
|
|
74
114
|
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")
|
|
@@ -97,6 +137,7 @@ function validateProfile<Adapter>(profile: AdapterCertificationProfile<Adapter>)
|
|
|
97
137
|
`certification check ${check.id} names undeclared capability ${check.capability}`,
|
|
98
138
|
)
|
|
99
139
|
ids.add(check.id)
|
|
140
|
+
validateTarget(check.target)
|
|
100
141
|
}
|
|
101
142
|
for (const capability of capabilities) {
|
|
102
143
|
if (!profile.checks.some((check) => check.capability === capability))
|
|
@@ -115,27 +156,40 @@ export function defineCertificationProfile<Adapter>(
|
|
|
115
156
|
export async function certifyAdapter<Adapter>(options: {
|
|
116
157
|
readonly profile: AdapterCertificationProfile<Adapter>
|
|
117
158
|
readonly adapterId: string
|
|
159
|
+
/** Optional identity shared by all checks; a check target can add or override its fields. */
|
|
160
|
+
readonly target?: CertificationTarget
|
|
118
161
|
/** A fresh adapter per check prevents one failed check from contaminating the next. */
|
|
119
162
|
readonly createAdapter: () => Adapter | Promise<Adapter>
|
|
120
163
|
readonly cleanup?: (adapter: Adapter) => void | Promise<void>
|
|
121
164
|
}): Promise<AdapterCertificationReport> {
|
|
122
165
|
validateProfile(options.profile)
|
|
123
166
|
if (!TOKEN.test(options.adapterId)) throw new TypeError("adapter id must be a bounded token")
|
|
167
|
+
validateTarget(options.target)
|
|
124
168
|
const checks: CertificationCheckEvidence[] = []
|
|
125
169
|
for (const check of options.profile.checks) {
|
|
126
170
|
let adapter: Adapter | undefined
|
|
171
|
+
const target =
|
|
172
|
+
options.target === undefined && check.target === undefined
|
|
173
|
+
? undefined
|
|
174
|
+
: { ...options.target, ...check.target }
|
|
127
175
|
try {
|
|
128
176
|
adapter = await options.createAdapter()
|
|
129
177
|
await check.run(adapter, {
|
|
130
178
|
key: (suffix) =>
|
|
131
179
|
`nifra-cert:${options.profile.id}:${options.adapterId}:${check.id}:${suffix}`,
|
|
132
180
|
})
|
|
133
|
-
checks.push({
|
|
181
|
+
checks.push({
|
|
182
|
+
id: check.id,
|
|
183
|
+
capability: check.capability,
|
|
184
|
+
ok: true,
|
|
185
|
+
...(target === undefined ? {} : { target }),
|
|
186
|
+
})
|
|
134
187
|
} catch (error) {
|
|
135
188
|
checks.push({
|
|
136
189
|
id: check.id,
|
|
137
190
|
capability: check.capability,
|
|
138
191
|
ok: false,
|
|
192
|
+
...(target === undefined ? {} : { target }),
|
|
139
193
|
errorName:
|
|
140
194
|
error instanceof Error && TOKEN.test(error.name.toLowerCase())
|
|
141
195
|
? error.name
|
|
@@ -165,6 +219,7 @@ export async function certifyAdapter<Adapter>(options: {
|
|
|
165
219
|
ok: checks.every((check) => check.ok),
|
|
166
220
|
profile: { id: options.profile.id, version: options.profile.version },
|
|
167
221
|
adapterId: options.adapterId,
|
|
222
|
+
...(options.target === undefined ? {} : { target: options.target }),
|
|
168
223
|
checks,
|
|
169
224
|
capabilities,
|
|
170
225
|
}
|
package/src/index.ts
CHANGED
|
@@ -83,6 +83,7 @@ export {
|
|
|
83
83
|
type CertificationCapabilityEvidence,
|
|
84
84
|
type CertificationCheck,
|
|
85
85
|
type CertificationCheckEvidence,
|
|
86
|
+
type CertificationTarget,
|
|
86
87
|
cacheStoreCertificationProfile,
|
|
87
88
|
certifyAdapter,
|
|
88
89
|
defineCertificationProfile,
|
|
@@ -151,6 +152,19 @@ export {
|
|
|
151
152
|
replayIncident,
|
|
152
153
|
shapeOf,
|
|
153
154
|
} from "./incident.ts"
|
|
155
|
+
export {
|
|
156
|
+
assertPredictionLab,
|
|
157
|
+
PREDICTION_LAB_SEED,
|
|
158
|
+
type PredictionLabContext,
|
|
159
|
+
type PredictionLabOptions,
|
|
160
|
+
type PredictionLabReplay,
|
|
161
|
+
type PredictionLabReport,
|
|
162
|
+
type PredictionLabResult,
|
|
163
|
+
type PredictionLabTarget,
|
|
164
|
+
predictionLabCaseIds,
|
|
165
|
+
runPredictionLab,
|
|
166
|
+
toPredictionLabContext,
|
|
167
|
+
} from "./prediction-lab.ts"
|
|
154
168
|
export { type AppLike, type TestSession, type TestSessionOptions, testSession } from "./session.ts"
|
|
155
169
|
export {
|
|
156
170
|
httpToolAdapter,
|