@deepseek-ai/dsh-client-test-runtime 0.1.6-alpha.1 → 0.1.7-alpha.1
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 +7 -3
- package/README.zh.md +7 -3
- package/lib/index.js +509 -217
- package/lib/types/assembly/bundle-roster.d.ts +4 -3
- package/lib/types/assembly/roster.d.ts +4 -4
- package/lib/types/assembly/test-client.d.ts +1 -1
- package/lib/types/{settings-scope.d.ts → config-form.d.ts} +8 -8
- package/lib/types/fixtures.d.ts +2 -0
- package/lib/types/index.d.ts +24 -9
- package/lib/types/remote.d.ts +8 -0
- package/lib/types/sessions.d.ts +54 -42
- package/lib/types/workspaces.d.ts +20 -1
- package/package.json +51 -51
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,
|
|
@@ -142,6 +232,7 @@ function workspaceSnapshot() {
|
|
|
142
232
|
return {
|
|
143
233
|
items: [],
|
|
144
234
|
archivedSessionIds: [],
|
|
235
|
+
pinnedSessionIds: [],
|
|
145
236
|
state: "idle",
|
|
146
237
|
phase: "ready",
|
|
147
238
|
error: null
|
|
@@ -286,23 +377,94 @@ var FixtureSession = class {
|
|
|
286
377
|
throw new Error(`test session "${this.sessionId}": rename is not stubbed — supply it on the fixture's session face`);
|
|
287
378
|
}
|
|
288
379
|
};
|
|
380
|
+
function freezeRetainedBy(counts) {
|
|
381
|
+
Object.setPrototypeOf(counts, null);
|
|
382
|
+
return Object.freeze(counts);
|
|
383
|
+
}
|
|
384
|
+
const EMPTY_RETAIN_INFO = Object.freeze({
|
|
385
|
+
referenceCount: 0,
|
|
386
|
+
retainedBy: freezeRetainedBy({})
|
|
387
|
+
});
|
|
388
|
+
async function waitForOpen(opening, signal) {
|
|
389
|
+
/* v8 ignore next -- TestSessionReference always supplies its release-composed signal. */
|
|
390
|
+
if (signal === void 0) return opening;
|
|
391
|
+
const aborted = Promise.withResolvers();
|
|
392
|
+
const onAbort = () => {
|
|
393
|
+
aborted.reject(signal.reason);
|
|
394
|
+
};
|
|
395
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
396
|
+
try {
|
|
397
|
+
if (signal.aborted) onAbort();
|
|
398
|
+
await Promise.race([opening, aborted.promise]);
|
|
399
|
+
} finally {
|
|
400
|
+
signal.removeEventListener("abort", onAbort);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
var TestSessionReference = class {
|
|
404
|
+
sessionId;
|
|
405
|
+
generation;
|
|
406
|
+
releaseReference;
|
|
407
|
+
released = new AbortController();
|
|
408
|
+
readiness = Promise.withResolvers();
|
|
409
|
+
ready = this.readiness.promise;
|
|
410
|
+
constructor(sessionId, generation, releaseReference) {
|
|
411
|
+
this.sessionId = sessionId;
|
|
412
|
+
this.generation = generation;
|
|
413
|
+
this.releaseReference = releaseReference;
|
|
414
|
+
this.ready.catch(() => {});
|
|
415
|
+
}
|
|
416
|
+
get binding() {
|
|
417
|
+
if (this.generation === void 0 || !this.generation.live) throw new Error(`Session reference "${this.sessionId}" is released`);
|
|
418
|
+
return this.generation.binding;
|
|
419
|
+
}
|
|
420
|
+
attachOpening(opening, signal) {
|
|
421
|
+
const waitSignal = signal === void 0 ? this.released.signal : AbortSignal.any([this.released.signal, signal]);
|
|
422
|
+
waitForOpen(opening, waitSignal).then(() => {
|
|
423
|
+
try {
|
|
424
|
+
waitSignal.throwIfAborted();
|
|
425
|
+
this.readiness.resolve(this.binding);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
this.readiness.reject(error);
|
|
428
|
+
}
|
|
429
|
+
}, (error) => {
|
|
430
|
+
this.readiness.reject(error);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
release() {
|
|
434
|
+
const reason = /* @__PURE__ */ new Error(`Session reference "${this.sessionId}" is released`);
|
|
435
|
+
const release = this.releaseReference;
|
|
436
|
+
this.released.abort(reason);
|
|
437
|
+
this.readiness.reject(reason);
|
|
438
|
+
this.generation = void 0;
|
|
439
|
+
this.releaseReference = void 0;
|
|
440
|
+
release?.();
|
|
441
|
+
}
|
|
442
|
+
[Symbol.dispose]() {
|
|
443
|
+
this.release();
|
|
444
|
+
}
|
|
445
|
+
};
|
|
289
446
|
/**
|
|
290
447
|
* Sessions test double behind the renderer host and feature injects: owns the
|
|
291
|
-
*
|
|
448
|
+
* catalog observable, scope minting through the production `createScope`,
|
|
292
449
|
* stable Controller bindings, and the session behavior face supplied per
|
|
293
450
|
* fixture. `ui-session` owns standard-source materialization.
|
|
294
451
|
*
|
|
295
452
|
* Implements the same ISessions face features receive as `ctx.sessions`, so
|
|
296
453
|
* a production face change breaks this double at compile time; the extra
|
|
297
|
-
* members (add/updateSessionSnapshot/event-window drivers/
|
|
454
|
+
* members (add/updateSessionSnapshot/event-window drivers/remove/
|
|
298
455
|
* behavior/calls/stubs) are bench-only surface.
|
|
299
456
|
*/
|
|
300
457
|
var TestSessions = class {
|
|
301
458
|
stabilize;
|
|
302
459
|
rootCtx;
|
|
303
|
-
/** The useSessions
|
|
460
|
+
/** The useSessions catalog feed, independent of view ownership. */
|
|
304
461
|
list;
|
|
305
462
|
records = /* @__PURE__ */ new Map();
|
|
463
|
+
generations = /* @__PURE__ */ new Map();
|
|
464
|
+
addresses = /* @__PURE__ */ new Map();
|
|
465
|
+
retentionStores = /* @__PURE__ */ new Map();
|
|
466
|
+
pendingDrops = /* @__PURE__ */ new Set();
|
|
467
|
+
closed = false;
|
|
306
468
|
/** Calls observed on the service-level face, newest last. */
|
|
307
469
|
calls = [];
|
|
308
470
|
/** The wire schema's `session.search` result bound (production parity). */
|
|
@@ -320,20 +482,27 @@ var TestSessions = class {
|
|
|
320
482
|
this.list = createSnapshotStore({
|
|
321
483
|
ids: [],
|
|
322
484
|
byId: {},
|
|
323
|
-
current: void 0,
|
|
324
485
|
phase: "ready",
|
|
325
|
-
|
|
326
|
-
jobsBySession: {},
|
|
327
|
-
currentAddress: void 0
|
|
486
|
+
projectionsBySession: {}
|
|
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,66 +739,27 @@ 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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
744
|
+
const retained = this.addresses.get(id);
|
|
745
|
+
if (retained !== void 0) return retained;
|
|
746
|
+
for (const [parentSessionId, projections] of Object.entries(this.list.getSnapshot().projectionsBySession)) {
|
|
747
|
+
const child = projections.values.subagentCatalog?.find((entry) => entry.id === id);
|
|
748
|
+
if (child !== void 0) return {
|
|
749
|
+
parentSessionId,
|
|
750
|
+
childSessionId: id,
|
|
751
|
+
mode: child.mode
|
|
752
|
+
};
|
|
753
|
+
}
|
|
567
754
|
}
|
|
568
|
-
/** Record a
|
|
569
|
-
|
|
755
|
+
/** Record a projection refresh; fixture callers drive snapshots explicitly. */
|
|
756
|
+
refreshProjections(sessionId) {
|
|
570
757
|
this.calls.push({
|
|
571
|
-
method: "
|
|
572
|
-
args: [
|
|
758
|
+
method: "refreshProjections",
|
|
759
|
+
args: [sessionId]
|
|
573
760
|
});
|
|
574
761
|
return Promise.resolve();
|
|
575
762
|
}
|
|
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
763
|
/** Record a list refresh; fixture callers publish list state explicitly. */
|
|
588
764
|
refresh() {
|
|
589
765
|
this.calls.push({
|
|
@@ -640,28 +816,138 @@ var TestSessions = class {
|
|
|
640
816
|
* @returns the FixtureSession carried by the Controller binding.
|
|
641
817
|
*/
|
|
642
818
|
behavior(id) {
|
|
643
|
-
return this.require(id).session;
|
|
819
|
+
return this.generations.get(id)?.session ?? this.require(id).session;
|
|
644
820
|
}
|
|
645
821
|
/** Dispose minted scope fibers (runtime dispose path). */
|
|
646
822
|
async disposeScopes() {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
record.scopeFiber = void 0;
|
|
651
|
-
record.binding = void 0;
|
|
652
|
-
}
|
|
823
|
+
this.closed = true;
|
|
824
|
+
for (const [id, generation] of this.generations) this.drop(id, generation);
|
|
825
|
+
await this.drainDrops();
|
|
653
826
|
}
|
|
654
|
-
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
827
|
+
resolveTarget(target) {
|
|
828
|
+
const id = typeof target === "string" ? target : target.childSessionId;
|
|
829
|
+
if (typeof target !== "string") this.addresses.set(id, target);
|
|
830
|
+
this.require(id);
|
|
831
|
+
return id;
|
|
832
|
+
}
|
|
833
|
+
retainGeneration(id, generation, source) {
|
|
834
|
+
const previous = generation.retention;
|
|
835
|
+
generation.retention = Object.freeze({
|
|
836
|
+
referenceCount: previous.referenceCount + 1,
|
|
837
|
+
retainedBy: freezeRetainedBy({
|
|
838
|
+
...previous.retainedBy,
|
|
839
|
+
[source]: (previous.retainedBy[source] ?? 0) + 1
|
|
840
|
+
})
|
|
841
|
+
});
|
|
842
|
+
const reference = new TestSessionReference(id, generation, () => {
|
|
843
|
+
if (!generation.live) return;
|
|
844
|
+
const count = generation.retention.referenceCount - 1;
|
|
845
|
+
const { [source]: sourceCount = 0, ...otherSources } = generation.retention.retainedBy;
|
|
846
|
+
const retainedBy = sourceCount > 1 ? {
|
|
847
|
+
...otherSources,
|
|
848
|
+
[source]: sourceCount - 1
|
|
849
|
+
} : otherSources;
|
|
850
|
+
generation.retention = count === 0 ? EMPTY_RETAIN_INFO : Object.freeze({
|
|
851
|
+
referenceCount: count,
|
|
852
|
+
retainedBy: freezeRetainedBy(retainedBy)
|
|
853
|
+
});
|
|
854
|
+
if (count === 0) this.drop(id, generation);
|
|
855
|
+
else this.publishRetention(id);
|
|
856
|
+
});
|
|
857
|
+
this.publishRetention(id);
|
|
858
|
+
return reference;
|
|
859
|
+
}
|
|
860
|
+
retentionSnapshot(id) {
|
|
861
|
+
return this.generations.get(id)?.retention ?? EMPTY_RETAIN_INFO;
|
|
862
|
+
}
|
|
863
|
+
publishRetention(id) {
|
|
864
|
+
const retention = this.retentionSnapshot(id);
|
|
865
|
+
const store = this.retentionStores.get(id);
|
|
866
|
+
if (store !== void 0 && store.getSnapshot() !== retention) store.set(retention);
|
|
867
|
+
const state = this.list.getSnapshot();
|
|
868
|
+
const row = state.byId[id];
|
|
869
|
+
if (row === void 0 || row.retainedBy === retention.retainedBy) return;
|
|
870
|
+
const summary = {
|
|
871
|
+
...row,
|
|
872
|
+
retainedBy: retention.retainedBy
|
|
664
873
|
};
|
|
874
|
+
const record = this.records.get(id);
|
|
875
|
+
/* v8 ignore next -- a catalog row and its fixture record are inserted and removed together. */
|
|
876
|
+
if (record !== void 0) record.summary = summary;
|
|
877
|
+
this.list.set({
|
|
878
|
+
...state,
|
|
879
|
+
byId: {
|
|
880
|
+
...state.byId,
|
|
881
|
+
[id]: summary
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
materialize(id, fixture) {
|
|
886
|
+
const { ctx, fiber } = createScope(this.rootCtx, id);
|
|
887
|
+
const snapshot = createSnapshotStore(fixture.snapshot.getSnapshot());
|
|
888
|
+
const session = new FixtureSession(id, snapshot, fixture.overrides);
|
|
889
|
+
const window = fixture.session.eventSource.getSnapshot();
|
|
890
|
+
session.eventSource.replace(window.entries, window.hasMore);
|
|
891
|
+
for (const [key, value] of fixture.projections) session.projections.set(key, value);
|
|
892
|
+
const opening = Promise.withResolvers();
|
|
893
|
+
opening.promise.catch(() => {});
|
|
894
|
+
const lifetime = new AbortController();
|
|
895
|
+
const generation = {
|
|
896
|
+
binding: {
|
|
897
|
+
sessionId: id,
|
|
898
|
+
session,
|
|
899
|
+
eventSource: session.eventSource,
|
|
900
|
+
ctx
|
|
901
|
+
},
|
|
902
|
+
snapshot,
|
|
903
|
+
session,
|
|
904
|
+
fiber,
|
|
905
|
+
lifetime,
|
|
906
|
+
opening: opening.promise,
|
|
907
|
+
retention: EMPTY_RETAIN_INFO,
|
|
908
|
+
live: true
|
|
909
|
+
};
|
|
910
|
+
this.generations.set(id, generation);
|
|
911
|
+
ctx.effect(() => async () => {
|
|
912
|
+
if (generation.live) {
|
|
913
|
+
generation.live = false;
|
|
914
|
+
generation.retention = EMPTY_RETAIN_INFO;
|
|
915
|
+
if (this.generations.get(id) === generation) this.generations.delete(id);
|
|
916
|
+
this.publishRetention(id);
|
|
917
|
+
}
|
|
918
|
+
lifetime.abort(/* @__PURE__ */ new Error(`test Session generation "${id}" is disposed`));
|
|
919
|
+
await Promise.allSettled([generation.opening]);
|
|
920
|
+
}, "test sessions: exact generation");
|
|
921
|
+
this.startOpening(fixture.initialOpen, lifetime.signal, opening);
|
|
922
|
+
return generation;
|
|
923
|
+
}
|
|
924
|
+
startOpening(initialOpen, signal, opening) {
|
|
925
|
+
try {
|
|
926
|
+
Promise.resolve(initialOpen?.(signal)).then(opening.resolve, opening.reject);
|
|
927
|
+
} catch (error) {
|
|
928
|
+
opening.reject(error);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
drop(id, generation) {
|
|
932
|
+
/* v8 ignore next -- only the live generation's retained callback can enter drop. */
|
|
933
|
+
if (!generation.live) return;
|
|
934
|
+
generation.live = false;
|
|
935
|
+
generation.retention = EMPTY_RETAIN_INFO;
|
|
936
|
+
/* v8 ignore next -- this synchronous path drops only the generation currently stored for id. */
|
|
937
|
+
if (this.generations.get(id) === generation) this.generations.delete(id);
|
|
938
|
+
generation.lifetime.abort(/* @__PURE__ */ new Error(`test Session generation "${id}" is released`));
|
|
939
|
+
this.publishRetention(id);
|
|
940
|
+
const disposal = generation.fiber.dispose();
|
|
941
|
+
this.pendingDrops.add(disposal);
|
|
942
|
+
disposal.then(() => {
|
|
943
|
+
this.pendingDrops.delete(disposal);
|
|
944
|
+
}, (error) => {
|
|
945
|
+
this.pendingDrops.delete(disposal);
|
|
946
|
+
this.rootCtx.logger.warn("test Session scope disposal failed:", error);
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
async drainDrops() {
|
|
950
|
+
while (this.pendingDrops.size !== 0) await Promise.all([...this.pendingDrops]);
|
|
665
951
|
}
|
|
666
952
|
require(id) {
|
|
667
953
|
const record = this.records.get(id);
|
|
@@ -732,6 +1018,19 @@ var TestWorkspaces = class {
|
|
|
732
1018
|
};
|
|
733
1019
|
}
|
|
734
1020
|
/**
|
|
1021
|
+
* Initialize the default Workspace through a test stub; defaults to an ineligible first use.
|
|
1022
|
+
* @param request - initial directory name and title.
|
|
1023
|
+
* @param signal - caller lifetime.
|
|
1024
|
+
* @returns the stubbed Workspace, or undefined when initialization is ineligible.
|
|
1025
|
+
*/
|
|
1026
|
+
async initializeDefault(request, signal) {
|
|
1027
|
+
this.calls.push({
|
|
1028
|
+
method: "initializeDefault",
|
|
1029
|
+
args: [request, signal]
|
|
1030
|
+
});
|
|
1031
|
+
return await this.stubs.get("initializeDefault")?.(request, signal);
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
735
1034
|
* Rename a Workspace (recorded). The default echoes a minimal view.
|
|
736
1035
|
* @param workspaceId - target workspace.
|
|
737
1036
|
* @param title - new title.
|
|
@@ -837,16 +1136,54 @@ var TestWorkspaces = class {
|
|
|
837
1136
|
draft.archivedSessionIds = draft.archivedSessionIds.filter((id) => id !== sessionId);
|
|
838
1137
|
});
|
|
839
1138
|
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Pin a session (recorded). The default mirrors the production face's
|
|
1141
|
+
* observable effect: the id leads the list state's pin set.
|
|
1142
|
+
* @param sessionId - session to pin.
|
|
1143
|
+
*/
|
|
1144
|
+
async pinSession(sessionId) {
|
|
1145
|
+
this.calls.push({
|
|
1146
|
+
method: "pinSession",
|
|
1147
|
+
args: [sessionId]
|
|
1148
|
+
});
|
|
1149
|
+
const stub = this.stubs.get("pinSession");
|
|
1150
|
+
if (stub !== void 0) {
|
|
1151
|
+
await stub(sessionId);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
await this.update((draft) => {
|
|
1155
|
+
draft.pinnedSessionIds = [sessionId, ...draft.pinnedSessionIds.filter((id) => id !== sessionId)];
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Unpin a session (recorded). The default mirrors the production face's
|
|
1160
|
+
* observable effect: the id leaves the list state's pin set.
|
|
1161
|
+
* @param sessionId - session to unpin.
|
|
1162
|
+
*/
|
|
1163
|
+
async unpinSession(sessionId) {
|
|
1164
|
+
this.calls.push({
|
|
1165
|
+
method: "unpinSession",
|
|
1166
|
+
args: [sessionId]
|
|
1167
|
+
});
|
|
1168
|
+
const stub = this.stubs.get("unpinSession");
|
|
1169
|
+
if (stub !== void 0) {
|
|
1170
|
+
await stub(sessionId);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
await this.update((draft) => {
|
|
1174
|
+
draft.pinnedSessionIds = draft.pinnedSessionIds.filter((id) => id !== sessionId);
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
840
1177
|
};
|
|
841
1178
|
//#endregion
|
|
842
|
-
//#region lib/types/
|
|
843
|
-
/** Test
|
|
1179
|
+
//#region lib/types/config-form.js
|
|
1180
|
+
/** Test doubles for settings transport. */
|
|
844
1181
|
/**
|
|
845
1182
|
* Build an in-memory settings scope for service specs: starts in the host
|
|
846
1183
|
* loading state, records writes, and lets the test publish Host acceptances.
|
|
847
1184
|
* @returns the stub handle.
|
|
848
1185
|
*/
|
|
849
|
-
function
|
|
1186
|
+
function stubConfigForm() {
|
|
850
1187
|
let snapshot = {
|
|
851
1188
|
status: "loading",
|
|
852
1189
|
value: void 0,
|
|
@@ -857,9 +1194,9 @@ function stubSettingsScope() {
|
|
|
857
1194
|
mode: "host"
|
|
858
1195
|
};
|
|
859
1196
|
const listeners = /* @__PURE__ */ new Set();
|
|
860
|
-
const set = vi.fn(() => Promise.resolve());
|
|
861
|
-
const mutate = vi.fn(() => Promise.resolve());
|
|
862
|
-
const unset = vi.fn(() => Promise.resolve());
|
|
1197
|
+
const set = vi.fn(() => Promise.resolve(true));
|
|
1198
|
+
const mutate = vi.fn(() => Promise.resolve(true));
|
|
1199
|
+
const unset = vi.fn(() => Promise.resolve(true));
|
|
863
1200
|
return {
|
|
864
1201
|
scope: {
|
|
865
1202
|
getSnapshot: () => snapshot,
|
|
@@ -887,80 +1224,6 @@ function stubSettingsScope() {
|
|
|
887
1224
|
};
|
|
888
1225
|
}
|
|
889
1226
|
//#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
1227
|
//#region lib/types/translate.js
|
|
965
1228
|
/**
|
|
966
1229
|
* Test double of the locale lookup chain: a translate stub over plain
|
|
@@ -1146,8 +1409,10 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1146
1409
|
slots;
|
|
1147
1410
|
/** The test-owned 'root' occupant. */
|
|
1148
1411
|
root;
|
|
1149
|
-
/**
|
|
1412
|
+
/** Fixture catalog, explicit references, scoped contexts, and behavior faces. */
|
|
1150
1413
|
sessions;
|
|
1414
|
+
/** One Remote double shared by every feature mounted in this runtime. */
|
|
1415
|
+
remote;
|
|
1151
1416
|
/** Workspaces double (list observable, recorded intent actions). */
|
|
1152
1417
|
workspaces;
|
|
1153
1418
|
/** Test-owned panel selection used by the framework's usePanelInfo hook. */
|
|
@@ -1174,6 +1439,7 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1174
1439
|
this.slots = slots;
|
|
1175
1440
|
this.root = new TestRoot(slots, this.stabilizer);
|
|
1176
1441
|
this.sessions = new TestSessions(this.stabilizer, ctx);
|
|
1442
|
+
this.remote = new TestRemote(ctx);
|
|
1177
1443
|
this.workspaces = new TestWorkspaces(this.stabilizer);
|
|
1178
1444
|
this.fileUpload = { upload: () => Promise.reject(/* @__PURE__ */ new Error("client test runtime: file upload is not stubbed")) };
|
|
1179
1445
|
ctx.provide("sessions", this.sessions);
|
|
@@ -1262,7 +1528,17 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1262
1528
|
const cell = this.ownerCell;
|
|
1263
1529
|
const AutoFrame = (props) => {
|
|
1264
1530
|
useSyncExternalStore(cell.subscribe, cell.getVersion);
|
|
1265
|
-
return createElement(Fragment, null, cell.entries().map(([key, { owner, opts }]) =>
|
|
1531
|
+
return createElement(Fragment, null, cell.entries().map(([key, { owner, opts }]) => {
|
|
1532
|
+
const body = props.renderSlot(key, owner, opts);
|
|
1533
|
+
const spec = children[key];
|
|
1534
|
+
if (spec.scope === "root") return createElement(Fragment, { key }, body);
|
|
1535
|
+
return createElement(props.SessionProvider, {
|
|
1536
|
+
key,
|
|
1537
|
+
session: opts?.session,
|
|
1538
|
+
children: body,
|
|
1539
|
+
...spec.scope === "session-maybe" ? { empty: () => body } : {}
|
|
1540
|
+
});
|
|
1541
|
+
}));
|
|
1266
1542
|
};
|
|
1267
1543
|
await this.root.declare(children, AutoFrame);
|
|
1268
1544
|
}
|
|
@@ -1274,14 +1550,16 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1274
1550
|
* slot of the same tree.
|
|
1275
1551
|
* @param key - a key declared through {@link SlotTestRuntime.declare}.
|
|
1276
1552
|
* @param owner - owner props share for the render site.
|
|
1277
|
-
* @param opts - explicit
|
|
1553
|
+
* @param opts - explicit entry selection and borrowed Session reference; retained by view updates.
|
|
1278
1554
|
* @returns the slot-local view (snapshot container, scoped queries, owner updates).
|
|
1279
1555
|
*/
|
|
1280
1556
|
renderSlot(key, owner, opts) {
|
|
1281
1557
|
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
|
-
|
|
1558
|
+
let options = opts;
|
|
1559
|
+
const install = (next, nextOptions = options) => {
|
|
1560
|
+
options = nextOptions;
|
|
1283
1561
|
act(() => {
|
|
1284
|
-
this.ownerCell.set(key, next,
|
|
1562
|
+
this.ownerCell.set(key, next, options);
|
|
1285
1563
|
});
|
|
1286
1564
|
};
|
|
1287
1565
|
install(owner);
|
|
@@ -1300,20 +1578,33 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1300
1578
|
* {@link SlotTestRuntime.renderRoot} — the host face exists only inside the
|
|
1301
1579
|
* installed renderer, exactly as in production.
|
|
1302
1580
|
* @param key - slot key whose first entry declares the store.
|
|
1303
|
-
* @param
|
|
1581
|
+
* @param session - retained Session reference for session-scope slots; omit for root scope.
|
|
1304
1582
|
* @returns the live store instance.
|
|
1305
1583
|
*/
|
|
1306
|
-
storeOf(key,
|
|
1584
|
+
storeOf(key, session) {
|
|
1307
1585
|
if (this.host === void 0) throw new Error("storeOf before renderRoot() — the host face exists only inside the installed renderer");
|
|
1308
1586
|
const entry = this.host.entriesOf(key)[0];
|
|
1309
1587
|
if (entry === void 0) throw new Error(`storeOf('${key}'): no registration on the ledger`);
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1588
|
+
const adapter = session === void 0 ? void 0 : this.host.scope("session");
|
|
1589
|
+
const resolved = session === void 0 ? void 0 : adapter?.bindingSource(session).getSnapshot();
|
|
1590
|
+
const scopeBinding = resolved?.key === void 0 ? void 0 : resolved;
|
|
1591
|
+
if (session !== void 0 && scopeBinding === void 0) throw new Error(`storeOf('${key}'): no live Session binding for '${session.sessionId}'`);
|
|
1312
1592
|
const instance = this.host.storeOf(entry, scopeBinding);
|
|
1313
1593
|
if (instance === void 0) throw new Error(`storeOf('${key}'): the entry declares no store`);
|
|
1314
1594
|
return instance;
|
|
1315
1595
|
}
|
|
1316
1596
|
/**
|
|
1597
|
+
* Read one registered Factory definition for direct contract assertions.
|
|
1598
|
+
* @param name - registered Factory name.
|
|
1599
|
+
* @returns the live Factory definition.
|
|
1600
|
+
*/
|
|
1601
|
+
factoryOf(name) {
|
|
1602
|
+
if (this.host === void 0) throw new Error("factoryOf before renderRoot()");
|
|
1603
|
+
const definition = this.host.factoryOf(name);
|
|
1604
|
+
if (definition === void 0) throw new Error(`factoryOf('${name}'): no definition`);
|
|
1605
|
+
return definition;
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1317
1608
|
* Flush pending ledger/store notifications inside act — for mutations made
|
|
1318
1609
|
* outside the runtime's own methods (e.g. a direct `slots.register`).
|
|
1319
1610
|
* @returns completion of the act pass.
|
|
@@ -1337,8 +1628,9 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1337
1628
|
this.disposeWorkspaceSource();
|
|
1338
1629
|
this.disposePanelInfoSource();
|
|
1339
1630
|
await this.sessions.disposeScopes();
|
|
1631
|
+
await this.stabilizer(() => this.ctx.fiber.dispose());
|
|
1340
1632
|
localStorage.clear();
|
|
1341
1633
|
}
|
|
1342
1634
|
};
|
|
1343
1635
|
//#endregion
|
|
1344
|
-
export { FixtureSession, RemoteError, SlotTestRuntime, TestRemote, TestRoot, TestSessions, TestWorkspaces, bindSnapshotSelector, chatSnapshot, conversationSnapshot, createSlotRenderer, domSnapshotSerializer, makeTranslate, registerDomSnapshotSerializer, sessionSnapshot,
|
|
1636
|
+
export { FixtureSession, RemoteError, SlotTestRuntime, TestRemote, TestRoot, TestSessions, TestWorkspaces, bindSnapshotSelector, chatSnapshot, conversationSnapshot, createSlotRenderer, domSnapshotSerializer, makeTranslate, registerDomSnapshotSerializer, sessionSnapshot, stubConfigForm, usePinnedBrowserLanguages, workspaceSnapshot };
|