@nifrajs/testing 2.11.0 → 2.12.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.
@@ -0,0 +1,200 @@
1
+ import { runAgent, } from "@nifrajs/agent";
2
+ import { createToolBudget } from "@nifrajs/core/tool-contract";
3
+ import { createFailureLab } from "./failure-lab.js";
4
+ const DEFAULT_SEED = 0x4e_49_46_52;
5
+ const FAULT_KINDS = [
6
+ "tool-error",
7
+ "malformed-model-output",
8
+ "budget-pressure",
9
+ "approval-denial",
10
+ "cancellation",
11
+ ];
12
+ /** Record the transcript emitted by a turn without adding a second execution recording path. */
13
+ export async function createTrajectoryTranscript(transcript, options = {}) {
14
+ validateAgentTranscript(transcript);
15
+ const payload = {
16
+ version: 1,
17
+ turn: transcript,
18
+ ...(options.faultProfile === undefined ? {} : { faultProfile: options.faultProfile }),
19
+ ...(options.seed === undefined ? {} : { seed: options.seed }),
20
+ };
21
+ return Object.freeze({ ...payload, version: 1, digest: await digest(payload) });
22
+ }
23
+ /** Alias that reads naturally at a call site which has just completed a run. */
24
+ export const recordTrajectory = createTrajectoryTranscript;
25
+ export async function runTrajectory(definition, input, ports, options) {
26
+ const result = await runAgent(definition, input, ports, options);
27
+ const transcript = await createTrajectoryTranscript(result.transcript);
28
+ return Object.freeze({ result, transcript });
29
+ }
30
+ /**
31
+ * Replay a recorded run with local model decisions and dry-run tools. The supplied model port is
32
+ * never called. Fault schedules use the existing deterministic failure-lab controller.
33
+ */
34
+ export async function replayTrajectory(definition, input, ports, transcript, options) {
35
+ validateAgentTranscript(transcript.turn);
36
+ const profile = options.faultProfile;
37
+ const seed = options.seed ?? DEFAULT_SEED;
38
+ const lab = createFailureLab({ seed, schedule: profile?.schedule ?? [] });
39
+ const faults = profile?.faults ?? [];
40
+ const counts = new Map();
41
+ const controller = new AbortController();
42
+ if (ports.signal?.aborted === true)
43
+ controller.abort();
44
+ ports.signal?.addEventListener("abort", () => controller.abort(), { once: true });
45
+ let responseIndex = 0;
46
+ let idSequence = 0;
47
+ const takeFault = (point, kind) => {
48
+ for (const candidateKind of kind === undefined ? FAULT_KINDS : [kind]) {
49
+ const key = `${point}:${candidateKind}`;
50
+ const occurrence = (counts.get(key) ?? 0) + 1;
51
+ counts.set(key, occurrence);
52
+ const fault = faults.find((candidate) => candidate.point === point &&
53
+ candidate.kind === candidateKind &&
54
+ (candidate.occurrence ?? 1) === occurrence);
55
+ if (fault !== undefined)
56
+ return fault;
57
+ }
58
+ return undefined;
59
+ };
60
+ const model = {
61
+ complete: async () => {
62
+ lab.checkpoint("trajectory.model");
63
+ const fault = takeFault("model");
64
+ if (fault?.kind === "malformed-model-output")
65
+ return { kind: "invalid" };
66
+ if (fault?.kind === "cancellation") {
67
+ controller.abort();
68
+ throw new Error("trajectory cancellation");
69
+ }
70
+ if (fault?.kind === "budget-pressure")
71
+ throw new Error("trajectory budget pressure");
72
+ const response = transcript.turn.responses[responseIndex++];
73
+ if (response === undefined)
74
+ throw new Error("trajectory transcript ended");
75
+ return lab.provider("trajectory.model", () => response);
76
+ },
77
+ };
78
+ const tools = definition.tools.map((tool) => wrapTool(tool, lab, () => takeFault("tool")));
79
+ const replayDefinition = { ...definition, tools };
80
+ let effectiveApproval = ports.approval;
81
+ if (effectiveApproval !== undefined) {
82
+ const approval = effectiveApproval;
83
+ effectiveApproval = {
84
+ request: async (request) => {
85
+ const fault = takeFault("effect", "approval-denial");
86
+ if (fault !== undefined)
87
+ return { status: "denied", reason: "trajectory fault" };
88
+ return approval.request(request);
89
+ },
90
+ };
91
+ }
92
+ const pressuredBudget = createToolBudget({ limits: { calls: 0 } });
93
+ const effectivePorts = {
94
+ ...ports,
95
+ model,
96
+ dryRun: true,
97
+ signal: controller.signal,
98
+ idFactory: () => `trajectory-${seed}-${++idSequence}`,
99
+ ...(effectiveApproval === undefined ? {} : { approval: effectiveApproval }),
100
+ ...(faults.some((fault) => fault.kind === "budget-pressure")
101
+ ? { budget: pressuredBudget }
102
+ : {}),
103
+ };
104
+ const result = await runAgent(replayDefinition, input, effectivePorts, {
105
+ state: options.state,
106
+ ...(options.maxTurns === undefined ? {} : { maxTurns: options.maxTurns }),
107
+ ...(options.goal === undefined ? {} : { goal: options.goal }),
108
+ });
109
+ const invariantResults = checkTrajectoryInvariants(result);
110
+ const regressionIds = await Promise.all(invariantResults.map((invariant) => trajectoryRegressionId(transcript, profile?.name ?? "none", invariant.id)));
111
+ return Object.freeze({
112
+ result,
113
+ transcript,
114
+ faults: lab.evidence(),
115
+ invariants: Object.freeze(invariantResults),
116
+ regressionIds: Object.freeze(regressionIds),
117
+ });
118
+ }
119
+ export function checkTrajectoryInvariants(result, options = {}) {
120
+ const evidence = result.evidence;
121
+ const committedTools = evidence.filter((item) => item.kind === "tool" && item.outcome === "committed");
122
+ const ledger = committedTools.every((item) => typeof item.ledgerHead === "string" && item.ledgerHead.length > 0);
123
+ const deniedEffects = new Set(evidence
124
+ .filter((item) => item.code === "capability_denied" && item.effectId !== undefined)
125
+ .map((item) => item.effectId));
126
+ const capability = committedTools.every((item) => item.effectId === undefined || !deniedEffects.has(item.effectId));
127
+ const bounded = evidence.length <= (options.maxSteps ?? 256) &&
128
+ (result.status === "completed" || result.status === "suspended");
129
+ const resumable = result.status !== "suspended" ||
130
+ (result.state.status === "suspended" && result.pending.effectId.length > 0);
131
+ const snapshots = options.budgetSnapshots ?? [];
132
+ const budget = snapshots.every((snapshot, index) => {
133
+ const values = Object.values(snapshot);
134
+ if (values.some((value) => !Number.isFinite(value) || value < 0))
135
+ return false;
136
+ if (index === 0)
137
+ return true;
138
+ const previous = snapshots[index - 1] ?? {};
139
+ return Object.entries(snapshot).every(([axis, value]) => value <= (previous[axis] ?? value));
140
+ });
141
+ return Object.freeze([
142
+ { id: "ledger-evidence", ok: ledger, ...(ledger ? {} : { code: "effect_without_ledger" }) },
143
+ {
144
+ id: "capability-admission",
145
+ ok: capability,
146
+ ...(capability ? {} : { code: "capability_admission_failed" }),
147
+ },
148
+ { id: "budget-monotonic", ok: budget, ...(budget ? {} : { code: "budget_negative" }) },
149
+ { id: "bounded-stop", ok: bounded, ...(bounded ? {} : { code: "stop_bound_exceeded" }) },
150
+ {
151
+ id: "resumable-suspension",
152
+ ok: resumable,
153
+ ...(resumable ? {} : { code: "suspension_not_resumable" }),
154
+ },
155
+ ]);
156
+ }
157
+ export function assertTrajectoryInvariants(result, options = {}) {
158
+ const failures = checkTrajectoryInvariants(result, options).filter((invariant) => !invariant.ok);
159
+ if (failures.length > 0)
160
+ throw new Error(`trajectory invariants failed: ${failures.map((invariant) => invariant.code ?? invariant.id).join(",")}`);
161
+ }
162
+ export async function trajectoryRegressionId(transcript, faultProfile, invariant) {
163
+ if (faultProfile.trim() === "")
164
+ throw new TypeError("trajectory: fault profile name is required");
165
+ return digest({ transcript: transcript.digest, faultProfile, invariant });
166
+ }
167
+ function wrapTool(tool, lab, nextFault) {
168
+ return Object.freeze({
169
+ ...tool,
170
+ execute: async (input, context) => {
171
+ lab.checkpoint(`trajectory.tool.${tool.name}`);
172
+ const fault = nextFault();
173
+ if (fault?.kind === "tool-error" || fault?.kind === "budget-pressure")
174
+ throw new Error("trajectory tool fault");
175
+ if (fault?.kind === "cancellation")
176
+ throw new Error("trajectory cancellation");
177
+ lab.checkpoint(`trajectory.effect.${tool.name}`);
178
+ return tool.execute(input, context);
179
+ },
180
+ });
181
+ }
182
+ function validateAgentTranscript(value) {
183
+ if (value === null ||
184
+ typeof value !== "object" ||
185
+ value.version !== 1 ||
186
+ typeof value.turnId !== "string" ||
187
+ !Array.isArray(value.responses) ||
188
+ !Array.isArray(value.evidence)) {
189
+ throw new TypeError("trajectory: invalid agent transcript");
190
+ }
191
+ }
192
+ async function digest(value) {
193
+ const serialized = JSON.stringify(value);
194
+ if (serialized === undefined)
195
+ throw new TypeError("trajectory: value is not serializable");
196
+ const bytes = new TextEncoder().encode(serialized);
197
+ const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
198
+ return [...hash].map((byte) => byte.toString(16).padStart(2, "0")).join("");
199
+ }
200
+ //# sourceMappingURL=trajectory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trajectory.js","sourceRoot":"","sources":["../src/trajectory.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "@nifrajs/testing",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "Contract-derived adversarial tests, runtime-matrix response conformance, and cookie-aware in-process sessions for Nifra apps.",
5
5
  "keywords": [
6
6
  "bun",
7
7
  "typescript",
8
8
  "nifra",
9
- "web-framework",
10
- "full-stack",
11
9
  "testing",
12
- "ssr"
10
+ "contract-testing",
11
+ "property-testing",
12
+ "fuzzing",
13
+ "conformance",
14
+ "adversarial"
13
15
  ],
14
16
  "type": "module",
15
17
  "main": "./dist/index.js",
@@ -42,9 +44,11 @@
42
44
  },
43
45
  "license": "MIT",
44
46
  "dependencies": {
45
- "@nifrajs/client": "2.11.0",
46
- "@nifrajs/core": "2.11.0",
47
- "@nifrajs/mock": "2.11.0"
47
+ "@nifrajs/client": "2.12.0",
48
+ "@nifrajs/agent": "2.12.0",
49
+ "@nifrajs/core": "2.12.0",
50
+ "@nifrajs/mcp": "2.12.0",
51
+ "@nifrajs/mock": "2.12.0"
48
52
  },
49
53
  "peerDependencies": {
50
54
  "zod": ">=4.0.0"
@@ -0,0 +1,174 @@
1
+ import {
2
+ type FailureDirective,
3
+ type FailureEvidence,
4
+ type FailureReplay,
5
+ type FailureScenario,
6
+ runFailureScenario,
7
+ } from "./failure-lab.ts"
8
+
9
+ /** A named adapter profile composed from the deterministic failure laboratory. */
10
+ export interface FaultProfile {
11
+ readonly name: string
12
+ readonly scenarios: readonly FaultProfileScenario[]
13
+ /** Optional trajectory faults consumed by the agent trajectory harness. */
14
+ readonly faults?: readonly FaultProfileFault[]
15
+ /** Existing deterministic failure-lab schedule shared by trajectory runs. */
16
+ readonly schedule?: readonly FailureDirective[]
17
+ }
18
+
19
+ export type FaultProfileFaultKind =
20
+ | "tool-error"
21
+ | "malformed-model-output"
22
+ | "budget-pressure"
23
+ | "approval-denial"
24
+ | "cancellation"
25
+
26
+ export interface FaultProfileFault {
27
+ readonly point: "model" | "tool" | "effect"
28
+ readonly kind: FaultProfileFaultKind
29
+ readonly occurrence?: number
30
+ }
31
+
32
+ export interface FaultProfileScenario extends FailureScenario<unknown> {
33
+ readonly id: string
34
+ readonly description: string
35
+ }
36
+
37
+ export interface FaultProfileScenarioReport {
38
+ readonly id: string
39
+ readonly description: string
40
+ readonly ok: boolean
41
+ readonly replay: FailureReplay
42
+ readonly evidence: readonly FailureEvidence[]
43
+ readonly error?: { readonly name: string; readonly kind?: string }
44
+ }
45
+
46
+ export interface FaultProfileReport {
47
+ readonly name: string
48
+ readonly ok: boolean
49
+ readonly scenarios: readonly FaultProfileScenarioReport[]
50
+ }
51
+
52
+ export interface RunFaultProfileOptions {
53
+ readonly seed?: number
54
+ readonly schedule?: readonly FailureDirective[]
55
+ }
56
+
57
+ const PROFILE_NAME = /^[a-z][a-z0-9._-]{0,127}$/
58
+
59
+ function validateProfile(profile: FaultProfile): FaultProfile {
60
+ if (!PROFILE_NAME.test(profile.name)) {
61
+ throw new TypeError(`fault profile: invalid name ${JSON.stringify(profile.name)}`)
62
+ }
63
+ if (!Array.isArray(profile.scenarios) || profile.scenarios.length === 0) {
64
+ throw new TypeError("fault profile: at least one scenario is required")
65
+ }
66
+ if (profile.faults !== undefined) {
67
+ const seenFaults = new Set<string>()
68
+ for (const fault of profile.faults) {
69
+ if (fault.point !== "model" && fault.point !== "tool" && fault.point !== "effect") {
70
+ throw new TypeError("fault profile: invalid trajectory fault point")
71
+ }
72
+ if (
73
+ ![
74
+ "tool-error",
75
+ "malformed-model-output",
76
+ "budget-pressure",
77
+ "approval-denial",
78
+ "cancellation",
79
+ ].includes(fault.kind)
80
+ ) {
81
+ throw new TypeError("fault profile: invalid trajectory fault kind")
82
+ }
83
+ if (
84
+ fault.occurrence !== undefined &&
85
+ (!Number.isSafeInteger(fault.occurrence) || fault.occurrence < 1)
86
+ ) {
87
+ throw new RangeError("fault profile: trajectory fault occurrence must be positive")
88
+ }
89
+ const identity = `${fault.point}:${fault.kind}:${fault.occurrence ?? 1}`
90
+ if (seenFaults.has(identity))
91
+ throw new Error(`fault profile: duplicate trajectory fault ${identity}`)
92
+ seenFaults.add(identity)
93
+ }
94
+ }
95
+ const ids = new Set<string>()
96
+ const scenarios = profile.scenarios.map((scenario) => {
97
+ if (!PROFILE_NAME.test(scenario.id)) {
98
+ throw new TypeError(`fault profile: invalid scenario id ${JSON.stringify(scenario.id)}`)
99
+ }
100
+ if (ids.has(scenario.id)) {
101
+ throw new Error(`fault profile: duplicate scenario id ${JSON.stringify(scenario.id)}`)
102
+ }
103
+ if (scenario.description.trim() === "") {
104
+ throw new Error(`fault profile: scenario ${JSON.stringify(scenario.id)} needs a description`)
105
+ }
106
+ if (typeof scenario.execute !== "function" || typeof scenario.verify !== "function") {
107
+ throw new TypeError(
108
+ `fault profile: scenario ${JSON.stringify(scenario.id)} is not executable`,
109
+ )
110
+ }
111
+ ids.add(scenario.id)
112
+ return Object.freeze({
113
+ ...scenario,
114
+ description: scenario.description.trim(),
115
+ })
116
+ })
117
+ return Object.freeze({
118
+ name: profile.name,
119
+ scenarios: Object.freeze(scenarios),
120
+ ...(profile.faults === undefined
121
+ ? {}
122
+ : { faults: Object.freeze(profile.faults.map((fault) => Object.freeze({ ...fault }))) }),
123
+ ...(profile.schedule === undefined ? {} : { schedule: Object.freeze([...profile.schedule]) }),
124
+ })
125
+ }
126
+
127
+ /** Validate and freeze a reusable fault profile. */
128
+ export function defineFaultProfile(profile: FaultProfile): FaultProfile {
129
+ return validateProfile(profile)
130
+ }
131
+
132
+ /** Run every profile scenario with the same deterministic seed and failure schedule. */
133
+ export async function runFaultProfile(
134
+ profile: FaultProfile,
135
+ options: RunFaultProfileOptions = {},
136
+ ): Promise<FaultProfileReport> {
137
+ const checked = validateProfile(profile)
138
+ const scenarios: FaultProfileScenarioReport[] = []
139
+ for (const scenario of checked.scenarios) {
140
+ const result = await runFailureScenario(scenario, {
141
+ ...(options.seed === undefined ? {} : { seed: options.seed }),
142
+ schedule: options.schedule ?? [],
143
+ })
144
+ scenarios.push({
145
+ id: scenario.id,
146
+ description: scenario.description,
147
+ ok: result.ok,
148
+ replay: result.replay,
149
+ evidence: result.evidence,
150
+ ...(result.error === undefined ? {} : { error: result.error }),
151
+ })
152
+ }
153
+ return Object.freeze({
154
+ name: checked.name,
155
+ ok: scenarios.every((scenario) => scenario.ok),
156
+ scenarios: Object.freeze(scenarios),
157
+ })
158
+ }
159
+
160
+ /** A small smoke profile for consumers that only need to verify the harness wiring. */
161
+ export const referenceFaultProfile = defineFaultProfile({
162
+ name: "reference",
163
+ scenarios: [
164
+ {
165
+ id: "checkpoint",
166
+ name: "reference-checkpoint",
167
+ description: "the reference adapter reaches a deterministic checkpoint",
168
+ execute(lab) {
169
+ lab.checkpoint("reference.checkpoint")
170
+ },
171
+ verify: () => true,
172
+ },
173
+ ],
174
+ })
@@ -0,0 +1,89 @@
1
+ import type { SealedEffectLedger } from "@nifrajs/core/ledger"
2
+
3
+ export type EffectLedger = SealedEffectLedger
4
+
5
+ export interface IdempotencyDivergence {
6
+ readonly step: number
7
+ readonly field: string
8
+ readonly first: unknown
9
+ readonly second: unknown
10
+ }
11
+
12
+ export interface IdempotencyProof {
13
+ readonly ok: boolean
14
+ readonly divergences: readonly IdempotencyDivergence[]
15
+ }
16
+
17
+ interface CanonicalLedger {
18
+ readonly method: string
19
+ readonly path: string
20
+ readonly declared: readonly string[]
21
+ readonly entries: readonly Readonly<Record<string, unknown>>[]
22
+ }
23
+
24
+ function canonicalize(ledger: EffectLedger): CanonicalLedger {
25
+ return {
26
+ method: ledger.method,
27
+ path: ledger.path,
28
+ declared: [...ledger.declared],
29
+ entries: ledger.entries.map((entry) => {
30
+ const value: Record<string, unknown> = {
31
+ seq: entry.seq,
32
+ ...(entry.effectId === undefined ? {} : { effectId: entry.effectId }),
33
+ capability: entry.capability,
34
+ phase: entry.phase,
35
+ ...(entry.target === undefined ? {} : { target: entry.target }),
36
+ ...(entry.cost === undefined ? {} : { cost: entry.cost }),
37
+ ...(entry.digest === undefined ? {} : { digest: entry.digest }),
38
+ ...(entry.error === undefined ? {} : { error: entry.error }),
39
+ }
40
+ return value
41
+ }),
42
+ }
43
+ }
44
+
45
+ function flatten(value: unknown, path = ""): Array<{ path: string; value: unknown }> {
46
+ if (Array.isArray(value))
47
+ return value.flatMap((item, index) => flatten(item, `${path}[${index}]`))
48
+ if (typeof value === "object" && value !== null) {
49
+ return Object.entries(value).flatMap(([key, item]) =>
50
+ flatten(item, path ? `${path}.${key}` : key),
51
+ )
52
+ }
53
+ return [{ path, value }]
54
+ }
55
+
56
+ /** Run a token-only effect workload repeatedly and report the first stable replay differences. */
57
+ export async function proveIdempotency(options: {
58
+ readonly run: () => Promise<EffectLedger> | EffectLedger
59
+ readonly runs?: number
60
+ }): Promise<IdempotencyProof> {
61
+ const runs = options.runs ?? 2
62
+ if (!Number.isSafeInteger(runs) || runs < 2)
63
+ throw new RangeError("idempotency runs must be at least 2")
64
+ const ledgers: CanonicalLedger[] = []
65
+ for (let index = 0; index < runs; index++) ledgers.push(canonicalize(await options.run()))
66
+ const first = flatten(ledgers[0])
67
+ const divergences: IdempotencyDivergence[] = []
68
+ for (let run = 1; run < ledgers.length; run++) {
69
+ const current = flatten(ledgers[run])
70
+ const max = Math.max(first.length, current.length)
71
+ for (let step = 0; step < max; step++) {
72
+ const left = first[step]
73
+ const right = current[step]
74
+ if (
75
+ left?.path === right?.path &&
76
+ JSON.stringify(left?.value) === JSON.stringify(right?.value)
77
+ )
78
+ continue
79
+ divergences.push({
80
+ step,
81
+ field: left?.path ?? right?.path ?? "ledger",
82
+ first: left?.value,
83
+ second: right?.value,
84
+ })
85
+ break
86
+ }
87
+ }
88
+ return Object.freeze({ ok: divergences.length === 0, divergences: Object.freeze(divergences) })
89
+ }
package/src/index.ts CHANGED
@@ -65,6 +65,24 @@ export {
65
65
  type FailureScenarioReport,
66
66
  runFailureScenario,
67
67
  } from "./failure-lab.ts"
68
+ export {
69
+ defineFaultProfile,
70
+ type FaultProfile,
71
+ type FaultProfileFault,
72
+ type FaultProfileFaultKind,
73
+ type FaultProfileReport,
74
+ type FaultProfileScenario,
75
+ type FaultProfileScenarioReport,
76
+ type RunFaultProfileOptions,
77
+ referenceFaultProfile,
78
+ runFaultProfile,
79
+ } from "./fault-profile.ts"
80
+ export {
81
+ type EffectLedger,
82
+ type IdempotencyDivergence,
83
+ type IdempotencyProof,
84
+ proveIdempotency,
85
+ } from "./idempotency.ts"
68
86
  export {
69
87
  assertIncidentReplays,
70
88
  type CapturedRequest,
@@ -82,3 +100,26 @@ export {
82
100
  shapeOf,
83
101
  } from "./incident.ts"
84
102
  export { type AppLike, type TestSession, type TestSessionOptions, testSession } from "./session.ts"
103
+ export {
104
+ httpToolAdapter,
105
+ inProcessToolAdapter,
106
+ mcpToolAdapter,
107
+ testToolAdapter,
108
+ } from "./tool-contract.ts"
109
+ export {
110
+ assertTrajectoryInvariants,
111
+ type CreateTrajectoryTranscriptOptions,
112
+ checkTrajectoryInvariants,
113
+ createTrajectoryTranscript,
114
+ type ReplayTrajectoryOptions,
115
+ recordTrajectory,
116
+ replayTrajectory,
117
+ runTrajectory,
118
+ type TrajectoryInvariantId,
119
+ type TrajectoryInvariantOptions,
120
+ type TrajectoryInvariantResult,
121
+ type TrajectoryReplayResult,
122
+ type TrajectoryRun,
123
+ type TrajectoryTranscript,
124
+ trajectoryRegressionId,
125
+ } from "./trajectory.ts"