@foldspace_npm/harness 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,482 @@
1
+ import {
2
+ CAPABILITIES,
3
+ ERROR_CODES,
4
+ createDiagnosticRequest,
5
+ diagnosticFailure,
6
+ diagnosticSuccess,
7
+ } from "./protocol.mjs";
8
+
9
+ // Diagnostic run functions are deliberately self-contained: CDP and extension
10
+ // adapters serialize them into the page, so they cannot close over this module.
11
+ export const DIAGNOSTICS = Object.freeze({
12
+ inspectSdkState: Object.freeze({
13
+ description:
14
+ "Report Foldspace SDK presence, configured key, script sources, and existing agent instance IDs.",
15
+ risk: "observe",
16
+ capabilities: Object.freeze([CAPABILITIES.PAGE_EVALUATE]),
17
+ run: function inspectSdkState(args = {}) {
18
+ const root = globalThis.window || globalThis;
19
+ const namespace =
20
+ typeof root.__FOLD_SPACE__ === "string" && root.__FOLD_SPACE__
21
+ ? root.__FOLD_SPACE__
22
+ : "foldspace";
23
+ const sdk = root[namespace];
24
+ let agentIds = [];
25
+ let agentIdsError = null;
26
+ try {
27
+ if (typeof sdk?.agentIds === "function") {
28
+ const value = sdk.agentIds();
29
+ if (Array.isArray(value)) {
30
+ agentIds = value
31
+ .filter((id) => typeof id === "string")
32
+ .slice(0, 50);
33
+ }
34
+ }
35
+ } catch (error) {
36
+ agentIdsError = String(error?.message || error).slice(0, 500);
37
+ }
38
+
39
+ let scripts = [];
40
+ try {
41
+ scripts = Array.from(root.document?.scripts || [])
42
+ .map((script) => script?.src)
43
+ .filter(
44
+ (source) =>
45
+ typeof source === "string" &&
46
+ /foldspace|eucera/i.test(source),
47
+ )
48
+ .slice(0, 20)
49
+ .map((source) => source.slice(0, 2_000));
50
+ } catch {
51
+ // A document is optional for SDK-only fixtures.
52
+ }
53
+ let trackerKey = null;
54
+ try {
55
+ const value = sdk?.getTracker?.()?.getOriginalKey?.();
56
+ if (typeof value === "string") trackerKey = value.slice(0, 500);
57
+ } catch {
58
+ // Older SDKs may not expose their tracker.
59
+ }
60
+
61
+ const expectedApiName =
62
+ typeof args.expectedApiName === "string" ? args.expectedApiName : null;
63
+ const expectedMode =
64
+ typeof args.expectedMode === "string"
65
+ ? args.expectedMode.toLowerCase()
66
+ : null;
67
+ const matchingAgentIds = expectedApiName
68
+ ? agentIds.filter((id) => {
69
+ const separator = id.indexOf("-");
70
+ if (separator < 1) return false;
71
+ const mode = id.slice(0, separator).toLowerCase();
72
+ const apiName = id.slice(separator + 1);
73
+ return (
74
+ apiName === expectedApiName &&
75
+ (expectedMode === null || mode === expectedMode)
76
+ );
77
+ })
78
+ : [];
79
+
80
+ return {
81
+ pageUrl:
82
+ typeof root.location?.href === "string"
83
+ ? root.location.href.slice(0, 2_000)
84
+ : null,
85
+ documentReadyState:
86
+ typeof root.document?.readyState === "string"
87
+ ? root.document.readyState
88
+ : null,
89
+ sdkPresent: sdk !== undefined && sdk !== null,
90
+ sdkReady: typeof sdk?.agent === "function",
91
+ namespace,
92
+ sdkKey:
93
+ typeof sdk?.k === "string" ? sdk.k.slice(0, 500) : null,
94
+ trackerKey,
95
+ agentIds,
96
+ agentIdsError,
97
+ scripts,
98
+ expectedApiName,
99
+ expectedMode,
100
+ matchingAgentIds,
101
+ };
102
+ },
103
+ }),
104
+
105
+ inspectRegistration: Object.freeze({
106
+ description:
107
+ "Read the exact action names registered on an existing Foldspace agent instance.",
108
+ risk: "observe",
109
+ capabilities: Object.freeze([CAPABILITIES.PAGE_EVALUATE]),
110
+ run: async function inspectRegistration(args = {}) {
111
+ const root = globalThis.window || globalThis;
112
+ const namespace =
113
+ typeof root.__FOLD_SPACE__ === "string" && root.__FOLD_SPACE__
114
+ ? root.__FOLD_SPACE__
115
+ : "foldspace";
116
+ const sdk = root[namespace];
117
+ if (typeof sdk?.agentIds !== "function" || typeof sdk?.agent !== "function") {
118
+ return {
119
+ sdkPresent: sdk !== undefined && sdk !== null,
120
+ sdkReady: false,
121
+ agentFound: false,
122
+ actionNames: [],
123
+ };
124
+ }
125
+
126
+ const apiName =
127
+ typeof args.apiName === "string" ? args.apiName : "";
128
+ const requestedMode =
129
+ typeof args.mode === "string" ? args.mode.toLowerCase() : null;
130
+ const ownerAgentId =
131
+ typeof args.ownerAgentId === "string" ? args.ownerAgentId : null;
132
+ const ids = sdk
133
+ .agentIds()
134
+ .filter((id) => typeof id === "string")
135
+ .slice(0, 50);
136
+ const matchedId = ownerAgentId
137
+ ? ids.find((id) => id === ownerAgentId)
138
+ : ids.find((id) => {
139
+ const separator = id.indexOf("-");
140
+ if (separator < 1) return false;
141
+ const mode = id.slice(0, separator).toLowerCase();
142
+ const name = id.slice(separator + 1);
143
+ return (
144
+ name === apiName &&
145
+ (requestedMode === null || requestedMode === mode)
146
+ );
147
+ });
148
+ if (!matchedId) {
149
+ return {
150
+ sdkPresent: true,
151
+ sdkReady: true,
152
+ agentFound: false,
153
+ agentIds: ids,
154
+ actionNames: [],
155
+ };
156
+ }
157
+
158
+ const separator = matchedId.indexOf("-");
159
+ const mode = matchedId.slice(0, separator);
160
+ const agent = sdk.agent({
161
+ apiName: matchedId.slice(separator + 1),
162
+ mode: mode.toUpperCase(),
163
+ });
164
+ const available = await agent?.getAvailableActions?.();
165
+ const actionNames =
166
+ available && typeof available === "object"
167
+ ? Object.keys(available).sort().slice(0, 200)
168
+ : [];
169
+ const expectedActionNames = Array.isArray(args.expectedActionNames)
170
+ ? args.expectedActionNames
171
+ .filter((name) => typeof name === "string")
172
+ .slice(0, 200)
173
+ : [];
174
+ const actionSet = new Set(actionNames);
175
+ const expectedSet = new Set(expectedActionNames);
176
+
177
+ return {
178
+ sdkPresent: true,
179
+ sdkReady: true,
180
+ agentFound: true,
181
+ agentId: matchedId,
182
+ actionNames,
183
+ expectedActionNames,
184
+ missingActionNames: expectedActionNames.filter(
185
+ (name) => !actionSet.has(name),
186
+ ),
187
+ unexpectedActionNames: actionNames.filter(
188
+ (name) => !expectedSet.has(name),
189
+ ),
190
+ };
191
+ },
192
+ }),
193
+
194
+ verifyWidgetVisible: Object.freeze({
195
+ description:
196
+ "Report whether a Foldspace widget candidate is mounted and has a visible bounding box.",
197
+ risk: "observe",
198
+ capabilities: Object.freeze([CAPABILITIES.PAGE_EVALUATE]),
199
+ run: function verifyWidgetVisible(args = {}) {
200
+ const root = globalThis.window || globalThis;
201
+ const selectors =
202
+ Array.isArray(args.selectors) && args.selectors.length
203
+ ? args.selectors
204
+ .filter((selector) => typeof selector === "string")
205
+ .slice(0, 10)
206
+ : [
207
+ "#eucera-agent-view",
208
+ "#foldspace-container",
209
+ "[data-foldspace-agent]",
210
+ ];
211
+
212
+ const candidates = [];
213
+ const seen = new Set();
214
+ for (const selector of selectors) {
215
+ let elements = [];
216
+ try {
217
+ if (typeof root.document?.querySelectorAll === "function") {
218
+ elements = Array.from(root.document.querySelectorAll(selector));
219
+ } else {
220
+ const element = root.document?.querySelector?.(selector);
221
+ if (element) elements = [element];
222
+ }
223
+ } catch {
224
+ continue;
225
+ }
226
+ for (const element of elements.slice(0, 30)) {
227
+ if (!element || seen.has(element)) continue;
228
+ seen.add(element);
229
+ const rect = element.getBoundingClientRect?.();
230
+ const width = Math.max(0, Math.round(rect?.width || 0));
231
+ const height = Math.max(0, Math.round(rect?.height || 0));
232
+ candidates.push({
233
+ selector,
234
+ visible: width > 0 && height > 0,
235
+ size: { width, height },
236
+ ownerAgentId:
237
+ element.getAttribute?.("data-foldspace-dev-agent-id") || null,
238
+ });
239
+ }
240
+ }
241
+
242
+ const visibleCandidates = candidates.filter(
243
+ (candidate) => candidate.visible,
244
+ );
245
+ const expectedOwner =
246
+ typeof args.ownerAgentId === "string" ? args.ownerAgentId : null;
247
+ const first = candidates[0] || null;
248
+ return {
249
+ mounted: candidates.length > 0,
250
+ visible: visibleCandidates.length > 0,
251
+ soleVisible: visibleCandidates.length === 1,
252
+ visibleCount: visibleCandidates.length,
253
+ ownerMatches:
254
+ expectedOwner === null ||
255
+ (visibleCandidates.length === 1 &&
256
+ visibleCandidates[0].ownerAgentId === expectedOwner),
257
+ expectedOwner,
258
+ selector: first?.selector || null,
259
+ size: first?.size || null,
260
+ candidates,
261
+ };
262
+ },
263
+ }),
264
+
265
+ inspectActionObservation: Object.freeze({
266
+ description:
267
+ "Report the local action registry and bounded passive action lifecycle evidence captured in the page.",
268
+ risk: "observe",
269
+ capabilities: Object.freeze([CAPABILITIES.PAGE_EVALUATE]),
270
+ run: function inspectActionObservation(args = {}) {
271
+ const root = globalThis.window || globalThis;
272
+ const state = root.__FOLDSPACE_DEV_ACTION_OBSERVER__;
273
+ if (!state || state.version !== 1) {
274
+ return {
275
+ installed: false,
276
+ targetAgentId: null,
277
+ expectedActionNames: [],
278
+ actionNameLimitExceeded: false,
279
+ captureCount: 0,
280
+ subscribed: false,
281
+ eventCount: 0,
282
+ droppedEvents: 0,
283
+ events: [],
284
+ };
285
+ }
286
+
287
+ const expectedTarget =
288
+ typeof args.ownerAgentId === "string" ? args.ownerAgentId : null;
289
+ const events = Array.isArray(state.events)
290
+ ? state.events.slice(-100).map((event) => ({
291
+ source:
292
+ typeof event?.source === "string"
293
+ ? event.source.slice(0, 100)
294
+ : "unknown",
295
+ actionName:
296
+ typeof event?.actionName === "string"
297
+ ? event.actionName.slice(0, 200)
298
+ : null,
299
+ phase:
300
+ typeof event?.phase === "string"
301
+ ? event.phase.slice(0, 100)
302
+ : "unknown",
303
+ status:
304
+ typeof event?.status === "string"
305
+ ? event.status.slice(0, 100)
306
+ : "unknown",
307
+ durationMs: Number.isFinite(event?.durationMs)
308
+ ? Math.max(0, Math.round(event.durationMs))
309
+ : null,
310
+ parameterKeys: Array.isArray(event?.parameterKeys)
311
+ ? event.parameterKeys
312
+ .filter((key) => typeof key === "string")
313
+ .slice(0, 50)
314
+ : [],
315
+ }))
316
+ : [];
317
+
318
+ return {
319
+ installed: true,
320
+ targetAgentId:
321
+ typeof state.targetAgentId === "string"
322
+ ? state.targetAgentId.slice(0, 200)
323
+ : null,
324
+ ownerMatches:
325
+ expectedTarget === null || state.targetAgentId === expectedTarget,
326
+ expectedActionNames: Array.isArray(state.expectedActionNames)
327
+ ? state.expectedActionNames
328
+ .filter((name) => typeof name === "string")
329
+ .sort()
330
+ .slice(0, 200)
331
+ : [],
332
+ actionNameLimitExceeded:
333
+ state.actionNameLimitExceeded === true,
334
+ captureCount: Number.isFinite(state.captureCount)
335
+ ? Math.max(0, Math.round(state.captureCount))
336
+ : 0,
337
+ subscribed: state.subscribed === true,
338
+ eventCount: Array.isArray(state.events) ? state.events.length : 0,
339
+ droppedEvents: Number.isFinite(state.droppedEvents)
340
+ ? Math.max(0, Math.round(state.droppedEvents))
341
+ : 0,
342
+ events,
343
+ };
344
+ },
345
+ }),
346
+
347
+ runActionCheck: Object.freeze({
348
+ description:
349
+ "Invoke one action on an existing Foldspace instance without relying on model action selection.",
350
+ risk: "test",
351
+ capabilities: Object.freeze([
352
+ CAPABILITIES.PAGE_EVALUATE,
353
+ CAPABILITIES.PAGE_MUTATE,
354
+ ]),
355
+ run: async function runActionCheck(args = {}) {
356
+ const root = globalThis.window || globalThis;
357
+ const namespace =
358
+ typeof root.__FOLD_SPACE__ === "string" && root.__FOLD_SPACE__
359
+ ? root.__FOLD_SPACE__
360
+ : "foldspace";
361
+ const sdk = root[namespace];
362
+ if (typeof sdk?.agentIds !== "function" || typeof sdk?.agent !== "function") {
363
+ return { invoked: false, reason: "SDK_NOT_FOUND" };
364
+ }
365
+
366
+ const apiName =
367
+ typeof args.apiName === "string" ? args.apiName : "";
368
+ const actionName =
369
+ typeof args.actionName === "string" ? args.actionName : "";
370
+ const requestedMode =
371
+ typeof args.mode === "string" ? args.mode.toLowerCase() : null;
372
+ const matchedId = sdk.agentIds().find((id) => {
373
+ if (typeof id !== "string") return false;
374
+ const separator = id.indexOf("-");
375
+ if (separator < 1) return false;
376
+ const mode = id.slice(0, separator).toLowerCase();
377
+ const name = id.slice(separator + 1);
378
+ return (
379
+ name === apiName &&
380
+ (requestedMode === null || requestedMode === mode)
381
+ );
382
+ });
383
+ if (!matchedId) {
384
+ return { invoked: false, reason: "AGENT_NOT_FOUND" };
385
+ }
386
+
387
+ const separator = matchedId.indexOf("-");
388
+ const agent = sdk.agent({
389
+ apiName: matchedId.slice(separator + 1),
390
+ mode: matchedId.slice(0, separator).toUpperCase(),
391
+ });
392
+ if (typeof agent?.runAction !== "function") {
393
+ return { invoked: false, reason: "RUN_ACTION_UNAVAILABLE" };
394
+ }
395
+
396
+ const value = await agent.runAction(
397
+ actionName,
398
+ args.params && typeof args.params === "object" ? args.params : {},
399
+ );
400
+ return { invoked: true, agentId: matchedId, actionName, value };
401
+ },
402
+ }),
403
+ });
404
+
405
+ export function diagnosticCatalogue() {
406
+ return Object.entries(DIAGNOSTICS).map(([name, diagnostic]) => ({
407
+ name,
408
+ description: diagnostic.description,
409
+ risk: diagnostic.risk,
410
+ capabilities: [...diagnostic.capabilities],
411
+ }));
412
+ }
413
+
414
+ export function serializeDiagnostic(name, args = {}) {
415
+ const diagnostic = DIAGNOSTICS[name];
416
+ if (!diagnostic) {
417
+ throw new Error(`Unknown diagnostic: ${name}`);
418
+ }
419
+ return `Promise.resolve((${diagnostic.run.toString()})(${JSON.stringify(args)}))`;
420
+ }
421
+
422
+ export async function executeDiagnostic(input, options = {}) {
423
+ let request;
424
+ try {
425
+ request = createDiagnosticRequest(input);
426
+ } catch (error) {
427
+ const fallback = {
428
+ id: typeof input?.id === "string" ? input.id : "invalid-request",
429
+ name: typeof input?.name === "string" ? input.name : "invalid-diagnostic",
430
+ };
431
+ return diagnosticFailure(fallback, {
432
+ code: ERROR_CODES.INVALID_REQUEST,
433
+ message: error instanceof Error ? error.message : String(error),
434
+ });
435
+ }
436
+
437
+ const diagnostic = DIAGNOSTICS[request.name];
438
+ if (!diagnostic) {
439
+ return diagnosticFailure(request, {
440
+ code: ERROR_CODES.INVALID_REQUEST,
441
+ message: `Unknown diagnostic: ${request.name}`,
442
+ });
443
+ }
444
+
445
+ const availableCapabilities = new Set(options.capabilities || []);
446
+ const missingCapabilities = diagnostic.capabilities.filter(
447
+ (capability) => !availableCapabilities.has(capability),
448
+ );
449
+ if (missingCapabilities.length) {
450
+ return diagnosticFailure(request, {
451
+ code: ERROR_CODES.CAPABILITY_DISABLED,
452
+ message: `Diagnostic requires: ${missingCapabilities.join(", ")}`,
453
+ });
454
+ }
455
+
456
+ const startedAt = Date.now();
457
+ let timeout;
458
+ try {
459
+ const result = await Promise.race([
460
+ Promise.resolve(diagnostic.run(request.args)),
461
+ new Promise((_, reject) => {
462
+ timeout = setTimeout(
463
+ () => reject(new Error("Diagnostic timed out")),
464
+ request.timeoutMs,
465
+ );
466
+ }),
467
+ ]);
468
+ return diagnosticSuccess(request, result, {
469
+ durationMs: Date.now() - startedAt,
470
+ });
471
+ } catch (error) {
472
+ return diagnosticFailure(request, {
473
+ code:
474
+ error instanceof Error && error.message === "Diagnostic timed out"
475
+ ? ERROR_CODES.TIMEOUT
476
+ : ERROR_CODES.DIAGNOSTIC_FAILED,
477
+ message: error instanceof Error ? error.message : String(error),
478
+ });
479
+ } finally {
480
+ clearTimeout(timeout);
481
+ }
482
+ }