@noego/testing 0.1.0 → 0.4.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 +94 -0
- package/dist/index.cjs +687 -637
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +278 -222
- package/dist/index.d.ts +278 -222
- package/dist/index.js +676 -618
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
package/dist/index.js
CHANGED
|
@@ -1,676 +1,734 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (instant.isBefore(this.current)) {
|
|
15
|
-
throw new RangeError(`ManualClock cannot move backwards: ${this.current.toString()} -> ${instant.toString()}`);
|
|
16
|
-
}
|
|
17
|
-
this.current = instant;
|
|
1
|
+
// src/builder.ts
|
|
2
|
+
import {
|
|
3
|
+
createContainer,
|
|
4
|
+
flattenModule,
|
|
5
|
+
LoadAs,
|
|
6
|
+
SCOPED_CONTAINER
|
|
7
|
+
} from "@noego/ioc";
|
|
8
|
+
|
|
9
|
+
// src/errors.ts
|
|
10
|
+
var TestingError = class extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = new.target.name;
|
|
18
14
|
}
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
);
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
};
|
|
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
|
+
);
|
|
52
28
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
}
|
|
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
|
+
);
|
|
70
35
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
}
|
|
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
|
+
);
|
|
96
42
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
);
|
|
105
51
|
}
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
);
|
|
108
58
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
);
|
|
112
65
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
66
|
+
};
|
|
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
|
+
);
|
|
116
72
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
73
|
+
};
|
|
74
|
+
var InvalidDescriptorError = class extends TestingError {
|
|
75
|
+
};
|
|
76
|
+
var AmbiguousNameKeyError = class extends TestingError {
|
|
77
|
+
constructor(name, space) {
|
|
78
|
+
super(
|
|
79
|
+
`.${space}() key "${name}" is ambiguous: multiple distinct tokens share that display name. Use the canonical tuple form with the exact token: .${space}([[TheToken, ...]]).`
|
|
80
|
+
);
|
|
81
|
+
this.name = "AmbiguousNameKeyError";
|
|
120
82
|
}
|
|
121
83
|
};
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
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);
|
|
84
|
+
var TokenIdentitySplitError = class extends TestingError {
|
|
85
|
+
constructor(names) {
|
|
86
|
+
super(
|
|
87
|
+
`token identity split: .methods() entr${names.length === 1 ? "y" : "ies"} for ${names.map((name) => `"${name}"`).join(", ")} never matched a constructed instance, but a DIFFERENT class with the same name was constructed. Two module registries have loaded the same class file (test-file import vs harness graph import) \u2014 the configured behavior did not apply. Route the harness's imports through your registry (e.g. testApp .importer((p) => import(p))) or pass the exact token the graph uses.`
|
|
88
|
+
);
|
|
89
|
+
this.name = "TokenIdentitySplitError";
|
|
135
90
|
}
|
|
136
91
|
};
|
|
137
92
|
|
|
138
|
-
// src/
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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;
|
|
93
|
+
// src/descriptors.ts
|
|
94
|
+
var DESCRIPTOR = /* @__PURE__ */ Symbol.for("noego:testing:descriptor");
|
|
95
|
+
function isDescriptor(value) {
|
|
96
|
+
return typeof value === "object" && value !== null && value[DESCRIPTOR] === true;
|
|
97
|
+
}
|
|
98
|
+
function frozen(value) {
|
|
99
|
+
return Object.freeze(value);
|
|
100
|
+
}
|
|
101
|
+
function assertBehavior(value, where) {
|
|
102
|
+
if (!isDescriptor(value) || !["returns", "throws", "original", "calls"].includes(value.kind)) {
|
|
103
|
+
throw new InvalidDescriptorError(
|
|
104
|
+
`${where} requires a behavior descriptor (test.returns/throws/original/calls).`
|
|
105
|
+
);
|
|
165
106
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
107
|
+
}
|
|
108
|
+
var test = {
|
|
109
|
+
/** Return the supplied value when the method is called. Auto-watches. */
|
|
110
|
+
returns(value) {
|
|
111
|
+
return frozen({ [DESCRIPTOR]: true, kind: "returns", value });
|
|
112
|
+
},
|
|
113
|
+
/** Throw/reject with the supplied error. Auto-watches. */
|
|
114
|
+
throws(error) {
|
|
115
|
+
return frozen({ [DESCRIPTOR]: true, kind: "throws", error });
|
|
116
|
+
},
|
|
117
|
+
/** Invoke the original effective method. Auto-watches. */
|
|
118
|
+
original() {
|
|
119
|
+
return frozen({ [DESCRIPTOR]: true, kind: "original" });
|
|
120
|
+
},
|
|
121
|
+
/**
|
|
122
|
+
* Per-invocation behavior script: call 1 uses entry 1, and so on. A call
|
|
123
|
+
* after exhaustion fails immediately. Unused entries do not fail verification.
|
|
124
|
+
*/
|
|
125
|
+
calls(script) {
|
|
126
|
+
if (!Array.isArray(script)) {
|
|
127
|
+
throw new InvalidDescriptorError("test.calls() requires an array of behavior descriptors.");
|
|
169
128
|
}
|
|
170
|
-
|
|
171
|
-
return
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
);
|
|
129
|
+
script.forEach((entry, i) => assertBehavior(entry, `test.calls() entry #${i + 1}`));
|
|
130
|
+
return frozen({ [DESCRIPTOR]: true, kind: "calls", script: Object.freeze([...script]) });
|
|
131
|
+
},
|
|
132
|
+
/**
|
|
133
|
+
* Keep original behavior and record calls. With a raw wrapper argument, the
|
|
134
|
+
* wrapper's behavior runs and is recorded.
|
|
135
|
+
*/
|
|
136
|
+
watch(wrapper) {
|
|
137
|
+
if (wrapper !== void 0 && typeof wrapper !== "function") {
|
|
138
|
+
throw new InvalidDescriptorError("test.watch() accepts only a raw wrapper function.");
|
|
191
139
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
140
|
+
return frozen({ [DESCRIPTOR]: true, kind: "watch", wrapper });
|
|
141
|
+
},
|
|
142
|
+
/** Require exactly one call; with no behavior, the original runs. */
|
|
143
|
+
once(behavior) {
|
|
144
|
+
if (behavior !== void 0) assertBehavior(behavior, "test.once()");
|
|
145
|
+
return frozen({ [DESCRIPTOR]: true, kind: "expect", expected: 1, behavior });
|
|
146
|
+
},
|
|
147
|
+
/** Require exactly `count` calls; with no behavior, the original runs. */
|
|
148
|
+
times(count, behavior) {
|
|
149
|
+
if (!Number.isInteger(count) || count < 0) {
|
|
150
|
+
throw new InvalidDescriptorError(
|
|
151
|
+
`test.times() requires a non-negative integer count, received ${String(count)}.`
|
|
195
152
|
);
|
|
196
153
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
154
|
+
if (behavior !== void 0) assertBehavior(behavior, "test.times()");
|
|
155
|
+
return frozen({ [DESCRIPTOR]: true, kind: "expect", expected: count, behavior });
|
|
156
|
+
},
|
|
157
|
+
/** Require zero calls; the first invocation fails and skips the original. */
|
|
158
|
+
never() {
|
|
159
|
+
return frozen({ [DESCRIPTOR]: true, kind: "expect", expected: 0 });
|
|
160
|
+
},
|
|
161
|
+
/** Read the recorded history for a watched method in one environment. */
|
|
162
|
+
inspect(environment, token, method) {
|
|
163
|
+
const registry = environment?.[ENV_REGISTRY];
|
|
164
|
+
if (!registry) {
|
|
165
|
+
throw new InvalidDescriptorError(
|
|
166
|
+
"test.inspect() requires a built @noego/testing environment as its first argument."
|
|
167
|
+
);
|
|
207
168
|
}
|
|
169
|
+
return registry.inspect(token, method);
|
|
208
170
|
}
|
|
209
171
|
};
|
|
172
|
+
Object.freeze(test);
|
|
173
|
+
var ENV_REGISTRY = /* @__PURE__ */ Symbol.for("noego:testing:env-registry");
|
|
210
174
|
|
|
211
|
-
// src/
|
|
212
|
-
|
|
213
|
-
var
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
175
|
+
// src/method_state.ts
|
|
176
|
+
var CONTEXT_WRAPPED = /* @__PURE__ */ Symbol.for("ioc:context-wrapped");
|
|
177
|
+
var CONTEXT_OWNER = /* @__PURE__ */ Symbol.for("ioc:context-owner");
|
|
178
|
+
function tokenLabel(token) {
|
|
179
|
+
if (typeof token === "function") return token.name || "[anonymous class]";
|
|
180
|
+
if (typeof token === "symbol") return String(token);
|
|
181
|
+
return String(token);
|
|
182
|
+
}
|
|
183
|
+
var MethodState = class {
|
|
184
|
+
constructor(tokenName, method, descriptor) {
|
|
185
|
+
this.tokenName = tokenName;
|
|
186
|
+
this.method = method;
|
|
187
|
+
this.descriptor = descriptor;
|
|
188
|
+
}
|
|
189
|
+
tokenName;
|
|
190
|
+
method;
|
|
191
|
+
descriptor;
|
|
192
|
+
calls = [];
|
|
193
|
+
/** Actual invocation count (includes the call currently executing). */
|
|
194
|
+
actual = 0;
|
|
195
|
+
/** Cursor into a test.calls() script. */
|
|
196
|
+
scriptCursor = 0;
|
|
197
|
+
get expectation() {
|
|
198
|
+
return this.descriptor.kind === "expect" ? this.descriptor : void 0;
|
|
199
|
+
}
|
|
200
|
+
inspection() {
|
|
201
|
+
return {
|
|
202
|
+
count: this.calls.length,
|
|
203
|
+
calls: this.calls.map((c) => ({ ...c, args: [...c.args] }))
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
var WatchRegistry = class {
|
|
208
|
+
/** entry-identity → method → state. Entries share states across instances. */
|
|
209
|
+
states = /* @__PURE__ */ new Map();
|
|
210
|
+
nameIndex = /* @__PURE__ */ new Map();
|
|
211
|
+
entries = [];
|
|
212
|
+
/** Class-token entries that identity-matched at least one construction. */
|
|
213
|
+
matchedEntryKeys = /* @__PURE__ */ new Set();
|
|
214
|
+
/** Constructed class tokens that matched NO entry, by display name. */
|
|
215
|
+
unmatchedConstructedByName = /* @__PURE__ */ new Map();
|
|
216
|
+
addEntry(entry) {
|
|
217
|
+
this.entries.push(entry);
|
|
218
|
+
const byMethod = /* @__PURE__ */ new Map();
|
|
219
|
+
for (const [method, descriptor] of entry.methods) {
|
|
220
|
+
if (typeof descriptor === "function") continue;
|
|
221
|
+
byMethod.set(method, new MethodState(this.entryLabel(entry), method, descriptor));
|
|
225
222
|
}
|
|
226
|
-
|
|
227
|
-
|
|
223
|
+
this.states.set(entry.key, byMethod);
|
|
224
|
+
if (entry.byName) this.nameIndex.set(entry.key, entry.key);
|
|
225
|
+
else if (typeof entry.key === "function" && entry.key.name) {
|
|
226
|
+
this.nameIndex.set(entry.key.name, entry.key);
|
|
228
227
|
}
|
|
229
|
-
this.cursor += 1;
|
|
230
|
-
return entry;
|
|
231
228
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
229
|
+
entryLabel(entry) {
|
|
230
|
+
return entry.byName ? String(entry.key) : tokenLabel(entry.key);
|
|
231
|
+
}
|
|
232
|
+
/** Entries applying to a resolving token (exact identity or name match). */
|
|
233
|
+
matchEntries(token) {
|
|
234
|
+
const name = typeof token === "function" ? token.name : typeof token === "string" ? token : void 0;
|
|
235
|
+
const matched = this.entries.filter(
|
|
236
|
+
(entry) => entry.key === token || entry.byName && name !== void 0 && entry.key === name
|
|
237
|
+
);
|
|
238
|
+
if (matched.length) {
|
|
239
|
+
for (const entry of matched) this.matchedEntryKeys.add(entry.key);
|
|
240
|
+
} else if (typeof token === "function" && name) {
|
|
241
|
+
this.unmatchedConstructedByName.set(name, token);
|
|
238
242
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
throw new Error(`ScriptedProcessRunner: ${this.openHandles.size} open process handle(s) at teardown: ${labels}`);
|
|
243
|
+
return matched;
|
|
244
|
+
}
|
|
245
|
+
state(entryKey, method) {
|
|
246
|
+
return this.states.get(entryKey)?.get(method);
|
|
247
|
+
}
|
|
248
|
+
inspect(token, method) {
|
|
249
|
+
const label = tokenLabel(token);
|
|
250
|
+
const keys = [token];
|
|
251
|
+
const name = typeof token === "function" ? token.name : typeof token === "string" ? token : void 0;
|
|
252
|
+
if (name !== void 0 && this.nameIndex.has(name)) keys.push(this.nameIndex.get(name));
|
|
253
|
+
for (const key of keys) {
|
|
254
|
+
const state = this.states.get(key)?.get(method);
|
|
255
|
+
if (state) return state.inspection();
|
|
253
256
|
}
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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();
|
|
257
|
+
throw new UnwatchedInspectionError(label, method);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Class-token entries that never identity-matched a construction while a
|
|
261
|
+
* DIFFERENT class with the same display name did construct. This is the
|
|
262
|
+
* split-module-registry signature (a test-file class object vs the graph's
|
|
263
|
+
* own load of the same file) — or two genuinely distinct same-named tokens
|
|
264
|
+
* where the configured one never resolved. Either way the configured
|
|
265
|
+
* behavior silently did not apply, which must be loud.
|
|
266
|
+
*/
|
|
267
|
+
identitySplits() {
|
|
268
|
+
const splits = [];
|
|
269
|
+
for (const entry of this.entries) {
|
|
270
|
+
if (entry.byName || typeof entry.key !== "function") continue;
|
|
271
|
+
if (this.matchedEntryKeys.has(entry.key)) continue;
|
|
272
|
+
const name = entry.key.name;
|
|
273
|
+
if (!name) continue;
|
|
274
|
+
const constructed = this.unmatchedConstructedByName.get(name);
|
|
275
|
+
if (constructed !== void 0 && constructed !== entry.key) {
|
|
276
|
+
splits.push({ name, entryToken: entry.key });
|
|
285
277
|
}
|
|
286
278
|
}
|
|
279
|
+
return splits;
|
|
287
280
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
this.resolveResult({ exitCode: null, signal, stdout: "", stderr: "" });
|
|
294
|
-
this.onClosed();
|
|
295
|
-
}
|
|
296
|
-
wait() {
|
|
297
|
-
return this.resultPromise;
|
|
298
|
-
}
|
|
299
|
-
};
|
|
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;
|
|
281
|
+
/** Repeatable snapshot check of all exact expectations. */
|
|
282
|
+
verify() {
|
|
283
|
+
const splits = this.identitySplits();
|
|
284
|
+
if (splits.length) {
|
|
285
|
+
throw new TokenIdentitySplitError(splits.map((split) => split.name));
|
|
311
286
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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);
|
|
287
|
+
const failures = [];
|
|
288
|
+
for (const byMethod of this.states.values()) {
|
|
289
|
+
for (const state of byMethod.values()) {
|
|
290
|
+
const expectation = state.expectation;
|
|
291
|
+
if (!expectation) continue;
|
|
292
|
+
if (state.actual !== expectation.expected) {
|
|
293
|
+
failures.push({
|
|
294
|
+
token: state.tokenName,
|
|
295
|
+
method: state.method,
|
|
296
|
+
expected: expectation.expected,
|
|
297
|
+
actual: state.actual
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
331
301
|
}
|
|
332
|
-
|
|
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();
|
|
302
|
+
if (failures.length) throw new VerificationError(failures);
|
|
342
303
|
}
|
|
343
304
|
};
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
return Promise.resolve(version);
|
|
362
|
-
}
|
|
363
|
-
delete(key) {
|
|
364
|
-
this.objects.delete(key);
|
|
365
|
-
return Promise.resolve();
|
|
305
|
+
function isPromiseLike(value) {
|
|
306
|
+
return !!value && typeof value.then === "function";
|
|
307
|
+
}
|
|
308
|
+
function recordOutcome(call, outcome, threw) {
|
|
309
|
+
if (!threw && isPromiseLike(outcome)) {
|
|
310
|
+
call.pending = true;
|
|
311
|
+
outcome.then(
|
|
312
|
+
(value) => {
|
|
313
|
+
call.result = value;
|
|
314
|
+
call.pending = false;
|
|
315
|
+
},
|
|
316
|
+
(error) => {
|
|
317
|
+
call.error = error;
|
|
318
|
+
call.pending = false;
|
|
319
|
+
}
|
|
320
|
+
);
|
|
321
|
+
return outcome;
|
|
366
322
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
queue = [];
|
|
380
|
-
async publish(event) {
|
|
381
|
-
this.published.push(event);
|
|
382
|
-
if (this.mode === "queued") {
|
|
383
|
-
this.queue.push(event);
|
|
384
|
-
return;
|
|
323
|
+
call.pending = false;
|
|
324
|
+
if (threw) call.error = outcome;
|
|
325
|
+
else call.result = outcome;
|
|
326
|
+
return outcome;
|
|
327
|
+
}
|
|
328
|
+
function createMethodDecorator(registry) {
|
|
329
|
+
const wrappers = /* @__PURE__ */ new WeakMap();
|
|
330
|
+
return function decorate(instance, token) {
|
|
331
|
+
const entries = registry.matchEntries(token);
|
|
332
|
+
if (entries.length === 0) return instance;
|
|
333
|
+
if (instance === null || typeof instance !== "object" && typeof instance !== "function") {
|
|
334
|
+
throw new NonObjectMethodTargetError(tokenLabel(token));
|
|
385
335
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const subscription = {
|
|
392
|
-
active: true,
|
|
393
|
-
unsubscribe() {
|
|
394
|
-
if (!subscription.active) return;
|
|
395
|
-
subscription.active = false;
|
|
396
|
-
bus.handlers.delete(handler);
|
|
336
|
+
if (wrappers.has(instance)) return wrappers.get(instance);
|
|
337
|
+
const effective = /* @__PURE__ */ new Map();
|
|
338
|
+
for (const entry of entries) {
|
|
339
|
+
for (const [method, descriptor] of entry.methods) {
|
|
340
|
+
effective.set(method, { entryKey: entry.key, descriptor });
|
|
397
341
|
}
|
|
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
342
|
}
|
|
343
|
+
for (const method of effective.keys()) {
|
|
344
|
+
if (typeof instance[method] !== "function") {
|
|
345
|
+
throw new MethodNotCallableError(tokenLabel(token), method);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const methodCache = /* @__PURE__ */ new Map();
|
|
349
|
+
const proxy = new Proxy(instance, {
|
|
350
|
+
get(target, prop, receiver) {
|
|
351
|
+
if (typeof prop === "string" && effective.has(prop)) {
|
|
352
|
+
let wrapped = methodCache.get(prop);
|
|
353
|
+
if (!wrapped) {
|
|
354
|
+
const { entryKey, descriptor } = effective.get(prop);
|
|
355
|
+
wrapped = buildMethodWrapper(target, prop, descriptor, registry.state(entryKey, prop));
|
|
356
|
+
methodCache.set(prop, wrapped);
|
|
357
|
+
}
|
|
358
|
+
return wrapped;
|
|
359
|
+
}
|
|
360
|
+
if (prop === CONTEXT_WRAPPED || prop === CONTEXT_OWNER) return target[prop];
|
|
361
|
+
return Reflect.get(target, prop, receiver);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
wrappers.set(instance, proxy);
|
|
365
|
+
return proxy;
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function buildMethodWrapper(target, method, descriptor, state) {
|
|
369
|
+
const callOriginal = (self, args) => {
|
|
370
|
+
const fn = target[method];
|
|
371
|
+
return fn.apply(self === void 0 ? target : self, args);
|
|
372
|
+
};
|
|
373
|
+
if (typeof descriptor === "function") {
|
|
374
|
+
const replacement = descriptor((...args) => callOriginal(target, args));
|
|
375
|
+
return function(...args) {
|
|
376
|
+
return replacement.apply(this, args);
|
|
377
|
+
};
|
|
407
378
|
}
|
|
408
|
-
|
|
409
|
-
|
|
379
|
+
if (!isDescriptor(descriptor)) {
|
|
380
|
+
throw new TypeError(`Invalid method descriptor for ${method}`);
|
|
410
381
|
}
|
|
411
|
-
|
|
412
|
-
if (
|
|
413
|
-
|
|
382
|
+
return function(...args) {
|
|
383
|
+
if (!state) throw new TypeError(`Missing method state for ${method}`);
|
|
384
|
+
const expectation = state.expectation;
|
|
385
|
+
const attempted = state.actual + 1;
|
|
386
|
+
if (expectation && attempted > expectation.expected) {
|
|
387
|
+
state.actual = attempted;
|
|
388
|
+
throw new ExpectationOverflowError(state.tokenName, method, expectation.expected, attempted);
|
|
414
389
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
390
|
+
state.actual = attempted;
|
|
391
|
+
const call = {
|
|
392
|
+
index: attempted,
|
|
393
|
+
args: [...args],
|
|
394
|
+
pending: false,
|
|
395
|
+
timestamp: Date.now()
|
|
396
|
+
};
|
|
397
|
+
state.calls.push(call);
|
|
398
|
+
let behavior;
|
|
399
|
+
const base = descriptor.kind === "expect" ? descriptor.behavior ?? "original-effective" : descriptor.kind === "watch" ? descriptor.wrapper ?? "original-effective" : descriptor;
|
|
400
|
+
if (base !== "original-effective" && typeof base !== "function" && base !== void 0 && base.kind === "calls") {
|
|
401
|
+
if (state.scriptCursor >= base.script.length) {
|
|
402
|
+
throw new CallScriptExhaustedError(state.tokenName, method, base.script.length, attempted);
|
|
403
|
+
}
|
|
404
|
+
behavior = base.script[state.scriptCursor];
|
|
405
|
+
state.scriptCursor += 1;
|
|
406
|
+
} else {
|
|
407
|
+
behavior = base;
|
|
419
408
|
}
|
|
420
|
-
|
|
421
|
-
|
|
409
|
+
try {
|
|
410
|
+
let outcome;
|
|
411
|
+
if (behavior === "original-effective") {
|
|
412
|
+
outcome = callOriginal(this, args);
|
|
413
|
+
} else if (typeof behavior === "function") {
|
|
414
|
+
outcome = behavior((...inner) => callOriginal(this, inner)).apply(this, args);
|
|
415
|
+
} else if (behavior.kind === "returns") {
|
|
416
|
+
outcome = behavior.value;
|
|
417
|
+
} else if (behavior.kind === "throws") {
|
|
418
|
+
throw behavior.error;
|
|
419
|
+
} else if (behavior.kind === "original") {
|
|
420
|
+
outcome = callOriginal(this, args);
|
|
421
|
+
} else {
|
|
422
|
+
throw new TypeError(`Nested test.calls() scripts are not supported (${method}).`);
|
|
423
|
+
}
|
|
424
|
+
return recordOutcome(call, outcome, false);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
recordOutcome(call, error, true);
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
}
|
|
422
431
|
|
|
423
|
-
// src/
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
var NoopLogSink = class extends LogSink {
|
|
444
|
-
emit(_envelope) {
|
|
432
|
+
// src/builder.ts
|
|
433
|
+
var COMPONENT_OPTIONS_KEY = /* @__PURE__ */ Symbol.for("ioc:component:options");
|
|
434
|
+
function* configEntries(config) {
|
|
435
|
+
if (Array.isArray(config)) {
|
|
436
|
+
for (const entry of config) {
|
|
437
|
+
if (!Array.isArray(entry) || entry.length !== 2) {
|
|
438
|
+
throw new InvalidDescriptorError(
|
|
439
|
+
"canonical composition entries are [token, value] tuples, e.g. .classes([[Token, Impl]])"
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
yield [entry[0], false, entry[1]];
|
|
443
|
+
}
|
|
444
|
+
} else if (config instanceof Map) {
|
|
445
|
+
for (const [key, value] of config) yield [key, false, value];
|
|
446
|
+
} else {
|
|
447
|
+
for (const key of [...Object.getOwnPropertyNames(config), ...Object.getOwnPropertySymbols(config)]) {
|
|
448
|
+
yield [key, typeof key === "string", config[key]];
|
|
449
|
+
}
|
|
445
450
|
}
|
|
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
|
-
});
|
|
464
451
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
});
|
|
452
|
+
function componentScope(cls) {
|
|
453
|
+
const options = typeof Reflect !== "undefined" && Reflect.getMetadata ? Reflect.getMetadata(COMPONENT_OPTIONS_KEY, cls) : void 0;
|
|
454
|
+
return options?.scope;
|
|
513
455
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
});
|
|
456
|
+
function lifetimeToLoadAs(lifetime) {
|
|
457
|
+
if (lifetime === "singleton") return LoadAs.Singleton;
|
|
458
|
+
if (lifetime === "scoped") return LoadAs.Scoped;
|
|
459
|
+
return LoadAs.Transient;
|
|
566
460
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
});
|
|
630
|
-
}
|
|
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}`);
|
|
461
|
+
var TestIocBuilder = class _TestIocBuilder {
|
|
462
|
+
constructor(log) {
|
|
463
|
+
this.log = log;
|
|
464
|
+
Object.freeze(this);
|
|
465
|
+
}
|
|
466
|
+
log;
|
|
467
|
+
/** @internal */
|
|
468
|
+
static create(inputs) {
|
|
469
|
+
return new _TestIocBuilder([]).useAll(inputs);
|
|
470
|
+
}
|
|
471
|
+
/** @internal — read by .use(builderPreset) */
|
|
472
|
+
get writes() {
|
|
473
|
+
return this.log;
|
|
474
|
+
}
|
|
475
|
+
derive(writes) {
|
|
476
|
+
return new _TestIocBuilder([...this.log, ...writes]);
|
|
477
|
+
}
|
|
478
|
+
useAll(inputs) {
|
|
479
|
+
let builder = this;
|
|
480
|
+
for (const input of inputs) builder = builder.use(input);
|
|
481
|
+
return builder;
|
|
482
|
+
}
|
|
483
|
+
/** Apply a reusable composition preset: an ApplicationModule or a builder. */
|
|
484
|
+
use(preset) {
|
|
485
|
+
if (preset instanceof _TestIocBuilder) {
|
|
486
|
+
return this.derive([...preset.writes]);
|
|
487
|
+
}
|
|
488
|
+
return this.derive([{ op: "use", module: preset }]);
|
|
489
|
+
}
|
|
490
|
+
/** Replace the implementation for IoC class tokens in the built environment. */
|
|
491
|
+
classes(config) {
|
|
492
|
+
const writes = [];
|
|
493
|
+
for (const [key, byName, implementation] of configEntries(config)) {
|
|
494
|
+
if (typeof implementation !== "function" || !implementation.prototype) {
|
|
495
|
+
throw new InvalidDescriptorError(
|
|
496
|
+
`.classes() value for "${tokenLabel(key)}" must be a class constructor.`
|
|
497
|
+
);
|
|
643
498
|
}
|
|
499
|
+
writes.push({ op: "classes", key, byName, implementation });
|
|
644
500
|
}
|
|
645
|
-
return
|
|
501
|
+
return this.derive(writes);
|
|
502
|
+
}
|
|
503
|
+
/** Replace IoC factory/provider registrations. */
|
|
504
|
+
functions(config) {
|
|
505
|
+
const writes = [];
|
|
506
|
+
for (const [key, byName, factory] of configEntries(config)) {
|
|
507
|
+
if (typeof factory !== "function") {
|
|
508
|
+
throw new InvalidDescriptorError(`.functions() value for "${tokenLabel(key)}" must be a function.`);
|
|
509
|
+
}
|
|
510
|
+
writes.push({ op: "functions", key, byName, factory });
|
|
511
|
+
}
|
|
512
|
+
return this.derive(writes);
|
|
646
513
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
514
|
+
/** Provide/replace IoC value registrations. */
|
|
515
|
+
values(config) {
|
|
516
|
+
const writes = [];
|
|
517
|
+
for (const [key, byName, value] of configEntries(config)) {
|
|
518
|
+
writes.push({ op: "values", key, byName, value });
|
|
519
|
+
}
|
|
520
|
+
return this.derive(writes);
|
|
521
|
+
}
|
|
522
|
+
/** Install method behavior/observation descriptors on IoC-managed instances. */
|
|
523
|
+
methods(config) {
|
|
524
|
+
const writes = [];
|
|
525
|
+
for (const [key, byName, methodsInput] of configEntries(config)) {
|
|
526
|
+
const methods = /* @__PURE__ */ new Map();
|
|
527
|
+
const entries = methodsInput instanceof Map ? methodsInput.entries() : Object.entries(methodsInput);
|
|
528
|
+
for (const [name, descriptor] of entries) {
|
|
529
|
+
if (typeof descriptor !== "function" && !isDescriptor(descriptor)) {
|
|
530
|
+
throw new InvalidDescriptorError(
|
|
531
|
+
`.methods() entry ${tokenLabel(key)}.${name} must be a test.* descriptor or a raw wrapper function.`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
methods.set(name, descriptor);
|
|
535
|
+
}
|
|
536
|
+
writes.push({ op: "methods", key, byName, methods });
|
|
537
|
+
}
|
|
538
|
+
return this.derive(writes);
|
|
539
|
+
}
|
|
540
|
+
/** Materialize a fresh, isolated real-IoC environment. Non-consuming. */
|
|
541
|
+
async build() {
|
|
542
|
+
const root = createContainer();
|
|
543
|
+
if (typeof root.setInstanceDecorator !== "function") {
|
|
544
|
+
throw new MissingIocSeamError();
|
|
545
|
+
}
|
|
546
|
+
const knownByName = /* @__PURE__ */ new Map();
|
|
547
|
+
const ambiguousNames = /* @__PURE__ */ new Set();
|
|
548
|
+
const knownLifetimes = /* @__PURE__ */ new Map();
|
|
549
|
+
const note = (token, loadAs) => {
|
|
550
|
+
const name = typeof token === "function" && token.name ? token.name : typeof token === "string" ? token : null;
|
|
551
|
+
if (name !== null) {
|
|
552
|
+
const existing = knownByName.get(name);
|
|
553
|
+
if (existing !== void 0 && existing !== token) ambiguousNames.add(name);
|
|
554
|
+
knownByName.set(name, token);
|
|
555
|
+
}
|
|
556
|
+
if (loadAs !== void 0) knownLifetimes.set(token, loadAs);
|
|
557
|
+
};
|
|
558
|
+
const effective = /* @__PURE__ */ new Map();
|
|
559
|
+
const methodWrites = [];
|
|
560
|
+
const resolveKey = (key, byName, space) => {
|
|
561
|
+
if (!byName) return key;
|
|
562
|
+
if (ambiguousNames.has(key)) {
|
|
563
|
+
throw new AmbiguousNameKeyError(key, space);
|
|
564
|
+
}
|
|
565
|
+
const known = knownByName.get(key);
|
|
566
|
+
if (known !== void 0) return known;
|
|
567
|
+
if (space === "values" || space === "functions") return key;
|
|
568
|
+
throw new UnknownTokenKeyError(key, space, [...knownByName.keys()]);
|
|
569
|
+
};
|
|
570
|
+
for (const write of this.log) {
|
|
571
|
+
switch (write.op) {
|
|
572
|
+
case "use": {
|
|
573
|
+
for (const reg of flattenModule(write.module)) {
|
|
574
|
+
const loadAs = lifetimeToLoadAs(reg.lifetime);
|
|
575
|
+
note(reg.token, loadAs);
|
|
576
|
+
if (reg.kind === "class") {
|
|
577
|
+
note(reg.implementation, loadAs);
|
|
578
|
+
effective.set(reg.token, {
|
|
579
|
+
space: "class",
|
|
580
|
+
token: reg.token,
|
|
581
|
+
implementation: reg.implementation,
|
|
582
|
+
loadAs
|
|
583
|
+
});
|
|
584
|
+
knownLifetimes.set(reg.token, loadAs);
|
|
585
|
+
knownLifetimes.set(reg.implementation, loadAs);
|
|
586
|
+
} else if (reg.kind === "factory") {
|
|
587
|
+
effective.set(reg.token, {
|
|
588
|
+
space: "factory",
|
|
589
|
+
token: reg.token,
|
|
590
|
+
factory: reg.implementation,
|
|
591
|
+
loadAs,
|
|
592
|
+
deps: [...reg.dependencies]
|
|
593
|
+
});
|
|
594
|
+
} else {
|
|
595
|
+
effective.set(reg.token, { space: "value", token: reg.token, value: reg.implementation });
|
|
596
|
+
knownLifetimes.set(reg.token, LoadAs.Singleton);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
case "classes": {
|
|
602
|
+
const token = resolveKey(write.key, write.byName, "classes");
|
|
603
|
+
note(write.implementation, void 0);
|
|
604
|
+
if (!write.byName) note(token, void 0);
|
|
605
|
+
effective.set(token, {
|
|
606
|
+
space: "class",
|
|
607
|
+
token,
|
|
608
|
+
implementation: write.implementation,
|
|
609
|
+
// Lifetime priority: the replacement's own @Component scope, then
|
|
610
|
+
// the lifetime the composition already knows for the token, then
|
|
611
|
+
// the TOKEN class's declared @Component scope — a plain stub
|
|
612
|
+
// class replacing a Singleton-scoped production service must not
|
|
613
|
+
// silently degrade to Transient (captive-lifetime validation
|
|
614
|
+
// would reject the production dependents).
|
|
615
|
+
loadAs: componentScope(write.implementation) ?? knownLifetimes.get(token) ?? componentScope(token)
|
|
616
|
+
});
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
case "functions": {
|
|
620
|
+
const token = resolveKey(write.key, write.byName, "functions");
|
|
621
|
+
const prior = effective.get(token);
|
|
622
|
+
effective.set(token, {
|
|
623
|
+
space: "factory",
|
|
624
|
+
token,
|
|
625
|
+
factory: write.factory,
|
|
626
|
+
// preserve configured lifetime unless the scenario overrides it
|
|
627
|
+
loadAs: prior && prior.space === "factory" ? prior.loadAs : knownLifetimes.get(token),
|
|
628
|
+
deps: prior && prior.space === "factory" ? prior.deps : void 0
|
|
629
|
+
});
|
|
630
|
+
note(token, void 0);
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
case "values": {
|
|
634
|
+
const token = resolveKey(write.key, write.byName, "values");
|
|
635
|
+
effective.set(token, { space: "value", token, value: write.value });
|
|
636
|
+
note(token, LoadAs.Singleton);
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
case "methods": {
|
|
640
|
+
if (write.byName && ambiguousNames.has(write.key)) {
|
|
641
|
+
throw new AmbiguousNameKeyError(write.key, "methods");
|
|
642
|
+
}
|
|
643
|
+
const token = write.byName && knownByName.has(write.key) ? knownByName.get(write.key) : write.key;
|
|
644
|
+
methodWrites.push({
|
|
645
|
+
key: token,
|
|
646
|
+
byName: write.byName && !knownByName.has(write.key),
|
|
647
|
+
methods: new Map(write.methods)
|
|
648
|
+
});
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
for (const entry of effective.values()) {
|
|
654
|
+
if (entry.space === "class") {
|
|
655
|
+
this.registerClassBinding(root, entry.token, entry.implementation, entry.loadAs);
|
|
656
|
+
} else if (entry.space === "factory") {
|
|
657
|
+
root.registerFunction(entry.token, entry.factory, {
|
|
658
|
+
loadAs: entry.loadAs,
|
|
659
|
+
param: entry.deps
|
|
660
|
+
});
|
|
661
|
+
} else {
|
|
662
|
+
const value = entry.value;
|
|
663
|
+
root.registerFunction(entry.token, () => value, { loadAs: LoadAs.Singleton });
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const registry = new WatchRegistry();
|
|
667
|
+
const mergedMethods = /* @__PURE__ */ new Map();
|
|
668
|
+
for (const write of methodWrites) {
|
|
669
|
+
const existing = mergedMethods.get(write.key);
|
|
670
|
+
if (existing) {
|
|
671
|
+
for (const [name, descriptor] of write.methods) existing.methods.set(name, descriptor);
|
|
672
|
+
} else {
|
|
673
|
+
mergedMethods.set(write.key, { byName: write.byName, methods: new Map(write.methods) });
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
for (const [key, { byName, methods }] of mergedMethods) {
|
|
677
|
+
registry.addEntry({ key, byName, methods });
|
|
678
|
+
}
|
|
679
|
+
root.setInstanceDecorator(createMethodDecorator(registry));
|
|
680
|
+
const env = {
|
|
681
|
+
root,
|
|
682
|
+
get: (token, params) => root.get(token, params),
|
|
683
|
+
instance: (cls, params) => root.instance(cls, params),
|
|
684
|
+
extend: () => root.extend(),
|
|
685
|
+
verify: async () => registry.verify(),
|
|
686
|
+
dispose: () => root.dispose(),
|
|
687
|
+
[ENV_REGISTRY]: registry
|
|
688
|
+
};
|
|
689
|
+
return env;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Register a class binding. When token === implementation this is a plain
|
|
693
|
+
* class registration. Otherwise an alias factory resolves the implementation
|
|
694
|
+
* through real IoC, mirroring the implementation's effective lifetime so
|
|
695
|
+
* lifetime validation (captive-lifetime checks) stays honest.
|
|
696
|
+
*/
|
|
697
|
+
registerClassBinding(root, token, implementation, configuredLoadAs) {
|
|
698
|
+
const loadAs = configuredLoadAs ?? componentScope(implementation) ?? LoadAs.Transient;
|
|
699
|
+
root.registerClass(implementation, configuredLoadAs !== void 0 ? { loadAs } : void 0);
|
|
700
|
+
if (token === implementation) return;
|
|
701
|
+
if (loadAs === LoadAs.Singleton) {
|
|
702
|
+
root.registerFunction(token, () => root.get(implementation), {
|
|
703
|
+
loadAs: LoadAs.Singleton
|
|
704
|
+
});
|
|
705
|
+
} else {
|
|
706
|
+
root.registerFunction(token, (scope) => scope.get(implementation), {
|
|
707
|
+
loadAs,
|
|
708
|
+
param: [SCOPED_CONTAINER]
|
|
709
|
+
});
|
|
652
710
|
}
|
|
653
711
|
}
|
|
654
712
|
};
|
|
713
|
+
function testIoc(...inputs) {
|
|
714
|
+
return TestIocBuilder.create(inputs);
|
|
715
|
+
}
|
|
655
716
|
export {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
runKeyValueStoreContract,
|
|
673
|
-
runObjectStoreContract,
|
|
674
|
-
testEvent
|
|
717
|
+
AmbiguousNameKeyError,
|
|
718
|
+
CallScriptExhaustedError,
|
|
719
|
+
ENV_REGISTRY,
|
|
720
|
+
ExpectationOverflowError,
|
|
721
|
+
InvalidDescriptorError,
|
|
722
|
+
MethodNotCallableError,
|
|
723
|
+
MissingIocSeamError,
|
|
724
|
+
NonObjectMethodTargetError,
|
|
725
|
+
TestIocBuilder,
|
|
726
|
+
TestingError,
|
|
727
|
+
TokenIdentitySplitError,
|
|
728
|
+
UnknownTokenKeyError,
|
|
729
|
+
UnwatchedInspectionError,
|
|
730
|
+
VerificationError,
|
|
731
|
+
test,
|
|
732
|
+
testIoc
|
|
675
733
|
};
|
|
676
734
|
//# sourceMappingURL=index.js.map
|