mindoodb-app-sdk 0.0.63 → 0.0.65
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/TESTING.md +38 -1
- package/dist/testing/index.d.ts +169 -4
- package/dist/testing/index.js +1169 -153
- package/package.json +1 -1
package/dist/testing/index.js
CHANGED
|
@@ -102,7 +102,7 @@ function getCategoryPath(entry) {
|
|
|
102
102
|
}
|
|
103
103
|
return path;
|
|
104
104
|
}
|
|
105
|
-
function serializeEntry(
|
|
105
|
+
function serializeEntry(navigator2, entry) {
|
|
106
106
|
const parent = entry.getParent();
|
|
107
107
|
return {
|
|
108
108
|
key: entry.getPositionStr(),
|
|
@@ -122,8 +122,8 @@ function serializeEntry(navigator, entry) {
|
|
|
122
122
|
childCategoryCount: entry.getChildCategoryCount(),
|
|
123
123
|
childDocumentCount: entry.getChildDocumentCount(),
|
|
124
124
|
position: entry.getPositionStr(),
|
|
125
|
-
expanded: entry.isCategory() ?
|
|
126
|
-
selected:
|
|
125
|
+
expanded: entry.isCategory() ? navigator2.isExpanded(entry) : false,
|
|
126
|
+
selected: navigator2.isSelected(entry.origin, entry.docId),
|
|
127
127
|
isVisible: true
|
|
128
128
|
};
|
|
129
129
|
}
|
|
@@ -173,24 +173,24 @@ async function buildVirtualView(mindoodb, definition, documents, categorizationS
|
|
|
173
173
|
break;
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
|
-
const
|
|
176
|
+
const view2 = await builder.build();
|
|
177
177
|
const changes = /* @__PURE__ */ new Map();
|
|
178
178
|
for (const document2 of documents) {
|
|
179
179
|
if (!matchesFilter(mindoodb, definition, document2)) {
|
|
180
180
|
continue;
|
|
181
181
|
}
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
changes.set(document2.origin,
|
|
182
|
+
const change2 = changes.get(document2.origin) ?? new mindoodb.VirtualViewDataChange(document2.origin);
|
|
183
|
+
change2.addEntry(document2.docId, computeColumnValues(definition, document2));
|
|
184
|
+
changes.set(document2.origin, change2);
|
|
185
185
|
}
|
|
186
|
-
for (const
|
|
187
|
-
if (
|
|
186
|
+
for (const change2 of changes.values()) {
|
|
187
|
+
if (change2.hasChanges()) view2.applyChanges(change2);
|
|
188
188
|
}
|
|
189
|
-
|
|
190
|
-
return
|
|
189
|
+
view2.getRoot();
|
|
190
|
+
return view2;
|
|
191
191
|
}
|
|
192
|
-
function createCoreNavigator(mindoodb,
|
|
193
|
-
const builder = mindoodb.VirtualViewFactory.createNavigator(
|
|
192
|
+
function createCoreNavigator(mindoodb, view2, options) {
|
|
193
|
+
const builder = mindoodb.VirtualViewFactory.createNavigator(view2);
|
|
194
194
|
const includeCategories = options?.includeCategories ?? true;
|
|
195
195
|
const includeDocuments = options?.includeDocuments ?? true;
|
|
196
196
|
if (!includeCategories && includeDocuments) builder.documentsOnly();
|
|
@@ -199,7 +199,7 @@ function createCoreNavigator(mindoodb, view, options) {
|
|
|
199
199
|
if (Array.isArray(options?.rootCategoryPath) && options.rootCategoryPath.length > 0) {
|
|
200
200
|
builder.fromCategory(options.rootCategoryPath.map((part) => String(part)).join("\\"));
|
|
201
201
|
} else if (options?.rootEntryKey) {
|
|
202
|
-
const temp = mindoodb.VirtualViewFactory.createSimpleNavigator(
|
|
202
|
+
const temp = mindoodb.VirtualViewFactory.createSimpleNavigator(view2);
|
|
203
203
|
const rootEntry = temp.getPos(options.rootEntryKey);
|
|
204
204
|
if (rootEntry) builder.fromEntry(rootEntry);
|
|
205
205
|
}
|
|
@@ -208,21 +208,21 @@ function createCoreNavigator(mindoodb, view, options) {
|
|
|
208
208
|
function normalizePageLimit(limit) {
|
|
209
209
|
return Math.max(1, Math.min(1e3, Math.trunc(limit ?? 100)));
|
|
210
210
|
}
|
|
211
|
-
function pageEntries(
|
|
211
|
+
function pageEntries(navigator2, direction, options = {}) {
|
|
212
212
|
const entries = [];
|
|
213
|
-
const startOk = options.startPosition ?
|
|
213
|
+
const startOk = options.startPosition ? navigator2.gotoPos(options.startPosition) : direction === "forward" ? navigator2.gotoFirst() : navigator2.gotoLast();
|
|
214
214
|
if (!startOk) {
|
|
215
215
|
return { entries, nextPosition: null, hasMore: false };
|
|
216
216
|
}
|
|
217
217
|
const moveNext = () => {
|
|
218
218
|
if (direction === "forward") {
|
|
219
|
-
return options.selectedOnly ?
|
|
219
|
+
return options.selectedOnly ? navigator2.gotoNextSelected() : navigator2.gotoNext();
|
|
220
220
|
}
|
|
221
|
-
return options.selectedOnly ?
|
|
221
|
+
return options.selectedOnly ? navigator2.gotoPrevSelected() : navigator2.gotoPrev();
|
|
222
222
|
};
|
|
223
223
|
if (options.selectedOnly) {
|
|
224
|
-
const current =
|
|
225
|
-
if (!current || !
|
|
224
|
+
const current = navigator2.getCurrentEntry();
|
|
225
|
+
if (!current || !navigator2.isSelected(current.origin, current.docId)) {
|
|
226
226
|
if (!moveNext()) return { entries, nextPosition: null, hasMore: false };
|
|
227
227
|
}
|
|
228
228
|
}
|
|
@@ -230,26 +230,26 @@ function pageEntries(navigator, direction, options = {}) {
|
|
|
230
230
|
let nextPosition = null;
|
|
231
231
|
let hasMore = false;
|
|
232
232
|
while (true) {
|
|
233
|
-
const current =
|
|
233
|
+
const current = navigator2.getCurrentEntry();
|
|
234
234
|
if (!current) break;
|
|
235
|
-
entries.push(serializeEntry(
|
|
235
|
+
entries.push(serializeEntry(navigator2, current));
|
|
236
236
|
if (entries.length >= limit) {
|
|
237
237
|
hasMore = moveNext();
|
|
238
|
-
nextPosition = hasMore ?
|
|
238
|
+
nextPosition = hasMore ? navigator2.getCurrentEntry()?.getPositionStr() ?? null : null;
|
|
239
239
|
break;
|
|
240
240
|
}
|
|
241
241
|
if (!moveNext()) break;
|
|
242
242
|
}
|
|
243
243
|
return { entries, nextPosition, hasMore };
|
|
244
244
|
}
|
|
245
|
-
function resolveEntry(
|
|
246
|
-
const entry =
|
|
245
|
+
function resolveEntry(navigator2, entryKey) {
|
|
246
|
+
const entry = navigator2.getPos(entryKey);
|
|
247
247
|
if (!entry) {
|
|
248
248
|
throw new Error(`Unknown view entry: ${entryKey}`);
|
|
249
249
|
}
|
|
250
250
|
return entry;
|
|
251
251
|
}
|
|
252
|
-
function wrapNavigator(definition,
|
|
252
|
+
function wrapNavigator(definition, navigator2) {
|
|
253
253
|
const updateListeners = /* @__PURE__ */ new Set();
|
|
254
254
|
let viewCursor = null;
|
|
255
255
|
return {
|
|
@@ -267,164 +267,164 @@ function wrapNavigator(definition, navigator) {
|
|
|
267
267
|
return () => updateListeners.delete(listener);
|
|
268
268
|
},
|
|
269
269
|
async getCurrentEntry() {
|
|
270
|
-
const entry =
|
|
271
|
-
return entry ? serializeEntry(
|
|
270
|
+
const entry = navigator2.getCurrentEntry();
|
|
271
|
+
return entry ? serializeEntry(navigator2, entry) : null;
|
|
272
272
|
},
|
|
273
273
|
async gotoFirst() {
|
|
274
|
-
return
|
|
274
|
+
return navigator2.gotoFirst();
|
|
275
275
|
},
|
|
276
276
|
async gotoLast() {
|
|
277
|
-
return
|
|
277
|
+
return navigator2.gotoLast();
|
|
278
278
|
},
|
|
279
279
|
async gotoNext() {
|
|
280
|
-
return
|
|
280
|
+
return navigator2.gotoNext();
|
|
281
281
|
},
|
|
282
282
|
async gotoPrev() {
|
|
283
|
-
return
|
|
283
|
+
return navigator2.gotoPrev();
|
|
284
284
|
},
|
|
285
285
|
async gotoNextSibling() {
|
|
286
|
-
return
|
|
286
|
+
return navigator2.gotoNextSibling();
|
|
287
287
|
},
|
|
288
288
|
async gotoPrevSibling() {
|
|
289
|
-
return
|
|
289
|
+
return navigator2.gotoPrevSibling();
|
|
290
290
|
},
|
|
291
291
|
async gotoParent() {
|
|
292
|
-
return
|
|
292
|
+
return navigator2.gotoParent();
|
|
293
293
|
},
|
|
294
294
|
async gotoFirstChild() {
|
|
295
|
-
return
|
|
295
|
+
return navigator2.gotoFirstChild();
|
|
296
296
|
},
|
|
297
297
|
async gotoLastChild() {
|
|
298
|
-
return
|
|
298
|
+
return navigator2.gotoLastChild();
|
|
299
299
|
},
|
|
300
300
|
async gotoPos(position) {
|
|
301
|
-
return
|
|
301
|
+
return navigator2.gotoPos(position);
|
|
302
302
|
},
|
|
303
303
|
async getPos(position) {
|
|
304
|
-
const entry =
|
|
305
|
-
return entry ? serializeEntry(
|
|
304
|
+
const entry = navigator2.getPos(position);
|
|
305
|
+
return entry ? serializeEntry(navigator2, entry) : null;
|
|
306
306
|
},
|
|
307
307
|
async findCategoryEntryByParts(parts) {
|
|
308
|
-
const entry =
|
|
309
|
-
return entry ? serializeEntry(
|
|
308
|
+
const entry = navigator2.findCategoryEntryByParts(parts);
|
|
309
|
+
return entry ? serializeEntry(navigator2, entry) : null;
|
|
310
310
|
},
|
|
311
311
|
async entriesForward(options) {
|
|
312
|
-
return pageEntries(
|
|
312
|
+
return pageEntries(navigator2, "forward", options);
|
|
313
313
|
},
|
|
314
314
|
async entriesBackward(options) {
|
|
315
|
-
return pageEntries(
|
|
315
|
+
return pageEntries(navigator2, "backward", options);
|
|
316
316
|
},
|
|
317
317
|
async gotoNextSelected() {
|
|
318
|
-
return
|
|
318
|
+
return navigator2.gotoNextSelected();
|
|
319
319
|
},
|
|
320
320
|
async gotoPrevSelected() {
|
|
321
|
-
return
|
|
321
|
+
return navigator2.gotoPrevSelected();
|
|
322
322
|
},
|
|
323
323
|
async select(origin, docId, selectParentCategories = false) {
|
|
324
|
-
|
|
324
|
+
navigator2.select(origin, docId, selectParentCategories);
|
|
325
325
|
},
|
|
326
326
|
async deselect(origin, docId) {
|
|
327
|
-
|
|
327
|
+
navigator2.deselect(origin, docId);
|
|
328
328
|
},
|
|
329
329
|
async selectAllEntries() {
|
|
330
|
-
|
|
330
|
+
navigator2.selectAllEntries();
|
|
331
331
|
},
|
|
332
332
|
async deselectAllEntries() {
|
|
333
|
-
|
|
333
|
+
navigator2.deselectAllEntries();
|
|
334
334
|
},
|
|
335
335
|
async isSelected(origin, docId) {
|
|
336
|
-
return
|
|
336
|
+
return navigator2.isSelected(origin, docId);
|
|
337
337
|
},
|
|
338
338
|
async getSelectionState() {
|
|
339
339
|
return {
|
|
340
|
-
selectAllByDefault:
|
|
341
|
-
entryKeys: Array.from(
|
|
340
|
+
selectAllByDefault: navigator2.isSelectAllByDefault(),
|
|
341
|
+
entryKeys: Array.from(navigator2.getSelectedOrDeselectedEntries())
|
|
342
342
|
};
|
|
343
343
|
},
|
|
344
344
|
async setSelectionState(state) {
|
|
345
|
-
if (state.selectAllByDefault)
|
|
346
|
-
else
|
|
347
|
-
|
|
345
|
+
if (state.selectAllByDefault) navigator2.selectAllEntries();
|
|
346
|
+
else navigator2.deselectAllEntries();
|
|
347
|
+
navigator2.setSelectedOrDeselectedEntries(new Set(state.entryKeys));
|
|
348
348
|
},
|
|
349
349
|
async expand(origin, docId) {
|
|
350
|
-
|
|
350
|
+
navigator2.expand(origin, docId);
|
|
351
351
|
},
|
|
352
352
|
async collapse(origin, docId) {
|
|
353
|
-
|
|
353
|
+
navigator2.collapse(origin, docId);
|
|
354
354
|
},
|
|
355
355
|
async expandAll() {
|
|
356
|
-
|
|
356
|
+
navigator2.expandAll();
|
|
357
357
|
},
|
|
358
358
|
async collapseAll() {
|
|
359
|
-
|
|
359
|
+
navigator2.collapseAll();
|
|
360
360
|
},
|
|
361
361
|
async expandToLevel(level) {
|
|
362
|
-
|
|
362
|
+
navigator2.expandToLevel(level);
|
|
363
363
|
},
|
|
364
364
|
async isExpanded(entryKey) {
|
|
365
|
-
const entry =
|
|
366
|
-
return entry ?
|
|
365
|
+
const entry = navigator2.getPos(entryKey);
|
|
366
|
+
return entry ? navigator2.isExpanded(entry) : false;
|
|
367
367
|
},
|
|
368
368
|
async getExpansionState() {
|
|
369
369
|
return {
|
|
370
|
-
expandAllByDefault:
|
|
371
|
-
expandLevel:
|
|
372
|
-
entryKeys: Array.from(
|
|
370
|
+
expandAllByDefault: navigator2.isExpandAllByDefault(),
|
|
371
|
+
expandLevel: navigator2.getExpandLevel(),
|
|
372
|
+
entryKeys: Array.from(navigator2.getExpandedOrCollapsedEntries())
|
|
373
373
|
};
|
|
374
374
|
},
|
|
375
375
|
async setExpansionState(state) {
|
|
376
|
-
if (state.expandAllByDefault)
|
|
377
|
-
else
|
|
378
|
-
|
|
379
|
-
|
|
376
|
+
if (state.expandAllByDefault) navigator2.expandAll();
|
|
377
|
+
else navigator2.collapseAll();
|
|
378
|
+
navigator2.expandToLevel(state.expandLevel);
|
|
379
|
+
navigator2.setExpandedOrCollapsedEntries(new Set(state.entryKeys));
|
|
380
380
|
},
|
|
381
381
|
async childEntries(entryKey, descending) {
|
|
382
|
-
return
|
|
382
|
+
return navigator2.childEntries(resolveEntry(navigator2, entryKey), descending).map((entry) => serializeEntry(navigator2, entry));
|
|
383
383
|
},
|
|
384
384
|
async childCategories(entryKey, descending) {
|
|
385
|
-
return
|
|
385
|
+
return navigator2.childCategories(resolveEntry(navigator2, entryKey), descending).map((entry) => serializeEntry(navigator2, entry));
|
|
386
386
|
},
|
|
387
387
|
async childDocuments(entryKey, descending) {
|
|
388
|
-
return
|
|
388
|
+
return navigator2.childDocuments(resolveEntry(navigator2, entryKey), descending).map((entry) => serializeEntry(navigator2, entry));
|
|
389
389
|
},
|
|
390
390
|
async childCategoriesByKey(entryKey, key, exact, descending) {
|
|
391
|
-
return
|
|
392
|
-
resolveEntry(
|
|
391
|
+
return navigator2.childCategoriesByKey(
|
|
392
|
+
resolveEntry(navigator2, entryKey),
|
|
393
393
|
String(key ?? ""),
|
|
394
394
|
Boolean(exact),
|
|
395
395
|
descending
|
|
396
|
-
).map((entry) => serializeEntry(
|
|
396
|
+
).map((entry) => serializeEntry(navigator2, entry));
|
|
397
397
|
},
|
|
398
398
|
async childDocumentsByKey(entryKey, key, exact, descending) {
|
|
399
|
-
return
|
|
400
|
-
resolveEntry(
|
|
399
|
+
return navigator2.childDocumentsByKey(
|
|
400
|
+
resolveEntry(navigator2, entryKey),
|
|
401
401
|
String(key ?? ""),
|
|
402
402
|
Boolean(exact),
|
|
403
403
|
descending
|
|
404
|
-
).map((entry) => serializeEntry(
|
|
404
|
+
).map((entry) => serializeEntry(navigator2, entry));
|
|
405
405
|
},
|
|
406
406
|
async childCategoriesBetween(entryKey, range) {
|
|
407
|
-
return
|
|
408
|
-
resolveEntry(
|
|
407
|
+
return navigator2.childCategoriesBetween(
|
|
408
|
+
resolveEntry(navigator2, entryKey),
|
|
409
409
|
range.startKey,
|
|
410
410
|
range.endKey,
|
|
411
411
|
range.descending
|
|
412
|
-
).map((entry) => serializeEntry(
|
|
412
|
+
).map((entry) => serializeEntry(navigator2, entry));
|
|
413
413
|
},
|
|
414
414
|
async childDocumentsBetween(entryKey, range) {
|
|
415
|
-
return
|
|
416
|
-
resolveEntry(
|
|
415
|
+
return navigator2.childDocumentsBetween(
|
|
416
|
+
resolveEntry(navigator2, entryKey),
|
|
417
417
|
range.startKey,
|
|
418
418
|
range.endKey,
|
|
419
419
|
range.descending
|
|
420
|
-
).map((entry) => serializeEntry(
|
|
420
|
+
).map((entry) => serializeEntry(navigator2, entry));
|
|
421
421
|
},
|
|
422
422
|
async getSortedDocIds(descending) {
|
|
423
|
-
const page = pageEntries(
|
|
423
|
+
const page = pageEntries(navigator2, descending ? "backward" : "forward", { limit: 1e3 });
|
|
424
424
|
return collectSortedDocIds(page.entries);
|
|
425
425
|
},
|
|
426
426
|
async getSortedDocIdsScoped(entryKey, descending) {
|
|
427
|
-
const docs =
|
|
427
|
+
const docs = navigator2.childDocuments(resolveEntry(navigator2, entryKey), descending);
|
|
428
428
|
return docs.map((entry) => ({ origin: entry.origin, docId: entry.docId }));
|
|
429
429
|
},
|
|
430
430
|
async dispose() {
|
|
@@ -447,14 +447,14 @@ function collectSortedDocIds(entries) {
|
|
|
447
447
|
async function createEvaluatingViewNavigator(input, documents) {
|
|
448
448
|
const mindoodb = await loadMindoodbForTesting();
|
|
449
449
|
if (!mindoodb) return null;
|
|
450
|
-
const
|
|
450
|
+
const view2 = await buildVirtualView(
|
|
451
451
|
mindoodb,
|
|
452
452
|
input.definition,
|
|
453
453
|
documents,
|
|
454
454
|
input.categorizationStyle
|
|
455
455
|
);
|
|
456
|
-
const
|
|
457
|
-
return wrapNavigator(input.definition,
|
|
456
|
+
const navigator2 = createCoreNavigator(mindoodb, view2, input.options);
|
|
457
|
+
return wrapNavigator(input.definition, navigator2);
|
|
458
458
|
}
|
|
459
459
|
|
|
460
460
|
// src/testing/mockEmbeds.ts
|
|
@@ -828,6 +828,246 @@ function guardDatabase(database, currentInfo, enforce) {
|
|
|
828
828
|
};
|
|
829
829
|
}
|
|
830
830
|
|
|
831
|
+
// src/testing/mockAgentTools.ts
|
|
832
|
+
var TOOL_NAME_PATTERN = /^[a-z][a-z0-9_]{0,47}$/;
|
|
833
|
+
var PREFIX_PATTERN = /^[a-z][a-z0-9_]{0,15}$/;
|
|
834
|
+
var MOCK_AGENT_MAX_TOOLS = 64;
|
|
835
|
+
var MOCK_AGENT_MAX_DESCRIPTION = 2e3;
|
|
836
|
+
var MOCK_AGENT_FILE_TTL_MS = 10 * 60 * 1e3;
|
|
837
|
+
var MOCK_AGENT_FILE_MAX_BYTES = 50 * 1024 * 1024;
|
|
838
|
+
var DEFAULT_INVOKE_TIMEOUT_MS = 12e4;
|
|
839
|
+
function invalid(message) {
|
|
840
|
+
return Object.assign(new Error(message), { name: "invalid-params" });
|
|
841
|
+
}
|
|
842
|
+
function notFound(message) {
|
|
843
|
+
return Object.assign(new Error(message), { name: "not-found" });
|
|
844
|
+
}
|
|
845
|
+
function mockAgentToolPrefix(appId, declared) {
|
|
846
|
+
if (declared && PREFIX_PATTERN.test(declared) && !declared.endsWith("_") && !/^haven(?:_|$)/.test(declared)) {
|
|
847
|
+
return declared;
|
|
848
|
+
}
|
|
849
|
+
const key = (appId.split("\\").pop() ?? "").trim().toLowerCase().replace(/^mindoodb-app-/, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 16).replace(/_+$/, "");
|
|
850
|
+
if (!key) return "app";
|
|
851
|
+
return /^[a-z]/.test(key) ? key : `app_${key}`.slice(0, 16);
|
|
852
|
+
}
|
|
853
|
+
function validateMockAgentTools(raw) {
|
|
854
|
+
if (!Array.isArray(raw)) throw invalid("tools must be an array.");
|
|
855
|
+
if (raw.length > MOCK_AGENT_MAX_TOOLS) throw invalid(`An app may offer at most ${MOCK_AGENT_MAX_TOOLS} agent tools.`);
|
|
856
|
+
const seen = /* @__PURE__ */ new Set();
|
|
857
|
+
return raw.map((entry) => {
|
|
858
|
+
const tool = entry;
|
|
859
|
+
if (!tool || typeof tool.name !== "string" || !TOOL_NAME_PATTERN.test(tool.name)) {
|
|
860
|
+
throw invalid(`Invalid agent tool name "${String(tool?.name)}".`);
|
|
861
|
+
}
|
|
862
|
+
if (seen.has(tool.name)) throw invalid(`Agent tool "${tool.name}" is declared twice.`);
|
|
863
|
+
seen.add(tool.name);
|
|
864
|
+
if (typeof tool.description !== "string" || !tool.description.trim()) {
|
|
865
|
+
throw invalid(`Agent tool "${tool.name}" needs a description.`);
|
|
866
|
+
}
|
|
867
|
+
if (tool.description.length > MOCK_AGENT_MAX_DESCRIPTION) {
|
|
868
|
+
throw invalid(`The description of agent tool "${tool.name}" is too long.`);
|
|
869
|
+
}
|
|
870
|
+
const schema = tool.inputSchema;
|
|
871
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema) || schema.type !== "object") {
|
|
872
|
+
throw invalid(`Agent tool "${tool.name}" needs an object inputSchema.`);
|
|
873
|
+
}
|
|
874
|
+
const annotations = tool.annotations ?? {};
|
|
875
|
+
for (const key of ["readOnlyHint", "consequentialHint", "untrustedContentHint"]) {
|
|
876
|
+
if (annotations[key] !== void 0 && typeof annotations[key] !== "boolean") {
|
|
877
|
+
throw invalid(`Agent tool "${tool.name}" has invalid annotations.`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
name: tool.name,
|
|
882
|
+
description: tool.description,
|
|
883
|
+
inputSchema: schema,
|
|
884
|
+
...tool.annotations ? { annotations: { ...tool.annotations } } : {}
|
|
885
|
+
};
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
async function bytesOf(data) {
|
|
889
|
+
if (typeof data === "string") return new TextEncoder().encode(data);
|
|
890
|
+
if (data instanceof Uint8Array) return data.slice();
|
|
891
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) return new Uint8Array(await data.arrayBuffer());
|
|
892
|
+
return new Uint8Array(data);
|
|
893
|
+
}
|
|
894
|
+
function structuralBytes(data) {
|
|
895
|
+
if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength).slice();
|
|
896
|
+
if (Object.prototype.toString.call(data) === "[object ArrayBuffer]") return new Uint8Array(data).slice();
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
function createMockAgentHost(options) {
|
|
900
|
+
const prefix = mockAgentToolPrefix(options.appId(), options.agentToolPrefix);
|
|
901
|
+
let tools = [];
|
|
902
|
+
let context = null;
|
|
903
|
+
let enabled = options.agentToolsEnabled ?? true;
|
|
904
|
+
let consent = options.agentConsent ?? "allow";
|
|
905
|
+
const timeoutMs = options.agentInvokeTimeoutMs ?? DEFAULT_INVOKE_TIMEOUT_MS;
|
|
906
|
+
const files = /* @__PURE__ */ new Map();
|
|
907
|
+
const calls = [];
|
|
908
|
+
const pending = /* @__PURE__ */ new Map();
|
|
909
|
+
let counter = 0;
|
|
910
|
+
const changed = () => options.onChange?.();
|
|
911
|
+
function prune(now = Date.now()) {
|
|
912
|
+
for (const [ref, file] of files) if (file.expiresAt <= now) files.delete(ref);
|
|
913
|
+
}
|
|
914
|
+
function addFile(file) {
|
|
915
|
+
prune();
|
|
916
|
+
if (file.data.length > MOCK_AGENT_FILE_MAX_BYTES) {
|
|
917
|
+
throw invalid(`Files for agents may be at most ${MOCK_AGENT_FILE_MAX_BYTES / 1024 / 1024} MB.`);
|
|
918
|
+
}
|
|
919
|
+
const now = Date.now();
|
|
920
|
+
counter += 1;
|
|
921
|
+
const entry = {
|
|
922
|
+
...file,
|
|
923
|
+
fileRef: `file_test${counter.toString(16).padStart(4, "0")}`,
|
|
924
|
+
createdAt: now,
|
|
925
|
+
expiresAt: now + MOCK_AGENT_FILE_TTL_MS
|
|
926
|
+
};
|
|
927
|
+
files.set(entry.fileRef, entry);
|
|
928
|
+
changed();
|
|
929
|
+
return entry;
|
|
930
|
+
}
|
|
931
|
+
function registration() {
|
|
932
|
+
return { enabled, exposedNames: tools.map((tool) => `${prefix}_${tool.name}`) };
|
|
933
|
+
}
|
|
934
|
+
async function dispatch(method, params) {
|
|
935
|
+
const input = params ?? {};
|
|
936
|
+
if (method === "agent.registerTools") {
|
|
937
|
+
tools = validateMockAgentTools(input.tools);
|
|
938
|
+
changed();
|
|
939
|
+
return { result: registration() };
|
|
940
|
+
}
|
|
941
|
+
if (method === "agent.setContext") {
|
|
942
|
+
const next = input.context ?? null;
|
|
943
|
+
if (next !== null && (typeof next !== "object" || Array.isArray(next))) {
|
|
944
|
+
throw invalid("context must be an object or null.");
|
|
945
|
+
}
|
|
946
|
+
context = next;
|
|
947
|
+
changed();
|
|
948
|
+
return { result: null };
|
|
949
|
+
}
|
|
950
|
+
if (method === "agent.provideFile") {
|
|
951
|
+
if (typeof input.name !== "string" || !input.name.trim()) throw invalid("provideFile needs a file name.");
|
|
952
|
+
const data = structuralBytes(input.data);
|
|
953
|
+
if (!data) throw invalid("provideFile needs the file content as an ArrayBuffer.");
|
|
954
|
+
const mimeType = typeof input.mimeType === "string" && input.mimeType ? input.mimeType : "application/octet-stream";
|
|
955
|
+
const file = addFile({ name: input.name.trim(), mimeType, data, source: "app" });
|
|
956
|
+
return { result: { fileRef: file.fileRef, size: file.data.length } };
|
|
957
|
+
}
|
|
958
|
+
if (method === "agent.takeFile") {
|
|
959
|
+
prune();
|
|
960
|
+
const file = typeof input.fileRef === "string" ? files.get(input.fileRef) : void 0;
|
|
961
|
+
if (!file || file.source !== "host") throw notFound("Unknown or expired file reference.");
|
|
962
|
+
return { result: { name: file.name, mimeType: file.mimeType, data: file.data.slice().buffer } };
|
|
963
|
+
}
|
|
964
|
+
return void 0;
|
|
965
|
+
}
|
|
966
|
+
function finish(callId, outcome) {
|
|
967
|
+
const call2 = calls.find((entry) => entry.callId === callId);
|
|
968
|
+
if (call2) call2.outcome = outcome;
|
|
969
|
+
changed();
|
|
970
|
+
return outcome;
|
|
971
|
+
}
|
|
972
|
+
async function call(name, input = {}) {
|
|
973
|
+
counter += 1;
|
|
974
|
+
const callId = `test-call-${counter}`;
|
|
975
|
+
const startedAt = Date.now();
|
|
976
|
+
const toolName = name.startsWith(`${prefix}_`) && !tools.some((t) => t.name === name) ? name.slice(prefix.length + 1) : name;
|
|
977
|
+
calls.push({ callId, tool: toolName, input, startedAt });
|
|
978
|
+
changed();
|
|
979
|
+
const failWith = (code, message, requiredAction) => finish(callId, {
|
|
980
|
+
ok: false,
|
|
981
|
+
error: { code, message, ...requiredAction ? { requiredAction } : {} },
|
|
982
|
+
durationMs: Date.now() - startedAt
|
|
983
|
+
});
|
|
984
|
+
if (!enabled) return failWith("NOT_ALLOWED", "The user has agent tools switched off for this app.");
|
|
985
|
+
const tool = tools.find((entry) => entry.name === toolName);
|
|
986
|
+
if (!tool) return failWith("NOT_FOUND", `The app offers no tool "${name}".`);
|
|
987
|
+
if (tool.annotations?.consequentialHint) {
|
|
988
|
+
const allowed = typeof consent === "function" ? await consent({ tool: toolName, input }) : consent === "allow";
|
|
989
|
+
if (!allowed) return failWith("NOT_ALLOWED", "The user declined this call.", "ask the user before trying again");
|
|
990
|
+
}
|
|
991
|
+
return await new Promise((resolve) => {
|
|
992
|
+
const timer = setTimeout(() => {
|
|
993
|
+
pending.delete(callId);
|
|
994
|
+
resolve(failWith("FAILED", `The app did not answer "${toolName}" in time.`));
|
|
995
|
+
}, timeoutMs);
|
|
996
|
+
pending.set(callId, { resolve, startedAt, timer });
|
|
997
|
+
const posted = options.post({ protocol: "mindoodb-app-bridge", kind: "agent-invoke", callId, tool: toolName, input });
|
|
998
|
+
if (!posted) {
|
|
999
|
+
clearTimeout(timer);
|
|
1000
|
+
pending.delete(callId);
|
|
1001
|
+
resolve(failWith("INVALID_STATE", "The app is not connected."));
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
function handleResult(message) {
|
|
1006
|
+
const entry = pending.get(message.callId);
|
|
1007
|
+
if (!entry) return;
|
|
1008
|
+
pending.delete(message.callId);
|
|
1009
|
+
clearTimeout(entry.timer);
|
|
1010
|
+
const durationMs = Date.now() - entry.startedAt;
|
|
1011
|
+
entry.resolve(
|
|
1012
|
+
finish(
|
|
1013
|
+
message.callId,
|
|
1014
|
+
message.ok ? { ok: true, result: message.result ?? null, durationMs } : {
|
|
1015
|
+
ok: false,
|
|
1016
|
+
error: {
|
|
1017
|
+
code: message.error?.code ?? "FAILED",
|
|
1018
|
+
message: message.error?.message ?? "The app reported an error.",
|
|
1019
|
+
...message.error?.requiredAction ? { requiredAction: message.error.requiredAction } : {}
|
|
1020
|
+
},
|
|
1021
|
+
durationMs
|
|
1022
|
+
}
|
|
1023
|
+
)
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
function reset(reason = "The app was closed.") {
|
|
1027
|
+
for (const [callId, entry] of pending) {
|
|
1028
|
+
clearTimeout(entry.timer);
|
|
1029
|
+
entry.resolve(
|
|
1030
|
+
finish(callId, { ok: false, error: { code: "INVALID_STATE", message: reason }, durationMs: Date.now() - entry.startedAt })
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
pending.clear();
|
|
1034
|
+
tools = [];
|
|
1035
|
+
context = null;
|
|
1036
|
+
changed();
|
|
1037
|
+
}
|
|
1038
|
+
const controller = {
|
|
1039
|
+
prefix,
|
|
1040
|
+
tools: () => tools.map((tool) => ({ ...tool })),
|
|
1041
|
+
exposedNames: () => registration().exposedNames,
|
|
1042
|
+
context: () => context,
|
|
1043
|
+
isEnabled: () => enabled,
|
|
1044
|
+
setEnabled(next) {
|
|
1045
|
+
enabled = next;
|
|
1046
|
+
changed();
|
|
1047
|
+
},
|
|
1048
|
+
setConsent(next) {
|
|
1049
|
+
consent = next;
|
|
1050
|
+
},
|
|
1051
|
+
call,
|
|
1052
|
+
calls: () => calls,
|
|
1053
|
+
async importFile(file) {
|
|
1054
|
+
const data = await bytesOf(file.data);
|
|
1055
|
+
const mimeType = file.mimeType || (typeof Blob !== "undefined" && file.data instanceof Blob ? file.data.type : "") || "application/octet-stream";
|
|
1056
|
+
return addFile({ name: file.name, mimeType, data, source: "host" }).fileRef;
|
|
1057
|
+
},
|
|
1058
|
+
getFile(fileRef) {
|
|
1059
|
+
prune();
|
|
1060
|
+
const file = files.get(fileRef);
|
|
1061
|
+
return file && file.source === "app" ? file : null;
|
|
1062
|
+
},
|
|
1063
|
+
files() {
|
|
1064
|
+
prune();
|
|
1065
|
+
return [...files.values()].sort((a, b) => b.createdAt - a.createdAt);
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
return { controller, dispatch, handleResult, reset };
|
|
1069
|
+
}
|
|
1070
|
+
|
|
831
1071
|
// src/testing/memoryAttachments.ts
|
|
832
1072
|
function concat(chunks) {
|
|
833
1073
|
const bytes = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0));
|
|
@@ -930,6 +1170,7 @@ function mockDatabasesFromDefinition(definition, seed = {}, options = {}) {
|
|
|
930
1170
|
capabilities: override?.capabilities ? [...new Set(override.capabilities)] : capabilitiesForDefinitionPermissions(database.permissions)
|
|
931
1171
|
},
|
|
932
1172
|
documents: seed[database.logicalDatabaseId],
|
|
1173
|
+
...options.automerge ? { automerge: true } : {},
|
|
933
1174
|
...options.attachments === "none" ? {} : { methods: { attachments: createMemoryAttachments() } }
|
|
934
1175
|
};
|
|
935
1176
|
});
|
|
@@ -950,6 +1191,8 @@ function readTestHostUrlSettings(search) {
|
|
|
950
1191
|
}
|
|
951
1192
|
const enforce = params.get("enforce");
|
|
952
1193
|
if (enforce !== null) settings.enforceCapabilities = enforce === "1" || enforce === "true";
|
|
1194
|
+
const automerge = params.get("automerge");
|
|
1195
|
+
if (automerge !== null) settings.automerge = automerge === "1" || automerge === "true";
|
|
953
1196
|
const users = Number.parseInt(params.get("users") ?? "", 10);
|
|
954
1197
|
if (Number.isFinite(users) && users >= 0) settings.directoryUserCount = users;
|
|
955
1198
|
const locale = params.get("locale");
|
|
@@ -1020,6 +1263,10 @@ function createBrowserTestHost(options) {
|
|
|
1020
1263
|
const changed = () => queueMicrotask(() => onChange?.());
|
|
1021
1264
|
const host = createFakeBridgeHost({
|
|
1022
1265
|
...hostOptions,
|
|
1266
|
+
onAgentChange() {
|
|
1267
|
+
hostOptions.onAgentChange?.();
|
|
1268
|
+
changed();
|
|
1269
|
+
},
|
|
1023
1270
|
onRequest(request) {
|
|
1024
1271
|
log.requests.push(request);
|
|
1025
1272
|
const params = request.params ?? {};
|
|
@@ -1110,6 +1357,7 @@ function createBrowserTestHost(options) {
|
|
|
1110
1357
|
return {
|
|
1111
1358
|
host,
|
|
1112
1359
|
log,
|
|
1360
|
+
agent: host.agent,
|
|
1113
1361
|
setNextScan(scan) {
|
|
1114
1362
|
nextScan = scan;
|
|
1115
1363
|
},
|
|
@@ -1142,6 +1390,11 @@ function createBrowserTestHost(options) {
|
|
|
1142
1390
|
host.setDirectoryUsers(users);
|
|
1143
1391
|
onChange?.();
|
|
1144
1392
|
},
|
|
1393
|
+
async applyRemoteUpdate(databaseId, docId, input, updateOptions) {
|
|
1394
|
+
const document2 = await host.applyRemoteUpdate(databaseId, docId, input, updateOptions);
|
|
1395
|
+
onChange?.();
|
|
1396
|
+
return document2;
|
|
1397
|
+
},
|
|
1145
1398
|
reloadApp,
|
|
1146
1399
|
dispose() {
|
|
1147
1400
|
window.removeEventListener("message", onMessage);
|
|
@@ -1200,6 +1453,10 @@ var STYLE = `
|
|
|
1200
1453
|
.htest__meta{margin-left:auto;color:#5b6478;font-size:11px;white-space:nowrap}
|
|
1201
1454
|
.htest__error{color:#b42318}
|
|
1202
1455
|
.htest pre{margin:4px 0 2px;padding:6px 8px;background:#fff;border:1px solid #e4e8f1;border-radius:6px;font:11px/1.4 ui-monospace,monospace;white-space:pre-wrap;word-break:break-all;max-height:260px;overflow:auto}
|
|
1456
|
+
.htest textarea{width:100%;min-height:70px;font:11px/1.4 ui-monospace,monospace;border:1px solid #c9d0e0;border-radius:6px;padding:6px}
|
|
1457
|
+
.htest__tag{display:inline-block;padding:0 6px;border-radius:999px;background:#e4e8f1;color:#334155;font-size:10px;margin-left:4px}
|
|
1458
|
+
.htest__tag--warn{background:#fee4e2;color:#912018}
|
|
1459
|
+
.htest__shot{max-width:100%;border:1px solid #e4e8f1;border-radius:6px;margin-top:4px;background:#fff}
|
|
1203
1460
|
@media (max-width:720px){.htest{grid-template-columns:1fr;grid-template-rows:65vh auto}.htest__panel{border-left:0;border-top:1px solid #d7dce8}}
|
|
1204
1461
|
`;
|
|
1205
1462
|
function previewJson(value, maxString = 400, maxItems = 30, maxDepth = 6) {
|
|
@@ -1249,7 +1506,7 @@ function mountHavenTestHost(options) {
|
|
|
1249
1506
|
const databases = (hostOptions.databases ?? []).filter((database) => url.overrides[database.info.id]?.mapped !== false).map((database) => {
|
|
1250
1507
|
const capabilities = url.overrides[database.info.id]?.capabilities;
|
|
1251
1508
|
return capabilities ? { ...database, info: { ...database.info, capabilities } } : database;
|
|
1252
|
-
});
|
|
1509
|
+
}).map((database) => url.automerge === void 0 ? database : { ...database, automerge: url.automerge });
|
|
1253
1510
|
const launchContext = {
|
|
1254
1511
|
...hostOptions.launchContext,
|
|
1255
1512
|
...url.locale ? { locale: url.locale } : {},
|
|
@@ -1430,14 +1687,24 @@ function mountHavenTestHost(options) {
|
|
|
1430
1687
|
lists.scans.replaceChildren(...log.scans.map(scanEntry));
|
|
1431
1688
|
lists.requests.replaceChildren(...log.requests.slice(-50).reverse().map(requestEntry));
|
|
1432
1689
|
}
|
|
1690
|
+
let agentReady = false;
|
|
1433
1691
|
const testHost = createBrowserTestHost({
|
|
1692
|
+
// Haven's consent dialog, as a browser confirm (scripts can switch it with agent.setConsent)
|
|
1693
|
+
agentConsent: ({ tool, input }) => window.confirm(`An agent wants to run "${tool}" (marked consequential).
|
|
1694
|
+
|
|
1695
|
+
${previewJson(input, 200, 10, 3)}
|
|
1696
|
+
|
|
1697
|
+
Allow?`),
|
|
1434
1698
|
...hostOptions,
|
|
1435
1699
|
databases,
|
|
1436
1700
|
launchContext,
|
|
1437
1701
|
enforceCapabilities: url.enforceCapabilities ?? hostOptions.enforceCapabilities,
|
|
1438
1702
|
directoryUsers: url.directoryUserCount !== void 0 ? generateDirectoryUsers(url.directoryUserCount) : hostOptions.directoryUsers,
|
|
1439
1703
|
frame,
|
|
1440
|
-
onChange:
|
|
1704
|
+
onChange: () => {
|
|
1705
|
+
render();
|
|
1706
|
+
if (agentReady) renderAgent();
|
|
1707
|
+
},
|
|
1441
1708
|
onEmbedChange(embeds) {
|
|
1442
1709
|
hostOptions.onEmbedChange?.(embeds);
|
|
1443
1710
|
renderEmbeds(embeds);
|
|
@@ -1574,11 +1841,213 @@ function mountHavenTestHost(options) {
|
|
|
1574
1841
|
scanInput,
|
|
1575
1842
|
hint("What attachments.scan() returns; the file is attached to the document like in Haven.")
|
|
1576
1843
|
);
|
|
1844
|
+
const agentState = document.createElement("div");
|
|
1845
|
+
agentState.className = "htest__muted";
|
|
1846
|
+
const agentEnabled = document.createElement("label");
|
|
1847
|
+
agentEnabled.className = "htest__check";
|
|
1848
|
+
const agentEnabledBox = document.createElement("input");
|
|
1849
|
+
agentEnabledBox.type = "checkbox";
|
|
1850
|
+
agentEnabledBox.dataset.testid = "haven-test-agent-enabled";
|
|
1851
|
+
agentEnabledBox.checked = testHost.agent.isEnabled();
|
|
1852
|
+
agentEnabledBox.addEventListener("change", () => testHost.agent.setEnabled(agentEnabledBox.checked));
|
|
1853
|
+
agentEnabled.append(agentEnabledBox, "Agent access on (registerTools answers enabled)");
|
|
1854
|
+
const agentTools = document.createElement("ul");
|
|
1855
|
+
const toolSelect = document.createElement("select");
|
|
1856
|
+
toolSelect.dataset.testid = "haven-test-agent-tool";
|
|
1857
|
+
toolSelect.setAttribute("aria-label", "Agent tool");
|
|
1858
|
+
const toolInput = document.createElement("textarea");
|
|
1859
|
+
toolInput.dataset.testid = "haven-test-agent-input";
|
|
1860
|
+
toolInput.setAttribute("aria-label", "Tool input (JSON)");
|
|
1861
|
+
toolInput.value = "{}";
|
|
1862
|
+
const toolResult = document.createElement("div");
|
|
1863
|
+
toolResult.dataset.testid = "haven-test-agent-result";
|
|
1864
|
+
const callButton = button("Call tool", () => void runTool());
|
|
1865
|
+
callButton.dataset.testid = "haven-test-agent-call";
|
|
1866
|
+
const agentContext = document.createElement("pre");
|
|
1867
|
+
agentContext.dataset.testid = "haven-test-agent-context";
|
|
1868
|
+
const agentFiles = document.createElement("ul");
|
|
1869
|
+
const agentCalls = document.createElement("ul");
|
|
1870
|
+
const importInput = document.createElement("input");
|
|
1871
|
+
importInput.type = "file";
|
|
1872
|
+
importInput.multiple = true;
|
|
1873
|
+
importInput.dataset.testid = "haven-test-agent-import";
|
|
1874
|
+
importInput.setAttribute("aria-label", "Import files for the app");
|
|
1875
|
+
importInput.addEventListener("change", () => {
|
|
1876
|
+
void (async () => {
|
|
1877
|
+
for (const file of importInput.files ?? []) {
|
|
1878
|
+
await testHost.agent.importFile({ name: file.name, mimeType: file.type, data: file });
|
|
1879
|
+
}
|
|
1880
|
+
importInput.value = "";
|
|
1881
|
+
})();
|
|
1882
|
+
});
|
|
1883
|
+
function showOutcome(target, call) {
|
|
1884
|
+
const pre = document.createElement("pre");
|
|
1885
|
+
const outcome = call.outcome;
|
|
1886
|
+
if (!outcome) {
|
|
1887
|
+
pre.textContent = "running\u2026";
|
|
1888
|
+
target.replaceChildren(pre);
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
if (outcome.ok) {
|
|
1892
|
+
pre.textContent = previewJson(outcome.result, 2e3, 60, 8);
|
|
1893
|
+
} else {
|
|
1894
|
+
pre.className = "htest__error";
|
|
1895
|
+
pre.textContent = `${outcome.error.code}: ${outcome.error.message}${outcome.error.requiredAction ? `
|
|
1896
|
+
\u2192 ${outcome.error.requiredAction}` : ""}`;
|
|
1897
|
+
}
|
|
1898
|
+
const images = [];
|
|
1899
|
+
const collect = (value, depth) => {
|
|
1900
|
+
if (depth > 4 || images.length >= 4) return;
|
|
1901
|
+
if (typeof value === "string" && /^data:image\/(png|jpeg|gif|webp|svg\+xml);base64,/.test(value)) images.push(value);
|
|
1902
|
+
else if (value && typeof value === "object") for (const child of Object.values(value)) collect(child, depth + 1);
|
|
1903
|
+
};
|
|
1904
|
+
if (outcome.ok) collect(outcome.result, 0);
|
|
1905
|
+
target.replaceChildren(
|
|
1906
|
+
pre,
|
|
1907
|
+
...images.map((src) => {
|
|
1908
|
+
const img = document.createElement("img");
|
|
1909
|
+
img.className = "htest__shot";
|
|
1910
|
+
img.src = src;
|
|
1911
|
+
img.alt = "image in the tool result";
|
|
1912
|
+
return img;
|
|
1913
|
+
})
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
async function runTool() {
|
|
1917
|
+
let input;
|
|
1918
|
+
try {
|
|
1919
|
+
const parsed = JSON.parse(toolInput.value || "{}");
|
|
1920
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("The input must be a JSON object.");
|
|
1921
|
+
input = parsed;
|
|
1922
|
+
} catch (error) {
|
|
1923
|
+
const pre = document.createElement("pre");
|
|
1924
|
+
pre.className = "htest__error";
|
|
1925
|
+
pre.textContent = error instanceof Error ? error.message : String(error);
|
|
1926
|
+
toolResult.replaceChildren(pre);
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
callButton.disabled = true;
|
|
1930
|
+
try {
|
|
1931
|
+
await testHost.agent.call(toolSelect.value, input);
|
|
1932
|
+
const last = testHost.agent.calls().at(-1);
|
|
1933
|
+
if (last) showOutcome(toolResult, last);
|
|
1934
|
+
} finally {
|
|
1935
|
+
callButton.disabled = false;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
function download(name, mimeType, data) {
|
|
1939
|
+
const link = document.createElement("a");
|
|
1940
|
+
link.textContent = name;
|
|
1941
|
+
link.download = name;
|
|
1942
|
+
link.href = URL.createObjectURL(new Blob([data.slice().buffer], { type: mimeType }));
|
|
1943
|
+
return link;
|
|
1944
|
+
}
|
|
1945
|
+
const openCalls = /* @__PURE__ */ new Set();
|
|
1946
|
+
function renderAgent() {
|
|
1947
|
+
const agent = testHost.agent;
|
|
1948
|
+
const tools = agent.tools();
|
|
1949
|
+
agentState.textContent = `prefix "${agent.prefix}" \xB7 ${tools.length} tool(s) \xB7 ${agent.isEnabled() ? "access on" : "access off"}`;
|
|
1950
|
+
agentEnabledBox.checked = agent.isEnabled();
|
|
1951
|
+
agentTools.replaceChildren(
|
|
1952
|
+
...tools.map((tool) => {
|
|
1953
|
+
const li = document.createElement("li");
|
|
1954
|
+
const details = document.createElement("details");
|
|
1955
|
+
const summary = document.createElement("summary");
|
|
1956
|
+
const name = document.createElement("span");
|
|
1957
|
+
name.textContent = `${agent.prefix}_${tool.name}`;
|
|
1958
|
+
summary.append(name);
|
|
1959
|
+
const hints = tool.annotations ?? {};
|
|
1960
|
+
if (hints.readOnlyHint) summary.append(Object.assign(document.createElement("span"), { className: "htest__tag", textContent: "read-only" }));
|
|
1961
|
+
if (hints.consequentialHint) summary.append(Object.assign(document.createElement("span"), { className: "htest__tag htest__tag--warn", textContent: "asks first" }));
|
|
1962
|
+
const description = document.createElement("p");
|
|
1963
|
+
description.textContent = tool.description;
|
|
1964
|
+
const schema = document.createElement("pre");
|
|
1965
|
+
schema.textContent = previewJson(tool.inputSchema, 400, 60, 10);
|
|
1966
|
+
details.append(summary, description, schema);
|
|
1967
|
+
li.append(details);
|
|
1968
|
+
return li;
|
|
1969
|
+
})
|
|
1970
|
+
);
|
|
1971
|
+
const selected = toolSelect.value;
|
|
1972
|
+
toolSelect.replaceChildren(...tools.map((tool) => new Option(`${agent.prefix}_${tool.name}`, tool.name)));
|
|
1973
|
+
if (tools.some((tool) => tool.name === selected)) toolSelect.value = selected;
|
|
1974
|
+
callButton.disabled = tools.length === 0;
|
|
1975
|
+
agentContext.textContent = previewJson(agent.context(), 400, 30, 6);
|
|
1976
|
+
agentFiles.replaceChildren(
|
|
1977
|
+
...agent.files().map((file) => {
|
|
1978
|
+
const li = document.createElement("li");
|
|
1979
|
+
const ref = document.createElement("code");
|
|
1980
|
+
ref.textContent = file.fileRef;
|
|
1981
|
+
const copy = button("copy ref", () => void navigator.clipboard?.writeText(file.fileRef));
|
|
1982
|
+
li.append(
|
|
1983
|
+
file.source === "app" ? "from app: " : "for app: ",
|
|
1984
|
+
file.source === "app" ? download(file.name, file.mimeType, file.data) : file.name,
|
|
1985
|
+
` (${file.data.length} bytes) `,
|
|
1986
|
+
ref,
|
|
1987
|
+
" ",
|
|
1988
|
+
copy
|
|
1989
|
+
);
|
|
1990
|
+
return li;
|
|
1991
|
+
})
|
|
1992
|
+
);
|
|
1993
|
+
agentCalls.replaceChildren(
|
|
1994
|
+
...agent.calls().slice(-20).reverse().map((call) => {
|
|
1995
|
+
const li = document.createElement("li");
|
|
1996
|
+
const details = document.createElement("details");
|
|
1997
|
+
const summary = document.createElement("summary");
|
|
1998
|
+
const label = document.createElement("span");
|
|
1999
|
+
label.textContent = call.tool;
|
|
2000
|
+
if (call.outcome && !call.outcome.ok) label.className = "htest__error";
|
|
2001
|
+
const meta = document.createElement("span");
|
|
2002
|
+
meta.className = "htest__meta";
|
|
2003
|
+
meta.textContent = call.outcome ? `${call.outcome.durationMs} ms` : "\u2026";
|
|
2004
|
+
summary.append(label, meta);
|
|
2005
|
+
const body = document.createElement("div");
|
|
2006
|
+
const input = document.createElement("pre");
|
|
2007
|
+
input.textContent = previewJson(call.input);
|
|
2008
|
+
details.append(summary, body);
|
|
2009
|
+
details.addEventListener("toggle", () => {
|
|
2010
|
+
if (details.open) {
|
|
2011
|
+
openCalls.add(call.callId);
|
|
2012
|
+
const out = document.createElement("div");
|
|
2013
|
+
showOutcome(out, call);
|
|
2014
|
+
body.replaceChildren(input, out);
|
|
2015
|
+
} else openCalls.delete(call.callId);
|
|
2016
|
+
});
|
|
2017
|
+
if (openCalls.has(call.callId)) details.open = true;
|
|
2018
|
+
li.append(details);
|
|
2019
|
+
return li;
|
|
2020
|
+
})
|
|
2021
|
+
);
|
|
2022
|
+
}
|
|
2023
|
+
const callRow = document.createElement("div");
|
|
2024
|
+
callRow.className = "htest__row";
|
|
2025
|
+
callRow.append(toolSelect, callButton);
|
|
2026
|
+
section(
|
|
2027
|
+
"Agent tools",
|
|
2028
|
+
agentState,
|
|
2029
|
+
agentEnabled,
|
|
2030
|
+
agentTools,
|
|
2031
|
+
callRow,
|
|
2032
|
+
toolInput,
|
|
2033
|
+
toolResult,
|
|
2034
|
+
hint("Calls a tool as an agent would; tools marked \u201Casks first\u201D go through a confirm, like Haven's consent dialog.")
|
|
2035
|
+
);
|
|
2036
|
+
section(
|
|
2037
|
+
"Agent files",
|
|
2038
|
+
importInput,
|
|
2039
|
+
hint("Import puts a file in the exchange for the app (takeFile); pass its fileRef to a tool. Files the app hands over (provideFile) can be downloaded."),
|
|
2040
|
+
agentFiles
|
|
2041
|
+
);
|
|
2042
|
+
section("Agent context", agentContext);
|
|
2043
|
+
section("Agent calls (latest first)", agentCalls);
|
|
1577
2044
|
section("Notifications", lists.notifications);
|
|
1578
2045
|
section("Previews", lists.previews);
|
|
1579
2046
|
section("Scans", lists.scans);
|
|
1580
2047
|
section("Requests (latest first, click for details)", lists.requests);
|
|
1581
2048
|
render();
|
|
2049
|
+
agentReady = true;
|
|
2050
|
+
renderAgent();
|
|
1582
2051
|
syncUrl();
|
|
1583
2052
|
window.__havenTestHost = testHost;
|
|
1584
2053
|
return testHost;
|
|
@@ -1882,23 +2351,23 @@ function mergeLaunchContext(current, patch) {
|
|
|
1882
2351
|
...database,
|
|
1883
2352
|
capabilities: [...database.capabilities]
|
|
1884
2353
|
})),
|
|
1885
|
-
views: current.views.map((
|
|
1886
|
-
...
|
|
1887
|
-
sources:
|
|
1888
|
-
columns:
|
|
2354
|
+
views: current.views.map((view2) => ({
|
|
2355
|
+
...view2,
|
|
2356
|
+
sources: view2.sources.map((source) => ({ ...source })),
|
|
2357
|
+
columns: view2.columns.map((column) => ({
|
|
1889
2358
|
...column,
|
|
1890
2359
|
expression: column.expression.mode === "field" ? { ...column.expression } : {
|
|
1891
2360
|
mode: "formula",
|
|
1892
2361
|
expression: structuredClone(column.expression.expression)
|
|
1893
2362
|
}
|
|
1894
2363
|
})),
|
|
1895
|
-
filter:
|
|
2364
|
+
filter: view2.filter.mode === "rules" ? {
|
|
1896
2365
|
mode: "rules",
|
|
1897
|
-
match:
|
|
1898
|
-
rules:
|
|
2366
|
+
match: view2.filter.match,
|
|
2367
|
+
rules: view2.filter.rules.map((rule) => ({ ...rule }))
|
|
1899
2368
|
} : {
|
|
1900
2369
|
mode: "formula",
|
|
1901
|
-
expression: structuredClone(
|
|
2370
|
+
expression: structuredClone(view2.filter.expression)
|
|
1902
2371
|
}
|
|
1903
2372
|
}))
|
|
1904
2373
|
};
|
|
@@ -1920,41 +2389,41 @@ function mergeLaunchContext(current, patch) {
|
|
|
1920
2389
|
...database,
|
|
1921
2390
|
capabilities: [...database.capabilities]
|
|
1922
2391
|
})),
|
|
1923
|
-
views: patch.views ? patch.views.map((
|
|
1924
|
-
...
|
|
1925
|
-
sources:
|
|
1926
|
-
columns:
|
|
2392
|
+
views: patch.views ? patch.views.map((view2) => ({
|
|
2393
|
+
...view2,
|
|
2394
|
+
sources: view2.sources.map((source) => ({ ...source })),
|
|
2395
|
+
columns: view2.columns.map((column) => ({
|
|
1927
2396
|
...column,
|
|
1928
2397
|
expression: column.expression.mode === "field" ? { ...column.expression } : {
|
|
1929
2398
|
mode: "formula",
|
|
1930
2399
|
expression: structuredClone(column.expression.expression)
|
|
1931
2400
|
}
|
|
1932
2401
|
})),
|
|
1933
|
-
filter:
|
|
2402
|
+
filter: view2.filter.mode === "rules" ? {
|
|
1934
2403
|
mode: "rules",
|
|
1935
|
-
match:
|
|
1936
|
-
rules:
|
|
2404
|
+
match: view2.filter.match,
|
|
2405
|
+
rules: view2.filter.rules.map((rule) => ({ ...rule }))
|
|
1937
2406
|
} : {
|
|
1938
2407
|
mode: "formula",
|
|
1939
|
-
expression: structuredClone(
|
|
2408
|
+
expression: structuredClone(view2.filter.expression)
|
|
1940
2409
|
}
|
|
1941
|
-
})) : current.views.map((
|
|
1942
|
-
...
|
|
1943
|
-
sources:
|
|
1944
|
-
columns:
|
|
2410
|
+
})) : current.views.map((view2) => ({
|
|
2411
|
+
...view2,
|
|
2412
|
+
sources: view2.sources.map((source) => ({ ...source })),
|
|
2413
|
+
columns: view2.columns.map((column) => ({
|
|
1945
2414
|
...column,
|
|
1946
2415
|
expression: column.expression.mode === "field" ? { ...column.expression } : {
|
|
1947
2416
|
mode: "formula",
|
|
1948
2417
|
expression: structuredClone(column.expression.expression)
|
|
1949
2418
|
}
|
|
1950
2419
|
})),
|
|
1951
|
-
filter:
|
|
2420
|
+
filter: view2.filter.mode === "rules" ? {
|
|
1952
2421
|
mode: "rules",
|
|
1953
|
-
match:
|
|
1954
|
-
rules:
|
|
2422
|
+
match: view2.filter.match,
|
|
2423
|
+
rules: view2.filter.rules.map((rule) => ({ ...rule }))
|
|
1955
2424
|
} : {
|
|
1956
2425
|
mode: "formula",
|
|
1957
|
-
expression: structuredClone(
|
|
2426
|
+
expression: structuredClone(view2.filter.expression)
|
|
1958
2427
|
}
|
|
1959
2428
|
}))
|
|
1960
2429
|
};
|
|
@@ -2045,9 +2514,12 @@ function mockDecryptionKeyId(document2) {
|
|
|
2045
2514
|
}
|
|
2046
2515
|
return { decryptionKeyId: document2.decryptionKeyId };
|
|
2047
2516
|
}
|
|
2048
|
-
function loadMockAutomergeDocument(document2) {
|
|
2517
|
+
function loadMockAutomergeDocument(document2, actor) {
|
|
2049
2518
|
if (document2.automergeBinary) {
|
|
2050
|
-
return Automerge.load(
|
|
2519
|
+
return Automerge.load(
|
|
2520
|
+
document2.automergeBinary,
|
|
2521
|
+
actor ? { actor } : void 0
|
|
2522
|
+
);
|
|
2051
2523
|
}
|
|
2052
2524
|
const initialDoc = Automerge.from({
|
|
2053
2525
|
...structuredClone(document2.data),
|
|
@@ -2058,9 +2530,401 @@ function loadMockAutomergeDocument(document2) {
|
|
|
2058
2530
|
}
|
|
2059
2531
|
function persistMockAutomergeDocument(document2, automergeDoc) {
|
|
2060
2532
|
document2.automergeBinary = Automerge.save(automergeDoc);
|
|
2061
|
-
document2.data =
|
|
2533
|
+
document2.data = projectMockAutomergeValue(
|
|
2534
|
+
Automerge.load(document2.automergeBinary)
|
|
2535
|
+
);
|
|
2062
2536
|
document2.heads = Automerge.getHeads(automergeDoc);
|
|
2063
2537
|
}
|
|
2538
|
+
var MOCK_AUTOMERGE_SEED_ACTOR = "5eed0000000000000000000000000000";
|
|
2539
|
+
var MOCK_AUTOMERGE_LOCAL_ACTOR = "10ca1000000000000000000000000000";
|
|
2540
|
+
var MOCK_AUTOMERGE_REMOTE_ACTOR = "2e307e00000000000000000000000000";
|
|
2541
|
+
function projectMockAutomergeValue(value, depth = 0) {
|
|
2542
|
+
if (value === null || typeof value !== "object") {
|
|
2543
|
+
return value;
|
|
2544
|
+
}
|
|
2545
|
+
if (Automerge.isCounter(value)) {
|
|
2546
|
+
return value.value;
|
|
2547
|
+
}
|
|
2548
|
+
if (Automerge.isImmutableString(value)) {
|
|
2549
|
+
return String(value);
|
|
2550
|
+
}
|
|
2551
|
+
if (value instanceof Date) {
|
|
2552
|
+
return value.toISOString();
|
|
2553
|
+
}
|
|
2554
|
+
if (value instanceof Uint8Array) {
|
|
2555
|
+
return value.slice();
|
|
2556
|
+
}
|
|
2557
|
+
if (depth > 100) {
|
|
2558
|
+
throw new Error("Document value is nested too deeply");
|
|
2559
|
+
}
|
|
2560
|
+
if (Array.isArray(value)) {
|
|
2561
|
+
return value.map((entry) => projectMockAutomergeValue(entry, depth + 1));
|
|
2562
|
+
}
|
|
2563
|
+
return Object.fromEntries(
|
|
2564
|
+
Object.entries(value).map(([key, entry]) => [
|
|
2565
|
+
key,
|
|
2566
|
+
projectMockAutomergeValue(entry, depth + 1)
|
|
2567
|
+
])
|
|
2568
|
+
);
|
|
2569
|
+
}
|
|
2570
|
+
function hydrateMockAutomergeValues(value, depth = 0) {
|
|
2571
|
+
if (value === null || typeof value !== "object") {
|
|
2572
|
+
return value;
|
|
2573
|
+
}
|
|
2574
|
+
if (depth > 100) {
|
|
2575
|
+
throw new Error("Document value is nested too deeply");
|
|
2576
|
+
}
|
|
2577
|
+
if (value instanceof Uint8Array || value instanceof Date || Automerge.isCounter(value) || Automerge.isImmutableString(value)) {
|
|
2578
|
+
return value;
|
|
2579
|
+
}
|
|
2580
|
+
if (Array.isArray(value)) {
|
|
2581
|
+
return value.map(
|
|
2582
|
+
(entry) => entry === void 0 ? null : hydrateMockAutomergeValues(entry, depth + 1)
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
const record = value;
|
|
2586
|
+
if (Object.prototype.hasOwnProperty.call(record, MINDOODB_APP_VALUE_TAG)) {
|
|
2587
|
+
switch (record[MINDOODB_APP_VALUE_TAG]) {
|
|
2588
|
+
case "atomic":
|
|
2589
|
+
if (typeof record.value !== "string") {
|
|
2590
|
+
throw new Error('Typed value "atomic" needs a string value');
|
|
2591
|
+
}
|
|
2592
|
+
return new Automerge.ImmutableString(record.value);
|
|
2593
|
+
case "counter":
|
|
2594
|
+
assertMockCounterAmount(record.value, 'Typed value "counter"');
|
|
2595
|
+
return new Automerge.Counter(record.value);
|
|
2596
|
+
case "timestamp":
|
|
2597
|
+
return new Date(toMockTimestamp(record.value));
|
|
2598
|
+
default:
|
|
2599
|
+
throw new Error(
|
|
2600
|
+
`Unknown typed value ${JSON.stringify(record[MINDOODB_APP_VALUE_TAG])}; the "${MINDOODB_APP_VALUE_TAG}" key is reserved`
|
|
2601
|
+
);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
const result = {};
|
|
2605
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
2606
|
+
if (entry !== void 0) {
|
|
2607
|
+
result[key] = hydrateMockAutomergeValues(entry, depth + 1);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
return result;
|
|
2611
|
+
}
|
|
2612
|
+
function createMockAutomergeDocument(values, actor, time) {
|
|
2613
|
+
const entries = Object.entries(values).filter(([, value]) => value !== void 0);
|
|
2614
|
+
const initial = Automerge.init({ actor });
|
|
2615
|
+
const options = time === void 0 ? {} : { time };
|
|
2616
|
+
if (entries.length === 0) {
|
|
2617
|
+
return Automerge.emptyChange(initial, options);
|
|
2618
|
+
}
|
|
2619
|
+
return Automerge.change(initial, options, (doc) => {
|
|
2620
|
+
for (const [key, value] of entries) {
|
|
2621
|
+
doc[key] = hydrateMockAutomergeValues(value);
|
|
2622
|
+
}
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2625
|
+
function validateMockJsonPath(path, label) {
|
|
2626
|
+
if (!Array.isArray(path) || path.length === 0) {
|
|
2627
|
+
throw new Error(`${label} path must contain at least one segment`);
|
|
2628
|
+
}
|
|
2629
|
+
for (const segment of path) {
|
|
2630
|
+
if (typeof segment !== "string" && typeof segment !== "number") {
|
|
2631
|
+
throw new Error(`${label} path segments must be strings or numbers`);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
function assertMockNonNegativeInteger(value, label) {
|
|
2636
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
2637
|
+
throw new Error(`${label} must be a non-negative integer`);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
function validateMockJsonPatch(patch) {
|
|
2641
|
+
const operationCount = (patch.set?.length ?? 0) + (patch.unset?.length ?? 0) + (patch.listDelete?.length ?? 0) + (patch.listInsert?.length ?? 0) + (patch.textSplice?.length ?? 0) + (patch.textMark?.length ?? 0) + (patch.textUnmark?.length ?? 0) + (patch.counterIncrement?.length ?? 0);
|
|
2642
|
+
if (operationCount === 0) {
|
|
2643
|
+
throw new Error("JSON patch must include at least one operation");
|
|
2644
|
+
}
|
|
2645
|
+
for (const operation of patch.set ?? []) {
|
|
2646
|
+
validateMockJsonPath(operation.path, "JSON set");
|
|
2647
|
+
hydrateMockAutomergeValues(operation.value);
|
|
2648
|
+
}
|
|
2649
|
+
for (const operation of patch.unset ?? []) {
|
|
2650
|
+
validateMockJsonPath(operation.path, "JSON unset");
|
|
2651
|
+
}
|
|
2652
|
+
for (const operation of patch.listDelete ?? []) {
|
|
2653
|
+
validateMockJsonPath(operation.path, "JSON listDelete");
|
|
2654
|
+
assertMockNonNegativeInteger(operation.index, "JSON listDelete index");
|
|
2655
|
+
assertMockNonNegativeInteger(operation.deleteCount, "JSON listDelete deleteCount");
|
|
2656
|
+
}
|
|
2657
|
+
for (const operation of patch.listInsert ?? []) {
|
|
2658
|
+
validateMockJsonPath(operation.path, "JSON listInsert");
|
|
2659
|
+
assertMockNonNegativeInteger(operation.index, "JSON listInsert index");
|
|
2660
|
+
if (!Array.isArray(operation.values)) {
|
|
2661
|
+
throw new Error("JSON listInsert values must be an array");
|
|
2662
|
+
}
|
|
2663
|
+
hydrateMockAutomergeValues(operation.values);
|
|
2664
|
+
}
|
|
2665
|
+
for (const operation of patch.textSplice ?? []) {
|
|
2666
|
+
validateMockJsonPath(operation.path, "JSON textSplice");
|
|
2667
|
+
assertMockNonNegativeInteger(operation.index, "JSON textSplice index");
|
|
2668
|
+
assertMockNonNegativeInteger(operation.deleteCount, "JSON textSplice deleteCount");
|
|
2669
|
+
if (operation.insert !== void 0 && typeof operation.insert !== "string") {
|
|
2670
|
+
throw new Error("JSON textSplice insert must be a string");
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
for (const operation of [...patch.textMark ?? [], ...patch.textUnmark ?? []]) {
|
|
2674
|
+
validateMockJsonPath(operation.path, "JSON text mark");
|
|
2675
|
+
assertMockNonNegativeInteger(operation.index, "JSON text mark index");
|
|
2676
|
+
if (!Number.isInteger(operation.length) || operation.length <= 0) {
|
|
2677
|
+
throw new Error("JSON text mark length must be a positive integer");
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
for (const operation of patch.counterIncrement ?? []) {
|
|
2681
|
+
validateMockJsonPath(operation.path, "JSON counterIncrement");
|
|
2682
|
+
assertMockCounterAmount(operation.delta, "JSON counterIncrement delta");
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
function ensureMockAutomergeParent(target, path) {
|
|
2686
|
+
let parent = target;
|
|
2687
|
+
for (let index = 0; index < path.length - 1; index += 1) {
|
|
2688
|
+
const segment = path[index];
|
|
2689
|
+
const nextSegment = path[index + 1];
|
|
2690
|
+
if (parent[segment] === void 0 || parent[segment] === null) {
|
|
2691
|
+
parent[segment] = typeof nextSegment === "number" ? [] : {};
|
|
2692
|
+
}
|
|
2693
|
+
parent = parent[segment];
|
|
2694
|
+
if (parent === null || typeof parent !== "object") {
|
|
2695
|
+
throw new Error(
|
|
2696
|
+
`Cannot apply JSON patch through non-object path segment '${String(segment)}'`
|
|
2697
|
+
);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
return parent;
|
|
2701
|
+
}
|
|
2702
|
+
function readMockAutomergeValue(target, path) {
|
|
2703
|
+
let value = target;
|
|
2704
|
+
for (const segment of path) {
|
|
2705
|
+
value = value?.[segment];
|
|
2706
|
+
}
|
|
2707
|
+
return value;
|
|
2708
|
+
}
|
|
2709
|
+
function setMockAutomergeValue(target, path, value) {
|
|
2710
|
+
const parent = ensureMockAutomergeParent(target, path);
|
|
2711
|
+
parent[path[path.length - 1]] = value;
|
|
2712
|
+
}
|
|
2713
|
+
function unsetMockAutomergeValue(target, path) {
|
|
2714
|
+
let parent = target;
|
|
2715
|
+
for (let index = 0; index < path.length - 1; index += 1) {
|
|
2716
|
+
parent = parent?.[path[index]];
|
|
2717
|
+
if (parent === null || typeof parent !== "object") {
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
delete parent[path[path.length - 1]];
|
|
2722
|
+
}
|
|
2723
|
+
function readMockAutomergeList(target, path) {
|
|
2724
|
+
const value = readMockAutomergeValue(target, path);
|
|
2725
|
+
if (!Array.isArray(value)) {
|
|
2726
|
+
throw new Error(
|
|
2727
|
+
`Cannot apply JSON list operation to non-array value at '${path.map(String).join(".")}'`
|
|
2728
|
+
);
|
|
2729
|
+
}
|
|
2730
|
+
return value;
|
|
2731
|
+
}
|
|
2732
|
+
function ensureMockAutomergeList(target, path) {
|
|
2733
|
+
const parent = ensureMockAutomergeParent(target, path);
|
|
2734
|
+
const leaf = path[path.length - 1];
|
|
2735
|
+
if (parent[leaf] === void 0 || parent[leaf] === null) {
|
|
2736
|
+
parent[leaf] = [];
|
|
2737
|
+
}
|
|
2738
|
+
return readMockAutomergeList(target, path);
|
|
2739
|
+
}
|
|
2740
|
+
function clampMockIndex(index, length) {
|
|
2741
|
+
if (!Number.isFinite(index)) {
|
|
2742
|
+
return length;
|
|
2743
|
+
}
|
|
2744
|
+
return Math.max(0, Math.min(Math.trunc(index), length));
|
|
2745
|
+
}
|
|
2746
|
+
function ensureMockAutomergeText(target, path, label) {
|
|
2747
|
+
const value = readMockAutomergeValue(target, path);
|
|
2748
|
+
if (value === void 0 || value === null) {
|
|
2749
|
+
setMockAutomergeValue(target, path, "");
|
|
2750
|
+
} else if (typeof value !== "string") {
|
|
2751
|
+
throw new Error(
|
|
2752
|
+
`Cannot apply ${label} to non-string value at '${path.map(String).join(".")}'`
|
|
2753
|
+
);
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
function spliceMockAutomergeText(target, path, index, deleteCount, insert) {
|
|
2757
|
+
const text = readMockAutomergeValue(target, path);
|
|
2758
|
+
const length = typeof text === "string" ? text.length : 0;
|
|
2759
|
+
const start = clampMockIndex(index, length);
|
|
2760
|
+
Automerge.splice(
|
|
2761
|
+
target,
|
|
2762
|
+
path,
|
|
2763
|
+
start,
|
|
2764
|
+
Math.max(0, Math.min(deleteCount, length - start)),
|
|
2765
|
+
insert
|
|
2766
|
+
);
|
|
2767
|
+
}
|
|
2768
|
+
function reviveMockRichTextValue(value) {
|
|
2769
|
+
if (Array.isArray(value)) {
|
|
2770
|
+
return value.map((entry) => reviveMockRichTextValue(entry));
|
|
2771
|
+
}
|
|
2772
|
+
if (!value || typeof value !== "object") {
|
|
2773
|
+
return value;
|
|
2774
|
+
}
|
|
2775
|
+
const record = value;
|
|
2776
|
+
if (record.type === "immutableString" && typeof record.value === "string") {
|
|
2777
|
+
return new Automerge.ImmutableString(record.value);
|
|
2778
|
+
}
|
|
2779
|
+
return Object.fromEntries(
|
|
2780
|
+
Object.entries(record).map(([key, entry]) => [key, reviveMockRichTextValue(entry)])
|
|
2781
|
+
);
|
|
2782
|
+
}
|
|
2783
|
+
function applyMockAutomergeJsonOperations(target, patch) {
|
|
2784
|
+
for (const operation of patch.set ?? []) {
|
|
2785
|
+
setMockAutomergeValue(target, operation.path, hydrateMockAutomergeValues(operation.value));
|
|
2786
|
+
}
|
|
2787
|
+
for (const operation of patch.unset ?? []) {
|
|
2788
|
+
unsetMockAutomergeValue(target, operation.path);
|
|
2789
|
+
}
|
|
2790
|
+
for (const operation of patch.listDelete ?? []) {
|
|
2791
|
+
readMockAutomergeList(target, operation.path).splice(operation.index, operation.deleteCount);
|
|
2792
|
+
}
|
|
2793
|
+
for (const operation of patch.listInsert ?? []) {
|
|
2794
|
+
ensureMockAutomergeList(target, operation.path).splice(
|
|
2795
|
+
operation.index,
|
|
2796
|
+
0,
|
|
2797
|
+
...hydrateMockAutomergeValues(operation.values)
|
|
2798
|
+
);
|
|
2799
|
+
}
|
|
2800
|
+
for (const operation of patch.textSplice ?? []) {
|
|
2801
|
+
ensureMockAutomergeText(target, operation.path, "JSON textSplice");
|
|
2802
|
+
spliceMockAutomergeText(
|
|
2803
|
+
target,
|
|
2804
|
+
operation.path,
|
|
2805
|
+
operation.index,
|
|
2806
|
+
operation.deleteCount,
|
|
2807
|
+
operation.insert ?? ""
|
|
2808
|
+
);
|
|
2809
|
+
}
|
|
2810
|
+
for (const operation of patch.textMark ?? []) {
|
|
2811
|
+
for (const [name, value] of Object.entries(operation.marks)) {
|
|
2812
|
+
Automerge.mark(
|
|
2813
|
+
target,
|
|
2814
|
+
operation.path,
|
|
2815
|
+
{ start: operation.index, end: operation.index + operation.length, expand: "none" },
|
|
2816
|
+
name,
|
|
2817
|
+
reviveMockRichTextValue(value)
|
|
2818
|
+
);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
for (const operation of patch.textUnmark ?? []) {
|
|
2822
|
+
for (const name of operation.names) {
|
|
2823
|
+
Automerge.unmark(
|
|
2824
|
+
target,
|
|
2825
|
+
operation.path,
|
|
2826
|
+
{ start: operation.index, end: operation.index + operation.length, expand: "none" },
|
|
2827
|
+
name
|
|
2828
|
+
);
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
for (const operation of patch.counterIncrement ?? []) {
|
|
2832
|
+
const parent = ensureMockAutomergeParent(target, operation.path);
|
|
2833
|
+
const leaf = operation.path[operation.path.length - 1];
|
|
2834
|
+
const existing = parent[leaf];
|
|
2835
|
+
if (existing === void 0 || existing === null) {
|
|
2836
|
+
parent[leaf] = new Automerge.Counter(operation.delta);
|
|
2837
|
+
} else if (Automerge.isCounter(existing)) {
|
|
2838
|
+
existing.increment(operation.delta);
|
|
2839
|
+
} else {
|
|
2840
|
+
throw new Error(
|
|
2841
|
+
`Cannot increment non-counter value at '${operation.path.map(String).join(".")}'; create it with MindooValue.counter() first`
|
|
2842
|
+
);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
function changeMockAutomergeDocument(doc, baseHeads, apply) {
|
|
2847
|
+
if (baseHeads && baseHeads.length > 0) {
|
|
2848
|
+
const { newDoc } = Automerge.changeAt(doc, baseHeads, apply);
|
|
2849
|
+
return Automerge.load(Automerge.save(newDoc), { actor: Automerge.getActorId(doc) });
|
|
2850
|
+
}
|
|
2851
|
+
return Automerge.change(doc, apply);
|
|
2852
|
+
}
|
|
2853
|
+
function applyMockAutomergeUpdate(doc, patch) {
|
|
2854
|
+
let next = doc;
|
|
2855
|
+
if (patch.set || (patch.unset?.length ?? 0) > 0) {
|
|
2856
|
+
next = Automerge.change(next, (target) => {
|
|
2857
|
+
for (const [key, value] of Object.entries(patch.set ?? {})) {
|
|
2858
|
+
if (value !== void 0) {
|
|
2859
|
+
target[key] = hydrateMockAutomergeValues(value);
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
for (const key of patch.unset ?? []) {
|
|
2863
|
+
delete target[key];
|
|
2864
|
+
}
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2867
|
+
if (patch.json) {
|
|
2868
|
+
const json = patch.json;
|
|
2869
|
+
validateMockJsonPatch(json);
|
|
2870
|
+
next = changeMockAutomergeDocument(
|
|
2871
|
+
next,
|
|
2872
|
+
json.baseHeads,
|
|
2873
|
+
(target) => applyMockAutomergeJsonOperations(target, json)
|
|
2874
|
+
);
|
|
2875
|
+
}
|
|
2876
|
+
for (const textPatch of patch.text ?? []) {
|
|
2877
|
+
validateMockJsonPath(textPatch.path, "Text patch");
|
|
2878
|
+
next = changeMockAutomergeDocument(next, textPatch.baseHeads, (target) => {
|
|
2879
|
+
ensureMockAutomergeText(target, textPatch.path, "text patch");
|
|
2880
|
+
for (const edit of textPatch.edits) {
|
|
2881
|
+
spliceMockAutomergeText(target, textPatch.path, edit.index, edit.deleteCount, edit.insert ?? "");
|
|
2882
|
+
}
|
|
2883
|
+
});
|
|
2884
|
+
}
|
|
2885
|
+
for (const richTextPatch of patch.richText ?? []) {
|
|
2886
|
+
validateMockJsonPath(richTextPatch.path, "Rich-text patch");
|
|
2887
|
+
const spans = richTextPatch.spansSequence?.at(-1) ?? richTextPatch.spans ?? [];
|
|
2888
|
+
next = Automerge.change(next, (target) => {
|
|
2889
|
+
setMockAutomergeValue(target, richTextPatch.path, hydrateMockAutomergeValues(structuredClone(spans)));
|
|
2890
|
+
});
|
|
2891
|
+
}
|
|
2892
|
+
for (const stepPatch of patch.richTextSteps ?? []) {
|
|
2893
|
+
validateMockJsonPath(stepPatch.path, "Rich-text steps patch");
|
|
2894
|
+
next = changeMockAutomergeDocument(next, stepPatch.baseHeads, (target) => {
|
|
2895
|
+
const current = readMockAutomergeValue(target, stepPatch.path);
|
|
2896
|
+
if (current !== void 0 && current !== null && typeof current !== "string") {
|
|
2897
|
+
setMockAutomergeValue(
|
|
2898
|
+
target,
|
|
2899
|
+
stepPatch.path,
|
|
2900
|
+
materializeMockRichTextValue(projectMockAutomergeValue(current))
|
|
2901
|
+
);
|
|
2902
|
+
}
|
|
2903
|
+
ensureMockAutomergeText(target, stepPatch.path, "rich-text steps");
|
|
2904
|
+
for (const step of stepPatch.steps) {
|
|
2905
|
+
spliceMockAutomergeText(target, stepPatch.path, step.index, step.deleteCount, step.insert ?? "");
|
|
2906
|
+
}
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
return next;
|
|
2910
|
+
}
|
|
2911
|
+
function forkMockAutomergeDocument(doc, baseHeads, actor) {
|
|
2912
|
+
if (!baseHeads || baseHeads.length === 0) {
|
|
2913
|
+
return Automerge.clone(doc, { actor });
|
|
2914
|
+
}
|
|
2915
|
+
const heads = new Set(baseHeads);
|
|
2916
|
+
let ownLatest;
|
|
2917
|
+
for (const change2 of Automerge.getAllChanges(doc)) {
|
|
2918
|
+
const decoded = Automerge.decodeChange(change2);
|
|
2919
|
+
if (decoded.actor === actor) {
|
|
2920
|
+
ownLatest = decoded.hash;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
if (ownLatest) {
|
|
2924
|
+
heads.add(ownLatest);
|
|
2925
|
+
}
|
|
2926
|
+
return Automerge.clone(Automerge.view(doc, [...heads]), { actor });
|
|
2927
|
+
}
|
|
2064
2928
|
function getFieldValue2(source, field) {
|
|
2065
2929
|
return field.split(".").reduce((current, part) => {
|
|
2066
2930
|
if (current && typeof current === "object" && part in current) {
|
|
@@ -2373,9 +3237,9 @@ function createLiveMockViewNavigator(initial, rebuild, subscribe) {
|
|
|
2373
3237
|
listeners.clear();
|
|
2374
3238
|
if (pendingNotify) clearTimeout(pendingNotify);
|
|
2375
3239
|
}
|
|
2376
|
-
const
|
|
2377
|
-
const member =
|
|
2378
|
-
return typeof member === "function" ? member.apply(
|
|
3240
|
+
const navigator2 = await current();
|
|
3241
|
+
const member = navigator2[property];
|
|
3242
|
+
return typeof member === "function" ? member.apply(navigator2, args) : member;
|
|
2379
3243
|
};
|
|
2380
3244
|
}
|
|
2381
3245
|
});
|
|
@@ -2913,9 +3777,17 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
2913
3777
|
let createCounter = 0;
|
|
2914
3778
|
let changeCounter = 0;
|
|
2915
3779
|
const defaultViewFactory = async () => createDefaultViewNavigator();
|
|
2916
|
-
const
|
|
3780
|
+
const automergeMode = definition.automerge === true;
|
|
3781
|
+
let changeSequence = 0;
|
|
3782
|
+
const changedAt = /* @__PURE__ */ new Map();
|
|
3783
|
+
const storedDocuments = new class extends Map {
|
|
3784
|
+
set(id, document2) {
|
|
3785
|
+
changedAt.set(id, ++changeSequence);
|
|
3786
|
+
return super.set(id, document2);
|
|
3787
|
+
}
|
|
3788
|
+
}();
|
|
2917
3789
|
for (const seed of definition.documents ?? []) {
|
|
2918
|
-
|
|
3790
|
+
const stored = {
|
|
2919
3791
|
id: seed.id,
|
|
2920
3792
|
data: structuredClone(seed.data),
|
|
2921
3793
|
heads: seed.heads ? [...seed.heads] : void 0,
|
|
@@ -2926,7 +3798,14 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
2926
3798
|
decryptionKeyId: seed.decryptionKeyId,
|
|
2927
3799
|
authorLabel: seed.authorLabel,
|
|
2928
3800
|
createdAt: seed.createdAt
|
|
2929
|
-
}
|
|
3801
|
+
};
|
|
3802
|
+
if (automergeMode) {
|
|
3803
|
+
persistMockAutomergeDocument(
|
|
3804
|
+
stored,
|
|
3805
|
+
createMockAutomergeDocument(seed.data, MOCK_AUTOMERGE_SEED_ACTOR, 0)
|
|
3806
|
+
);
|
|
3807
|
+
}
|
|
3808
|
+
storedDocuments.set(seed.id, stored);
|
|
2930
3809
|
}
|
|
2931
3810
|
const liveQuerySubscriptions = /* @__PURE__ */ new Set();
|
|
2932
3811
|
const refreshLiveQueries = () => {
|
|
@@ -2969,6 +3848,15 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
2969
3848
|
throw new Error("Recipient mutators require a document created with `recipients`");
|
|
2970
3849
|
}
|
|
2971
3850
|
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3851
|
+
if (automergeMode) {
|
|
3852
|
+
return writeAutomergeDocument(
|
|
3853
|
+
docId,
|
|
3854
|
+
existing,
|
|
3855
|
+
Automerge.change(loadMockAutomergeDocument(existing, MOCK_AUTOMERGE_LOCAL_ACTOR), (target) => {
|
|
3856
|
+
target._encryptFor = hydrateMockAutomergeValues(nextEncryptFor);
|
|
3857
|
+
})
|
|
3858
|
+
);
|
|
3859
|
+
}
|
|
2972
3860
|
const updated = {
|
|
2973
3861
|
id: docId,
|
|
2974
3862
|
data: { ...existing.data, _encryptFor: nextEncryptFor },
|
|
@@ -2987,6 +3875,28 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
2987
3875
|
notifyLiveQueries();
|
|
2988
3876
|
return updated;
|
|
2989
3877
|
}
|
|
3878
|
+
function writeAutomergeDocument(docId, existing, automergeDoc) {
|
|
3879
|
+
const stored = {
|
|
3880
|
+
...existing ?? {},
|
|
3881
|
+
id: docId,
|
|
3882
|
+
data: {},
|
|
3883
|
+
attachments: existing?.attachments ? structuredClone(existing.attachments) : [],
|
|
3884
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3885
|
+
isDeleted: false,
|
|
3886
|
+
decryptionKeyId: existing?.decryptionKeyId ?? "default"
|
|
3887
|
+
};
|
|
3888
|
+
persistMockAutomergeDocument(stored, automergeDoc);
|
|
3889
|
+
storedDocuments.set(docId, stored);
|
|
3890
|
+
notifyLiveQueries();
|
|
3891
|
+
return {
|
|
3892
|
+
id: docId,
|
|
3893
|
+
data: structuredClone(stored.data),
|
|
3894
|
+
heads: [...stored.heads ?? []],
|
|
3895
|
+
attachments: structuredClone(stored.attachments ?? []),
|
|
3896
|
+
updatedAt: stored.updatedAt,
|
|
3897
|
+
...mockDecryptionKeyId(stored)
|
|
3898
|
+
};
|
|
3899
|
+
}
|
|
2990
3900
|
const defaultDocuments = {
|
|
2991
3901
|
async query(query) {
|
|
2992
3902
|
return runMockDocumentQuery(
|
|
@@ -3039,12 +3949,17 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3039
3949
|
const offset = decodeMockListCursor(query?.cursor) + skip;
|
|
3040
3950
|
const metadataOnly = query?.metadataOnly ?? false;
|
|
3041
3951
|
const idPrefix = query?.idPrefix;
|
|
3042
|
-
const
|
|
3952
|
+
const after = automergeMode ? decodeMockListCursor(query?.cursor) : 0;
|
|
3953
|
+
const items = Array.from(storedDocuments.values()).filter((document2) => !automergeMode || (changedAt.get(document2.id) ?? 0) > after).filter((document2) => !document2.inaccessible).filter(
|
|
3043
3954
|
(document2) => status === "all" ? true : status === "deleted" ? document2.isDeleted : !document2.isDeleted
|
|
3044
3955
|
).filter((document2) => matchesMockIdPrefix(document2.id, idPrefix)).filter(
|
|
3045
3956
|
(document2) => metadataOnly ? true : matchesDocumentFilter(document2, query?.filter)
|
|
3046
|
-
).sort(
|
|
3047
|
-
|
|
3957
|
+
).sort(
|
|
3958
|
+
(left, right) => automergeMode ? (changedAt.get(left.id) ?? 0) - (changedAt.get(right.id) ?? 0) : left.id.localeCompare(right.id)
|
|
3959
|
+
);
|
|
3960
|
+
const pageStart = automergeMode ? skip : offset;
|
|
3961
|
+
const pageDocuments = items.slice(pageStart, pageStart + limit);
|
|
3962
|
+
const page = pageDocuments.map((document2) => {
|
|
3048
3963
|
if (metadataOnly) {
|
|
3049
3964
|
return {
|
|
3050
3965
|
id: document2.id,
|
|
@@ -3065,6 +3980,13 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3065
3980
|
isDeleted: status !== "existing" ? document2.isDeleted : void 0
|
|
3066
3981
|
};
|
|
3067
3982
|
});
|
|
3983
|
+
if (automergeMode) {
|
|
3984
|
+
const last = pageDocuments[pageDocuments.length - 1];
|
|
3985
|
+
return {
|
|
3986
|
+
items: page,
|
|
3987
|
+
nextCursor: last ? String(changedAt.get(last.id) ?? after) : null
|
|
3988
|
+
};
|
|
3989
|
+
}
|
|
3068
3990
|
const nextCursor = offset + page.length < items.length ? String(offset + page.length) : null;
|
|
3069
3991
|
return {
|
|
3070
3992
|
items: page,
|
|
@@ -3073,7 +3995,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3073
3995
|
},
|
|
3074
3996
|
async getHeadCursor() {
|
|
3075
3997
|
return {
|
|
3076
|
-
cursor: String(storedDocuments.size)
|
|
3998
|
+
cursor: String(automergeMode ? changeSequence : storedDocuments.size)
|
|
3077
3999
|
};
|
|
3078
4000
|
},
|
|
3079
4001
|
async listInaccessible(query) {
|
|
@@ -3169,7 +4091,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3169
4091
|
const currentDoc = loadMockAutomergeDocument(document2);
|
|
3170
4092
|
const [mergedDoc] = Automerge.applyChanges(
|
|
3171
4093
|
currentDoc,
|
|
3172
|
-
patch.changes.map((
|
|
4094
|
+
patch.changes.map((change2) => new Uint8Array(change2))
|
|
3173
4095
|
);
|
|
3174
4096
|
persistMockAutomergeDocument(document2, mergedDoc);
|
|
3175
4097
|
document2.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -3182,7 +4104,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3182
4104
|
);
|
|
3183
4105
|
changesSince = {
|
|
3184
4106
|
sinceHeads: [...patch.replicaHeads],
|
|
3185
|
-
changes: missingChanges.map((
|
|
4107
|
+
changes: missingChanges.map((change2) => cloneBytes(new Uint8Array(change2)))
|
|
3186
4108
|
};
|
|
3187
4109
|
}
|
|
3188
4110
|
return {
|
|
@@ -3219,7 +4141,10 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3219
4141
|
if (existing.isDeleted) {
|
|
3220
4142
|
existing.isDeleted = false;
|
|
3221
4143
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3222
|
-
|
|
4144
|
+
if (!automergeMode) {
|
|
4145
|
+
existing.heads = [`mock-head-${++changeCounter}`];
|
|
4146
|
+
}
|
|
4147
|
+
storedDocuments.set(callerId, existing);
|
|
3223
4148
|
notifyLiveQueries();
|
|
3224
4149
|
}
|
|
3225
4150
|
return {
|
|
@@ -3248,11 +4173,39 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3248
4173
|
"recipients: [] with includeSelf: false would produce a document nobody can read"
|
|
3249
4174
|
);
|
|
3250
4175
|
}
|
|
4176
|
+
const encryptFor = Array.isArray(input.recipients) ? { _encryptFor: mockEncryptForMap(input.recipients) } : {};
|
|
4177
|
+
if (automergeMode) {
|
|
4178
|
+
const stored2 = {
|
|
4179
|
+
id,
|
|
4180
|
+
data: {},
|
|
4181
|
+
attachments: [],
|
|
4182
|
+
updatedAt: createdAt,
|
|
4183
|
+
isDeleted: false,
|
|
4184
|
+
decryptionKeyId: input.decryptionKeyId ?? "default"
|
|
4185
|
+
};
|
|
4186
|
+
persistMockAutomergeDocument(
|
|
4187
|
+
stored2,
|
|
4188
|
+
createMockAutomergeDocument(
|
|
4189
|
+
{ ...input.set ?? {}, ...encryptFor },
|
|
4190
|
+
MOCK_AUTOMERGE_LOCAL_ACTOR
|
|
4191
|
+
)
|
|
4192
|
+
);
|
|
4193
|
+
storedDocuments.set(id, stored2);
|
|
4194
|
+
notifyLiveQueries();
|
|
4195
|
+
return {
|
|
4196
|
+
id,
|
|
4197
|
+
data: structuredClone(stored2.data),
|
|
4198
|
+
heads: [...stored2.heads ?? []],
|
|
4199
|
+
attachments: [],
|
|
4200
|
+
updatedAt: createdAt,
|
|
4201
|
+
...mockDecryptionKeyId(stored2)
|
|
4202
|
+
};
|
|
4203
|
+
}
|
|
3251
4204
|
const created = {
|
|
3252
4205
|
id,
|
|
3253
4206
|
data: {
|
|
3254
4207
|
...readMockTypedValues(input.set ?? {}),
|
|
3255
|
-
...
|
|
4208
|
+
...encryptFor
|
|
3256
4209
|
},
|
|
3257
4210
|
heads: [`mock-head-${++changeCounter}`],
|
|
3258
4211
|
attachments: [],
|
|
@@ -3283,6 +4236,10 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3283
4236
|
async update(docId, patch) {
|
|
3284
4237
|
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3285
4238
|
const existing = storedDocuments.get(docId);
|
|
4239
|
+
if (automergeMode) {
|
|
4240
|
+
const current = existing?.automergeBinary ? loadMockAutomergeDocument(existing, MOCK_AUTOMERGE_LOCAL_ACTOR) : createMockAutomergeDocument(existing?.data ?? {}, MOCK_AUTOMERGE_LOCAL_ACTOR);
|
|
4241
|
+
return writeAutomergeDocument(docId, existing, applyMockAutomergeUpdate(current, patch));
|
|
4242
|
+
}
|
|
3286
4243
|
const updated = {
|
|
3287
4244
|
id: docId,
|
|
3288
4245
|
data: applyDocumentUpdatePatch(existing?.data ?? {}, patch),
|
|
@@ -3369,7 +4326,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3369
4326
|
storedDocuments.set(_docId, {
|
|
3370
4327
|
...existing,
|
|
3371
4328
|
isDeleted: false,
|
|
3372
|
-
heads: [`mock-head-${++changeCounter}`],
|
|
4329
|
+
heads: automergeMode ? existing.heads : [`mock-head-${++changeCounter}`],
|
|
3373
4330
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3374
4331
|
});
|
|
3375
4332
|
notifyLiveQueries();
|
|
@@ -3593,7 +4550,26 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3593
4550
|
createdAt: document2.updatedAt ?? null
|
|
3594
4551
|
})),
|
|
3595
4552
|
documents: storedDocuments,
|
|
3596
|
-
notifyLiveQueries: refreshLiveQueries
|
|
4553
|
+
notifyLiveQueries: refreshLiveQueries,
|
|
4554
|
+
async applyRemoteUpdate(docId, input, options) {
|
|
4555
|
+
const existing = storedDocuments.get(docId);
|
|
4556
|
+
if (!existing || existing.isDeleted) {
|
|
4557
|
+
throw new Error(`Document ${docId} was not found.`);
|
|
4558
|
+
}
|
|
4559
|
+
if (!automergeMode || !existing.automergeBinary) {
|
|
4560
|
+
return await defaultDocuments.update(docId, input);
|
|
4561
|
+
}
|
|
4562
|
+
const current = loadMockAutomergeDocument(existing, MOCK_AUTOMERGE_LOCAL_ACTOR);
|
|
4563
|
+
const remote = applyMockAutomergeUpdate(
|
|
4564
|
+
forkMockAutomergeDocument(
|
|
4565
|
+
current,
|
|
4566
|
+
input.json?.baseHeads,
|
|
4567
|
+
options?.actor ?? MOCK_AUTOMERGE_REMOTE_ACTOR
|
|
4568
|
+
),
|
|
4569
|
+
input.json ? { ...input, json: { ...input.json, baseHeads: void 0 } } : input
|
|
4570
|
+
);
|
|
4571
|
+
return writeAutomergeDocument(docId, existing, Automerge.merge(current, remote));
|
|
4572
|
+
}
|
|
3597
4573
|
};
|
|
3598
4574
|
}
|
|
3599
4575
|
function createMockSessionState(options = {}) {
|
|
@@ -3619,6 +4595,7 @@ function createMockSessionState(options = {}) {
|
|
|
3619
4595
|
const databaseViewDocumentLists = /* @__PURE__ */ new Map();
|
|
3620
4596
|
const databaseDocumentStores = /* @__PURE__ */ new Map();
|
|
3621
4597
|
const databaseLiveQueryRefreshers = /* @__PURE__ */ new Map();
|
|
4598
|
+
const databaseRemoteUpdaters = /* @__PURE__ */ new Map();
|
|
3622
4599
|
const databasePeers = {
|
|
3623
4600
|
getDocuments: (databaseId) => databaseDocumentStores.get(databaseId),
|
|
3624
4601
|
refreshAllLiveQueries: () => {
|
|
@@ -3709,6 +4686,7 @@ function createMockSessionState(options = {}) {
|
|
|
3709
4686
|
databaseViewApis.clear();
|
|
3710
4687
|
databaseDocumentStores.clear();
|
|
3711
4688
|
databaseLiveQueryRefreshers.clear();
|
|
4689
|
+
databaseRemoteUpdaters.clear();
|
|
3712
4690
|
databaseInfos = definitions.map((definition) => ({
|
|
3713
4691
|
...definition.info,
|
|
3714
4692
|
capabilities: [...definition.info.capabilities]
|
|
@@ -3727,6 +4705,7 @@ function createMockSessionState(options = {}) {
|
|
|
3727
4705
|
databaseViewDocumentLists.set(definition.info.id, created.listViewDocuments);
|
|
3728
4706
|
databaseDocumentStores.set(definition.info.id, created.documents);
|
|
3729
4707
|
databaseLiveQueryRefreshers.set(definition.info.id, created.notifyLiveQueries);
|
|
4708
|
+
databaseRemoteUpdaters.set(definition.info.id, created.applyRemoteUpdate);
|
|
3730
4709
|
databaseViewApis.set(definition.info.id, {
|
|
3731
4710
|
async create(input) {
|
|
3732
4711
|
const build = () => createEvaluatingViewNavigator(
|
|
@@ -3849,10 +4828,10 @@ function createMockSessionState(options = {}) {
|
|
|
3849
4828
|
`Unknown test database for view creation: ${firstDatabaseId}`
|
|
3850
4829
|
);
|
|
3851
4830
|
}
|
|
3852
|
-
const
|
|
4831
|
+
const view2 = await api.create(input);
|
|
3853
4832
|
const viewId = input.definition.id || crypto.randomUUID();
|
|
3854
|
-
sessionViews.set(viewId,
|
|
3855
|
-
return
|
|
4833
|
+
sessionViews.set(viewId, view2);
|
|
4834
|
+
return view2;
|
|
3856
4835
|
},
|
|
3857
4836
|
async openViewNavigator(viewId, options2) {
|
|
3858
4837
|
const existing = sessionViews.get(viewId);
|
|
@@ -3860,7 +4839,7 @@ function createMockSessionState(options = {}) {
|
|
|
3860
4839
|
return existing;
|
|
3861
4840
|
}
|
|
3862
4841
|
const sourceDatabaseId = launchContext.views.find(
|
|
3863
|
-
(
|
|
4842
|
+
(view3) => view3.id === viewId
|
|
3864
4843
|
)?.sources[0]?.databaseId;
|
|
3865
4844
|
if (!sourceDatabaseId) {
|
|
3866
4845
|
throw new Error(`Unknown test view: ${viewId}`);
|
|
@@ -3871,9 +4850,9 @@ function createMockSessionState(options = {}) {
|
|
|
3871
4850
|
`Unknown test database for view ${viewId}: ${sourceDatabaseId}`
|
|
3872
4851
|
);
|
|
3873
4852
|
}
|
|
3874
|
-
const
|
|
3875
|
-
sessionViews.set(viewId,
|
|
3876
|
-
return
|
|
4853
|
+
const view2 = await api.open(viewId, options2);
|
|
4854
|
+
sessionViews.set(viewId, view2);
|
|
4855
|
+
return view2;
|
|
3877
4856
|
},
|
|
3878
4857
|
menus,
|
|
3879
4858
|
drag,
|
|
@@ -4010,6 +4989,13 @@ function createMockSessionState(options = {}) {
|
|
|
4010
4989
|
}
|
|
4011
4990
|
return database;
|
|
4012
4991
|
},
|
|
4992
|
+
async applyRemoteUpdate(databaseId, docId, input, updateOptions) {
|
|
4993
|
+
const applyRemoteUpdate = databaseRemoteUpdaters.get(databaseId);
|
|
4994
|
+
if (!applyRemoteUpdate) {
|
|
4995
|
+
throw new Error(`Unknown test database: ${databaseId}`);
|
|
4996
|
+
}
|
|
4997
|
+
return await applyRemoteUpdate(docId, input, updateOptions);
|
|
4998
|
+
},
|
|
4013
4999
|
getLicensedProducts() {
|
|
4014
5000
|
return [...launchContext.licensedProducts ?? []];
|
|
4015
5001
|
},
|
|
@@ -4126,6 +5112,7 @@ function createMockMindooDBAppSession(options = {}) {
|
|
|
4126
5112
|
setCapabilities: state.setCapabilities,
|
|
4127
5113
|
setEnforceCapabilities: state.setEnforceCapabilities,
|
|
4128
5114
|
setDirectoryUsers: state.setDirectoryUsers,
|
|
5115
|
+
applyRemoteUpdate: state.applyRemoteUpdate,
|
|
4129
5116
|
emitThemeChange: state.emitThemeChange,
|
|
4130
5117
|
emitViewportChange: state.emitViewportChange,
|
|
4131
5118
|
emitUiPreferencesChange: state.emitUiPreferencesChange,
|
|
@@ -4158,6 +5145,20 @@ function createFakeBridgeHost(options = {}) {
|
|
|
4158
5145
|
);
|
|
4159
5146
|
const requests = [];
|
|
4160
5147
|
const connectedPorts = /* @__PURE__ */ new Set();
|
|
5148
|
+
const agentHost = createMockAgentHost({
|
|
5149
|
+
...options.agentToolPrefix !== void 0 ? { agentToolPrefix: options.agentToolPrefix } : {},
|
|
5150
|
+
...options.agentToolsEnabled !== void 0 ? { agentToolsEnabled: options.agentToolsEnabled } : {},
|
|
5151
|
+
...options.agentConsent !== void 0 ? { agentConsent: options.agentConsent } : {},
|
|
5152
|
+
...options.agentInvokeTimeoutMs !== void 0 ? { agentInvokeTimeoutMs: options.agentInvokeTimeoutMs } : {},
|
|
5153
|
+
appId: () => state.getLaunchContext().appId,
|
|
5154
|
+
post(message) {
|
|
5155
|
+
const port = [...connectedPorts].pop();
|
|
5156
|
+
if (!port) return false;
|
|
5157
|
+
port.postMessage(message);
|
|
5158
|
+
return true;
|
|
5159
|
+
},
|
|
5160
|
+
onChange: () => options.onAgentChange?.()
|
|
5161
|
+
});
|
|
4161
5162
|
state.embedHost.onClosed((event) => {
|
|
4162
5163
|
connectedPorts.forEach(
|
|
4163
5164
|
(port) => port.postMessage({ protocol: PROTOCOL, kind: "embed-event", event: { type: "closed", ...event } })
|
|
@@ -4199,6 +5200,7 @@ function createFakeBridgeHost(options = {}) {
|
|
|
4199
5200
|
"Expected the bridge connection to transfer a MessagePort."
|
|
4200
5201
|
);
|
|
4201
5202
|
}
|
|
5203
|
+
if (connectedPorts.size > 0) agentHost.reset("The app was reloaded.");
|
|
4202
5204
|
connectedPorts.add(port);
|
|
4203
5205
|
port.addEventListener("message", (event) => {
|
|
4204
5206
|
void handlePortMessage(port, event.data);
|
|
@@ -4289,9 +5291,9 @@ function createFakeBridgeHost(options = {}) {
|
|
|
4289
5291
|
if (existing) {
|
|
4290
5292
|
return existing;
|
|
4291
5293
|
}
|
|
4292
|
-
const
|
|
4293
|
-
viewSessions.set(viewId,
|
|
4294
|
-
return
|
|
5294
|
+
const navigator2 = await state.openViewNavigator(viewId);
|
|
5295
|
+
viewSessions.set(viewId, navigator2);
|
|
5296
|
+
return navigator2;
|
|
4295
5297
|
}
|
|
4296
5298
|
function postRpcSuccess(port, requestId, result) {
|
|
4297
5299
|
port.postMessage({
|
|
@@ -4340,14 +5342,14 @@ function createFakeBridgeHost(options = {}) {
|
|
|
4340
5342
|
return { ok: true };
|
|
4341
5343
|
case "session.createViewNavigator": {
|
|
4342
5344
|
const navigatorId = `navigator-${viewCounter += 1}`;
|
|
4343
|
-
const
|
|
5345
|
+
const navigator2 = await state.createViewNavigator({
|
|
4344
5346
|
databaseIds: Array.isArray(params.databaseIds) ? params.databaseIds.map((entry) => String(entry)) : [],
|
|
4345
5347
|
definition: params.definition,
|
|
4346
5348
|
categorizationStyle: params.categorizationStyle === "category_then_document" ? "category_then_document" : "document_then_category",
|
|
4347
5349
|
options: params.options
|
|
4348
5350
|
});
|
|
4349
|
-
viewSessions.set(navigatorId,
|
|
4350
|
-
|
|
5351
|
+
viewSessions.set(navigatorId, navigator2);
|
|
5352
|
+
navigator2.onDidUpdate((stats) => {
|
|
4351
5353
|
if (!viewSessions.has(navigatorId)) return;
|
|
4352
5354
|
const payload = {
|
|
4353
5355
|
protocol: PROTOCOL,
|
|
@@ -4361,11 +5363,11 @@ function createFakeBridgeHost(options = {}) {
|
|
|
4361
5363
|
}
|
|
4362
5364
|
case "session.openViewNavigator": {
|
|
4363
5365
|
const navigatorId = `navigator-${viewCounter += 1}`;
|
|
4364
|
-
const
|
|
5366
|
+
const navigator2 = await state.openViewNavigator(
|
|
4365
5367
|
String(params.viewId),
|
|
4366
5368
|
params.options
|
|
4367
5369
|
);
|
|
4368
|
-
viewSessions.set(navigatorId,
|
|
5370
|
+
viewSessions.set(navigatorId, navigator2);
|
|
4369
5371
|
return { navigatorId };
|
|
4370
5372
|
}
|
|
4371
5373
|
case "menus.show":
|
|
@@ -4858,8 +5860,8 @@ function createFakeBridgeHost(options = {}) {
|
|
|
4858
5860
|
);
|
|
4859
5861
|
case "viewNavigators.dispose": {
|
|
4860
5862
|
const key = String(params.navigatorId);
|
|
4861
|
-
const
|
|
4862
|
-
await
|
|
5863
|
+
const navigator2 = await resolveViewNavigator(key);
|
|
5864
|
+
await navigator2.dispose();
|
|
4863
5865
|
viewSessions.delete(key);
|
|
4864
5866
|
return void 0;
|
|
4865
5867
|
}
|
|
@@ -4944,6 +5946,10 @@ function createFakeBridgeHost(options = {}) {
|
|
|
4944
5946
|
beforeCloseWaiters.get(message.closeId)?.();
|
|
4945
5947
|
return;
|
|
4946
5948
|
}
|
|
5949
|
+
if (message.kind === "agent-result") {
|
|
5950
|
+
agentHost.handleResult(message);
|
|
5951
|
+
return;
|
|
5952
|
+
}
|
|
4947
5953
|
if (message.kind === "workspace-focus-requested") {
|
|
4948
5954
|
state.setHostFocused(true);
|
|
4949
5955
|
const payload = {
|
|
@@ -4985,6 +5991,11 @@ function createFakeBridgeHost(options = {}) {
|
|
|
4985
5991
|
succeed(result);
|
|
4986
5992
|
return;
|
|
4987
5993
|
}
|
|
5994
|
+
const agentResult = await agentHost.dispatch(message.method, message.params);
|
|
5995
|
+
if (agentResult) {
|
|
5996
|
+
succeed(agentResult.result);
|
|
5997
|
+
return;
|
|
5998
|
+
}
|
|
4988
5999
|
const builtinResult = await handleBuiltinRequest(message);
|
|
4989
6000
|
if (builtinResult === NOT_HANDLED) {
|
|
4990
6001
|
fail({
|
|
@@ -5026,6 +6037,7 @@ function createFakeBridgeHost(options = {}) {
|
|
|
5026
6037
|
const controller = {
|
|
5027
6038
|
bridge: state.bridge,
|
|
5028
6039
|
session: state.session,
|
|
6040
|
+
agent: agentHost.controller,
|
|
5029
6041
|
get launchId() {
|
|
5030
6042
|
return state.getLaunchContext().launchId;
|
|
5031
6043
|
},
|
|
@@ -5043,6 +6055,7 @@ function createFakeBridgeHost(options = {}) {
|
|
|
5043
6055
|
getEnforceCapabilities: state.getEnforceCapabilities,
|
|
5044
6056
|
setDirectoryUsers: state.setDirectoryUsers,
|
|
5045
6057
|
getDirectoryUsers: state.getDirectoryUsers,
|
|
6058
|
+
applyRemoteUpdate: state.applyRemoteUpdate,
|
|
5046
6059
|
listEmbeds: () => state.embedHost.listEmbeds(),
|
|
5047
6060
|
closeEmbed: (embedId, reason, result) => state.embedHost.closeEmbed(embedId, reason, result),
|
|
5048
6061
|
getEmbeddingFinish: () => state.embedHost.getEmbeddingFinish(),
|
|
@@ -5183,12 +6196,15 @@ export {
|
|
|
5183
6196
|
createEvaluatingViewNavigator,
|
|
5184
6197
|
createFakeBridgeHost,
|
|
5185
6198
|
createMemoryAttachments,
|
|
6199
|
+
createMockAgentHost,
|
|
5186
6200
|
createMockMindooDBAppBridge,
|
|
5187
6201
|
createMockMindooDBAppSession,
|
|
5188
6202
|
generateDirectoryUsers,
|
|
5189
6203
|
loadMindoodbForTesting,
|
|
6204
|
+
mockAgentToolPrefix,
|
|
5190
6205
|
mockDatabasesFromDefinition,
|
|
5191
6206
|
mountHavenTestHost,
|
|
5192
6207
|
previewJson,
|
|
5193
|
-
readTestHostUrlSettings
|
|
6208
|
+
readTestHostUrlSettings,
|
|
6209
|
+
validateMockAgentTools
|
|
5194
6210
|
};
|