@deepseek-ai/dsh-client-test-runtime 0.1.6-alpha.1 → 0.1.6-alpha.2
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.i18n.yaml +2 -2
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/index.js +446 -198
- package/lib/types/assembly/test-client.d.ts +1 -1
- package/lib/types/fixtures.d.ts +2 -0
- package/lib/types/index.d.ts +22 -7
- package/lib/types/remote.d.ts +8 -0
- package/lib/types/sessions.d.ts +52 -38
- package/package.json +45 -45
package/lib/index.js
CHANGED
|
@@ -7,10 +7,11 @@ import { createSlotRenderer as createSlotRenderer$1 } from "@deepseek-ai/dsh-cli
|
|
|
7
7
|
import { apply, inject } from "@deepseek-ai/dsh-client-ui-session/client";
|
|
8
8
|
import { createSnapshotStore } from "@deepseek-ai/dsh-client-store";
|
|
9
9
|
import { afterEach, beforeEach, expect, vi } from "vitest";
|
|
10
|
+
import { RemoteError } from "@deepseek-ai/dsh-typert-protocol";
|
|
10
11
|
import { MutableSessionEventSource, SESSION_SEARCH_RESULT_LIMIT, createScope, scopeOf } from "@deepseek-ai/dsh-api-session-controller/client";
|
|
12
|
+
import { scopeIdentityOf } from "@deepseek-ai/dsh-api-session-controller/src/client/scope.ts";
|
|
11
13
|
import { EMPTY_CONVERSATION_SNAPSHOT } from "@deepseek-ai/dsh-client-ui-conversation/client";
|
|
12
14
|
import { EMPTY_CHAT_SNAPSHOT } from "@deepseek-ai/dsh-client-ui-chat/client";
|
|
13
|
-
import { RemoteError } from "@deepseek-ai/dsh-typert-protocol";
|
|
14
15
|
//#region lib/types/snapshot.js
|
|
15
16
|
/**
|
|
16
17
|
* DOM snapshot hygiene: a vitest snapshot serializer that keeps `.snap`
|
|
@@ -87,6 +88,96 @@ function registerDomSnapshotSerializer() {
|
|
|
87
88
|
expect.addSnapshotSerializer(domSnapshotSerializer);
|
|
88
89
|
}
|
|
89
90
|
//#endregion
|
|
91
|
+
//#region lib/types/remote.js
|
|
92
|
+
/**
|
|
93
|
+
* Remote service test double for the forwarded-event path. Feature specs need
|
|
94
|
+
* `ctx.remote.$on` to exist (their plugins inject `remote`) and need forwarded
|
|
95
|
+
* Host events to reach those subscribers, but not the wire — so this double
|
|
96
|
+
* implements subscription plus an explicit `emit` driver available only on the
|
|
97
|
+
* concrete test object. A spec that also calls one namespace scripts it through
|
|
98
|
+
* the constructor rather than reaching the real Client Remote service.
|
|
99
|
+
*
|
|
100
|
+
* `$mount` rejects: a spec that needs a real generated contribution installed —
|
|
101
|
+
* codecs, descriptors, and the wire — has outgrown this double and needs the
|
|
102
|
+
* real Client Remote service.
|
|
103
|
+
*
|
|
104
|
+
* One deliberate asymmetry with production: a throwing listener propagates out
|
|
105
|
+
* of the emit instead of being contained and logged, so a spec cannot lean on
|
|
106
|
+
* this double for the containment guarantee `$on` documents — assert that
|
|
107
|
+
* against the real service.
|
|
108
|
+
*/
|
|
109
|
+
var TestRemote = class {
|
|
110
|
+
ctx;
|
|
111
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
112
|
+
/**
|
|
113
|
+
* Fixed Host facts mirrored from the production `ctx.remote.$host`. Plain
|
|
114
|
+
* mutable field: a spec assigns it to script a non-loopback or homed Host.
|
|
115
|
+
*/
|
|
116
|
+
$host = {
|
|
117
|
+
home: void 0,
|
|
118
|
+
isLoopback: true
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Register the double as `ctx.remote`, plus one service per scripted
|
|
122
|
+
* namespace so a plugin injecting `remote.<name>` also unparks.
|
|
123
|
+
* @param ctx - the spec's root Context.
|
|
124
|
+
* @param namespaces - scripted namespace faces reached as `ctx.remote.<name>`.
|
|
125
|
+
*/
|
|
126
|
+
constructor(ctx, namespaces = {}) {
|
|
127
|
+
this.ctx = ctx;
|
|
128
|
+
this.validateNamespaces(namespaces);
|
|
129
|
+
ctx.provide("remote", this);
|
|
130
|
+
this.installNamespaces(namespaces);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Add scripted namespace faces to this Remote service.
|
|
134
|
+
* @param namespaces - scripted namespace faces reached as `ctx.remote.<name>`.
|
|
135
|
+
*/
|
|
136
|
+
provideNamespaces(namespaces) {
|
|
137
|
+
this.validateNamespaces(namespaces);
|
|
138
|
+
this.installNamespaces(namespaces);
|
|
139
|
+
}
|
|
140
|
+
validateNamespaces(namespaces) {
|
|
141
|
+
for (const name of Object.keys(namespaces)) if (name in this) throw new TypeError(`TestRemote: scripted namespace "${name}" would shadow the double's own member`);
|
|
142
|
+
}
|
|
143
|
+
installNamespaces(namespaces) {
|
|
144
|
+
Object.assign(this, namespaces);
|
|
145
|
+
for (const [name, face] of Object.entries(namespaces)) this.ctx.provide(`remote.${name}`, face);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Deliver one forwarded host event to its subscribers, standing in for the
|
|
149
|
+
* carrier that owns the frame sink.
|
|
150
|
+
* @param event - forwarded host event name.
|
|
151
|
+
* @param args - the Host argument list, verbatim.
|
|
152
|
+
*/
|
|
153
|
+
emit(event, args) {
|
|
154
|
+
const listeners = this.subscriptions.get(event);
|
|
155
|
+
if (listeners === void 0) return;
|
|
156
|
+
for (const listener of [...listeners]) listener(...args);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Subscribe to one forwarded host event.
|
|
160
|
+
* @param event - forwarded host event name.
|
|
161
|
+
* @param listener - receives the Host argument list verbatim.
|
|
162
|
+
* @returns disposer removing this subscription.
|
|
163
|
+
*/
|
|
164
|
+
$on(event, listener) {
|
|
165
|
+
const listeners = this.subscriptions.get(event) ?? /* @__PURE__ */ new Set();
|
|
166
|
+
this.subscriptions.set(event, listeners);
|
|
167
|
+
listeners.add(listener);
|
|
168
|
+
return () => {
|
|
169
|
+
listeners.delete(listener);
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Generated-namespace mount, unsupported by this double.
|
|
174
|
+
* @returns never; always rejects.
|
|
175
|
+
*/
|
|
176
|
+
$mount() {
|
|
177
|
+
return Promise.reject(/* @__PURE__ */ new Error("TestRemote: $mount needs the real Client Remote service"));
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
//#endregion
|
|
90
181
|
//#region lib/types/fixtures.js
|
|
91
182
|
/**
|
|
92
183
|
* A complete quiescent Session Controller snapshot.
|
|
@@ -96,7 +187,6 @@ function registerDomSnapshotSerializer() {
|
|
|
96
187
|
function sessionSnapshot(sessionId) {
|
|
97
188
|
return {
|
|
98
189
|
sessionId,
|
|
99
|
-
queue: [],
|
|
100
190
|
pendingSubmissions: [],
|
|
101
191
|
running: false,
|
|
102
192
|
subagent: null,
|
|
@@ -286,23 +376,94 @@ var FixtureSession = class {
|
|
|
286
376
|
throw new Error(`test session "${this.sessionId}": rename is not stubbed — supply it on the fixture's session face`);
|
|
287
377
|
}
|
|
288
378
|
};
|
|
379
|
+
function freezeRetainedBy(counts) {
|
|
380
|
+
Object.setPrototypeOf(counts, null);
|
|
381
|
+
return Object.freeze(counts);
|
|
382
|
+
}
|
|
383
|
+
const EMPTY_RETAIN_INFO = Object.freeze({
|
|
384
|
+
referenceCount: 0,
|
|
385
|
+
retainedBy: freezeRetainedBy({})
|
|
386
|
+
});
|
|
387
|
+
async function waitForOpen(opening, signal) {
|
|
388
|
+
/* v8 ignore next -- TestSessionReference always supplies its release-composed signal. */
|
|
389
|
+
if (signal === void 0) return opening;
|
|
390
|
+
const aborted = Promise.withResolvers();
|
|
391
|
+
const onAbort = () => {
|
|
392
|
+
aborted.reject(signal.reason);
|
|
393
|
+
};
|
|
394
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
395
|
+
try {
|
|
396
|
+
if (signal.aborted) onAbort();
|
|
397
|
+
await Promise.race([opening, aborted.promise]);
|
|
398
|
+
} finally {
|
|
399
|
+
signal.removeEventListener("abort", onAbort);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
var TestSessionReference = class {
|
|
403
|
+
sessionId;
|
|
404
|
+
generation;
|
|
405
|
+
releaseReference;
|
|
406
|
+
released = new AbortController();
|
|
407
|
+
readiness = Promise.withResolvers();
|
|
408
|
+
ready = this.readiness.promise;
|
|
409
|
+
constructor(sessionId, generation, releaseReference) {
|
|
410
|
+
this.sessionId = sessionId;
|
|
411
|
+
this.generation = generation;
|
|
412
|
+
this.releaseReference = releaseReference;
|
|
413
|
+
this.ready.catch(() => {});
|
|
414
|
+
}
|
|
415
|
+
get binding() {
|
|
416
|
+
if (this.generation === void 0 || !this.generation.live) throw new Error(`Session reference "${this.sessionId}" is released`);
|
|
417
|
+
return this.generation.binding;
|
|
418
|
+
}
|
|
419
|
+
attachOpening(opening, signal) {
|
|
420
|
+
const waitSignal = signal === void 0 ? this.released.signal : AbortSignal.any([this.released.signal, signal]);
|
|
421
|
+
waitForOpen(opening, waitSignal).then(() => {
|
|
422
|
+
try {
|
|
423
|
+
waitSignal.throwIfAborted();
|
|
424
|
+
this.readiness.resolve(this.binding);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
this.readiness.reject(error);
|
|
427
|
+
}
|
|
428
|
+
}, (error) => {
|
|
429
|
+
this.readiness.reject(error);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
release() {
|
|
433
|
+
const reason = /* @__PURE__ */ new Error(`Session reference "${this.sessionId}" is released`);
|
|
434
|
+
const release = this.releaseReference;
|
|
435
|
+
this.released.abort(reason);
|
|
436
|
+
this.readiness.reject(reason);
|
|
437
|
+
this.generation = void 0;
|
|
438
|
+
this.releaseReference = void 0;
|
|
439
|
+
release?.();
|
|
440
|
+
}
|
|
441
|
+
[Symbol.dispose]() {
|
|
442
|
+
this.release();
|
|
443
|
+
}
|
|
444
|
+
};
|
|
289
445
|
/**
|
|
290
446
|
* Sessions test double behind the renderer host and feature injects: owns the
|
|
291
|
-
*
|
|
447
|
+
* catalog observable, scope minting through the production `createScope`,
|
|
292
448
|
* stable Controller bindings, and the session behavior face supplied per
|
|
293
449
|
* fixture. `ui-session` owns standard-source materialization.
|
|
294
450
|
*
|
|
295
451
|
* Implements the same ISessions face features receive as `ctx.sessions`, so
|
|
296
452
|
* a production face change breaks this double at compile time; the extra
|
|
297
|
-
* members (add/updateSessionSnapshot/event-window drivers/
|
|
453
|
+
* members (add/updateSessionSnapshot/event-window drivers/remove/
|
|
298
454
|
* behavior/calls/stubs) are bench-only surface.
|
|
299
455
|
*/
|
|
300
456
|
var TestSessions = class {
|
|
301
457
|
stabilize;
|
|
302
458
|
rootCtx;
|
|
303
|
-
/** The useSessions
|
|
459
|
+
/** The useSessions catalog feed, independent of view ownership. */
|
|
304
460
|
list;
|
|
305
461
|
records = /* @__PURE__ */ new Map();
|
|
462
|
+
generations = /* @__PURE__ */ new Map();
|
|
463
|
+
addresses = /* @__PURE__ */ new Map();
|
|
464
|
+
retentionStores = /* @__PURE__ */ new Map();
|
|
465
|
+
pendingDrops = /* @__PURE__ */ new Set();
|
|
466
|
+
closed = false;
|
|
306
467
|
/** Calls observed on the service-level face, newest last. */
|
|
307
468
|
calls = [];
|
|
308
469
|
/** The wire schema's `session.search` result bound (production parity). */
|
|
@@ -320,20 +481,28 @@ var TestSessions = class {
|
|
|
320
481
|
this.list = createSnapshotStore({
|
|
321
482
|
ids: [],
|
|
322
483
|
byId: {},
|
|
323
|
-
current: void 0,
|
|
324
484
|
phase: "ready",
|
|
325
485
|
subagentsByParent: {},
|
|
326
|
-
jobsBySession: {}
|
|
327
|
-
currentAddress: void 0
|
|
486
|
+
jobsBySession: {}
|
|
328
487
|
});
|
|
488
|
+
rootCtx.effect(() => async () => {
|
|
489
|
+
this.closed = true;
|
|
490
|
+
for (const [id, generation] of this.generations) {
|
|
491
|
+
generation.live = false;
|
|
492
|
+
generation.retention = EMPTY_RETAIN_INFO;
|
|
493
|
+
generation.lifetime.abort(/* @__PURE__ */ new Error("test Session Controller is disposed"));
|
|
494
|
+
this.publishRetention(id);
|
|
495
|
+
}
|
|
496
|
+
this.generations.clear();
|
|
497
|
+
await this.drainDrops();
|
|
498
|
+
}, "test sessions: Client generations");
|
|
329
499
|
}
|
|
330
500
|
/**
|
|
331
|
-
* Add a
|
|
501
|
+
* Add a Session fixture to the catalog without retaining a generation.
|
|
332
502
|
* @param fixture - identity + snapshot/summary overrides + behavior face.
|
|
333
|
-
* @param opts - pass `current: false` to add without selecting.
|
|
334
503
|
* @returns the stable session id (branded view of `fixture.id`).
|
|
335
504
|
*/
|
|
336
|
-
async add(fixture
|
|
505
|
+
async add(fixture) {
|
|
337
506
|
const id = fixture.id;
|
|
338
507
|
if (this.records.has(id)) throw new Error(`test session "${id}" already added`);
|
|
339
508
|
const summary = {
|
|
@@ -342,7 +511,8 @@ var TestSessions = class {
|
|
|
342
511
|
running: false,
|
|
343
512
|
blank: false,
|
|
344
513
|
updatedAt: this.records.size + 1,
|
|
345
|
-
...fixture.summary
|
|
514
|
+
...fixture.summary,
|
|
515
|
+
retainedBy: this.retentionSnapshot(id).retainedBy
|
|
346
516
|
};
|
|
347
517
|
const snapshot = createSnapshotStore({
|
|
348
518
|
...sessionSnapshot(id),
|
|
@@ -354,15 +524,14 @@ var TestSessions = class {
|
|
|
354
524
|
summary,
|
|
355
525
|
snapshot,
|
|
356
526
|
session,
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
527
|
+
overrides: fixture.session ?? {},
|
|
528
|
+
projections: /* @__PURE__ */ new Map(),
|
|
529
|
+
initialOpen: fixture.initialOpen
|
|
360
530
|
});
|
|
361
531
|
await this.stabilize(() => {
|
|
362
532
|
this.list.update((draft) => {
|
|
363
533
|
draft.ids.push(id);
|
|
364
534
|
draft.byId[id] = summary;
|
|
365
|
-
if (opts?.current !== false) draft.current = id;
|
|
366
535
|
});
|
|
367
536
|
});
|
|
368
537
|
return id;
|
|
@@ -376,6 +545,21 @@ var TestSessions = class {
|
|
|
376
545
|
const record = this.require(id);
|
|
377
546
|
await this.stabilize(() => {
|
|
378
547
|
record.snapshot.update(mutate);
|
|
548
|
+
this.generations.get(id)?.snapshot.set(record.snapshot.getSnapshot());
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Publish one complete projection value through the fixture Session face.
|
|
553
|
+
* @param id - session id.
|
|
554
|
+
* @param key - registered projection key.
|
|
555
|
+
* @param value - complete value for that key.
|
|
556
|
+
*/
|
|
557
|
+
async setProjection(id, key, value) {
|
|
558
|
+
const record = this.require(id);
|
|
559
|
+
record.projections.set(key, value);
|
|
560
|
+
await this.stabilize(() => {
|
|
561
|
+
record.session.projections.set(key, value);
|
|
562
|
+
this.generations.get(id)?.session.projections.set(key, value);
|
|
379
563
|
});
|
|
380
564
|
}
|
|
381
565
|
/**
|
|
@@ -387,6 +571,7 @@ var TestSessions = class {
|
|
|
387
571
|
async replaceEvents(id, entries, hasMore = false) {
|
|
388
572
|
await this.stabilize(() => {
|
|
389
573
|
this.require(id).session.eventSource.replace(entries, hasMore);
|
|
574
|
+
this.generations.get(id)?.session.eventSource.replace(entries, hasMore);
|
|
390
575
|
});
|
|
391
576
|
}
|
|
392
577
|
/**
|
|
@@ -398,6 +583,7 @@ var TestSessions = class {
|
|
|
398
583
|
async prependEvents(id, entries, hasMore = false) {
|
|
399
584
|
await this.stabilize(() => {
|
|
400
585
|
this.require(id).session.eventSource.prepend(entries, hasMore);
|
|
586
|
+
this.generations.get(id)?.session.eventSource.prepend(entries, hasMore);
|
|
401
587
|
});
|
|
402
588
|
}
|
|
403
589
|
/**
|
|
@@ -408,6 +594,7 @@ var TestSessions = class {
|
|
|
408
594
|
async appendEvent(id, entry) {
|
|
409
595
|
await this.stabilize(() => {
|
|
410
596
|
this.require(id).session.eventSource.append(entry);
|
|
597
|
+
this.generations.get(id)?.session.eventSource.append(entry);
|
|
411
598
|
});
|
|
412
599
|
}
|
|
413
600
|
/**
|
|
@@ -420,7 +607,8 @@ var TestSessions = class {
|
|
|
420
607
|
const record = this.require(id);
|
|
421
608
|
record.summary = {
|
|
422
609
|
...record.summary,
|
|
423
|
-
...patch
|
|
610
|
+
...patch,
|
|
611
|
+
retainedBy: this.retentionSnapshot(id).retainedBy
|
|
424
612
|
};
|
|
425
613
|
await this.stabilize(() => {
|
|
426
614
|
this.list.update((draft) => {
|
|
@@ -429,63 +617,89 @@ var TestSessions = class {
|
|
|
429
617
|
});
|
|
430
618
|
}
|
|
431
619
|
/**
|
|
432
|
-
*
|
|
433
|
-
* @param id - session id to select, or undefined to clear.
|
|
434
|
-
*/
|
|
435
|
-
async setCurrent(id) {
|
|
436
|
-
if (id !== void 0) this.require(id);
|
|
437
|
-
await this.stabilize(() => {
|
|
438
|
-
this.list.update((draft) => {
|
|
439
|
-
draft.current = id;
|
|
440
|
-
});
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
/**
|
|
444
|
-
* Remove a session: list row, scope fiber, and per-session store instances
|
|
445
|
-
* (with persisted state) die together — the same single lifecycle axis the
|
|
446
|
-
* production Client Sessions service drives on session death, minus staging.
|
|
620
|
+
* Remove a catalog row and mark its retained Session removed without releasing owners.
|
|
447
621
|
* @param id - session id.
|
|
448
622
|
*/
|
|
449
623
|
async remove(id) {
|
|
450
|
-
|
|
624
|
+
this.require(id);
|
|
451
625
|
this.records.delete(id);
|
|
452
|
-
await this.stabilize(
|
|
626
|
+
await this.stabilize(() => {
|
|
453
627
|
this.list.update((draft) => {
|
|
454
628
|
draft.ids = draft.ids.filter((existing) => existing !== id);
|
|
455
629
|
const { [id]: _dead, ...rest } = draft.byId;
|
|
456
630
|
draft.byId = rest;
|
|
457
|
-
if (draft.current === id) draft.current = void 0;
|
|
458
631
|
});
|
|
459
|
-
|
|
632
|
+
this.generations.get(id)?.snapshot.update((draft) => {
|
|
633
|
+
draft.removed = true;
|
|
634
|
+
});
|
|
460
635
|
});
|
|
461
636
|
}
|
|
462
637
|
/**
|
|
463
|
-
*
|
|
464
|
-
* the production `createScope`, so real `scopeOf`/scope-addressed services
|
|
465
|
-
* resolve it.
|
|
638
|
+
* Borrow the already-retained session-scoped Cordis context.
|
|
466
639
|
* @param id - session id.
|
|
467
|
-
* @returns the scoped context, or undefined
|
|
640
|
+
* @returns the scoped context, or undefined without a live reference.
|
|
468
641
|
*/
|
|
469
642
|
scope(id) {
|
|
470
|
-
|
|
471
|
-
if (record === void 0) return void 0;
|
|
472
|
-
if (record.scope === void 0) {
|
|
473
|
-
const handle = createScope(this.rootCtx, id);
|
|
474
|
-
record.scope = handle.ctx;
|
|
475
|
-
record.scopeFiber = handle.fiber;
|
|
476
|
-
}
|
|
477
|
-
return record.scope;
|
|
643
|
+
return this.generations.get(id)?.binding.ctx;
|
|
478
644
|
}
|
|
479
645
|
/**
|
|
480
646
|
* Session assembly binding (inject factories and provide resolvers receive it).
|
|
481
647
|
* @param id - session id.
|
|
482
|
-
* @returns
|
|
648
|
+
* @returns the live generation's binding, or undefined without a reference.
|
|
483
649
|
*/
|
|
484
650
|
binding(id) {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
651
|
+
return this.generations.get(id)?.binding;
|
|
652
|
+
}
|
|
653
|
+
retain(target, options = { source: "testFixture" }) {
|
|
654
|
+
const { source, signal } = options;
|
|
655
|
+
signal?.throwIfAborted();
|
|
656
|
+
if (this.closed) throw new Error("test Session Controller is disposed");
|
|
657
|
+
const id = this.resolveTarget(target);
|
|
658
|
+
const generation = this.generations.get(id) ?? this.materialize(id, this.require(id));
|
|
659
|
+
const reference = this.retainGeneration(id, generation, source);
|
|
660
|
+
try {
|
|
661
|
+
reference.attachOpening(generation.opening, signal);
|
|
662
|
+
return reference;
|
|
663
|
+
} catch (error) {
|
|
664
|
+
reference.release();
|
|
665
|
+
throw error;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async using(target, options, operation) {
|
|
669
|
+
const reference = this.retain(target, options);
|
|
670
|
+
try {
|
|
671
|
+
await reference.ready;
|
|
672
|
+
return await operation(reference);
|
|
673
|
+
} finally {
|
|
674
|
+
reference.release();
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
retainInfo(id) {
|
|
678
|
+
let store = this.retentionStores.get(id);
|
|
679
|
+
if (store === void 0) {
|
|
680
|
+
store = createSnapshotStore(this.retentionSnapshot(id));
|
|
681
|
+
this.retentionStores.set(id, store);
|
|
682
|
+
}
|
|
683
|
+
return store;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Retain one fixture Session until the supplied Cordis owner stops.
|
|
687
|
+
* @param ownerCtx - context whose disposal releases the reference.
|
|
688
|
+
* @param target - fixture Session identity or subagent address.
|
|
689
|
+
* @param options - reference source and optional readiness cancellation.
|
|
690
|
+
* @returns the owned reference immediately.
|
|
691
|
+
*/
|
|
692
|
+
retainFor(ownerCtx, target, options = { source: "testFixture" }) {
|
|
693
|
+
const reference = this.retain(target, options);
|
|
694
|
+
try {
|
|
695
|
+
ownerCtx.effect(() => () => {
|
|
696
|
+
reference.release();
|
|
697
|
+
}, "test sessions: owned reference");
|
|
698
|
+
return reference;
|
|
699
|
+
} catch (error) {
|
|
700
|
+
reference.release();
|
|
701
|
+
throw error;
|
|
702
|
+
}
|
|
489
703
|
}
|
|
490
704
|
/**
|
|
491
705
|
* Read the session scope tag off a context (service-method boundary mirror).
|
|
@@ -504,7 +718,8 @@ var TestSessions = class {
|
|
|
504
718
|
sessionOf(ctx) {
|
|
505
719
|
const id = scopeOf(ctx);
|
|
506
720
|
if (id === void 0) return void 0;
|
|
507
|
-
|
|
721
|
+
const generation = this.generations.get(id);
|
|
722
|
+
return generation !== void 0 && scopeIdentityOf(generation.binding.ctx) === scopeIdentityOf(ctx) ? generation.session : void 0;
|
|
508
723
|
}
|
|
509
724
|
/**
|
|
510
725
|
* Install Session creation behavior for navigation tests.
|
|
@@ -513,7 +728,7 @@ var TestSessions = class {
|
|
|
513
728
|
stubCreate(impl) {
|
|
514
729
|
this.createStub = impl;
|
|
515
730
|
}
|
|
516
|
-
/** Create through the installed test behavior and require
|
|
731
|
+
/** Create through the installed test behavior and require a catalogued fixture. */
|
|
517
732
|
async create(opts) {
|
|
518
733
|
this.calls.push({
|
|
519
734
|
method: "create",
|
|
@@ -524,39 +739,18 @@ var TestSessions = class {
|
|
|
524
739
|
this.require(id);
|
|
525
740
|
return id;
|
|
526
741
|
}
|
|
527
|
-
/**
|
|
528
|
-
* Service-level selection call (recorded, then applied to the list store
|
|
529
|
-
* synchronously — inject callbacks call this outside any act window; the
|
|
530
|
-
* store notify is microtask-batched so the next stabilized step observes it).
|
|
531
|
-
* @param id - session id.
|
|
532
|
-
*/
|
|
533
|
-
open(id) {
|
|
534
|
-
this.calls.push({
|
|
535
|
-
method: "open",
|
|
536
|
-
args: [id]
|
|
537
|
-
});
|
|
538
|
-
this.require(id);
|
|
539
|
-
this.list.update((draft) => {
|
|
540
|
-
draft.current = id;
|
|
541
|
-
draft.currentAddress = void 0;
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
/** Open an existing fixture through its catalog address. */
|
|
545
|
-
openSubagent(address) {
|
|
546
|
-
this.calls.push({
|
|
547
|
-
method: "openSubagent",
|
|
548
|
-
args: [address]
|
|
549
|
-
});
|
|
550
|
-
this.require(address.childSessionId);
|
|
551
|
-
this.list.update((draft) => {
|
|
552
|
-
draft.current = address.childSessionId;
|
|
553
|
-
draft.currentAddress = address;
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
/** Resolve the current fixture's retained catalog address. */
|
|
742
|
+
/** Resolve a retained or catalog-derived address independently of a view. */
|
|
557
743
|
subagentAddress(id) {
|
|
558
|
-
const
|
|
559
|
-
|
|
744
|
+
const retained = this.addresses.get(id);
|
|
745
|
+
if (retained !== void 0) return retained;
|
|
746
|
+
for (const [parentSessionId, catalog] of Object.entries(this.list.getSnapshot().subagentsByParent)) {
|
|
747
|
+
const child = catalog.entries.find((entry) => entry.kind === "child" && entry.id === id);
|
|
748
|
+
if (child?.kind === "child") return {
|
|
749
|
+
parentSessionId,
|
|
750
|
+
childSessionId: id,
|
|
751
|
+
mode: child.mode
|
|
752
|
+
};
|
|
753
|
+
}
|
|
560
754
|
}
|
|
561
755
|
/** Record catalog consumption; fixture callers drive snapshots explicitly. */
|
|
562
756
|
setSubagentCatalogOpen(parentSessionId, open) {
|
|
@@ -573,17 +767,6 @@ var TestSessions = class {
|
|
|
573
767
|
});
|
|
574
768
|
return Promise.resolve();
|
|
575
769
|
}
|
|
576
|
-
/** Clear the current selection (recorded; the production no-session flow). */
|
|
577
|
-
clear() {
|
|
578
|
-
this.calls.push({
|
|
579
|
-
method: "clear",
|
|
580
|
-
args: []
|
|
581
|
-
});
|
|
582
|
-
this.list.update((draft) => {
|
|
583
|
-
draft.current = void 0;
|
|
584
|
-
draft.currentAddress = void 0;
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
770
|
/** Record a list refresh; fixture callers publish list state explicitly. */
|
|
588
771
|
refresh() {
|
|
589
772
|
this.calls.push({
|
|
@@ -640,28 +823,138 @@ var TestSessions = class {
|
|
|
640
823
|
* @returns the FixtureSession carried by the Controller binding.
|
|
641
824
|
*/
|
|
642
825
|
behavior(id) {
|
|
643
|
-
return this.require(id).session;
|
|
826
|
+
return this.generations.get(id)?.session ?? this.require(id).session;
|
|
644
827
|
}
|
|
645
828
|
/** Dispose minted scope fibers (runtime dispose path). */
|
|
646
829
|
async disposeScopes() {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
record.scopeFiber = void 0;
|
|
651
|
-
record.binding = void 0;
|
|
652
|
-
}
|
|
830
|
+
this.closed = true;
|
|
831
|
+
for (const [id, generation] of this.generations) this.drop(id, generation);
|
|
832
|
+
await this.drainDrops();
|
|
653
833
|
}
|
|
654
|
-
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
834
|
+
resolveTarget(target) {
|
|
835
|
+
const id = typeof target === "string" ? target : target.childSessionId;
|
|
836
|
+
if (typeof target !== "string") this.addresses.set(id, target);
|
|
837
|
+
this.require(id);
|
|
838
|
+
return id;
|
|
839
|
+
}
|
|
840
|
+
retainGeneration(id, generation, source) {
|
|
841
|
+
const previous = generation.retention;
|
|
842
|
+
generation.retention = Object.freeze({
|
|
843
|
+
referenceCount: previous.referenceCount + 1,
|
|
844
|
+
retainedBy: freezeRetainedBy({
|
|
845
|
+
...previous.retainedBy,
|
|
846
|
+
[source]: (previous.retainedBy[source] ?? 0) + 1
|
|
847
|
+
})
|
|
848
|
+
});
|
|
849
|
+
const reference = new TestSessionReference(id, generation, () => {
|
|
850
|
+
if (!generation.live) return;
|
|
851
|
+
const count = generation.retention.referenceCount - 1;
|
|
852
|
+
const { [source]: sourceCount = 0, ...otherSources } = generation.retention.retainedBy;
|
|
853
|
+
const retainedBy = sourceCount > 1 ? {
|
|
854
|
+
...otherSources,
|
|
855
|
+
[source]: sourceCount - 1
|
|
856
|
+
} : otherSources;
|
|
857
|
+
generation.retention = count === 0 ? EMPTY_RETAIN_INFO : Object.freeze({
|
|
858
|
+
referenceCount: count,
|
|
859
|
+
retainedBy: freezeRetainedBy(retainedBy)
|
|
860
|
+
});
|
|
861
|
+
if (count === 0) this.drop(id, generation);
|
|
862
|
+
else this.publishRetention(id);
|
|
863
|
+
});
|
|
864
|
+
this.publishRetention(id);
|
|
865
|
+
return reference;
|
|
866
|
+
}
|
|
867
|
+
retentionSnapshot(id) {
|
|
868
|
+
return this.generations.get(id)?.retention ?? EMPTY_RETAIN_INFO;
|
|
869
|
+
}
|
|
870
|
+
publishRetention(id) {
|
|
871
|
+
const retention = this.retentionSnapshot(id);
|
|
872
|
+
const store = this.retentionStores.get(id);
|
|
873
|
+
if (store !== void 0 && store.getSnapshot() !== retention) store.set(retention);
|
|
874
|
+
const state = this.list.getSnapshot();
|
|
875
|
+
const row = state.byId[id];
|
|
876
|
+
if (row === void 0 || row.retainedBy === retention.retainedBy) return;
|
|
877
|
+
const summary = {
|
|
878
|
+
...row,
|
|
879
|
+
retainedBy: retention.retainedBy
|
|
664
880
|
};
|
|
881
|
+
const record = this.records.get(id);
|
|
882
|
+
/* v8 ignore next -- a catalog row and its fixture record are inserted and removed together. */
|
|
883
|
+
if (record !== void 0) record.summary = summary;
|
|
884
|
+
this.list.set({
|
|
885
|
+
...state,
|
|
886
|
+
byId: {
|
|
887
|
+
...state.byId,
|
|
888
|
+
[id]: summary
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
materialize(id, fixture) {
|
|
893
|
+
const { ctx, fiber } = createScope(this.rootCtx, id);
|
|
894
|
+
const snapshot = createSnapshotStore(fixture.snapshot.getSnapshot());
|
|
895
|
+
const session = new FixtureSession(id, snapshot, fixture.overrides);
|
|
896
|
+
const window = fixture.session.eventSource.getSnapshot();
|
|
897
|
+
session.eventSource.replace(window.entries, window.hasMore);
|
|
898
|
+
for (const [key, value] of fixture.projections) session.projections.set(key, value);
|
|
899
|
+
const opening = Promise.withResolvers();
|
|
900
|
+
opening.promise.catch(() => {});
|
|
901
|
+
const lifetime = new AbortController();
|
|
902
|
+
const generation = {
|
|
903
|
+
binding: {
|
|
904
|
+
sessionId: id,
|
|
905
|
+
session,
|
|
906
|
+
eventSource: session.eventSource,
|
|
907
|
+
ctx
|
|
908
|
+
},
|
|
909
|
+
snapshot,
|
|
910
|
+
session,
|
|
911
|
+
fiber,
|
|
912
|
+
lifetime,
|
|
913
|
+
opening: opening.promise,
|
|
914
|
+
retention: EMPTY_RETAIN_INFO,
|
|
915
|
+
live: true
|
|
916
|
+
};
|
|
917
|
+
this.generations.set(id, generation);
|
|
918
|
+
ctx.effect(() => async () => {
|
|
919
|
+
if (generation.live) {
|
|
920
|
+
generation.live = false;
|
|
921
|
+
generation.retention = EMPTY_RETAIN_INFO;
|
|
922
|
+
if (this.generations.get(id) === generation) this.generations.delete(id);
|
|
923
|
+
this.publishRetention(id);
|
|
924
|
+
}
|
|
925
|
+
lifetime.abort(/* @__PURE__ */ new Error(`test Session generation "${id}" is disposed`));
|
|
926
|
+
await Promise.allSettled([generation.opening]);
|
|
927
|
+
}, "test sessions: exact generation");
|
|
928
|
+
this.startOpening(fixture.initialOpen, lifetime.signal, opening);
|
|
929
|
+
return generation;
|
|
930
|
+
}
|
|
931
|
+
startOpening(initialOpen, signal, opening) {
|
|
932
|
+
try {
|
|
933
|
+
Promise.resolve(initialOpen?.(signal)).then(opening.resolve, opening.reject);
|
|
934
|
+
} catch (error) {
|
|
935
|
+
opening.reject(error);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
drop(id, generation) {
|
|
939
|
+
/* v8 ignore next -- only the live generation's retained callback can enter drop. */
|
|
940
|
+
if (!generation.live) return;
|
|
941
|
+
generation.live = false;
|
|
942
|
+
generation.retention = EMPTY_RETAIN_INFO;
|
|
943
|
+
/* v8 ignore next -- this synchronous path drops only the generation currently stored for id. */
|
|
944
|
+
if (this.generations.get(id) === generation) this.generations.delete(id);
|
|
945
|
+
generation.lifetime.abort(/* @__PURE__ */ new Error(`test Session generation "${id}" is released`));
|
|
946
|
+
this.publishRetention(id);
|
|
947
|
+
const disposal = generation.fiber.dispose();
|
|
948
|
+
this.pendingDrops.add(disposal);
|
|
949
|
+
disposal.then(() => {
|
|
950
|
+
this.pendingDrops.delete(disposal);
|
|
951
|
+
}, (error) => {
|
|
952
|
+
this.pendingDrops.delete(disposal);
|
|
953
|
+
this.rootCtx.logger.warn("test Session scope disposal failed:", error);
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
async drainDrops() {
|
|
957
|
+
while (this.pendingDrops.size !== 0) await Promise.all([...this.pendingDrops]);
|
|
665
958
|
}
|
|
666
959
|
require(id) {
|
|
667
960
|
const record = this.records.get(id);
|
|
@@ -887,80 +1180,6 @@ function stubSettingsScope() {
|
|
|
887
1180
|
};
|
|
888
1181
|
}
|
|
889
1182
|
//#endregion
|
|
890
|
-
//#region lib/types/remote.js
|
|
891
|
-
/**
|
|
892
|
-
* Remote service test double for the forwarded-event path. Feature specs need
|
|
893
|
-
* `ctx.remote.$on` to exist (their plugins inject `remote`) and need forwarded
|
|
894
|
-
* Host events to reach those subscribers, but not the wire — so this double
|
|
895
|
-
* implements subscription plus an explicit `emit` driver available only on the
|
|
896
|
-
* concrete test object. A spec that also calls one namespace scripts it through
|
|
897
|
-
* the constructor rather than reaching the real Client Remote service.
|
|
898
|
-
*
|
|
899
|
-
* `$mount` rejects: a spec that needs a real generated contribution installed —
|
|
900
|
-
* codecs, descriptors, and the wire — has outgrown this double and needs the
|
|
901
|
-
* real Client Remote service.
|
|
902
|
-
*
|
|
903
|
-
* One deliberate asymmetry with production: a throwing listener propagates out
|
|
904
|
-
* of the emit instead of being contained and logged, so a spec cannot lean on
|
|
905
|
-
* this double for the containment guarantee `$on` documents — assert that
|
|
906
|
-
* against the real service.
|
|
907
|
-
*/
|
|
908
|
-
var TestRemote = class TestRemote {
|
|
909
|
-
subscriptions = /* @__PURE__ */ new Map();
|
|
910
|
-
/**
|
|
911
|
-
* Fixed Host facts mirrored from the production `ctx.remote.$host`. Plain
|
|
912
|
-
* mutable field: a spec assigns it to script a non-loopback or homed Host.
|
|
913
|
-
*/
|
|
914
|
-
$host = {
|
|
915
|
-
home: void 0,
|
|
916
|
-
isLoopback: true
|
|
917
|
-
};
|
|
918
|
-
/**
|
|
919
|
-
* Register the double as `ctx.remote`, plus one service per scripted
|
|
920
|
-
* namespace so a plugin injecting `remote.<name>` also unparks.
|
|
921
|
-
* @param ctx - the spec's root Context.
|
|
922
|
-
* @param namespaces - scripted namespace faces reached as `ctx.remote.<name>`.
|
|
923
|
-
*/
|
|
924
|
-
constructor(ctx, namespaces = {}) {
|
|
925
|
-
for (const name of Object.keys(namespaces)) if (name in TestRemote.prototype || name === "subscriptions" || name === "$host") throw new TypeError(`TestRemote: scripted namespace "${name}" would shadow the double's own member`);
|
|
926
|
-
Object.assign(this, namespaces);
|
|
927
|
-
ctx.provide("remote", this);
|
|
928
|
-
for (const [name, face] of Object.entries(namespaces)) ctx.provide(`remote.${name}`, face);
|
|
929
|
-
}
|
|
930
|
-
/**
|
|
931
|
-
* Deliver one forwarded host event to its subscribers, standing in for the
|
|
932
|
-
* carrier that owns the frame sink.
|
|
933
|
-
* @param event - forwarded host event name.
|
|
934
|
-
* @param args - the Host argument list, verbatim.
|
|
935
|
-
*/
|
|
936
|
-
emit(event, args) {
|
|
937
|
-
const listeners = this.subscriptions.get(event);
|
|
938
|
-
if (listeners === void 0) return;
|
|
939
|
-
for (const listener of [...listeners]) listener(...args);
|
|
940
|
-
}
|
|
941
|
-
/**
|
|
942
|
-
* Subscribe to one forwarded host event.
|
|
943
|
-
* @param event - forwarded host event name.
|
|
944
|
-
* @param listener - receives the Host argument list verbatim.
|
|
945
|
-
* @returns disposer removing this subscription.
|
|
946
|
-
*/
|
|
947
|
-
$on(event, listener) {
|
|
948
|
-
const listeners = this.subscriptions.get(event) ?? /* @__PURE__ */ new Set();
|
|
949
|
-
this.subscriptions.set(event, listeners);
|
|
950
|
-
listeners.add(listener);
|
|
951
|
-
return () => {
|
|
952
|
-
listeners.delete(listener);
|
|
953
|
-
};
|
|
954
|
-
}
|
|
955
|
-
/**
|
|
956
|
-
* Generated-namespace mount, unsupported by this double.
|
|
957
|
-
* @returns never; always rejects.
|
|
958
|
-
*/
|
|
959
|
-
$mount() {
|
|
960
|
-
return Promise.reject(/* @__PURE__ */ new Error("TestRemote: $mount needs the real Client Remote service"));
|
|
961
|
-
}
|
|
962
|
-
};
|
|
963
|
-
//#endregion
|
|
964
1183
|
//#region lib/types/translate.js
|
|
965
1184
|
/**
|
|
966
1185
|
* Test double of the locale lookup chain: a translate stub over plain
|
|
@@ -1146,8 +1365,10 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1146
1365
|
slots;
|
|
1147
1366
|
/** The test-owned 'root' occupant. */
|
|
1148
1367
|
root;
|
|
1149
|
-
/**
|
|
1368
|
+
/** Fixture catalog, explicit references, scoped contexts, and behavior faces. */
|
|
1150
1369
|
sessions;
|
|
1370
|
+
/** One Remote double shared by every feature mounted in this runtime. */
|
|
1371
|
+
remote;
|
|
1151
1372
|
/** Workspaces double (list observable, recorded intent actions). */
|
|
1152
1373
|
workspaces;
|
|
1153
1374
|
/** Test-owned panel selection used by the framework's usePanelInfo hook. */
|
|
@@ -1174,6 +1395,7 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1174
1395
|
this.slots = slots;
|
|
1175
1396
|
this.root = new TestRoot(slots, this.stabilizer);
|
|
1176
1397
|
this.sessions = new TestSessions(this.stabilizer, ctx);
|
|
1398
|
+
this.remote = new TestRemote(ctx);
|
|
1177
1399
|
this.workspaces = new TestWorkspaces(this.stabilizer);
|
|
1178
1400
|
this.fileUpload = { upload: () => Promise.reject(/* @__PURE__ */ new Error("client test runtime: file upload is not stubbed")) };
|
|
1179
1401
|
ctx.provide("sessions", this.sessions);
|
|
@@ -1262,7 +1484,17 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1262
1484
|
const cell = this.ownerCell;
|
|
1263
1485
|
const AutoFrame = (props) => {
|
|
1264
1486
|
useSyncExternalStore(cell.subscribe, cell.getVersion);
|
|
1265
|
-
return createElement(Fragment, null, cell.entries().map(([key, { owner, opts }]) =>
|
|
1487
|
+
return createElement(Fragment, null, cell.entries().map(([key, { owner, opts }]) => {
|
|
1488
|
+
const body = props.renderSlot(key, owner, opts);
|
|
1489
|
+
const spec = children[key];
|
|
1490
|
+
if (spec.scope === "root") return createElement(Fragment, { key }, body);
|
|
1491
|
+
return createElement(props.SessionProvider, {
|
|
1492
|
+
key,
|
|
1493
|
+
session: opts?.session,
|
|
1494
|
+
children: body,
|
|
1495
|
+
...spec.scope === "session-maybe" ? { empty: () => body } : {}
|
|
1496
|
+
});
|
|
1497
|
+
}));
|
|
1266
1498
|
};
|
|
1267
1499
|
await this.root.declare(children, AutoFrame);
|
|
1268
1500
|
}
|
|
@@ -1274,14 +1506,16 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1274
1506
|
* slot of the same tree.
|
|
1275
1507
|
* @param key - a key declared through {@link SlotTestRuntime.declare}.
|
|
1276
1508
|
* @param owner - owner props share for the render site.
|
|
1277
|
-
* @param opts - explicit
|
|
1509
|
+
* @param opts - explicit entry selection and borrowed Session reference; retained by view updates.
|
|
1278
1510
|
* @returns the slot-local view (snapshot container, scoped queries, owner updates).
|
|
1279
1511
|
*/
|
|
1280
1512
|
renderSlot(key, owner, opts) {
|
|
1281
1513
|
if (!this.autoDeclared.has(key)) throw new Error(`renderSlot('${key}') without declare() — declare the key first (or use root.declare for a custom frame)`);
|
|
1282
|
-
|
|
1514
|
+
let options = opts;
|
|
1515
|
+
const install = (next, nextOptions = options) => {
|
|
1516
|
+
options = nextOptions;
|
|
1283
1517
|
act(() => {
|
|
1284
|
-
this.ownerCell.set(key, next,
|
|
1518
|
+
this.ownerCell.set(key, next, options);
|
|
1285
1519
|
});
|
|
1286
1520
|
};
|
|
1287
1521
|
install(owner);
|
|
@@ -1300,20 +1534,33 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1300
1534
|
* {@link SlotTestRuntime.renderRoot} — the host face exists only inside the
|
|
1301
1535
|
* installed renderer, exactly as in production.
|
|
1302
1536
|
* @param key - slot key whose first entry declares the store.
|
|
1303
|
-
* @param
|
|
1537
|
+
* @param session - retained Session reference for session-scope slots; omit for root scope.
|
|
1304
1538
|
* @returns the live store instance.
|
|
1305
1539
|
*/
|
|
1306
|
-
storeOf(key,
|
|
1540
|
+
storeOf(key, session) {
|
|
1307
1541
|
if (this.host === void 0) throw new Error("storeOf before renderRoot() — the host face exists only inside the installed renderer");
|
|
1308
1542
|
const entry = this.host.entriesOf(key)[0];
|
|
1309
1543
|
if (entry === void 0) throw new Error(`storeOf('${key}'): no registration on the ledger`);
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1544
|
+
const adapter = session === void 0 ? void 0 : this.host.scope("session");
|
|
1545
|
+
const resolved = session === void 0 ? void 0 : adapter?.bindingSource(session).getSnapshot();
|
|
1546
|
+
const scopeBinding = resolved?.key === void 0 ? void 0 : resolved;
|
|
1547
|
+
if (session !== void 0 && scopeBinding === void 0) throw new Error(`storeOf('${key}'): no live Session binding for '${session.sessionId}'`);
|
|
1312
1548
|
const instance = this.host.storeOf(entry, scopeBinding);
|
|
1313
1549
|
if (instance === void 0) throw new Error(`storeOf('${key}'): the entry declares no store`);
|
|
1314
1550
|
return instance;
|
|
1315
1551
|
}
|
|
1316
1552
|
/**
|
|
1553
|
+
* Read one registered Factory definition for direct contract assertions.
|
|
1554
|
+
* @param name - registered Factory name.
|
|
1555
|
+
* @returns the live Factory definition.
|
|
1556
|
+
*/
|
|
1557
|
+
factoryOf(name) {
|
|
1558
|
+
if (this.host === void 0) throw new Error("factoryOf before renderRoot()");
|
|
1559
|
+
const definition = this.host.factoryOf(name);
|
|
1560
|
+
if (definition === void 0) throw new Error(`factoryOf('${name}'): no definition`);
|
|
1561
|
+
return definition;
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1317
1564
|
* Flush pending ledger/store notifications inside act — for mutations made
|
|
1318
1565
|
* outside the runtime's own methods (e.g. a direct `slots.register`).
|
|
1319
1566
|
* @returns completion of the act pass.
|
|
@@ -1337,6 +1584,7 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1337
1584
|
this.disposeWorkspaceSource();
|
|
1338
1585
|
this.disposePanelInfoSource();
|
|
1339
1586
|
await this.sessions.disposeScopes();
|
|
1587
|
+
await this.stabilizer(() => this.ctx.fiber.dispose());
|
|
1340
1588
|
localStorage.clear();
|
|
1341
1589
|
}
|
|
1342
1590
|
};
|