@syrin/iris 0.5.0 → 0.6.10
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/dist/cli.js +2587 -1027
- package/dist/index.js +414 -114
- package/dist/next.js +5 -1
- package/dist/server.d.ts +9 -0
- package/dist/server.js +1903 -1005
- package/dist/test.js +1918 -1074
- package/dist/vite.d.ts +43 -0
- package/dist/vite.js +144 -0
- package/package.json +15 -10
package/dist/test.js
CHANGED
|
@@ -201,6 +201,30 @@ import { join as join6 } from "path";
|
|
|
201
201
|
// ../protocol/dist/constants.js
|
|
202
202
|
var IRIS_DEFAULT_PORT = 4400;
|
|
203
203
|
var IRIS_WS_PATH = "/iris";
|
|
204
|
+
var IRIS_PROTOCOL_VERSION = 1;
|
|
205
|
+
var TRANSPORT_LIMITS = {
|
|
206
|
+
MAX_MESSAGE_BYTES: 1024 * 1024,
|
|
207
|
+
MAX_MESSAGES_PER_SECOND: 1e3,
|
|
208
|
+
MAX_SESSIONS: 32,
|
|
209
|
+
MAX_PENDING_CONNECTIONS: 16,
|
|
210
|
+
HELLO_TIMEOUT_MS: 5e3,
|
|
211
|
+
MAX_BUFFER_BYTES: 8 * 1024 * 1024,
|
|
212
|
+
MAX_SESSION_ID_LENGTH: 128,
|
|
213
|
+
MAX_URL_LENGTH: 4096,
|
|
214
|
+
MAX_TITLE_LENGTH: 512,
|
|
215
|
+
MAX_ADAPTERS: 32,
|
|
216
|
+
MAX_ADAPTER_NAME_LENGTH: 128,
|
|
217
|
+
MAX_TOKEN_LENGTH: 512,
|
|
218
|
+
MAX_COMMAND_ID_LENGTH: 128,
|
|
219
|
+
MAX_COMMAND_NAME_LENGTH: 128,
|
|
220
|
+
MAX_REF_LENGTH: 128,
|
|
221
|
+
MAX_ERROR_LENGTH: 4096,
|
|
222
|
+
MAX_SERIALIZE_DEPTH: 8,
|
|
223
|
+
MAX_COLLECTION_ITEMS: 200,
|
|
224
|
+
MAX_OBJECT_KEYS: 200,
|
|
225
|
+
MAX_STRING_LENGTH: 64 * 1024
|
|
226
|
+
};
|
|
227
|
+
var DANGEROUS_ACTION_CONFIRM_ARG = "confirmDangerous";
|
|
204
228
|
var REPLAY_PROGRAM_VERSION = 1;
|
|
205
229
|
var IrisDir = {
|
|
206
230
|
ROOT: ".iris",
|
|
@@ -302,7 +326,7 @@ var ReplayStatus = {
|
|
|
302
326
|
DRIFT: "drift",
|
|
303
327
|
// an anchor missed (testid renamed / signal not observed) — legible drift returned
|
|
304
328
|
ERROR: "error"
|
|
305
|
-
// the flow
|
|
329
|
+
// the flow could not load or a resolved action failed
|
|
306
330
|
};
|
|
307
331
|
var DriftReason = {
|
|
308
332
|
TESTID_NOT_FOUND: "testid_not_found",
|
|
@@ -336,8 +360,10 @@ var HealStatus = {
|
|
|
336
360
|
// drift exists but no proposal cleared the confidence floor
|
|
337
361
|
NOTHING_TO_HEAL: "nothing_to_heal",
|
|
338
362
|
// replay was green
|
|
363
|
+
CONSEQUENCE_BROKEN: "consequence_broken",
|
|
364
|
+
// rebind resolves a locator but the flow's success consequence no longer fires — REFUSED (file untouched)
|
|
339
365
|
ERROR: "error"
|
|
340
|
-
// flow missing/malformed/invalid-name
|
|
366
|
+
// flow missing/malformed/invalid-name, or a resolved action failed
|
|
341
367
|
};
|
|
342
368
|
var HEAL_CONFIDENCE_MIN = 0.5;
|
|
343
369
|
var AnnotationTarget = {
|
|
@@ -353,7 +379,8 @@ var AnnotationErrorCode = {
|
|
|
353
379
|
var COMPILED_PREDICATE_PREFIX = "will";
|
|
354
380
|
var RING_BUFFER_DEFAULTS = {
|
|
355
381
|
MAX_EVENTS: 2e3,
|
|
356
|
-
MAX_AGE_MS: 6e4
|
|
382
|
+
MAX_AGE_MS: 6e4,
|
|
383
|
+
MAX_BYTES: TRANSPORT_LIMITS.MAX_BUFFER_BYTES
|
|
357
384
|
};
|
|
358
385
|
var EventType = {
|
|
359
386
|
DOM_ADDED: "dom.added",
|
|
@@ -538,6 +565,8 @@ var MessageKind = {
|
|
|
538
565
|
|
|
539
566
|
// ../protocol/dist/messages.js
|
|
540
567
|
import { z } from "zod";
|
|
568
|
+
var sessionIdSchema = z.string().min(1).max(TRANSPORT_LIMITS.MAX_SESSION_ID_LENGTH);
|
|
569
|
+
var refSchema = z.string().max(TRANSPORT_LIMITS.MAX_REF_LENGTH);
|
|
541
570
|
var HumanControlDataSchema = z.object({
|
|
542
571
|
kind: z.nativeEnum(HumanControlKind),
|
|
543
572
|
text: z.string().optional()
|
|
@@ -545,35 +574,37 @@ var HumanControlDataSchema = z.object({
|
|
|
545
574
|
var IrisEventSchema = z.object({
|
|
546
575
|
t: z.number(),
|
|
547
576
|
type: z.nativeEnum(EventType),
|
|
548
|
-
sessionId:
|
|
577
|
+
sessionId: sessionIdSchema,
|
|
549
578
|
/** Stable element reference this event concerns, when applicable (e.g. "e7"). */
|
|
550
|
-
ref:
|
|
579
|
+
ref: refSchema.optional(),
|
|
551
580
|
/** Event-type-specific payload. Kept open here; refined per observer at the edges. */
|
|
552
581
|
data: z.record(z.unknown()).default({})
|
|
553
582
|
});
|
|
554
583
|
var HelloMessageSchema = z.object({
|
|
555
584
|
kind: z.literal(MessageKind.HELLO),
|
|
556
|
-
protocolVersion: z.
|
|
557
|
-
sessionId:
|
|
558
|
-
url: z.string(),
|
|
559
|
-
title: z.string(),
|
|
560
|
-
adapters: z.array(z.string()),
|
|
585
|
+
protocolVersion: z.literal(IRIS_PROTOCOL_VERSION),
|
|
586
|
+
sessionId: sessionIdSchema,
|
|
587
|
+
url: z.string().max(TRANSPORT_LIMITS.MAX_URL_LENGTH),
|
|
588
|
+
title: z.string().max(TRANSPORT_LIMITS.MAX_TITLE_LENGTH),
|
|
589
|
+
adapters: z.array(z.string().max(TRANSPORT_LIMITS.MAX_ADAPTER_NAME_LENGTH)).max(TRANSPORT_LIMITS.MAX_ADAPTERS),
|
|
590
|
+
/** Optional browser/bridge pairing token. Required when the bridge configures one. */
|
|
591
|
+
token: z.string().max(TRANSPORT_LIMITS.MAX_TOKEN_LENGTH).optional(),
|
|
561
592
|
/** Whether the app has advertised a capability registry (iris.describe). */
|
|
562
593
|
hasCapabilities: z.boolean().optional()
|
|
563
594
|
});
|
|
564
595
|
var CommandMessageSchema = z.object({
|
|
565
596
|
kind: z.literal(MessageKind.COMMAND),
|
|
566
|
-
id: z.string(),
|
|
567
|
-
sessionId:
|
|
568
|
-
name: z.string(),
|
|
597
|
+
id: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_ID_LENGTH),
|
|
598
|
+
sessionId: sessionIdSchema.optional(),
|
|
599
|
+
name: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_NAME_LENGTH),
|
|
569
600
|
args: z.record(z.unknown()).default({})
|
|
570
601
|
});
|
|
571
602
|
var CommandResultSchema = z.object({
|
|
572
603
|
kind: z.literal(MessageKind.COMMAND_RESULT),
|
|
573
|
-
id: z.string(),
|
|
604
|
+
id: z.string().min(1).max(TRANSPORT_LIMITS.MAX_COMMAND_ID_LENGTH),
|
|
574
605
|
ok: z.boolean(),
|
|
575
606
|
result: z.unknown().optional(),
|
|
576
|
-
error: z.string().optional()
|
|
607
|
+
error: z.string().max(TRANSPORT_LIMITS.MAX_ERROR_LENGTH).optional()
|
|
577
608
|
});
|
|
578
609
|
var EventMessageSchema = z.object({
|
|
579
610
|
kind: z.literal(MessageKind.EVENT),
|
|
@@ -586,6 +617,25 @@ var IrisMessageSchema = z.discriminatedUnion("kind", [
|
|
|
586
617
|
EventMessageSchema
|
|
587
618
|
]);
|
|
588
619
|
|
|
620
|
+
// ../protocol/dist/security.js
|
|
621
|
+
var DANGEROUS_ACTION = /\b(delete|remove|destroy|erase|drop|terminate|revoke|reset|logout|log out|sign out|close account|cancel subscription|purchase|buy|pay|place order|confirm order|deploy|publish|send|transfer|withdraw|refund)\b/i;
|
|
622
|
+
function isLoopbackHostname(hostname) {
|
|
623
|
+
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
624
|
+
if (normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") {
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
const octets = normalized.split(".");
|
|
628
|
+
return octets.length === 4 && octets[0] === "127" && octets.every((octet) => {
|
|
629
|
+
if (!/^\d{1,3}$/.test(octet))
|
|
630
|
+
return false;
|
|
631
|
+
const value = Number(octet);
|
|
632
|
+
return value >= 0 && value <= 255;
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
function isDangerousActionText(text) {
|
|
636
|
+
return DANGEROUS_ACTION.test(text.replace(/[_-]+/g, " "));
|
|
637
|
+
}
|
|
638
|
+
|
|
589
639
|
// ../protocol/dist/toon.js
|
|
590
640
|
var ROLE_MAP = {
|
|
591
641
|
button: "btn",
|
|
@@ -862,6 +912,7 @@ function log(event, fields = {}) {
|
|
|
862
912
|
}
|
|
863
913
|
|
|
864
914
|
// ../server/dist/bridge.js
|
|
915
|
+
import { timingSafeEqual } from "crypto";
|
|
865
916
|
import * as http2 from "http";
|
|
866
917
|
import { WebSocketServer } from "ws";
|
|
867
918
|
|
|
@@ -869,14 +920,21 @@ import { WebSocketServer } from "ws";
|
|
|
869
920
|
var RingBuffer = class {
|
|
870
921
|
#maxEvents;
|
|
871
922
|
#maxAgeMs;
|
|
923
|
+
#maxBytes;
|
|
872
924
|
#events = [];
|
|
925
|
+
#eventBytes = [];
|
|
926
|
+
#totalBytes = 0;
|
|
873
927
|
#droppedCount = 0;
|
|
874
928
|
constructor(options = {}) {
|
|
875
929
|
this.#maxEvents = options.maxEvents ?? RING_BUFFER_DEFAULTS.MAX_EVENTS;
|
|
876
930
|
this.#maxAgeMs = options.maxAgeMs ?? RING_BUFFER_DEFAULTS.MAX_AGE_MS;
|
|
931
|
+
this.#maxBytes = options.maxBytes ?? RING_BUFFER_DEFAULTS.MAX_BYTES;
|
|
877
932
|
}
|
|
878
933
|
push(event, now) {
|
|
879
934
|
this.#events.push(event);
|
|
935
|
+
const bytes = Buffer.byteLength(JSON.stringify(event), "utf8");
|
|
936
|
+
this.#eventBytes.push(bytes);
|
|
937
|
+
this.#totalBytes += bytes;
|
|
880
938
|
this.#evict(now);
|
|
881
939
|
}
|
|
882
940
|
/** Events at or after a given timestamp cursor. */
|
|
@@ -902,10 +960,14 @@ var RingBuffer = class {
|
|
|
902
960
|
#evict(now) {
|
|
903
961
|
const before = this.#events.length;
|
|
904
962
|
const cutoff = now - this.#maxAgeMs;
|
|
905
|
-
|
|
906
|
-
this.#events
|
|
963
|
+
while (this.#events.length > this.#maxEvents || this.#totalBytes > this.#maxBytes && this.#events.length > 0) {
|
|
964
|
+
this.#events.shift();
|
|
965
|
+
this.#totalBytes -= this.#eventBytes.shift() ?? 0;
|
|
966
|
+
}
|
|
967
|
+
while ((this.#events[0]?.t ?? cutoff) < cutoff) {
|
|
968
|
+
this.#events.shift();
|
|
969
|
+
this.#totalBytes -= this.#eventBytes.shift() ?? 0;
|
|
907
970
|
}
|
|
908
|
-
this.#events = this.#events.filter((e) => e.t >= cutoff);
|
|
909
971
|
this.#droppedCount += before - this.#events.length;
|
|
910
972
|
}
|
|
911
973
|
/** Snapshot of buffer health for the agent — total events held and cumulative drops since connect. */
|
|
@@ -1142,6 +1204,14 @@ var Session = class {
|
|
|
1142
1204
|
this.#pending.delete(id);
|
|
1143
1205
|
}
|
|
1144
1206
|
}
|
|
1207
|
+
/** End this transport without letting a stale socket remove its replacement session. */
|
|
1208
|
+
disconnect(reason) {
|
|
1209
|
+
this.rejectAll(reason);
|
|
1210
|
+
try {
|
|
1211
|
+
this.#socket.close(1008, reason);
|
|
1212
|
+
} catch {
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1145
1215
|
// ── Live-control: state machine + human→agent inbox (server-owned) ───────────────
|
|
1146
1216
|
getState() {
|
|
1147
1217
|
return this.#state;
|
|
@@ -1263,11 +1333,15 @@ var Session = class {
|
|
|
1263
1333
|
var SessionManager = class {
|
|
1264
1334
|
#sessions = /* @__PURE__ */ new Map();
|
|
1265
1335
|
add(session) {
|
|
1336
|
+
const previous = this.#sessions.get(session.id);
|
|
1266
1337
|
this.#sessions.set(session.id, session);
|
|
1338
|
+
return previous;
|
|
1267
1339
|
}
|
|
1268
|
-
remove(
|
|
1269
|
-
this.#sessions.get(
|
|
1270
|
-
|
|
1340
|
+
remove(session) {
|
|
1341
|
+
if (this.#sessions.get(session.id) !== session)
|
|
1342
|
+
return false;
|
|
1343
|
+
session.rejectAll("session disconnected");
|
|
1344
|
+
return this.#sessions.delete(session.id);
|
|
1271
1345
|
}
|
|
1272
1346
|
get(sessionId) {
|
|
1273
1347
|
return this.#sessions.get(sessionId);
|
|
@@ -1335,6 +1409,20 @@ var SessionManager = class {
|
|
|
1335
1409
|
};
|
|
1336
1410
|
|
|
1337
1411
|
// ../server/dist/bridge.js
|
|
1412
|
+
function normalizeOrigin(origin) {
|
|
1413
|
+
try {
|
|
1414
|
+
return new URL(origin).origin;
|
|
1415
|
+
} catch {
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
function tokensMatch(expected, received) {
|
|
1420
|
+
if (received === void 0)
|
|
1421
|
+
return false;
|
|
1422
|
+
const expectedBytes = Buffer.from(expected);
|
|
1423
|
+
const receivedBytes = Buffer.from(received);
|
|
1424
|
+
return expectedBytes.length === receivedBytes.length && timingSafeEqual(expectedBytes, receivedBytes);
|
|
1425
|
+
}
|
|
1338
1426
|
function rawToString(raw) {
|
|
1339
1427
|
if (typeof raw === "string")
|
|
1340
1428
|
return raw;
|
|
@@ -1350,11 +1438,41 @@ var Bridge = class {
|
|
|
1350
1438
|
ready;
|
|
1351
1439
|
#wss;
|
|
1352
1440
|
#clock;
|
|
1441
|
+
#token;
|
|
1442
|
+
#allowedOrigins;
|
|
1443
|
+
#maxMessagesPerSecond;
|
|
1444
|
+
#maxSessions;
|
|
1445
|
+
#maxPendingConnections;
|
|
1446
|
+
#helloTimeoutMs;
|
|
1447
|
+
#pendingConnections = 0;
|
|
1353
1448
|
constructor(options) {
|
|
1449
|
+
const host = options.host ?? "127.0.0.1";
|
|
1450
|
+
if ((options.token?.length ?? 0) > TRANSPORT_LIMITS.MAX_TOKEN_LENGTH) {
|
|
1451
|
+
throw new Error(`Iris pairing token exceeds ${String(TRANSPORT_LIMITS.MAX_TOKEN_LENGTH)} characters`);
|
|
1452
|
+
}
|
|
1453
|
+
if (!isLoopbackHostname(host) && (options.token === void 0 || options.token.length === 0)) {
|
|
1454
|
+
throw new Error("a pairing token is required when the Iris bridge binds beyond localhost");
|
|
1455
|
+
}
|
|
1354
1456
|
this.#clock = options.clock ?? (() => Date.now());
|
|
1457
|
+
this.#token = options.token !== void 0 && options.token.length > 0 ? options.token : void 0;
|
|
1458
|
+
this.#allowedOrigins = new Set((options.allowedOrigins ?? []).map(normalizeOrigin).filter((origin) => origin !== null));
|
|
1459
|
+
this.#maxMessagesPerSecond = options.maxMessagesPerSecond ?? TRANSPORT_LIMITS.MAX_MESSAGES_PER_SECOND;
|
|
1460
|
+
this.#maxSessions = options.maxSessions ?? TRANSPORT_LIMITS.MAX_SESSIONS;
|
|
1461
|
+
this.#maxPendingConnections = options.maxPendingConnections ?? TRANSPORT_LIMITS.MAX_PENDING_CONNECTIONS;
|
|
1462
|
+
this.#helloTimeoutMs = options.helloTimeoutMs ?? TRANSPORT_LIMITS.HELLO_TIMEOUT_MS;
|
|
1355
1463
|
if (options.server !== void 0) {
|
|
1356
1464
|
const srv = options.server;
|
|
1357
|
-
this.#wss = new WebSocketServer({
|
|
1465
|
+
this.#wss = new WebSocketServer({
|
|
1466
|
+
server: srv,
|
|
1467
|
+
path: IRIS_WS_PATH,
|
|
1468
|
+
maxPayload: TRANSPORT_LIMITS.MAX_MESSAGE_BYTES,
|
|
1469
|
+
verifyClient: ({ origin }, done) => {
|
|
1470
|
+
const allowed = this.#originAllowed(origin);
|
|
1471
|
+
if (!allowed)
|
|
1472
|
+
log("origin_rejected", { origin: origin ?? "missing" });
|
|
1473
|
+
done(allowed, 403, "Forbidden");
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1358
1476
|
this.ready = new Promise((resolve) => {
|
|
1359
1477
|
if (srv.listening) {
|
|
1360
1478
|
resolve(srv.address().port);
|
|
@@ -1367,8 +1485,15 @@ var Bridge = class {
|
|
|
1367
1485
|
} else {
|
|
1368
1486
|
this.#wss = new WebSocketServer({
|
|
1369
1487
|
port: options.port,
|
|
1370
|
-
host
|
|
1371
|
-
path: IRIS_WS_PATH
|
|
1488
|
+
host,
|
|
1489
|
+
path: IRIS_WS_PATH,
|
|
1490
|
+
maxPayload: TRANSPORT_LIMITS.MAX_MESSAGE_BYTES,
|
|
1491
|
+
verifyClient: ({ origin }, done) => {
|
|
1492
|
+
const allowed = this.#originAllowed(origin);
|
|
1493
|
+
if (!allowed)
|
|
1494
|
+
log("origin_rejected", { origin: origin ?? "missing" });
|
|
1495
|
+
done(allowed, 403, "Forbidden");
|
|
1496
|
+
}
|
|
1372
1497
|
});
|
|
1373
1498
|
this.ready = new Promise((resolve) => {
|
|
1374
1499
|
this.#wss.on("listening", () => {
|
|
@@ -1381,14 +1506,64 @@ var Bridge = class {
|
|
|
1381
1506
|
});
|
|
1382
1507
|
}
|
|
1383
1508
|
#onConnection(socket) {
|
|
1509
|
+
if (this.#pendingConnections >= this.#maxPendingConnections) {
|
|
1510
|
+
socket.close(1013, "too many pending handshakes");
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
this.#pendingConnections += 1;
|
|
1514
|
+
let awaitingHello = true;
|
|
1384
1515
|
let session;
|
|
1516
|
+
let messageWindowStartedAt = this.#clock();
|
|
1517
|
+
let messagesInWindow = 0;
|
|
1518
|
+
const releasePending = () => {
|
|
1519
|
+
if (!awaitingHello)
|
|
1520
|
+
return;
|
|
1521
|
+
awaitingHello = false;
|
|
1522
|
+
this.#pendingConnections -= 1;
|
|
1523
|
+
};
|
|
1524
|
+
const helloTimer = setTimeout(() => {
|
|
1525
|
+
if (!awaitingHello)
|
|
1526
|
+
return;
|
|
1527
|
+
releasePending();
|
|
1528
|
+
socket.close(1008, "hello timeout");
|
|
1529
|
+
}, this.#helloTimeoutMs);
|
|
1385
1530
|
socket.on("message", (raw) => {
|
|
1531
|
+
const now = this.#clock();
|
|
1532
|
+
if (now - messageWindowStartedAt >= 1e3) {
|
|
1533
|
+
messageWindowStartedAt = now;
|
|
1534
|
+
messagesInWindow = 0;
|
|
1535
|
+
}
|
|
1536
|
+
messagesInWindow += 1;
|
|
1537
|
+
if (messagesInWindow > this.#maxMessagesPerSecond) {
|
|
1538
|
+
log("message_rate_exceeded", {});
|
|
1539
|
+
socket.close(1008, "message rate exceeded");
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1386
1542
|
const parsed = this.#parse(rawToString(raw));
|
|
1387
|
-
if (parsed === null)
|
|
1543
|
+
if (parsed === null) {
|
|
1544
|
+
socket.close(1008, "invalid message");
|
|
1388
1545
|
return;
|
|
1546
|
+
}
|
|
1389
1547
|
if (parsed.kind === MessageKind.HELLO) {
|
|
1548
|
+
if (session !== void 0) {
|
|
1549
|
+
socket.close(1008, "hello already received");
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
if (this.#token !== void 0 && !tokensMatch(this.#token, parsed.token)) {
|
|
1553
|
+
log("authentication_failed", {});
|
|
1554
|
+
socket.close(1008, "authentication failed");
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
const existing = this.sessions.get(parsed.sessionId);
|
|
1558
|
+
if (existing === void 0 && this.sessions.count() >= this.#maxSessions) {
|
|
1559
|
+
socket.close(1013, "session limit reached");
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
clearTimeout(helloTimer);
|
|
1563
|
+
releasePending();
|
|
1390
1564
|
session = new Session(parsed, socket, this.#clock);
|
|
1391
|
-
this.sessions.add(session);
|
|
1565
|
+
const replaced = this.sessions.add(session);
|
|
1566
|
+
replaced?.disconnect("session replaced by a newer connection");
|
|
1392
1567
|
log("session_connected", { sessionId: session.id, url: session.url });
|
|
1393
1568
|
return;
|
|
1394
1569
|
}
|
|
@@ -1402,15 +1577,28 @@ var Bridge = class {
|
|
|
1402
1577
|
}
|
|
1403
1578
|
});
|
|
1404
1579
|
socket.on("close", () => {
|
|
1580
|
+
clearTimeout(helloTimer);
|
|
1581
|
+
releasePending();
|
|
1405
1582
|
if (session !== void 0) {
|
|
1406
|
-
this.sessions.remove(session
|
|
1407
|
-
|
|
1583
|
+
if (this.sessions.remove(session)) {
|
|
1584
|
+
log("session_disconnected", { sessionId: session.id });
|
|
1585
|
+
}
|
|
1408
1586
|
}
|
|
1409
1587
|
});
|
|
1410
1588
|
socket.on("error", (err) => {
|
|
1411
1589
|
log("socket_error", { error: err.message });
|
|
1412
1590
|
});
|
|
1413
1591
|
}
|
|
1592
|
+
#originAllowed(origin) {
|
|
1593
|
+
if (origin === void 0)
|
|
1594
|
+
return true;
|
|
1595
|
+
const normalized = normalizeOrigin(origin);
|
|
1596
|
+
if (normalized === null)
|
|
1597
|
+
return false;
|
|
1598
|
+
if (this.#allowedOrigins.has(normalized))
|
|
1599
|
+
return true;
|
|
1600
|
+
return isLoopbackHostname(new URL(normalized).hostname);
|
|
1601
|
+
}
|
|
1414
1602
|
#parse(text) {
|
|
1415
1603
|
let json;
|
|
1416
1604
|
try {
|
|
@@ -1427,6 +1615,8 @@ var Bridge = class {
|
|
|
1427
1615
|
}
|
|
1428
1616
|
close() {
|
|
1429
1617
|
return new Promise((resolve) => {
|
|
1618
|
+
for (const client of this.#wss.clients)
|
|
1619
|
+
client.terminate();
|
|
1430
1620
|
this.#wss.close(() => {
|
|
1431
1621
|
resolve();
|
|
1432
1622
|
});
|
|
@@ -1541,6 +1731,7 @@ var IrisTool = {
|
|
|
1541
1731
|
STATE: "iris_state",
|
|
1542
1732
|
CAPABILITIES: "iris_capabilities",
|
|
1543
1733
|
CONTRACT_SAVE: "iris_contract_save",
|
|
1734
|
+
DOMAIN: "iris_domain",
|
|
1544
1735
|
FLOW_SAVE: "iris_flow_save",
|
|
1545
1736
|
FLOW_LIST: "iris_flow_list",
|
|
1546
1737
|
FLOW_LOAD: "iris_flow_load",
|
|
@@ -1583,189 +1774,558 @@ var IrisTool = {
|
|
|
1583
1774
|
ROLLBACK: "iris_rollback"
|
|
1584
1775
|
};
|
|
1585
1776
|
|
|
1586
|
-
// ../server/dist/
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
baselines: join(root, IrisDir.BASELINES_SUBDIR),
|
|
1594
|
-
project: join(root, IrisDir.PROJECT_FILE),
|
|
1595
|
-
visual: join(root, IrisDir.VISUAL_SUBDIR)
|
|
1596
|
-
};
|
|
1597
|
-
}
|
|
1598
|
-
function visualPath(root, name) {
|
|
1599
|
-
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.png`);
|
|
1600
|
-
}
|
|
1601
|
-
function visualDiffPath(root, name) {
|
|
1602
|
-
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.diff.png`);
|
|
1603
|
-
}
|
|
1604
|
-
function flowPath(root, name) {
|
|
1605
|
-
return join(root, IrisDir.FLOWS_SUBDIR, `${name}.json`);
|
|
1606
|
-
}
|
|
1607
|
-
function isValidFlowName(name) {
|
|
1608
|
-
return FLOW_NAME_PATTERN.test(name) && !name.includes("..");
|
|
1609
|
-
}
|
|
1610
|
-
async function ensureIrisDir(fs2, root) {
|
|
1611
|
-
const p = irisDirPaths(root);
|
|
1612
|
-
await fs2.mkdir(p.root);
|
|
1613
|
-
await fs2.mkdir(p.flows);
|
|
1614
|
-
await fs2.mkdir(p.baselines);
|
|
1615
|
-
}
|
|
1616
|
-
var JSON_INDENT = 2;
|
|
1617
|
-
function stableSerialize(capabilities, generatedAt) {
|
|
1618
|
-
const envelope = {
|
|
1619
|
-
version: CONTRACT_FILE_VERSION,
|
|
1620
|
-
generatedAt,
|
|
1621
|
-
capabilities: {
|
|
1622
|
-
testids: [...capabilities.testids].sort(),
|
|
1623
|
-
signals: [...capabilities.signals].sort(),
|
|
1624
|
-
stores: [...capabilities.stores].sort(),
|
|
1625
|
-
flows: [...capabilities.flows].map((f) => ({ name: f.name, steps: [...f.steps] })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
|
|
1777
|
+
// ../server/dist/tools/tools-helpers.js
|
|
1778
|
+
function parseInteractive(tree) {
|
|
1779
|
+
const items = [];
|
|
1780
|
+
for (const line of tree.split("\n")) {
|
|
1781
|
+
const match = /\(ref=(e\d+)\)/.exec(line);
|
|
1782
|
+
if (match !== null) {
|
|
1783
|
+
items.push({ ref: match[1] ?? "", desc: line.replace(/\s*\(ref=e\d+\)/, "").trim() });
|
|
1626
1784
|
}
|
|
1627
|
-
};
|
|
1628
|
-
return `${JSON.stringify(envelope, null, JSON_INDENT)}
|
|
1629
|
-
`;
|
|
1630
|
-
}
|
|
1631
|
-
async function writeContract(fs2, root, capabilities, now) {
|
|
1632
|
-
await ensureIrisDir(fs2, root);
|
|
1633
|
-
await fs2.writeFile(irisDirPaths(root).contract, stableSerialize(capabilities, now()));
|
|
1634
|
-
}
|
|
1635
|
-
async function readContract(fs2, root) {
|
|
1636
|
-
const path = irisDirPaths(root).contract;
|
|
1637
|
-
if (!await fs2.exists(path))
|
|
1638
|
-
return { ok: false, reason: ContractReadError.MISSING };
|
|
1639
|
-
let text;
|
|
1640
|
-
try {
|
|
1641
|
-
text = await fs2.readFile(path);
|
|
1642
|
-
} catch (error) {
|
|
1643
|
-
return {
|
|
1644
|
-
ok: false,
|
|
1645
|
-
reason: fs2.isNotFound(error) ? ContractReadError.MISSING : ContractReadError.MALFORMED
|
|
1646
|
-
};
|
|
1647
|
-
}
|
|
1648
|
-
let parsed;
|
|
1649
|
-
try {
|
|
1650
|
-
parsed = JSON.parse(text);
|
|
1651
|
-
} catch {
|
|
1652
|
-
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
1653
1785
|
}
|
|
1654
|
-
|
|
1655
|
-
if (!result.success)
|
|
1656
|
-
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
1657
|
-
return { ok: true, capabilities: result.data.capabilities, generatedAt: result.data.generatedAt };
|
|
1786
|
+
return items;
|
|
1658
1787
|
}
|
|
1659
|
-
|
|
1660
|
-
// ../server/dist/flows/flows.js
|
|
1661
1788
|
function asString(value) {
|
|
1662
1789
|
return typeof value === "string" ? value : void 0;
|
|
1663
1790
|
}
|
|
1791
|
+
function asNumber(value) {
|
|
1792
|
+
return typeof value === "number" ? value : void 0;
|
|
1793
|
+
}
|
|
1664
1794
|
function asRecord(value) {
|
|
1665
1795
|
return typeof value === "object" && value !== null ? value : {};
|
|
1666
1796
|
}
|
|
1667
|
-
|
|
1668
|
-
|
|
1797
|
+
|
|
1798
|
+
// ../server/dist/flows/replay.js
|
|
1799
|
+
function asString2(value) {
|
|
1800
|
+
return typeof value === "string" ? value : void 0;
|
|
1669
1801
|
}
|
|
1670
|
-
function
|
|
1671
|
-
|
|
1672
|
-
const by = asString(sub["by"]);
|
|
1673
|
-
const value = asString(sub["value"]);
|
|
1674
|
-
const action = asString(sub["action"]);
|
|
1675
|
-
const args = asRecord(sub["args"]);
|
|
1676
|
-
if (by === QueryBy.TESTID && value !== void 0) {
|
|
1677
|
-
return buildStep(IrisTool.ACT, { kind: AnchorKind.TESTID, value }, action, args, false);
|
|
1678
|
-
}
|
|
1679
|
-
return buildStep(IrisTool.ACT, degradedAnchor(), action, args, true);
|
|
1802
|
+
function asRecord2(value) {
|
|
1803
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
1680
1804
|
}
|
|
1681
|
-
function
|
|
1682
|
-
const
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
return step;
|
|
1805
|
+
function replayActionArgs(value, confirmDangerous = false) {
|
|
1806
|
+
const args = { ...asRecord2(value) };
|
|
1807
|
+
delete args[DANGEROUS_ACTION_CONFIRM_ARG];
|
|
1808
|
+
if (confirmDangerous)
|
|
1809
|
+
args[DANGEROUS_ACTION_CONFIRM_ARG] = true;
|
|
1810
|
+
return args;
|
|
1688
1811
|
}
|
|
1689
|
-
function
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
out2.expect = step.expect;
|
|
1700
|
-
return out2;
|
|
1812
|
+
function compileActStep(args, res) {
|
|
1813
|
+
const testid = asString2(asRecord2(res)["testid"]);
|
|
1814
|
+
const action = asString2(args["action"]) ?? "";
|
|
1815
|
+
const actArgs = replayActionArgs(args["args"]);
|
|
1816
|
+
if (testid !== void 0) {
|
|
1817
|
+
return {
|
|
1818
|
+
tool: IrisTool.ACT,
|
|
1819
|
+
stable: true,
|
|
1820
|
+
args: { by: QueryBy.TESTID, value: testid, action, args: actArgs }
|
|
1821
|
+
};
|
|
1701
1822
|
}
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
if (step.expect !== void 0)
|
|
1708
|
-
out.expect = step.expect;
|
|
1709
|
-
return out;
|
|
1823
|
+
return {
|
|
1824
|
+
tool: IrisTool.ACT,
|
|
1825
|
+
stable: false,
|
|
1826
|
+
args: { ref: asString2(args["ref"]) ?? "", action, args: actArgs }
|
|
1827
|
+
};
|
|
1710
1828
|
}
|
|
1711
|
-
function
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1829
|
+
function compileSequenceStep(args, res) {
|
|
1830
|
+
const inputSteps = Array.isArray(args["steps"]) ? args["steps"] : [];
|
|
1831
|
+
const resolved = Array.isArray(asRecord2(res)["steps"]) ? asRecord2(res)["steps"] : [];
|
|
1832
|
+
let stable = inputSteps.length > 0;
|
|
1833
|
+
const subSteps = inputSteps.map((raw, i) => {
|
|
1834
|
+
const step = asRecord2(raw);
|
|
1835
|
+
const action = asString2(step["action"]) ?? "";
|
|
1836
|
+
const stepArgs = replayActionArgs(step["args"]);
|
|
1837
|
+
const testid = asString2(asRecord2(resolved[i])["testid"]);
|
|
1838
|
+
if (testid !== void 0) {
|
|
1839
|
+
return { by: QueryBy.TESTID, value: testid, action, args: stepArgs };
|
|
1840
|
+
}
|
|
1841
|
+
stable = false;
|
|
1842
|
+
return { ref: asString2(step["ref"]) ?? "", action, args: stepArgs };
|
|
1717
1843
|
});
|
|
1718
|
-
|
|
1719
|
-
if (ann.dynamic.length > 0) {
|
|
1720
|
-
out.dynamic = ann.dynamic.map((value) => ({ kind: AnchorKind.TESTID, value }));
|
|
1721
|
-
}
|
|
1722
|
-
if (ann.success !== void 0)
|
|
1723
|
-
out.success = ann.success;
|
|
1724
|
-
return out;
|
|
1844
|
+
return { tool: IrisTool.ACT_SEQUENCE, stable, args: { steps: subSteps } };
|
|
1725
1845
|
}
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
* The single byte-stable flow serializer: 2-space indent + one trailing newline. save(),
|
|
1739
|
-
* saveFlow() and heal() all route through it so an unchanged flow that round-trips through any
|
|
1740
|
-
* of them produces byte-identical on-disk content (locked by the byte-stability tests).
|
|
1741
|
-
*/
|
|
1742
|
-
#serialize(flow) {
|
|
1743
|
-
return `${JSON.stringify(flow, null, JSON_INDENT2)}
|
|
1744
|
-
`;
|
|
1846
|
+
async function resolveRef(session, step) {
|
|
1847
|
+
const by = asString2(step.by);
|
|
1848
|
+
const value = asString2(step.value);
|
|
1849
|
+
if (by === QueryBy.TESTID && value !== void 0) {
|
|
1850
|
+
const result = await session.command(IrisCommand.QUERY, { by, value });
|
|
1851
|
+
if (!result.ok)
|
|
1852
|
+
throw new Error(result.error ?? "query failed");
|
|
1853
|
+
const elements = Array.isArray(asRecord2(result.result)["elements"]) ? asRecord2(result.result)["elements"] : [];
|
|
1854
|
+
const ref2 = asString2(asRecord2(elements[0])["ref"]);
|
|
1855
|
+
if (ref2 === void 0)
|
|
1856
|
+
throw new Error(`testid '${value}' did not resolve in current page`);
|
|
1857
|
+
return elements.length > 1 ? { ref: ref2, note: `ambiguous testid '${value}', used first match` } : { ref: ref2 };
|
|
1745
1858
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1859
|
+
const ref = asString2(step.ref);
|
|
1860
|
+
if (ref === void 0 || ref.length === 0)
|
|
1861
|
+
throw new Error("step has no testid or ref to resolve");
|
|
1862
|
+
return { ref, note: "replayed by stale ref (not portable across sessions)" };
|
|
1863
|
+
}
|
|
1864
|
+
async function replayProgram(session, program, confirmDangerous = false) {
|
|
1865
|
+
const results = [];
|
|
1866
|
+
for (const step of program.steps) {
|
|
1867
|
+
try {
|
|
1868
|
+
if (step.tool === IrisTool.ACT_SEQUENCE) {
|
|
1869
|
+
const subs = Array.isArray(step.args["steps"]) ? step.args["steps"] : [];
|
|
1870
|
+
const notes = [];
|
|
1871
|
+
const liveSteps = [];
|
|
1872
|
+
for (const raw of subs) {
|
|
1873
|
+
const sub = asRecord2(raw);
|
|
1874
|
+
const { ref, note } = await resolveRef(session, sub);
|
|
1875
|
+
if (note !== void 0)
|
|
1876
|
+
notes.push(note);
|
|
1877
|
+
liveSteps.push({
|
|
1878
|
+
ref,
|
|
1879
|
+
action: asString2(sub["action"]) ?? "",
|
|
1880
|
+
args: replayActionArgs(sub["args"], confirmDangerous)
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
const r = await session.command(IrisCommand.ACT_SEQUENCE, { steps: liveSteps });
|
|
1884
|
+
results.push(buildResult(step.tool, r.ok, r.error, notes));
|
|
1885
|
+
if (!r.ok)
|
|
1886
|
+
break;
|
|
1887
|
+
} else {
|
|
1888
|
+
const { ref, note } = await resolveRef(session, step.args);
|
|
1889
|
+
const r = await session.command(IrisCommand.ACT, {
|
|
1890
|
+
ref,
|
|
1891
|
+
action: asString2(step.args["action"]) ?? "",
|
|
1892
|
+
args: replayActionArgs(step.args["args"], confirmDangerous)
|
|
1893
|
+
});
|
|
1894
|
+
results.push(buildResult(step.tool, r.ok, r.error, note !== void 0 ? [note] : []));
|
|
1895
|
+
if (!r.ok)
|
|
1896
|
+
break;
|
|
1897
|
+
}
|
|
1898
|
+
} catch (e) {
|
|
1899
|
+
results.push({
|
|
1900
|
+
tool: step.tool,
|
|
1901
|
+
ok: false,
|
|
1902
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1903
|
+
});
|
|
1904
|
+
break;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
return results;
|
|
1908
|
+
}
|
|
1909
|
+
function buildResult(tool, ok, error, notes) {
|
|
1910
|
+
const base = { tool, ok };
|
|
1911
|
+
if (!ok)
|
|
1912
|
+
base.error = error ?? "command failed";
|
|
1913
|
+
if (notes.length > 0)
|
|
1914
|
+
base.note = notes.join("; ");
|
|
1915
|
+
return base;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// ../server/dist/flows/flow-replay.js
|
|
1919
|
+
function editDistance(a, b) {
|
|
1920
|
+
const s = a.toLowerCase();
|
|
1921
|
+
const t = b.toLowerCase();
|
|
1922
|
+
const rows = s.length + 1;
|
|
1923
|
+
const cols = t.length + 1;
|
|
1924
|
+
const prev = new Array(cols);
|
|
1925
|
+
const curr = new Array(cols);
|
|
1926
|
+
for (let j = 0; j < cols; j++)
|
|
1927
|
+
prev[j] = j;
|
|
1928
|
+
for (let i = 1; i < rows; i++) {
|
|
1929
|
+
curr[0] = i;
|
|
1930
|
+
for (let j = 1; j < cols; j++) {
|
|
1931
|
+
const cost = s[i - 1] === t[j - 1] ? 0 : 1;
|
|
1932
|
+
curr[j] = Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
|
|
1933
|
+
}
|
|
1934
|
+
for (let j = 0; j < cols; j++)
|
|
1935
|
+
prev[j] = curr[j] ?? 0;
|
|
1936
|
+
}
|
|
1937
|
+
return prev[cols - 1] ?? 0;
|
|
1938
|
+
}
|
|
1939
|
+
function nearestTestid(missing, present) {
|
|
1940
|
+
let best = null;
|
|
1941
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
1942
|
+
for (const candidate of present) {
|
|
1943
|
+
const distance = editDistance(missing, candidate);
|
|
1944
|
+
if (distance < bestDistance || distance === bestDistance && best !== null && candidate.length < best.length || distance === bestDistance && best !== null && candidate.length === best.length && candidate < best) {
|
|
1945
|
+
best = candidate;
|
|
1946
|
+
bestDistance = distance;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
return best;
|
|
1950
|
+
}
|
|
1951
|
+
function readQuery(result) {
|
|
1952
|
+
if (!result.ok)
|
|
1953
|
+
return { refs: [] };
|
|
1954
|
+
const payload = asRecord(result.result);
|
|
1955
|
+
const elements = Array.isArray(payload["elements"]) ? payload["elements"] : [];
|
|
1956
|
+
const refs = elements.map((e) => asString(asRecord(e)["ref"]) ?? "").filter((r) => r.length > 0);
|
|
1957
|
+
const rawHint = payload["hint"];
|
|
1958
|
+
if (typeof rawHint === "object" && rawHint !== null) {
|
|
1959
|
+
const hint = asRecord(rawHint);
|
|
1960
|
+
const present = Array.isArray(hint["presentTestids"]) ? hint["presentTestids"].filter((t) => typeof t === "string") : [];
|
|
1961
|
+
return {
|
|
1962
|
+
refs,
|
|
1963
|
+
hint: {
|
|
1964
|
+
route: asString(hint["route"]) ?? "",
|
|
1965
|
+
presentTestids: present,
|
|
1966
|
+
presentRegions: [],
|
|
1967
|
+
knownEmptyState: hint["knownEmptyState"] === true
|
|
1968
|
+
}
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
return { refs };
|
|
1972
|
+
}
|
|
1973
|
+
function nearestIsAmbiguous(missing, present) {
|
|
1974
|
+
if (present.length < 2)
|
|
1975
|
+
return false;
|
|
1976
|
+
let min = Number.POSITIVE_INFINITY;
|
|
1977
|
+
let count = 0;
|
|
1978
|
+
for (const candidate of present) {
|
|
1979
|
+
const distance = editDistance(missing, candidate);
|
|
1980
|
+
if (distance < min) {
|
|
1981
|
+
min = distance;
|
|
1982
|
+
count = 1;
|
|
1983
|
+
} else if (distance === min) {
|
|
1984
|
+
count += 1;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
return count >= 2;
|
|
1988
|
+
}
|
|
1989
|
+
function testidDrift(value, hint) {
|
|
1990
|
+
const present = hint?.presentTestids ?? [];
|
|
1991
|
+
const drift = {
|
|
1992
|
+
reasonKind: DriftReason.TESTID_NOT_FOUND,
|
|
1993
|
+
reason: `testid "${value}" not found`,
|
|
1994
|
+
anchor: value,
|
|
1995
|
+
nearest: nearestTestid(value, present)
|
|
1996
|
+
};
|
|
1997
|
+
if (nearestIsAmbiguous(value, present))
|
|
1998
|
+
drift.ambiguous = true;
|
|
1999
|
+
return drift;
|
|
2000
|
+
}
|
|
2001
|
+
function anchorLabel(anchor) {
|
|
2002
|
+
if (anchor.kind === AnchorKind.TESTID)
|
|
2003
|
+
return anchor.value;
|
|
2004
|
+
if (anchor.kind === AnchorKind.SIGNAL)
|
|
2005
|
+
return anchor.name;
|
|
2006
|
+
return anchor.name ?? anchor.role;
|
|
2007
|
+
}
|
|
2008
|
+
async function runTestidStep(session, step, index, value, dynamic, confirmDangerous) {
|
|
2009
|
+
const queryResult = await session.command(IrisCommand.QUERY, { by: QueryBy.TESTID, value });
|
|
2010
|
+
const { refs, hint } = readQuery(queryResult);
|
|
2011
|
+
if (refs.length === 0) {
|
|
2012
|
+
return {
|
|
2013
|
+
step: index,
|
|
2014
|
+
tool: step.tool,
|
|
2015
|
+
anchor: value,
|
|
2016
|
+
ok: false,
|
|
2017
|
+
drift: testidDrift(value, hint)
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
const ref = refs[0] ?? "";
|
|
2021
|
+
const note = refs.length > 1 ? `ambiguous testid '${value}', used first match` : void 0;
|
|
2022
|
+
const act = await session.command(IrisCommand.ACT, {
|
|
2023
|
+
ref,
|
|
2024
|
+
action: step.action ?? "",
|
|
2025
|
+
args: replayActionArgs(step.args, confirmDangerous)
|
|
2026
|
+
});
|
|
2027
|
+
const result = { step: index, tool: step.tool, anchor: value, ok: act.ok };
|
|
2028
|
+
if (!act.ok) {
|
|
2029
|
+
result.error = act.error ?? "command failed";
|
|
2030
|
+
if (note !== void 0)
|
|
2031
|
+
result.note = note;
|
|
2032
|
+
return result;
|
|
2033
|
+
}
|
|
2034
|
+
const expectTestid = step.expect?.element?.testid;
|
|
2035
|
+
if (expectTestid !== void 0 && !dynamic.has(expectTestid)) {
|
|
2036
|
+
const expectQuery = await session.command(IrisCommand.QUERY, {
|
|
2037
|
+
by: QueryBy.TESTID,
|
|
2038
|
+
value: expectTestid
|
|
2039
|
+
});
|
|
2040
|
+
const expectRefs = readQuery(expectQuery);
|
|
2041
|
+
if (expectRefs.refs.length === 0) {
|
|
2042
|
+
return {
|
|
2043
|
+
step: index,
|
|
2044
|
+
tool: step.tool,
|
|
2045
|
+
anchor: expectTestid,
|
|
2046
|
+
ok: false,
|
|
2047
|
+
drift: testidDrift(expectTestid, expectRefs.hint)
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
if (note !== void 0)
|
|
2052
|
+
result.note = note;
|
|
2053
|
+
return result;
|
|
2054
|
+
}
|
|
2055
|
+
async function runSignalStep(session, step, index, name, waitForSignal, signalTimeoutMs) {
|
|
2056
|
+
const verdict = await waitForSignal(session, { kind: "signal", name }, signalTimeoutMs);
|
|
2057
|
+
if (verdict.pass)
|
|
2058
|
+
return { step: index, tool: step.tool, anchor: name, ok: true };
|
|
2059
|
+
return {
|
|
2060
|
+
step: index,
|
|
2061
|
+
tool: step.tool,
|
|
2062
|
+
anchor: name,
|
|
2063
|
+
ok: false,
|
|
2064
|
+
drift: {
|
|
2065
|
+
reasonKind: DriftReason.SIGNAL_NOT_OBSERVED,
|
|
2066
|
+
reason: `signal "${name}" not observed`,
|
|
2067
|
+
anchor: name,
|
|
2068
|
+
nearest: null
|
|
2069
|
+
}
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
async function replayFlow(session, flow, waitForSignal, signalTimeoutMs, confirmDangerous = false) {
|
|
2073
|
+
const results = [];
|
|
2074
|
+
const dynamic = new Set((flow.dynamic ?? []).filter((a) => a.kind === AnchorKind.TESTID).map((a) => a.kind === AnchorKind.TESTID ? a.value : ""));
|
|
2075
|
+
let index = 0;
|
|
2076
|
+
for (const step of flow.steps) {
|
|
2077
|
+
const label = anchorLabel(step.anchor);
|
|
2078
|
+
let result;
|
|
2079
|
+
if (step.anchor.kind === AnchorKind.SIGNAL) {
|
|
2080
|
+
result = await runSignalStep(session, step, index, label, waitForSignal, signalTimeoutMs);
|
|
2081
|
+
} else {
|
|
2082
|
+
result = await runTestidStep(session, step, index, label, dynamic, confirmDangerous);
|
|
2083
|
+
}
|
|
2084
|
+
results.push(result);
|
|
2085
|
+
if (result.drift !== void 0 || !result.ok)
|
|
2086
|
+
break;
|
|
2087
|
+
index += 1;
|
|
2088
|
+
}
|
|
2089
|
+
return results;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// ../server/dist/flows/heal.js
|
|
2093
|
+
function confidenceFor(from, to) {
|
|
2094
|
+
if (from === to)
|
|
2095
|
+
return 1;
|
|
2096
|
+
const span = Math.max(from.length, to.length);
|
|
2097
|
+
if (span === 0)
|
|
2098
|
+
return 1;
|
|
2099
|
+
const raw = 1 - editDistance(from, to) / span;
|
|
2100
|
+
if (raw >= 1)
|
|
2101
|
+
return 1;
|
|
2102
|
+
if (raw <= 0)
|
|
2103
|
+
return Number.EPSILON;
|
|
2104
|
+
return raw;
|
|
2105
|
+
}
|
|
2106
|
+
function applyHealChanges(flow, changes) {
|
|
2107
|
+
const byStep = /* @__PURE__ */ new Map();
|
|
2108
|
+
for (const change of changes)
|
|
2109
|
+
byStep.set(change.step, change);
|
|
2110
|
+
const applied = [];
|
|
2111
|
+
const steps = flow.steps.map((step, index) => {
|
|
2112
|
+
const change = byStep.get(index);
|
|
2113
|
+
if (change === void 0 || step.anchor.kind !== AnchorKind.TESTID || step.anchor.value !== change.from) {
|
|
2114
|
+
return step;
|
|
2115
|
+
}
|
|
2116
|
+
applied.push(change);
|
|
2117
|
+
return { ...step, anchor: { kind: AnchorKind.TESTID, value: change.to } };
|
|
2118
|
+
});
|
|
2119
|
+
return { flow: { ...flow, steps }, applied };
|
|
2120
|
+
}
|
|
2121
|
+
function proposeRebindWith(drift, step, minConfidence) {
|
|
2122
|
+
if (drift.reasonKind !== DriftReason.TESTID_NOT_FOUND)
|
|
2123
|
+
return void 0;
|
|
2124
|
+
if (drift.ambiguous === true)
|
|
2125
|
+
return void 0;
|
|
2126
|
+
const to = drift.nearest;
|
|
2127
|
+
if (to === null)
|
|
2128
|
+
return void 0;
|
|
2129
|
+
const confidence = confidenceFor(drift.anchor, to);
|
|
2130
|
+
if (confidence < minConfidence)
|
|
2131
|
+
return void 0;
|
|
2132
|
+
return { step, from: drift.anchor, to, confidence };
|
|
2133
|
+
}
|
|
2134
|
+
function collectProposals(steps, minConfidence = HEAL_CONFIDENCE_MIN) {
|
|
2135
|
+
const proposals = [];
|
|
2136
|
+
for (const step of steps) {
|
|
2137
|
+
if (step.drift === void 0)
|
|
2138
|
+
continue;
|
|
2139
|
+
const proposal = proposeRebindWith(step.drift, step.step, minConfidence);
|
|
2140
|
+
if (proposal !== void 0)
|
|
2141
|
+
proposals.push(proposal);
|
|
2142
|
+
}
|
|
2143
|
+
return proposals;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// ../server/dist/project/iris-dir.js
|
|
2147
|
+
import { join } from "path";
|
|
2148
|
+
function irisDirPaths(root) {
|
|
2149
|
+
return {
|
|
2150
|
+
root,
|
|
2151
|
+
contract: join(root, IrisDir.CONTRACT_FILE),
|
|
2152
|
+
flows: join(root, IrisDir.FLOWS_SUBDIR),
|
|
2153
|
+
baselines: join(root, IrisDir.BASELINES_SUBDIR),
|
|
2154
|
+
project: join(root, IrisDir.PROJECT_FILE),
|
|
2155
|
+
visual: join(root, IrisDir.VISUAL_SUBDIR)
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
function visualPath(root, name) {
|
|
2159
|
+
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.png`);
|
|
2160
|
+
}
|
|
2161
|
+
function visualDiffPath(root, name) {
|
|
2162
|
+
return join(root, IrisDir.VISUAL_SUBDIR, `${name}.diff.png`);
|
|
2163
|
+
}
|
|
2164
|
+
function flowPath(root, name) {
|
|
2165
|
+
return join(root, IrisDir.FLOWS_SUBDIR, `${name}.json`);
|
|
2166
|
+
}
|
|
2167
|
+
function isValidFlowName(name) {
|
|
2168
|
+
return FLOW_NAME_PATTERN.test(name) && !name.includes("..");
|
|
2169
|
+
}
|
|
2170
|
+
async function ensureIrisDir(fs2, root) {
|
|
2171
|
+
const p = irisDirPaths(root);
|
|
2172
|
+
await fs2.mkdir(p.root);
|
|
2173
|
+
await fs2.mkdir(p.flows);
|
|
2174
|
+
await fs2.mkdir(p.baselines);
|
|
2175
|
+
}
|
|
2176
|
+
var JSON_INDENT = 2;
|
|
2177
|
+
function stableSerialize(capabilities, generatedAt) {
|
|
2178
|
+
const envelope = {
|
|
2179
|
+
version: CONTRACT_FILE_VERSION,
|
|
2180
|
+
generatedAt,
|
|
2181
|
+
capabilities: {
|
|
2182
|
+
testids: [...capabilities.testids].sort(),
|
|
2183
|
+
signals: [...capabilities.signals].sort(),
|
|
2184
|
+
stores: [...capabilities.stores].sort(),
|
|
2185
|
+
flows: [...capabilities.flows].map((f) => ({ name: f.name, steps: [...f.steps] })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
return `${JSON.stringify(envelope, null, JSON_INDENT)}
|
|
2189
|
+
`;
|
|
2190
|
+
}
|
|
2191
|
+
async function writeContract(fs2, root, capabilities, now) {
|
|
2192
|
+
await ensureIrisDir(fs2, root);
|
|
2193
|
+
await fs2.writeFile(irisDirPaths(root).contract, stableSerialize(capabilities, now()));
|
|
2194
|
+
}
|
|
2195
|
+
async function readContract(fs2, root) {
|
|
2196
|
+
const path = irisDirPaths(root).contract;
|
|
2197
|
+
if (!await fs2.exists(path))
|
|
2198
|
+
return { ok: false, reason: ContractReadError.MISSING };
|
|
2199
|
+
let text;
|
|
2200
|
+
try {
|
|
2201
|
+
text = await fs2.readFile(path);
|
|
2202
|
+
} catch (error) {
|
|
2203
|
+
return {
|
|
2204
|
+
ok: false,
|
|
2205
|
+
reason: fs2.isNotFound(error) ? ContractReadError.MISSING : ContractReadError.MALFORMED
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
let parsed;
|
|
2209
|
+
try {
|
|
2210
|
+
parsed = JSON.parse(text);
|
|
2211
|
+
} catch {
|
|
2212
|
+
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
2213
|
+
}
|
|
2214
|
+
const result = ContractFileSchema.safeParse(parsed);
|
|
2215
|
+
if (!result.success)
|
|
2216
|
+
return { ok: false, reason: ContractReadError.MALFORMED };
|
|
2217
|
+
return { ok: true, capabilities: result.data.capabilities, generatedAt: result.data.generatedAt };
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
// ../server/dist/flows/flows.js
|
|
2221
|
+
function asString3(value) {
|
|
2222
|
+
return typeof value === "string" ? value : void 0;
|
|
2223
|
+
}
|
|
2224
|
+
function asRecord3(value) {
|
|
2225
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
2226
|
+
}
|
|
2227
|
+
function degradedAnchor() {
|
|
2228
|
+
return { kind: AnchorKind.ROLE, role: DEGRADED_ANCHOR_ROLE };
|
|
2229
|
+
}
|
|
2230
|
+
function subStepToFlowStep(raw) {
|
|
2231
|
+
const sub = asRecord3(raw);
|
|
2232
|
+
const by = asString3(sub["by"]);
|
|
2233
|
+
const value = asString3(sub["value"]);
|
|
2234
|
+
const action = asString3(sub["action"]);
|
|
2235
|
+
const args = asRecord3(sub["args"]);
|
|
2236
|
+
if (by === QueryBy.TESTID && value !== void 0) {
|
|
2237
|
+
return buildStep(IrisTool.ACT, { kind: AnchorKind.TESTID, value }, action, args, false);
|
|
2238
|
+
}
|
|
2239
|
+
return buildStep(IrisTool.ACT, degradedAnchor(), action, args, true);
|
|
2240
|
+
}
|
|
2241
|
+
function buildStep(tool, anchor, action, args, degraded) {
|
|
2242
|
+
const step = { tool, anchor, args };
|
|
2243
|
+
if (action !== void 0)
|
|
2244
|
+
step.action = action;
|
|
2245
|
+
if (degraded)
|
|
2246
|
+
step.degraded = true;
|
|
2247
|
+
return step;
|
|
2248
|
+
}
|
|
2249
|
+
function recordedStepToFlowStep(step) {
|
|
2250
|
+
if (step.tool === IrisTool.ACT_SEQUENCE) {
|
|
2251
|
+
const rawSubs = Array.isArray(step.args["steps"]) ? step.args["steps"] : [];
|
|
2252
|
+
const subs = rawSubs.map(subStepToFlowStep);
|
|
2253
|
+
const degraded = subs.some((s) => s.degraded === true);
|
|
2254
|
+
const anchor = subs[0]?.anchor ?? degradedAnchor();
|
|
2255
|
+
const out2 = { tool: IrisTool.ACT_SEQUENCE, anchor, steps: subs };
|
|
2256
|
+
if (degraded)
|
|
2257
|
+
out2.degraded = true;
|
|
2258
|
+
if (step.expect !== void 0)
|
|
2259
|
+
out2.expect = step.expect;
|
|
2260
|
+
return out2;
|
|
2261
|
+
}
|
|
2262
|
+
const by = asString3(step.args["by"]);
|
|
2263
|
+
const value = asString3(step.args["value"]);
|
|
2264
|
+
const action = asString3(step.args["action"]);
|
|
2265
|
+
const args = asRecord3(step.args["args"]);
|
|
2266
|
+
const out = by === QueryBy.TESTID && value !== void 0 ? buildStep(step.tool, { kind: AnchorKind.TESTID, value }, action, args, false) : buildStep(step.tool, degradedAnchor(), action, args, true);
|
|
2267
|
+
if (step.expect !== void 0)
|
|
2268
|
+
out.expect = step.expect;
|
|
2269
|
+
return out;
|
|
2270
|
+
}
|
|
2271
|
+
function withAnnotations(flow, ann) {
|
|
2272
|
+
if (ann === void 0)
|
|
2273
|
+
return flow;
|
|
2274
|
+
const steps = flow.steps.map((step, i) => {
|
|
2275
|
+
const expect = ann.stepExpect.get(i);
|
|
2276
|
+
return expect === void 0 ? step : { ...step, expect };
|
|
2277
|
+
});
|
|
2278
|
+
const out = { ...flow, steps };
|
|
2279
|
+
if (ann.dynamic.length > 0) {
|
|
2280
|
+
out.dynamic = ann.dynamic.map((value) => ({ kind: AnchorKind.TESTID, value }));
|
|
2281
|
+
}
|
|
2282
|
+
if (ann.success !== void 0)
|
|
2283
|
+
out.success = ann.success;
|
|
2284
|
+
return out;
|
|
2285
|
+
}
|
|
2286
|
+
var JSON_INDENT2 = 2;
|
|
2287
|
+
var FLOW_SUFFIX = ".json";
|
|
2288
|
+
var FlowStore = class {
|
|
2289
|
+
#fs;
|
|
2290
|
+
#root;
|
|
2291
|
+
#clock;
|
|
2292
|
+
constructor(fs2, root, clock) {
|
|
2293
|
+
this.#fs = fs2;
|
|
2294
|
+
this.#root = root;
|
|
2295
|
+
this.#clock = clock;
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* The single byte-stable flow serializer: 2-space indent + one trailing newline. save(),
|
|
2299
|
+
* saveFlow() and heal() all route through it so an unchanged flow that round-trips through any
|
|
2300
|
+
* of them produces byte-identical on-disk content (locked by the byte-stability tests).
|
|
2301
|
+
*/
|
|
2302
|
+
#serialize(flow) {
|
|
2303
|
+
return `${JSON.stringify(flow, null, JSON_INDENT2)}
|
|
2304
|
+
`;
|
|
2305
|
+
}
|
|
2306
|
+
/**
|
|
2307
|
+
* Convert a CompiledProgram (testid-normalized) into an anchored, on-disk flow + write it.
|
|
2308
|
+
* Optionally fold structured annotations (per-step expect, dynamic[], success) onto
|
|
2309
|
+
* the flow before writing. Omitting `annotations` reproduces the same bytes.
|
|
2310
|
+
*/
|
|
2311
|
+
async save(program, annotations) {
|
|
2312
|
+
if (!isValidFlowName(program.name)) {
|
|
2313
|
+
return { ok: false, code: FlowErrorCode.INVALID_NAME };
|
|
2314
|
+
}
|
|
2315
|
+
const steps = program.steps.map(recordedStepToFlowStep);
|
|
2316
|
+
const base = {
|
|
2317
|
+
version: FLOW_FILE_VERSION,
|
|
2318
|
+
name: program.name,
|
|
2319
|
+
createdAt: this.#clock.now(),
|
|
2320
|
+
steps
|
|
2321
|
+
};
|
|
2322
|
+
const flow = withAnnotations(base, annotations);
|
|
2323
|
+
await this.#fs.mkdir(irisDirPaths(this.#root).flows);
|
|
2324
|
+
await this.#fs.writeFile(flowPath(this.#root, program.name), this.#serialize(flow));
|
|
2325
|
+
const degraded = flow.steps.filter((s) => s.degraded === true).length;
|
|
2326
|
+
return {
|
|
2327
|
+
ok: true,
|
|
2328
|
+
value: {
|
|
1769
2329
|
name: program.name,
|
|
1770
2330
|
stepCount: flow.steps.length,
|
|
1771
2331
|
degraded,
|
|
@@ -1816,19 +2376,7 @@ var FlowStore = class {
|
|
|
1816
2376
|
if (!loaded.ok)
|
|
1817
2377
|
return { ok: false, code: loaded.code };
|
|
1818
2378
|
const flow = loaded.value;
|
|
1819
|
-
const
|
|
1820
|
-
for (const change of changes)
|
|
1821
|
-
byStep.set(change.step, change);
|
|
1822
|
-
const applied = [];
|
|
1823
|
-
const steps = flow.steps.map((step, index) => {
|
|
1824
|
-
const change = byStep.get(index);
|
|
1825
|
-
if (change === void 0 || step.anchor.kind !== AnchorKind.TESTID || step.anchor.value !== change.from) {
|
|
1826
|
-
return step;
|
|
1827
|
-
}
|
|
1828
|
-
applied.push(change);
|
|
1829
|
-
return { ...step, anchor: { kind: AnchorKind.TESTID, value: change.to } };
|
|
1830
|
-
});
|
|
1831
|
-
const next = { ...flow, steps };
|
|
2379
|
+
const { flow: next, applied } = applyHealChanges(flow, changes);
|
|
1832
2380
|
await this.#fs.writeFile(flowPath(this.#root, name), this.#serialize(next));
|
|
1833
2381
|
return { ok: true, value: { name, changed: applied } };
|
|
1834
2382
|
}
|
|
@@ -2056,10 +2604,10 @@ function createNodeFileSystem() {
|
|
|
2056
2604
|
|
|
2057
2605
|
// ../server/dist/mcp.js
|
|
2058
2606
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2059
|
-
import { z as
|
|
2607
|
+
import { z as z17 } from "zod";
|
|
2060
2608
|
|
|
2061
2609
|
// ../server/dist/tools/tools.js
|
|
2062
|
-
import { z as
|
|
2610
|
+
import { z as z16 } from "zod";
|
|
2063
2611
|
|
|
2064
2612
|
// ../server/dist/input/real-input.js
|
|
2065
2613
|
var DriveError = class extends Error {
|
|
@@ -2120,8 +2668,10 @@ async function performGesture(page, action, box, args, sleep) {
|
|
|
2120
2668
|
}
|
|
2121
2669
|
return { performed: false, center };
|
|
2122
2670
|
}
|
|
2671
|
+
var HIDE_IRIS_CHROME_CSS = "[data-iris-overlay]{display:none !important}";
|
|
2672
|
+
var SCREENSHOT_DETERMINISM = { style: HIDE_IRIS_CHROME_CSS, animations: "disabled" };
|
|
2123
2673
|
async function capturePage(page, opts) {
|
|
2124
|
-
const buf = await page.screenshot(opts.clip !== void 0 ? { clip: opts.clip } : opts.fullPage === true ? { fullPage: true } : {});
|
|
2674
|
+
const buf = await page.screenshot(opts.clip !== void 0 ? { ...SCREENSHOT_DETERMINISM, clip: opts.clip } : opts.fullPage === true ? { ...SCREENSHOT_DETERMINISM, fullPage: true } : { ...SCREENSHOT_DETERMINISM });
|
|
2125
2675
|
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
2126
2676
|
}
|
|
2127
2677
|
var nodeSleep = (ms) => new Promise((resolve) => {
|
|
@@ -2356,6 +2906,7 @@ var PredicateSchema = z3.lazy(() => z3.discriminatedUnion("kind", [
|
|
|
2356
2906
|
name: z3.string().optional(),
|
|
2357
2907
|
dataMatches: z3.record(z3.unknown()).optional()
|
|
2358
2908
|
}),
|
|
2909
|
+
z3.object({ kind: z3.literal("settled"), quietMs: z3.number().positive().optional() }),
|
|
2359
2910
|
z3.object({ kind: z3.literal("allOf"), predicates: z3.array(PredicateSchema) }),
|
|
2360
2911
|
z3.object({ kind: z3.literal("anyOf"), predicates: z3.array(PredicateSchema) }),
|
|
2361
2912
|
z3.object({ kind: z3.literal("not"), predicate: PredicateSchema })
|
|
@@ -2552,6 +3103,39 @@ function evalSignal(events, p) {
|
|
|
2552
3103
|
evidence: sameName.length > 0 ? { nearMiss: sameName } : void 0
|
|
2553
3104
|
};
|
|
2554
3105
|
}
|
|
3106
|
+
var SETTLE_ACTIVITY = /* @__PURE__ */ new Set([
|
|
3107
|
+
EventType.NET_REQUEST,
|
|
3108
|
+
EventType.DOM_ADDED,
|
|
3109
|
+
EventType.DOM_REMOVED,
|
|
3110
|
+
EventType.DOM_ATTR
|
|
3111
|
+
]);
|
|
3112
|
+
var DEFAULT_QUIET_MS = 500;
|
|
3113
|
+
function evalSettled(events, p, now) {
|
|
3114
|
+
const quietMs = p.quietMs ?? DEFAULT_QUIET_MS;
|
|
3115
|
+
let lastT = -1;
|
|
3116
|
+
let lastType;
|
|
3117
|
+
for (const e of events) {
|
|
3118
|
+
if (SETTLE_ACTIVITY.has(e.type) && e.t > lastT) {
|
|
3119
|
+
lastT = e.t;
|
|
3120
|
+
lastType = e.type;
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
if (lastT < 0) {
|
|
3124
|
+
return {
|
|
3125
|
+
pass: true,
|
|
3126
|
+
evidence: { settled: true, quietForMs: null, note: "no activity to settle" }
|
|
3127
|
+
};
|
|
3128
|
+
}
|
|
3129
|
+
const quietForMs = now - lastT;
|
|
3130
|
+
if (quietForMs >= quietMs) {
|
|
3131
|
+
return { pass: true, evidence: { settled: true, quietForMs, lastActivity: lastType } };
|
|
3132
|
+
}
|
|
3133
|
+
return {
|
|
3134
|
+
pass: false,
|
|
3135
|
+
failureReason: `not settled: last activity (${String(lastType)}) ${String(quietForMs)}ms ago, need ${String(quietMs)}ms quiet`,
|
|
3136
|
+
evidence: { quietForMs, lastActivity: lastType }
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
2555
3139
|
async function evaluatePredicate(session, predicate, since = 0) {
|
|
2556
3140
|
const events = session.eventsSince(since);
|
|
2557
3141
|
switch (predicate.kind) {
|
|
@@ -2569,6 +3153,8 @@ async function evaluatePredicate(session, predicate, since = 0) {
|
|
|
2569
3153
|
return evalAnimation(events, predicate);
|
|
2570
3154
|
case "signal":
|
|
2571
3155
|
return evalSignal(events, predicate);
|
|
3156
|
+
case "settled":
|
|
3157
|
+
return evalSettled(events, predicate, session.elapsed());
|
|
2572
3158
|
case "allOf": {
|
|
2573
3159
|
const results = await Promise.all(predicate.predicates.map((p) => evaluatePredicate(session, p, since)));
|
|
2574
3160
|
const failed = results.find((r) => !r.pass);
|
|
@@ -2594,6 +3180,10 @@ async function evaluatePredicate(session, predicate, since = 0) {
|
|
|
2594
3180
|
function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
2595
3181
|
return new Promise((resolve) => {
|
|
2596
3182
|
let done = false;
|
|
3183
|
+
const failed = (error) => ({
|
|
3184
|
+
pass: false,
|
|
3185
|
+
failureReason: error instanceof Error ? error.message : String(error)
|
|
3186
|
+
});
|
|
2597
3187
|
const finish = (result) => {
|
|
2598
3188
|
if (done)
|
|
2599
3189
|
return;
|
|
@@ -2607,6 +3197,8 @@ function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
|
2607
3197
|
void evaluatePredicate(session, predicate, since).then((r) => {
|
|
2608
3198
|
if (r.pass)
|
|
2609
3199
|
finish(r);
|
|
3200
|
+
}).catch((error) => {
|
|
3201
|
+
finish(failed(error));
|
|
2610
3202
|
});
|
|
2611
3203
|
};
|
|
2612
3204
|
const unsub = session.onEvent(() => {
|
|
@@ -2615,146 +3207,35 @@ function waitForPredicate(session, predicate, timeoutMs, since = 0) {
|
|
|
2615
3207
|
const interval = setInterval(check, 150);
|
|
2616
3208
|
const timer = setTimeout(() => {
|
|
2617
3209
|
void evaluatePredicate(session, predicate, since).then((r) => {
|
|
2618
|
-
finish({
|
|
2619
|
-
pass: false,
|
|
2620
|
-
evidence: r.evidence,
|
|
2621
|
-
failureReason: r.failureReason ?? "timed out waiting for predicate"
|
|
2622
|
-
});
|
|
2623
|
-
});
|
|
2624
|
-
}, timeoutMs);
|
|
2625
|
-
check();
|
|
2626
|
-
});
|
|
2627
|
-
}
|
|
2628
|
-
|
|
2629
|
-
// ../server/dist/flows/replay.js
|
|
2630
|
-
function asString2(value) {
|
|
2631
|
-
return typeof value === "string" ? value : void 0;
|
|
2632
|
-
}
|
|
2633
|
-
function asRecord2(value) {
|
|
2634
|
-
return typeof value === "object" && value !== null ? value : {};
|
|
2635
|
-
}
|
|
2636
|
-
function compileActStep(args, res) {
|
|
2637
|
-
const testid = asString2(asRecord2(res)["testid"]);
|
|
2638
|
-
const action = asString2(args["action"]) ?? "";
|
|
2639
|
-
const actArgs = asRecord2(args["args"]);
|
|
2640
|
-
if (testid !== void 0) {
|
|
2641
|
-
return {
|
|
2642
|
-
tool: IrisTool.ACT,
|
|
2643
|
-
stable: true,
|
|
2644
|
-
args: { by: QueryBy.TESTID, value: testid, action, args: actArgs }
|
|
2645
|
-
};
|
|
2646
|
-
}
|
|
2647
|
-
return {
|
|
2648
|
-
tool: IrisTool.ACT,
|
|
2649
|
-
stable: false,
|
|
2650
|
-
args: { ref: asString2(args["ref"]) ?? "", action, args: actArgs }
|
|
2651
|
-
};
|
|
2652
|
-
}
|
|
2653
|
-
function compileSequenceStep(args, res) {
|
|
2654
|
-
const inputSteps = Array.isArray(args["steps"]) ? args["steps"] : [];
|
|
2655
|
-
const resolved = Array.isArray(asRecord2(res)["steps"]) ? asRecord2(res)["steps"] : [];
|
|
2656
|
-
let stable = inputSteps.length > 0;
|
|
2657
|
-
const subSteps = inputSteps.map((raw, i) => {
|
|
2658
|
-
const step = asRecord2(raw);
|
|
2659
|
-
const action = asString2(step["action"]) ?? "";
|
|
2660
|
-
const stepArgs = asRecord2(step["args"]);
|
|
2661
|
-
const testid = asString2(asRecord2(resolved[i])["testid"]);
|
|
2662
|
-
if (testid !== void 0) {
|
|
2663
|
-
return { by: QueryBy.TESTID, value: testid, action, args: stepArgs };
|
|
2664
|
-
}
|
|
2665
|
-
stable = false;
|
|
2666
|
-
return { ref: asString2(step["ref"]) ?? "", action, args: stepArgs };
|
|
2667
|
-
});
|
|
2668
|
-
return { tool: IrisTool.ACT_SEQUENCE, stable, args: { steps: subSteps } };
|
|
2669
|
-
}
|
|
2670
|
-
async function resolveRef(session, step) {
|
|
2671
|
-
const by = asString2(step.by);
|
|
2672
|
-
const value = asString2(step.value);
|
|
2673
|
-
if (by === QueryBy.TESTID && value !== void 0) {
|
|
2674
|
-
const result = await session.command(IrisCommand.QUERY, { by, value });
|
|
2675
|
-
if (!result.ok)
|
|
2676
|
-
throw new Error(result.error ?? "query failed");
|
|
2677
|
-
const elements = Array.isArray(asRecord2(result.result)["elements"]) ? asRecord2(result.result)["elements"] : [];
|
|
2678
|
-
const ref2 = asString2(asRecord2(elements[0])["ref"]);
|
|
2679
|
-
if (ref2 === void 0)
|
|
2680
|
-
throw new Error(`testid '${value}' did not resolve in current page`);
|
|
2681
|
-
return elements.length > 1 ? { ref: ref2, note: `ambiguous testid '${value}', used first match` } : { ref: ref2 };
|
|
2682
|
-
}
|
|
2683
|
-
const ref = asString2(step.ref);
|
|
2684
|
-
if (ref === void 0 || ref.length === 0)
|
|
2685
|
-
throw new Error("step has no testid or ref to resolve");
|
|
2686
|
-
return { ref, note: "replayed by stale ref (not portable across sessions)" };
|
|
2687
|
-
}
|
|
2688
|
-
async function replayProgram(session, program) {
|
|
2689
|
-
const results = [];
|
|
2690
|
-
for (const step of program.steps) {
|
|
2691
|
-
try {
|
|
2692
|
-
if (step.tool === IrisTool.ACT_SEQUENCE) {
|
|
2693
|
-
const subs = Array.isArray(step.args["steps"]) ? step.args["steps"] : [];
|
|
2694
|
-
const notes = [];
|
|
2695
|
-
const liveSteps = [];
|
|
2696
|
-
for (const raw of subs) {
|
|
2697
|
-
const sub = asRecord2(raw);
|
|
2698
|
-
const { ref, note } = await resolveRef(session, sub);
|
|
2699
|
-
if (note !== void 0)
|
|
2700
|
-
notes.push(note);
|
|
2701
|
-
liveSteps.push({
|
|
2702
|
-
ref,
|
|
2703
|
-
action: asString2(sub["action"]) ?? "",
|
|
2704
|
-
args: asRecord2(sub["args"])
|
|
2705
|
-
});
|
|
2706
|
-
}
|
|
2707
|
-
const r = await session.command(IrisCommand.ACT_SEQUENCE, { steps: liveSteps });
|
|
2708
|
-
results.push(buildResult(step.tool, r.ok, r.error, notes));
|
|
2709
|
-
if (!r.ok)
|
|
2710
|
-
break;
|
|
2711
|
-
} else {
|
|
2712
|
-
const { ref, note } = await resolveRef(session, step.args);
|
|
2713
|
-
const r = await session.command(IrisCommand.ACT, {
|
|
2714
|
-
ref,
|
|
2715
|
-
action: asString2(step.args["action"]) ?? "",
|
|
2716
|
-
args: asRecord2(step.args["args"])
|
|
3210
|
+
finish({
|
|
3211
|
+
pass: false,
|
|
3212
|
+
evidence: r.evidence,
|
|
3213
|
+
failureReason: r.failureReason ?? "timed out waiting for predicate"
|
|
2717
3214
|
});
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
break;
|
|
2721
|
-
}
|
|
2722
|
-
} catch (e) {
|
|
2723
|
-
results.push({
|
|
2724
|
-
tool: step.tool,
|
|
2725
|
-
ok: false,
|
|
2726
|
-
error: e instanceof Error ? e.message : String(e)
|
|
3215
|
+
}).catch((error) => {
|
|
3216
|
+
finish(failed(error));
|
|
2727
3217
|
});
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
}
|
|
2731
|
-
return results;
|
|
2732
|
-
}
|
|
2733
|
-
function buildResult(tool, ok, error, notes) {
|
|
2734
|
-
const base = { tool, ok };
|
|
2735
|
-
if (!ok)
|
|
2736
|
-
base.error = error ?? "command failed";
|
|
2737
|
-
if (notes.length > 0)
|
|
2738
|
-
base.note = notes.join("; ");
|
|
2739
|
-
return base;
|
|
3218
|
+
}, timeoutMs);
|
|
3219
|
+
check();
|
|
3220
|
+
});
|
|
2740
3221
|
}
|
|
2741
3222
|
|
|
2742
3223
|
// ../server/dist/events/event-filters.js
|
|
2743
|
-
function
|
|
3224
|
+
function asString4(value) {
|
|
2744
3225
|
return typeof value === "string" ? value : void 0;
|
|
2745
3226
|
}
|
|
2746
|
-
function
|
|
3227
|
+
function asNumber2(value) {
|
|
2747
3228
|
return typeof value === "number" ? value : void 0;
|
|
2748
3229
|
}
|
|
2749
3230
|
function matchNet(e, method, urlContains, status) {
|
|
2750
3231
|
const d = e.data;
|
|
2751
|
-
if (method !== void 0 &&
|
|
3232
|
+
if (method !== void 0 && asString4(d["method"])?.toUpperCase() !== method.toUpperCase()) {
|
|
2752
3233
|
return false;
|
|
2753
3234
|
}
|
|
2754
|
-
if (urlContains !== void 0 && !(
|
|
3235
|
+
if (urlContains !== void 0 && !(asString4(d["url"]) ?? "").includes(urlContains)) {
|
|
2755
3236
|
return false;
|
|
2756
3237
|
}
|
|
2757
|
-
if (status !== void 0 &&
|
|
3238
|
+
if (status !== void 0 && asNumber2(d["status"]) !== status)
|
|
2758
3239
|
return false;
|
|
2759
3240
|
return true;
|
|
2760
3241
|
}
|
|
@@ -2771,8 +3252,8 @@ function matchConsole(e, level) {
|
|
|
2771
3252
|
var HINT_SAMPLE_MAX = 5;
|
|
2772
3253
|
function netEmptyHint(allNet) {
|
|
2773
3254
|
const present = allNet.slice(-HINT_SAMPLE_MAX).reverse().map((e) => {
|
|
2774
|
-
const status =
|
|
2775
|
-
const base = { method:
|
|
3255
|
+
const status = asNumber2(e.data["status"]);
|
|
3256
|
+
const base = { method: asString4(e.data["method"]) ?? "", url: asString4(e.data["url"]) ?? "" };
|
|
2776
3257
|
return status === void 0 ? base : { ...base, status };
|
|
2777
3258
|
});
|
|
2778
3259
|
return { totalInWindow: allNet.length, present };
|
|
@@ -2803,6 +3284,8 @@ function refuseIfThrottled(session, refuse) {
|
|
|
2803
3284
|
}
|
|
2804
3285
|
|
|
2805
3286
|
// ../server/dist/session/output-budget.js
|
|
3287
|
+
var LARGE_TIMELINE_EVENTS = 80;
|
|
3288
|
+
var LARGE_TIMELINE_BYTES = 8e3;
|
|
2806
3289
|
function applyEventBudget(events, maxEvents) {
|
|
2807
3290
|
if (maxEvents === void 0 || maxEvents < 0 || events.length <= maxEvents) {
|
|
2808
3291
|
return { events, droppedOldest: 0 };
|
|
@@ -2813,8 +3296,92 @@ function applyEventBudget(events, maxEvents) {
|
|
|
2813
3296
|
};
|
|
2814
3297
|
}
|
|
2815
3298
|
function costHint(payload, events, droppedOldest = 0) {
|
|
2816
|
-
const
|
|
2817
|
-
|
|
3299
|
+
const json = JSON.stringify(payload) ?? "";
|
|
3300
|
+
const bytes = json.length;
|
|
3301
|
+
const base = droppedOldest > 0 ? { events, bytes, droppedOldest } : { events, bytes };
|
|
3302
|
+
if (events >= LARGE_TIMELINE_EVENTS || bytes >= LARGE_TIMELINE_BYTES) {
|
|
3303
|
+
base.recommendation = `large timeline (${String(events)} events, ~${String(estimateTokens(json))} tokens) \u2014 pass filters:[...] (e.g. ["signal","net"]) or max_events to scope your next call and cut tokens`;
|
|
3304
|
+
}
|
|
3305
|
+
return base;
|
|
3306
|
+
}
|
|
3307
|
+
var CHARS_PER_TOKEN = 4;
|
|
3308
|
+
function estimateTokens(text) {
|
|
3309
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
3310
|
+
}
|
|
3311
|
+
function sizeCost(payload) {
|
|
3312
|
+
const json = JSON.stringify(payload) ?? "";
|
|
3313
|
+
return { bytes: json.length, tokens: estimateTokens(json) };
|
|
3314
|
+
}
|
|
3315
|
+
function withSizeCost(result) {
|
|
3316
|
+
if (typeof result !== "object" || result === null)
|
|
3317
|
+
return result;
|
|
3318
|
+
return { ...result, cost: sizeCost(result) };
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
// ../server/dist/tools/snapshot-delta.js
|
|
3322
|
+
var SnapshotDeltaMode = {
|
|
3323
|
+
FULL: "full",
|
|
3324
|
+
DELTA: "delta",
|
|
3325
|
+
UNCHANGED: "unchanged"
|
|
3326
|
+
};
|
|
3327
|
+
function snapshotDelta(prevTree, nextTree) {
|
|
3328
|
+
if (prevTree === void 0)
|
|
3329
|
+
return { mode: SnapshotDeltaMode.FULL };
|
|
3330
|
+
const { added, removed } = diffLines(normalizeLines(prevTree), normalizeLines(nextTree));
|
|
3331
|
+
if (added.length === 0 && removed.length === 0)
|
|
3332
|
+
return { mode: SnapshotDeltaMode.UNCHANGED };
|
|
3333
|
+
return {
|
|
3334
|
+
mode: SnapshotDeltaMode.DELTA,
|
|
3335
|
+
delta: { added, removed, addedCount: added.length, removedCount: removed.length }
|
|
3336
|
+
};
|
|
3337
|
+
}
|
|
3338
|
+
var DEFAULT_MAX_ENTRIES = 50;
|
|
3339
|
+
var SnapshotCache = class {
|
|
3340
|
+
#map = /* @__PURE__ */ new Map();
|
|
3341
|
+
#max;
|
|
3342
|
+
constructor(max = DEFAULT_MAX_ENTRIES) {
|
|
3343
|
+
this.#max = max;
|
|
3344
|
+
}
|
|
3345
|
+
/** Last tree for this key IF the route still matches; undefined when absent or route changed. */
|
|
3346
|
+
recall(key, route) {
|
|
3347
|
+
const entry = this.#map.get(key);
|
|
3348
|
+
return entry !== void 0 && entry.route === route ? entry.tree : void 0;
|
|
3349
|
+
}
|
|
3350
|
+
remember(key, route, tree) {
|
|
3351
|
+
if (this.#map.size >= this.#max && !this.#map.has(key)) {
|
|
3352
|
+
const oldest = this.#map.keys().next().value;
|
|
3353
|
+
if (oldest !== void 0)
|
|
3354
|
+
this.#map.delete(oldest);
|
|
3355
|
+
}
|
|
3356
|
+
this.#map.set(key, { route, tree });
|
|
3357
|
+
}
|
|
3358
|
+
};
|
|
3359
|
+
function snapshotCacheKey(sessionId, scope, mode) {
|
|
3360
|
+
return `${sessionId}\0${scope}\0${mode}`;
|
|
3361
|
+
}
|
|
3362
|
+
function applySnapshotDelta(raw, opts, cache) {
|
|
3363
|
+
if (typeof raw !== "object" || raw === null)
|
|
3364
|
+
return raw;
|
|
3365
|
+
const r = raw;
|
|
3366
|
+
if (typeof r["tree"] !== "string")
|
|
3367
|
+
return raw;
|
|
3368
|
+
const tree = r["tree"];
|
|
3369
|
+
const status = typeof r["status"] === "object" && r["status"] !== null ? r["status"] : {};
|
|
3370
|
+
const route = typeof status["route"] === "string" ? status["route"] : "";
|
|
3371
|
+
const key = snapshotCacheKey(opts.sessionId, opts.scope, opts.mode);
|
|
3372
|
+
if (!opts.diff) {
|
|
3373
|
+
cache.remember(key, route, tree);
|
|
3374
|
+
return raw;
|
|
3375
|
+
}
|
|
3376
|
+
const prev = cache.recall(key, route);
|
|
3377
|
+
cache.remember(key, route, tree);
|
|
3378
|
+
const decision = snapshotDelta(prev, tree);
|
|
3379
|
+
if (decision.mode === SnapshotDeltaMode.FULL)
|
|
3380
|
+
return raw;
|
|
3381
|
+
if (decision.mode === SnapshotDeltaMode.UNCHANGED) {
|
|
3382
|
+
return { mode: SnapshotDeltaMode.UNCHANGED, status: r["status"] };
|
|
3383
|
+
}
|
|
3384
|
+
return { mode: SnapshotDeltaMode.DELTA, delta: decision.delta, status: r["status"] };
|
|
2818
3385
|
}
|
|
2819
3386
|
|
|
2820
3387
|
// ../server/dist/session/state-select.js
|
|
@@ -2865,25 +3432,55 @@ function capDepth(value, maxDepth) {
|
|
|
2865
3432
|
return value;
|
|
2866
3433
|
}
|
|
2867
3434
|
|
|
2868
|
-
// ../server/dist/tools/
|
|
2869
|
-
function
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
3435
|
+
// ../server/dist/tools/query-paginate.js
|
|
3436
|
+
function paginateQueryResult(result, limit, countOnly) {
|
|
3437
|
+
if (typeof result !== "object" || result === null)
|
|
3438
|
+
return result;
|
|
3439
|
+
const record = result;
|
|
3440
|
+
const elements = record["elements"];
|
|
3441
|
+
if (!Array.isArray(elements))
|
|
3442
|
+
return result;
|
|
3443
|
+
const total = elements.length;
|
|
3444
|
+
if (countOnly) {
|
|
3445
|
+
const { elements: _dropped, ...rest } = record;
|
|
3446
|
+
return { ...rest, count: total };
|
|
2876
3447
|
}
|
|
2877
|
-
|
|
2878
|
-
}
|
|
2879
|
-
|
|
2880
|
-
return
|
|
3448
|
+
if (limit !== void 0 && limit >= 0 && total > limit) {
|
|
3449
|
+
return { ...record, elements: elements.slice(0, limit), total, truncated: true };
|
|
3450
|
+
}
|
|
3451
|
+
return result;
|
|
2881
3452
|
}
|
|
2882
|
-
|
|
2883
|
-
|
|
3453
|
+
|
|
3454
|
+
// ../server/dist/tools/assert-grade.js
|
|
3455
|
+
var PRESENCE_ONLY_ADVICE = "This predicate only checks element/text presence, not an observable consequence. A locator healed to the wrong element (or a stale render) can satisfy it while the feature is broken. Prefer a { signal } or { net } assertion \u2014 or allOf it with one \u2014 so green means the feature actually worked.";
|
|
3456
|
+
function walk(predicate) {
|
|
3457
|
+
switch (predicate.kind) {
|
|
3458
|
+
case "signal":
|
|
3459
|
+
case "net":
|
|
3460
|
+
return { consequence: true, presence: false };
|
|
3461
|
+
case "element":
|
|
3462
|
+
case "text":
|
|
3463
|
+
return { consequence: false, presence: true };
|
|
3464
|
+
case "route":
|
|
3465
|
+
case "console":
|
|
3466
|
+
case "animation":
|
|
3467
|
+
case "settled":
|
|
3468
|
+
return { consequence: false, presence: false };
|
|
3469
|
+
case "allOf":
|
|
3470
|
+
case "anyOf": {
|
|
3471
|
+
const subs = predicate.predicates.map(walk);
|
|
3472
|
+
return {
|
|
3473
|
+
consequence: subs.some((s) => s.consequence),
|
|
3474
|
+
presence: subs.some((s) => s.presence)
|
|
3475
|
+
};
|
|
3476
|
+
}
|
|
3477
|
+
case "not":
|
|
3478
|
+
return walk(predicate.predicate);
|
|
3479
|
+
}
|
|
2884
3480
|
}
|
|
2885
|
-
function
|
|
2886
|
-
|
|
3481
|
+
function isPresenceOnlyAssertion(predicate) {
|
|
3482
|
+
const kinds = walk(predicate);
|
|
3483
|
+
return kinds.presence && !kinds.consequence;
|
|
2887
3484
|
}
|
|
2888
3485
|
|
|
2889
3486
|
// ../server/dist/tools/contract-tools.js
|
|
@@ -2917,7 +3514,7 @@ var CONTRACT_TOOLS = [
|
|
|
2917
3514
|
throw new Error(r.reason === ContractReadError.MISSING ? "no .iris/contract.json on disk \u2014 run iris_contract_save first (or omit fromDisk to read the live session)" : ".iris/contract.json is malformed \u2014 fix or regenerate it with iris_contract_save");
|
|
2918
3515
|
return { ...r.capabilities, source: "disk", generatedAt: r.generatedAt };
|
|
2919
3516
|
}
|
|
2920
|
-
const caps = await commandOrThrow(deps,
|
|
3517
|
+
const caps = await commandOrThrow(deps, asString(args["sessionId"]), IrisCommand.CAPABILITIES, {});
|
|
2921
3518
|
return { ...caps, source: "live" };
|
|
2922
3519
|
}
|
|
2923
3520
|
},
|
|
@@ -2932,7 +3529,7 @@ var CONTRACT_TOOLS = [
|
|
|
2932
3529
|
signalCount: z4.number()
|
|
2933
3530
|
},
|
|
2934
3531
|
handler: async (deps, args) => {
|
|
2935
|
-
const res = await commandOrThrow(deps,
|
|
3532
|
+
const res = await commandOrThrow(deps, asString(args["sessionId"]), IrisCommand.CAPABILITIES, {});
|
|
2936
3533
|
const caps = CapabilitiesSchema.parse(res);
|
|
2937
3534
|
await writeContract(deps.fs, deps.irisRoot, caps, deps.now);
|
|
2938
3535
|
return {
|
|
@@ -2945,253 +3542,399 @@ var CONTRACT_TOOLS = [
|
|
|
2945
3542
|
}
|
|
2946
3543
|
];
|
|
2947
3544
|
|
|
2948
|
-
// ../server/dist/
|
|
3545
|
+
// ../server/dist/domain/domain-tools.js
|
|
2949
3546
|
import { z as z5 } from "zod";
|
|
2950
|
-
|
|
2951
|
-
|
|
3547
|
+
|
|
3548
|
+
// ../server/dist/flows/flow-classify.js
|
|
3549
|
+
var FlowAssertionGrade = {
|
|
3550
|
+
/** At least one step (or the success end-condition) asserts a signal/network consequence. */
|
|
3551
|
+
ASSERTED: "asserted",
|
|
3552
|
+
/** Only element-presence checks — a healed-but-wrong locator could still pass. */
|
|
3553
|
+
PRESENCE_ONLY: "presence-only",
|
|
3554
|
+
/** Performs actions but asserts nothing observable — passes even if the feature is broken. */
|
|
3555
|
+
ASSERTION_FREE: "assertion-free"
|
|
2952
3556
|
};
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
throw new Error(result.error ?? `command '${name}' failed`);
|
|
2958
|
-
return result.result;
|
|
3557
|
+
var ASSERTION_FREE_WARNING = "This flow performs actions but asserts no observable consequence \u2014 it will pass even if the feature is broken. Add a consequence assertion with iris_annotate (assert-signal / assert-net) or a success-state.";
|
|
3558
|
+
var PRESENCE_ONLY_WARNING = "This flow only checks element presence, not an observable consequence (signal/network). A locator healed to the wrong element can still pass it. Add a consequence assertion (assert-signal / assert-net / success-state).";
|
|
3559
|
+
function expectIsConsequence(e) {
|
|
3560
|
+
return e !== void 0 && (e.signal !== void 0 || e.net !== void 0);
|
|
2959
3561
|
}
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
name: IrisTool.NAVIGATE,
|
|
2963
|
-
description: "Navigate the connected browser tab to a URL. The SDK reconnects automatically after the page loads. Use iris_sessions to confirm the new tab is connected before acting.",
|
|
2964
|
-
inputSchema: {
|
|
2965
|
-
url: z5.string().describe("The URL to navigate to."),
|
|
2966
|
-
...sessionIdShape2
|
|
2967
|
-
},
|
|
2968
|
-
outputSchema: {
|
|
2969
|
-
ok: z5.boolean(),
|
|
2970
|
-
url: z5.string().optional(),
|
|
2971
|
-
reason: z5.string().optional()
|
|
2972
|
-
},
|
|
2973
|
-
handler: async (deps, args) => {
|
|
2974
|
-
const url = asString4(args["url"]);
|
|
2975
|
-
if (url === void 0 || url.length === 0)
|
|
2976
|
-
return { ok: false, reason: "url required" };
|
|
2977
|
-
await commandOrThrow2(deps, asString4(args["sessionId"]), IrisCommand.NAVIGATE, { url });
|
|
2978
|
-
return { ok: true, url };
|
|
2979
|
-
}
|
|
2980
|
-
},
|
|
2981
|
-
{
|
|
2982
|
-
name: IrisTool.REFRESH,
|
|
2983
|
-
description: "Reload the connected browser tab. Pass { hard: true } to bypass the browser cache (equivalent to Cmd+Shift+R). The SDK reconnects automatically after the reload.",
|
|
2984
|
-
inputSchema: {
|
|
2985
|
-
hard: z5.boolean().optional().describe("Set true to bypass the browser cache. Default: false (normal reload)."),
|
|
2986
|
-
...sessionIdShape2
|
|
2987
|
-
},
|
|
2988
|
-
outputSchema: {
|
|
2989
|
-
ok: z5.boolean()
|
|
2990
|
-
},
|
|
2991
|
-
handler: async (deps, args) => {
|
|
2992
|
-
await commandOrThrow2(deps, asString4(args["sessionId"]), IrisCommand.REFRESH, {
|
|
2993
|
-
hard: args["hard"] === true
|
|
2994
|
-
});
|
|
2995
|
-
return { ok: true };
|
|
2996
|
-
}
|
|
2997
|
-
}
|
|
2998
|
-
];
|
|
2999
|
-
|
|
3000
|
-
// ../server/dist/flows/flow-tools.js
|
|
3001
|
-
import { z as z6 } from "zod";
|
|
3002
|
-
|
|
3003
|
-
// ../server/dist/flows/flow-replay.js
|
|
3004
|
-
function editDistance(a, b) {
|
|
3005
|
-
const s = a.toLowerCase();
|
|
3006
|
-
const t = b.toLowerCase();
|
|
3007
|
-
const rows = s.length + 1;
|
|
3008
|
-
const cols = t.length + 1;
|
|
3009
|
-
const prev = new Array(cols);
|
|
3010
|
-
const curr = new Array(cols);
|
|
3011
|
-
for (let j = 0; j < cols; j++)
|
|
3012
|
-
prev[j] = j;
|
|
3013
|
-
for (let i = 1; i < rows; i++) {
|
|
3014
|
-
curr[0] = i;
|
|
3015
|
-
for (let j = 1; j < cols; j++) {
|
|
3016
|
-
const cost = s[i - 1] === t[j - 1] ? 0 : 1;
|
|
3017
|
-
curr[j] = Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
|
|
3018
|
-
}
|
|
3019
|
-
for (let j = 0; j < cols; j++)
|
|
3020
|
-
prev[j] = curr[j] ?? 0;
|
|
3021
|
-
}
|
|
3022
|
-
return prev[cols - 1] ?? 0;
|
|
3562
|
+
function expectIsWeak(e) {
|
|
3563
|
+
return e !== void 0 && e.element !== void 0 && e.signal === void 0 && e.net === void 0;
|
|
3023
3564
|
}
|
|
3024
|
-
function
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
best = candidate;
|
|
3031
|
-
bestDistance = distance;
|
|
3032
|
-
}
|
|
3565
|
+
function flattenSteps(steps) {
|
|
3566
|
+
const out = [];
|
|
3567
|
+
for (const s of steps) {
|
|
3568
|
+
out.push(s);
|
|
3569
|
+
if (s.steps !== void 0)
|
|
3570
|
+
out.push(...flattenSteps(s.steps));
|
|
3033
3571
|
}
|
|
3034
|
-
return
|
|
3572
|
+
return out;
|
|
3035
3573
|
}
|
|
3036
|
-
function
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
const
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3574
|
+
function classifyFlowAssertions(flow) {
|
|
3575
|
+
const all = flattenSteps(flow.steps);
|
|
3576
|
+
let consequenceSteps = 0;
|
|
3577
|
+
let weakSteps = 0;
|
|
3578
|
+
for (const s of all) {
|
|
3579
|
+
if (expectIsConsequence(s.expect))
|
|
3580
|
+
consequenceSteps++;
|
|
3581
|
+
else if (expectIsWeak(s.expect))
|
|
3582
|
+
weakSteps++;
|
|
3583
|
+
}
|
|
3584
|
+
const successIsConsequence = expectIsConsequence(flow.success);
|
|
3585
|
+
const successIsWeak = expectIsWeak(flow.success);
|
|
3586
|
+
const hasConsequenceAssertion = consequenceSteps > 0 || successIsConsequence;
|
|
3587
|
+
const hasAnyAssertion = hasConsequenceAssertion || weakSteps > 0 || successIsWeak;
|
|
3588
|
+
let grade;
|
|
3589
|
+
let warning;
|
|
3590
|
+
if (hasConsequenceAssertion) {
|
|
3591
|
+
grade = FlowAssertionGrade.ASSERTED;
|
|
3592
|
+
} else if (hasAnyAssertion) {
|
|
3593
|
+
grade = FlowAssertionGrade.PRESENCE_ONLY;
|
|
3594
|
+
warning = PRESENCE_ONLY_WARNING;
|
|
3595
|
+
} else {
|
|
3596
|
+
grade = FlowAssertionGrade.ASSERTION_FREE;
|
|
3597
|
+
warning = ASSERTION_FREE_WARNING;
|
|
3055
3598
|
}
|
|
3056
|
-
return { refs };
|
|
3057
|
-
}
|
|
3058
|
-
function testidDrift(value, hint) {
|
|
3059
3599
|
return {
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3600
|
+
grade,
|
|
3601
|
+
hasConsequenceAssertion,
|
|
3602
|
+
totalSteps: all.length,
|
|
3603
|
+
consequenceSteps,
|
|
3604
|
+
weakSteps,
|
|
3605
|
+
successIsConsequence,
|
|
3606
|
+
...warning !== void 0 ? { warning } : {}
|
|
3064
3607
|
};
|
|
3065
3608
|
}
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
return anchor.name;
|
|
3071
|
-
return anchor.name ?? anchor.role;
|
|
3609
|
+
|
|
3610
|
+
// ../server/dist/flows/flow-success.js
|
|
3611
|
+
function dynamicTestids(flow) {
|
|
3612
|
+
return new Set((flow.dynamic ?? []).filter((a) => a.kind === AnchorKind.TESTID).map((a) => a.kind === AnchorKind.TESTID ? a.value : ""));
|
|
3072
3613
|
}
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
if (
|
|
3077
|
-
return
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
};
|
|
3614
|
+
function successLabel(success) {
|
|
3615
|
+
if (success.signal !== void 0)
|
|
3616
|
+
return success.signal;
|
|
3617
|
+
if (success.net !== void 0)
|
|
3618
|
+
return success.net.urlContains ?? success.net.method ?? "net";
|
|
3619
|
+
return success.element?.testid ?? success.element?.name ?? success.element?.role ?? "success";
|
|
3620
|
+
}
|
|
3621
|
+
function successToPredicate(success, dynamic) {
|
|
3622
|
+
const parts = [];
|
|
3623
|
+
if (success.signal !== void 0) {
|
|
3624
|
+
parts.push(success.signalData !== void 0 ? { kind: "signal", name: success.signal, dataMatches: success.signalData } : { kind: "signal", name: success.signal });
|
|
3084
3625
|
}
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
result.error = act.error ?? "command failed";
|
|
3095
|
-
if (note !== void 0)
|
|
3096
|
-
result.note = note;
|
|
3097
|
-
return result;
|
|
3626
|
+
if (success.net !== void 0) {
|
|
3627
|
+
const net = { kind: "net" };
|
|
3628
|
+
if (success.net.method !== void 0)
|
|
3629
|
+
net.method = success.net.method;
|
|
3630
|
+
if (success.net.urlContains !== void 0)
|
|
3631
|
+
net.urlContains = success.net.urlContains;
|
|
3632
|
+
if (success.net.status !== void 0)
|
|
3633
|
+
net.status = success.net.status;
|
|
3634
|
+
parts.push(net);
|
|
3098
3635
|
}
|
|
3099
|
-
const
|
|
3100
|
-
if (
|
|
3101
|
-
const
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
drift: testidDrift(expectTestid, expectRefs.hint)
|
|
3113
|
-
};
|
|
3636
|
+
const element = success.element;
|
|
3637
|
+
if (element !== void 0) {
|
|
3638
|
+
const testid = element.testid;
|
|
3639
|
+
if (testid === void 0 || !dynamic.has(testid)) {
|
|
3640
|
+
const query = {};
|
|
3641
|
+
if (testid !== void 0)
|
|
3642
|
+
query["testid"] = testid;
|
|
3643
|
+
if (element.role !== void 0)
|
|
3644
|
+
query["role"] = element.role;
|
|
3645
|
+
if (element.name !== void 0)
|
|
3646
|
+
query["name"] = element.name;
|
|
3647
|
+
if (Object.keys(query).length > 0)
|
|
3648
|
+
parts.push({ kind: "element", query });
|
|
3114
3649
|
}
|
|
3115
3650
|
}
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3651
|
+
const [first] = parts;
|
|
3652
|
+
if (parts.length === 0)
|
|
3653
|
+
return void 0;
|
|
3654
|
+
if (parts.length === 1 && first !== void 0)
|
|
3655
|
+
return first;
|
|
3656
|
+
return { kind: "allOf", predicates: parts };
|
|
3119
3657
|
}
|
|
3120
|
-
async function
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
anchor: name,
|
|
3128
|
-
ok: false,
|
|
3129
|
-
drift: {
|
|
3130
|
-
reasonKind: DriftReason.SIGNAL_NOT_OBSERVED,
|
|
3131
|
-
reason: `signal "${name}" not observed`,
|
|
3132
|
-
anchor: name,
|
|
3133
|
-
nearest: null
|
|
3134
|
-
}
|
|
3135
|
-
};
|
|
3658
|
+
async function assertSuccess(session, success, dynamic, waitForSignal, timeoutMs, since = 0) {
|
|
3659
|
+
if (success === void 0)
|
|
3660
|
+
return { pass: true };
|
|
3661
|
+
const predicate = successToPredicate(success, dynamic);
|
|
3662
|
+
if (predicate === void 0)
|
|
3663
|
+
return { pass: true };
|
|
3664
|
+
return waitForSignal(session, predicate, timeoutMs, since);
|
|
3136
3665
|
}
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3666
|
+
|
|
3667
|
+
// ../server/dist/domain/flow-risk.js
|
|
3668
|
+
var RiskLevel = {
|
|
3669
|
+
HIGH: "high",
|
|
3670
|
+
MEDIUM: "medium",
|
|
3671
|
+
LOW: "low",
|
|
3672
|
+
UNKNOWN: "unknown"
|
|
3673
|
+
};
|
|
3674
|
+
var RANK = {
|
|
3675
|
+
[RiskLevel.HIGH]: 3,
|
|
3676
|
+
[RiskLevel.MEDIUM]: 2,
|
|
3677
|
+
[RiskLevel.UNKNOWN]: 1,
|
|
3678
|
+
[RiskLevel.LOW]: 0
|
|
3679
|
+
};
|
|
3680
|
+
function latestRun(name, runs) {
|
|
3681
|
+
let best;
|
|
3682
|
+
for (const run of runs) {
|
|
3683
|
+
if (run.name === name && (best === void 0 || run.at > best.at))
|
|
3684
|
+
best = run;
|
|
3153
3685
|
}
|
|
3154
|
-
return
|
|
3686
|
+
return best;
|
|
3687
|
+
}
|
|
3688
|
+
function runRisk(run) {
|
|
3689
|
+
if (run === void 0)
|
|
3690
|
+
return { level: RiskLevel.UNKNOWN, reason: "never run" };
|
|
3691
|
+
if (run.status === RunStatus.ERROR || run.status === RunStatus.FAIL) {
|
|
3692
|
+
return { level: RiskLevel.HIGH, reason: "last run failed" };
|
|
3693
|
+
}
|
|
3694
|
+
if (run.status === RunStatus.DRIFT)
|
|
3695
|
+
return { level: RiskLevel.HIGH, reason: "last run drifted" };
|
|
3696
|
+
const errors = (run.evidence?.consoleErrors ?? 0) + (run.evidence?.networkErrors ?? 0);
|
|
3697
|
+
if (errors > 0) {
|
|
3698
|
+
return {
|
|
3699
|
+
level: RiskLevel.MEDIUM,
|
|
3700
|
+
reason: `last run passed but logged ${String(errors)} error(s)`
|
|
3701
|
+
};
|
|
3702
|
+
}
|
|
3703
|
+
return { level: RiskLevel.LOW, reason: "last run passed clean" };
|
|
3704
|
+
}
|
|
3705
|
+
function gradeRisk(grade) {
|
|
3706
|
+
if (grade === FlowAssertionGrade.ASSERTION_FREE) {
|
|
3707
|
+
return {
|
|
3708
|
+
level: RiskLevel.MEDIUM,
|
|
3709
|
+
reason: "asserts no consequence \u2014 a green run proves little"
|
|
3710
|
+
};
|
|
3711
|
+
}
|
|
3712
|
+
if (grade === FlowAssertionGrade.PRESENCE_ONLY) {
|
|
3713
|
+
return { level: RiskLevel.LOW, reason: "presence-only assertion" };
|
|
3714
|
+
}
|
|
3715
|
+
return { level: RiskLevel.LOW, reason: "asserts a consequence" };
|
|
3716
|
+
}
|
|
3717
|
+
function flowRisk(grade, run) {
|
|
3718
|
+
const r = runRisk(run);
|
|
3719
|
+
const g = gradeRisk(grade);
|
|
3720
|
+
const top = RANK[r.level] >= RANK[g.level] ? r : g;
|
|
3721
|
+
return run === void 0 ? { level: top.level, reason: top.reason } : { level: top.level, reason: top.reason, lastStatus: run.status };
|
|
3722
|
+
}
|
|
3723
|
+
function rankByRisk(entries) {
|
|
3724
|
+
return [...entries].sort((a, b) => RANK[b.risk.level] - RANK[a.risk.level] || a.name.localeCompare(b.name)).map((e) => e.name);
|
|
3155
3725
|
}
|
|
3156
3726
|
|
|
3157
|
-
// ../server/dist/
|
|
3158
|
-
function
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
return 1;
|
|
3167
|
-
if (raw <= 0)
|
|
3168
|
-
return Number.EPSILON;
|
|
3169
|
-
return raw;
|
|
3727
|
+
// ../server/dist/domain/domain-model.js
|
|
3728
|
+
function flatten(steps) {
|
|
3729
|
+
const out = [];
|
|
3730
|
+
for (const s of steps) {
|
|
3731
|
+
out.push(s);
|
|
3732
|
+
if (s.steps !== void 0)
|
|
3733
|
+
out.push(...flatten(s.steps));
|
|
3734
|
+
}
|
|
3735
|
+
return out;
|
|
3170
3736
|
}
|
|
3171
|
-
function
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3737
|
+
function flowSignals(flow) {
|
|
3738
|
+
const set = /* @__PURE__ */ new Set();
|
|
3739
|
+
for (const step of flatten(flow.steps)) {
|
|
3740
|
+
if (step.anchor.kind === AnchorKind.SIGNAL)
|
|
3741
|
+
set.add(step.anchor.name);
|
|
3742
|
+
if (step.expect?.signal !== void 0)
|
|
3743
|
+
set.add(step.expect.signal);
|
|
3744
|
+
}
|
|
3745
|
+
if (flow.success?.signal !== void 0)
|
|
3746
|
+
set.add(flow.success.signal);
|
|
3747
|
+
return [...set];
|
|
3748
|
+
}
|
|
3749
|
+
function flowTestids(flow) {
|
|
3750
|
+
const set = /* @__PURE__ */ new Set();
|
|
3751
|
+
for (const step of flatten(flow.steps)) {
|
|
3752
|
+
if (step.anchor.kind === AnchorKind.TESTID)
|
|
3753
|
+
set.add(step.anchor.value);
|
|
3754
|
+
if (step.expect?.element?.testid !== void 0)
|
|
3755
|
+
set.add(step.expect.element.testid);
|
|
3756
|
+
}
|
|
3757
|
+
if (flow.success?.element?.testid !== void 0)
|
|
3758
|
+
set.add(flow.success.element.testid);
|
|
3759
|
+
return [...set];
|
|
3760
|
+
}
|
|
3761
|
+
var EMPTY_CONTRACT = { testids: [], signals: [], stores: [], flows: [] };
|
|
3762
|
+
function buildDomainModel(flows, contract, runs = []) {
|
|
3763
|
+
const caps = contract ?? EMPTY_CONTRACT;
|
|
3764
|
+
const hasHistory = runs.length > 0;
|
|
3765
|
+
const flowSummaries = flows.map((flow) => {
|
|
3766
|
+
const c = classifyFlowAssertions(flow);
|
|
3767
|
+
const summary = {
|
|
3768
|
+
name: flow.name,
|
|
3769
|
+
steps: c.totalSteps,
|
|
3770
|
+
grade: c.grade,
|
|
3771
|
+
asserts: c.hasConsequenceAssertion,
|
|
3772
|
+
signals: flowSignals(flow),
|
|
3773
|
+
testids: flowTestids(flow)
|
|
3774
|
+
};
|
|
3775
|
+
if (flow.success !== void 0)
|
|
3776
|
+
summary.mustHold = successLabel(flow.success);
|
|
3777
|
+
if (c.warning !== void 0)
|
|
3778
|
+
summary.warning = c.warning;
|
|
3779
|
+
if (hasHistory)
|
|
3780
|
+
summary.risk = flowRisk(c.grade, latestRun(flow.name, runs));
|
|
3781
|
+
return summary;
|
|
3782
|
+
});
|
|
3783
|
+
const testedSignals = new Set(flowSummaries.flatMap((f) => f.signals));
|
|
3784
|
+
const testedTestids = new Set(flowSummaries.flatMap((f) => f.testids));
|
|
3785
|
+
const coverage = {
|
|
3786
|
+
asserted: flowSummaries.filter((f) => f.grade === FlowAssertionGrade.ASSERTED).length,
|
|
3787
|
+
presenceOnly: flowSummaries.filter((f) => f.grade === FlowAssertionGrade.PRESENCE_ONLY).length,
|
|
3788
|
+
assertionFree: flowSummaries.filter((f) => f.grade === FlowAssertionGrade.ASSERTION_FREE).length
|
|
3789
|
+
};
|
|
3790
|
+
const gaps = {
|
|
3791
|
+
unassertedFlows: flowSummaries.filter((f) => !f.asserts).map((f) => f.name),
|
|
3792
|
+
declaredUntestedSignals: caps.signals.filter((s) => !testedSignals.has(s)),
|
|
3793
|
+
declaredUntestedTestids: caps.testids.filter((t) => !testedTestids.has(t))
|
|
3794
|
+
};
|
|
3795
|
+
const riskRanked = hasHistory ? rankByRisk(flowSummaries.filter((f) => f.risk !== void 0).map((f) => ({ name: f.name, risk: f.risk }))) : [];
|
|
3796
|
+
const top = riskRanked[0];
|
|
3797
|
+
const topFlow = top === void 0 ? void 0 : flowSummaries.find((f) => f.name === top);
|
|
3798
|
+
const topRisk = topFlow?.risk !== void 0 && (topFlow.risk.level === RiskLevel.HIGH || topFlow.risk.level === RiskLevel.MEDIUM) ? { name: topFlow.name, reason: topFlow.risk.reason } : void 0;
|
|
3799
|
+
return {
|
|
3800
|
+
flowCount: flows.length,
|
|
3801
|
+
flows: flowSummaries,
|
|
3802
|
+
declared: { testids: caps.testids.length, signals: caps.signals, stores: caps.stores },
|
|
3803
|
+
coverage,
|
|
3804
|
+
gaps,
|
|
3805
|
+
riskRanked,
|
|
3806
|
+
summary: buildSummary(flows.length, coverage, gaps, topRisk)
|
|
3807
|
+
};
|
|
3181
3808
|
}
|
|
3182
|
-
function
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
if (step.drift === void 0)
|
|
3186
|
-
continue;
|
|
3187
|
-
const proposal = proposeRebindWith(step.drift, step.step, minConfidence);
|
|
3188
|
-
if (proposal !== void 0)
|
|
3189
|
-
proposals.push(proposal);
|
|
3809
|
+
function buildSummary(flowCount, coverage, gaps, topRisk) {
|
|
3810
|
+
if (flowCount === 0) {
|
|
3811
|
+
return "No saved flows yet \u2014 record the critical journeys (iris_record_start) so the agent learns the app.";
|
|
3190
3812
|
}
|
|
3191
|
-
|
|
3813
|
+
const parts = [
|
|
3814
|
+
`${String(flowCount)} flow${flowCount === 1 ? "" : "s"}: ${String(coverage.asserted)} asserted, ${String(coverage.presenceOnly)} presence-only, ${String(coverage.assertionFree)} assertion-free`
|
|
3815
|
+
];
|
|
3816
|
+
if (topRisk !== void 0) {
|
|
3817
|
+
parts.push(`test first: ${topRisk.name} (${topRisk.reason})`);
|
|
3818
|
+
}
|
|
3819
|
+
if (gaps.declaredUntestedSignals.length > 0) {
|
|
3820
|
+
parts.push(`${String(gaps.declaredUntestedSignals.length)} declared signal(s) no flow asserts (${gaps.declaredUntestedSignals.join(", ")})`);
|
|
3821
|
+
}
|
|
3822
|
+
if (gaps.unassertedFlows.length > 0) {
|
|
3823
|
+
parts.push(`${String(gaps.unassertedFlows.length)} flow(s) assert no consequence`);
|
|
3824
|
+
}
|
|
3825
|
+
return parts.join(". ") + ".";
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3828
|
+
// ../server/dist/domain/domain-tools.js
|
|
3829
|
+
var DOMAIN_TOOLS = [
|
|
3830
|
+
{
|
|
3831
|
+
name: IrisTool.DOMAIN,
|
|
3832
|
+
description: "Read the app domain model BEFORE testing: every saved flow with its assertion grade, the consequence that MUST hold for it (mustHold = what it actually tests), the anchors/signals it exercises, plus GAPS \u2014 declared signals/testids that NO flow asserts (untested intent), and flows that assert no observable consequence. Use this to decide what to test and where the real risk is, instead of crawling the whole app. Reads .iris/flows/ + .iris/contract.json (no browser needed).",
|
|
3833
|
+
inputSchema: {},
|
|
3834
|
+
outputSchema: {
|
|
3835
|
+
flowCount: z5.number(),
|
|
3836
|
+
flows: z5.array(z5.object({
|
|
3837
|
+
name: z5.string(),
|
|
3838
|
+
steps: z5.number(),
|
|
3839
|
+
grade: z5.string(),
|
|
3840
|
+
asserts: z5.boolean(),
|
|
3841
|
+
mustHold: z5.string().optional().describe("The success consequence that must hold for this flow (what it actually tests)."),
|
|
3842
|
+
warning: z5.string().optional(),
|
|
3843
|
+
signals: z5.array(z5.string()),
|
|
3844
|
+
testids: z5.array(z5.string())
|
|
3845
|
+
})),
|
|
3846
|
+
declared: z5.object({
|
|
3847
|
+
testids: z5.number(),
|
|
3848
|
+
signals: z5.array(z5.string()),
|
|
3849
|
+
stores: z5.array(z5.string())
|
|
3850
|
+
}),
|
|
3851
|
+
coverage: z5.object({
|
|
3852
|
+
asserted: z5.number(),
|
|
3853
|
+
presenceOnly: z5.number(),
|
|
3854
|
+
assertionFree: z5.number()
|
|
3855
|
+
}),
|
|
3856
|
+
gaps: z5.object({
|
|
3857
|
+
unassertedFlows: z5.array(z5.string()),
|
|
3858
|
+
declaredUntestedSignals: z5.array(z5.string()),
|
|
3859
|
+
declaredUntestedTestids: z5.array(z5.string())
|
|
3860
|
+
}),
|
|
3861
|
+
riskRanked: z5.array(z5.string()).describe("Flow names worst-risk first (run history + assertion quality). Test these first."),
|
|
3862
|
+
summary: z5.string()
|
|
3863
|
+
},
|
|
3864
|
+
handler: async (deps) => {
|
|
3865
|
+
const names = await deps.flows.list();
|
|
3866
|
+
const flows = [];
|
|
3867
|
+
for (const name of names) {
|
|
3868
|
+
const loaded = await deps.flows.load(name);
|
|
3869
|
+
if (loaded.ok)
|
|
3870
|
+
flows.push(loaded.value);
|
|
3871
|
+
}
|
|
3872
|
+
const contract = await readContract(deps.fs, deps.irisRoot);
|
|
3873
|
+
const project = await deps.project.read();
|
|
3874
|
+
const runs = project.ok ? project.file.runs : [];
|
|
3875
|
+
return buildDomainModel(flows, contract.ok ? contract.capabilities : null, runs);
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
];
|
|
3879
|
+
|
|
3880
|
+
// ../server/dist/tools/browser-tools.js
|
|
3881
|
+
import { z as z6 } from "zod";
|
|
3882
|
+
var sessionIdShape2 = {
|
|
3883
|
+
sessionId: z6.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3884
|
+
};
|
|
3885
|
+
async function commandOrThrow2(deps, sessionId, name, args) {
|
|
3886
|
+
const session = deps.sessions.resolve(sessionId);
|
|
3887
|
+
const result = await session.command(name, args);
|
|
3888
|
+
if (!result.ok)
|
|
3889
|
+
throw new Error(result.error ?? `command '${name}' failed`);
|
|
3890
|
+
return result.result;
|
|
3192
3891
|
}
|
|
3892
|
+
var BROWSER_TOOLS = [
|
|
3893
|
+
{
|
|
3894
|
+
name: IrisTool.NAVIGATE,
|
|
3895
|
+
description: "Navigate the connected browser tab to a URL. The SDK reconnects automatically after the page loads. Use iris_sessions to confirm the new tab is connected before acting.",
|
|
3896
|
+
inputSchema: {
|
|
3897
|
+
url: z6.string().describe("The URL to navigate to."),
|
|
3898
|
+
...sessionIdShape2
|
|
3899
|
+
},
|
|
3900
|
+
outputSchema: {
|
|
3901
|
+
ok: z6.boolean(),
|
|
3902
|
+
url: z6.string().optional(),
|
|
3903
|
+
reason: z6.string().optional()
|
|
3904
|
+
},
|
|
3905
|
+
handler: async (deps, args) => {
|
|
3906
|
+
const url = asString(args["url"]);
|
|
3907
|
+
if (url === void 0 || url.length === 0)
|
|
3908
|
+
return { ok: false, reason: "url required" };
|
|
3909
|
+
const result = await commandOrThrow2(deps, asString(args["sessionId"]), IrisCommand.NAVIGATE, { url });
|
|
3910
|
+
return {
|
|
3911
|
+
ok: result.ok === true,
|
|
3912
|
+
...typeof result.url === "string" ? { url: result.url } : {},
|
|
3913
|
+
...typeof result.reason === "string" ? { reason: result.reason } : {}
|
|
3914
|
+
};
|
|
3915
|
+
}
|
|
3916
|
+
},
|
|
3917
|
+
{
|
|
3918
|
+
name: IrisTool.REFRESH,
|
|
3919
|
+
description: "Reload the connected browser tab. Pass { hard: true } to bypass the browser cache (equivalent to Cmd+Shift+R). The SDK reconnects automatically after the reload.",
|
|
3920
|
+
inputSchema: {
|
|
3921
|
+
hard: z6.boolean().optional().describe("Set true to bypass the browser cache. Default: false (normal reload)."),
|
|
3922
|
+
...sessionIdShape2
|
|
3923
|
+
},
|
|
3924
|
+
outputSchema: {
|
|
3925
|
+
ok: z6.boolean()
|
|
3926
|
+
},
|
|
3927
|
+
handler: async (deps, args) => {
|
|
3928
|
+
await commandOrThrow2(deps, asString(args["sessionId"]), IrisCommand.REFRESH, {
|
|
3929
|
+
hard: args["hard"] === true
|
|
3930
|
+
});
|
|
3931
|
+
return { ok: true };
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
];
|
|
3193
3935
|
|
|
3194
3936
|
// ../server/dist/flows/flow-tools.js
|
|
3937
|
+
import { z as z7 } from "zod";
|
|
3195
3938
|
function latestRecordedFlow(events) {
|
|
3196
3939
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
3197
3940
|
const event = events[i];
|
|
@@ -3237,18 +3980,26 @@ async function recordReplayRun(deps, name, status, driftSteps, durationMs) {
|
|
|
3237
3980
|
var FLOW_TOOLS = [
|
|
3238
3981
|
{
|
|
3239
3982
|
name: IrisTool.FLOW_SAVE,
|
|
3240
|
-
description: 'Persist the last/active recording (by name) as a git-checked, anchor-resolved flow at .iris/flows/<name>.json. Each step is bound to a SEMANTIC anchor (testid/role/signal), never a volatile ref; steps without a resolvable testid are kept with degraded:true (a "add a data-testid here" marker) rather than dropped. Returns { name, stepCount, degraded, empty } or
|
|
3983
|
+
description: 'Persist the last/active recording (by name) as a git-checked, anchor-resolved flow at .iris/flows/<name>.json. Each step is bound to a SEMANTIC anchor (testid/role/signal), never a volatile ref; steps without a resolvable testid are kept with degraded:true (a "add a data-testid here" marker) rather than dropped. Returns { name, stepCount, degraded, empty, assertions } \u2014 `assertions.grade` is asserted | presence-only | assertion-free: a flow that only acts (or only checks element presence) will pass even if the feature breaks, so when grade is not "asserted" follow assertions.warning and add a consequence assertion via iris_annotate (assert-signal / assert-net / success-state).',
|
|
3241
3984
|
inputSchema: {
|
|
3242
|
-
flowName:
|
|
3985
|
+
flowName: z7.string().describe("Name for the flow file (saved to .iris/flows/<flowName>.json). Use again in iris_flow_load/iris_flow_replay.")
|
|
3243
3986
|
},
|
|
3244
3987
|
outputSchema: {
|
|
3245
|
-
saved:
|
|
3246
|
-
path:
|
|
3247
|
-
stepCount:
|
|
3248
|
-
degraded:
|
|
3988
|
+
saved: z7.boolean(),
|
|
3989
|
+
path: z7.string(),
|
|
3990
|
+
stepCount: z7.number().optional(),
|
|
3991
|
+
degraded: z7.number().optional(),
|
|
3992
|
+
assertions: z7.object({
|
|
3993
|
+
grade: z7.string().describe("asserted | presence-only | assertion-free"),
|
|
3994
|
+
hasConsequenceAssertion: z7.boolean(),
|
|
3995
|
+
totalSteps: z7.number(),
|
|
3996
|
+
consequenceSteps: z7.number(),
|
|
3997
|
+
weakSteps: z7.number(),
|
|
3998
|
+
warning: z7.string().optional()
|
|
3999
|
+
}).optional()
|
|
3249
4000
|
},
|
|
3250
4001
|
handler: (deps, args) => {
|
|
3251
|
-
const name =
|
|
4002
|
+
const name = asString(args["flowName"]) ?? "";
|
|
3252
4003
|
const program = deps.recordings.getCompiled(name);
|
|
3253
4004
|
if (program === void 0) {
|
|
3254
4005
|
return Promise.resolve({
|
|
@@ -3262,10 +4013,12 @@ var FLOW_TOOLS = [
|
|
|
3262
4013
|
dynamic: deps.annotations.dynamic(name),
|
|
3263
4014
|
...success !== void 0 ? { success } : {}
|
|
3264
4015
|
};
|
|
3265
|
-
return deps.flows.save(program, annotations).then((res) => {
|
|
3266
|
-
if (res.ok)
|
|
3267
|
-
|
|
3268
|
-
|
|
4016
|
+
return deps.flows.save(program, annotations).then(async (res) => {
|
|
4017
|
+
if (!res.ok)
|
|
4018
|
+
return { error: flowErrorMessage(res.code), code: res.code };
|
|
4019
|
+
deps.annotations.clear(name);
|
|
4020
|
+
const loaded = await deps.flows.load(res.value.name);
|
|
4021
|
+
return loaded.ok ? { ...res.value, assertions: classifyFlowAssertions(loaded.value) } : res.value;
|
|
3269
4022
|
});
|
|
3270
4023
|
}
|
|
3271
4024
|
},
|
|
@@ -3274,22 +4027,27 @@ var FLOW_TOOLS = [
|
|
|
3274
4027
|
description: "List saved flow names under .iris/flows (a fresh agent learns the demonstrated journeys without a browser).",
|
|
3275
4028
|
inputSchema: {},
|
|
3276
4029
|
outputSchema: {
|
|
3277
|
-
flows:
|
|
4030
|
+
flows: z7.array(z7.object({ name: z7.string(), path: z7.string(), createdAt: z7.number().optional() }))
|
|
3278
4031
|
},
|
|
3279
|
-
|
|
4032
|
+
// Return {name, path} objects to MATCH the declared outputSchema. Returning bare name strings
|
|
4033
|
+
// (the prior bug) made schema-validating MCP clients reject the result ("expected object,
|
|
4034
|
+
// received string") — caught driving the live demo.
|
|
4035
|
+
handler: (deps) => deps.flows.list().then((names) => ({
|
|
4036
|
+
flows: names.map((name) => ({ name, path: flowPath(deps.irisRoot, name) }))
|
|
4037
|
+
}))
|
|
3280
4038
|
},
|
|
3281
4039
|
{
|
|
3282
4040
|
name: IrisTool.FLOW_LOAD,
|
|
3283
4041
|
description: "Read + validate a saved flow by flowName from .iris/flows/<flowName>.json. Returns the FlowFile (version, flowName, createdAt, anchored steps) or a structured { error, code }.",
|
|
3284
4042
|
inputSchema: {
|
|
3285
|
-
flowName:
|
|
4043
|
+
flowName: z7.string().describe("Flow file name (without .json extension) from iris_flow_list.")
|
|
3286
4044
|
},
|
|
3287
4045
|
outputSchema: {
|
|
3288
|
-
flowName:
|
|
3289
|
-
steps:
|
|
3290
|
-
createdAt:
|
|
4046
|
+
flowName: z7.string(),
|
|
4047
|
+
steps: z7.array(z7.unknown()),
|
|
4048
|
+
createdAt: z7.number().optional()
|
|
3291
4049
|
},
|
|
3292
|
-
handler: (deps, args) => deps.flows.load(
|
|
4050
|
+
handler: (deps, args) => deps.flows.load(asString(args["flowName"]) ?? "").then((res) => {
|
|
3293
4051
|
if (!res.ok)
|
|
3294
4052
|
return { error: flowErrorMessage(res.code), code: res.code };
|
|
3295
4053
|
const { name, ...rest } = res.value;
|
|
@@ -3298,20 +4056,21 @@ var FLOW_TOOLS = [
|
|
|
3298
4056
|
},
|
|
3299
4057
|
{
|
|
3300
4058
|
name: IrisTool.FLOW_REPLAY,
|
|
3301
|
-
description: `Replay a git-checked flow from .iris/flows/<name>.json. RE-RESOLVES each step's semantic anchor (testid via iris_query; signal via predicate) against the LIVE DOM \u2014 never reuses a stale ref. On an anchor MISS returns legible DRIFT { step, anchor, drift:{ reasonKind, reason, nearest } } (the closest surviving testid) and stops \u2014 the "whose fault is it" contract. Returns { name, status: ok|drift|error, steps:[...] };
|
|
4059
|
+
description: `Replay a git-checked flow from .iris/flows/<name>.json. RE-RESOLVES each step's semantic anchor (testid via iris_query; signal via predicate) against the LIVE DOM \u2014 never reuses a stale ref. On an anchor MISS returns legible DRIFT { step, anchor, drift:{ reasonKind, reason, nearest } } (the closest surviving testid) and stops \u2014 the "whose fault is it" contract. Returns { name, status: ok|drift|error, steps:[...] }; missing/malformed files and action failures are status:error with a structured code (distinct from contract-changed drift).`,
|
|
3302
4060
|
inputSchema: {
|
|
3303
|
-
flowName:
|
|
3304
|
-
|
|
4061
|
+
flowName: z7.string().describe("Flow file name (without .json extension) from iris_flow_list."),
|
|
4062
|
+
confirmDangerous: z7.boolean().optional().describe("Set true to allow destructive controls during this replay only."),
|
|
4063
|
+
sessionId: z7.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3305
4064
|
},
|
|
3306
4065
|
outputSchema: {
|
|
3307
|
-
status:
|
|
3308
|
-
steps:
|
|
3309
|
-
proposals:
|
|
3310
|
-
error:
|
|
4066
|
+
status: z7.string().describe("ok | drift | error"),
|
|
4067
|
+
steps: z7.array(z7.unknown()),
|
|
4068
|
+
proposals: z7.array(z7.unknown()).optional(),
|
|
4069
|
+
error: z7.object({ code: z7.string(), message: z7.string() }).optional()
|
|
3311
4070
|
},
|
|
3312
4071
|
handler: async (deps, args) => {
|
|
3313
4072
|
const startedAt = deps.now();
|
|
3314
|
-
const name =
|
|
4073
|
+
const name = asString(args["flowName"]) ?? "";
|
|
3315
4074
|
const loaded = await deps.flows.load(name);
|
|
3316
4075
|
if (!loaded.ok) {
|
|
3317
4076
|
await recordReplayRun(deps, name, ReplayStatus.ERROR, 0, deps.now() - startedAt);
|
|
@@ -3322,12 +4081,35 @@ var FLOW_TOOLS = [
|
|
|
3322
4081
|
error: { code: loaded.code, message: flowErrorMessage(loaded.code) }
|
|
3323
4082
|
};
|
|
3324
4083
|
}
|
|
3325
|
-
const session = deps.sessions.resolve(
|
|
3326
|
-
const
|
|
4084
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4085
|
+
const replayFloor = session.elapsed();
|
|
4086
|
+
const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, args["confirmDangerous"] === true);
|
|
4087
|
+
const stepsClean = steps.length > 0 && steps.every((s) => s.ok && s.drift === void 0);
|
|
4088
|
+
if (stepsClean && loaded.value.success !== void 0) {
|
|
4089
|
+
const verdict = await assertSuccess(session, loaded.value.success, dynamicTestids(loaded.value), waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, replayFloor);
|
|
4090
|
+
const row = {
|
|
4091
|
+
step: steps.length,
|
|
4092
|
+
tool: "success",
|
|
4093
|
+
anchor: successLabel(loaded.value.success),
|
|
4094
|
+
ok: verdict.pass,
|
|
4095
|
+
...verdict.pass ? {} : { error: verdict.failureReason ?? "flow.success not satisfied" }
|
|
4096
|
+
};
|
|
4097
|
+
steps.push(row);
|
|
4098
|
+
}
|
|
3327
4099
|
const driftSteps = steps.filter((s) => s.drift !== void 0).length;
|
|
3328
4100
|
const allOk = steps.every((s) => s.ok);
|
|
3329
|
-
const status = driftSteps > 0 ? ReplayStatus.DRIFT : allOk ? ReplayStatus.OK : ReplayStatus.
|
|
4101
|
+
const status = driftSteps > 0 ? ReplayStatus.DRIFT : allOk ? ReplayStatus.OK : ReplayStatus.ERROR;
|
|
3330
4102
|
await recordReplayRun(deps, name, status, driftSteps, deps.now() - startedAt);
|
|
4103
|
+
const failed = steps.find((step) => !step.ok && step.drift === void 0);
|
|
4104
|
+
if (failed !== void 0) {
|
|
4105
|
+
const message = failed.error ?? "flow action failed";
|
|
4106
|
+
return {
|
|
4107
|
+
name,
|
|
4108
|
+
status,
|
|
4109
|
+
steps,
|
|
4110
|
+
error: { code: ReplayStatus.ERROR, message }
|
|
4111
|
+
};
|
|
4112
|
+
}
|
|
3331
4113
|
return { name, status, steps };
|
|
3332
4114
|
}
|
|
3333
4115
|
},
|
|
@@ -3335,20 +4117,20 @@ var FLOW_TOOLS = [
|
|
|
3335
4117
|
name: IrisTool.FLOW_SAVE_RECORDED,
|
|
3336
4118
|
description: "Persist the HUMAN-recorded flow from the live tab. The recorder toolbar compiles the human's real clicks/inputs into a semantically anchored FlowFile in-page and emits it; this tool reads the LATEST recorded-flow from the session and writes it to .iris/flows/<name>.json (no recompilation \u2014 the browser already resolved every anchor). Pass `name` to override the recorded name. Returns { name, stepCount, degraded, empty } or { error, code } (code flow_no_recorded when no recording is present).",
|
|
3337
4119
|
inputSchema: {
|
|
3338
|
-
flowName:
|
|
4120
|
+
flowName: z7.string().optional().describe("Override the flow name embedded in the recorded flow. Omit to use the recorder-assigned name."),
|
|
3339
4121
|
...{
|
|
3340
|
-
sessionId:
|
|
4122
|
+
sessionId: z7.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3341
4123
|
}
|
|
3342
4124
|
},
|
|
3343
4125
|
outputSchema: {
|
|
3344
|
-
flowName:
|
|
3345
|
-
stepCount:
|
|
3346
|
-
degraded:
|
|
3347
|
-
error:
|
|
3348
|
-
code:
|
|
4126
|
+
flowName: z7.string().optional(),
|
|
4127
|
+
stepCount: z7.number().optional(),
|
|
4128
|
+
degraded: z7.number().optional(),
|
|
4129
|
+
error: z7.string().optional(),
|
|
4130
|
+
code: z7.string().optional()
|
|
3349
4131
|
},
|
|
3350
4132
|
handler: async (deps, args) => {
|
|
3351
|
-
const session = deps.sessions.resolve(
|
|
4133
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
3352
4134
|
const recorded = latestRecordedFlow(session.eventsSince(0));
|
|
3353
4135
|
if (recorded === void 0) {
|
|
3354
4136
|
return {
|
|
@@ -3356,7 +4138,7 @@ var FLOW_TOOLS = [
|
|
|
3356
4138
|
code: RecordedSaveError.NO_RECORDED_FLOW
|
|
3357
4139
|
};
|
|
3358
4140
|
}
|
|
3359
|
-
const override =
|
|
4141
|
+
const override = asString(args["flowName"]);
|
|
3360
4142
|
const flow = override !== void 0 ? { ...recorded.flow, name: override } : recorded.flow;
|
|
3361
4143
|
const res = await deps.flows.saveFlow(flow);
|
|
3362
4144
|
if (!res.ok)
|
|
@@ -3367,35 +4149,38 @@ var FLOW_TOOLS = [
|
|
|
3367
4149
|
},
|
|
3368
4150
|
{
|
|
3369
4151
|
name: IrisTool.FLOW_HEAL,
|
|
3370
|
-
description: "Self-healing replay. Re-runs iris_flow_replay; on testid DRIFT computes confidence-scored nearest-match rebind PROPOSALS. With apply:false (default) returns the proposed diff WITHOUT writing. With apply:true, writes the confident rebind(s) back into .iris/flows/<name>.json and returns what changed \u2014 never silently. A drift with no proposal above the confidence floor is status:unhealable (file untouched). Returns { name, status: healed|drift|unhealable|nothing_to_heal|error, applied, proposals[], changed[], message }.",
|
|
4152
|
+
description: "Self-healing replay. Re-runs iris_flow_replay; on testid DRIFT computes confidence-scored nearest-match rebind PROPOSALS. With apply:false (default) returns the proposed diff WITHOUT writing. With apply:true, writes the confident rebind(s) back into .iris/flows/<name>.json and returns what changed \u2014 never silently. Before writing, apply re-replays the healed flow and re-asserts its success consequence: if the rebound locator resolves but the consequence no longer fires, the write is REFUSED (status:consequence_broken) \u2014 it heals the locator, never the intent. A drift with no proposal above the confidence floor is status:unhealable (file untouched). Returns { name, status: healed|drift|unhealable|consequence_broken|nothing_to_heal|error, applied, proposals[], changed[], message }.",
|
|
3371
4153
|
inputSchema: {
|
|
3372
|
-
flowName:
|
|
3373
|
-
apply:
|
|
3374
|
-
|
|
4154
|
+
flowName: z7.string().describe("Flow file name to heal (from iris_flow_list)."),
|
|
4155
|
+
apply: z7.boolean().optional(),
|
|
4156
|
+
confirmDangerous: z7.boolean().optional().describe("Set true to allow destructive controls during this heal replay only."),
|
|
4157
|
+
sessionId: z7.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3375
4158
|
},
|
|
3376
4159
|
outputSchema: {
|
|
3377
|
-
flowName:
|
|
3378
|
-
status:
|
|
3379
|
-
applied:
|
|
3380
|
-
proposals:
|
|
3381
|
-
changed:
|
|
3382
|
-
message:
|
|
3383
|
-
error:
|
|
4160
|
+
flowName: z7.string(),
|
|
4161
|
+
status: z7.string(),
|
|
4162
|
+
applied: z7.boolean(),
|
|
4163
|
+
proposals: z7.array(z7.unknown()),
|
|
4164
|
+
changed: z7.array(z7.unknown()),
|
|
4165
|
+
message: z7.string(),
|
|
4166
|
+
error: z7.object({ code: z7.string(), message: z7.string() }).optional()
|
|
3384
4167
|
},
|
|
3385
4168
|
handler: (deps, args) => healFlow(deps, args).then(({ name, ...rest }) => ({ flowName: name, ...rest }))
|
|
3386
4169
|
}
|
|
3387
4170
|
];
|
|
3388
4171
|
var HEAL_MESSAGES = {
|
|
3389
4172
|
NOTHING: "nothing to heal \u2014 every anchor resolved on replay",
|
|
3390
|
-
HEALED: "rewrote drifted testid anchors to their nearest surviving match",
|
|
4173
|
+
HEALED: "rewrote drifted testid anchors to their nearest surviving match and re-verified the flow's success consequence still fires",
|
|
3391
4174
|
DRIFT_DRY: "confident rebind(s) proposed \u2014 re-run with apply:true to write them to disk",
|
|
3392
|
-
UNHEALABLE: `drift found, but no nearest match cleared the confidence floor (HEAL_CONFIDENCE_MIN=${HEAL_CONFIDENCE_MIN}); file left untouched \u2014 add a data-testid or fix the flow by hand
|
|
4175
|
+
UNHEALABLE: `drift found, but no nearest match cleared the confidence floor (HEAL_CONFIDENCE_MIN=${HEAL_CONFIDENCE_MIN}); file left untouched \u2014 add a data-testid or fix the flow by hand`,
|
|
4176
|
+
HEALED_UNVERIFIED: "rewrote drifted testid anchors \u2014 but this flow declares no success consequence, so the rebind resolves a locator without proving the intent still holds. Add a success-state assertion (iris_annotate) so future heals can be verified.",
|
|
4177
|
+
CONSEQUENCE_BROKEN: "rebind resolves the drifted locator to a surviving element, but the healed flow no longer satisfies its success consequence \u2014 refusing to write (a heal that loses the intent would ship a green-but-dead test). Fix by hand and verify"
|
|
3393
4178
|
};
|
|
3394
4179
|
function toChange(proposal) {
|
|
3395
4180
|
return { step: proposal.step, from: proposal.from, to: proposal.to };
|
|
3396
4181
|
}
|
|
3397
4182
|
async function healFlow(deps, args) {
|
|
3398
|
-
const name =
|
|
4183
|
+
const name = asString(args["flowName"]) ?? "";
|
|
3399
4184
|
const apply = args["apply"] === true;
|
|
3400
4185
|
const loaded = await deps.flows.load(name);
|
|
3401
4186
|
if (!loaded.ok) {
|
|
@@ -3409,9 +4194,22 @@ async function healFlow(deps, args) {
|
|
|
3409
4194
|
error: { code: loaded.code, message: flowErrorMessage(loaded.code) }
|
|
3410
4195
|
};
|
|
3411
4196
|
}
|
|
3412
|
-
const session = deps.sessions.resolve(
|
|
3413
|
-
const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS);
|
|
4197
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4198
|
+
const steps = await replayFlow(session, loaded.value, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, args["confirmDangerous"] === true);
|
|
3414
4199
|
const drifted = steps.some((s) => s.drift !== void 0);
|
|
4200
|
+
const failed = steps.find((s) => !s.ok && s.drift === void 0);
|
|
4201
|
+
if (failed !== void 0) {
|
|
4202
|
+
const message = failed.error ?? "flow replay failed before an anchor could be healed";
|
|
4203
|
+
return {
|
|
4204
|
+
name,
|
|
4205
|
+
status: HealStatus.ERROR,
|
|
4206
|
+
applied: false,
|
|
4207
|
+
proposals: [],
|
|
4208
|
+
changed: [],
|
|
4209
|
+
message,
|
|
4210
|
+
error: { code: ReplayStatus.ERROR, message }
|
|
4211
|
+
};
|
|
4212
|
+
}
|
|
3415
4213
|
if (!drifted) {
|
|
3416
4214
|
return {
|
|
3417
4215
|
name,
|
|
@@ -3443,6 +4241,23 @@ async function healFlow(deps, args) {
|
|
|
3443
4241
|
message: HEAL_MESSAGES.DRIFT_DRY
|
|
3444
4242
|
};
|
|
3445
4243
|
}
|
|
4244
|
+
const { flow: healed } = applyHealChanges(loaded.value, proposals.map(toChange));
|
|
4245
|
+
if (healed.success !== void 0) {
|
|
4246
|
+
const verifyFloor = session.elapsed();
|
|
4247
|
+
const verifySteps = await replayFlow(session, healed, waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, args["confirmDangerous"] === true);
|
|
4248
|
+
const verifyClean = verifySteps.length > 0 && verifySteps.every((s) => s.ok && s.drift === void 0);
|
|
4249
|
+
const verdict = verifyClean ? await assertSuccess(session, healed.success, dynamicTestids(healed), waitForPredicate, FLOW_SIGNAL_TIMEOUT_MS, verifyFloor) : { pass: false, failureReason: "healed flow did not replay cleanly" };
|
|
4250
|
+
if (!verdict.pass) {
|
|
4251
|
+
return {
|
|
4252
|
+
name,
|
|
4253
|
+
status: HealStatus.CONSEQUENCE_BROKEN,
|
|
4254
|
+
applied: false,
|
|
4255
|
+
proposals,
|
|
4256
|
+
changed: [],
|
|
4257
|
+
message: `${HEAL_MESSAGES.CONSEQUENCE_BROKEN} (${successLabel(healed.success)}: ${verdict.failureReason ?? "not satisfied"})`
|
|
4258
|
+
};
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
3446
4261
|
const written = await deps.flows.heal(name, proposals.map(toChange));
|
|
3447
4262
|
if (!written.ok) {
|
|
3448
4263
|
return {
|
|
@@ -3461,14 +4276,14 @@ async function healFlow(deps, args) {
|
|
|
3461
4276
|
applied: written.value.changed.length > 0,
|
|
3462
4277
|
proposals,
|
|
3463
4278
|
changed: written.value.changed,
|
|
3464
|
-
message: HEAL_MESSAGES.HEALED
|
|
4279
|
+
message: loaded.value.success !== void 0 ? HEAL_MESSAGES.HEALED : HEAL_MESSAGES.HEALED_UNVERIFIED
|
|
3465
4280
|
};
|
|
3466
4281
|
}
|
|
3467
4282
|
|
|
3468
4283
|
// ../server/dist/project/project-tools.js
|
|
3469
|
-
import { z as
|
|
4284
|
+
import { z as z8 } from "zod";
|
|
3470
4285
|
var sessionIdShape3 = {
|
|
3471
|
-
sessionId:
|
|
4286
|
+
sessionId: z8.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3472
4287
|
};
|
|
3473
4288
|
var REGRESSION_STATUSES = /* @__PURE__ */ new Set([
|
|
3474
4289
|
RunStatus.FAIL,
|
|
@@ -3509,12 +4324,12 @@ var PROJECT_TOOLS = [
|
|
|
3509
4324
|
name: IrisTool.PROJECT,
|
|
3510
4325
|
description: 'Read cross-run history from .iris/project.json \u2014 the memory of how past runs behaved. With { name } it also returns the last run for that flow plus a diff-vs-last summary (status change, regressed flag, consoleErrors/driftSteps deltas) so you can answer "did it behave like last time?". Returns { runs, learned?, lastRun?, diff? } or { error, reason } when no/invalid history exists.',
|
|
3511
4326
|
inputSchema: {
|
|
3512
|
-
name:
|
|
4327
|
+
name: z8.string().optional().describe("Filter runs by this name. Omit to return all runs."),
|
|
3513
4328
|
...sessionIdShape3
|
|
3514
4329
|
},
|
|
3515
4330
|
outputSchema: {
|
|
3516
|
-
runs:
|
|
3517
|
-
diff:
|
|
4331
|
+
runs: z8.array(z8.unknown()),
|
|
4332
|
+
diff: z8.unknown().optional()
|
|
3518
4333
|
},
|
|
3519
4334
|
handler: async (deps, args) => {
|
|
3520
4335
|
const read = await deps.project.read();
|
|
@@ -3524,7 +4339,7 @@ var PROJECT_TOOLS = [
|
|
|
3524
4339
|
reason: read.reason
|
|
3525
4340
|
};
|
|
3526
4341
|
}
|
|
3527
|
-
const name =
|
|
4342
|
+
const name = asString(args["name"]);
|
|
3528
4343
|
if (name === void 0) {
|
|
3529
4344
|
return { runs: read.file.runs, learned: read.file.learned };
|
|
3530
4345
|
}
|
|
@@ -3542,22 +4357,22 @@ var PROJECT_TOOLS = [
|
|
|
3542
4357
|
name: IrisTool.RUN_RECORD,
|
|
3543
4358
|
description: "Explicitly record a run outcome into .iris/project.json (the manual companion to the auto-record on iris_flow_replay). Use it to log the result of an assertion sequence or a manual journey so future runs can diff against it. Returns { recorded:true, name, status }.",
|
|
3544
4359
|
inputSchema: {
|
|
3545
|
-
name:
|
|
3546
|
-
status:
|
|
3547
|
-
kind:
|
|
3548
|
-
summary:
|
|
4360
|
+
name: z8.string().describe("Run name for grouping in iris_project history."),
|
|
4361
|
+
status: z8.nativeEnum(RunStatus).describe("Outcome: pass | fail | drift | error"),
|
|
4362
|
+
kind: z8.nativeEnum(RunKind).optional(),
|
|
4363
|
+
summary: z8.string().optional().describe("One-line human summary of what this run covered."),
|
|
3549
4364
|
...sessionIdShape3
|
|
3550
4365
|
},
|
|
3551
4366
|
outputSchema: {
|
|
3552
|
-
recorded:
|
|
3553
|
-
runName:
|
|
3554
|
-
status:
|
|
4367
|
+
recorded: z8.boolean(),
|
|
4368
|
+
runName: z8.string(),
|
|
4369
|
+
status: z8.string()
|
|
3555
4370
|
},
|
|
3556
4371
|
handler: async (deps, args) => {
|
|
3557
|
-
const name =
|
|
4372
|
+
const name = asString(args["name"]) ?? "";
|
|
3558
4373
|
const status = args["status"];
|
|
3559
4374
|
const kindArg = args["kind"];
|
|
3560
|
-
const summary =
|
|
4375
|
+
const summary = asString(args["summary"]);
|
|
3561
4376
|
await deps.project.recordRun({
|
|
3562
4377
|
kind: typeof kindArg === "string" ? kindArg : RunKind.MANUAL,
|
|
3563
4378
|
name,
|
|
@@ -3570,7 +4385,7 @@ var PROJECT_TOOLS = [
|
|
|
3570
4385
|
];
|
|
3571
4386
|
|
|
3572
4387
|
// ../server/dist/visual/visual-tools.js
|
|
3573
|
-
import { z as
|
|
4388
|
+
import { z as z9 } from "zod";
|
|
3574
4389
|
|
|
3575
4390
|
// ../server/dist/visual/visual-diff.js
|
|
3576
4391
|
async function loadDeps() {
|
|
@@ -3721,13 +4536,13 @@ var VisualStore = class {
|
|
|
3721
4536
|
|
|
3722
4537
|
// ../server/dist/visual/visual-tools.js
|
|
3723
4538
|
var sessionIdShape4 = {
|
|
3724
|
-
sessionId:
|
|
4539
|
+
sessionId: z9.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3725
4540
|
};
|
|
3726
|
-
var rectShape =
|
|
3727
|
-
x:
|
|
3728
|
-
y:
|
|
3729
|
-
width:
|
|
3730
|
-
height:
|
|
4541
|
+
var rectShape = z9.object({
|
|
4542
|
+
x: z9.number(),
|
|
4543
|
+
y: z9.number(),
|
|
4544
|
+
width: z9.number(),
|
|
4545
|
+
height: z9.number()
|
|
3731
4546
|
});
|
|
3732
4547
|
function screenshotProvider(deps) {
|
|
3733
4548
|
const p = deps.realInput;
|
|
@@ -3739,11 +4554,11 @@ var noProvider = {
|
|
|
3739
4554
|
recommendation: VISUAL_NO_PROVIDER_RECOMMENDATION
|
|
3740
4555
|
};
|
|
3741
4556
|
function asBox(value) {
|
|
3742
|
-
const b =
|
|
3743
|
-
const x =
|
|
3744
|
-
const y =
|
|
3745
|
-
const w =
|
|
3746
|
-
const h =
|
|
4557
|
+
const b = asRecord(asRecord(value)["box"]);
|
|
4558
|
+
const x = asNumber(b["x"]);
|
|
4559
|
+
const y = asNumber(b["y"]);
|
|
4560
|
+
const w = asNumber(b["width"]);
|
|
4561
|
+
const h = asNumber(b["height"]);
|
|
3747
4562
|
if (x === void 0 || y === void 0 || w === void 0 || h === void 0)
|
|
3748
4563
|
return void 0;
|
|
3749
4564
|
if (w <= 0 || h <= 0)
|
|
@@ -3753,12 +4568,12 @@ function asBox(value) {
|
|
|
3753
4568
|
async function buildOpts(deps, sessionId, args) {
|
|
3754
4569
|
const clipArg = args["clip"];
|
|
3755
4570
|
if (clipArg !== void 0) {
|
|
3756
|
-
const c =
|
|
4571
|
+
const c = asRecord(clipArg);
|
|
3757
4572
|
const box = asBox({ box: c });
|
|
3758
4573
|
if (box !== void 0)
|
|
3759
4574
|
return { clip: box };
|
|
3760
4575
|
}
|
|
3761
|
-
const ref =
|
|
4576
|
+
const ref = asString(args["ref"]);
|
|
3762
4577
|
if (ref !== void 0) {
|
|
3763
4578
|
const session = deps.sessions.resolve(sessionId);
|
|
3764
4579
|
const res = await session.command(IrisCommand.INSPECT, { ref });
|
|
@@ -3784,31 +4599,31 @@ var VISUAL_TOOLS = [
|
|
|
3784
4599
|
name: IrisTool.SCREENSHOT,
|
|
3785
4600
|
description: "Capture a pixel screenshot of the DRIVEN page (needs `iris drive`/IRIS_CDP_URL \u2014 the SDK has no screenshotter) and save it as a visual baseline at .iris/visual/<name>.png. { fullPage } for the whole scroll height, { ref } or { clip:{x,y,width,height} } for one element/region. Returns { saved:true, name, path, bytes } or { ok:false, reason } when no driven browser is attached.",
|
|
3786
4601
|
inputSchema: {
|
|
3787
|
-
name:
|
|
3788
|
-
fullPage:
|
|
3789
|
-
ref:
|
|
4602
|
+
name: z9.string().describe("Baseline name \u2014 saved as .iris/visual/<name>.png. Use the same name in iris_visual_diff to compare."),
|
|
4603
|
+
fullPage: z9.boolean().optional().describe("Capture the full scroll height. Default: viewport only."),
|
|
4604
|
+
ref: z9.string().optional().describe("Element ref to screenshot (scopes to element bounding box). Omit for full page."),
|
|
3790
4605
|
clip: rectShape.optional().describe("Explicit { x, y, width, height } clip rectangle in page coordinates."),
|
|
3791
4606
|
...sessionIdShape4
|
|
3792
4607
|
},
|
|
3793
4608
|
outputSchema: {
|
|
3794
|
-
ok:
|
|
3795
|
-
saved:
|
|
3796
|
-
name:
|
|
3797
|
-
path:
|
|
3798
|
-
bytes:
|
|
3799
|
-
reason:
|
|
3800
|
-
recommendation:
|
|
4609
|
+
ok: z9.boolean(),
|
|
4610
|
+
saved: z9.boolean().optional(),
|
|
4611
|
+
name: z9.string().optional(),
|
|
4612
|
+
path: z9.string().optional(),
|
|
4613
|
+
bytes: z9.number().optional(),
|
|
4614
|
+
reason: z9.string().optional(),
|
|
4615
|
+
recommendation: z9.string().optional()
|
|
3801
4616
|
},
|
|
3802
4617
|
handler: async (deps, args) => {
|
|
3803
4618
|
const provider = screenshotProvider(deps);
|
|
3804
4619
|
if (provider === void 0)
|
|
3805
4620
|
return noProvider;
|
|
3806
|
-
const sessionId =
|
|
4621
|
+
const sessionId = asString(args["sessionId"]);
|
|
3807
4622
|
const session = deps.sessions.resolve(sessionId);
|
|
3808
4623
|
const png = await provider.screenshot(session.url, await buildOpts(deps, sessionId, args));
|
|
3809
4624
|
if (png === void 0)
|
|
3810
4625
|
return { ok: false, reason: VisualReason.CAPTURE_FAILED };
|
|
3811
|
-
const name =
|
|
4626
|
+
const name = asString(args["name"]) ?? "default";
|
|
3812
4627
|
const store = new VisualStore(deps.fs, deps.irisRoot);
|
|
3813
4628
|
const path = await store.saveBaseline(name, png);
|
|
3814
4629
|
return { ok: true, saved: true, name, path, bytes: png.length };
|
|
@@ -3818,39 +4633,39 @@ var VISUAL_TOOLS = [
|
|
|
3818
4633
|
name: IrisTool.VISUAL_DIFF,
|
|
3819
4634
|
description: "Perceptually diff the DRIVEN page against a saved visual baseline (see iris_screenshot). { masks:[{x,y,width,height}] } neutralizes volatile regions; { maxRatio } sets the pass tolerance (default 0). Returns { matched, changedPixels, totalPixels, ratio, region?, diffPath, dimensionMismatch } \u2014 the overlay diff is written to .iris/visual/<baseline>.diff.png \u2014 or { ok:false, reason } (no-provider / baseline-missing).",
|
|
3820
4635
|
inputSchema: {
|
|
3821
|
-
baseline:
|
|
3822
|
-
fullPage:
|
|
3823
|
-
ref:
|
|
4636
|
+
baseline: z9.string().describe("Baseline screenshot name (from iris_screenshot). Used to compare with the current screenshot."),
|
|
4637
|
+
fullPage: z9.boolean().optional(),
|
|
4638
|
+
ref: z9.string().optional(),
|
|
3824
4639
|
clip: rectShape.optional(),
|
|
3825
|
-
masks:
|
|
3826
|
-
maxRatio:
|
|
3827
|
-
threshold:
|
|
4640
|
+
masks: z9.array(rectShape).optional(),
|
|
4641
|
+
maxRatio: z9.number().optional(),
|
|
4642
|
+
threshold: z9.number().optional().describe("Pixel difference threshold (0\u20131). Default: 0.01."),
|
|
3828
4643
|
...sessionIdShape4
|
|
3829
4644
|
},
|
|
3830
4645
|
outputSchema: {
|
|
3831
|
-
ok:
|
|
3832
|
-
match:
|
|
3833
|
-
diffPct:
|
|
3834
|
-
diffPath:
|
|
3835
|
-
reason:
|
|
4646
|
+
ok: z9.boolean(),
|
|
4647
|
+
match: z9.boolean().optional(),
|
|
4648
|
+
diffPct: z9.number().optional(),
|
|
4649
|
+
diffPath: z9.string().optional(),
|
|
4650
|
+
reason: z9.string().optional()
|
|
3836
4651
|
},
|
|
3837
4652
|
handler: async (deps, args) => {
|
|
3838
4653
|
const provider = screenshotProvider(deps);
|
|
3839
4654
|
if (provider === void 0)
|
|
3840
4655
|
return noProvider;
|
|
3841
|
-
const baseline =
|
|
4656
|
+
const baseline = asString(args["baseline"]) ?? "";
|
|
3842
4657
|
const store = new VisualStore(deps.fs, deps.irisRoot);
|
|
3843
4658
|
const baselineBytes = await store.readBaseline(baseline);
|
|
3844
4659
|
if (baselineBytes === void 0)
|
|
3845
4660
|
return { ok: false, reason: VisualReason.BASELINE_MISSING };
|
|
3846
|
-
const sessionId =
|
|
4661
|
+
const sessionId = asString(args["sessionId"]);
|
|
3847
4662
|
const session = deps.sessions.resolve(sessionId);
|
|
3848
4663
|
const current = await provider.screenshot(session.url, await buildOpts(deps, sessionId, args));
|
|
3849
4664
|
if (current === void 0)
|
|
3850
4665
|
return { ok: false, reason: VisualReason.CAPTURE_FAILED };
|
|
3851
4666
|
const masks = rectsFrom(args["masks"]);
|
|
3852
|
-
const threshold =
|
|
3853
|
-
const maxRatio =
|
|
4667
|
+
const threshold = asNumber(args["threshold"]);
|
|
4668
|
+
const maxRatio = asNumber(args["maxRatio"]);
|
|
3854
4669
|
const result = await diffPng(baselineBytes, current, {
|
|
3855
4670
|
...threshold !== void 0 ? { threshold } : {},
|
|
3856
4671
|
...maxRatio !== void 0 ? { maxRatio } : {},
|
|
@@ -3867,7 +4682,7 @@ var VISUAL_TOOLS = [
|
|
|
3867
4682
|
];
|
|
3868
4683
|
|
|
3869
4684
|
// ../server/dist/crawl/crawl-tools.js
|
|
3870
|
-
import { z as
|
|
4685
|
+
import { z as z10 } from "zod";
|
|
3871
4686
|
|
|
3872
4687
|
// ../server/dist/crawl/crawl.js
|
|
3873
4688
|
function isActivity(e) {
|
|
@@ -3880,7 +4695,7 @@ function failedRequests(events, floor) {
|
|
|
3880
4695
|
return events.filter((e) => {
|
|
3881
4696
|
if (e.type !== EventType.NET_REQUEST)
|
|
3882
4697
|
return false;
|
|
3883
|
-
const status =
|
|
4698
|
+
const status = asNumber(e.data["status"]);
|
|
3884
4699
|
return status !== void 0 && status >= floor;
|
|
3885
4700
|
});
|
|
3886
4701
|
}
|
|
@@ -3910,7 +4725,7 @@ async function crawl(session, opts, sleep) {
|
|
|
3910
4725
|
const act = await session.command(IrisCommand.ACT, {
|
|
3911
4726
|
ref: item.ref,
|
|
3912
4727
|
action: ActionType.CLICK,
|
|
3913
|
-
args: {}
|
|
4728
|
+
args: opts.confirmDangerous === true ? { [DANGEROUS_ACTION_CONFIRM_ARG]: true } : {}
|
|
3914
4729
|
});
|
|
3915
4730
|
await sleep(settleMs);
|
|
3916
4731
|
const events = session.eventsSince(since);
|
|
@@ -3921,14 +4736,14 @@ async function crawl(session, opts, sleep) {
|
|
|
3921
4736
|
kind: CrawlAnomalyKind.CONSOLE_ERROR,
|
|
3922
4737
|
ref: item.ref,
|
|
3923
4738
|
desc: item.desc,
|
|
3924
|
-
detail:
|
|
4739
|
+
detail: asString(e.data["message"]) ?? e.type
|
|
3925
4740
|
});
|
|
3926
4741
|
}
|
|
3927
4742
|
for (const e of failedRequests(events, CRAWL_DEFAULTS.FAILED_STATUS)) {
|
|
3928
4743
|
counts.failedRequests += 1;
|
|
3929
|
-
const method =
|
|
3930
|
-
const url =
|
|
3931
|
-
const status =
|
|
4744
|
+
const method = asString(e.data["method"]) ?? "";
|
|
4745
|
+
const url = asString(e.data["url"]) ?? "";
|
|
4746
|
+
const status = asNumber(e.data["status"]);
|
|
3932
4747
|
anomalies.push({
|
|
3933
4748
|
kind: CrawlAnomalyKind.FAILED_REQUEST,
|
|
3934
4749
|
ref: item.ref,
|
|
@@ -3936,7 +4751,7 @@ async function crawl(session, opts, sleep) {
|
|
|
3936
4751
|
detail: `${method} ${url} \u2192 ${status ?? ""}`.trim()
|
|
3937
4752
|
});
|
|
3938
4753
|
}
|
|
3939
|
-
const dispatched =
|
|
4754
|
+
const dispatched = asRecord(act.result)["dispatched"] !== false && act.ok;
|
|
3940
4755
|
if (dispatched && errs.length === 0 && !events.some(isActivity)) {
|
|
3941
4756
|
counts.deadControls += 1;
|
|
3942
4757
|
anomalies.push({
|
|
@@ -3964,32 +4779,34 @@ var CRAWL_TOOLS = [
|
|
|
3964
4779
|
name: IrisTool.CRAWL,
|
|
3965
4780
|
description: "Autonomously click every reachable interactive control (bounded by maxSteps, default 25) and report anomalies WITHOUT a script: console errors, failed requests (status \u2265 400), and DEAD controls (dispatched but the app did nothing). DESTRUCTIVE \u2014 it really clicks (may navigate/mutate state); use iris_explore first for a non-destructive list. Returns { interactiveFound, stepsRun, anomalies[{kind,ref,desc,detail}], counts, visited, truncated }.",
|
|
3966
4781
|
inputSchema: {
|
|
3967
|
-
maxSteps:
|
|
3968
|
-
settleMs:
|
|
3969
|
-
scope:
|
|
3970
|
-
|
|
4782
|
+
maxSteps: z10.number().optional().describe("Maximum number of controls to click. Default: 25."),
|
|
4783
|
+
settleMs: z10.number().optional().describe("Milliseconds to wait after each click for the app to react. Default: 500."),
|
|
4784
|
+
scope: z10.string().optional().describe("CSS selector or element ref to restrict crawling to a subtree."),
|
|
4785
|
+
confirmDangerous: z10.boolean().optional().describe("Set true to allow controls classified as destructive. Default false; those controls are blocked by the browser."),
|
|
4786
|
+
sessionId: z10.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
3971
4787
|
},
|
|
3972
4788
|
outputSchema: {
|
|
3973
|
-
interactiveFound:
|
|
3974
|
-
stepsRun:
|
|
3975
|
-
anomalies:
|
|
3976
|
-
kind:
|
|
3977
|
-
ref:
|
|
3978
|
-
desc:
|
|
3979
|
-
detail:
|
|
4789
|
+
interactiveFound: z10.number(),
|
|
4790
|
+
stepsRun: z10.number(),
|
|
4791
|
+
anomalies: z10.array(z10.object({
|
|
4792
|
+
kind: z10.string(),
|
|
4793
|
+
ref: z10.string(),
|
|
4794
|
+
desc: z10.string(),
|
|
4795
|
+
detail: z10.string().optional()
|
|
3980
4796
|
})),
|
|
3981
|
-
counts:
|
|
3982
|
-
truncated:
|
|
4797
|
+
counts: z10.record(z10.number()),
|
|
4798
|
+
truncated: z10.boolean()
|
|
3983
4799
|
},
|
|
3984
4800
|
handler: (deps, args) => {
|
|
3985
|
-
const session = deps.sessions.resolve(
|
|
3986
|
-
const maxSteps =
|
|
3987
|
-
const settleMs =
|
|
3988
|
-
const scope =
|
|
4801
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4802
|
+
const maxSteps = asNumber(args["maxSteps"]);
|
|
4803
|
+
const settleMs = asNumber(args["settleMs"]);
|
|
4804
|
+
const scope = asString(args["scope"]);
|
|
3989
4805
|
const opts = {
|
|
3990
4806
|
...maxSteps !== void 0 ? { maxSteps } : {},
|
|
3991
4807
|
...settleMs !== void 0 ? { settleMs } : {},
|
|
3992
|
-
...scope !== void 0 ? { scope } : {}
|
|
4808
|
+
...scope !== void 0 ? { scope } : {},
|
|
4809
|
+
...args["confirmDangerous"] === true ? { confirmDangerous: true } : {}
|
|
3993
4810
|
};
|
|
3994
4811
|
return crawl(session, opts, nodeSleep2);
|
|
3995
4812
|
}
|
|
@@ -3997,7 +4814,7 @@ var CRAWL_TOOLS = [
|
|
|
3997
4814
|
];
|
|
3998
4815
|
|
|
3999
4816
|
// ../server/dist/input/scroll-tools.js
|
|
4000
|
-
import { z as
|
|
4817
|
+
import { z as z11 } from "zod";
|
|
4001
4818
|
|
|
4002
4819
|
// ../server/dist/input/scroll-find.js
|
|
4003
4820
|
async function queryFirst(session, q) {
|
|
@@ -4006,9 +4823,9 @@ async function queryFirst(session, q) {
|
|
|
4006
4823
|
value: q.value,
|
|
4007
4824
|
...q.name !== void 0 ? { name: q.name } : {}
|
|
4008
4825
|
});
|
|
4009
|
-
const elements =
|
|
4826
|
+
const elements = asRecord(res.result)["elements"];
|
|
4010
4827
|
if (Array.isArray(elements) && elements.length > 0)
|
|
4011
|
-
return
|
|
4828
|
+
return asRecord(elements[0]);
|
|
4012
4829
|
return void 0;
|
|
4013
4830
|
}
|
|
4014
4831
|
async function scrollToFind(session, q, opts = {}) {
|
|
@@ -4027,7 +4844,7 @@ async function scrollToFind(session, q, opts = {}) {
|
|
|
4027
4844
|
const hit = await queryFirst(session, q);
|
|
4028
4845
|
if (hit !== void 0)
|
|
4029
4846
|
return { found: true, element: hit, scrolls, exhausted: false };
|
|
4030
|
-
const data =
|
|
4847
|
+
const data = asRecord(sr.result);
|
|
4031
4848
|
if (data["atEnd"] === true || data["scrolled"] === false) {
|
|
4032
4849
|
return { found: false, scrolls, exhausted: true };
|
|
4033
4850
|
}
|
|
@@ -4035,7 +4852,7 @@ async function scrollToFind(session, q, opts = {}) {
|
|
|
4035
4852
|
for (let i = 0; i < max; i += 1) {
|
|
4036
4853
|
const sr = await session.command(IrisCommand.SCROLL, q.container !== void 0 ? { ref: q.container } : {});
|
|
4037
4854
|
scrolls += 1;
|
|
4038
|
-
const data =
|
|
4855
|
+
const data = asRecord(sr.result);
|
|
4039
4856
|
const hit = await queryFirst(session, q);
|
|
4040
4857
|
if (hit !== void 0)
|
|
4041
4858
|
return { found: true, element: hit, scrolls, exhausted: false };
|
|
@@ -4052,58 +4869,58 @@ var SCROLL_TOOLS = [
|
|
|
4052
4869
|
name: IrisTool.SCROLL_TO,
|
|
4053
4870
|
description: "Find an element in a VIRTUALIZED list that has not rendered yet. Pass `by` (role|text|testid|label|placeholder|alt) and `value` (query string) to identify the target row. Scrolls the container until the row mounts, the list ends, or maxScrolls (default 20) is spent. Pass targetIndex + totalCount for bisection \u2014 jumps directly to the estimated offset in one scroll (e.g. targetIndex:800 totalCount:1000 jumps to 80% of scrollHeight). Returns { found, element?, scrolls, exhausted }.",
|
|
4054
4871
|
inputSchema: {
|
|
4055
|
-
by:
|
|
4056
|
-
value:
|
|
4057
|
-
name:
|
|
4058
|
-
container:
|
|
4059
|
-
maxScrolls:
|
|
4060
|
-
targetIndex:
|
|
4061
|
-
totalCount:
|
|
4062
|
-
sessionId:
|
|
4872
|
+
by: z11.string().describe("Query strategy for finding the target: role | text | testid | label | placeholder | alt"),
|
|
4873
|
+
value: z11.string().describe("Query value for the selected strategy (the element to scroll into view)."),
|
|
4874
|
+
name: z11.string().optional().describe("Optional accessible name filter when using by=role."),
|
|
4875
|
+
container: z11.string().optional().describe("Element ref for the scrollable container. Omit to scroll the document."),
|
|
4876
|
+
maxScrolls: z11.number().optional().describe("Maximum number of scroll steps before giving up. Default: 20."),
|
|
4877
|
+
targetIndex: z11.number().optional().describe("Known row index of the target in the list. Combine with totalCount for bisection \u2014 jumps directly to the estimated offset."),
|
|
4878
|
+
totalCount: z11.number().optional().describe("Total item count in the virtualized list. Required for bisection with targetIndex."),
|
|
4879
|
+
sessionId: z11.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
4063
4880
|
},
|
|
4064
4881
|
outputSchema: {
|
|
4065
|
-
found:
|
|
4066
|
-
element:
|
|
4067
|
-
scrolls:
|
|
4068
|
-
exhausted:
|
|
4882
|
+
found: z11.boolean(),
|
|
4883
|
+
element: z11.object({ ref: z11.string(), role: z11.string(), name: z11.string() }).optional(),
|
|
4884
|
+
scrolls: z11.number(),
|
|
4885
|
+
exhausted: z11.boolean()
|
|
4069
4886
|
},
|
|
4070
4887
|
handler: (deps, args) => {
|
|
4071
|
-
const session = deps.sessions.resolve(
|
|
4072
|
-
const name =
|
|
4073
|
-
const container =
|
|
4074
|
-
const targetIndex =
|
|
4075
|
-
const totalCount =
|
|
4888
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4889
|
+
const name = asString(args["name"]);
|
|
4890
|
+
const container = asString(args["container"]);
|
|
4891
|
+
const targetIndex = asNumber(args["targetIndex"]);
|
|
4892
|
+
const totalCount = asNumber(args["totalCount"]);
|
|
4076
4893
|
const q = {
|
|
4077
|
-
by:
|
|
4078
|
-
value:
|
|
4894
|
+
by: asString(args["by"]) ?? "",
|
|
4895
|
+
value: asString(args["value"]) ?? "",
|
|
4079
4896
|
...name !== void 0 ? { name } : {},
|
|
4080
4897
|
...container !== void 0 ? { container } : {},
|
|
4081
4898
|
...targetIndex !== void 0 ? { targetIndex } : {},
|
|
4082
4899
|
...totalCount !== void 0 ? { totalCount } : {}
|
|
4083
4900
|
};
|
|
4084
|
-
const maxScrolls =
|
|
4901
|
+
const maxScrolls = asNumber(args["maxScrolls"]);
|
|
4085
4902
|
return scrollToFind(session, q, maxScrolls !== void 0 ? { maxScrolls } : {});
|
|
4086
4903
|
}
|
|
4087
4904
|
}
|
|
4088
4905
|
];
|
|
4089
4906
|
|
|
4090
4907
|
// ../server/dist/session/session-tools.js
|
|
4091
|
-
import { z as
|
|
4908
|
+
import { z as z12 } from "zod";
|
|
4092
4909
|
var SESSION_TOOLS = [
|
|
4093
4910
|
{
|
|
4094
4911
|
name: IrisTool.SESSION,
|
|
4095
4912
|
description: "Tune the presenter session for this app. { idleEndMs } sets how long the session stays open after you go quiet before it AUTO-ENDS (page glow off, the floating panel is kept so the human can read + Copy/Export the run). Default 5min. Raise it for slow apps, lower it for quick checks. The auto-end is enforced SERVER-SIDE (immune to background-tab throttling) and also fires if you (the MCP client) disconnect \u2014 so a forgotten or crashed session never leaves the HUD running forever. If you go quiet and then act again, the session revives automatically. Returns { applied, idleEndMs }.",
|
|
4096
4913
|
inputSchema: {
|
|
4097
|
-
idleEndMs:
|
|
4098
|
-
sessionId:
|
|
4914
|
+
idleEndMs: z12.number().optional().describe("Idle window in milliseconds after which the presenter session auto-ends. Default: 300000 (5 min). Raise for slow apps."),
|
|
4915
|
+
sessionId: z12.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
4099
4916
|
},
|
|
4100
4917
|
outputSchema: {
|
|
4101
|
-
applied:
|
|
4102
|
-
idleEndMs:
|
|
4918
|
+
applied: z12.boolean(),
|
|
4919
|
+
idleEndMs: z12.number().optional()
|
|
4103
4920
|
},
|
|
4104
4921
|
handler: async (deps, args) => {
|
|
4105
|
-
const session = deps.sessions.resolve(
|
|
4106
|
-
const idleEndMs =
|
|
4922
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4923
|
+
const idleEndMs = asNumber(args["idleEndMs"]);
|
|
4107
4924
|
if (idleEndMs !== void 0)
|
|
4108
4925
|
session.setIdleEndMs(idleEndMs);
|
|
4109
4926
|
const res = await session.command(IrisCommand.SESSION_CONFIG, idleEndMs !== void 0 ? { idleEndMs } : {});
|
|
@@ -4115,7 +4932,7 @@ var SESSION_TOOLS = [
|
|
|
4115
4932
|
];
|
|
4116
4933
|
|
|
4117
4934
|
// ../server/dist/flows/annotate-tools.js
|
|
4118
|
-
import { z as
|
|
4935
|
+
import { z as z13 } from "zod";
|
|
4119
4936
|
|
|
4120
4937
|
// ../server/dist/flows/annotate.js
|
|
4121
4938
|
function compileAnnotation(a, stepCount) {
|
|
@@ -4192,23 +5009,23 @@ var ANNOTATE_TOOLS = [
|
|
|
4192
5009
|
description: "Attach a STRUCTURED annotation to the active recording, compiling it into the flow. kind: assert-signal { name, dataMatches? } \u2192 the last step asserts that signal; assert-visible { testid } \u2192 the last step asserts that element is present; mark-dynamic { testid } \u2192 the flow records that region as LLM-dynamic (replay won't assert its content); success-state { signal | testid } \u2192 the flow golden end-condition. Folded onto disk by iris_flow_save. Returns { ok:true, target:step|flow, compiled } (e.g. \"will assert signal diff:shown\") or { ok:false, code } (annotate_no_recording | annotate_no_step | annotate_unknown_kind | annotate_missing_field). FIRST CUT: structured only \u2014 a free natural-language string is rejected (annotate_unknown_kind), never guessed into a predicate. Pass `flow` to target a named recording (defaults to 'default'); `name` is the assert-signal's SIGNAL name, not the recording.",
|
|
4193
5010
|
inputSchema: {
|
|
4194
5011
|
// `flow` selects the recording; `name`/`signal`/`testid`/`dataMatches` are the annotation fields.
|
|
4195
|
-
flow:
|
|
4196
|
-
kind:
|
|
4197
|
-
name:
|
|
4198
|
-
testid:
|
|
4199
|
-
signal:
|
|
4200
|
-
dataMatches:
|
|
4201
|
-
sessionId:
|
|
4202
|
-
annotation:
|
|
5012
|
+
flow: z13.string().optional().describe("Named recording to annotate. Defaults to 'default'."),
|
|
5013
|
+
kind: z13.string().describe("Annotation kind: assert-signal | assert-visible | mark-dynamic | success-state."),
|
|
5014
|
+
name: z13.string().optional().describe("Signal name for assert-signal annotations."),
|
|
5015
|
+
testid: z13.string().optional().describe("data-testid value for assert-visible / mark-dynamic / success-state annotations."),
|
|
5016
|
+
signal: z13.string().optional().describe("Signal name for success-state annotations."),
|
|
5017
|
+
dataMatches: z13.record(z13.unknown()).optional().describe("Key/value pairs the signal payload must match (assert-signal only)."),
|
|
5018
|
+
sessionId: z13.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open."),
|
|
5019
|
+
annotation: z13.unknown().optional().describe("Structured annotation: { kind, name, dataMatches? } for assert-signal; { kind, testid } for assert-visible / mark-dynamic; { kind, signal?, testid? } for success-state.")
|
|
4203
5020
|
},
|
|
4204
5021
|
outputSchema: {
|
|
4205
|
-
ok:
|
|
4206
|
-
target:
|
|
4207
|
-
compiled:
|
|
4208
|
-
code:
|
|
5022
|
+
ok: z13.boolean(),
|
|
5023
|
+
target: z13.string().optional(),
|
|
5024
|
+
compiled: z13.string().optional(),
|
|
5025
|
+
code: z13.string().optional()
|
|
4209
5026
|
},
|
|
4210
5027
|
handler: (deps, args) => {
|
|
4211
|
-
const name =
|
|
5028
|
+
const name = asString(args["flow"]) ?? DEFAULT_RECORDING;
|
|
4212
5029
|
const parsed = AnnotationSchema.safeParse(args);
|
|
4213
5030
|
if (!parsed.success) {
|
|
4214
5031
|
return Promise.resolve({ ok: false, code: AnnotationErrorCode.UNKNOWN_KIND });
|
|
@@ -4237,19 +5054,19 @@ var ANNOTATE_TOOLS = [
|
|
|
4237
5054
|
];
|
|
4238
5055
|
|
|
4239
5056
|
// ../server/dist/session/live-control-tools.js
|
|
4240
|
-
import { z as
|
|
5057
|
+
import { z as z14 } from "zod";
|
|
4241
5058
|
var sessionIdShape5 = {
|
|
4242
|
-
sessionId:
|
|
5059
|
+
sessionId: z14.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open.")
|
|
4243
5060
|
};
|
|
4244
5061
|
var LIVE_CONTROL_TOOLS = [
|
|
4245
5062
|
{
|
|
4246
5063
|
name: IrisTool.END_SESSION,
|
|
4247
5064
|
description: 'End this live testing session. Sets the server state to "ended", tells the human panel (PRESENTER), and stops driving. Optional `summary` is shown in the panel. Idempotent.',
|
|
4248
|
-
inputSchema: { summary:
|
|
4249
|
-
outputSchema: { ended:
|
|
5065
|
+
inputSchema: { summary: z14.string().optional(), ...sessionIdShape5 },
|
|
5066
|
+
outputSchema: { ended: z14.boolean(), sessionId: z14.string() },
|
|
4250
5067
|
handler: (deps, args) => {
|
|
4251
|
-
const session = deps.sessions.resolve(
|
|
4252
|
-
session.setState(SessionState.ENDED,
|
|
5068
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5069
|
+
session.setState(SessionState.ENDED, asString(args["summary"]));
|
|
4253
5070
|
return Promise.resolve({ ended: true, sessionId: session.id });
|
|
4254
5071
|
}
|
|
4255
5072
|
},
|
|
@@ -4257,9 +5074,9 @@ var LIVE_CONTROL_TOOLS = [
|
|
|
4257
5074
|
name: IrisTool.RESUME,
|
|
4258
5075
|
description: 'Clear a human pause and resume driving the page. Sets state "active" and syncs the panel (PRESENTER). Call after you have addressed the human guidance returned by a paused iris_act.',
|
|
4259
5076
|
inputSchema: { ...sessionIdShape5 },
|
|
4260
|
-
outputSchema: { ok:
|
|
5077
|
+
outputSchema: { ok: z14.boolean() },
|
|
4261
5078
|
handler: (deps, args) => {
|
|
4262
|
-
const session = deps.sessions.resolve(
|
|
5079
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4263
5080
|
session.setState(SessionState.ACTIVE);
|
|
4264
5081
|
return Promise.resolve({ ok: true });
|
|
4265
5082
|
}
|
|
@@ -4268,9 +5085,9 @@ var LIVE_CONTROL_TOOLS = [
|
|
|
4268
5085
|
name: IrisTool.MESSAGES,
|
|
4269
5086
|
description: "Drain and return any messages the human queued from the panel since the last poll. Use to explicitly check for human guidance without acting.",
|
|
4270
5087
|
inputSchema: { ...sessionIdShape5 },
|
|
4271
|
-
outputSchema: { messages:
|
|
5088
|
+
outputSchema: { messages: z14.array(z14.unknown()) },
|
|
4272
5089
|
handler: (deps, args) => {
|
|
4273
|
-
const session = deps.sessions.resolve(
|
|
5090
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4274
5091
|
return Promise.resolve({ messages: session.drainInbox() });
|
|
4275
5092
|
}
|
|
4276
5093
|
}
|
|
@@ -4296,7 +5113,7 @@ function withControl(session, result) {
|
|
|
4296
5113
|
}
|
|
4297
5114
|
|
|
4298
5115
|
// ../server/dist/update/update-tools.js
|
|
4299
|
-
import { z as
|
|
5116
|
+
import { z as z15 } from "zod";
|
|
4300
5117
|
|
|
4301
5118
|
// ../server/dist/update/update-checker.js
|
|
4302
5119
|
import * as fs from "fs";
|
|
@@ -4490,14 +5307,14 @@ var UPDATE_TOOLS = [
|
|
|
4490
5307
|
description: "Returns the running Iris version, latest available version, release changelog, and any breaking changes. Call this at the start of a session or when unexpected tool behavior suggests a version mismatch.",
|
|
4491
5308
|
inputSchema: {},
|
|
4492
5309
|
outputSchema: {
|
|
4493
|
-
currentVersion:
|
|
4494
|
-
latestVersion:
|
|
4495
|
-
updateAvailable:
|
|
4496
|
-
executionKind:
|
|
4497
|
-
changelog:
|
|
4498
|
-
breakingChanges:
|
|
4499
|
-
rollbackAvailable:
|
|
4500
|
-
previousVersion:
|
|
5310
|
+
currentVersion: z15.string().describe("The Iris server version currently running."),
|
|
5311
|
+
latestVersion: z15.string().optional().describe("Latest published version on npm."),
|
|
5312
|
+
updateAvailable: z15.boolean().describe("True when a newer version is available to install."),
|
|
5313
|
+
executionKind: z15.string().describe('How iris was launched: "npx" (no install needed \u2014 restart applies update), "global" (npm install -g), or "local" (project node_modules).'),
|
|
5314
|
+
changelog: z15.string().optional().describe("Release notes for the latest version."),
|
|
5315
|
+
breakingChanges: z15.array(z15.string()).optional().describe("Breaking changes in the latest version that may affect your scripts."),
|
|
5316
|
+
rollbackAvailable: z15.boolean().describe("True when a previous version is stored and can be restored."),
|
|
5317
|
+
previousVersion: z15.string().optional().describe("The version that would be restored on rollback.")
|
|
4501
5318
|
},
|
|
4502
5319
|
handler: async (_deps) => {
|
|
4503
5320
|
const manifest = await checkForUpdate(SERVER_VERSION);
|
|
@@ -4517,11 +5334,11 @@ var UPDATE_TOOLS = [
|
|
|
4517
5334
|
name: IrisTool.APPLY_UPDATE,
|
|
4518
5335
|
description: 'Install the latest Iris server version and restart. Strategy depends on how iris was launched (check executionKind from iris_version_info): "global" and "local" installs run npm install then exit; "npx" just exits \u2014 Claude Code restarts and npx re-resolves the latest version from npm automatically. The MCP connection briefly drops during restart.',
|
|
4519
5336
|
inputSchema: {
|
|
4520
|
-
confirm:
|
|
5337
|
+
confirm: z15.boolean().describe("Set to true to confirm the update should be applied. Required to prevent accidental upgrades.")
|
|
4521
5338
|
},
|
|
4522
5339
|
outputSchema: {
|
|
4523
|
-
ok:
|
|
4524
|
-
message:
|
|
5340
|
+
ok: z15.boolean(),
|
|
5341
|
+
message: z15.string().optional()
|
|
4525
5342
|
},
|
|
4526
5343
|
handler: async (_deps, args) => {
|
|
4527
5344
|
if (args["confirm"] !== true) {
|
|
@@ -4539,11 +5356,11 @@ var UPDATE_TOOLS = [
|
|
|
4539
5356
|
name: IrisTool.ROLLBACK,
|
|
4540
5357
|
description: "Restore the previous Iris server version and restart. Use when an update introduced a regression. The MCP connection will briefly drop \u2014 Claude Code restarts the process automatically with the restored binary.",
|
|
4541
5358
|
inputSchema: {
|
|
4542
|
-
confirm:
|
|
5359
|
+
confirm: z15.boolean().describe("Set to true to confirm the rollback. Required to prevent accidental downgrades.")
|
|
4543
5360
|
},
|
|
4544
5361
|
outputSchema: {
|
|
4545
|
-
ok:
|
|
4546
|
-
message:
|
|
5362
|
+
ok: z15.boolean(),
|
|
5363
|
+
message: z15.string().optional()
|
|
4547
5364
|
},
|
|
4548
5365
|
handler: async (_deps, args) => {
|
|
4549
5366
|
if (args["confirm"] !== true) {
|
|
@@ -4565,7 +5382,7 @@ async function snapshotTree(deps, sessionId) {
|
|
|
4565
5382
|
return { lines: normalizeLines(snap.tree ?? ""), route: snap.status?.route ?? "" };
|
|
4566
5383
|
}
|
|
4567
5384
|
var sessionIdShape6 = {
|
|
4568
|
-
sessionId:
|
|
5385
|
+
sessionId: z16.string().optional().describe("Active session ID from iris_sessions. Omit when only one browser session is open \u2014 Iris resolves it automatically.")
|
|
4569
5386
|
};
|
|
4570
5387
|
async function commandOrThrow3(deps, sessionId, name, args) {
|
|
4571
5388
|
const session = deps.sessions.resolve(sessionId);
|
|
@@ -4575,11 +5392,11 @@ async function commandOrThrow3(deps, sessionId, name, args) {
|
|
|
4575
5392
|
return result.result;
|
|
4576
5393
|
}
|
|
4577
5394
|
function asBox2(value) {
|
|
4578
|
-
const b =
|
|
4579
|
-
const x =
|
|
4580
|
-
const y =
|
|
4581
|
-
const w =
|
|
4582
|
-
const h =
|
|
5395
|
+
const b = asRecord(asRecord(value)["box"]);
|
|
5396
|
+
const x = asNumber(b["x"]);
|
|
5397
|
+
const y = asNumber(b["y"]);
|
|
5398
|
+
const w = asNumber(b["width"]);
|
|
5399
|
+
const h = asNumber(b["height"]);
|
|
4583
5400
|
if (x === void 0 || y === void 0 || w === void 0 || h === void 0)
|
|
4584
5401
|
return void 0;
|
|
4585
5402
|
if (w <= 0 || h <= 0)
|
|
@@ -4595,29 +5412,51 @@ async function tryRealInput(deps, session, ref, action, args) {
|
|
|
4595
5412
|
return synthetic();
|
|
4596
5413
|
if (!isPointerAction(action))
|
|
4597
5414
|
return synthetic(InputModeReason.NOT_POINTER);
|
|
4598
|
-
const inner =
|
|
5415
|
+
const inner = asRecord(args["args"]);
|
|
4599
5416
|
if ((action === ActionType.CLICK || action === ActionType.DBLCLICK) && inner["native"] !== true) {
|
|
4600
5417
|
return synthetic(InputModeReason.SYNTHETIC_CLICK_PREFERRED);
|
|
4601
5418
|
}
|
|
4602
5419
|
if (!await provider.isAvailableFor(session.url))
|
|
4603
5420
|
return synthetic(InputModeReason.PAGE_NOT_CORRELATED);
|
|
4604
|
-
const
|
|
5421
|
+
const inspected = await commandOrThrow3(deps, session.id, IrisCommand.INSPECT, { ref });
|
|
5422
|
+
const confirmed = inner[DANGEROUS_ACTION_CONFIRM_ARG] === true;
|
|
5423
|
+
const dangerousDescriptorText = (value2) => {
|
|
5424
|
+
const descriptor = asRecord(value2);
|
|
5425
|
+
return [
|
|
5426
|
+
asString(descriptor["name"]) ?? "",
|
|
5427
|
+
asString(descriptor["text"]) ?? "",
|
|
5428
|
+
asString(descriptor["value"]) ?? "",
|
|
5429
|
+
asString(descriptor["href"]) ?? "",
|
|
5430
|
+
asString(descriptor["formAction"]) ?? "",
|
|
5431
|
+
asString(descriptor["formText"]) ?? ""
|
|
5432
|
+
].join(" ");
|
|
5433
|
+
};
|
|
5434
|
+
if ((action === ActionType.CLICK || action === ActionType.DBLCLICK) && !confirmed && isDangerousActionText(dangerousDescriptorText(inspected))) {
|
|
5435
|
+
throw new Error(`potentially destructive native action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
5436
|
+
}
|
|
5437
|
+
const box = asBox2(inspected);
|
|
4605
5438
|
if (box === void 0)
|
|
4606
5439
|
return synthetic(InputModeReason.ELEMENT_NOT_LOCATABLE);
|
|
4607
5440
|
let toBox;
|
|
4608
5441
|
if (action === ActionType.DRAG) {
|
|
4609
|
-
const toRef =
|
|
5442
|
+
const toRef = asString(inner["toRef"]);
|
|
4610
5443
|
if (toRef === void 0)
|
|
4611
5444
|
return synthetic(InputModeReason.DRAG_TARGET_UNRESOLVED);
|
|
4612
|
-
|
|
5445
|
+
const targetInspected = await commandOrThrow3(deps, session.id, IrisCommand.INSPECT, {
|
|
5446
|
+
ref: toRef
|
|
5447
|
+
});
|
|
5448
|
+
if (!confirmed && isDangerousActionText(`${dangerousDescriptorText(inspected)} ${dangerousDescriptorText(targetInspected)}`)) {
|
|
5449
|
+
throw new Error(`potentially destructive native action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
5450
|
+
}
|
|
5451
|
+
toBox = asBox2(targetInspected);
|
|
4613
5452
|
if (toBox === void 0)
|
|
4614
5453
|
return synthetic(InputModeReason.DRAG_TARGET_UNRESOLVED);
|
|
4615
5454
|
}
|
|
4616
5455
|
const performArgs = {};
|
|
4617
|
-
const value =
|
|
5456
|
+
const value = asString(inner["value"]);
|
|
4618
5457
|
if (value !== void 0)
|
|
4619
5458
|
performArgs.value = value;
|
|
4620
|
-
const text =
|
|
5459
|
+
const text = asString(inner["text"]);
|
|
4621
5460
|
if (text !== void 0)
|
|
4622
5461
|
performArgs.text = text;
|
|
4623
5462
|
if (toBox !== void 0)
|
|
@@ -4636,23 +5475,24 @@ async function tryRealInput(deps, session, ref, action, args) {
|
|
|
4636
5475
|
};
|
|
4637
5476
|
}
|
|
4638
5477
|
}
|
|
5478
|
+
var SNAPSHOT_CACHE = new SnapshotCache();
|
|
4639
5479
|
var TOOLS = [
|
|
4640
5480
|
{
|
|
4641
5481
|
name: IrisTool.SESSIONS,
|
|
4642
5482
|
description: "List connected browser sessions (tab url/title, sessionId, last-seen, health: hidden/focused/throttled, and `realInputAvailable` \u2014 true when native CDP/launched real input is driving this tab), plus a `recommendation` pointing to `iris drive` when a tab is hidden/throttled and may be un-scriptable from here.",
|
|
4643
5483
|
inputSchema: {},
|
|
4644
5484
|
outputSchema: {
|
|
4645
|
-
sessions:
|
|
4646
|
-
sessionId:
|
|
4647
|
-
url:
|
|
4648
|
-
title:
|
|
4649
|
-
lastSeenMs:
|
|
4650
|
-
throttled:
|
|
4651
|
-
focused:
|
|
4652
|
-
hidden:
|
|
4653
|
-
realInputAvailable:
|
|
4654
|
-
stale:
|
|
4655
|
-
recommendation:
|
|
5485
|
+
sessions: z16.array(z16.object({
|
|
5486
|
+
sessionId: z16.string(),
|
|
5487
|
+
url: z16.string(),
|
|
5488
|
+
title: z16.string().optional(),
|
|
5489
|
+
lastSeenMs: z16.number(),
|
|
5490
|
+
throttled: z16.boolean(),
|
|
5491
|
+
focused: z16.boolean(),
|
|
5492
|
+
hidden: z16.boolean(),
|
|
5493
|
+
realInputAvailable: z16.boolean().optional(),
|
|
5494
|
+
stale: z16.boolean().optional(),
|
|
5495
|
+
recommendation: z16.string().optional()
|
|
4656
5496
|
})).describe("Connected browser sessions with health state.")
|
|
4657
5497
|
},
|
|
4658
5498
|
handler: async (deps) => {
|
|
@@ -4666,71 +5506,95 @@ var TOOLS = [
|
|
|
4666
5506
|
},
|
|
4667
5507
|
{
|
|
4668
5508
|
name: IrisTool.SNAPSHOT,
|
|
4669
|
-
description: "Semantic accessibility snapshot of the page or a subtree. mode: full|interactive|status. Use to see what is on screen right now.",
|
|
5509
|
+
description: "Semantic accessibility snapshot of the page or a subtree. mode: full|interactive|status. Use to see what is on screen right now. The result carries cost:{ bytes, tokens } (estimated) \u2014 if it is large, re-scope (pass `scope`) or use mode:interactive/status instead of reading the whole tree. Pass diff:true after your first snapshot to get back ONLY what changed since your last look (mode:delta with added/removed, or mode:unchanged) \u2014 far fewer tokens and no stale tree to mis-read; a route change resets it to a full snapshot automatically.",
|
|
4670
5510
|
inputSchema: {
|
|
4671
|
-
scope:
|
|
4672
|
-
mode:
|
|
5511
|
+
scope: z16.string().optional().describe("CSS selector or element ref to restrict the snapshot to a subtree. Omit to snapshot the whole page."),
|
|
5512
|
+
mode: z16.nativeEnum(SnapshotMode).optional().describe("full = all elements; interactive = only clickable/focusable elements; status = only route + title. Default: full."),
|
|
5513
|
+
diff: z16.boolean().optional().describe("Return only what changed since your last snapshot of the same scope/mode (mode:delta|unchanged). First call (or after a route change) still returns the full tree."),
|
|
4673
5514
|
...sessionIdShape6
|
|
4674
5515
|
},
|
|
4675
5516
|
outputSchema: {
|
|
4676
|
-
tree:
|
|
4677
|
-
status:
|
|
5517
|
+
tree: z16.string().optional().describe("Indented ARIA tree of every element on the page (or the scoped subtree)."),
|
|
5518
|
+
status: z16.object({ route: z16.string(), title: z16.string().optional() }).optional(),
|
|
5519
|
+
mode: z16.string().optional().describe("delta | unchanged when diff:true returned a change set."),
|
|
5520
|
+
delta: z16.object({
|
|
5521
|
+
added: z16.array(z16.string()),
|
|
5522
|
+
removed: z16.array(z16.string()),
|
|
5523
|
+
addedCount: z16.number(),
|
|
5524
|
+
removedCount: z16.number()
|
|
5525
|
+
}).optional().describe("Only present on a diff:true call that found changes."),
|
|
5526
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional().describe("Estimated size of this result \u2014 re-scope if large.")
|
|
4678
5527
|
},
|
|
4679
|
-
handler: (deps, args) =>
|
|
4680
|
-
|
|
4681
|
-
mode
|
|
4682
|
-
|
|
5528
|
+
handler: (deps, args) => {
|
|
5529
|
+
const sessionId = asString(args["sessionId"]);
|
|
5530
|
+
const mode = asString(args["mode"]) ?? SnapshotMode.FULL;
|
|
5531
|
+
return commandOrThrow3(deps, sessionId, IrisCommand.SNAPSHOT, {
|
|
5532
|
+
scope: args["scope"],
|
|
5533
|
+
mode
|
|
5534
|
+
}).then((raw) => withSizeCost(applySnapshotDelta(raw, {
|
|
5535
|
+
sessionId: sessionId ?? "default",
|
|
5536
|
+
scope: asString(args["scope"]) ?? "",
|
|
5537
|
+
mode,
|
|
5538
|
+
diff: args["diff"] === true
|
|
5539
|
+
}, SNAPSHOT_CACHE)));
|
|
5540
|
+
}
|
|
4683
5541
|
},
|
|
4684
5542
|
{
|
|
4685
5543
|
name: IrisTool.QUERY,
|
|
4686
|
-
description: "Find elements by Testing-Library semantics. Pass `by` (role|text|label|placeholder|testid|alt) and `value` (the query string). Returns matching refs + descriptors + visibility. On zero matches, also returns hint:{ route, presentTestids[], knownEmptyState } so you can distinguish an empty state from a missing element WITHOUT taking a snapshot.",
|
|
5544
|
+
description: "Find elements by Testing-Library semantics. Pass `by` (role|text|label|placeholder|testid|alt) and `value` (the query string). Returns matching refs + descriptors + visibility. Pass `limit` to cap descriptors (broad role queries can be large) or `count_only:true` for just the match count \u2014 both cut tokens. On zero matches, also returns hint:{ route, presentTestids[], knownEmptyState } so you can distinguish an empty state from a missing element WITHOUT taking a snapshot.",
|
|
4687
5545
|
inputSchema: {
|
|
4688
|
-
by:
|
|
4689
|
-
value:
|
|
4690
|
-
name:
|
|
4691
|
-
scope:
|
|
5546
|
+
by: z16.string().describe("Query strategy: role | text | label | placeholder | testid | alt"),
|
|
5547
|
+
value: z16.string().describe("Query value for the selected strategy (e.g. by=role value=button, or by=testid value=submit-btn)."),
|
|
5548
|
+
name: z16.string().optional().describe("Accessible name filter \u2014 narrows results when `by` is role and the page has many elements of that role."),
|
|
5549
|
+
scope: z16.string().optional().describe("CSS selector or element ref to restrict the search to a subtree."),
|
|
5550
|
+
limit: z16.number().optional().describe("Cap the returned descriptors to the first N (cuts tokens on broad queries). If more matched, the result carries total + truncated:true so the trim is never silent \u2014 narrow with name/scope."),
|
|
5551
|
+
count_only: z16.boolean().optional().describe('Return just { count } (no element descriptors) \u2014 use when you only need "how many match?" and not their refs.'),
|
|
4692
5552
|
...sessionIdShape6
|
|
4693
5553
|
},
|
|
4694
5554
|
outputSchema: {
|
|
4695
|
-
elements:
|
|
4696
|
-
ref:
|
|
4697
|
-
role:
|
|
4698
|
-
name:
|
|
4699
|
-
value:
|
|
4700
|
-
states:
|
|
4701
|
-
visible:
|
|
4702
|
-
})),
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
5555
|
+
elements: z16.array(z16.object({
|
|
5556
|
+
ref: z16.string(),
|
|
5557
|
+
role: z16.string(),
|
|
5558
|
+
name: z16.string(),
|
|
5559
|
+
value: z16.string().optional(),
|
|
5560
|
+
states: z16.array(z16.string()),
|
|
5561
|
+
visible: z16.boolean()
|
|
5562
|
+
})).optional(),
|
|
5563
|
+
count: z16.number().optional().describe("Match count \u2014 present when count_only is set."),
|
|
5564
|
+
total: z16.number().optional().describe("Total matches before `limit` truncation \u2014 present only when truncated."),
|
|
5565
|
+
truncated: z16.boolean().optional().describe("True when `limit` dropped some matches."),
|
|
5566
|
+
hint: z16.object({
|
|
5567
|
+
route: z16.string(),
|
|
5568
|
+
presentTestids: z16.array(z16.string()),
|
|
5569
|
+
knownEmptyState: z16.boolean()
|
|
5570
|
+
}).optional().describe("Present only on zero matches \u2014 tells you what IS on the page so you can diagnose the miss."),
|
|
5571
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional().describe("Estimated size of this result \u2014 narrow with `name`/`scope`/`limit` if large.")
|
|
4708
5572
|
},
|
|
4709
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
5573
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.QUERY, {
|
|
4710
5574
|
by: args["by"],
|
|
4711
5575
|
value: args["value"],
|
|
4712
5576
|
name: args["name"],
|
|
4713
5577
|
scope: args["scope"]
|
|
4714
|
-
})
|
|
5578
|
+
}).then((result) => withSizeCost(paginateQueryResult(result, asNumber(args["limit"]), args["count_only"] === true)))
|
|
4715
5579
|
},
|
|
4716
5580
|
{
|
|
4717
5581
|
name: IrisTool.INSPECT,
|
|
4718
5582
|
description: "Deep info on one element by ref: full a11y props, visibility, box, and (with @syrin/iris-react) component stack + source file.",
|
|
4719
5583
|
inputSchema: {
|
|
4720
|
-
ref:
|
|
5584
|
+
ref: z16.string().describe("Element ref from iris_snapshot or iris_query (e.g. 'e42')."),
|
|
4721
5585
|
...sessionIdShape6
|
|
4722
5586
|
},
|
|
4723
5587
|
outputSchema: {
|
|
4724
|
-
ref:
|
|
4725
|
-
role:
|
|
4726
|
-
name:
|
|
4727
|
-
value:
|
|
4728
|
-
states:
|
|
4729
|
-
visible:
|
|
4730
|
-
box:
|
|
4731
|
-
component:
|
|
5588
|
+
ref: z16.string(),
|
|
5589
|
+
role: z16.string(),
|
|
5590
|
+
name: z16.string(),
|
|
5591
|
+
value: z16.string().optional(),
|
|
5592
|
+
states: z16.array(z16.string()),
|
|
5593
|
+
visible: z16.boolean(),
|
|
5594
|
+
box: z16.object({ x: z16.number(), y: z16.number(), width: z16.number(), height: z16.number() }).optional(),
|
|
5595
|
+
component: z16.object({ name: z16.string().optional(), sourceFile: z16.string().optional() }).optional()
|
|
4732
5596
|
},
|
|
4733
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
5597
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.INSPECT, {
|
|
4734
5598
|
ref: args["ref"]
|
|
4735
5599
|
})
|
|
4736
5600
|
},
|
|
@@ -4738,30 +5602,30 @@ var TOOLS = [
|
|
|
4738
5602
|
name: IrisTool.ACT,
|
|
4739
5603
|
description: 'Execute one action against a ref: click|dblclick|hover|focus|fill|type|clear|select|check|uncheck|submit|press|scrollIntoView. Returns immediately with a `since` cursor \u2014 observe the reaction with iris_observe. Carries effect:{dispatched,targetMatched,visible,enabled,focusMoved,valueChanged,domMutatedWithin,occluded,occludedBy,scrolledIntoView} to tell "action missed" from "app didn\'t react"; dispatched=landed, settled=a real frame flushed, and a settle timeout never fails the tool. occluded=true means the click point is covered by another element (a real user could not click it) \u2014 synthetic dispatch still delivered the event; scrolledIntoView=true means an off-viewport target was scrolled in first. inputMode is "real" (native CDP, no synthetic effect block) or "synthetic"; clicks default to the occlusion-honest synthetic path even when CDP is configured \u2014 pass args.native:true to force a trusted native click (file pickers, clipboard). inputModeReason explains any real\u2192synthetic choice so it is never silent. Full model (real-input, throttled tabs, `iris drive`): docs/usage.md \xA718.',
|
|
4740
5604
|
inputSchema: {
|
|
4741
|
-
ref:
|
|
4742
|
-
action:
|
|
4743
|
-
args:
|
|
4744
|
-
refuseWhenThrottled:
|
|
5605
|
+
ref: z16.string().describe("Element ref from iris_snapshot or iris_query (e.g. 'e42')."),
|
|
5606
|
+
action: z16.string().describe("Action to perform: click | dblclick | hover | focus | fill | type | clear | select | check | uncheck | submit | press | scrollIntoView"),
|
|
5607
|
+
args: z16.record(z16.unknown()).optional().describe("Action-specific arguments: { value } for fill/select, { text } for type/press, { native: true } to force a trusted native click, { confirmDangerous: true } to allow a potentially destructive control."),
|
|
5608
|
+
refuseWhenThrottled: z16.boolean().optional().describe("Throw instead of silently sending synthetic events when the tab is throttled/backgrounded. Default: false (synthetic events are still sent)."),
|
|
4745
5609
|
...sessionIdShape6
|
|
4746
5610
|
},
|
|
4747
5611
|
outputSchema: {
|
|
4748
|
-
since:
|
|
4749
|
-
dispatched:
|
|
4750
|
-
settled:
|
|
4751
|
-
inputMode:
|
|
4752
|
-
result:
|
|
4753
|
-
session:
|
|
5612
|
+
since: z16.number().describe("Cursor \u2014 pass to iris_observe/iris_wait_for/iris_assert to scope reaction queries to this act."),
|
|
5613
|
+
dispatched: z16.boolean(),
|
|
5614
|
+
settled: z16.boolean().nullable(),
|
|
5615
|
+
inputMode: z16.string(),
|
|
5616
|
+
result: z16.unknown().optional(),
|
|
5617
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4754
5618
|
},
|
|
4755
5619
|
handler: async (deps, args) => {
|
|
4756
|
-
const session = deps.sessions.resolve(
|
|
5620
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4757
5621
|
const paused = pausedShortCircuit(session);
|
|
4758
5622
|
if (paused !== void 0)
|
|
4759
5623
|
return paused;
|
|
4760
5624
|
refuseIfThrottled(session, args["refuseWhenThrottled"]);
|
|
4761
5625
|
const since = session.elapsed();
|
|
4762
5626
|
session.markActCursor(since);
|
|
4763
|
-
const ref =
|
|
4764
|
-
const action =
|
|
5627
|
+
const ref = asString(args["ref"]) ?? "";
|
|
5628
|
+
const action = asString(args["action"]) ?? "";
|
|
4765
5629
|
const real = await tryRealInput(deps, session, ref, action, args);
|
|
4766
5630
|
if (real.result !== void 0) {
|
|
4767
5631
|
if (deps.recordings.active().length > 0) {
|
|
@@ -4787,7 +5651,7 @@ var TOOLS = [
|
|
|
4787
5651
|
if (deps.recordings.active().length > 0) {
|
|
4788
5652
|
deps.recordings.capture(compileActStep(args, result.result));
|
|
4789
5653
|
}
|
|
4790
|
-
const r =
|
|
5654
|
+
const r = asRecord(result.result);
|
|
4791
5655
|
return withControl(session, {
|
|
4792
5656
|
since,
|
|
4793
5657
|
inputMode: InputMode.SYNTHETIC,
|
|
@@ -4806,17 +5670,17 @@ var TOOLS = [
|
|
|
4806
5670
|
name: IrisTool.ACT_SEQUENCE,
|
|
4807
5671
|
description: "Run multiple actions in order (fill -> fill -> submit) in one round-trip. Returns per-step effects[] (see iris_act).",
|
|
4808
5672
|
inputSchema: {
|
|
4809
|
-
steps:
|
|
5673
|
+
steps: z16.array(z16.record(z16.unknown())).describe("Ordered list of { ref, action, args? } objects. Each step is equivalent to one iris_act call; put confirmDangerous:true in a destructive step args object."),
|
|
4810
5674
|
...sessionIdShape6
|
|
4811
5675
|
},
|
|
4812
5676
|
outputSchema: {
|
|
4813
|
-
since:
|
|
4814
|
-
dispatched:
|
|
4815
|
-
result:
|
|
4816
|
-
session:
|
|
5677
|
+
since: z16.number(),
|
|
5678
|
+
dispatched: z16.boolean(),
|
|
5679
|
+
result: z16.unknown().optional(),
|
|
5680
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4817
5681
|
},
|
|
4818
5682
|
handler: async (deps, args) => {
|
|
4819
|
-
const session = deps.sessions.resolve(
|
|
5683
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4820
5684
|
const paused = pausedShortCircuit(session);
|
|
4821
5685
|
if (paused !== void 0)
|
|
4822
5686
|
return paused;
|
|
@@ -4828,7 +5692,7 @@ var TOOLS = [
|
|
|
4828
5692
|
if (deps.recordings.active().length > 0) {
|
|
4829
5693
|
deps.recordings.capture(compileSequenceStep(args, result.result));
|
|
4830
5694
|
}
|
|
4831
|
-
const r =
|
|
5695
|
+
const r = asRecord(result.result);
|
|
4832
5696
|
return withControl(session, {
|
|
4833
5697
|
since,
|
|
4834
5698
|
dispatched: r["count"] !== void 0,
|
|
@@ -4839,34 +5703,34 @@ var TOOLS = [
|
|
|
4839
5703
|
},
|
|
4840
5704
|
{
|
|
4841
5705
|
name: IrisTool.ACT_AND_WAIT,
|
|
4842
|
-
description: "Act on a ref, then wait for a predicate to hold \u2014 one hop for the act->observe->assert loop. Returns { effect } (the action result), { verdict } (predicate pass/evidence/near-miss), and { trace } (the reaction report of everything the app did after the action). timeout_ms 0 evaluates the predicate once without waiting.",
|
|
5706
|
+
description: "Act on a ref, then wait for a predicate to hold \u2014 one hop for the act->observe->assert loop. Omit `until` to wait for the page to settle (network + DOM idle) \u2014 use this instead of a fixed sleep. Returns { effect } (the action result), { verdict } (predicate pass/evidence/near-miss), and { trace } (the reaction report of everything the app did after the action). timeout_ms 0 evaluates the predicate once without waiting.",
|
|
4843
5707
|
inputSchema: {
|
|
4844
|
-
ref:
|
|
4845
|
-
action:
|
|
4846
|
-
args:
|
|
4847
|
-
until: PredicateSchema.describe(
|
|
4848
|
-
timeout_ms:
|
|
4849
|
-
refuseWhenThrottled:
|
|
5708
|
+
ref: z16.string().describe("Element ref from iris_snapshot or iris_query."),
|
|
5709
|
+
action: z16.string().describe("Action to perform: click | dblclick | hover | focus | fill | type | clear | select | check | uncheck | submit | press | scrollIntoView"),
|
|
5710
|
+
args: z16.record(z16.unknown()).optional().describe("Action-specific arguments: { value } for fill/select, { text } for type/press, { confirmDangerous: true } for a potentially destructive control."),
|
|
5711
|
+
until: PredicateSchema.optional().describe('Predicate to wait for after the action completes (same shape as iris_assert). OMIT to wait for the page to SETTLE \u2014 network + DOM idle \u2014 the deterministic default instead of a sleep. To assert a consequence AND settle, allOf them: { kind: "allOf", predicates: [<your predicate>, { kind: "settled" }] }.'),
|
|
5712
|
+
timeout_ms: z16.number().optional().describe("Maximum wait time in milliseconds. 0 = evaluate once without waiting. Default: 4000."),
|
|
5713
|
+
refuseWhenThrottled: z16.boolean().optional().describe("Throw if the tab is throttled. Default: false."),
|
|
4850
5714
|
...sessionIdShape6
|
|
4851
5715
|
},
|
|
4852
5716
|
outputSchema: {
|
|
4853
|
-
effect:
|
|
4854
|
-
verdict:
|
|
4855
|
-
pass:
|
|
4856
|
-
evidence:
|
|
4857
|
-
failureReason:
|
|
5717
|
+
effect: z16.unknown().describe("The iris_act result (dispatched, settled, inputMode, etc.)."),
|
|
5718
|
+
verdict: z16.object({
|
|
5719
|
+
pass: z16.boolean(),
|
|
5720
|
+
evidence: z16.unknown().optional(),
|
|
5721
|
+
failureReason: z16.string().optional()
|
|
4858
5722
|
}),
|
|
4859
|
-
trace:
|
|
4860
|
-
session:
|
|
5723
|
+
trace: z16.unknown().describe("Reaction report (same shape as iris_observe summary)."),
|
|
5724
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4861
5725
|
},
|
|
4862
5726
|
handler: async (deps, args) => {
|
|
4863
|
-
const session = deps.sessions.resolve(
|
|
5727
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4864
5728
|
const paused = pausedShortCircuit(session);
|
|
4865
5729
|
if (paused !== void 0)
|
|
4866
5730
|
return paused;
|
|
4867
5731
|
refuseIfThrottled(session, args["refuseWhenThrottled"]);
|
|
4868
|
-
const until = PredicateSchema.parse(args["until"]);
|
|
4869
|
-
const timeout2 =
|
|
5732
|
+
const until = args["until"] !== void 0 ? PredicateSchema.parse(args["until"]) : { kind: "settled" };
|
|
5733
|
+
const timeout2 = asNumber(args["timeout_ms"]) ?? 4e3;
|
|
4870
5734
|
const since = session.elapsed();
|
|
4871
5735
|
session.markActCursor(since);
|
|
4872
5736
|
const actResult = await session.command(IrisCommand.ACT, {
|
|
@@ -4890,40 +5754,41 @@ var TOOLS = [
|
|
|
4890
5754
|
name: IrisTool.OBSERVE,
|
|
4891
5755
|
description: "Return the timeline of everything the app did in a window (DOM/network/route/console/animation/signal), with a summary. Use after an action. Pass `max_events` to cap the timeline to the most recent N (older events are dropped and counted in cost.droppedOldest). Every result carries a `cost:{events,bytes}` hint so you can self-budget your next call.",
|
|
4892
5756
|
inputSchema: {
|
|
4893
|
-
window_ms:
|
|
4894
|
-
since:
|
|
4895
|
-
filters:
|
|
4896
|
-
max_events:
|
|
5757
|
+
window_ms: z16.number().optional().describe("Time window to look back. Default: 2000ms. Ignored when `since` is provided."),
|
|
5758
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act or iris_observe call. Scopes the event window to exactly that span."),
|
|
5759
|
+
filters: z16.array(z16.string()).optional().describe("Event type allowlist: dom | net | route | console | animation | signal. Omit to return all types."),
|
|
5760
|
+
max_events: z16.number().optional().describe("Cap the timeline to the most recent N events. Older events are counted in cost.droppedOldest."),
|
|
4897
5761
|
...sessionIdShape6
|
|
4898
5762
|
},
|
|
4899
5763
|
outputSchema: {
|
|
4900
|
-
events:
|
|
4901
|
-
summary:
|
|
4902
|
-
total:
|
|
4903
|
-
network:
|
|
4904
|
-
domAdded:
|
|
4905
|
-
domRemoved:
|
|
4906
|
-
domChanged:
|
|
4907
|
-
routeChanges:
|
|
4908
|
-
consoleErrors:
|
|
4909
|
-
animations:
|
|
4910
|
-
signals:
|
|
5764
|
+
events: z16.array(z16.unknown()),
|
|
5765
|
+
summary: z16.object({
|
|
5766
|
+
total: z16.number(),
|
|
5767
|
+
network: z16.number(),
|
|
5768
|
+
domAdded: z16.number(),
|
|
5769
|
+
domRemoved: z16.number(),
|
|
5770
|
+
domChanged: z16.number(),
|
|
5771
|
+
routeChanges: z16.number(),
|
|
5772
|
+
consoleErrors: z16.number(),
|
|
5773
|
+
animations: z16.number(),
|
|
5774
|
+
signals: z16.number()
|
|
4911
5775
|
}),
|
|
4912
|
-
cost:
|
|
4913
|
-
events:
|
|
4914
|
-
bytes:
|
|
4915
|
-
droppedOldest:
|
|
5776
|
+
cost: z16.object({
|
|
5777
|
+
events: z16.number(),
|
|
5778
|
+
bytes: z16.number(),
|
|
5779
|
+
droppedOldest: z16.number().optional(),
|
|
5780
|
+
recommendation: z16.string().optional().describe("Present when the timeline is large \u2014 scope your next call (filters/max_events).")
|
|
4916
5781
|
}),
|
|
4917
|
-
session:
|
|
5782
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4918
5783
|
},
|
|
4919
5784
|
handler: (deps, args) => {
|
|
4920
|
-
const session = deps.sessions.resolve(
|
|
4921
|
-
const since =
|
|
4922
|
-
const windowMs =
|
|
5785
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5786
|
+
const since = asNumber(args["since"]);
|
|
5787
|
+
const windowMs = asNumber(args["window_ms"]) ?? 2e3;
|
|
4923
5788
|
const events = since !== void 0 ? session.eventsSince(since) : session.eventsInWindow(windowMs);
|
|
4924
5789
|
const filters = Array.isArray(args["filters"]) ? args["filters"] : void 0;
|
|
4925
5790
|
const filtered = filters === void 0 ? events : events.filter((e) => filters.includes(e.type));
|
|
4926
|
-
const { events: budgeted, droppedOldest } = applyEventBudget(filtered,
|
|
5791
|
+
const { events: budgeted, droppedOldest } = applyEventBudget(filtered, asNumber(args["max_events"]));
|
|
4927
5792
|
const report = buildReactionReport(budgeted, windowMs);
|
|
4928
5793
|
return Promise.resolve(withControl(session, {
|
|
4929
5794
|
...report,
|
|
@@ -4936,99 +5801,113 @@ var TOOLS = [
|
|
|
4936
5801
|
name: IrisTool.WAIT_FOR,
|
|
4937
5802
|
description: "Block until a predicate is satisfied (or already true in the recent buffer), else time out. Returns matching evidence or a near-miss diagnosis. By default it only counts events since your last act, so a signal buffered BEFORE the action can never fake a pass; pass `since` (an observe/act cursor) to widen or narrow that window explicitly.",
|
|
4938
5803
|
inputSchema: {
|
|
4939
|
-
predicate: PredicateSchema.describe(
|
|
4940
|
-
timeout_ms:
|
|
4941
|
-
since:
|
|
5804
|
+
predicate: PredicateSchema.describe('Predicate to wait for: { signal }, { net }, { element }, { kind: "settled", quietMs } (deterministic network + DOM idle \u2014 prefer this over a fixed sleep), or a combination via allOf/anyOf.'),
|
|
5805
|
+
timeout_ms: z16.number().optional().describe("Maximum wait in milliseconds. Default: 4000."),
|
|
5806
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the wait to events after that act."),
|
|
4942
5807
|
...sessionIdShape6
|
|
4943
5808
|
},
|
|
4944
5809
|
outputSchema: {
|
|
4945
|
-
pass:
|
|
4946
|
-
evidence:
|
|
4947
|
-
failureReason:
|
|
4948
|
-
session:
|
|
5810
|
+
pass: z16.boolean(),
|
|
5811
|
+
evidence: z16.unknown().optional(),
|
|
5812
|
+
failureReason: z16.string().optional(),
|
|
5813
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4949
5814
|
},
|
|
4950
5815
|
handler: async (deps, args) => {
|
|
4951
|
-
const session = deps.sessions.resolve(
|
|
5816
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4952
5817
|
const predicate = PredicateSchema.parse(args["predicate"]);
|
|
4953
|
-
const since =
|
|
4954
|
-
const verdict = await waitForPredicate(session, predicate,
|
|
5818
|
+
const since = asNumber(args["since"]) ?? session.lastActCursor() ?? 0;
|
|
5819
|
+
const verdict = await waitForPredicate(session, predicate, asNumber(args["timeout_ms"]) ?? 4e3, since);
|
|
4955
5820
|
return withControl(session, { ...verdict, ...healthEnvelope(session) });
|
|
4956
5821
|
}
|
|
4957
5822
|
},
|
|
4958
5823
|
{
|
|
4959
5824
|
name: IrisTool.ASSERT,
|
|
4960
|
-
description: "Evaluate a predicate (optionally waiting up to timeout_ms). Returns { pass, evidence, failureReason? }. The end of every verify loop. By default it only counts events since your last act, so a stale buffered signal can never fake a pass; pass `since` (an observe/act cursor) to set the window explicitly.",
|
|
5825
|
+
description: "Evaluate a predicate (optionally waiting up to timeout_ms). Returns { pass, evidence, failureReason? }. The end of every verify loop. Prefer a { signal } or { net } consequence over { element }/{ text } presence \u2014 a passing presence-only assertion returns `advice` because a wrong/healed element can fake it. By default it only counts events since your last act, so a stale buffered signal can never fake a pass; pass `since` (an observe/act cursor) to set the window explicitly.",
|
|
4961
5826
|
inputSchema: {
|
|
4962
5827
|
predicate: PredicateSchema.describe("Predicate to evaluate: { signal }, { net }, { element } or a combination."),
|
|
4963
|
-
timeout_ms:
|
|
4964
|
-
since:
|
|
5828
|
+
timeout_ms: z16.number().optional().describe("If > 0, wait up to this many milliseconds before failing. Default: 0 (evaluate once)."),
|
|
5829
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the assertion to events after that act."),
|
|
4965
5830
|
...sessionIdShape6
|
|
4966
5831
|
},
|
|
4967
5832
|
outputSchema: {
|
|
4968
|
-
pass:
|
|
4969
|
-
evidence:
|
|
4970
|
-
failureReason:
|
|
4971
|
-
|
|
5833
|
+
pass: z16.boolean(),
|
|
5834
|
+
evidence: z16.unknown().optional(),
|
|
5835
|
+
failureReason: z16.string().optional(),
|
|
5836
|
+
advice: z16.string().optional().describe("Present on a PASSING presence-only assertion \u2014 nudges toward a consequence."),
|
|
5837
|
+
session: z16.object({ lastSeenMs: z16.number(), throttled: z16.boolean(), focused: z16.boolean() }).optional()
|
|
4972
5838
|
},
|
|
4973
5839
|
handler: async (deps, args) => {
|
|
4974
|
-
const session = deps.sessions.resolve(
|
|
5840
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
4975
5841
|
const predicate = PredicateSchema.parse(args["predicate"]);
|
|
4976
|
-
const timeout2 =
|
|
4977
|
-
const since =
|
|
5842
|
+
const timeout2 = asNumber(args["timeout_ms"]) ?? 0;
|
|
5843
|
+
const since = asNumber(args["since"]) ?? session.lastActCursor() ?? 0;
|
|
4978
5844
|
const verdict = timeout2 > 0 ? await waitForPredicate(session, predicate, timeout2, since) : await evaluatePredicate(session, predicate, since);
|
|
4979
|
-
|
|
5845
|
+
const advice = verdict.pass && isPresenceOnlyAssertion(predicate) ? { advice: PRESENCE_ONLY_ADVICE } : {};
|
|
5846
|
+
return withControl(session, { ...verdict, ...advice, ...healthEnvelope(session) });
|
|
4980
5847
|
}
|
|
4981
5848
|
},
|
|
4982
5849
|
{
|
|
4983
5850
|
name: IrisTool.NETWORK,
|
|
4984
5851
|
description: 'Filtered list of network calls. Fast path for "did POST /x return 200?". A zero-match filter returns a `hint` { totalInWindow, present[] } of the calls that DID fire, so a miss is diagnosable.',
|
|
4985
5852
|
inputSchema: {
|
|
4986
|
-
since:
|
|
4987
|
-
method:
|
|
4988
|
-
urlContains:
|
|
4989
|
-
status:
|
|
5853
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the query to requests fired after that act."),
|
|
5854
|
+
method: z16.string().optional().describe("HTTP method filter: GET | POST | PUT | DELETE | PATCH etc."),
|
|
5855
|
+
urlContains: z16.string().optional().describe("Substring that the request URL must contain."),
|
|
5856
|
+
status: z16.number().optional().describe("HTTP status code filter (e.g. 200, 404, 500)."),
|
|
5857
|
+
limit: z16.number().optional().describe("Keep only the most recent N matching calls (older are dropped and counted in droppedOldest) \u2014 cuts tokens on a wide window."),
|
|
4990
5858
|
...sessionIdShape6
|
|
4991
5859
|
},
|
|
4992
5860
|
outputSchema: {
|
|
4993
|
-
calls:
|
|
4994
|
-
|
|
5861
|
+
calls: z16.array(z16.unknown()),
|
|
5862
|
+
total: z16.number().optional().describe("Total matches before `limit` \u2014 present only when capped."),
|
|
5863
|
+
droppedOldest: z16.number().optional().describe("How many older matches `limit` dropped."),
|
|
5864
|
+
hint: z16.object({ totalInWindow: z16.number(), present: z16.array(z16.string()) }).optional(),
|
|
5865
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional()
|
|
4995
5866
|
},
|
|
4996
5867
|
handler: (deps, args) => {
|
|
4997
|
-
const session = deps.sessions.resolve(
|
|
4998
|
-
const since =
|
|
4999
|
-
const method =
|
|
5000
|
-
const urlContains =
|
|
5001
|
-
const status =
|
|
5868
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5869
|
+
const since = asNumber(args["since"]) ?? 0;
|
|
5870
|
+
const method = asString(args["method"]);
|
|
5871
|
+
const urlContains = asString(args["urlContains"]);
|
|
5872
|
+
const status = asNumber(args["status"]);
|
|
5873
|
+
const limit = asNumber(args["limit"]);
|
|
5002
5874
|
const allNet = session.eventsSince(since).filter((e) => e.type === EventType.NET_REQUEST);
|
|
5003
|
-
const
|
|
5004
|
-
if (
|
|
5005
|
-
return Promise.resolve({ calls, hint: netEmptyHint(allNet) });
|
|
5875
|
+
const matched = allNet.filter((e) => matchNet(e, method, urlContains, status));
|
|
5876
|
+
if (matched.length === 0 && allNet.length > 0) {
|
|
5877
|
+
return Promise.resolve(withSizeCost({ calls: matched, hint: netEmptyHint(allNet) }));
|
|
5006
5878
|
}
|
|
5007
|
-
|
|
5879
|
+
const { events: calls, droppedOldest } = applyEventBudget(matched, limit);
|
|
5880
|
+
return Promise.resolve(withSizeCost(droppedOldest > 0 ? { calls, total: matched.length, droppedOldest } : { calls }));
|
|
5008
5881
|
}
|
|
5009
5882
|
},
|
|
5010
5883
|
{
|
|
5011
5884
|
name: IrisTool.CONSOLE,
|
|
5012
5885
|
description: 'Console/error log. Fast path for "were there any errors during this flow?". When a level filter matches nothing, returns a `hint` { totalInWindow, byLevel } so 0 errors is distinguishable from a silent page.',
|
|
5013
5886
|
inputSchema: {
|
|
5014
|
-
level:
|
|
5015
|
-
since:
|
|
5887
|
+
level: z16.string().optional().describe("Log level filter: error | warn | info | log. Omit to return all levels."),
|
|
5888
|
+
since: z16.number().optional().describe("Cursor from a prior iris_act \u2014 scopes the query to log entries after that act."),
|
|
5889
|
+
limit: z16.number().optional().describe("Keep only the most recent N matching entries (older are dropped and counted in droppedOldest) \u2014 cuts tokens when a page spams the console."),
|
|
5016
5890
|
...sessionIdShape6
|
|
5017
5891
|
},
|
|
5018
5892
|
outputSchema: {
|
|
5019
|
-
logs:
|
|
5020
|
-
|
|
5893
|
+
logs: z16.array(z16.unknown()),
|
|
5894
|
+
total: z16.number().optional().describe("Total matches before `limit` \u2014 present only when capped."),
|
|
5895
|
+
droppedOldest: z16.number().optional().describe("How many older matches `limit` dropped."),
|
|
5896
|
+
hint: z16.object({ totalInWindow: z16.number(), byLevel: z16.record(z16.number()) }).optional(),
|
|
5897
|
+
cost: z16.object({ bytes: z16.number(), tokens: z16.number() }).optional()
|
|
5021
5898
|
},
|
|
5022
5899
|
handler: (deps, args) => {
|
|
5023
|
-
const session = deps.sessions.resolve(
|
|
5024
|
-
const since =
|
|
5025
|
-
const level =
|
|
5900
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5901
|
+
const since = asNumber(args["since"]) ?? 0;
|
|
5902
|
+
const level = asString(args["level"]);
|
|
5903
|
+
const limit = asNumber(args["limit"]);
|
|
5026
5904
|
const allConsole = session.eventsSince(since).filter(isConsoleEvent);
|
|
5027
|
-
const
|
|
5028
|
-
if (
|
|
5029
|
-
return Promise.resolve({ logs, hint: consoleEmptyHint(allConsole) });
|
|
5905
|
+
const matched = allConsole.filter((e) => matchConsole(e, level));
|
|
5906
|
+
if (matched.length === 0 && allConsole.length > 0) {
|
|
5907
|
+
return Promise.resolve(withSizeCost({ logs: matched, hint: consoleEmptyHint(allConsole) }));
|
|
5030
5908
|
}
|
|
5031
|
-
|
|
5909
|
+
const { events: logs, droppedOldest } = applyEventBudget(matched, limit);
|
|
5910
|
+
return Promise.resolve(withSizeCost(droppedOldest > 0 ? { logs, total: matched.length, droppedOldest } : { logs }));
|
|
5032
5911
|
}
|
|
5033
5912
|
},
|
|
5034
5913
|
{
|
|
@@ -5036,24 +5915,24 @@ var TOOLS = [
|
|
|
5036
5915
|
description: "Currently running + recently completed animations with targets/timing.",
|
|
5037
5916
|
inputSchema: { ...sessionIdShape6 },
|
|
5038
5917
|
outputSchema: {
|
|
5039
|
-
animations:
|
|
5918
|
+
animations: z16.array(z16.unknown())
|
|
5040
5919
|
},
|
|
5041
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
5920
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.ANIMATIONS, {})
|
|
5042
5921
|
},
|
|
5043
5922
|
{
|
|
5044
5923
|
name: IrisTool.BASELINE_SAVE,
|
|
5045
5924
|
description: "Snapshot the current semantic state under a name, to diff against later (regression detection).",
|
|
5046
5925
|
inputSchema: {
|
|
5047
|
-
name:
|
|
5926
|
+
name: z16.string().describe('Label for this baseline snapshot (e.g. "dashboard-initial"). Use the same name in iris_diff to compare.'),
|
|
5048
5927
|
...sessionIdShape6
|
|
5049
5928
|
},
|
|
5050
5929
|
outputSchema: {
|
|
5051
|
-
baseline:
|
|
5052
|
-
lineCount:
|
|
5930
|
+
baseline: z16.string().describe("Saved baseline name \u2014 pass to iris_diff to compare."),
|
|
5931
|
+
lineCount: z16.number()
|
|
5053
5932
|
},
|
|
5054
5933
|
handler: async (deps, args) => {
|
|
5055
|
-
const name =
|
|
5056
|
-
const { lines, route } = await snapshotTree(deps,
|
|
5934
|
+
const name = asString(args["name"]) ?? "default";
|
|
5935
|
+
const { lines, route } = await snapshotTree(deps, asString(args["sessionId"]));
|
|
5057
5936
|
deps.baselines.save({ name, lines, route });
|
|
5058
5937
|
return { baseline: name, lineCount: lines.length };
|
|
5059
5938
|
}
|
|
@@ -5063,7 +5942,7 @@ var TOOLS = [
|
|
|
5063
5942
|
description: "List saved baseline names.",
|
|
5064
5943
|
inputSchema: {},
|
|
5065
5944
|
outputSchema: {
|
|
5066
|
-
baselines:
|
|
5945
|
+
baselines: z16.array(z16.string())
|
|
5067
5946
|
},
|
|
5068
5947
|
handler: (deps) => Promise.resolve({ baselines: deps.baselines.list() })
|
|
5069
5948
|
},
|
|
@@ -5071,23 +5950,23 @@ var TOOLS = [
|
|
|
5071
5950
|
name: IrisTool.DIFF,
|
|
5072
5951
|
description: 'Diff current semantic state vs a saved baseline: REMOVED/ADDED elements + console-error count. Call iris_baseline_list to list saved baselines, iris_baseline_save to create one. Pass `baseline` (name from iris_baseline_list). Answers "did anything silently go missing/break?".',
|
|
5073
5952
|
inputSchema: {
|
|
5074
|
-
baseline:
|
|
5953
|
+
baseline: z16.string().describe("Baseline name to compare against. Call iris_baseline_list to get available names; names are created by iris_baseline_save."),
|
|
5075
5954
|
...sessionIdShape6
|
|
5076
5955
|
},
|
|
5077
5956
|
outputSchema: {
|
|
5078
|
-
baseline:
|
|
5079
|
-
removed:
|
|
5080
|
-
added:
|
|
5081
|
-
consoleErrors:
|
|
5082
|
-
routeChanged:
|
|
5957
|
+
baseline: z16.string(),
|
|
5958
|
+
removed: z16.array(z16.string()),
|
|
5959
|
+
added: z16.array(z16.string()),
|
|
5960
|
+
consoleErrors: z16.number(),
|
|
5961
|
+
routeChanged: z16.boolean()
|
|
5083
5962
|
},
|
|
5084
5963
|
handler: async (deps, args) => {
|
|
5085
|
-
const name =
|
|
5964
|
+
const name = asString(args["baseline"]) ?? "default";
|
|
5086
5965
|
const base = deps.baselines.get(name);
|
|
5087
5966
|
if (base === void 0)
|
|
5088
5967
|
throw new Error(`no baseline named '${name}'`);
|
|
5089
|
-
const session = deps.sessions.resolve(
|
|
5090
|
-
const { lines, route } = await snapshotTree(deps,
|
|
5968
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5969
|
+
const { lines, route } = await snapshotTree(deps, asString(args["sessionId"]));
|
|
5091
5970
|
const { removed, added } = diffLines(base.lines, lines);
|
|
5092
5971
|
const consoleErrors = session.eventsSince(0).filter((e) => e.type === EventType.CONSOLE_ERROR || e.type === EventType.ERROR_UNCAUGHT).length;
|
|
5093
5972
|
return { baseline: name, removed, added, consoleErrors, routeChanged: base.route !== route };
|
|
@@ -5097,16 +5976,16 @@ var TOOLS = [
|
|
|
5097
5976
|
name: IrisTool.RECORD_START,
|
|
5098
5977
|
description: "Start recording the event timeline under a name (for replay / a flow report).",
|
|
5099
5978
|
inputSchema: {
|
|
5100
|
-
recordingName:
|
|
5979
|
+
recordingName: z16.string().describe("Identifier for this recording. Pass the same name to iris_record_stop and iris_replay."),
|
|
5101
5980
|
...sessionIdShape6
|
|
5102
5981
|
},
|
|
5103
5982
|
outputSchema: {
|
|
5104
|
-
recordingName:
|
|
5105
|
-
since:
|
|
5983
|
+
recordingName: z16.string(),
|
|
5984
|
+
since: z16.number()
|
|
5106
5985
|
},
|
|
5107
5986
|
handler: (deps, args) => {
|
|
5108
|
-
const session = deps.sessions.resolve(
|
|
5109
|
-
const name =
|
|
5987
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5988
|
+
const name = asString(args["recordingName"]) ?? "default";
|
|
5110
5989
|
const cursor = session.elapsed();
|
|
5111
5990
|
deps.recordings.start(name, cursor);
|
|
5112
5991
|
return Promise.resolve({ recordingName: name, since: cursor });
|
|
@@ -5116,17 +5995,17 @@ var TOOLS = [
|
|
|
5116
5995
|
name: IrisTool.RECORD_STOP,
|
|
5117
5996
|
description: "Stop the recording identified by `recordingName` and return both the reaction report for the span and a compiled, replayable { program: { version, steps:[{tool,args,stable}] } } of the agent acts captured during it.",
|
|
5118
5997
|
inputSchema: {
|
|
5119
|
-
recordingName:
|
|
5998
|
+
recordingName: z16.string().describe("Identifier of an active recording started with iris_record_start."),
|
|
5120
5999
|
...sessionIdShape6
|
|
5121
6000
|
},
|
|
5122
6001
|
outputSchema: {
|
|
5123
|
-
recordingName:
|
|
5124
|
-
program:
|
|
5125
|
-
warning:
|
|
6002
|
+
recordingName: z16.string(),
|
|
6003
|
+
program: z16.unknown(),
|
|
6004
|
+
warning: z16.string().optional()
|
|
5126
6005
|
},
|
|
5127
6006
|
handler: (deps, args) => {
|
|
5128
|
-
const session = deps.sessions.resolve(
|
|
5129
|
-
const name =
|
|
6007
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
6008
|
+
const name = asString(args["recordingName"]) ?? "default";
|
|
5130
6009
|
const rec = deps.recordings.stop(name);
|
|
5131
6010
|
if (rec === void 0)
|
|
5132
6011
|
throw new Error(`no active recording named '${name}'`);
|
|
@@ -5152,29 +6031,30 @@ var TOOLS = [
|
|
|
5152
6031
|
},
|
|
5153
6032
|
{
|
|
5154
6033
|
name: IrisTool.REPLAY,
|
|
5155
|
-
description: "Re-execute a previously recorded program by recordingName. Re-resolves each step to its element by testid (falling back to the stored ref for unstable steps) and runs the actions in order against the live session. Stops at the first failure. Returns { ok, steps:[{tool,ok,error?,note?}] }.",
|
|
6034
|
+
description: "Re-execute a previously recorded program by recordingName. Re-resolves each step to its element by testid (falling back to the stored ref for unstable steps) and runs the actions in order against the live session. Stops at the first failure. Destructive controls require confirmDangerous:true on every replay; confirmation is never persisted. Returns { ok, steps:[{tool,ok,error?,note?}] }.",
|
|
5156
6035
|
inputSchema: {
|
|
5157
|
-
recordingName:
|
|
6036
|
+
recordingName: z16.string().describe("Name of a compiled recording (from iris_record_stop) to re-execute."),
|
|
6037
|
+
confirmDangerous: z16.boolean().optional().describe("Set true to allow destructive controls during this replay only."),
|
|
5158
6038
|
...sessionIdShape6
|
|
5159
6039
|
},
|
|
5160
6040
|
outputSchema: {
|
|
5161
|
-
recordingName:
|
|
5162
|
-
ok:
|
|
5163
|
-
steps:
|
|
5164
|
-
tool:
|
|
5165
|
-
ok:
|
|
5166
|
-
error:
|
|
5167
|
-
note:
|
|
6041
|
+
recordingName: z16.string(),
|
|
6042
|
+
ok: z16.boolean(),
|
|
6043
|
+
steps: z16.array(z16.object({
|
|
6044
|
+
tool: z16.string(),
|
|
6045
|
+
ok: z16.boolean(),
|
|
6046
|
+
error: z16.string().optional(),
|
|
6047
|
+
note: z16.string().optional()
|
|
5168
6048
|
}))
|
|
5169
6049
|
},
|
|
5170
6050
|
handler: async (deps, args) => {
|
|
5171
|
-
const name =
|
|
6051
|
+
const name = asString(args["recordingName"]) ?? "default";
|
|
5172
6052
|
const program = deps.recordings.getCompiled(name);
|
|
5173
6053
|
if (program === void 0)
|
|
5174
6054
|
throw new Error(`no compiled recording named '${name}'`);
|
|
5175
|
-
const session = deps.sessions.resolve(
|
|
6055
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5176
6056
|
const since = session.elapsed();
|
|
5177
|
-
const steps = await replayProgram(session, program);
|
|
6057
|
+
const steps = await replayProgram(session, program, args["confirmDangerous"] === true);
|
|
5178
6058
|
return { recordingName: name, since, steps, ok: steps.every((s) => s.ok) };
|
|
5179
6059
|
}
|
|
5180
6060
|
},
|
|
@@ -5182,13 +6062,13 @@ var TOOLS = [
|
|
|
5182
6062
|
name: IrisTool.NARRATE,
|
|
5183
6063
|
description: "Narrate your intent on the page (presenter HUD) so the human watching sees what you are about to do and why. Use a short sentence before a meaningful action.",
|
|
5184
6064
|
inputSchema: {
|
|
5185
|
-
text:
|
|
5186
|
-
level:
|
|
6065
|
+
text: z16.string().describe("Short sentence describing your next action, shown on the presenter HUD for the developer watching."),
|
|
6066
|
+
level: z16.string().optional().describe("Display severity: info | warn | error. Default: info."),
|
|
5187
6067
|
...sessionIdShape6
|
|
5188
6068
|
},
|
|
5189
|
-
outputSchema: { ok:
|
|
6069
|
+
outputSchema: { ok: z16.boolean() },
|
|
5190
6070
|
handler: async (deps, args) => {
|
|
5191
|
-
const result = await commandOrThrow3(deps,
|
|
6071
|
+
const result = await commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.NARRATE, {
|
|
5192
6072
|
text: args["text"],
|
|
5193
6073
|
level: args["level"]
|
|
5194
6074
|
});
|
|
@@ -5199,16 +6079,16 @@ var TOOLS = [
|
|
|
5199
6079
|
name: IrisTool.CLOCK,
|
|
5200
6080
|
description: "Control a fake clock: { freeze:true } to freeze time, { advanceMs:N } to fast-forward timers (toasts, debounces, auto-dismiss), { reset:true } to restore. Lets you test time-gated UI deterministically.",
|
|
5201
6081
|
inputSchema: {
|
|
5202
|
-
freeze:
|
|
5203
|
-
advanceMs:
|
|
5204
|
-
reset:
|
|
6082
|
+
freeze: z16.boolean().optional().describe("Freeze the fake clock. Time stops advancing until advanceMs or reset."),
|
|
6083
|
+
advanceMs: z16.number().optional().describe("Fast-forward time by this many milliseconds \u2014 triggers debounces, toasts, auto-dismiss timers."),
|
|
6084
|
+
reset: z16.boolean().optional().describe("Restore the real clock."),
|
|
5205
6085
|
...sessionIdShape6
|
|
5206
6086
|
},
|
|
5207
6087
|
outputSchema: {
|
|
5208
|
-
ok:
|
|
5209
|
-
elapsed:
|
|
6088
|
+
ok: z16.boolean().optional(),
|
|
6089
|
+
elapsed: z16.number().optional()
|
|
5210
6090
|
},
|
|
5211
|
-
handler: (deps, args) => commandOrThrow3(deps,
|
|
6091
|
+
handler: (deps, args) => commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.CLOCK, {
|
|
5212
6092
|
freeze: args["freeze"],
|
|
5213
6093
|
advanceMs: args["advanceMs"],
|
|
5214
6094
|
reset: args["reset"]
|
|
@@ -5218,27 +6098,27 @@ var TOOLS = [
|
|
|
5218
6098
|
name: IrisTool.STATE,
|
|
5219
6099
|
description: "Read live framework state without the app pre-broadcasting it. PREFERRED/RELIABLE: `store` reads a registered store (e.g. 'workspace'); omit `store` to read all stores. To avoid paying for a huge store, scope the read: `path` extracts a dot-path sub-tree (e.g. 'captionCache.v3', with numeric array indices), and `depth` collapses anything deeper than N levels to a size marker. A wrong `path` returns { found:false, availableKeys } so it is diagnosable. `ref` attempts a best-effort read of the nearest React component's hook state and is BOUNDED \u2014 on failure it returns component: { ok: false, reason: 'component-state-unavailable' }. Without path/depth: returns { stores, storeNames, component? }.",
|
|
5220
6100
|
inputSchema: {
|
|
5221
|
-
ref:
|
|
5222
|
-
store:
|
|
5223
|
-
path:
|
|
5224
|
-
depth:
|
|
6101
|
+
ref: z16.string().optional().describe("Element ref \u2014 attempts a best-effort read of the nearest React component's hook state."),
|
|
6102
|
+
store: z16.string().optional().describe("Registered store name (e.g. 'workspace'). Omit to read all stores."),
|
|
6103
|
+
path: z16.string().optional().describe("Dot-path into the store (e.g. 'captionCache.v3'). Numeric array indices are supported."),
|
|
6104
|
+
depth: z16.number().optional().describe("Collapse anything deeper than N levels to a size marker \u2014 avoids huge outputs for large stores."),
|
|
5225
6105
|
...sessionIdShape6
|
|
5226
6106
|
},
|
|
5227
6107
|
outputSchema: {
|
|
5228
|
-
stores:
|
|
5229
|
-
storeNames:
|
|
5230
|
-
found:
|
|
5231
|
-
value:
|
|
5232
|
-
component:
|
|
6108
|
+
stores: z16.record(z16.unknown()).optional(),
|
|
6109
|
+
storeNames: z16.array(z16.string()).optional(),
|
|
6110
|
+
found: z16.boolean().optional(),
|
|
6111
|
+
value: z16.unknown().optional(),
|
|
6112
|
+
component: z16.object({ ok: z16.boolean(), reason: z16.string().optional(), state: z16.unknown().optional() }).optional()
|
|
5233
6113
|
},
|
|
5234
6114
|
handler: async (deps, args) => {
|
|
5235
|
-
const store =
|
|
5236
|
-
const result = await commandOrThrow3(deps,
|
|
6115
|
+
const store = asString(args["store"]);
|
|
6116
|
+
const result = await commandOrThrow3(deps, asString(args["sessionId"]), IrisCommand.STATE_READ, {
|
|
5237
6117
|
ref: args["ref"],
|
|
5238
6118
|
store
|
|
5239
6119
|
});
|
|
5240
|
-
const path =
|
|
5241
|
-
const depth =
|
|
6120
|
+
const path = asString(args["path"]);
|
|
6121
|
+
const depth = asNumber(args["depth"]);
|
|
5242
6122
|
if (path === void 0 && depth === void 0)
|
|
5243
6123
|
return result;
|
|
5244
6124
|
const root = result;
|
|
@@ -5258,16 +6138,16 @@ var TOOLS = [
|
|
|
5258
6138
|
name: IrisTool.EXPLORE,
|
|
5259
6139
|
description: "Autonomous-exploration helper: list interactive elements (with refs) + current console-error count, so the agent can drive the app and report anomalies.",
|
|
5260
6140
|
inputSchema: {
|
|
5261
|
-
scope:
|
|
6141
|
+
scope: z16.string().optional().describe("CSS selector or element ref to restrict the interactive element list to a subtree."),
|
|
5262
6142
|
...sessionIdShape6
|
|
5263
6143
|
},
|
|
5264
6144
|
outputSchema: {
|
|
5265
|
-
interactive:
|
|
5266
|
-
consoleErrors:
|
|
5267
|
-
hint:
|
|
6145
|
+
interactive: z16.array(z16.unknown()),
|
|
6146
|
+
consoleErrors: z16.number(),
|
|
6147
|
+
hint: z16.string()
|
|
5268
6148
|
},
|
|
5269
6149
|
handler: async (deps, args) => {
|
|
5270
|
-
const session = deps.sessions.resolve(
|
|
6150
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5271
6151
|
const result = await session.command(IrisCommand.SNAPSHOT, {
|
|
5272
6152
|
mode: SnapshotMode.INTERACTIVE,
|
|
5273
6153
|
scope: args["scope"]
|
|
@@ -5285,6 +6165,7 @@ var TOOLS = [
|
|
|
5285
6165
|
},
|
|
5286
6166
|
// iris_capabilities (live | fromDisk) + iris_contract_save. See contract-tools.ts.
|
|
5287
6167
|
...CONTRACT_TOOLS,
|
|
6168
|
+
...DOMAIN_TOOLS,
|
|
5288
6169
|
// iris_flow_save / iris_flow_list / iris_flow_load. See flow-tools.ts.
|
|
5289
6170
|
...FLOW_TOOLS,
|
|
5290
6171
|
// iris_project (read history + diff-vs-last) / iris_run_record. See project-tools.ts.
|
|
@@ -5438,7 +6319,7 @@ async function runTool(tool, deps, args) {
|
|
|
5438
6319
|
return result;
|
|
5439
6320
|
if (!isPlainObject(result) || "session" in result)
|
|
5440
6321
|
return result;
|
|
5441
|
-
const session = deps.sessions.resolve(
|
|
6322
|
+
const session = deps.sessions.resolve(asString(args["sessionId"]));
|
|
5442
6323
|
const envelope = { ...healthEnvelope(session) };
|
|
5443
6324
|
const lease = session.takeSessionLease();
|
|
5444
6325
|
if (lease !== void 0)
|
|
@@ -5574,7 +6455,17 @@ var IRIS_HOME2 = join4(homedir2(), ".iris");
|
|
|
5574
6455
|
// ../server/dist/index.js
|
|
5575
6456
|
async function start(options = {}) {
|
|
5576
6457
|
const port = options.port ?? IRIS_DEFAULT_PORT;
|
|
5577
|
-
const
|
|
6458
|
+
const envToken = process.env["IRIS_TOKEN"];
|
|
6459
|
+
const envOrigins = process.env["IRIS_ALLOWED_ORIGINS"];
|
|
6460
|
+
const host = options.host ?? process.env["IRIS_HOST"];
|
|
6461
|
+
const token = options.token ?? (envToken !== void 0 && envToken.length > 0 ? envToken : void 0);
|
|
6462
|
+
const allowedOrigins = options.allowedOrigins ?? envOrigins?.split(",").map((origin) => origin.trim()).filter((origin) => origin.length > 0);
|
|
6463
|
+
const bridge = new Bridge({
|
|
6464
|
+
port,
|
|
6465
|
+
...host === void 0 ? {} : { host },
|
|
6466
|
+
...token === void 0 ? {} : { token },
|
|
6467
|
+
...allowedOrigins === void 0 ? {} : { allowedOrigins }
|
|
6468
|
+
});
|
|
5578
6469
|
const reaper = new SessionReaper(bridge.sessions);
|
|
5579
6470
|
reaper.start();
|
|
5580
6471
|
const baselines = new BaselineStore();
|
|
@@ -5916,53 +6807,6 @@ function createTestContext(invoke, options = {}) {
|
|
|
5916
6807
|
};
|
|
5917
6808
|
}
|
|
5918
6809
|
|
|
5919
|
-
// ../test/dist/success-assert.js
|
|
5920
|
-
function successToPredicate(success, dynamic) {
|
|
5921
|
-
const parts = [];
|
|
5922
|
-
if (success.signal !== void 0) {
|
|
5923
|
-
parts.push(success.signalData !== void 0 ? { kind: PredicateKind.SIGNAL, name: success.signal, dataMatches: success.signalData } : { kind: PredicateKind.SIGNAL, name: success.signal });
|
|
5924
|
-
}
|
|
5925
|
-
if (success.net !== void 0) {
|
|
5926
|
-
const net = { kind: PredicateKind.NET };
|
|
5927
|
-
if (success.net.method !== void 0)
|
|
5928
|
-
net.method = success.net.method;
|
|
5929
|
-
if (success.net.urlContains !== void 0)
|
|
5930
|
-
net.urlContains = success.net.urlContains;
|
|
5931
|
-
if (success.net.status !== void 0)
|
|
5932
|
-
net.status = success.net.status;
|
|
5933
|
-
parts.push(net);
|
|
5934
|
-
}
|
|
5935
|
-
const element = success.element;
|
|
5936
|
-
if (element !== void 0) {
|
|
5937
|
-
const testid = element.testid;
|
|
5938
|
-
if (testid === void 0 || !dynamic.has(testid)) {
|
|
5939
|
-
const query = {};
|
|
5940
|
-
if (testid !== void 0)
|
|
5941
|
-
query["testid"] = testid;
|
|
5942
|
-
if (element.role !== void 0)
|
|
5943
|
-
query["role"] = element.role;
|
|
5944
|
-
if (element.name !== void 0)
|
|
5945
|
-
query["name"] = element.name;
|
|
5946
|
-
if (Object.keys(query).length > 0) {
|
|
5947
|
-
parts.push({ kind: PredicateKind.ELEMENT, query });
|
|
5948
|
-
}
|
|
5949
|
-
}
|
|
5950
|
-
}
|
|
5951
|
-
if (parts.length === 0)
|
|
5952
|
-
return void 0;
|
|
5953
|
-
if (parts.length === 1)
|
|
5954
|
-
return parts[0];
|
|
5955
|
-
return { kind: "allOf", predicates: parts };
|
|
5956
|
-
}
|
|
5957
|
-
async function assertSuccess(session, success, dynamic, waitForSignal, timeoutMs) {
|
|
5958
|
-
if (success === void 0)
|
|
5959
|
-
return { pass: true };
|
|
5960
|
-
const predicate = successToPredicate(success, dynamic);
|
|
5961
|
-
if (predicate === void 0)
|
|
5962
|
-
return { pass: true };
|
|
5963
|
-
return waitForSignal(session, predicate, timeoutMs);
|
|
5964
|
-
}
|
|
5965
|
-
|
|
5966
6810
|
// ../test/dist/flow-spec.js
|
|
5967
6811
|
function toDynamicSet(flow) {
|
|
5968
6812
|
return new Set((flow.dynamic ?? []).filter((a) => a.kind === AnchorKind.TESTID).map((a) => a.kind === AnchorKind.TESTID ? a.value : ""));
|