@flowriki/agent 0.1.0-beta.62 → 0.1.0-beta.63

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.
@@ -658,6 +658,7 @@ import { z as z9 } from "zod";
658
658
  import { z as z10 } from "zod";
659
659
  import { z as z11 } from "zod";
660
660
  import { z as z12 } from "zod";
661
+ import { z as z13 } from "zod";
661
662
  var EngineSchema = z3.enum(["chromium", "firefox", "webkit", "android", "ios"]);
662
663
  var CapabilitySchema = z3.object({
663
664
  id: z3.string(),
@@ -1858,6 +1859,13 @@ var RunMemorySchema = z12.object({
1858
1859
  buckets: z12.array(Bucket).max(RUN_MEMORY_WEEKS + 1),
1859
1860
  updatedAt: z12.number()
1860
1861
  }).strict();
1862
+ var WhatToDoSchema = z13.object({
1863
+ id: z13.string().regex(/^wtd_/),
1864
+ projectId: z13.string().min(1),
1865
+ /** Step ids skipped — "we do this differently". */
1866
+ skipped: z13.array(z13.string().min(1).max(80)).max(100),
1867
+ updatedAt: z13.string()
1868
+ }).strict();
1861
1869
 
1862
1870
  // ../engine-appium/dist/index.js
1863
1871
  import path5 from "path";
@@ -68,7 +68,7 @@ import {
68
68
  textIndependent,
69
69
  validateActors,
70
70
  webCloudOf
71
- } from "./chunk-YCFTNJQU.js";
71
+ } from "./chunk-IFUODU4W.js";
72
72
 
73
73
  // src/engine/machine.ts
74
74
  import fs from "fs";
@@ -76,7 +76,7 @@ import os from "os";
76
76
  import path from "path";
77
77
  import { createRequire } from "module";
78
78
  import { spawnSync } from "child_process";
79
- var AGENT_VERSION = "0.1.0-beta.62";
79
+ var AGENT_VERSION = "0.1.0-beta.63";
80
80
  async function prepareBrowsers() {
81
81
  if (browsersReady()) {
82
82
  return { ok: true, already: true, message: "chromium is already installed" };
@@ -136,7 +136,7 @@ async function checkMachine() {
136
136
  }
137
137
  } catch {
138
138
  }
139
- const { mobileDoctor: mobileDoctor2 } = await import("./dist-EGC7VEBG.js");
139
+ const { mobileDoctor: mobileDoctor2 } = await import("./dist-Z67YCHL5.js");
140
140
  return {
141
141
  agentVersion: AGENT_VERSION,
142
142
  node: process.versions.node,
package/dist/cli.js CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  runtimeValue,
25
25
  start,
26
26
  status
27
- } from "./chunk-GPSLXDGQ.js";
27
+ } from "./chunk-QIZZ4HZ7.js";
28
28
  import {
29
29
  CLOUD_PROVIDERS,
30
30
  CLOUD_PROVIDER_NAME,
@@ -52,7 +52,7 @@ import {
52
52
  slugify,
53
53
  stepRefusedOn,
54
54
  targetFor
55
- } from "./chunk-YCFTNJQU.js";
55
+ } from "./chunk-IFUODU4W.js";
56
56
 
57
57
  // src/cli.ts
58
58
  import { parseArgs } from "util";
@@ -433,6 +433,22 @@ var STEP_TIMEOUT_MS = 1e4;
433
433
  var 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;
434
434
  var TYPED = "typed_";
435
435
  var withoutTyped = (steps) => steps.map((st) => st.secretRef?.startsWith(TYPED) ? (({ secretRef: _, ...rest }) => rest)(st) : st);
436
+ var SIGN_UP = /\b(sign[\s-]?up|register|registration|create (an |your )?account|join)\b/i;
437
+ function shapeOf(draft, typed) {
438
+ const fills = draft.steps.filter((st) => st.type === "fill" && !st.disabled);
439
+ const values = {};
440
+ for (const st of fills) {
441
+ const value = st.secret ? st.secretRef ? typed.get(st.secretRef) : void 0 : st.value;
442
+ if (st.name && typeof value === "string") values[st.name] = value;
443
+ }
444
+ const base = { flowName: draft.name, platform: draft.platform ?? "web", values };
445
+ const secret = fills.some((st) => st.secret);
446
+ const fixed = Object.values(values).every((v) => !v.includes("{{"));
447
+ if (!secret || !fixed || Object.keys(values).length !== fills.length) return { kind: null, ...base };
448
+ if (SIGN_UP.test(draft.name) || fills.length > 3) return { kind: "signup", ...base };
449
+ if (draft.signIn || draft.after?.length) return { kind: null, ...base };
450
+ return { kind: "login", ...base };
451
+ }
436
452
  var ACTIONS = [
437
453
  "click",
438
454
  "dblclick",
@@ -756,6 +772,14 @@ ${view}`;
756
772
  }
757
773
  if (this.liveCase) await this.sendCaseReport();
758
774
  }
775
+ /** What the last saved flow was — a login sign-in can be made from, a
776
+ * sign-up — with the values it typed. Taken once. */
777
+ lastSaved = null;
778
+ takeLastSaved() {
779
+ const saved = this.lastSaved;
780
+ this.lastSaved = null;
781
+ return saved;
782
+ }
759
783
  // ── The test case a recording is for ────────────────────────────────
760
784
  //
761
785
  // A PERSON THINKS IN TEST CASES. The flows are how the assistant makes one
@@ -1181,6 +1205,7 @@ Fix the step that failed (remove_step, then act again) and save again \u2014 or
1181
1205
  const note = others.length ? `
1182
1206
  Claude has ${others.length} other open branch${others.length === 1 ? "" : "es"} on this project \u2014 gather_branches puts them into this one, for one review.` : "";
1183
1207
  const after = draft.after?.length ? ` It starts after ${draft.after.map((a) => `"${a.name}"`).join(", ")}.` : "";
1208
+ this.lastSaved = { projectId: draft.projectId, ...shapeOf(draft, this.typed) };
1184
1209
  this.dropDraft(draft.projectId, draft.name);
1185
1210
  const live = this.liveCase;
1186
1211
  if (live && live.projectId === draft.projectId) {
@@ -1495,12 +1520,36 @@ THE MODEL \u2014 get this right before recording anything:
1495
1520
  write_test_cases. To automate one, record the flows its steps need, then
1496
1521
  save_case with the SAME name in the SAME folder: that fills it, and its
1497
1522
  business steps stay. Its Then lines are the checks to switch on.
1498
- - SIGN-IN is shared setup: a flow or case that needs somebody signed in uses a
1499
- sign-in profile from the catalog (start_flow's signIn), never typed login steps.
1500
- None yet? Make it: record the login as a flow that ends with a check proving
1501
- it worked (the account menu, a heading, the URL), then create_sign_in from it
1502
- and create_account for each person who signs in (a customer, an admin). The
1503
- login flow types into its fields; the account supplies the values.
1523
+ - AUTHENTICATION IS YOURS, END TO END. The person records nothing by hand and
1524
+ is not asked to set sign-in up. A flow or case that needs somebody signed in
1525
+ starts signed in with a sign-in profile (start_flow's signIn) \u2014 never typed
1526
+ login steps. Use one from project_catalog made on the SAME platform, with an
1527
+ account for the role. None yet? Make it, without asking:
1528
+ 1. A test account, in this order: credentials the person gave you; a demo
1529
+ login the app itself shows; or SIGN UP a dedicated test user yourself \u2014 a
1530
+ fixed address for the role (e.g. flowriki.customer@ the domain of the
1531
+ project's mailbox helper) and a strong password you invent (it is sealed
1532
+ in the vault). Sign it up once; it is reused by every run.
1533
+ 2. Record the login as a flow on that platform that ends with a check proving
1534
+ it worked (the account menu, a heading, the URL), then create_sign_in from
1535
+ it \u2014 one per platform per role, e.g. "Customer sign-in" \u2014 and
1536
+ create_account with the test user's values.
1537
+ 3. Only what you cannot create yourself \u2014 an admin, a role only staff can
1538
+ grant, a mailbox when sign-up must verify an email and the catalog has no
1539
+ mailbox helper \u2014 is asked of the person: once, saying exactly what you
1540
+ need, while you carry on with everything else.
1541
+ - AUTHENTICATION IS ALSO A PRODUCT AREA TO TEST. Asked to test sign-up,
1542
+ sign-in, accounts or security, cover it as a tester would \u2014 each its own
1543
+ test case, in folders such as "Account \u2013 Sign up", "Account \u2013 Sign in":
1544
+ sign up (valid; an email already taken; a weak password; required fields
1545
+ missing), sign in (valid; wrong password; unknown email; empty fields),
1546
+ password reset (request it, set a new one through the mailbox helper),
1547
+ sign out (and Back cannot return to a signed-in page), a signed-in page
1548
+ opened while signed out goes to sign-in, a session survives a reload,
1549
+ lockout or throttling after repeated wrong passwords (stop at the limit plus
1550
+ one), and roles (a customer cannot reach an admin page). Test the app's own
1551
+ behaviour only: no attacks, no load, no other systems, no real person's
1552
+ account.
1504
1553
  - HELPERS (an inbox for email codes, an authenticator, SMS, an API call) are made
1505
1554
  with create_helper when the catalog has none that fits.
1506
1555
  - SEVERAL ELEMENTS WITH THE SAME NAME (two "Proceed to Checkout" buttons, twelve
@@ -1510,8 +1559,9 @@ THE MODEL \u2014 get this right before recording anything:
1510
1559
  - PASSWORDS AND KEYS never go into a flow or a reply. Typed into a password
1511
1560
  field, a value is kept as a secret and saved to the project's vault; given to
1512
1561
  create_account or create_helper, it is sealed there too and never shown back.
1513
- Only use credentials the person gave you or that the app itself shows for
1514
- testing (a demo login) \u2014 never guess or reuse anyone's real password.
1562
+ Only use credentials the person gave you, that the app itself shows for
1563
+ testing (a demo login), or of a test user you signed up yourself \u2014 never
1564
+ guess or reuse anyone's real password.
1515
1565
  - FOLDERS do not nest. Name each one after the product area and what is in it,
1516
1566
  the way a tester would look for it \u2014 "Account \u2013 Sign up", "Account \u2013 Sign in",
1517
1567
  "Ordering \u2013 Pizza builder", "Checkout \u2013 Payment". Flows (features) and the
@@ -1698,6 +1748,33 @@ function authoringTools(service, session) {
1698
1748
  const lines = session.caseLinesFor(spec.name);
1699
1749
  return lines ? { ...spec, businessSteps: lines } : spec;
1700
1750
  };
1751
+ const signInFrom = async (shape) => {
1752
+ if (!shape?.kind) return "";
1753
+ const NAME = { web: "Web", android: "Android", ios: "iOS" };
1754
+ if (shape.kind === "signup") {
1755
+ return `
1756
+
1757
+ That signed up a fixed test user. Next: record the login as that user on ${NAME[shape.platform]} (a flow that types its email or username and password, then checks it is signed in) \u2014 saving it sets sign-in up for the project.`;
1758
+ }
1759
+ try {
1760
+ const catalog = await catalogOf(shape.projectId, true);
1761
+ const same = catalog.signIns.find((si) => (si.platform ?? "web") === shape.platform);
1762
+ if (same) return `
1763
+
1764
+ ${NAME[shape.platform]} already has the sign-in "${same.name}" (profileId ${same.profileId}) \u2014 use it with start_flow signIn.`;
1765
+ const made = await service.createSignIn(shape.projectId, { flow: shape.flowName, name: `${NAME[shape.platform]} sign-in` });
1766
+ const values = Object.fromEntries(made.fields.flatMap((f) => shape.values[f.label] !== void 0 ? [[f.label, shape.values[f.label]]] : []));
1767
+ const account = await service.createAccount(shape.projectId, { signIn: made.profileId, name: "Test user", values });
1768
+ catalogs.delete(shape.projectId);
1769
+ return `
1770
+
1771
+ This is a login, so it is now the project's sign-in on ${NAME[shape.platform]}: "${made.name}" (profileId ${made.profileId}) with the account "${account.name}" (accountId ${account.accountId}; its secrets are in the vault). Every flow that needs somebody signed in starts with start_flow signIn {"profileId": "${made.profileId}", "accountId": "${account.accountId}"}.`;
1772
+ } catch (error) {
1773
+ return `
1774
+
1775
+ It looks like a login, but sign-in could not be set up from it (${error.message.split("\n")[0]}) \u2014 create_sign_in and create_account can do it.`;
1776
+ }
1777
+ };
1701
1778
  const tools = [
1702
1779
  {
1703
1780
  name: "list_projects",
@@ -2027,10 +2104,13 @@ ${page}`);
2027
2104
  },
2028
2105
  additionalProperties: false
2029
2106
  },
2030
- run: async (args) => text(await session.save(str(args["title"]), {
2031
- ...str(args["branch"]) ? { changeId: str(args["branch"]) } : {},
2032
- ...args["newBranch"] === true ? { newBranch: true } : {}
2033
- }, { ...args["force"] === true ? { force: true } : {} }))
2107
+ run: async (args) => {
2108
+ const saved = await session.save(str(args["title"]), {
2109
+ ...str(args["branch"]) ? { changeId: str(args["branch"]) } : {},
2110
+ ...args["newBranch"] === true ? { newBranch: true } : {}
2111
+ }, { ...args["force"] === true ? { force: true } : {} });
2112
+ return text(saved + await signInFrom(session.takeLastSaved()));
2113
+ }
2034
2114
  },
2035
2115
  {
2036
2116
  name: "list_my_branches",
@@ -46,7 +46,7 @@ import {
46
46
  textOf,
47
47
  toolEnv,
48
48
  typesSecrets
49
- } from "./chunk-YCFTNJQU.js";
49
+ } from "./chunk-IFUODU4W.js";
50
50
  export {
51
51
  AGENT_HOME,
52
52
  APPIUM_DIR,
package/dist/engine.js CHANGED
@@ -9,8 +9,8 @@ import {
9
9
  resolveCheckouts,
10
10
  start,
11
11
  status
12
- } from "./chunk-GPSLXDGQ.js";
13
- import "./chunk-YCFTNJQU.js";
12
+ } from "./chunk-QIZZ4HZ7.js";
13
+ import "./chunk-IFUODU4W.js";
14
14
  export {
15
15
  AGENT_VERSION,
16
16
  browsersReady,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowriki/agent",
3
- "version": "0.1.0-beta.62",
3
+ "version": "0.1.0-beta.63",
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",