@flowriki/agent 0.1.0-beta.5 → 0.1.0-beta.6

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.
@@ -4994,12 +4994,19 @@ function redactPanel(state) {
4994
4994
  if (!Array.isArray(panel.steps)) return state;
4995
4995
  return { ...panel, steps: panel.steps.map((s) => redact(s)) };
4996
4996
  }
4997
+ var START_GRACE_MS = 3e3;
4997
4998
  var RecordingCoordinator = class {
4998
4999
  constructor(deps) {
4999
5000
  this.deps = deps;
5000
5001
  }
5001
5002
  deps;
5002
5003
  session = null;
5004
+ /** The browser opening, while `start` has already answered.
5005
+ *
5006
+ * Held so that stop and status can wait for it rather than racing it: a
5007
+ * recording asked to stop a second after it was asked to start must not find
5008
+ * a half-built session. */
5009
+ starting = null;
5003
5010
  draft = null;
5004
5011
  stopInFlight = null;
5005
5012
  active() {
@@ -5008,7 +5015,10 @@ var RecordingCoordinator = class {
5008
5015
  }
5009
5016
  status() {
5010
5017
  return {
5011
- active: Boolean(this.session),
5018
+ // A browser still opening counts as active: `start` has answered, the
5019
+ // draft exists, and telling the dashboard "nothing is recording" in that
5020
+ // window is what makes a Stop pressed early do nothing at all.
5021
+ active: Boolean(this.session) || Boolean(this.draft && this.starting),
5012
5022
  featureId: this.draft?.featureId ?? null,
5013
5023
  name: this.draft?.name ?? null,
5014
5024
  steps: (this.draft?.steps ?? []).map(redact),
@@ -5047,6 +5057,30 @@ var RecordingCoordinator = class {
5047
5057
  startPath: "/",
5048
5058
  recordedBy: recordedBy?.trim() || ""
5049
5059
  };
5060
+ let failure = null;
5061
+ this.starting = this.open(target, opts).catch((error) => {
5062
+ failure = error;
5063
+ this.draft = null;
5064
+ this.session = null;
5065
+ this.deps.lease?.release("recording");
5066
+ this.deps.emit({ type: "recording.interrupted", reason: String(error.message).split("\n")[0] });
5067
+ return null;
5068
+ });
5069
+ await Promise.race([this.starting, new Promise((r) => setTimeout(r, START_GRACE_MS).unref?.())]);
5070
+ if (failure) throw failure;
5071
+ return { url: target };
5072
+ }
5073
+ /** Resolves once the browser is open, or the attempt has failed.
5074
+ *
5075
+ * For callers that need the session itself rather than the answer — the
5076
+ * local server driving a panel, and every test that types into the page. */
5077
+ async ready() {
5078
+ await this.starting?.catch(() => null);
5079
+ }
5080
+ /** Everything that used to happen before `start` answered. */
5081
+ async open(target, opts) {
5082
+ const config = this.deps.store.readConfig();
5083
+ const configuredBase = config.baseUrl;
5050
5084
  try {
5051
5085
  this.session = await startRecordingSession({
5052
5086
  url: target,
@@ -5077,12 +5111,13 @@ var RecordingCoordinator = class {
5077
5111
  throw err;
5078
5112
  }
5079
5113
  this.deps.lease?.activate("recording");
5114
+ if (!this.session || !this.draft) return null;
5080
5115
  const opened = new URL(this.session.startUrl);
5081
5116
  const projectOrigin = configuredBase ? new URL(normalizeStartUrl(configuredBase)).origin : "";
5082
5117
  this.draft.baseUrl = opened.origin === projectOrigin ? "" : opened.origin;
5083
5118
  this.draft.startPath = opened.pathname + opened.search;
5084
5119
  this.deps.emit({ type: "recording.started", name: this.draft.name, url: this.session.startUrl });
5085
- return { url: this.session.startUrl };
5120
+ return null;
5086
5121
  }
5087
5122
  /** onStep fires more than once per step — again when the outcome window
5088
5123
  * resolves and attaches assertions — so this is an upsert, not an append. */
@@ -5114,6 +5149,8 @@ var RecordingCoordinator = class {
5114
5149
  return this.stopInFlight;
5115
5150
  }
5116
5151
  async doStop() {
5152
+ await this.starting?.catch(() => null);
5153
+ this.starting = null;
5117
5154
  const draft = this.draft;
5118
5155
  if (!draft) return { ok: false, error: "no recording in progress" };
5119
5156
  const environment = this.session?.environment;
@@ -5536,8 +5573,26 @@ var CommandSchema = z4.object({
5536
5573
  /** Wall-clock expiry. A command that surfaced from a queue an hour late must
5537
5574
  * not start a browser on somebody's machine. */
5538
5575
  expiresAt: z4.number().int().positive(),
5576
+ /** When the service stamped this, by the service's clock.
5577
+ *
5578
+ * Present so the agent never has to compare a timestamp minted on another
5579
+ * machine against its own clock. `expiresAt` alone made the deadline
5580
+ * meaningless the moment two clocks disagreed: a machine running five
5581
+ * minutes fast rejected every command the instant it arrived, saying it had
5582
+ * expired before it got there — which is true only if you believe both
5583
+ * clocks. Optional, so an older service that does not send it still works;
5584
+ * the agent falls back to the absolute deadline it has always used. */
5585
+ issuedAt: z4.number().int().positive().optional(),
5539
5586
  args: z4.record(z4.unknown()).default({})
5540
- }).strict();
5587
+ // UNKNOWN FIELDS ARE IGNORED, not refused.
5588
+ //
5589
+ // Strictness here looked like caution and was a compatibility trap: the day
5590
+ // the service added `issuedAt`, every agent older than that release began
5591
+ // refusing every command as unrecognised — over a field it had no need to
5592
+ // read. The allowlist on command NAMES is the security boundary and is
5593
+ // unchanged; which extra keys ride along is not, because nothing acts on a
5594
+ // field it does not know about.
5595
+ });
5541
5596
  var AckSchema = z4.object({
5542
5597
  type: z4.literal("ack"),
5543
5598
  commandId: z4.string().min(1),
@@ -5718,7 +5773,7 @@ var AgentState = class {
5718
5773
  if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, v]) => [key, canonical(v)]));
5719
5774
  return value;
5720
5775
  };
5721
- const { expiresAt: _expiry, ...work } = command;
5776
+ const { expiresAt: _expiry, issuedAt: _issued, ...work } = command;
5722
5777
  const text = JSON.stringify(canonical(work));
5723
5778
  if (Buffer.byteLength(text) > 256 * 1024) throw new Error("command exceeds local size limit");
5724
5779
  return crypto22.createHash("sha256").update(text).digest("hex");
@@ -5882,7 +5937,9 @@ var Agent = class {
5882
5937
  }
5883
5938
  const previous = this.previous(command.id, fingerprint2);
5884
5939
  if (previous) return previous;
5885
- if (command.expiresAt <= this.now()) {
5940
+ const ttl = command.issuedAt === void 0 ? null : command.expiresAt - command.issuedAt;
5941
+ const expired = ttl === null ? command.expiresAt <= this.now() : ttl <= 0;
5942
+ if (expired) {
5886
5943
  return this.refuse(command.id, "expired", "the command expired before it reached this machine");
5887
5944
  }
5888
5945
  const handler = this.opts.handlers[command.name];
@@ -6981,7 +7038,7 @@ function openBrowser(url, env = process.env) {
6981
7038
  }
6982
7039
 
6983
7040
  // src/engine.ts
6984
- var AGENT_VERSION = "0.1.0-beta.5";
7041
+ var AGENT_VERSION = "0.1.0-beta.6";
6985
7042
  async function connect(options = {}) {
6986
7043
  let service;
6987
7044
  try {
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  resolveCheckouts,
9
9
  start,
10
10
  status
11
- } from "./chunk-QJOMJVCO.js";
11
+ } from "./chunk-ZUHML4TR.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-QJOMJVCO.js";
12
+ } from "./chunk-ZUHML4TR.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.5",
3
+ "version": "0.1.0-beta.6",
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",