@flowriki/agent 0.1.0-beta.23 → 0.1.0-beta.24

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.
@@ -6,7 +6,7 @@ import os from "os";
6
6
  import path from "path";
7
7
  import { createRequire } from "module";
8
8
  import { spawnSync } from "child_process";
9
- var AGENT_VERSION = "0.1.0-beta.23";
9
+ var AGENT_VERSION = "0.1.0-beta.24";
10
10
  async function prepareBrowsers() {
11
11
  if (browsersReady()) {
12
12
  return { ok: true, already: true, message: "chromium is already installed" };
@@ -135,7 +135,7 @@ function forgetConnection(service, env = process.env) {
135
135
  }
136
136
  var fingerprint = (secret) => crypto2.createHash("sha256").update(secret).digest("hex").slice(0, 12);
137
137
 
138
- // ../contracts/dist/chunk-BZ2CJR5T.js
138
+ // ../contracts/dist/chunk-MQTBBCDK.js
139
139
  import { z } from "zod";
140
140
  var PROTOCOL_VERSION = 1;
141
141
  var PAIRING_CODE_TTL_MS = 5 * 60 * 1e3;
@@ -290,7 +290,10 @@ var AGENT_FEATURES = /* @__PURE__ */ new Set([
290
290
  "authentication.v1",
291
291
  /** More than one participant, each with a browser session of its own —
292
292
  * which also covers starting one over and sharing a captured output. */
293
- "actors.v1"
293
+ "actors.v1",
294
+ /** Replaying other flows' steps before a recording starts capturing, so a
295
+ * flow can be recorded from where its prerequisites leave the browser. */
296
+ "prerequisites.v1"
294
297
  ]);
295
298
  var AgentMessageSchema = z.discriminatedUnion("type", [
296
299
  AckSchema,
@@ -458,7 +461,7 @@ import path7 from "path";
458
461
  // ../engine-playwright/dist/index.js
459
462
  import path3 from "path";
460
463
 
461
- // ../test-model/dist/chunk-X4X4KGMA.js
464
+ // ../test-model/dist/chunk-TSFRTYLG.js
462
465
  import { z as z3 } from "zod";
463
466
  var LocatorOverrideSchema = z3.object({
464
467
  strategy: z3.enum(["css", "xpath", "id", "testId", "text", "label", "placeholder", "altText", "title"]),
@@ -759,6 +762,12 @@ var ActorDefinitionSchema = z3.object({
759
762
  name: z3.string().min(1),
760
763
  startSignedIn: z3.object({ profileId: z3.string().min(1), accountId: z3.string().optional() }).strict().optional()
761
764
  }).strict();
765
+ var FlowSignInSchema = z3.object({
766
+ profileId: z3.string().min(1),
767
+ accountId: z3.string().optional(),
768
+ from: z3.enum(["loginPage", "here"]).optional()
769
+ }).strict();
770
+ var FlowPrerequisitesSchema = z3.array(z3.object({ flowId: z3.string().min(1) }).strict());
762
771
  var CURRENT_SCHEMA_VERSION = 5;
763
772
  var FlowDocumentSchema = z3.object({
764
773
  schemaVersion: z3.literal(CURRENT_SCHEMA_VERSION),
@@ -800,6 +809,24 @@ var FlowDocumentSchema = z3.object({
800
809
  path: z3.string().optional(),
801
810
  actors: z3.array(ActorDefinitionSchema).optional(),
802
811
  defaultActorId: z3.string().optional(),
812
+ /** SIGN IN FIRST: the account this flow starts as, before anything else runs.
813
+ *
814
+ * A setting rather than step one, which is what it used to be saved as. As a
815
+ * step it could not be switched off without deleting it, and a flow reused
816
+ * as somebody else's prerequisite could not tell "my own sign-in" from a
817
+ * sign-in that is part of what it tests. `parseFlowDocument` lifts an old
818
+ * step one into this field, so both spellings read the same. A reference to
819
+ * a profile, never a copy of its steps. */
820
+ signIn: FlowSignInSchema.optional(),
821
+ /** Flows replayed, in this order, after signing in and before this flow's
822
+ * own steps — to reach the state it starts from.
823
+ *
824
+ * LINKS, NOT COPIES: re-record "Save basic info" once and every flow that
825
+ * starts after it follows. Each runs WITHOUT its own sign-in; this flow's
826
+ * is the only one. Explicit and flat — nothing here pulls in a
827
+ * prerequisite's prerequisites at run time, so what runs is exactly this
828
+ * list. The editor fills those in when a flow is added. */
829
+ prerequisites: FlowPrerequisitesSchema.optional(),
803
830
  /** Bumped on every save. An edit carries the revision it was made against,
804
831
  * so a save built on a stale copy is refused instead of overwriting work
805
832
  * somebody else — or a debug session on the same machine — has since done.
@@ -895,8 +922,19 @@ function parseFlowDocument(input) {
895
922
  version += 1;
896
923
  raw.schemaVersion = version;
897
924
  }
925
+ liftSignInFirst(raw);
898
926
  return FlowDocumentSchema.parse(raw);
899
927
  }
928
+ var SIGN_IN_FIRST_STEP_ID = "sign-in-first";
929
+ function liftSignInFirst(raw) {
930
+ const first = raw.steps?.[0];
931
+ const auth = first?.auth;
932
+ if (!first || first.type !== "signIn" || first.disabled === true || first.actorId !== void 0) return;
933
+ if (!auth || typeof auth.profileId !== "string" || !auth.profileId) return;
934
+ if (raw["signIn"] !== void 0 || Array.isArray(raw["actors"]) && raw["actors"].length > 0) return;
935
+ raw["signIn"] = auth;
936
+ raw.steps = raw.steps.slice(1);
937
+ }
900
938
 
901
939
  // ../test-model/dist/index.js
902
940
  import { z as z4 } from "zod";
@@ -1177,6 +1215,65 @@ function sessionLanes(occurrences, defaultActorId) {
1177
1215
  }
1178
1216
  return rows;
1179
1217
  }
1218
+ function randomUUID() {
1219
+ const crypto5 = globalThis.crypto;
1220
+ if (crypto5?.randomUUID) return crypto5.randomUUID();
1221
+ return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2, 14)}`;
1222
+ }
1223
+ var mintPage = (remap, pageId) => {
1224
+ if (pageId === "main") return "main";
1225
+ const existing = remap.pages.get(pageId);
1226
+ if (existing) return existing;
1227
+ const fresh = `pg_${randomUUID().replace(/-/g, "").slice(0, 12)}`;
1228
+ remap.pages.set(pageId, fresh);
1229
+ return fresh;
1230
+ };
1231
+ function retarget(target, remap) {
1232
+ if (!target.pageId) return target;
1233
+ return { ...target, pageId: mintPage(remap, target.pageId) };
1234
+ }
1235
+ function copyStep(step, remap) {
1236
+ const copied = retarget({ ...step, id: remap.steps.get(step.id) }, remap);
1237
+ if (copied.opensPage) copied.opensPage = mintPage(remap, copied.opensPage);
1238
+ if (copied.to) copied.to = retarget({ ...copied.to }, remap);
1239
+ if (copied.scroll?.container) {
1240
+ copied.scroll = { ...copied.scroll, container: retarget({ ...copied.scroll.container }, remap) };
1241
+ }
1242
+ copied.proposals = step.proposals.map((assertion) => ({ ...assertion }));
1243
+ return copied;
1244
+ }
1245
+ function copyFlowIntoCase(flow, context) {
1246
+ const remap = { steps: /* @__PURE__ */ new Map(), pages: /* @__PURE__ */ new Map() };
1247
+ for (const step of flow.steps) {
1248
+ remap.steps.set(step.id, `st_${randomUUID().replace(/-/g, "").slice(0, 12)}`);
1249
+ }
1250
+ const steps = flow.steps.map((step) => copyStep(step, remap));
1251
+ return {
1252
+ occurrenceId: `occ_${randomUUID().replace(/-/g, "").slice(0, 12)}`,
1253
+ name: flow.name,
1254
+ // WHO RUNS THIS COPY. The flow said which of its participants it belongs
1255
+ // to; dropping that here made every copy run as the main participant, so a
1256
+ // flow recorded as the approver silently became the buyer doing approvals.
1257
+ // The case has to be given the participant too — see `mergeParticipants`.
1258
+ ...flow.defaultActorId && flow.defaultActorId !== "default" ? { actorId: flow.defaultActorId } : {},
1259
+ ...flow.baseUrl ? { baseUrl: flow.baseUrl } : {},
1260
+ startPath: flow.startPath,
1261
+ navigateFirst: false,
1262
+ steps,
1263
+ ...flow.environment ? { environment: flow.environment } : {},
1264
+ origin: {
1265
+ flowId: flow.id,
1266
+ schemaVersion: flow.schemaVersion,
1267
+ revision: flow.revision,
1268
+ ...context.commit ? { commit: context.commit } : {},
1269
+ featureId: flow.featureId,
1270
+ featureName: context.featureName,
1271
+ flowName: flow.name,
1272
+ stepIds: Object.fromEntries(remap.steps),
1273
+ copiedAt: (context.at ?? /* @__PURE__ */ new Date()).toISOString()
1274
+ }
1275
+ };
1276
+ }
1180
1277
  function caseDebugPlan(testCase, baseUrl) {
1181
1278
  const owners = {};
1182
1279
  const steps = [];
@@ -1396,6 +1493,85 @@ function sameIdentity(a, b2) {
1396
1493
  if (a.accountId && b2.accountId) return a.accountId === b2.accountId;
1397
1494
  return true;
1398
1495
  }
1496
+ var PrerequisiteProblem = class extends Error {
1497
+ constructor(message) {
1498
+ super(message);
1499
+ this.name = "PrerequisiteProblem";
1500
+ }
1501
+ };
1502
+ function signInFirstStep(flow, id = SIGN_IN_FIRST_STEP_ID) {
1503
+ if (!flow.signIn) return null;
1504
+ return {
1505
+ id,
1506
+ type: "signIn",
1507
+ auth: flow.signIn,
1508
+ name: "Sign in first",
1509
+ role: "",
1510
+ scope: "",
1511
+ value: "",
1512
+ secret: false,
1513
+ legacySelector: "",
1514
+ proposals: []
1515
+ };
1516
+ }
1517
+ function stepsWithoutSignIn(flow) {
1518
+ const first = flow.steps[0];
1519
+ return first && first.type === "signIn" && !first.disabled ? flow.steps.slice(1) : flow.steps;
1520
+ }
1521
+ var idsOf = (flow) => (flow.prerequisites ?? []).map((p) => p.flowId);
1522
+ function prerequisiteProblems(flow, lookup) {
1523
+ const problems = [];
1524
+ const seen = /* @__PURE__ */ new Set();
1525
+ for (const id of idsOf(flow)) {
1526
+ if (id === flow.id) {
1527
+ problems.push(`"${flow.name}" cannot run before itself`);
1528
+ continue;
1529
+ }
1530
+ if (seen.has(id)) {
1531
+ problems.push(`"${lookup(id)?.name ?? id}" is listed twice before "${flow.name}"`);
1532
+ continue;
1533
+ }
1534
+ seen.add(id);
1535
+ const prerequisite = lookup(id);
1536
+ if (!prerequisite) {
1537
+ problems.push(`a flow "${flow.name}" runs after no longer exists \u2014 remove it from the list`);
1538
+ continue;
1539
+ }
1540
+ if (reaches(prerequisite, flow.id, lookup, /* @__PURE__ */ new Set())) {
1541
+ problems.push(`"${prerequisite.name}" already runs after "${flow.name}", so it cannot also run before it`);
1542
+ }
1543
+ }
1544
+ return problems;
1545
+ }
1546
+ function reaches(from, target, lookup, visited) {
1547
+ if (visited.has(from.id)) return false;
1548
+ visited.add(from.id);
1549
+ return idsOf(from).some((id) => {
1550
+ if (id === target) return true;
1551
+ const next = lookup(id);
1552
+ return next ? reaches(next, target, lookup, visited) : false;
1553
+ });
1554
+ }
1555
+ function prerequisitesOf(flow, lookup) {
1556
+ const problems = prerequisiteProblems(flow, lookup);
1557
+ if (problems.length > 0) throw new PrerequisiteProblem(problems.join("; "));
1558
+ return idsOf(flow).map((id) => lookup(id));
1559
+ }
1560
+ function stepsForHost(prerequisite) {
1561
+ return copyFlowIntoCase({ ...prerequisite, steps: stepsWithoutSignIn(prerequisite) }, { featureName: "" }).steps;
1562
+ }
1563
+ function preparationFor(flow, lookup) {
1564
+ return {
1565
+ signIn: signInFirstStep(flow),
1566
+ steps: prerequisitesOf(flow, lookup).flatMap(stepsForHost)
1567
+ };
1568
+ }
1569
+ function expandForRun(flow, lookup) {
1570
+ const { signIn: _signIn, prerequisites: _prerequisites, ...rest } = flow;
1571
+ if (!flow.signIn && idsOf(flow).length === 0) return rest;
1572
+ const { signIn, steps } = preparationFor(flow, lookup);
1573
+ return { ...rest, steps: [...signIn ? [signIn] : [], ...steps, ...flow.steps] };
1574
+ }
1399
1575
 
1400
1576
  // ../engine-playwright/dist/index.js
1401
1577
  import { createHmac, randomBytes } from "crypto";
@@ -4388,6 +4564,26 @@ var checkStep = (id, proposals) => ({
4388
4564
  legacySelector: "",
4389
4565
  proposals
4390
4566
  });
4567
+ async function replayInto(target, steps, opts) {
4568
+ const session = {
4569
+ ownsBrowser: false,
4570
+ browser: target.browser,
4571
+ context: target.context,
4572
+ page: target.page,
4573
+ pages: new ExecutionPages(target.context, target.page),
4574
+ config: target.config,
4575
+ artifacts: resolveCapture({ policy: { screenshot: "off", video: "off", trace: "off", console: false } }),
4576
+ consoleLines: [],
4577
+ runArtifacts: []
4578
+ };
4579
+ const outcome = await runStepsIn(session, steps, {
4580
+ startUrl: null,
4581
+ ...opts.resolveSecret ? { resolveSecret: opts.resolveSecret } : {},
4582
+ ...opts.resolveAuth ? { resolveAuth: opts.resolveAuth } : {},
4583
+ ...opts.baseUrl ? { baseUrl: opts.baseUrl } : {}
4584
+ });
4585
+ return outcome.status === "passed" ? { ok: true } : { ok: false, error: outcome.error ?? "a step before recording did not complete" };
4586
+ }
4391
4587
  function opensItsOwnEntry(steps, opts) {
4392
4588
  const first = steps.find((step) => !step.disabled);
4393
4589
  if (!first || first.type !== "signIn" || !first.auth) return false;
@@ -5441,6 +5637,25 @@ async function startRecordingSession(opts) {
5441
5637
  throw new Error(`${firstLine2(error)} \u2014 the browser was on ${landed || "/"}${title ? ` ("${title}")` : ""}`);
5442
5638
  }
5443
5639
  }
5640
+ const prepared = Boolean(opts.preparation?.steps.length);
5641
+ if (opts.preparation && prepared) {
5642
+ try {
5643
+ if (!opts.authentication) await page.goto(url, { waitUntil: "domcontentloaded" });
5644
+ const replayed = await replayInto({ browser, context, page, config }, opts.preparation.steps, {
5645
+ resolveSecret: opts.preparation.resolveSecret,
5646
+ ...opts.preparation.resolveAuth ? { resolveAuth: opts.preparation.resolveAuth } : {},
5647
+ baseUrl: url
5648
+ });
5649
+ if (!replayed.ok) throw new Error(replayed.error);
5650
+ } catch (error) {
5651
+ const landed = page.url().replace(/^https?:\/\/[^/]+/, "");
5652
+ const title = await page.title().catch(() => "");
5653
+ stopped = true;
5654
+ await browser.close().catch(() => {
5655
+ });
5656
+ throw new Error(`the flows this one starts after did not finish: ${firstLine2(error)} \u2014 the browser was on ${landed || "/"}${title ? ` ("${title}")` : ""}`);
5657
+ }
5658
+ }
5444
5659
  await context.addInitScript({ content: INIT_SCRIPT });
5445
5660
  const ensureInstalled = async (p) => {
5446
5661
  try {
@@ -5493,7 +5708,7 @@ async function startRecordingSession(opts) {
5493
5708
  });
5494
5709
  });
5495
5710
  try {
5496
- if (!opts.authentication) await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
5711
+ if (!opts.authentication && !prepared) await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
5497
5712
  else await ensureInstalled(page);
5498
5713
  } catch (err) {
5499
5714
  stopped = true;
@@ -7762,8 +7977,15 @@ var LOCAL_CHROMIUM = { id: "chromium", provider: "local", engine: "chromium" };
7762
7977
  async function executeFlow(opts) {
7763
7978
  const { store, storage = null, flowId, trigger, watch = false, onEvent } = opts;
7764
7979
  const headless = watch ? false : opts.headless ?? store.readConfig().headless;
7765
- const flow = store.getFlow(flowId);
7766
- if (!flow) throw new FlowNotRunnable(`flow not found: ${flowId}`, "not_found");
7980
+ const found = store.getFlow(flowId);
7981
+ if (!found) throw new FlowNotRunnable(`flow not found: ${flowId}`, "not_found");
7982
+ let flow = found;
7983
+ try {
7984
+ flow = expandForRun(found, (id) => store.getFlow(id));
7985
+ } catch (problem) {
7986
+ if (problem instanceof PrerequisiteProblem) throw new FlowNotRunnable(problem.message, "empty");
7987
+ throw problem;
7988
+ }
7767
7989
  if (opts.expectedRevision !== void 0 && flow.revision !== opts.expectedRevision) {
7768
7990
  throw new FlowConflict("the flow changed before execution \u2014 reload it before running", flow.revision);
7769
7991
  }
@@ -8313,6 +8535,11 @@ var RecordingCoordinator = class {
8313
8535
  resolveAuth: authenticationResolver(opts.authentications),
8314
8536
  resolveSecret: (ref) => opts.secrets?.[ref]
8315
8537
  } } : {},
8538
+ ...opts.preparation?.length ? { preparation: {
8539
+ steps: opts.preparation,
8540
+ resolveAuth: authenticationResolver(opts.authentications),
8541
+ resolveSecret: (ref) => opts.secrets?.[ref]
8542
+ } } : {},
8316
8543
  // The project's saved defaults, unless this recording asks for
8317
8544
  // something else. Without the fallback the settings screen would write
8318
8545
  // a file nothing ever read.
@@ -8757,7 +8984,7 @@ function resolveCheckouts(dirs) {
8757
8984
  return { projects, skipped };
8758
8985
  }
8759
8986
 
8760
- // ../agent-core/dist/chunk-EJDGHASQ.js
8987
+ // ../agent-core/dist/chunk-4MOK25KG.js
8761
8988
  import { WebSocket, WebSocketServer } from "ws";
8762
8989
  var HEARTBEAT_MS = 3e4;
8763
8990
  function adapt(socket) {
@@ -9827,8 +10054,10 @@ function recordingHandlers({ recorder, onActivity }) {
9827
10054
  // returned on stop for the service to commit to the repository. A
9828
10055
  // machine that has the project checked out still records into it.
9829
10056
  root,
9830
- ...command.args["authentication"] ? {
9831
- authentication: AuthReferenceSchema.parse(command.args["authentication"]),
10057
+ ...command.args["authentication"] ? { authentication: AuthReferenceSchema.parse(command.args["authentication"]) } : {},
10058
+ // The flows this recording starts after, replayed before capturing.
10059
+ ...Array.isArray(command.args["preparation"]) && command.args["preparation"].length > 0 ? { preparation: RecordedStepSchema.array().parse(command.args["preparation"]) } : {},
10060
+ ...command.args["authentication"] || command.args["preparation"] ? {
9832
10061
  authentications: command.args["authentications"],
9833
10062
  secrets: command.args["secrets"] ?? {}
9834
10063
  } : {},
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  resolveCheckouts,
9
9
  start,
10
10
  status
11
- } from "./chunk-5J5D7IIT.js";
11
+ } from "./chunk-EBT7VSPY.js";
12
12
 
13
13
  // src/cli.ts
14
14
  import { parseArgs } from "util";
package/dist/engine.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  resolveCheckouts,
10
10
  start,
11
11
  status
12
- } from "./chunk-5J5D7IIT.js";
12
+ } from "./chunk-EBT7VSPY.js";
13
13
  export {
14
14
  AGENT_VERSION,
15
15
  browsersReady,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowriki/agent",
3
- "version": "0.1.0-beta.23",
3
+ "version": "0.1.0-beta.24",
4
4
  "description": "Run Flowriki UI tests on your own computer. Connects this machine to your Flowriki account.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",