@noego/testing 0.1.0 → 0.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/dist/index.js CHANGED
@@ -1,676 +1,655 @@
1
- // src/time/manual_clock.ts
2
- import { Clock, Instant } from "@noego/runtime";
3
- var ManualClock = class _ManualClock extends Clock {
4
- static defaultStart = Instant.ofEpochMilliseconds(Date.UTC(2020, 0, 1));
5
- current;
6
- constructor(start = _ManualClock.defaultStart) {
7
- super();
8
- this.current = start;
9
- }
10
- now() {
11
- return this.current;
12
- }
13
- set(instant) {
14
- if (instant.isBefore(this.current)) {
15
- throw new RangeError(`ManualClock cannot move backwards: ${this.current.toString()} -> ${instant.toString()}`);
16
- }
17
- this.current = instant;
18
- }
19
- advanceBy(duration) {
20
- this.current = this.current.plus(duration);
21
- }
22
- };
1
+ // src/builder.ts
2
+ import {
3
+ createContainer,
4
+ flattenModule,
5
+ LoadAs,
6
+ SCOPED_CONTAINER
7
+ } from "@noego/ioc";
23
8
 
24
- // src/time/manual_scheduler.ts
25
- import { Scheduler, brandId } from "@noego/runtime";
26
- var ManualScheduler = class extends Scheduler {
27
- constructor(clock) {
28
- super();
29
- this.clock = clock;
30
- }
31
- clock;
32
- tasks = [];
33
- nextSequence = 0;
34
- nextId = 0;
35
- schedule(input) {
36
- const id = brandId(`task-${++this.nextId}`, "ScheduledTask");
37
- const internal = {
38
- id,
39
- label: input.label,
40
- deadline: this.clock.now().plus(input.delay),
41
- sequence: this.nextSequence++,
42
- task: input.task,
43
- cancelled: false
44
- };
45
- this.tasks.push(internal);
46
- return {
47
- id,
48
- cancel: () => {
49
- internal.cancelled = true;
50
- }
51
- };
9
+ // src/errors.ts
10
+ var TestingError = class extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = new.target.name;
52
14
  }
53
- pending() {
54
- return this.live().sort(compareTasks).map(({ id, label, deadline }) => ({ id, label, deadline }));
55
- }
56
- /** Advance the clock, running every task due inside the window in order. */
57
- async advanceBy(duration) {
58
- const target = this.clock.now().plus(duration);
59
- for (; ; ) {
60
- const next = this.nextDue(target);
61
- if (next === null) break;
62
- if (next.deadline.isAfter(this.clock.now())) {
63
- this.clock.set(next.deadline);
64
- }
65
- await this.execute(next);
66
- }
67
- if (target.isAfter(this.clock.now())) {
68
- this.clock.set(target);
69
- }
15
+ };
16
+ var MissingIocSeamError = class extends TestingError {
17
+ constructor() {
18
+ super(
19
+ "@noego/testing requires an @noego/ioc version that provides Container.setInstanceDecorator (>= 0.5.x with the instance-decoration seam). Upgrade @noego/ioc."
20
+ );
70
21
  }
71
- /** Advance to the earliest pending deadline and run exactly that task. */
72
- async runNext() {
73
- const next = this.live().sort(compareTasks)[0];
74
- if (next === void 0) {
75
- throw new Error("ManualScheduler.runNext: no pending tasks");
76
- }
77
- if (next.deadline.isAfter(this.clock.now())) {
78
- this.clock.set(next.deadline);
79
- }
80
- await this.execute(next);
81
- }
82
- /** Run tasks (advancing time as needed) until none remain. */
83
- async runUntilIdle(options) {
84
- const maxTasks = options?.maxTasks ?? 1e4;
85
- let executed = 0;
86
- while (this.live().length > 0) {
87
- if (executed >= maxTasks) {
88
- const labels = this.pending().slice(0, 5).map((t) => t.label).join(", ");
89
- throw new Error(
90
- `ManualScheduler.runUntilIdle: still not idle after ${maxTasks} tasks \u2014 probable infinite reschedule loop (next: ${labels})`
91
- );
92
- }
93
- await this.runNext();
94
- executed += 1;
95
- }
22
+ };
23
+ var UnwatchedInspectionError = class extends TestingError {
24
+ constructor(token, method) {
25
+ super(
26
+ `Method "${method}" on ${token} is not watched in this environment. Only watched methods are inspectable \u2014 install test.watch() or any test.* behavior/expectation descriptor for it.`
27
+ );
96
28
  }
97
- /** Teardown assertion: fails when pending tasks remain, unless allowed. */
98
- assertNoPendingTasks(options) {
99
- if (options?.allow === true) return;
100
- const remaining = this.pending();
101
- if (remaining.length > 0) {
102
- const labels = remaining.map((t) => `${t.label} @ ${t.deadline.toString()}`).join("; ");
103
- throw new Error(`ManualScheduler: ${remaining.length} pending task(s) at teardown: ${labels}`);
104
- }
29
+ };
30
+ var CallScriptExhaustedError = class extends TestingError {
31
+ constructor(token, method, scriptLength, callIndex) {
32
+ super(
33
+ `Call #${callIndex} to ${token}.${method} exceeds its test.calls() script of ${scriptLength} ${scriptLength === 1 ? "entry" : "entries"}.`
34
+ );
105
35
  }
106
- live() {
107
- return this.tasks.filter((t) => !t.cancelled);
36
+ };
37
+ var ExpectationOverflowError = class extends TestingError {
38
+ constructor(token, method, expected, attempted) {
39
+ super(
40
+ expected === 0 ? `${token}.${method} was expected never to be called, but it was invoked.` : `${token}.${method} was expected exactly ${expected} ${expected === 1 ? "call" : "calls"}, but call #${attempted} arrived.`
41
+ );
108
42
  }
109
- nextDue(target) {
110
- const candidates = this.live().filter((t) => !t.deadline.isAfter(target)).sort(compareTasks);
111
- return candidates[0] ?? null;
43
+ };
44
+ var VerificationError = class extends TestingError {
45
+ constructor(failures) {
46
+ super(
47
+ "Exact method expectations were not satisfied:\n" + failures.map(
48
+ (f) => ` - ${f.token}.${f.method}: expected exactly ${f.expected} ${f.expected === 1 ? "call" : "calls"}, observed ${f.actual}`
49
+ ).join("\n")
50
+ );
112
51
  }
113
- async execute(task) {
114
- this.remove(task);
115
- await task.task();
52
+ };
53
+ var MethodNotCallableError = class extends TestingError {
54
+ constructor(token, method) {
55
+ super(
56
+ `Cannot install a test.* descriptor on ${token}.${method}: the resolved instance has no callable method with that name.`
57
+ );
116
58
  }
117
- remove(task) {
118
- const index = this.tasks.indexOf(task);
119
- this.tasks.splice(index, 1);
59
+ };
60
+ var NonObjectMethodTargetError = class extends TestingError {
61
+ constructor(token) {
62
+ super(
63
+ `.methods() is configured for ${token}, but that token resolved to a non-object value. Method descriptors apply only to IoC-managed instances.`
64
+ );
120
65
  }
121
66
  };
122
- function compareTasks(a, b) {
123
- const byDeadline = a.deadline.compareTo(b.deadline);
124
- return byDeadline !== 0 ? byDeadline : a.sequence - b.sequence;
125
- }
126
-
127
- // src/identity/sequence_id_generator.ts
128
- import { IdGenerator, brandId as brandId2 } from "@noego/runtime";
129
- var SequenceIdGenerator = class extends IdGenerator {
130
- counters = /* @__PURE__ */ new Map();
131
- next(brand) {
132
- const current = (this.counters.get(brand) ?? 0) + 1;
133
- this.counters.set(brand, current);
134
- return brandId2(`${brand.toLowerCase()}-${current}`, brand);
67
+ var UnknownTokenKeyError = class extends TestingError {
68
+ constructor(key, space, known) {
69
+ super(
70
+ `Unknown ${space} key "${key}" \u2014 it does not match any token known to this builder. Pass the class/token itself via a Map, or include the registration through .use(...). Known tokens: ` + (known.length ? known.join(", ") : "(none)")
71
+ );
135
72
  }
136
73
  };
74
+ var InvalidDescriptorError = class extends TestingError {
75
+ };
137
76
 
138
- // src/identity/seeded_random_source.ts
139
- import { RandomSource } from "@noego/runtime";
140
- var SeededRandomSource = class extends RandomSource {
141
- state;
142
- constructor(seed = 1) {
143
- super();
144
- if (!Number.isInteger(seed)) {
145
- throw new RangeError(`SeededRandomSource seed must be an integer, got ${seed}`);
146
- }
147
- this.state = seed >>> 0;
148
- }
149
- nextUint32() {
150
- this.state = this.state + 1831565813 >>> 0;
151
- let t = this.state;
152
- t = Math.imul(t ^ t >>> 15, t | 1);
153
- t ^= t + Math.imul(t ^ t >>> 7, t | 61);
154
- return (t ^ t >>> 14) >>> 0;
155
- }
156
- bytes(length) {
157
- if (!Number.isInteger(length) || length < 0) {
158
- throw new RangeError(`bytes length must be a non-negative integer, got ${length}`);
159
- }
160
- const out = new Uint8Array(length);
161
- for (let i = 0; i < length; i += 1) {
162
- out[i] = this.nextUint32() & 255;
163
- }
164
- return out;
77
+ // src/descriptors.ts
78
+ var DESCRIPTOR = /* @__PURE__ */ Symbol.for("noego:testing:descriptor");
79
+ function isDescriptor(value) {
80
+ return typeof value === "object" && value !== null && value[DESCRIPTOR] === true;
81
+ }
82
+ function frozen(value) {
83
+ return Object.freeze(value);
84
+ }
85
+ function assertBehavior(value, where) {
86
+ if (!isDescriptor(value) || !["returns", "throws", "original", "calls"].includes(value.kind)) {
87
+ throw new InvalidDescriptorError(
88
+ `${where} requires a behavior descriptor (test.returns/throws/original/calls).`
89
+ );
165
90
  }
166
- integer(minInclusive, maxExclusive) {
167
- if (maxExclusive <= minInclusive) {
168
- throw new RangeError(`integer range is empty: [${minInclusive}, ${maxExclusive})`);
91
+ }
92
+ var test = {
93
+ /** Return the supplied value when the method is called. Auto-watches. */
94
+ returns(value) {
95
+ return frozen({ [DESCRIPTOR]: true, kind: "returns", value });
96
+ },
97
+ /** Throw/reject with the supplied error. Auto-watches. */
98
+ throws(error) {
99
+ return frozen({ [DESCRIPTOR]: true, kind: "throws", error });
100
+ },
101
+ /** Invoke the original effective method. Auto-watches. */
102
+ original() {
103
+ return frozen({ [DESCRIPTOR]: true, kind: "original" });
104
+ },
105
+ /**
106
+ * Per-invocation behavior script: call 1 uses entry 1, and so on. A call
107
+ * after exhaustion fails immediately. Unused entries do not fail verification.
108
+ */
109
+ calls(script) {
110
+ if (!Array.isArray(script)) {
111
+ throw new InvalidDescriptorError("test.calls() requires an array of behavior descriptors.");
169
112
  }
170
- const span = maxExclusive - minInclusive;
171
- return minInclusive + this.nextUint32() % span;
172
- }
173
- };
174
-
175
- // src/network/scripted_fetch_client.ts
176
- import { FetchClient, FetchError } from "@noego/runtime";
177
- var ScriptedFetchClient = class extends FetchClient {
178
- script;
179
- cursor = 0;
180
- executed = [];
181
- constructor(script) {
182
- super();
183
- this.script = [...script];
184
- }
185
- execute(request) {
186
- const entry = this.script[this.cursor];
187
- if (entry === void 0) {
188
- return Promise.reject(
189
- new Error(`ScriptedFetchClient: unexpected call #${this.cursor + 1} (${request.method} ${request.url}); script is exhausted`)
190
- );
113
+ script.forEach((entry, i) => assertBehavior(entry, `test.calls() entry #${i + 1}`));
114
+ return frozen({ [DESCRIPTOR]: true, kind: "calls", script: Object.freeze([...script]) });
115
+ },
116
+ /**
117
+ * Keep original behavior and record calls. With a raw wrapper argument, the
118
+ * wrapper's behavior runs and is recorded.
119
+ */
120
+ watch(wrapper) {
121
+ if (wrapper !== void 0 && typeof wrapper !== "function") {
122
+ throw new InvalidDescriptorError("test.watch() accepts only a raw wrapper function.");
191
123
  }
192
- if (!entry.matches(request)) {
193
- return Promise.reject(
194
- new Error(`ScriptedFetchClient: call #${this.cursor + 1} (${request.method} ${request.url}) does not match script entry "${entry.describe}"`)
124
+ return frozen({ [DESCRIPTOR]: true, kind: "watch", wrapper });
125
+ },
126
+ /** Require exactly one call; with no behavior, the original runs. */
127
+ once(behavior) {
128
+ if (behavior !== void 0) assertBehavior(behavior, "test.once()");
129
+ return frozen({ [DESCRIPTOR]: true, kind: "expect", expected: 1, behavior });
130
+ },
131
+ /** Require exactly `count` calls; with no behavior, the original runs. */
132
+ times(count, behavior) {
133
+ if (!Number.isInteger(count) || count < 0) {
134
+ throw new InvalidDescriptorError(
135
+ `test.times() requires a non-negative integer count, received ${String(count)}.`
195
136
  );
196
137
  }
197
- this.cursor += 1;
198
- this.executed.push(request);
199
- const outcome = entry.respond(request);
200
- return outcome instanceof FetchError ? Promise.reject(outcome) : Promise.resolve(outcome);
201
- }
202
- /** Teardown assertion: every scripted call must have been consumed. */
203
- assertScriptConsumed() {
204
- if (this.cursor < this.script.length) {
205
- const remaining = this.script.slice(this.cursor).map((e) => e.describe).join(", ");
206
- throw new Error(`ScriptedFetchClient: ${this.script.length - this.cursor} unconsumed script entr(ies): ${remaining}`);
138
+ if (behavior !== void 0) assertBehavior(behavior, "test.times()");
139
+ return frozen({ [DESCRIPTOR]: true, kind: "expect", expected: count, behavior });
140
+ },
141
+ /** Require zero calls; the first invocation fails and skips the original. */
142
+ never() {
143
+ return frozen({ [DESCRIPTOR]: true, kind: "expect", expected: 0 });
144
+ },
145
+ /** Read the recorded history for a watched method in one environment. */
146
+ inspect(environment, token, method) {
147
+ const registry = environment?.[ENV_REGISTRY];
148
+ if (!registry) {
149
+ throw new InvalidDescriptorError(
150
+ "test.inspect() requires a built @noego/testing environment as its first argument."
151
+ );
207
152
  }
153
+ return registry.inspect(token, method);
208
154
  }
209
155
  };
156
+ Object.freeze(test);
157
+ var ENV_REGISTRY = /* @__PURE__ */ Symbol.for("noego:testing:env-registry");
210
158
 
211
- // src/process/scripted_process_runner.ts
212
- import { ProcessRunner } from "@noego/runtime";
213
- var ScriptedProcessRunner = class extends ProcessRunner {
214
- script;
215
- cursor = 0;
216
- openHandles = /* @__PURE__ */ new Set();
217
- constructor(script) {
218
- super();
219
- this.script = [...script];
220
- }
221
- consume(command, kind) {
222
- const entry = this.script[this.cursor];
223
- if (entry === void 0) {
224
- throw new Error(`ScriptedProcessRunner: unexpected ${kind} of "${command.executable}"; script is exhausted`);
225
- }
226
- if (!entry.matches(command)) {
227
- throw new Error(`ScriptedProcessRunner: ${kind} of "${command.executable}" does not match script entry "${entry.describe}"`);
228
- }
229
- this.cursor += 1;
230
- return entry;
159
+ // src/method_state.ts
160
+ var CONTEXT_WRAPPED = /* @__PURE__ */ Symbol.for("ioc:context-wrapped");
161
+ var CONTEXT_OWNER = /* @__PURE__ */ Symbol.for("ioc:context-owner");
162
+ function tokenLabel(token) {
163
+ if (typeof token === "function") return token.name || "[anonymous class]";
164
+ if (typeof token === "symbol") return String(token);
165
+ return String(token);
166
+ }
167
+ var MethodState = class {
168
+ constructor(tokenName, method, descriptor) {
169
+ this.tokenName = tokenName;
170
+ this.method = method;
171
+ this.descriptor = descriptor;
172
+ }
173
+ tokenName;
174
+ method;
175
+ descriptor;
176
+ calls = [];
177
+ /** Actual invocation count (includes the call currently executing). */
178
+ actual = 0;
179
+ /** Cursor into a test.calls() script. */
180
+ scriptCursor = 0;
181
+ get expectation() {
182
+ return this.descriptor.kind === "expect" ? this.descriptor : void 0;
183
+ }
184
+ inspection() {
185
+ return {
186
+ count: this.calls.length,
187
+ calls: this.calls.map((c) => ({ ...c, args: [...c.args] }))
188
+ };
231
189
  }
232
- run(command) {
233
- try {
234
- const entry = this.consume(command, "run");
235
- return Promise.resolve(foldEvents(entry.events));
236
- } catch (error) {
237
- return Promise.reject(error);
190
+ };
191
+ var WatchRegistry = class {
192
+ /** entry-identity method → state. Entries share states across instances. */
193
+ states = /* @__PURE__ */ new Map();
194
+ nameIndex = /* @__PURE__ */ new Map();
195
+ entries = [];
196
+ addEntry(entry) {
197
+ this.entries.push(entry);
198
+ const byMethod = /* @__PURE__ */ new Map();
199
+ for (const [method, descriptor] of entry.methods) {
200
+ if (typeof descriptor === "function") continue;
201
+ byMethod.set(method, new MethodState(this.entryLabel(entry), method, descriptor));
238
202
  }
239
- }
240
- spawn(command) {
241
- const entry = this.consume(command, "spawn");
242
- const handle = new ScriptedProcessHandle(command.label, entry.events, () => {
243
- this.openHandles.delete(handle);
244
- });
245
- this.openHandles.add(handle);
246
- return handle;
247
- }
248
- /** Teardown assertion: no spawned process may remain open. */
249
- assertNoOpenHandles() {
250
- if (this.openHandles.size > 0) {
251
- const labels = [...this.openHandles].map((h) => h.label).join(", ");
252
- throw new Error(`ScriptedProcessRunner: ${this.openHandles.size} open process handle(s) at teardown: ${labels}`);
203
+ this.states.set(entry.key, byMethod);
204
+ if (entry.byName) this.nameIndex.set(entry.key, entry.key);
205
+ else if (typeof entry.key === "function" && entry.key.name) {
206
+ this.nameIndex.set(entry.key.name, entry.key);
253
207
  }
254
208
  }
255
- };
256
- var ScriptedProcessHandle = class {
257
- constructor(label, events, onClosed) {
258
- this.label = label;
259
- this.events = events;
260
- this.onClosed = onClosed;
261
- this.resultPromise = new Promise((resolve) => {
262
- this.resolveResult = resolve;
263
- });
264
- }
265
- label;
266
- events;
267
- onClosed;
268
- listeners = [];
269
- resultPromise;
270
- resolveResult;
271
- flushed = false;
272
- killed = false;
273
- onEvent(listener) {
274
- this.listeners.push(listener);
275
- }
276
- /** Deliver all scripted events now (deterministic, test-controlled). */
277
- flush() {
278
- if (this.flushed || this.killed) return;
279
- this.flushed = true;
280
- for (const event of this.events) {
281
- for (const listener of this.listeners) listener(event);
282
- if (event.kind === "exit") {
283
- this.resolveResult(foldEvents(this.events));
284
- this.onClosed();
209
+ entryLabel(entry) {
210
+ return entry.byName ? String(entry.key) : tokenLabel(entry.key);
211
+ }
212
+ /** Entries applying to a resolving token (exact identity or name match). */
213
+ matchEntries(token) {
214
+ const name = typeof token === "function" ? token.name : typeof token === "string" ? token : void 0;
215
+ return this.entries.filter(
216
+ (entry) => entry.key === token || entry.byName && name !== void 0 && entry.key === name
217
+ );
218
+ }
219
+ state(entryKey, method) {
220
+ return this.states.get(entryKey)?.get(method);
221
+ }
222
+ inspect(token, method) {
223
+ const label = tokenLabel(token);
224
+ const keys = [token];
225
+ const name = typeof token === "function" ? token.name : typeof token === "string" ? token : void 0;
226
+ if (name !== void 0 && this.nameIndex.has(name)) keys.push(this.nameIndex.get(name));
227
+ for (const key of keys) {
228
+ const state = this.states.get(key)?.get(method);
229
+ if (state) return state.inspection();
230
+ }
231
+ throw new UnwatchedInspectionError(label, method);
232
+ }
233
+ /** Repeatable snapshot check of all exact expectations. */
234
+ verify() {
235
+ const failures = [];
236
+ for (const byMethod of this.states.values()) {
237
+ for (const state of byMethod.values()) {
238
+ const expectation = state.expectation;
239
+ if (!expectation) continue;
240
+ if (state.actual !== expectation.expected) {
241
+ failures.push({
242
+ token: state.tokenName,
243
+ method: state.method,
244
+ expected: expectation.expected,
245
+ actual: state.actual
246
+ });
247
+ }
285
248
  }
286
249
  }
287
- }
288
- kill(signal = "SIGTERM") {
289
- if (this.flushed || this.killed) return;
290
- this.killed = true;
291
- const exit = { kind: "exit", exitCode: null, signal };
292
- for (const listener of this.listeners) listener(exit);
293
- this.resolveResult({ exitCode: null, signal, stdout: "", stderr: "" });
294
- this.onClosed();
295
- }
296
- wait() {
297
- return this.resultPromise;
250
+ if (failures.length) throw new VerificationError(failures);
298
251
  }
299
252
  };
300
- function foldEvents(events) {
301
- let stdout = "";
302
- let stderr = "";
303
- let exitCode = null;
304
- let signal = null;
305
- for (const event of events) {
306
- if (event.kind === "stdout") stdout += event.chunk;
307
- else if (event.kind === "stderr") stderr += event.chunk;
308
- else {
309
- exitCode = event.exitCode;
310
- signal = event.signal;
311
- }
253
+ function isPromiseLike(value) {
254
+ return !!value && typeof value.then === "function";
255
+ }
256
+ function recordOutcome(call, outcome, threw) {
257
+ if (!threw && isPromiseLike(outcome)) {
258
+ call.pending = true;
259
+ outcome.then(
260
+ (value) => {
261
+ call.result = value;
262
+ call.pending = false;
263
+ },
264
+ (error) => {
265
+ call.error = error;
266
+ call.pending = false;
267
+ }
268
+ );
269
+ return outcome;
312
270
  }
313
- return { exitCode, signal, stdout, stderr };
271
+ call.pending = false;
272
+ if (threw) call.error = outcome;
273
+ else call.result = outcome;
274
+ return outcome;
314
275
  }
315
-
316
- // src/storage/memory_key_value_store.ts
317
- import { KeyValueStore } from "@noego/runtime";
318
- var MemoryKeyValueStore = class extends KeyValueStore {
319
- constructor(clock) {
320
- super();
321
- this.clock = clock;
322
- }
323
- clock;
324
- entries = /* @__PURE__ */ new Map();
325
- get(key, codec) {
326
- const entry = this.entries.get(key);
327
- if (entry === void 0) return Promise.resolve(null);
328
- if (entry.expiresAt !== null && !this.clock.now().isBefore(entry.expiresAt)) {
329
- this.entries.delete(key);
330
- return Promise.resolve(null);
276
+ function createMethodDecorator(registry) {
277
+ const wrappers = /* @__PURE__ */ new WeakMap();
278
+ return function decorate(instance, token) {
279
+ const entries = registry.matchEntries(token);
280
+ if (entries.length === 0) return instance;
281
+ if (instance === null || typeof instance !== "object" && typeof instance !== "function") {
282
+ throw new NonObjectMethodTargetError(tokenLabel(token));
331
283
  }
332
- return Promise.resolve(codec.decode(entry.encoded));
333
- }
334
- put(key, value, codec, options) {
335
- const expiresAt = options?.timeToLive !== void 0 ? this.clock.now().plus(options.timeToLive) : null;
336
- this.entries.set(key, { encoded: codec.encode(value), expiresAt });
337
- return Promise.resolve();
338
- }
339
- delete(key) {
340
- this.entries.delete(key);
341
- return Promise.resolve();
342
- }
343
- };
344
-
345
- // src/storage/memory_object_store.ts
346
- import { ObjectStore, brandId as brandId3 } from "@noego/runtime";
347
- var MemoryObjectStore = class extends ObjectStore {
348
- objects = /* @__PURE__ */ new Map();
349
- nextVersion = 0;
350
- read(key) {
351
- return Promise.resolve(this.objects.get(key) ?? null);
352
- }
353
- write(key, object) {
354
- const version = brandId3(`v${++this.nextVersion}`, "ObjectVersion");
355
- this.objects.set(key, {
356
- bytes: object.bytes.slice(),
357
- contentType: object.contentType,
358
- metadata: object.metadata !== void 0 ? new Map(object.metadata) : void 0,
359
- version
360
- });
361
- return Promise.resolve(version);
362
- }
363
- delete(key) {
364
- this.objects.delete(key);
365
- return Promise.resolve();
366
- }
367
- };
368
-
369
- // src/events/recording_event_bus.ts
370
- import { EventBus } from "@noego/runtime";
371
- var RecordingEventBus = class extends EventBus {
372
- constructor(mode = "immediate") {
373
- super();
374
- this.mode = mode;
375
- }
376
- mode;
377
- published = [];
378
- handlers = /* @__PURE__ */ new Set();
379
- queue = [];
380
- async publish(event) {
381
- this.published.push(event);
382
- if (this.mode === "queued") {
383
- this.queue.push(event);
384
- return;
284
+ if (wrappers.has(instance)) return wrappers.get(instance);
285
+ const effective = /* @__PURE__ */ new Map();
286
+ for (const entry of entries) {
287
+ for (const [method, descriptor] of entry.methods) {
288
+ effective.set(method, { entryKey: entry.key, descriptor });
289
+ }
385
290
  }
386
- await this.deliver(event);
387
- }
388
- subscribe(handler) {
389
- this.handlers.add(handler);
390
- const bus = this;
391
- const subscription = {
392
- active: true,
393
- unsubscribe() {
394
- if (!subscription.active) return;
395
- subscription.active = false;
396
- bus.handlers.delete(handler);
291
+ for (const method of effective.keys()) {
292
+ if (typeof instance[method] !== "function") {
293
+ throw new MethodNotCallableError(tokenLabel(token), method);
397
294
  }
398
- };
399
- return subscription;
400
- }
401
- /** Deliver buffered events (queued mode) in publication order. */
402
- async deliverQueued() {
403
- while (this.queue.length > 0) {
404
- const event = this.queue.shift();
405
- await this.deliver(event);
406
295
  }
296
+ const methodCache = /* @__PURE__ */ new Map();
297
+ const proxy = new Proxy(instance, {
298
+ get(target, prop, receiver) {
299
+ if (typeof prop === "string" && effective.has(prop)) {
300
+ let wrapped = methodCache.get(prop);
301
+ if (!wrapped) {
302
+ const { entryKey, descriptor } = effective.get(prop);
303
+ wrapped = buildMethodWrapper(target, prop, descriptor, registry.state(entryKey, prop));
304
+ methodCache.set(prop, wrapped);
305
+ }
306
+ return wrapped;
307
+ }
308
+ if (prop === CONTEXT_WRAPPED || prop === CONTEXT_OWNER) return target[prop];
309
+ return Reflect.get(target, prop, receiver);
310
+ }
311
+ });
312
+ wrappers.set(instance, proxy);
313
+ return proxy;
314
+ };
315
+ }
316
+ function buildMethodWrapper(target, method, descriptor, state) {
317
+ const callOriginal = (self, args) => {
318
+ const fn = target[method];
319
+ return fn.apply(self === void 0 ? target : self, args);
320
+ };
321
+ if (typeof descriptor === "function") {
322
+ const replacement = descriptor((...args) => callOriginal(target, args));
323
+ return function(...args) {
324
+ return replacement.apply(this, args);
325
+ };
407
326
  }
408
- get activeSubscriptionCount() {
409
- return this.handlers.size;
327
+ if (!isDescriptor(descriptor)) {
328
+ throw new TypeError(`Invalid method descriptor for ${method}`);
410
329
  }
411
- assertNoActiveSubscriptions() {
412
- if (this.handlers.size > 0) {
413
- throw new Error(`RecordingEventBus: ${this.handlers.size} active subscription(s) at teardown`);
330
+ return function(...args) {
331
+ if (!state) throw new TypeError(`Missing method state for ${method}`);
332
+ const expectation = state.expectation;
333
+ const attempted = state.actual + 1;
334
+ if (expectation && attempted > expectation.expected) {
335
+ state.actual = attempted;
336
+ throw new ExpectationOverflowError(state.tokenName, method, expectation.expected, attempted);
414
337
  }
415
- }
416
- async deliver(event) {
417
- for (const handler of [...this.handlers]) {
418
- await handler(event);
338
+ state.actual = attempted;
339
+ const call = {
340
+ index: attempted,
341
+ args: [...args],
342
+ pending: false,
343
+ timestamp: Date.now()
344
+ };
345
+ state.calls.push(call);
346
+ let behavior;
347
+ const base = descriptor.kind === "expect" ? descriptor.behavior ?? "original-effective" : descriptor.kind === "watch" ? descriptor.wrapper ?? "original-effective" : descriptor;
348
+ if (base !== "original-effective" && typeof base !== "function" && base !== void 0 && base.kind === "calls") {
349
+ if (state.scriptCursor >= base.script.length) {
350
+ throw new CallScriptExhaustedError(state.tokenName, method, base.script.length, attempted);
351
+ }
352
+ behavior = base.script[state.scriptCursor];
353
+ state.scriptCursor += 1;
354
+ } else {
355
+ behavior = base;
419
356
  }
420
- }
421
- };
422
-
423
- // src/observability/recording_sinks.ts
424
- import { LogSink, TelemetrySink, TraceSink } from "@noego/runtime";
425
- var RecordingLogSink = class extends LogSink {
426
- envelopes = [];
427
- emit(envelope) {
428
- this.envelopes.push(envelope);
429
- }
430
- };
431
- var RecordingTraceSink = class extends TraceSink {
432
- envelopes = [];
433
- emit(envelope) {
434
- this.envelopes.push(envelope);
435
- }
436
- };
437
- var RecordingTelemetrySink = class extends TelemetrySink {
438
- envelopes = [];
439
- emit(envelope) {
440
- this.envelopes.push(envelope);
441
- }
442
- };
443
- var NoopLogSink = class extends LogSink {
444
- emit(_envelope) {
445
- }
446
- };
447
-
448
- // src/contracts/clock.contract.ts
449
- function runClockContract(name, setup) {
450
- describe(`Clock contract: ${name}`, () => {
451
- it("returns a monotonically non-decreasing instant across consecutive reads", () => {
452
- const { clock } = setup();
453
- const first = clock.now();
454
- const second = clock.now();
455
- expect(second.isBefore(first)).toBe(false);
456
- });
457
- it("nowMs agrees with now()", () => {
458
- const { clock } = setup();
459
- const instant = clock.now();
460
- const ms = clock.nowMs();
461
- expect(ms).toBeGreaterThanOrEqual(instant.epochMilliseconds);
462
- });
463
- });
357
+ try {
358
+ let outcome;
359
+ if (behavior === "original-effective") {
360
+ outcome = callOriginal(this, args);
361
+ } else if (typeof behavior === "function") {
362
+ outcome = behavior((...inner) => callOriginal(this, inner)).apply(this, args);
363
+ } else if (behavior.kind === "returns") {
364
+ outcome = behavior.value;
365
+ } else if (behavior.kind === "throws") {
366
+ throw behavior.error;
367
+ } else if (behavior.kind === "original") {
368
+ outcome = callOriginal(this, args);
369
+ } else {
370
+ throw new TypeError(`Nested test.calls() scripts are not supported (${method}).`);
371
+ }
372
+ return recordOutcome(call, outcome, false);
373
+ } catch (error) {
374
+ recordOutcome(call, error, true);
375
+ throw error;
376
+ }
377
+ };
464
378
  }
465
379
 
466
- // src/contracts/key_value_store.contract.ts
467
- import { Duration as Duration3, jsonCodec, storageKey } from "@noego/runtime";
468
- function runKeyValueStoreContract(name, setup) {
469
- const codec = jsonCodec();
470
- describe(`KeyValueStore contract: ${name}`, () => {
471
- it("returns null for a missing key", async () => {
472
- const { store } = setup();
473
- await expect(store.get(storageKey("missing"), codec)).resolves.toBeNull();
474
- });
475
- it("round-trips a stored value", async () => {
476
- const { store } = setup();
477
- const key = storageKey("k1");
478
- await store.put(key, { n: 42 }, codec);
479
- await expect(store.get(key, codec)).resolves.toEqual({ n: 42 });
480
- });
481
- it("overwrites an existing value", async () => {
482
- const { store } = setup();
483
- const key = storageKey("k1");
484
- await store.put(key, { n: 1 }, codec);
485
- await store.put(key, { n: 2 }, codec);
486
- await expect(store.get(key, codec)).resolves.toEqual({ n: 2 });
487
- });
488
- it("deletes a value and tolerates deleting a missing key", async () => {
489
- const { store } = setup();
490
- const key = storageKey("k1");
491
- await store.put(key, { n: 1 }, codec);
492
- await store.delete(key);
493
- await expect(store.get(key, codec)).resolves.toBeNull();
494
- await expect(store.delete(storageKey("missing"))).resolves.toBeUndefined();
495
- });
496
- it("expires values after their TTL when TTL is supported", async () => {
497
- const context = setup();
498
- if (!context.supportsTtl) return;
499
- const key = storageKey("expiring");
500
- await context.store.put(key, { n: 7 }, codec, { timeToLive: Duration3.ofSeconds(30) });
501
- await expect(context.store.get(key, codec)).resolves.toEqual({ n: 7 });
502
- await context.advanceBy(Duration3.ofSeconds(31));
503
- await expect(context.store.get(key, codec)).resolves.toBeNull();
504
- });
505
- it("keeps values without a TTL alive as time passes", async () => {
506
- const context = setup();
507
- const key = storageKey("durable");
508
- await context.store.put(key, { n: 9 }, codec);
509
- await context.advanceBy(Duration3.ofMinutes(60));
510
- await expect(context.store.get(key, codec)).resolves.toEqual({ n: 9 });
511
- });
512
- });
380
+ // src/builder.ts
381
+ var COMPONENT_OPTIONS_KEY = /* @__PURE__ */ Symbol.for("ioc:component:options");
382
+ function* configEntries(config) {
383
+ if (config instanceof Map) {
384
+ for (const [key, value] of config) yield [key, false, value];
385
+ } else {
386
+ for (const key of [...Object.getOwnPropertyNames(config), ...Object.getOwnPropertySymbols(config)]) {
387
+ yield [key, typeof key === "string", config[key]];
388
+ }
389
+ }
513
390
  }
514
-
515
- // src/contracts/object_store.contract.ts
516
- import { objectKey } from "@noego/runtime";
517
- function runObjectStoreContract(name, setup) {
518
- const bytes = (...values) => new Uint8Array(values);
519
- describe(`ObjectStore contract: ${name}`, () => {
520
- it("returns null for a missing object", async () => {
521
- const { store } = setup();
522
- await expect(store.read(objectKey("missing"))).resolves.toBeNull();
523
- });
524
- it("round-trips bytes, content type, and metadata", async () => {
525
- const { store } = setup();
526
- const key = objectKey("docs/a.bin");
527
- await store.write(key, {
528
- bytes: bytes(1, 2, 3),
529
- contentType: "application/octet-stream",
530
- metadata: /* @__PURE__ */ new Map([["owner", "alice"]])
531
- });
532
- const stored = await store.read(key);
533
- expect(stored).not.toBeNull();
534
- expect([...stored.bytes]).toEqual([1, 2, 3]);
535
- expect(stored.contentType).toBe("application/octet-stream");
536
- expect(stored.metadata?.get("owner")).toBe("alice");
537
- });
538
- it("issues a new version on every write", async () => {
539
- const { store } = setup();
540
- const key = objectKey("docs/a.bin");
541
- const v1 = await store.write(key, { bytes: bytes(1), contentType: "text/plain" });
542
- const v2 = await store.write(key, { bytes: bytes(2), contentType: "text/plain" });
543
- expect(v1).not.toBe(v2);
544
- const stored = await store.read(key);
545
- expect(stored.version).toBe(v2);
546
- expect([...stored.bytes]).toEqual([2]);
547
- });
548
- it("deletes an object and tolerates deleting a missing key", async () => {
549
- const { store } = setup();
550
- const key = objectKey("docs/a.bin");
551
- await store.write(key, { bytes: bytes(1), contentType: "text/plain" });
552
- await store.delete(key);
553
- await expect(store.read(key)).resolves.toBeNull();
554
- await expect(store.delete(objectKey("missing"))).resolves.toBeUndefined();
555
- });
556
- it("does not expose caller mutations of written bytes", async () => {
557
- const { store } = setup();
558
- const key = objectKey("docs/mutable.bin");
559
- const input = bytes(9);
560
- await store.write(key, { bytes: input, contentType: "text/plain" });
561
- input[0] = 0;
562
- const stored = await store.read(key);
563
- expect([...stored.bytes]).toEqual([9]);
564
- });
565
- });
391
+ function componentScope(cls) {
392
+ const options = typeof Reflect !== "undefined" && Reflect.getMetadata ? Reflect.getMetadata(COMPONENT_OPTIONS_KEY, cls) : void 0;
393
+ return options?.scope;
566
394
  }
567
-
568
- // src/contracts/event_bus.contract.ts
569
- var testEvent = (value) => ({ type: "test-event", value });
570
- function runEventBusContract(name, setup) {
571
- describe(`EventBus contract: ${name}`, () => {
572
- it("delivers published events to subscribers in order", async () => {
573
- const { bus, deliver } = setup();
574
- const seen = [];
575
- const subscription = bus.subscribe((event) => {
576
- seen.push(event.value);
577
- });
578
- await bus.publish(testEvent(1));
579
- await bus.publish(testEvent(2));
580
- await deliver();
581
- expect(seen).toEqual([1, 2]);
582
- subscription.unsubscribe();
583
- });
584
- it("stops delivering after unsubscribe", async () => {
585
- const { bus, deliver } = setup();
586
- const seen = [];
587
- const subscription = bus.subscribe((event) => {
588
- seen.push(event.value);
589
- });
590
- await bus.publish(testEvent(1));
591
- await deliver();
592
- subscription.unsubscribe();
593
- await bus.publish(testEvent(2));
594
- await deliver();
595
- expect(seen).toEqual([1]);
596
- });
597
- it("supports multiple subscribers", async () => {
598
- const { bus, deliver } = setup();
599
- const a = [];
600
- const b = [];
601
- const sa = bus.subscribe((event) => {
602
- a.push(event.value);
603
- });
604
- const sb = bus.subscribe((event) => {
605
- b.push(event.value);
606
- });
607
- await bus.publish(testEvent(5));
608
- await deliver();
609
- expect(a).toEqual([5]);
610
- expect(b).toEqual([5]);
611
- sa.unsubscribe();
612
- sb.unsubscribe();
613
- });
614
- it("marks subscriptions inactive after unsubscribe and tolerates double unsubscribe", async () => {
615
- const { bus, deliver } = setup();
616
- const seen = [];
617
- const subscription = bus.subscribe((event) => {
618
- seen.push(event.value);
619
- });
620
- await bus.publish(testEvent(1));
621
- await deliver();
622
- expect(seen).toEqual([1]);
623
- expect(subscription.active).toBe(true);
624
- subscription.unsubscribe();
625
- expect(subscription.active).toBe(false);
626
- subscription.unsubscribe();
627
- expect(subscription.active).toBe(false);
628
- });
629
- });
395
+ function lifetimeToLoadAs(lifetime) {
396
+ if (lifetime === "singleton") return LoadAs.Singleton;
397
+ if (lifetime === "scoped") return LoadAs.Scoped;
398
+ return LoadAs.Transient;
630
399
  }
631
-
632
- // src/leak/leak_detector.ts
633
- var LeakDetector = class {
634
- checks = [];
635
- register(check) {
636
- this.checks.push(check);
637
- }
638
- findLeaks() {
639
- const leaks = [];
640
- for (const { name, check } of this.checks) {
641
- for (const description of check()) {
642
- leaks.push(`[${name}] ${description}`);
400
+ var TestIocBuilder = class _TestIocBuilder {
401
+ constructor(log) {
402
+ this.log = log;
403
+ Object.freeze(this);
404
+ }
405
+ log;
406
+ /** @internal */
407
+ static create(inputs) {
408
+ return new _TestIocBuilder([]).useAll(inputs);
409
+ }
410
+ /** @internal read by .use(builderPreset) */
411
+ get writes() {
412
+ return this.log;
413
+ }
414
+ derive(writes) {
415
+ return new _TestIocBuilder([...this.log, ...writes]);
416
+ }
417
+ useAll(inputs) {
418
+ let builder = this;
419
+ for (const input of inputs) builder = builder.use(input);
420
+ return builder;
421
+ }
422
+ /** Apply a reusable composition preset: an ApplicationModule or a builder. */
423
+ use(preset) {
424
+ if (preset instanceof _TestIocBuilder) {
425
+ return this.derive([...preset.writes]);
426
+ }
427
+ return this.derive([{ op: "use", module: preset }]);
428
+ }
429
+ /** Replace the implementation for IoC class tokens in the built environment. */
430
+ classes(config) {
431
+ const writes = [];
432
+ for (const [key, byName, implementation] of configEntries(config)) {
433
+ if (typeof implementation !== "function" || !implementation.prototype) {
434
+ throw new InvalidDescriptorError(
435
+ `.classes() value for "${tokenLabel(key)}" must be a class constructor.`
436
+ );
437
+ }
438
+ writes.push({ op: "classes", key, byName, implementation });
439
+ }
440
+ return this.derive(writes);
441
+ }
442
+ /** Replace IoC factory/provider registrations. */
443
+ functions(config) {
444
+ const writes = [];
445
+ for (const [key, byName, factory] of configEntries(config)) {
446
+ if (typeof factory !== "function") {
447
+ throw new InvalidDescriptorError(`.functions() value for "${tokenLabel(key)}" must be a function.`);
643
448
  }
449
+ writes.push({ op: "functions", key, byName, factory });
644
450
  }
645
- return leaks;
451
+ return this.derive(writes);
646
452
  }
647
- assertNoLeaks() {
648
- const leaks = this.findLeaks();
649
- if (leaks.length > 0) {
650
- throw new Error(`LeakDetector: ${leaks.length} leaked resource(s):
651
- ${leaks.join("\n")}`);
453
+ /** Provide/replace IoC value registrations. */
454
+ values(config) {
455
+ const writes = [];
456
+ for (const [key, byName, value] of configEntries(config)) {
457
+ writes.push({ op: "values", key, byName, value });
458
+ }
459
+ return this.derive(writes);
460
+ }
461
+ /** Install method behavior/observation descriptors on IoC-managed instances. */
462
+ methods(config) {
463
+ const writes = [];
464
+ for (const [key, byName, methodsInput] of configEntries(config)) {
465
+ const methods = /* @__PURE__ */ new Map();
466
+ const entries = methodsInput instanceof Map ? methodsInput.entries() : Object.entries(methodsInput);
467
+ for (const [name, descriptor] of entries) {
468
+ if (typeof descriptor !== "function" && !isDescriptor(descriptor)) {
469
+ throw new InvalidDescriptorError(
470
+ `.methods() entry ${tokenLabel(key)}.${name} must be a test.* descriptor or a raw wrapper function.`
471
+ );
472
+ }
473
+ methods.set(name, descriptor);
474
+ }
475
+ writes.push({ op: "methods", key, byName, methods });
476
+ }
477
+ return this.derive(writes);
478
+ }
479
+ /** Materialize a fresh, isolated real-IoC environment. Non-consuming. */
480
+ async build() {
481
+ const root = createContainer();
482
+ if (typeof root.setInstanceDecorator !== "function") {
483
+ throw new MissingIocSeamError();
484
+ }
485
+ const knownByName = /* @__PURE__ */ new Map();
486
+ const knownLifetimes = /* @__PURE__ */ new Map();
487
+ const note = (token, loadAs) => {
488
+ if (typeof token === "function" && token.name) knownByName.set(token.name, token);
489
+ else if (typeof token === "string") knownByName.set(token, token);
490
+ if (loadAs !== void 0) knownLifetimes.set(token, loadAs);
491
+ };
492
+ const effective = /* @__PURE__ */ new Map();
493
+ const methodWrites = [];
494
+ const resolveKey = (key, byName, space) => {
495
+ if (!byName) return key;
496
+ const known = knownByName.get(key);
497
+ if (known !== void 0) return known;
498
+ if (space === "values" || space === "functions") return key;
499
+ throw new UnknownTokenKeyError(key, space, [...knownByName.keys()]);
500
+ };
501
+ for (const write of this.log) {
502
+ switch (write.op) {
503
+ case "use": {
504
+ for (const reg of flattenModule(write.module)) {
505
+ const loadAs = lifetimeToLoadAs(reg.lifetime);
506
+ note(reg.token, loadAs);
507
+ if (reg.kind === "class") {
508
+ note(reg.implementation, loadAs);
509
+ effective.set(reg.token, {
510
+ space: "class",
511
+ token: reg.token,
512
+ implementation: reg.implementation,
513
+ loadAs
514
+ });
515
+ knownLifetimes.set(reg.token, loadAs);
516
+ knownLifetimes.set(reg.implementation, loadAs);
517
+ } else if (reg.kind === "factory") {
518
+ effective.set(reg.token, {
519
+ space: "factory",
520
+ token: reg.token,
521
+ factory: reg.implementation,
522
+ loadAs,
523
+ deps: [...reg.dependencies]
524
+ });
525
+ } else {
526
+ effective.set(reg.token, { space: "value", token: reg.token, value: reg.implementation });
527
+ knownLifetimes.set(reg.token, LoadAs.Singleton);
528
+ }
529
+ }
530
+ break;
531
+ }
532
+ case "classes": {
533
+ const token = resolveKey(write.key, write.byName, "classes");
534
+ note(write.implementation, void 0);
535
+ if (!write.byName) note(token, void 0);
536
+ effective.set(token, {
537
+ space: "class",
538
+ token,
539
+ implementation: write.implementation,
540
+ // preserve configured lifetime unless the replacement declares its own scope
541
+ loadAs: componentScope(write.implementation) ?? knownLifetimes.get(token)
542
+ });
543
+ break;
544
+ }
545
+ case "functions": {
546
+ const token = resolveKey(write.key, write.byName, "functions");
547
+ const prior = effective.get(token);
548
+ effective.set(token, {
549
+ space: "factory",
550
+ token,
551
+ factory: write.factory,
552
+ // preserve configured lifetime unless the scenario overrides it
553
+ loadAs: prior && prior.space === "factory" ? prior.loadAs : knownLifetimes.get(token),
554
+ deps: prior && prior.space === "factory" ? prior.deps : void 0
555
+ });
556
+ note(token, void 0);
557
+ break;
558
+ }
559
+ case "values": {
560
+ const token = resolveKey(write.key, write.byName, "values");
561
+ effective.set(token, { space: "value", token, value: write.value });
562
+ note(token, LoadAs.Singleton);
563
+ break;
564
+ }
565
+ case "methods": {
566
+ const token = write.byName && knownByName.has(write.key) ? knownByName.get(write.key) : write.key;
567
+ methodWrites.push({
568
+ key: token,
569
+ byName: write.byName && !knownByName.has(write.key),
570
+ methods: new Map(write.methods)
571
+ });
572
+ break;
573
+ }
574
+ }
575
+ }
576
+ for (const entry of effective.values()) {
577
+ if (entry.space === "class") {
578
+ this.registerClassBinding(root, entry.token, entry.implementation, entry.loadAs);
579
+ } else if (entry.space === "factory") {
580
+ root.registerFunction(entry.token, entry.factory, {
581
+ loadAs: entry.loadAs,
582
+ param: entry.deps
583
+ });
584
+ } else {
585
+ const value = entry.value;
586
+ root.registerFunction(entry.token, () => value, { loadAs: LoadAs.Singleton });
587
+ }
588
+ }
589
+ const registry = new WatchRegistry();
590
+ const mergedMethods = /* @__PURE__ */ new Map();
591
+ for (const write of methodWrites) {
592
+ const existing = mergedMethods.get(write.key);
593
+ if (existing) {
594
+ for (const [name, descriptor] of write.methods) existing.methods.set(name, descriptor);
595
+ } else {
596
+ mergedMethods.set(write.key, { byName: write.byName, methods: new Map(write.methods) });
597
+ }
598
+ }
599
+ for (const [key, { byName, methods }] of mergedMethods) {
600
+ registry.addEntry({ key, byName, methods });
601
+ }
602
+ root.setInstanceDecorator(createMethodDecorator(registry));
603
+ const env = {
604
+ root,
605
+ get: (token, params) => root.get(token, params),
606
+ instance: (cls, params) => root.instance(cls, params),
607
+ extend: () => root.extend(),
608
+ verify: async () => registry.verify(),
609
+ dispose: () => root.dispose(),
610
+ [ENV_REGISTRY]: registry
611
+ };
612
+ return env;
613
+ }
614
+ /**
615
+ * Register a class binding. When token === implementation this is a plain
616
+ * class registration. Otherwise an alias factory resolves the implementation
617
+ * through real IoC, mirroring the implementation's effective lifetime so
618
+ * lifetime validation (captive-lifetime checks) stays honest.
619
+ */
620
+ registerClassBinding(root, token, implementation, configuredLoadAs) {
621
+ const loadAs = configuredLoadAs ?? componentScope(implementation) ?? LoadAs.Transient;
622
+ root.registerClass(implementation, configuredLoadAs !== void 0 ? { loadAs } : void 0);
623
+ if (token === implementation) return;
624
+ if (loadAs === LoadAs.Singleton) {
625
+ root.registerFunction(token, () => root.get(implementation), {
626
+ loadAs: LoadAs.Singleton
627
+ });
628
+ } else {
629
+ root.registerFunction(token, (scope) => scope.get(implementation), {
630
+ loadAs,
631
+ param: [SCOPED_CONTAINER]
632
+ });
652
633
  }
653
634
  }
654
635
  };
636
+ function testIoc(...inputs) {
637
+ return TestIocBuilder.create(inputs);
638
+ }
655
639
  export {
656
- LeakDetector,
657
- ManualClock,
658
- ManualScheduler,
659
- MemoryKeyValueStore,
660
- MemoryObjectStore,
661
- NoopLogSink,
662
- RecordingEventBus,
663
- RecordingLogSink,
664
- RecordingTelemetrySink,
665
- RecordingTraceSink,
666
- ScriptedFetchClient,
667
- ScriptedProcessRunner,
668
- SeededRandomSource,
669
- SequenceIdGenerator,
670
- runClockContract,
671
- runEventBusContract,
672
- runKeyValueStoreContract,
673
- runObjectStoreContract,
674
- testEvent
640
+ CallScriptExhaustedError,
641
+ ENV_REGISTRY,
642
+ ExpectationOverflowError,
643
+ InvalidDescriptorError,
644
+ MethodNotCallableError,
645
+ MissingIocSeamError,
646
+ NonObjectMethodTargetError,
647
+ TestIocBuilder,
648
+ TestingError,
649
+ UnknownTokenKeyError,
650
+ UnwatchedInspectionError,
651
+ VerificationError,
652
+ test,
653
+ testIoc
675
654
  };
676
655
  //# sourceMappingURL=index.js.map