@deepseek-ai/dsh-client-test-runtime 0.1.1-rc.2 → 0.1.2-alpha.3
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 +132 -6
- package/README.zh.md +133 -7
- package/lib/index.js +246 -239
- package/lib/types/fixtures.d.ts +35 -10
- package/lib/types/index.d.ts +12 -18
- package/lib/types/remote.d.ts +22 -13
- package/lib/types/sessions.d.ts +70 -62
- package/lib/types/settings-remote.d.ts +83 -0
- package/lib/types/settings-scope.d.ts +3 -1
- package/lib/types/workspaces.d.ts +17 -43
- package/package.json +33 -13
package/lib/index.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { Context, Inject } from "@deepseek-ai/cordis";
|
|
2
2
|
import { Fragment, createElement, useSyncExternalStore } from "react";
|
|
3
3
|
import { act, render, within } from "@testing-library/react";
|
|
4
|
-
import {
|
|
4
|
+
import { SlotRegistry } from "@deepseek-ai/dsh-client-ui-renderer/client";
|
|
5
5
|
import { bindSnapshotSelector as bindSnapshotSelector$1 } from "@deepseek-ai/dsh-client-ui-renderer/src/client/bind.ts";
|
|
6
6
|
import { createSlotRenderer as createSlotRenderer$1 } from "@deepseek-ai/dsh-client-ui-renderer/src/client/scoped-slots.tsx";
|
|
7
|
+
import { apply, inject } from "@deepseek-ai/dsh-client-ui-session/client";
|
|
7
8
|
import { afterEach, beforeEach, expect, vi } from "vitest";
|
|
8
|
-
import { SESSION_SEARCH_RESULT_LIMIT } from "@deepseek-ai/dsh-
|
|
9
|
+
import { MutableSessionEventSource, SESSION_SEARCH_RESULT_LIMIT, createScope, scopeOf } from "@deepseek-ai/dsh-api-session-controller/client";
|
|
10
|
+
import { createSnapshotStore } from "@deepseek-ai/dsh-client-store";
|
|
11
|
+
import { EMPTY_CONVERSATION_SNAPSHOT } from "@deepseek-ai/dsh-client-ui-conversation/client";
|
|
12
|
+
import { EMPTY_CHAT_SNAPSHOT } from "@deepseek-ai/dsh-client-ui-chat/client";
|
|
13
|
+
import { RemoteError } from "@deepseek-ai/dsh-typert-protocol";
|
|
9
14
|
//#region lib/types/snapshot.js
|
|
10
15
|
/**
|
|
11
16
|
* DOM snapshot hygiene: a vitest snapshot serializer that keeps `.snap`
|
|
@@ -84,25 +89,17 @@ function registerDomSnapshotSerializer() {
|
|
|
84
89
|
//#endregion
|
|
85
90
|
//#region lib/types/fixtures.js
|
|
86
91
|
/**
|
|
87
|
-
* A complete quiescent
|
|
92
|
+
* A complete quiescent Session Controller snapshot.
|
|
88
93
|
* @param sessionId - owning session id.
|
|
89
94
|
* @returns the snapshot; spread fixture overrides on top.
|
|
90
95
|
*/
|
|
91
|
-
function
|
|
96
|
+
function sessionSnapshot(sessionId) {
|
|
92
97
|
return {
|
|
93
98
|
sessionId,
|
|
94
|
-
views: EMPTY_CONVERSATION_VIEWS,
|
|
95
|
-
chat: EMPTY_CHAT_SNAPSHOT,
|
|
96
|
-
nodes: [],
|
|
97
|
-
turnTimings: /* @__PURE__ */ new Map(),
|
|
98
|
-
turnEnds: /* @__PURE__ */ new Map(),
|
|
99
|
-
partial: null,
|
|
100
|
-
runningCalls: [],
|
|
101
|
-
pending: [],
|
|
102
99
|
queue: [],
|
|
100
|
+
pendingSubmissions: [],
|
|
103
101
|
running: false,
|
|
104
102
|
subagent: null,
|
|
105
|
-
composerPhase: "active",
|
|
106
103
|
removed: false,
|
|
107
104
|
openState: "open",
|
|
108
105
|
openError: null,
|
|
@@ -110,30 +107,51 @@ function conversationSnapshot(sessionId) {
|
|
|
110
107
|
loadingOlder: false,
|
|
111
108
|
promptError: null,
|
|
112
109
|
blank: false,
|
|
113
|
-
lastAgentError: null
|
|
110
|
+
lastAgentError: null,
|
|
111
|
+
promptAttempted: false,
|
|
112
|
+
awaitingFirstTurn: false
|
|
114
113
|
};
|
|
115
114
|
}
|
|
116
115
|
/**
|
|
117
|
-
* A
|
|
118
|
-
*
|
|
119
|
-
* @returns
|
|
116
|
+
* A target-neutral Conversation snapshot.
|
|
117
|
+
* @param overrides - target roster or activity overrides.
|
|
118
|
+
* @returns an immutable fixture value.
|
|
120
119
|
*/
|
|
121
|
-
function
|
|
120
|
+
function conversationSnapshot(overrides = {}) {
|
|
121
|
+
return {
|
|
122
|
+
...EMPTY_CONVERSATION_SNAPSHOT,
|
|
123
|
+
...overrides
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A Chat target snapshot.
|
|
128
|
+
* @param overrides - Chat target overrides.
|
|
129
|
+
* @returns an immutable fixture value.
|
|
130
|
+
*/
|
|
131
|
+
function chatSnapshot(overrides = {}) {
|
|
132
|
+
return {
|
|
133
|
+
...EMPTY_CHAT_SNAPSHOT,
|
|
134
|
+
...overrides
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* A ready Workspace Controller snapshot with no Workspace rows.
|
|
139
|
+
* @returns the initial state of the test Workspace source.
|
|
140
|
+
*/
|
|
141
|
+
function workspaceSnapshot() {
|
|
122
142
|
return {
|
|
123
143
|
items: [],
|
|
124
144
|
archivedSessionIds: [],
|
|
125
145
|
state: "idle",
|
|
126
146
|
phase: "ready",
|
|
127
|
-
error: null
|
|
128
|
-
baselinesReady: true,
|
|
129
|
-
recentWorkspaceId: void 0
|
|
147
|
+
error: null
|
|
130
148
|
};
|
|
131
149
|
}
|
|
132
150
|
//#endregion
|
|
133
151
|
//#region lib/types/sessions.js
|
|
134
152
|
/**
|
|
135
|
-
* The fixture-backed session face:
|
|
136
|
-
*
|
|
153
|
+
* The fixture-backed session face: lifecycle reads delegate to the fixture's
|
|
154
|
+
* snapshot store; Session verbs are fail-loud stubs unless the
|
|
137
155
|
* fixture supplies them (the runtime never fakes behavior a test did not
|
|
138
156
|
* declare — an unstubbed call names itself instead of half-working). Extra
|
|
139
157
|
* fixture methods are grafted verbatim for feature-side casts.
|
|
@@ -141,14 +159,15 @@ function workspaceListState() {
|
|
|
141
159
|
var FixtureSession = class {
|
|
142
160
|
sessionId;
|
|
143
161
|
store;
|
|
162
|
+
/** Mutable event source consumed only by Conversation assembly. */
|
|
163
|
+
eventSource = new MutableSessionEventSource();
|
|
144
164
|
/**
|
|
145
|
-
*
|
|
146
|
-
* projection values (set via {@link TestSessions.setProjection}).
|
|
165
|
+
* Identity-stable per-key faces over fixture-controlled projection values.
|
|
147
166
|
*/
|
|
148
167
|
projections;
|
|
149
168
|
/**
|
|
150
169
|
* @param sessionId - host identity (branded view of the fixture id).
|
|
151
|
-
* @param store -
|
|
170
|
+
* @param store - Session Controller snapshot store.
|
|
152
171
|
* @param overrides - fixture-declared behavior face, grafted over the stubs.
|
|
153
172
|
*/
|
|
154
173
|
constructor(sessionId, store, overrides) {
|
|
@@ -183,7 +202,7 @@ var FixtureSession = class {
|
|
|
183
202
|
};
|
|
184
203
|
Object.assign(this, overrides);
|
|
185
204
|
}
|
|
186
|
-
/** @returns the fixture
|
|
205
|
+
/** @returns the fixture Session Controller snapshot (useSession read side). */
|
|
187
206
|
getSnapshot() {
|
|
188
207
|
return this.store.getSnapshot();
|
|
189
208
|
}
|
|
@@ -203,6 +222,20 @@ var FixtureSession = class {
|
|
|
203
222
|
throw new Error(`test session "${this.sessionId}": prompt is not stubbed — supply it on the fixture's session face`);
|
|
204
223
|
}
|
|
205
224
|
/**
|
|
225
|
+
* Minimal local-echo registration: mints an identity without touching the
|
|
226
|
+
* fixture snapshot (submission echoes are client-only presentation state).
|
|
227
|
+
* Supply `beginSubmission` on the fixture's session face to observe echoes.
|
|
228
|
+
* @returns a handle whose abandon is a no-op.
|
|
229
|
+
*/
|
|
230
|
+
beginSubmission() {
|
|
231
|
+
this.submissionSeq += 1;
|
|
232
|
+
return {
|
|
233
|
+
requestId: `test-submission-${this.submissionSeq}`,
|
|
234
|
+
abandon: () => {}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
submissionSeq = 0;
|
|
238
|
+
/**
|
|
206
239
|
* Fail-loud stub; supply `readAttachment` on the fixture's session face to exercise it.
|
|
207
240
|
* @param _attachmentId - opaque durable attachment id.
|
|
208
241
|
* @returns never — always throws.
|
|
@@ -239,6 +272,13 @@ var FixtureSession = class {
|
|
|
239
272
|
throw new Error(`test session "${this.sessionId}": loadOlder is not stubbed — supply it on the fixture's session face`);
|
|
240
273
|
}
|
|
241
274
|
/**
|
|
275
|
+
* Fail-loud stub; supply `loadThrough` on the fixture's session face to exercise it.
|
|
276
|
+
* @returns never — always throws.
|
|
277
|
+
*/
|
|
278
|
+
loadThrough() {
|
|
279
|
+
throw new Error(`test session "${this.sessionId}": loadThrough is not stubbed — supply it on the fixture's session face`);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
242
282
|
* Fail-loud stub; supply `rename` on the fixture's session face to exercise it.
|
|
243
283
|
* @returns never — always throws.
|
|
244
284
|
*/
|
|
@@ -248,36 +288,28 @@ var FixtureSession = class {
|
|
|
248
288
|
};
|
|
249
289
|
/**
|
|
250
290
|
* Sessions test double behind the renderer host and feature injects: owns the
|
|
251
|
-
* list/current observable,
|
|
252
|
-
*
|
|
253
|
-
* `
|
|
291
|
+
* list/current observable, scope minting through the production `createScope`,
|
|
292
|
+
* stable Controller bindings, and the session behavior face supplied per
|
|
293
|
+
* fixture. `ui-session` owns standard-source materialization.
|
|
254
294
|
*
|
|
255
295
|
* Implements the same ISessions face features receive as `ctx.sessions`, so
|
|
256
296
|
* a production face change breaks this double at compile time; the extra
|
|
257
|
-
* members (add/
|
|
258
|
-
*
|
|
297
|
+
* members (add/updateSessionSnapshot/event-window drivers/setCurrent/remove/
|
|
298
|
+
* behavior/calls/stubs) are bench-only surface.
|
|
259
299
|
*/
|
|
260
300
|
var TestSessions = class {
|
|
261
301
|
stabilize;
|
|
262
302
|
rootCtx;
|
|
263
303
|
/** The useSessions standard feed (list rows + current selection). */
|
|
264
304
|
list;
|
|
265
|
-
/**
|
|
266
|
-
* Atomic current-session provide projection (production SessionRuntime
|
|
267
|
-
* mirror): selection changes and provider-roster changes publish through
|
|
268
|
-
* this one source — the member the SlotRegistry host face hands the
|
|
269
|
-
* renderer's SessionProvider.
|
|
270
|
-
*/
|
|
271
|
-
currentProvideInfo;
|
|
272
305
|
records = /* @__PURE__ */ new Map();
|
|
273
|
-
/** The production provide channel (roster, materialization rules, current projection) — no test-side mirror. */
|
|
274
|
-
channel;
|
|
275
306
|
/** Calls observed on the service-level face, newest last. */
|
|
276
307
|
calls = [];
|
|
277
308
|
/** The wire schema's `session.search` result bound (production parity). */
|
|
278
309
|
searchResultLimit = SESSION_SEARCH_RESULT_LIMIT;
|
|
279
310
|
/** Replaceable search behavior (see {@link TestSessions.stubSearch}). */
|
|
280
311
|
searchStub;
|
|
312
|
+
createStub;
|
|
281
313
|
/**
|
|
282
314
|
* @param stabilize - the owning runtime's act wrapper.
|
|
283
315
|
* @param rootCtx - the runtime's Cordis root; scope fibers mount under it.
|
|
@@ -294,16 +326,6 @@ var TestSessions = class {
|
|
|
294
326
|
jobsBySession: {},
|
|
295
327
|
currentAddress: void 0
|
|
296
328
|
});
|
|
297
|
-
this.channel = new SessionProvideChannel({
|
|
298
|
-
rebuildBundles: () => {
|
|
299
|
-
for (const record of this.records.values()) if (record.provideInfo !== void 0) record.provideInfo = this.channel.materializeInfo(this.bindingOf(record.session.sessionId, record));
|
|
300
|
-
},
|
|
301
|
-
resolveCurrent: () => this.maybeProvideInfo(this.list.getSnapshot().current)
|
|
302
|
-
});
|
|
303
|
-
this.currentProvideInfo = this.channel.currentProvideInfo;
|
|
304
|
-
this.list.subscribe(() => {
|
|
305
|
-
this.channel.publishCurrent();
|
|
306
|
-
});
|
|
307
329
|
}
|
|
308
330
|
/**
|
|
309
331
|
* Add a session from a fixture and (by default) make it current.
|
|
@@ -323,16 +345,18 @@ var TestSessions = class {
|
|
|
323
345
|
...fixture.summary
|
|
324
346
|
};
|
|
325
347
|
const snapshot = createSnapshotStore({
|
|
326
|
-
...
|
|
348
|
+
...sessionSnapshot(id),
|
|
327
349
|
...fixture.snapshot
|
|
328
350
|
});
|
|
351
|
+
const session = new FixtureSession(id, snapshot, fixture.session ?? {});
|
|
352
|
+
if (fixture.events !== void 0 || fixture.hasMore === true) session.eventSource.replace(fixture.events ?? [], fixture.hasMore ?? false);
|
|
329
353
|
this.records.set(id, {
|
|
330
354
|
summary,
|
|
331
355
|
snapshot,
|
|
332
|
-
session
|
|
356
|
+
session,
|
|
333
357
|
scope: void 0,
|
|
334
358
|
scopeFiber: void 0,
|
|
335
|
-
|
|
359
|
+
binding: void 0
|
|
336
360
|
});
|
|
337
361
|
await this.stabilize(() => {
|
|
338
362
|
this.list.update((draft) => {
|
|
@@ -344,18 +368,49 @@ var TestSessions = class {
|
|
|
344
368
|
return id;
|
|
345
369
|
}
|
|
346
370
|
/**
|
|
347
|
-
* Update
|
|
348
|
-
* live-stream stand-in: components subscribed via useSession re-render).
|
|
371
|
+
* Update Session Controller lifecycle state through an immer draft.
|
|
349
372
|
* @param id - session id.
|
|
350
373
|
* @param mutate - draft mutator.
|
|
351
374
|
*/
|
|
352
|
-
async
|
|
375
|
+
async updateSessionSnapshot(id, mutate) {
|
|
353
376
|
const record = this.require(id);
|
|
354
377
|
await this.stabilize(() => {
|
|
355
378
|
record.snapshot.update(mutate);
|
|
356
379
|
});
|
|
357
380
|
}
|
|
358
381
|
/**
|
|
382
|
+
* Replace a Session's complete contiguous event window.
|
|
383
|
+
* @param id - Session identity.
|
|
384
|
+
* @param entries - complete event window.
|
|
385
|
+
* @param hasMore - whether older history remains.
|
|
386
|
+
*/
|
|
387
|
+
async replaceEvents(id, entries, hasMore = false) {
|
|
388
|
+
await this.stabilize(() => {
|
|
389
|
+
this.require(id).session.eventSource.replace(entries, hasMore);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Prepend one older contiguous event page.
|
|
394
|
+
* @param id - Session identity.
|
|
395
|
+
* @param entries - older entries.
|
|
396
|
+
* @param hasMore - whether another older page remains.
|
|
397
|
+
*/
|
|
398
|
+
async prependEvents(id, entries, hasMore = false) {
|
|
399
|
+
await this.stabilize(() => {
|
|
400
|
+
this.require(id).session.eventSource.prepend(entries, hasMore);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Append one live event to a Session's contiguous window.
|
|
405
|
+
* @param id - Session identity.
|
|
406
|
+
* @param entry - live event entry.
|
|
407
|
+
*/
|
|
408
|
+
async appendEvent(id, entry) {
|
|
409
|
+
await this.stabilize(() => {
|
|
410
|
+
this.require(id).session.eventSource.append(entry);
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
359
414
|
* Update a session's list row (the wire-echo stand-in: title settles,
|
|
360
415
|
* running flips — components subscribed via useSessions re-render).
|
|
361
416
|
* @param id - session id.
|
|
@@ -388,7 +443,7 @@ var TestSessions = class {
|
|
|
388
443
|
/**
|
|
389
444
|
* Remove a session: list row, scope fiber, and per-session store instances
|
|
390
445
|
* (with persisted state) die together — the same single lifecycle axis the
|
|
391
|
-
* production
|
|
446
|
+
* production Client Sessions service drives on session death, minus staging.
|
|
392
447
|
* @param id - session id.
|
|
393
448
|
*/
|
|
394
449
|
async remove(id) {
|
|
@@ -402,40 +457,9 @@ var TestSessions = class {
|
|
|
402
457
|
if (draft.current === id) draft.current = void 0;
|
|
403
458
|
});
|
|
404
459
|
if (record.scopeFiber !== void 0) await record.scopeFiber.dispose();
|
|
405
|
-
this.rootCtx.get("slots")?.pruneStoreScope(id);
|
|
406
460
|
});
|
|
407
461
|
}
|
|
408
462
|
/**
|
|
409
|
-
* Register a per-session standard-props provider (production `provide`
|
|
410
|
-
* contract: hooks become `use<Name>` selector hooks on the render side,
|
|
411
|
-
* props spread verbatim; duplicate names fail loud at materialization).
|
|
412
|
-
* @param descriptor - static member roster plus per-session resolver.
|
|
413
|
-
* @returns disposer removing the provider.
|
|
414
|
-
*/
|
|
415
|
-
provide(descriptor) {
|
|
416
|
-
return this.channel.provide(descriptor);
|
|
417
|
-
}
|
|
418
|
-
/**
|
|
419
|
-
* Resolve the definite per-session standard-props bundle (host face member).
|
|
420
|
-
* @param id - session id.
|
|
421
|
-
* @returns the identity-stable bundle, or undefined for unknown sessions.
|
|
422
|
-
*/
|
|
423
|
-
provideInfo(id) {
|
|
424
|
-
const record = this.records.get(id);
|
|
425
|
-
if (record === void 0) return void 0;
|
|
426
|
-
record.provideInfo ??= this.channel.materializeInfo(this.bindingOf(id, record));
|
|
427
|
-
return record.provideInfo;
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Resolve the current-session-optional standard kit (host face member):
|
|
431
|
-
* unknown or absent ids return the static no-session projection.
|
|
432
|
-
* @param id - current session id, when selected.
|
|
433
|
-
* @returns a definite or no-session provide bundle.
|
|
434
|
-
*/
|
|
435
|
-
maybeProvideInfo(id) {
|
|
436
|
-
return (id === void 0 ? void 0 : this.provideInfo(id)) ?? this.channel.maybeInfo;
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
463
|
* Resolve (mint on first touch) the session-scoped Cordis context through
|
|
440
464
|
* the production `createScope`, so real `scopeOf`/scope-addressed services
|
|
441
465
|
* resolve it.
|
|
@@ -460,7 +484,8 @@ var TestSessions = class {
|
|
|
460
484
|
binding(id) {
|
|
461
485
|
const record = this.records.get(id);
|
|
462
486
|
if (record === void 0) return void 0;
|
|
463
|
-
|
|
487
|
+
record.binding ??= this.bindingOf(id, record);
|
|
488
|
+
return record.binding;
|
|
464
489
|
}
|
|
465
490
|
/**
|
|
466
491
|
* Read the session scope tag off a context (service-method boundary mirror).
|
|
@@ -482,6 +507,24 @@ var TestSessions = class {
|
|
|
482
507
|
return this.records.get(id)?.session;
|
|
483
508
|
}
|
|
484
509
|
/**
|
|
510
|
+
* Install Session creation behavior for navigation tests.
|
|
511
|
+
* @param impl - implementation that must return an already-added fixture id.
|
|
512
|
+
*/
|
|
513
|
+
stubCreate(impl) {
|
|
514
|
+
this.createStub = impl;
|
|
515
|
+
}
|
|
516
|
+
/** Create through the installed test behavior and require an addressable binding. */
|
|
517
|
+
async create(opts) {
|
|
518
|
+
this.calls.push({
|
|
519
|
+
method: "create",
|
|
520
|
+
args: [opts]
|
|
521
|
+
});
|
|
522
|
+
if (this.createStub === void 0) throw new Error("test sessions: create is not stubbed — call stubCreate() first");
|
|
523
|
+
const id = await this.createStub(opts);
|
|
524
|
+
this.require(id);
|
|
525
|
+
return id;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
485
528
|
* Service-level selection call (recorded, then applied to the list store
|
|
486
529
|
* synchronously — inject callbacks call this outside any act window; the
|
|
487
530
|
* store notify is microtask-batched so the next stabilized step observes it).
|
|
@@ -530,16 +573,6 @@ var TestSessions = class {
|
|
|
530
573
|
});
|
|
531
574
|
return Promise.resolve();
|
|
532
575
|
}
|
|
533
|
-
/** Apply a confirmed preset switch into the fixture list, as production does. */
|
|
534
|
-
noteAgentPreset(sessionId, agentPreset) {
|
|
535
|
-
this.list.update((draft) => {
|
|
536
|
-
const summary = draft.byId[sessionId];
|
|
537
|
-
if (summary !== void 0) draft.byId[sessionId] = {
|
|
538
|
-
...summary,
|
|
539
|
-
agentPreset
|
|
540
|
-
};
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
576
|
/** Clear the current selection (recorded; the production no-session flow). */
|
|
544
577
|
clear() {
|
|
545
578
|
this.calls.push({
|
|
@@ -551,6 +584,14 @@ var TestSessions = class {
|
|
|
551
584
|
draft.currentAddress = void 0;
|
|
552
585
|
});
|
|
553
586
|
}
|
|
587
|
+
/** Record a list refresh; fixture callers publish list state explicitly. */
|
|
588
|
+
refresh() {
|
|
589
|
+
this.calls.push({
|
|
590
|
+
method: "refresh",
|
|
591
|
+
args: []
|
|
592
|
+
});
|
|
593
|
+
return Promise.resolve();
|
|
594
|
+
}
|
|
554
595
|
/**
|
|
555
596
|
* Replace the sidebar-search result page (the call is still recorded).
|
|
556
597
|
* @param impl - hits for a query, as the Host would rank them.
|
|
@@ -596,7 +637,7 @@ var TestSessions = class {
|
|
|
596
637
|
* The session face of a fixture (typed view for assertions; fixture
|
|
597
638
|
* behavior methods are grafted onto it).
|
|
598
639
|
* @param id - session id.
|
|
599
|
-
* @returns the FixtureSession
|
|
640
|
+
* @returns the FixtureSession carried by the Controller binding.
|
|
600
641
|
*/
|
|
601
642
|
behavior(id) {
|
|
602
643
|
return this.require(id).session;
|
|
@@ -607,6 +648,7 @@ var TestSessions = class {
|
|
|
607
648
|
await record.scopeFiber.dispose();
|
|
608
649
|
record.scope = void 0;
|
|
609
650
|
record.scopeFiber = void 0;
|
|
651
|
+
record.binding = void 0;
|
|
610
652
|
}
|
|
611
653
|
}
|
|
612
654
|
bindingOf(id, record) {
|
|
@@ -617,6 +659,7 @@ var TestSessions = class {
|
|
|
617
659
|
return {
|
|
618
660
|
sessionId: id,
|
|
619
661
|
session: record.session,
|
|
662
|
+
eventSource: record.session.eventSource,
|
|
620
663
|
ctx
|
|
621
664
|
};
|
|
622
665
|
}
|
|
@@ -649,7 +692,7 @@ var TestWorkspaces = class {
|
|
|
649
692
|
*/
|
|
650
693
|
constructor(stabilize) {
|
|
651
694
|
this.stabilize = stabilize;
|
|
652
|
-
this.list = createSnapshotStore(
|
|
695
|
+
this.list = createSnapshotStore({ ...workspaceSnapshot() });
|
|
653
696
|
}
|
|
654
697
|
/**
|
|
655
698
|
* Update the workspace list state through an immer draft.
|
|
@@ -662,40 +705,13 @@ var TestWorkspaces = class {
|
|
|
662
705
|
}
|
|
663
706
|
/**
|
|
664
707
|
* Replace an action's behavior (the recorded call is still appended first).
|
|
665
|
-
* @param method - action name (e.g. '
|
|
708
|
+
* @param method - Controller action name (e.g. 'create').
|
|
666
709
|
* @param impl - replacement behavior.
|
|
667
710
|
*/
|
|
668
711
|
stub(method, impl) {
|
|
669
712
|
this.stubs.set(method, impl);
|
|
670
713
|
}
|
|
671
714
|
/**
|
|
672
|
-
* Connect a workspace to its reusable/new blank session (recorded). The
|
|
673
|
-
* default resolves the workspace id back as the session id; stub for
|
|
674
|
-
* cross-session flows.
|
|
675
|
-
* @param workspaceId - target workspace.
|
|
676
|
-
* @returns the connected session id.
|
|
677
|
-
*/
|
|
678
|
-
async connectWorkspace(workspaceId) {
|
|
679
|
-
this.calls.push({
|
|
680
|
-
method: "connectWorkspace",
|
|
681
|
-
args: [workspaceId]
|
|
682
|
-
});
|
|
683
|
-
const stub = this.stubs.get("connectWorkspace");
|
|
684
|
-
if (stub !== void 0) return await stub(workspaceId);
|
|
685
|
-
return `session-of-${workspaceId}`;
|
|
686
|
-
}
|
|
687
|
-
/**
|
|
688
|
-
* New-session flow (recorded; stubbed behavior runs when installed).
|
|
689
|
-
* @param workspaceId - optional explicit workspace target.
|
|
690
|
-
*/
|
|
691
|
-
startSession(workspaceId) {
|
|
692
|
-
this.calls.push({
|
|
693
|
-
method: "startSession",
|
|
694
|
-
args: [workspaceId]
|
|
695
|
-
});
|
|
696
|
-
this.stubs.get("startSession")?.(workspaceId);
|
|
697
|
-
}
|
|
698
|
-
/**
|
|
699
715
|
* Create a Workspace (recorded). The default echoes a view derived from
|
|
700
716
|
* the input; stub for failure or list-coupled flows.
|
|
701
717
|
* @param input - the Host create payload.
|
|
@@ -716,82 +732,6 @@ var TestWorkspaces = class {
|
|
|
716
732
|
};
|
|
717
733
|
}
|
|
718
734
|
/**
|
|
719
|
-
* Open a path with the host OS default application (recorded; default no-op).
|
|
720
|
-
* @param path - host-resolvable path.
|
|
721
|
-
*/
|
|
722
|
-
async openPath(path) {
|
|
723
|
-
this.calls.push({
|
|
724
|
-
method: "openPath",
|
|
725
|
-
args: [path]
|
|
726
|
-
});
|
|
727
|
-
await this.stubs.get("openPath")?.(path);
|
|
728
|
-
}
|
|
729
|
-
/**
|
|
730
|
-
* Directory picker (recorded). The default cancels (null); stub to select.
|
|
731
|
-
* @returns the picked path, or null.
|
|
732
|
-
*/
|
|
733
|
-
async pickDirectory() {
|
|
734
|
-
this.calls.push({
|
|
735
|
-
method: "pickDirectory",
|
|
736
|
-
args: []
|
|
737
|
-
});
|
|
738
|
-
const stub = this.stubs.get("pickDirectory");
|
|
739
|
-
if (stub !== void 0) return await stub();
|
|
740
|
-
return null;
|
|
741
|
-
}
|
|
742
|
-
/**
|
|
743
|
-
* Browse listing (recorded). The default serves an empty home level; stub
|
|
744
|
-
* to shape a tree.
|
|
745
|
-
* @param path - absolute directory to list; absent lists the home level.
|
|
746
|
-
* @returns the level's listing.
|
|
747
|
-
*/
|
|
748
|
-
async listDirectory(path, signal) {
|
|
749
|
-
this.calls.push({
|
|
750
|
-
method: "listDirectory",
|
|
751
|
-
args: [path, signal]
|
|
752
|
-
});
|
|
753
|
-
const stub = this.stubs.get("listDirectory");
|
|
754
|
-
if (stub !== void 0) return await stub(path, signal);
|
|
755
|
-
return {
|
|
756
|
-
path: "/home/test",
|
|
757
|
-
home: "/home/test",
|
|
758
|
-
crumbs: [
|
|
759
|
-
{
|
|
760
|
-
name: "/",
|
|
761
|
-
path: "/",
|
|
762
|
-
hidden: false
|
|
763
|
-
},
|
|
764
|
-
{
|
|
765
|
-
name: "home",
|
|
766
|
-
path: "/home",
|
|
767
|
-
hidden: false
|
|
768
|
-
},
|
|
769
|
-
{
|
|
770
|
-
name: "test",
|
|
771
|
-
path: "/home/test",
|
|
772
|
-
hidden: false
|
|
773
|
-
}
|
|
774
|
-
],
|
|
775
|
-
entries: [],
|
|
776
|
-
truncated: false
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
/**
|
|
780
|
-
* Browse child creation (recorded). The default joins parent and name.
|
|
781
|
-
* @param path - absolute existing parent directory.
|
|
782
|
-
* @param name - single path segment.
|
|
783
|
-
* @returns the created directory's absolute path.
|
|
784
|
-
*/
|
|
785
|
-
async createDirectory(path, name) {
|
|
786
|
-
this.calls.push({
|
|
787
|
-
method: "createDirectory",
|
|
788
|
-
args: [path, name]
|
|
789
|
-
});
|
|
790
|
-
const stub = this.stubs.get("createDirectory");
|
|
791
|
-
if (stub !== void 0) return await stub(path, name);
|
|
792
|
-
return `${path}/${name}`;
|
|
793
|
-
}
|
|
794
|
-
/**
|
|
795
735
|
* Rename a Workspace (recorded). The default echoes a minimal view.
|
|
796
736
|
* @param workspaceId - target workspace.
|
|
797
737
|
* @param title - new title.
|
|
@@ -899,6 +839,7 @@ function stubSettingsScope() {
|
|
|
899
839
|
};
|
|
900
840
|
const listeners = /* @__PURE__ */ new Set();
|
|
901
841
|
const set = vi.fn(() => Promise.resolve());
|
|
842
|
+
const mutate = vi.fn(() => Promise.resolve());
|
|
902
843
|
const unset = vi.fn(() => Promise.resolve());
|
|
903
844
|
return {
|
|
904
845
|
scope: {
|
|
@@ -909,10 +850,12 @@ function stubSettingsScope() {
|
|
|
909
850
|
listeners.delete(listener);
|
|
910
851
|
};
|
|
911
852
|
},
|
|
853
|
+
mutate,
|
|
912
854
|
set,
|
|
913
855
|
unset
|
|
914
856
|
},
|
|
915
857
|
set,
|
|
858
|
+
mutate,
|
|
916
859
|
unset,
|
|
917
860
|
listenerCount: () => listeners.size,
|
|
918
861
|
publish: (next) => {
|
|
@@ -925,34 +868,100 @@ function stubSettingsScope() {
|
|
|
925
868
|
};
|
|
926
869
|
}
|
|
927
870
|
//#endregion
|
|
871
|
+
//#region lib/types/settings-remote.js
|
|
872
|
+
/** Test double for the `settings` Remote namespace a bench's plugins inject. */
|
|
873
|
+
/**
|
|
874
|
+
* Build a scripted `settings` Remote namespace for a bench. Each write answers
|
|
875
|
+
* with the addressed namespace unchanged, so a bench that only needs its
|
|
876
|
+
* plugins to activate scripts nothing; one asserting a write reads the
|
|
877
|
+
* corresponding spy or replaces the face.
|
|
878
|
+
* @param namespaces - namespace views the first describe answers with.
|
|
879
|
+
* @param options - deployment facts the describe answer reports.
|
|
880
|
+
* @returns the face and its controls.
|
|
881
|
+
*/
|
|
882
|
+
function scriptedSettingsRemote(namespaces = [], options = {}) {
|
|
883
|
+
let served = namespaces;
|
|
884
|
+
const writable = options.writable ?? true;
|
|
885
|
+
const hasDocument = options.hasDocument ?? false;
|
|
886
|
+
const answer = (ns) => {
|
|
887
|
+
const view = served.find((candidate) => candidate.ns === ns);
|
|
888
|
+
return Promise.resolve(view === void 0 ? {
|
|
889
|
+
ok: false,
|
|
890
|
+
error: {
|
|
891
|
+
code: "settings/rejected",
|
|
892
|
+
message: `no scripted namespace "${ns}"`,
|
|
893
|
+
details: { ns }
|
|
894
|
+
}
|
|
895
|
+
} : {
|
|
896
|
+
ok: true,
|
|
897
|
+
value: view
|
|
898
|
+
});
|
|
899
|
+
};
|
|
900
|
+
const update = vi.fn((ns, _patch, _expectedRevision) => answer(ns));
|
|
901
|
+
const replace = vi.fn((ns, _section, _expectedRevision) => answer(ns));
|
|
902
|
+
const mutate = vi.fn((ns, _ops, _expectedRevision) => answer(ns));
|
|
903
|
+
return {
|
|
904
|
+
settings: {
|
|
905
|
+
describe: () => Promise.resolve({
|
|
906
|
+
ok: true,
|
|
907
|
+
value: {
|
|
908
|
+
writable,
|
|
909
|
+
hasDocument,
|
|
910
|
+
namespaces: served
|
|
911
|
+
}
|
|
912
|
+
}),
|
|
913
|
+
update: (ns, patch, expectedRevision) => update(ns, patch, expectedRevision),
|
|
914
|
+
replace: (ns, section, expectedRevision) => replace(ns, section, expectedRevision),
|
|
915
|
+
mutate: (ns, ops, expectedRevision) => mutate(ns, ops, expectedRevision)
|
|
916
|
+
},
|
|
917
|
+
update,
|
|
918
|
+
replace,
|
|
919
|
+
mutate,
|
|
920
|
+
publish(next) {
|
|
921
|
+
served = next;
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
//#endregion
|
|
928
926
|
//#region lib/types/remote.js
|
|
929
927
|
/**
|
|
930
928
|
* Remote service test double for the forwarded-event path. Feature specs need
|
|
931
929
|
* `ctx.remote.$on` to exist (their plugins inject `remote`) and need forwarded
|
|
932
|
-
*
|
|
933
|
-
*
|
|
930
|
+
* Host events to reach those subscribers, but not the wire — so this double
|
|
931
|
+
* implements subscription plus an explicit `emit` driver available only on the
|
|
932
|
+
* concrete test object. A spec that also calls one namespace scripts it through
|
|
933
|
+
* the constructor rather than reaching the real Client Remote service.
|
|
934
934
|
*
|
|
935
|
-
*
|
|
936
|
-
*
|
|
937
|
-
*
|
|
938
|
-
* `$dispatch(name, args)` on this double.
|
|
939
|
-
*
|
|
940
|
-
* `$mount` rejects: a spec that reaches a generated namespace through this
|
|
941
|
-
* double has outgrown it and needs the real Client Remote service.
|
|
935
|
+
* `$mount` rejects: a spec that needs a real generated contribution installed —
|
|
936
|
+
* codecs, descriptors, and the wire — has outgrown this double and needs the
|
|
937
|
+
* real Client Remote service.
|
|
942
938
|
*
|
|
943
939
|
* One deliberate asymmetry with production: a throwing listener propagates out
|
|
944
940
|
* of the emit instead of being contained and logged, so a spec cannot lean on
|
|
945
941
|
* this double for the containment guarantee `$on` documents — assert that
|
|
946
942
|
* against the real service.
|
|
947
943
|
*/
|
|
948
|
-
var TestRemote = class {
|
|
944
|
+
var TestRemote = class TestRemote {
|
|
949
945
|
subscriptions = /* @__PURE__ */ new Map();
|
|
950
946
|
/**
|
|
951
|
-
*
|
|
947
|
+
* Fixed Host facts mirrored from the production `ctx.remote.$host`. Plain
|
|
948
|
+
* mutable field: a spec assigns it to script a non-loopback or homed Host.
|
|
949
|
+
*/
|
|
950
|
+
$host = {
|
|
951
|
+
home: void 0,
|
|
952
|
+
isLoopback: true
|
|
953
|
+
};
|
|
954
|
+
/**
|
|
955
|
+
* Register the double as `ctx.remote`, plus one service per scripted
|
|
956
|
+
* namespace so a plugin injecting `remote.<name>` also unparks.
|
|
952
957
|
* @param ctx - the spec's root Context.
|
|
958
|
+
* @param namespaces - scripted namespace faces reached as `ctx.remote.<name>`.
|
|
953
959
|
*/
|
|
954
|
-
constructor(ctx) {
|
|
960
|
+
constructor(ctx, namespaces = {}) {
|
|
961
|
+
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`);
|
|
962
|
+
Object.assign(this, namespaces);
|
|
955
963
|
ctx.provide("remote", this);
|
|
964
|
+
for (const [name, face] of Object.entries(namespaces)) ctx.provide(`remote.${name}`, face);
|
|
956
965
|
}
|
|
957
966
|
/**
|
|
958
967
|
* Deliver one forwarded host event to its subscribers, standing in for the
|
|
@@ -960,7 +969,7 @@ var TestRemote = class {
|
|
|
960
969
|
* @param event - forwarded host event name.
|
|
961
970
|
* @param args - the Host argument list, verbatim.
|
|
962
971
|
*/
|
|
963
|
-
|
|
972
|
+
emit(event, args) {
|
|
964
973
|
const listeners = this.subscriptions.get(event);
|
|
965
974
|
if (listeners === void 0) return;
|
|
966
975
|
for (const listener of [...listeners]) listener(...args);
|
|
@@ -1054,7 +1063,7 @@ function usePinnedBrowserLanguages(primary, ...rest) {
|
|
|
1054
1063
|
//#region lib/types/index.js
|
|
1055
1064
|
/**
|
|
1056
1065
|
* jsdom slot test runtime: a real small runtime — Cordis `Context`, the
|
|
1057
|
-
*
|
|
1066
|
+
* renderer-owned `SlotRegistry`, the `ui-session` adapter, and the UI renderer — assembled around
|
|
1058
1067
|
* test-owned session/workspace doubles, so feature specs exercise
|
|
1059
1068
|
* declaration, registration, scope, store, inject, rendering, updates, and
|
|
1060
1069
|
* disposal without hand-building the machinery per suite.
|
|
@@ -1163,7 +1172,7 @@ var TestRoot = class {
|
|
|
1163
1172
|
* batching or React act themselves.
|
|
1164
1173
|
*/
|
|
1165
1174
|
var SlotTestRuntime = class SlotTestRuntime {
|
|
1166
|
-
/** The runtime's Cordis root
|
|
1175
|
+
/** The runtime's Cordis root for owner APIs and explicit test-only services. */
|
|
1167
1176
|
ctx;
|
|
1168
1177
|
/** The production SlotRegistry mounted on {@link SlotTestRuntime.ctx}. */
|
|
1169
1178
|
slots;
|
|
@@ -1186,6 +1195,7 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1186
1195
|
ownerCell = new OwnerPropsCell();
|
|
1187
1196
|
autoDeclared = /* @__PURE__ */ new Set();
|
|
1188
1197
|
autoRootView;
|
|
1198
|
+
disposeWorkspaceSource;
|
|
1189
1199
|
constructor(ctx, slots) {
|
|
1190
1200
|
this.ctx = ctx;
|
|
1191
1201
|
this.slots = slots;
|
|
@@ -1194,6 +1204,7 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1194
1204
|
this.workspaces = new TestWorkspaces(this.stabilizer);
|
|
1195
1205
|
ctx.provide("sessions", this.sessions);
|
|
1196
1206
|
ctx.provide("workspaces", this.workspaces);
|
|
1207
|
+
this.disposeWorkspaceSource = slots.provideRoot({ hooks: { workspaces: this.workspaces.list } });
|
|
1197
1208
|
const renderer = createSlotRenderer();
|
|
1198
1209
|
slots.install({ renderRoot: (host, ownerProps) => {
|
|
1199
1210
|
this.host = host;
|
|
@@ -1209,22 +1220,12 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1209
1220
|
registerDomSnapshotSerializer();
|
|
1210
1221
|
const ctx = new Context();
|
|
1211
1222
|
await ctx.plugin(SlotRegistry).await();
|
|
1212
|
-
|
|
1213
|
-
await ctx.plugin(
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
* fake). Sugar over `ctx.provide`, typed against the Context declaration
|
|
1219
|
-
* merge: for a declared service name the fake must be a subset of that
|
|
1220
|
-
* service's outward face (Partial — supply only what the feature calls),
|
|
1221
|
-
* so a production face change breaks the fake at compile time. Undeclared
|
|
1222
|
-
* names stay unchecked (ad-hoc test services).
|
|
1223
|
-
* @param name - service name.
|
|
1224
|
-
* @param value - service implementation (test double).
|
|
1225
|
-
*/
|
|
1226
|
-
provide(name, value) {
|
|
1227
|
-
this.ctx.provide(name, value);
|
|
1223
|
+
const runtime = new SlotTestRuntime(ctx, ctx.get("slots"));
|
|
1224
|
+
await ctx.plugin({
|
|
1225
|
+
inject: [...inject],
|
|
1226
|
+
apply
|
|
1227
|
+
}).await();
|
|
1228
|
+
return runtime;
|
|
1228
1229
|
}
|
|
1229
1230
|
/**
|
|
1230
1231
|
* Mount a feature plugin on a real fiber. Required services are prechecked
|
|
@@ -1252,6 +1253,10 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1252
1253
|
this.handles.push(handle);
|
|
1253
1254
|
return handle;
|
|
1254
1255
|
}
|
|
1256
|
+
/** Release the default Workspace hook before mounting its production owner. */
|
|
1257
|
+
releaseWorkspaceSource() {
|
|
1258
|
+
this.disposeWorkspaceSource();
|
|
1259
|
+
}
|
|
1255
1260
|
/**
|
|
1256
1261
|
* Render the root slot tree through the ctx-level entry (the shell's own
|
|
1257
1262
|
* entry point): `ctx.slots.renderSlot('root', {})` under Testing Library.
|
|
@@ -1321,7 +1326,9 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1321
1326
|
if (this.host === void 0) throw new Error("storeOf before renderRoot() — the host face exists only inside the installed renderer");
|
|
1322
1327
|
const entry = this.host.entriesOf(key)[0];
|
|
1323
1328
|
if (entry === void 0) throw new Error(`storeOf('${key}'): no registration on the ledger`);
|
|
1324
|
-
const
|
|
1329
|
+
const scopeBinding = scopeKey === void 0 ? void 0 : this.host.scope("session")?.resolve(scopeKey);
|
|
1330
|
+
if (scopeKey !== void 0 && scopeBinding === void 0) throw new Error(`storeOf('${key}'): no live Session binding for '${scopeKey}'`);
|
|
1331
|
+
const instance = this.host.storeOf(entry, scopeBinding);
|
|
1325
1332
|
if (instance === void 0) throw new Error(`storeOf('${key}'): the entry declares no store`);
|
|
1326
1333
|
return instance;
|
|
1327
1334
|
}
|
|
@@ -1351,4 +1358,4 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1351
1358
|
}
|
|
1352
1359
|
};
|
|
1353
1360
|
//#endregion
|
|
1354
|
-
export { FixtureSession, SlotTestRuntime, TestRemote, TestRoot, TestSessions, TestWorkspaces, bindSnapshotSelector, conversationSnapshot, createSlotRenderer, domSnapshotSerializer, makeTranslate, registerDomSnapshotSerializer, stubSettingsScope, usePinnedBrowserLanguages,
|
|
1361
|
+
export { FixtureSession, RemoteError, SlotTestRuntime, TestRemote, TestRoot, TestSessions, TestWorkspaces, bindSnapshotSelector, chatSnapshot, conversationSnapshot, createSlotRenderer, domSnapshotSerializer, makeTranslate, registerDomSnapshotSerializer, scriptedSettingsRemote, sessionSnapshot, stubSettingsScope, usePinnedBrowserLanguages, workspaceSnapshot };
|