@nifrajs/testing 3.3.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.
@@ -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({ id: check.id, capability: check.capability, ok: true })
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,