@flowriki/agent 0.1.0-beta.15 → 0.1.0-beta.18

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.15";
9
+ var AGENT_VERSION = "0.1.0-beta.18";
10
10
  async function prepareBrowsers() {
11
11
  if (browsersReady()) {
12
12
  return { ok: true, already: true, message: "chromium is already installed" };
@@ -458,7 +458,7 @@ import path7 from "path";
458
458
  // ../engine-playwright/dist/index.js
459
459
  import path3 from "path";
460
460
 
461
- // ../test-model/dist/chunk-T76NYOOM.js
461
+ // ../test-model/dist/chunk-ZRUZU4TI.js
462
462
  import { z as z3 } from "zod";
463
463
  var LocatorOverrideSchema = z3.object({
464
464
  strategy: z3.enum(["css", "xpath", "id", "testId", "text", "label", "placeholder", "altText", "title"]),
@@ -758,6 +758,20 @@ var FlowDocumentSchema = z3.object({
758
758
  updatedAt: z3.string(),
759
759
  steps: z3.array(RecordedStepSchema),
760
760
  environment: ExecutionEnvironmentSchema.optional(),
761
+ /** WHICH DEPLOYMENT THIS WAS RECORDED AGAINST, by name — "staging",
762
+ * "production", whatever the project calls them.
763
+ *
764
+ * A record of fact, not a constraint: the flow can still be run against any
765
+ * environment, which is the point of naming them. What it answers is the
766
+ * question somebody asks of a test that fails only in one place — "where
767
+ * was this made?" — which was previously unanswerable, because the address
768
+ * it used belonged to a setting that had since moved on.
769
+ *
770
+ * Deliberately NOT the address. An origin in a git-tracked file is the
771
+ * mistake documented on `baseUrl` above; a name survives the deployment
772
+ * moving. Absent on everything recorded before this, and on a project that
773
+ * has never named its environments. */
774
+ recordedIn: z3.string().min(1).optional(),
761
775
  path: z3.string().optional(),
762
776
  actors: z3.array(ActorDefinitionSchema).optional(),
763
777
  defaultActorId: z3.string().optional(),
@@ -1134,17 +1148,25 @@ var AuthProfileSchema = z32.object({
1134
1148
  updatedAt: z32.string(),
1135
1149
  revision: z32.number().int().nonnegative().default(0)
1136
1150
  }).strict();
1137
- var ACCOUNT_BINDING_SCHEMA_VERSION = 2;
1151
+ var ACCOUNT_BINDING_SCHEMA_VERSION = 3;
1138
1152
  var AccountBindingSchema = z32.object({
1139
1153
  schemaVersion: z32.literal(ACCOUNT_BINDING_SCHEMA_VERSION),
1140
1154
  id: z32.string().min(1),
1141
1155
  slug: z32.string().regex(/^[a-z0-9][a-z0-9-]*$/),
1142
1156
  name: z32.string().min(1),
1143
1157
  profileId: z32.string().min(1),
1144
- /** Which environment of the APPLICATION UNDER TEST these credentials belong
1145
- * to — never Flowriki's own dev or staging. Null means every environment,
1146
- * which is what a project with one environment wants. */
1147
- environment: z32.string().min(1).nullable().default(null),
1158
+ /** Which environments of the APPLICATION UNDER TEST these credentials work
1159
+ * in — never Flowriki's own dev or staging.
1160
+ *
1161
+ * EMPTY MEANS EVERY ONE, which is what a project with a single deployment
1162
+ * wants and what most accounts will always say.
1163
+ *
1164
+ * A LIST because the same person usually exists in more than one place. One
1165
+ * environment per account was the shape until version 3, and it forced a
1166
+ * duplicate account — same name, same password, same everything — for each
1167
+ * deployment the identity existed in, or a blank "works anywhere" that also
1168
+ * claimed environments it had never been created in. Neither was true. */
1169
+ environments: z32.array(z32.string().min(1)).default([]),
1148
1170
  /** Step id → what this identity types there. Ordinary values: a username is
1149
1171
  * not a credential, and keeping it here is what lets somebody read an
1150
1172
  * account and know who it is. */
@@ -2125,6 +2147,20 @@ function installRecorder() {
2125
2147
  },
2126
2148
  true
2127
2149
  );
2150
+ const SECRET_WORDS = /password|passwd|pass[-_\s]?code|pwd|secret|otp|one[-_\s]?time[-_\s]?(code|pass)|security[-_\s]?(code|answer)|cvv|cvc|\bpin\b/i;
2151
+ const looksSecret = (el, type, name) => {
2152
+ if (type === "password") return true;
2153
+ const auto = String(el.autocomplete || el.getAttribute?.("autocomplete") || "").toLowerCase();
2154
+ if (/current-password|new-password|one-time-code/.test(auto)) return true;
2155
+ try {
2156
+ const masked = getComputedStyle(el).webkitTextSecurity;
2157
+ if (masked && masked !== "none") return true;
2158
+ } catch {
2159
+ }
2160
+ return SECRET_WORDS.test(
2161
+ [name, el.name, el.id, el.getAttribute?.("aria-label"), el.placeholder].filter(Boolean).join(" ")
2162
+ );
2163
+ };
2128
2164
  const editedValues = /* @__PURE__ */ new WeakMap();
2129
2165
  const onEdit = (e) => {
2130
2166
  if (e.isComposing) return;
@@ -2155,15 +2191,16 @@ function installRecorder() {
2155
2191
  const signature = JSON.stringify(values ?? value);
2156
2192
  if (editedValues.get(el) === signature) return;
2157
2193
  editedValues.set(el, signature);
2194
+ const identity = identify(el);
2158
2195
  send({
2159
2196
  type: type === "range" ? "setRange" : tag === "select" ? "select" : "fill",
2160
2197
  role: roleOf(el),
2161
- ...identify(el),
2162
- ...scoping(el, roleOf(el), identify(el).name),
2198
+ ...identity,
2199
+ ...scoping(el, roleOf(el), identity.name),
2163
2200
  legacySelector: cssPath(el),
2164
2201
  value,
2165
2202
  ...values ? { values } : {},
2166
- secret: type === "password"
2203
+ secret: looksSecret(el, type, identity.name)
2167
2204
  });
2168
2205
  };
2169
2206
  document.addEventListener("input", onEdit, true);
@@ -2985,7 +3022,7 @@ var RunActors = class {
2985
3022
  this.current = session;
2986
3023
  this.currentId = id;
2987
3024
  if (this.initialized.has(id)) return;
2988
- await session.page.goto(startUrl, { waitUntil: "domcontentloaded" });
3025
+ if (startUrl) await session.page.goto(startUrl, { waitUntil: "domcontentloaded" });
2989
3026
  if (actor?.startSignedIn) {
2990
3027
  if (kept?.state) {
2991
3028
  const confirmed = await confirmSignedIn(session.page, kept.auth, this.opts.reuseCheckMs ?? 3e3);
@@ -3027,7 +3064,7 @@ var RunActors = class {
3027
3064
  } catch {
3028
3065
  }
3029
3066
  }
3030
- await session.page.goto(startUrl, { waitUntil: "domcontentloaded" });
3067
+ if (startUrl) await session.page.goto(startUrl, { waitUntil: "domcontentloaded" });
3031
3068
  }
3032
3069
  this.initialized.add(id);
3033
3070
  }
@@ -3919,6 +3956,12 @@ var checkStep = (id, proposals) => ({
3919
3956
  legacySelector: "",
3920
3957
  proposals
3921
3958
  });
3959
+ function opensItsOwnEntry(steps, opts) {
3960
+ const first = steps.find((step) => !step.disabled);
3961
+ if (!first || first.type !== "signIn" || !first.auth) return false;
3962
+ if (first.auth.from === "here") return false;
3963
+ return opts.resolveAuth?.(first.auth)?.navigation.kind === "open";
3964
+ }
3922
3965
  function stepFailure(step, detail) {
3923
3966
  return {
3924
3967
  stepId: step.id,
@@ -3940,7 +3983,9 @@ async function runStepsIn(session, steps, opts) {
3940
3983
  let status2 = "passed";
3941
3984
  let error = null;
3942
3985
  try {
3943
- if (opts.startUrl) await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
3986
+ if (opts.startUrl && !opensItsOwnEntry(steps, opts)) {
3987
+ await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
3988
+ }
3944
3989
  for (const recorded of steps) {
3945
3990
  let step = recorded;
3946
3991
  if (!recorded.disabled) {
@@ -4158,8 +4203,9 @@ async function replayFlow(opts) {
4158
4203
  hydrated: hydrated ? { ...hydrated, id: opts.defaultActorId ?? "default" } : null
4159
4204
  });
4160
4205
  let prepared = null;
4206
+ const entry = opensItsOwnEntry(steps, opts) ? "" : void 0;
4161
4207
  try {
4162
- await actors.select(opts.defaultActorId ?? "default");
4208
+ await actors.select(opts.defaultActorId ?? "default", entry);
4163
4209
  } catch (e) {
4164
4210
  prepared = e.message;
4165
4211
  }
@@ -4785,10 +4831,8 @@ async function startRecordingSession(opts) {
4785
4831
  await page.goto(url, { waitUntil: "domcontentloaded" });
4786
4832
  }
4787
4833
  await executeStep(page, opts.authentication.step, { ...opts.authentication, baseUrl: url });
4788
- const resume = { id: "auth-return", type: "navigate", name: "Open recording start page", role: "", scope: "", value: new URL(url).pathname + new URL(url).search + new URL(url).hash, navigationBase: "application", secret: false, legacySelector: "", proposals: [] };
4789
- steps.push(opts.authentication.step, resume);
4834
+ steps.push(opts.authentication.step);
4790
4835
  onStep?.(opts.authentication.step);
4791
- onStep?.(resume);
4792
4836
  } catch (error) {
4793
4837
  const landed = await page.url().replace(/^https?:\/\/[^/]+/, "");
4794
4838
  const title = await page.title().catch(() => "");
@@ -4850,7 +4894,8 @@ async function startRecordingSession(opts) {
4850
4894
  });
4851
4895
  });
4852
4896
  try {
4853
- await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
4897
+ if (!opts.authentication) await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
4898
+ else await ensureInstalled(page);
4854
4899
  } catch (err) {
4855
4900
  stopped = true;
4856
4901
  await browser.close().catch(() => {
@@ -5183,7 +5228,11 @@ async function startDebugSession(opts) {
5183
5228
  publish();
5184
5229
  });
5185
5230
  await context2.exposeBinding("__flCmd", (_src, msg) => {
5186
- void handleCommand(msg);
5231
+ void handleCommand(msg).catch((error) => {
5232
+ failure = { stepId: cursor ?? steps[0]?.id ?? "", error: firstLine2(error) };
5233
+ status2 = "paused";
5234
+ void paint(snapshot());
5235
+ });
5187
5236
  });
5188
5237
  await context2.addInitScript({ content: `${accNameScript2()}
5189
5238
  ;(${installRecorder.toString()})();
@@ -6298,7 +6347,7 @@ function toSummary(row) {
6298
6347
  };
6299
6348
  }
6300
6349
 
6301
- // ../repository-store/dist/chunk-IXRMPDVC.js
6350
+ // ../repository-store/dist/chunk-A4JZPKZT.js
6302
6351
  var FLOWRIKI_DIR = ".flowriki";
6303
6352
  var FEATURES_DIR = `${FLOWRIKI_DIR}/features`;
6304
6353
  var CONFIG_PATH = `${FLOWRIKI_DIR}/config.json`;
@@ -6339,6 +6388,7 @@ function parseConfig(raw) {
6339
6388
  return {
6340
6389
  baseUrl: typeof source["baseUrl"] === "string" ? source["baseUrl"] : "",
6341
6390
  ...typeof source["applicationEnvironment"] === "string" ? { applicationEnvironment: source["applicationEnvironment"] } : {},
6391
+ ...Array.isArray(source["environments"]) ? { environments: readEnvironments(source["environments"]) } : {},
6342
6392
  reuseAuthentication: source["reuseAuthentication"] === true,
6343
6393
  headless: source["headless"] === true,
6344
6394
  environment: ExecutionEnvironmentSchema.parse(source["environment"] ?? {}),
@@ -6346,6 +6396,20 @@ function parseConfig(raw) {
6346
6396
  flakinessRepeat: clampRepeat(source["flakinessRepeat"])
6347
6397
  };
6348
6398
  }
6399
+ function readEnvironments(raw) {
6400
+ const out = [];
6401
+ const seen = /* @__PURE__ */ new Set();
6402
+ for (const entry of raw) {
6403
+ if (!entry || typeof entry !== "object") continue;
6404
+ const { name, baseUrl } = entry;
6405
+ if (typeof name !== "string" || !name.trim()) continue;
6406
+ const key = name.trim().toLowerCase();
6407
+ if (seen.has(key)) continue;
6408
+ seen.add(key);
6409
+ out.push({ name: name.trim(), baseUrl: typeof baseUrl === "string" ? baseUrl : "" });
6410
+ }
6411
+ return out;
6412
+ }
6349
6413
  var FlowNotFound = class extends Error {
6350
6414
  constructor(message) {
6351
6415
  super(message);
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  resolveCheckouts,
9
9
  start,
10
10
  status
11
- } from "./chunk-KY7KDJTV.js";
11
+ } from "./chunk-ZGXVQ2QA.js";
12
12
 
13
13
  // src/cli.ts
14
14
  import { parseArgs } from "util";
@@ -111,6 +111,11 @@ Run \`flowriki-agent --help\`.`);
111
111
  return 0;
112
112
  }
113
113
  case "start": {
114
+ process.on("unhandledRejection", (reason) => {
115
+ const said = reason instanceof Error ? reason.stack ?? reason.message : String(reason);
116
+ console.error(red(`! a command failed without being handled \u2014 the agent is still running`));
117
+ console.error(dim(said.split("\n").slice(0, 4).join("\n")));
118
+ });
114
119
  const started = await start({
115
120
  ...service ? { service } : {},
116
121
  ...projects.length ? { projects } : {},
package/dist/engine.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  resolveCheckouts,
10
10
  start,
11
11
  status
12
- } from "./chunk-KY7KDJTV.js";
12
+ } from "./chunk-ZGXVQ2QA.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.15",
3
+ "version": "0.1.0-beta.18",
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",