@deepseek-ai/dsh-client-test-runtime 0.1.1-rc.1 → 0.1.2-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 +132 -6
- package/README.zh.md +133 -7
- package/lib/index.js +239 -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 +65 -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.
|
|
@@ -248,36 +281,28 @@ var FixtureSession = class {
|
|
|
248
281
|
};
|
|
249
282
|
/**
|
|
250
283
|
* Sessions test double behind the renderer host and feature injects: owns the
|
|
251
|
-
* list/current observable,
|
|
252
|
-
*
|
|
253
|
-
* `
|
|
284
|
+
* list/current observable, scope minting through the production `createScope`,
|
|
285
|
+
* stable Controller bindings, and the session behavior face supplied per
|
|
286
|
+
* fixture. `ui-session` owns standard-source materialization.
|
|
254
287
|
*
|
|
255
288
|
* Implements the same ISessions face features receive as `ctx.sessions`, so
|
|
256
289
|
* a production face change breaks this double at compile time; the extra
|
|
257
|
-
* members (add/
|
|
258
|
-
*
|
|
290
|
+
* members (add/updateSessionSnapshot/event-window drivers/setCurrent/remove/
|
|
291
|
+
* behavior/calls/stubs) are bench-only surface.
|
|
259
292
|
*/
|
|
260
293
|
var TestSessions = class {
|
|
261
294
|
stabilize;
|
|
262
295
|
rootCtx;
|
|
263
296
|
/** The useSessions standard feed (list rows + current selection). */
|
|
264
297
|
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
298
|
records = /* @__PURE__ */ new Map();
|
|
273
|
-
/** The production provide channel (roster, materialization rules, current projection) — no test-side mirror. */
|
|
274
|
-
channel;
|
|
275
299
|
/** Calls observed on the service-level face, newest last. */
|
|
276
300
|
calls = [];
|
|
277
301
|
/** The wire schema's `session.search` result bound (production parity). */
|
|
278
302
|
searchResultLimit = SESSION_SEARCH_RESULT_LIMIT;
|
|
279
303
|
/** Replaceable search behavior (see {@link TestSessions.stubSearch}). */
|
|
280
304
|
searchStub;
|
|
305
|
+
createStub;
|
|
281
306
|
/**
|
|
282
307
|
* @param stabilize - the owning runtime's act wrapper.
|
|
283
308
|
* @param rootCtx - the runtime's Cordis root; scope fibers mount under it.
|
|
@@ -294,16 +319,6 @@ var TestSessions = class {
|
|
|
294
319
|
jobsBySession: {},
|
|
295
320
|
currentAddress: void 0
|
|
296
321
|
});
|
|
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
322
|
}
|
|
308
323
|
/**
|
|
309
324
|
* Add a session from a fixture and (by default) make it current.
|
|
@@ -323,16 +338,18 @@ var TestSessions = class {
|
|
|
323
338
|
...fixture.summary
|
|
324
339
|
};
|
|
325
340
|
const snapshot = createSnapshotStore({
|
|
326
|
-
...
|
|
341
|
+
...sessionSnapshot(id),
|
|
327
342
|
...fixture.snapshot
|
|
328
343
|
});
|
|
344
|
+
const session = new FixtureSession(id, snapshot, fixture.session ?? {});
|
|
345
|
+
if (fixture.events !== void 0 || fixture.hasMore === true) session.eventSource.replace(fixture.events ?? [], fixture.hasMore ?? false);
|
|
329
346
|
this.records.set(id, {
|
|
330
347
|
summary,
|
|
331
348
|
snapshot,
|
|
332
|
-
session
|
|
349
|
+
session,
|
|
333
350
|
scope: void 0,
|
|
334
351
|
scopeFiber: void 0,
|
|
335
|
-
|
|
352
|
+
binding: void 0
|
|
336
353
|
});
|
|
337
354
|
await this.stabilize(() => {
|
|
338
355
|
this.list.update((draft) => {
|
|
@@ -344,18 +361,49 @@ var TestSessions = class {
|
|
|
344
361
|
return id;
|
|
345
362
|
}
|
|
346
363
|
/**
|
|
347
|
-
* Update
|
|
348
|
-
* live-stream stand-in: components subscribed via useSession re-render).
|
|
364
|
+
* Update Session Controller lifecycle state through an immer draft.
|
|
349
365
|
* @param id - session id.
|
|
350
366
|
* @param mutate - draft mutator.
|
|
351
367
|
*/
|
|
352
|
-
async
|
|
368
|
+
async updateSessionSnapshot(id, mutate) {
|
|
353
369
|
const record = this.require(id);
|
|
354
370
|
await this.stabilize(() => {
|
|
355
371
|
record.snapshot.update(mutate);
|
|
356
372
|
});
|
|
357
373
|
}
|
|
358
374
|
/**
|
|
375
|
+
* Replace a Session's complete contiguous event window.
|
|
376
|
+
* @param id - Session identity.
|
|
377
|
+
* @param entries - complete event window.
|
|
378
|
+
* @param hasMore - whether older history remains.
|
|
379
|
+
*/
|
|
380
|
+
async replaceEvents(id, entries, hasMore = false) {
|
|
381
|
+
await this.stabilize(() => {
|
|
382
|
+
this.require(id).session.eventSource.replace(entries, hasMore);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Prepend one older contiguous event page.
|
|
387
|
+
* @param id - Session identity.
|
|
388
|
+
* @param entries - older entries.
|
|
389
|
+
* @param hasMore - whether another older page remains.
|
|
390
|
+
*/
|
|
391
|
+
async prependEvents(id, entries, hasMore = false) {
|
|
392
|
+
await this.stabilize(() => {
|
|
393
|
+
this.require(id).session.eventSource.prepend(entries, hasMore);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Append one live event to a Session's contiguous window.
|
|
398
|
+
* @param id - Session identity.
|
|
399
|
+
* @param entry - live event entry.
|
|
400
|
+
*/
|
|
401
|
+
async appendEvent(id, entry) {
|
|
402
|
+
await this.stabilize(() => {
|
|
403
|
+
this.require(id).session.eventSource.append(entry);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
359
407
|
* Update a session's list row (the wire-echo stand-in: title settles,
|
|
360
408
|
* running flips — components subscribed via useSessions re-render).
|
|
361
409
|
* @param id - session id.
|
|
@@ -388,7 +436,7 @@ var TestSessions = class {
|
|
|
388
436
|
/**
|
|
389
437
|
* Remove a session: list row, scope fiber, and per-session store instances
|
|
390
438
|
* (with persisted state) die together — the same single lifecycle axis the
|
|
391
|
-
* production
|
|
439
|
+
* production Client Sessions service drives on session death, minus staging.
|
|
392
440
|
* @param id - session id.
|
|
393
441
|
*/
|
|
394
442
|
async remove(id) {
|
|
@@ -402,40 +450,9 @@ var TestSessions = class {
|
|
|
402
450
|
if (draft.current === id) draft.current = void 0;
|
|
403
451
|
});
|
|
404
452
|
if (record.scopeFiber !== void 0) await record.scopeFiber.dispose();
|
|
405
|
-
this.rootCtx.get("slots")?.pruneStoreScope(id);
|
|
406
453
|
});
|
|
407
454
|
}
|
|
408
455
|
/**
|
|
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
456
|
* Resolve (mint on first touch) the session-scoped Cordis context through
|
|
440
457
|
* the production `createScope`, so real `scopeOf`/scope-addressed services
|
|
441
458
|
* resolve it.
|
|
@@ -460,7 +477,8 @@ var TestSessions = class {
|
|
|
460
477
|
binding(id) {
|
|
461
478
|
const record = this.records.get(id);
|
|
462
479
|
if (record === void 0) return void 0;
|
|
463
|
-
|
|
480
|
+
record.binding ??= this.bindingOf(id, record);
|
|
481
|
+
return record.binding;
|
|
464
482
|
}
|
|
465
483
|
/**
|
|
466
484
|
* Read the session scope tag off a context (service-method boundary mirror).
|
|
@@ -482,6 +500,24 @@ var TestSessions = class {
|
|
|
482
500
|
return this.records.get(id)?.session;
|
|
483
501
|
}
|
|
484
502
|
/**
|
|
503
|
+
* Install Session creation behavior for navigation tests.
|
|
504
|
+
* @param impl - implementation that must return an already-added fixture id.
|
|
505
|
+
*/
|
|
506
|
+
stubCreate(impl) {
|
|
507
|
+
this.createStub = impl;
|
|
508
|
+
}
|
|
509
|
+
/** Create through the installed test behavior and require an addressable binding. */
|
|
510
|
+
async create(opts) {
|
|
511
|
+
this.calls.push({
|
|
512
|
+
method: "create",
|
|
513
|
+
args: [opts]
|
|
514
|
+
});
|
|
515
|
+
if (this.createStub === void 0) throw new Error("test sessions: create is not stubbed — call stubCreate() first");
|
|
516
|
+
const id = await this.createStub(opts);
|
|
517
|
+
this.require(id);
|
|
518
|
+
return id;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
485
521
|
* Service-level selection call (recorded, then applied to the list store
|
|
486
522
|
* synchronously — inject callbacks call this outside any act window; the
|
|
487
523
|
* store notify is microtask-batched so the next stabilized step observes it).
|
|
@@ -530,16 +566,6 @@ var TestSessions = class {
|
|
|
530
566
|
});
|
|
531
567
|
return Promise.resolve();
|
|
532
568
|
}
|
|
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
569
|
/** Clear the current selection (recorded; the production no-session flow). */
|
|
544
570
|
clear() {
|
|
545
571
|
this.calls.push({
|
|
@@ -551,6 +577,14 @@ var TestSessions = class {
|
|
|
551
577
|
draft.currentAddress = void 0;
|
|
552
578
|
});
|
|
553
579
|
}
|
|
580
|
+
/** Record a list refresh; fixture callers publish list state explicitly. */
|
|
581
|
+
refresh() {
|
|
582
|
+
this.calls.push({
|
|
583
|
+
method: "refresh",
|
|
584
|
+
args: []
|
|
585
|
+
});
|
|
586
|
+
return Promise.resolve();
|
|
587
|
+
}
|
|
554
588
|
/**
|
|
555
589
|
* Replace the sidebar-search result page (the call is still recorded).
|
|
556
590
|
* @param impl - hits for a query, as the Host would rank them.
|
|
@@ -596,7 +630,7 @@ var TestSessions = class {
|
|
|
596
630
|
* The session face of a fixture (typed view for assertions; fixture
|
|
597
631
|
* behavior methods are grafted onto it).
|
|
598
632
|
* @param id - session id.
|
|
599
|
-
* @returns the FixtureSession
|
|
633
|
+
* @returns the FixtureSession carried by the Controller binding.
|
|
600
634
|
*/
|
|
601
635
|
behavior(id) {
|
|
602
636
|
return this.require(id).session;
|
|
@@ -607,6 +641,7 @@ var TestSessions = class {
|
|
|
607
641
|
await record.scopeFiber.dispose();
|
|
608
642
|
record.scope = void 0;
|
|
609
643
|
record.scopeFiber = void 0;
|
|
644
|
+
record.binding = void 0;
|
|
610
645
|
}
|
|
611
646
|
}
|
|
612
647
|
bindingOf(id, record) {
|
|
@@ -617,6 +652,7 @@ var TestSessions = class {
|
|
|
617
652
|
return {
|
|
618
653
|
sessionId: id,
|
|
619
654
|
session: record.session,
|
|
655
|
+
eventSource: record.session.eventSource,
|
|
620
656
|
ctx
|
|
621
657
|
};
|
|
622
658
|
}
|
|
@@ -649,7 +685,7 @@ var TestWorkspaces = class {
|
|
|
649
685
|
*/
|
|
650
686
|
constructor(stabilize) {
|
|
651
687
|
this.stabilize = stabilize;
|
|
652
|
-
this.list = createSnapshotStore(
|
|
688
|
+
this.list = createSnapshotStore({ ...workspaceSnapshot() });
|
|
653
689
|
}
|
|
654
690
|
/**
|
|
655
691
|
* Update the workspace list state through an immer draft.
|
|
@@ -662,40 +698,13 @@ var TestWorkspaces = class {
|
|
|
662
698
|
}
|
|
663
699
|
/**
|
|
664
700
|
* Replace an action's behavior (the recorded call is still appended first).
|
|
665
|
-
* @param method - action name (e.g. '
|
|
701
|
+
* @param method - Controller action name (e.g. 'create').
|
|
666
702
|
* @param impl - replacement behavior.
|
|
667
703
|
*/
|
|
668
704
|
stub(method, impl) {
|
|
669
705
|
this.stubs.set(method, impl);
|
|
670
706
|
}
|
|
671
707
|
/**
|
|
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
708
|
* Create a Workspace (recorded). The default echoes a view derived from
|
|
700
709
|
* the input; stub for failure or list-coupled flows.
|
|
701
710
|
* @param input - the Host create payload.
|
|
@@ -716,82 +725,6 @@ var TestWorkspaces = class {
|
|
|
716
725
|
};
|
|
717
726
|
}
|
|
718
727
|
/**
|
|
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
728
|
* Rename a Workspace (recorded). The default echoes a minimal view.
|
|
796
729
|
* @param workspaceId - target workspace.
|
|
797
730
|
* @param title - new title.
|
|
@@ -899,6 +832,7 @@ function stubSettingsScope() {
|
|
|
899
832
|
};
|
|
900
833
|
const listeners = /* @__PURE__ */ new Set();
|
|
901
834
|
const set = vi.fn(() => Promise.resolve());
|
|
835
|
+
const mutate = vi.fn(() => Promise.resolve());
|
|
902
836
|
const unset = vi.fn(() => Promise.resolve());
|
|
903
837
|
return {
|
|
904
838
|
scope: {
|
|
@@ -909,10 +843,12 @@ function stubSettingsScope() {
|
|
|
909
843
|
listeners.delete(listener);
|
|
910
844
|
};
|
|
911
845
|
},
|
|
846
|
+
mutate,
|
|
912
847
|
set,
|
|
913
848
|
unset
|
|
914
849
|
},
|
|
915
850
|
set,
|
|
851
|
+
mutate,
|
|
916
852
|
unset,
|
|
917
853
|
listenerCount: () => listeners.size,
|
|
918
854
|
publish: (next) => {
|
|
@@ -925,34 +861,100 @@ function stubSettingsScope() {
|
|
|
925
861
|
};
|
|
926
862
|
}
|
|
927
863
|
//#endregion
|
|
864
|
+
//#region lib/types/settings-remote.js
|
|
865
|
+
/** Test double for the `settings` Remote namespace a bench's plugins inject. */
|
|
866
|
+
/**
|
|
867
|
+
* Build a scripted `settings` Remote namespace for a bench. Each write answers
|
|
868
|
+
* with the addressed namespace unchanged, so a bench that only needs its
|
|
869
|
+
* plugins to activate scripts nothing; one asserting a write reads the
|
|
870
|
+
* corresponding spy or replaces the face.
|
|
871
|
+
* @param namespaces - namespace views the first describe answers with.
|
|
872
|
+
* @param options - deployment facts the describe answer reports.
|
|
873
|
+
* @returns the face and its controls.
|
|
874
|
+
*/
|
|
875
|
+
function scriptedSettingsRemote(namespaces = [], options = {}) {
|
|
876
|
+
let served = namespaces;
|
|
877
|
+
const writable = options.writable ?? true;
|
|
878
|
+
const hasDocument = options.hasDocument ?? false;
|
|
879
|
+
const answer = (ns) => {
|
|
880
|
+
const view = served.find((candidate) => candidate.ns === ns);
|
|
881
|
+
return Promise.resolve(view === void 0 ? {
|
|
882
|
+
ok: false,
|
|
883
|
+
error: {
|
|
884
|
+
code: "settings/rejected",
|
|
885
|
+
message: `no scripted namespace "${ns}"`,
|
|
886
|
+
details: { ns }
|
|
887
|
+
}
|
|
888
|
+
} : {
|
|
889
|
+
ok: true,
|
|
890
|
+
value: view
|
|
891
|
+
});
|
|
892
|
+
};
|
|
893
|
+
const update = vi.fn((ns, _patch, _expectedRevision) => answer(ns));
|
|
894
|
+
const replace = vi.fn((ns, _section, _expectedRevision) => answer(ns));
|
|
895
|
+
const mutate = vi.fn((ns, _ops, _expectedRevision) => answer(ns));
|
|
896
|
+
return {
|
|
897
|
+
settings: {
|
|
898
|
+
describe: () => Promise.resolve({
|
|
899
|
+
ok: true,
|
|
900
|
+
value: {
|
|
901
|
+
writable,
|
|
902
|
+
hasDocument,
|
|
903
|
+
namespaces: served
|
|
904
|
+
}
|
|
905
|
+
}),
|
|
906
|
+
update: (ns, patch, expectedRevision) => update(ns, patch, expectedRevision),
|
|
907
|
+
replace: (ns, section, expectedRevision) => replace(ns, section, expectedRevision),
|
|
908
|
+
mutate: (ns, ops, expectedRevision) => mutate(ns, ops, expectedRevision)
|
|
909
|
+
},
|
|
910
|
+
update,
|
|
911
|
+
replace,
|
|
912
|
+
mutate,
|
|
913
|
+
publish(next) {
|
|
914
|
+
served = next;
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
//#endregion
|
|
928
919
|
//#region lib/types/remote.js
|
|
929
920
|
/**
|
|
930
921
|
* Remote service test double for the forwarded-event path. Feature specs need
|
|
931
922
|
* `ctx.remote.$on` to exist (their plugins inject `remote`) and need forwarded
|
|
932
|
-
*
|
|
933
|
-
*
|
|
934
|
-
*
|
|
935
|
-
*
|
|
936
|
-
* host frame sink and hands each decoded `host/remote-event` frame to
|
|
937
|
-
* `$dispatch`. A spec therefore exercises its refresh chains by calling
|
|
938
|
-
* `$dispatch(name, args)` on this double.
|
|
923
|
+
* Host events to reach those subscribers, but not the wire — so this double
|
|
924
|
+
* implements subscription plus an explicit `emit` driver available only on the
|
|
925
|
+
* concrete test object. A spec that also calls one namespace scripts it through
|
|
926
|
+
* the constructor rather than reaching the real Client Remote service.
|
|
939
927
|
*
|
|
940
|
-
* `$mount` rejects: a spec that
|
|
941
|
-
*
|
|
928
|
+
* `$mount` rejects: a spec that needs a real generated contribution installed —
|
|
929
|
+
* codecs, descriptors, and the wire — has outgrown this double and needs the
|
|
930
|
+
* real Client Remote service.
|
|
942
931
|
*
|
|
943
932
|
* One deliberate asymmetry with production: a throwing listener propagates out
|
|
944
933
|
* of the emit instead of being contained and logged, so a spec cannot lean on
|
|
945
934
|
* this double for the containment guarantee `$on` documents — assert that
|
|
946
935
|
* against the real service.
|
|
947
936
|
*/
|
|
948
|
-
var TestRemote = class {
|
|
937
|
+
var TestRemote = class TestRemote {
|
|
949
938
|
subscriptions = /* @__PURE__ */ new Map();
|
|
950
939
|
/**
|
|
951
|
-
*
|
|
940
|
+
* Fixed Host facts mirrored from the production `ctx.remote.$host`. Plain
|
|
941
|
+
* mutable field: a spec assigns it to script a non-loopback or homed Host.
|
|
942
|
+
*/
|
|
943
|
+
$host = {
|
|
944
|
+
home: void 0,
|
|
945
|
+
isLoopback: true
|
|
946
|
+
};
|
|
947
|
+
/**
|
|
948
|
+
* Register the double as `ctx.remote`, plus one service per scripted
|
|
949
|
+
* namespace so a plugin injecting `remote.<name>` also unparks.
|
|
952
950
|
* @param ctx - the spec's root Context.
|
|
951
|
+
* @param namespaces - scripted namespace faces reached as `ctx.remote.<name>`.
|
|
953
952
|
*/
|
|
954
|
-
constructor(ctx) {
|
|
953
|
+
constructor(ctx, namespaces = {}) {
|
|
954
|
+
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`);
|
|
955
|
+
Object.assign(this, namespaces);
|
|
955
956
|
ctx.provide("remote", this);
|
|
957
|
+
for (const [name, face] of Object.entries(namespaces)) ctx.provide(`remote.${name}`, face);
|
|
956
958
|
}
|
|
957
959
|
/**
|
|
958
960
|
* Deliver one forwarded host event to its subscribers, standing in for the
|
|
@@ -960,7 +962,7 @@ var TestRemote = class {
|
|
|
960
962
|
* @param event - forwarded host event name.
|
|
961
963
|
* @param args - the Host argument list, verbatim.
|
|
962
964
|
*/
|
|
963
|
-
|
|
965
|
+
emit(event, args) {
|
|
964
966
|
const listeners = this.subscriptions.get(event);
|
|
965
967
|
if (listeners === void 0) return;
|
|
966
968
|
for (const listener of [...listeners]) listener(...args);
|
|
@@ -1054,7 +1056,7 @@ function usePinnedBrowserLanguages(primary, ...rest) {
|
|
|
1054
1056
|
//#region lib/types/index.js
|
|
1055
1057
|
/**
|
|
1056
1058
|
* jsdom slot test runtime: a real small runtime — Cordis `Context`, the
|
|
1057
|
-
*
|
|
1059
|
+
* renderer-owned `SlotRegistry`, the `ui-session` adapter, and the UI renderer — assembled around
|
|
1058
1060
|
* test-owned session/workspace doubles, so feature specs exercise
|
|
1059
1061
|
* declaration, registration, scope, store, inject, rendering, updates, and
|
|
1060
1062
|
* disposal without hand-building the machinery per suite.
|
|
@@ -1163,7 +1165,7 @@ var TestRoot = class {
|
|
|
1163
1165
|
* batching or React act themselves.
|
|
1164
1166
|
*/
|
|
1165
1167
|
var SlotTestRuntime = class SlotTestRuntime {
|
|
1166
|
-
/** The runtime's Cordis root
|
|
1168
|
+
/** The runtime's Cordis root for owner APIs and explicit test-only services. */
|
|
1167
1169
|
ctx;
|
|
1168
1170
|
/** The production SlotRegistry mounted on {@link SlotTestRuntime.ctx}. */
|
|
1169
1171
|
slots;
|
|
@@ -1186,6 +1188,7 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1186
1188
|
ownerCell = new OwnerPropsCell();
|
|
1187
1189
|
autoDeclared = /* @__PURE__ */ new Set();
|
|
1188
1190
|
autoRootView;
|
|
1191
|
+
disposeWorkspaceSource;
|
|
1189
1192
|
constructor(ctx, slots) {
|
|
1190
1193
|
this.ctx = ctx;
|
|
1191
1194
|
this.slots = slots;
|
|
@@ -1194,6 +1197,7 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1194
1197
|
this.workspaces = new TestWorkspaces(this.stabilizer);
|
|
1195
1198
|
ctx.provide("sessions", this.sessions);
|
|
1196
1199
|
ctx.provide("workspaces", this.workspaces);
|
|
1200
|
+
this.disposeWorkspaceSource = slots.provideRoot({ hooks: { workspaces: this.workspaces.list } });
|
|
1197
1201
|
const renderer = createSlotRenderer();
|
|
1198
1202
|
slots.install({ renderRoot: (host, ownerProps) => {
|
|
1199
1203
|
this.host = host;
|
|
@@ -1209,22 +1213,12 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1209
1213
|
registerDomSnapshotSerializer();
|
|
1210
1214
|
const ctx = new Context();
|
|
1211
1215
|
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);
|
|
1216
|
+
const runtime = new SlotTestRuntime(ctx, ctx.get("slots"));
|
|
1217
|
+
await ctx.plugin({
|
|
1218
|
+
inject: [...inject],
|
|
1219
|
+
apply
|
|
1220
|
+
}).await();
|
|
1221
|
+
return runtime;
|
|
1228
1222
|
}
|
|
1229
1223
|
/**
|
|
1230
1224
|
* Mount a feature plugin on a real fiber. Required services are prechecked
|
|
@@ -1252,6 +1246,10 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1252
1246
|
this.handles.push(handle);
|
|
1253
1247
|
return handle;
|
|
1254
1248
|
}
|
|
1249
|
+
/** Release the default Workspace hook before mounting its production owner. */
|
|
1250
|
+
releaseWorkspaceSource() {
|
|
1251
|
+
this.disposeWorkspaceSource();
|
|
1252
|
+
}
|
|
1255
1253
|
/**
|
|
1256
1254
|
* Render the root slot tree through the ctx-level entry (the shell's own
|
|
1257
1255
|
* entry point): `ctx.slots.renderSlot('root', {})` under Testing Library.
|
|
@@ -1321,7 +1319,9 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1321
1319
|
if (this.host === void 0) throw new Error("storeOf before renderRoot() — the host face exists only inside the installed renderer");
|
|
1322
1320
|
const entry = this.host.entriesOf(key)[0];
|
|
1323
1321
|
if (entry === void 0) throw new Error(`storeOf('${key}'): no registration on the ledger`);
|
|
1324
|
-
const
|
|
1322
|
+
const scopeBinding = scopeKey === void 0 ? void 0 : this.host.scope("session")?.resolve(scopeKey);
|
|
1323
|
+
if (scopeKey !== void 0 && scopeBinding === void 0) throw new Error(`storeOf('${key}'): no live Session binding for '${scopeKey}'`);
|
|
1324
|
+
const instance = this.host.storeOf(entry, scopeBinding);
|
|
1325
1325
|
if (instance === void 0) throw new Error(`storeOf('${key}'): the entry declares no store`);
|
|
1326
1326
|
return instance;
|
|
1327
1327
|
}
|
|
@@ -1351,4 +1351,4 @@ var SlotTestRuntime = class SlotTestRuntime {
|
|
|
1351
1351
|
}
|
|
1352
1352
|
};
|
|
1353
1353
|
//#endregion
|
|
1354
|
-
export { FixtureSession, SlotTestRuntime, TestRemote, TestRoot, TestSessions, TestWorkspaces, bindSnapshotSelector, conversationSnapshot, createSlotRenderer, domSnapshotSerializer, makeTranslate, registerDomSnapshotSerializer, stubSettingsScope, usePinnedBrowserLanguages,
|
|
1354
|
+
export { FixtureSession, RemoteError, SlotTestRuntime, TestRemote, TestRoot, TestSessions, TestWorkspaces, bindSnapshotSelector, chatSnapshot, conversationSnapshot, createSlotRenderer, domSnapshotSerializer, makeTranslate, registerDomSnapshotSerializer, scriptedSettingsRemote, sessionSnapshot, stubSettingsScope, usePinnedBrowserLanguages, workspaceSnapshot };
|