@flowriki/agent 0.1.0-beta.10 → 0.1.0-beta.12

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.10";
9
+ var AGENT_VERSION = "0.1.0-beta.12";
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-XE4AJTQE.js
138
+ // ../contracts/dist/chunk-BU7BOLBB.js
139
139
  import { z } from "zod";
140
140
  var PROTOCOL_VERSION = 1;
141
141
  var PAIRING_CODE_TTL_MS = 5 * 60 * 1e3;
@@ -148,6 +148,7 @@ var CommandNameSchema = z.enum([
148
148
  "record.dialog",
149
149
  "flow.run",
150
150
  "case.run",
151
+ "case.debug",
151
152
  "flow.save",
152
153
  "flow.update",
153
154
  "flow.delete",
@@ -180,6 +181,7 @@ var EFFECTFUL = /* @__PURE__ */ new Set([
180
181
  "record.dialog",
181
182
  "flow.run",
182
183
  "case.run",
184
+ "case.debug",
183
185
  "flow.save",
184
186
  "flow.update",
185
187
  "flow.delete",
@@ -889,6 +891,7 @@ var TestCaseSchema = z22.object({
889
891
  /** Where the case itself begins. The first row continues from here unless it
890
892
  * carries its own navigation. */
891
893
  startPath: z22.string().default("/"),
894
+ useStartPath: z22.boolean().default(false),
892
895
  baseUrl: z22.string().url().optional(),
893
896
  environment: ExecutionEnvironmentSchema.optional(),
894
897
  occurrences: z22.array(FlowOccurrenceSchema).default([]),
@@ -964,6 +967,63 @@ var SuiteSchema = z22.object({
964
967
  seen.add(entry.entryId);
965
968
  });
966
969
  });
970
+ function caseOccurrences(testCase) {
971
+ return testCase.occurrences.map((item, index) => index === 0 && testCase.useStartPath && !item.navigateFirst ? { ...item, startPath: testCase.startPath, ...testCase.baseUrl ? { baseUrl: testCase.baseUrl } : {} } : item);
972
+ }
973
+ function caseDebugPlan(testCase, baseUrl) {
974
+ const owners = {};
975
+ const steps = [];
976
+ const occurrences = caseOccurrences(testCase);
977
+ for (const [index, item] of occurrences.entries()) {
978
+ if (index > 0 && item.navigateFirst) {
979
+ const id = `case-nav:${encodeURIComponent(item.occurrenceId)}`;
980
+ owners[id] = { occurrenceId: item.occurrenceId, stepId: null };
981
+ steps.push({ id, type: "navigate", role: "", name: `Go to ${item.name}`, scope: "", value: new URL(item.startPath ?? "/", item.baseUrl ?? testCase.baseUrl ?? baseUrl).href, secret: false, legacySelector: "", proposals: [] });
982
+ }
983
+ for (const step of item.steps) {
984
+ const id = `case-step:${encodeURIComponent(item.occurrenceId)}:${encodeURIComponent(step.id)}`;
985
+ owners[id] = { occurrenceId: item.occurrenceId, stepId: step.id };
986
+ steps.push({ ...step, id });
987
+ }
988
+ }
989
+ const first = occurrences[0];
990
+ const flow = FlowDocumentSchema.parse({
991
+ schemaVersion: 5,
992
+ id: testCase.id,
993
+ featureId: testCase.folderId,
994
+ slug: testCase.slug,
995
+ name: testCase.name,
996
+ revision: testCase.revision,
997
+ recordedAt: testCase.createdAt,
998
+ recordedBy: testCase.createdBy,
999
+ updatedAt: testCase.updatedAt,
1000
+ steps,
1001
+ startPath: new URL(first?.startPath ?? "/", first?.baseUrl ?? testCase.baseUrl ?? baseUrl).href,
1002
+ ...testCase.environment ? { environment: testCase.environment } : {}
1003
+ });
1004
+ return { flow, owners };
1005
+ }
1006
+ function restoreDebugCase(testCase, owners, steps) {
1007
+ const occurrences = testCase.occurrences.map((item) => ({ ...item, steps: [] }));
1008
+ const byId = new Map(occurrences.map((item) => [item.occurrenceId, item]));
1009
+ let current = occurrences[0];
1010
+ const present = new Set(steps.map((step) => step.id));
1011
+ for (const [id, owner] of Object.entries(owners)) {
1012
+ if (owner.stepId === null && !present.has(id)) byId.get(owner.occurrenceId).navigateFirst = false;
1013
+ }
1014
+ for (const step of steps) {
1015
+ const owner = owners[step.id];
1016
+ if (owner) current = byId.get(owner.occurrenceId);
1017
+ if (!current) throw new Error("debug step has no case occurrence");
1018
+ if (owner?.stepId === null) {
1019
+ if (step.type !== "navigate") throw new Error("a case boundary must remain a navigation step");
1020
+ current.startPath = step.value;
1021
+ continue;
1022
+ }
1023
+ current.steps.push({ ...step, id: owner?.stepId ?? step.id });
1024
+ }
1025
+ return { ...testCase, occurrences };
1026
+ }
967
1027
 
968
1028
  // ../recorder/dist/index.js
969
1029
  function openShadowRoots() {
@@ -3785,7 +3845,7 @@ async function runStepsIn(session, steps, opts) {
3785
3845
  const evidence = await captureFailure({
3786
3846
  page,
3787
3847
  dir: evidenceDir,
3788
- ordinal: results.length + 1,
3848
+ ordinal: (opts.stepOffset ?? 0) + results.length + 1,
3789
3849
  steps
3790
3850
  });
3791
3851
  if (evidence) result.evidence = evidence;
@@ -4010,6 +4070,7 @@ async function replayCase(opts) {
4010
4070
  const navigates = index === 0 || occurrence.navigateFirst;
4011
4071
  const at0 = Date.now();
4012
4072
  const { results, status: ran, error: failed } = await runStepsIn(session, occurrence.steps, {
4073
+ stepOffset: outcomes.reduce((count, previous) => count + previous.stepsTotal, 0),
4013
4074
  ...opts.resolveSecret === void 0 ? {} : { resolveSecret: opts.resolveSecret },
4014
4075
  ...opts.resolveFile === void 0 ? {} : { resolveFile: opts.resolveFile },
4015
4076
  ...opts.evidenceDir === void 0 ? {} : { evidenceDir: opts.evidenceDir },
@@ -4041,6 +4102,10 @@ async function replayCase(opts) {
4041
4102
  passed: status2 === "passed"
4042
4103
  });
4043
4104
  }
4105
+ if (opts.signal?.aborted) {
4106
+ status2 = "errored";
4107
+ error = "the run was cancelled";
4108
+ }
4044
4109
  const steps = outcomes.flatMap((o) => o.steps);
4045
4110
  return {
4046
4111
  status: status2,
@@ -6344,6 +6409,7 @@ async function executeCase(opts) {
6344
6409
  const { testCase, config, secrets, storage = null, trigger, baseUrl } = opts;
6345
6410
  const headless = opts.watch ? false : opts.headless ?? config.headless ?? true;
6346
6411
  const stepsTotal = testCase.occurrences.reduce((n2, o) => n2 + o.steps.length, 0);
6412
+ if (stepsTotal === 0) throw new Error("this test case has no recorded steps to run");
6347
6413
  const run = storage ? await storage.createRun({
6348
6414
  ...opts.projectId ? { projectId: opts.projectId } : {},
6349
6415
  kind: "test",
@@ -6388,44 +6454,70 @@ async function executeCase(opts) {
6388
6454
  );
6389
6455
  }
6390
6456
  }
6391
- const byStepId = new Map(testCase.occurrences.flatMap((o) => o.steps.map((s2) => [s2.id, s2])));
6457
+ const stepsByOccurrence = new Map(testCase.occurrences.map((o) => [o.occurrenceId, new Map(o.steps.map((s2) => [s2.id, s2]))]));
6392
6458
  const ordinals = /* @__PURE__ */ new Map();
6393
6459
  const writes = [];
6394
- const outcome = await replayCase({
6395
- occurrences: testCase.occurrences,
6396
- baseUrl,
6397
- ...testCase.environment ? { environment: testCase.environment } : {},
6398
- headless,
6399
- ...opts.watch ? { slowMo: 400 } : {},
6400
- resolveSecret: (ref) => secrets[ref],
6401
- ...opts.resolveFile ? { resolveFile: opts.resolveFile } : {},
6402
- ...config.artifacts ? { artifacts: config.artifacts } : {},
6403
- ...runDir ? { evidenceDir: runDir } : {},
6404
- ...opts.signal ? { signal: opts.signal } : {},
6405
- onStepResult: (result, occurrenceId) => {
6406
- const taskId = taskFor.get(occurrenceId);
6407
- if (storage && taskId && run) {
6408
- const at = ordinals.get(occurrenceId) ?? 0;
6409
- ordinals.set(occurrenceId, at + 1);
6460
+ let outcome;
6461
+ try {
6462
+ outcome = await replayCase({
6463
+ occurrences: caseOccurrences(testCase),
6464
+ baseUrl: testCase.baseUrl ?? baseUrl,
6465
+ ...testCase.environment ? { environment: testCase.environment } : {},
6466
+ headless,
6467
+ ...opts.watch ? { slowMo: 400 } : {},
6468
+ resolveSecret: (ref) => secrets[ref],
6469
+ ...opts.resolveFile ? { resolveFile: opts.resolveFile } : {},
6470
+ ...config.artifacts ? { artifacts: config.artifacts } : {},
6471
+ ...runDir ? { evidenceDir: runDir } : {},
6472
+ ...opts.signal ? { signal: opts.signal } : {},
6473
+ onStepResult: (result, occurrenceId) => {
6474
+ const taskId = taskFor.get(occurrenceId);
6475
+ if (storage && taskId && run) {
6476
+ const at = ordinals.get(occurrenceId) ?? 0;
6477
+ ordinals.set(occurrenceId, at + 1);
6478
+ writes.push(
6479
+ storage.recordStep(taskId, toStepRow(at, result, stepsByOccurrence.get(occurrenceId)?.get(result.stepId))).then(async (stepRowId) => {
6480
+ const shot = result.evidence?.screenshot;
6481
+ if (!shot) return;
6482
+ await storage.addArtifact(run.id, {
6483
+ runStepId: stepRowId,
6484
+ kind: "screenshot",
6485
+ relPath: shot,
6486
+ bytes: null,
6487
+ masked: true
6488
+ });
6489
+ })
6490
+ );
6491
+ }
6410
6492
  writes.push(
6411
- storage.recordStep(taskId, toStepRow(at, result, byStepId.get(result.stepId))).then(async (stepRowId) => {
6412
- const shot = result.evidence?.screenshot;
6413
- if (!shot) return;
6414
- await storage.addArtifact(run.id, {
6415
- runStepId: stepRowId,
6416
- kind: "screenshot",
6417
- relPath: shot,
6418
- bytes: null,
6419
- masked: true
6420
- });
6421
- })
6493
+ emit({ type: "case.step", runId: run?.id ?? "", caseId: testCase.id, occurrenceId, result })
6422
6494
  );
6423
6495
  }
6424
- writes.push(
6425
- emit({ type: "case.step", runId: run?.id ?? "", caseId: testCase.id, occurrenceId, result })
6426
- );
6427
- }
6428
- });
6496
+ });
6497
+ } catch (failure) {
6498
+ const error = failure instanceof Error ? failure.message : String(failure);
6499
+ outcome = {
6500
+ status: "errored",
6501
+ error,
6502
+ artifacts: [],
6503
+ durationMs: 0,
6504
+ steps: [],
6505
+ stepsRun: 0,
6506
+ stepsTotal,
6507
+ warnings: [],
6508
+ occurrences: testCase.occurrences.map((item, index) => ({
6509
+ occurrenceId: item.occurrenceId,
6510
+ name: item.name,
6511
+ status: index === 0 ? "errored" : "skipped",
6512
+ error: index === 0 ? error : "browser could not start",
6513
+ steps: [],
6514
+ stepsRun: 0,
6515
+ stepsTotal: item.steps.length,
6516
+ durationMs: 0,
6517
+ continued: false
6518
+ }))
6519
+ };
6520
+ }
6429
6521
  await Promise.all(writes);
6430
6522
  if (storage && run) {
6431
6523
  for (const each of outcome.occurrences) {
@@ -6477,6 +6569,20 @@ var RecordingCoordinator = class {
6477
6569
  starting = null;
6478
6570
  draft = null;
6479
6571
  stopInFlight = null;
6572
+ /** The last completed stop, kept so a caller that missed the answer can ask
6573
+ * again.
6574
+ *
6575
+ * IN MEMORY AND NEVER ON THE EVENT STREAM. The recording.stopped event is
6576
+ * journalled and replayed to the browser, and this result carries the
6577
+ * passwords the recording captured — so recovery reads it from here rather
6578
+ * than rebuilding it from the event, which would either lose the values or
6579
+ * publish them. */
6580
+ lastResult = null;
6581
+ /** What the last completed stop answered, for a caller that missed it —
6582
+ * a dropped socket, or a Stop pressed inside the recorded page. */
6583
+ lastStop() {
6584
+ return this.lastResult;
6585
+ }
6480
6586
  active() {
6481
6587
  if (!this.session || !this.draft) return null;
6482
6588
  return { sessionId: this.draft.name, steps: this.draft.steps.length };
@@ -6629,7 +6735,13 @@ var RecordingCoordinator = class {
6629
6735
  this.draft = null;
6630
6736
  return { ok: false, error: "the feature this recording belongs to is gone" };
6631
6737
  }
6738
+ const before = this.deps.store.readSecrets();
6632
6739
  this.deps.store.extractSecrets(slugify(draft.name), steps);
6740
+ const after = this.deps.store.readSecrets();
6741
+ const captured = {};
6742
+ for (const [ref, value] of Object.entries(after)) {
6743
+ if (before[ref] !== value) captured[ref] = value;
6744
+ }
6633
6745
  const doc = {
6634
6746
  schemaVersion: CURRENT_SCHEMA_VERSION,
6635
6747
  id: `flw_${crypto3.randomUUID().slice(0, 8)}`,
@@ -6650,7 +6762,7 @@ var RecordingCoordinator = class {
6650
6762
  this.draft = null;
6651
6763
  const saved = { ...this.deps.store.getFlow(doc.id) ?? doc, path: file };
6652
6764
  this.deps.emit({ type: "recording.stopped", flow: saved, file });
6653
- return { ok: true, flow: saved };
6765
+ return this.lastResult = { ok: true, flow: saved, secrets: captured };
6654
6766
  }
6655
6767
  /** Throw the recording away: close the browser, drop the draft, write
6656
6768
  * nothing.
@@ -6708,14 +6820,20 @@ var DebugCoordinator = class {
6708
6820
  this.deps = deps;
6709
6821
  }
6710
6822
  deps;
6823
+ retainCompleted = false;
6824
+ lastState = null;
6825
+ lastEnd = null;
6711
6826
  current = null;
6712
6827
  status() {
6713
- return this.current ? { flowId: this.current.flowId, state: this.current.session.state() } : { flowId: null, state: null };
6828
+ return this.current ? { flowId: this.current.flowId, state: this.current.session.state() } : this.lastEnd?.ok && this.lastEnd.flow && this.lastState ? { flowId: this.lastEnd.flow.id, state: this.lastState } : { flowId: null, state: null };
6714
6829
  }
6715
6830
  active() {
6716
6831
  return this.current !== null;
6717
6832
  }
6718
6833
  async start(flowId, opts = {}) {
6834
+ this.retainCompleted = opts.retainCompleted === true;
6835
+ this.lastState = null;
6836
+ this.lastEnd = null;
6719
6837
  const store = this.deps.store;
6720
6838
  const flow = store.getFlow(flowId);
6721
6839
  if (!flow) throw new FlowNotFound("flow not found");
@@ -6794,7 +6912,14 @@ var DebugCoordinator = class {
6794
6912
  * panel's own buttons — so all of them take the same path, including secret
6795
6913
  * extraction, which must never be skipped by one of several entry points. */
6796
6914
  async end(commit) {
6797
- if (!this.current) return { ok: false, error: "no debug session is running" };
6915
+ if (!this.current) {
6916
+ if (!commit && this.lastEnd) {
6917
+ this.lastEnd = null;
6918
+ this.lastState = null;
6919
+ return { ok: true, committed: false };
6920
+ }
6921
+ return this.lastEnd ?? { ok: false, error: "no debug session is running" };
6922
+ }
6798
6923
  const { flowId, session } = this.current;
6799
6924
  this.current = null;
6800
6925
  this.deps.lease?.beginRelease("debug");
@@ -6804,6 +6929,7 @@ var DebugCoordinator = class {
6804
6929
  this.deps.emit({ type: "debug.ended", flowId, committed: false });
6805
6930
  return { ok: true, committed: false };
6806
6931
  }
6932
+ this.lastState = this.retainCompleted ? { ...session.state(), status: "finished" } : null;
6807
6933
  const steps = await session.finish();
6808
6934
  const flow = this.deps.store.getFlow(flowId);
6809
6935
  if (!flow) return { ok: false, error: "flow not found" };
@@ -6818,7 +6944,7 @@ var DebugCoordinator = class {
6818
6944
  }
6819
6945
  const updated2 = { ...flow, steps, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
6820
6946
  this.deps.emit({ type: "debug.ended", flowId, committed: true });
6821
- return { ok: true, committed: true, flow: updated2, secrets: captured };
6947
+ return this.lastEnd = { ok: true, committed: true, flow: updated2, secrets: captured };
6822
6948
  }
6823
6949
  const { store } = this.deps.commit;
6824
6950
  store.extractSecrets(flow.slug, steps);
@@ -7875,6 +8001,8 @@ function executionHandlers(ctx) {
7875
8001
  onActivity?.({ kind: "run", projectId: project.id, detail: flow.name });
7876
8002
  const aborter = new AbortController();
7877
8003
  cancels.set(command.id, { projectId: project.id, cancel: () => aborter.abort() });
8004
+ const cancelKey = command.args["cancelKey"];
8005
+ if (typeof cancelKey === "string" && cancelKey.startsWith("suite:")) cancels.set(cancelKey, cancels.get(command.id));
7878
8006
  let runId = null;
7879
8007
  try {
7880
8008
  const outcome = await executeFlow({
@@ -7913,6 +8041,7 @@ function executionHandlers(ctx) {
7913
8041
  throw err;
7914
8042
  } finally {
7915
8043
  cancels.delete(command.id);
8044
+ if (typeof cancelKey === "string" && cancelKey.startsWith("suite:")) cancels.delete(cancelKey);
7916
8045
  if (runId) cancels.delete(runId);
7917
8046
  onActivity?.(null);
7918
8047
  lease?.release("run");
@@ -7941,10 +8070,13 @@ function executionHandlers(ctx) {
7941
8070
  onActivity?.({ kind: "run", projectId: project.id, detail: testCase.name });
7942
8071
  const aborter = new AbortController();
7943
8072
  cancels.set(command.id, { projectId: project.id, cancel: () => aborter.abort() });
8073
+ const cancelKey = command.args["cancelKey"];
8074
+ if (typeof cancelKey === "string" && cancelKey.startsWith("suite:")) cancels.set(cancelKey, cancels.get(command.id));
7944
8075
  let runId = null;
7945
8076
  try {
7946
8077
  const { outcome, run } = await executeCase({
7947
8078
  testCase,
8079
+ projectId: project.id,
7948
8080
  config: command.args["config"] ?? {},
7949
8081
  secrets: command.args["secrets"] ?? {},
7950
8082
  baseUrl: String(command.args["baseUrl"] ?? ""),
@@ -7976,6 +8108,7 @@ function executionHandlers(ctx) {
7976
8108
  };
7977
8109
  } finally {
7978
8110
  cancels.delete(command.id);
8111
+ if (typeof cancelKey === "string" && cancelKey.startsWith("suite:")) cancels.delete(cancelKey);
7979
8112
  if (runId) cancels.delete(runId);
7980
8113
  onActivity?.(null);
7981
8114
  lease?.release("run");
@@ -8040,7 +8173,10 @@ function recordingHandlers({ recorder, onActivity }) {
8040
8173
  try {
8041
8174
  const saved = await recorder.stop(project.id);
8042
8175
  if (!saved.ok) return { ok: false, reason: "failed", detail: saved.error };
8043
- return { ok: true, result: { flow: saved.flow, projectId: project.id } };
8176
+ return {
8177
+ ok: true,
8178
+ result: { flow: saved.flow, projectId: project.id, secrets: saved.secrets }
8179
+ };
8044
8180
  } finally {
8045
8181
  onActivity?.(null);
8046
8182
  }
@@ -8083,6 +8219,8 @@ function recordingHandlers({ recorder, onActivity }) {
8083
8219
  }
8084
8220
  function debuggingHandlers(ctx) {
8085
8221
  const { debugging, onActivity, deps } = ctx;
8222
+ let sessionProject = null;
8223
+ let caseSession = null;
8086
8224
  return {
8087
8225
  "flow.verify": async (command, project, emit) => {
8088
8226
  const carried = command.args["flow"];
@@ -8120,14 +8258,17 @@ function debuggingHandlers(ctx) {
8120
8258
  // The browser opens on this machine and the panel lives inside that window,
8121
8259
  // so whoever is at the keyboard sees the same state the dashboard does.
8122
8260
  "debug.start": async (command, project) => {
8123
- const carried = command.args["flow"];
8261
+ const document2 = command.args["case"] ? TestCaseSchema.parse(command.args["case"]) : null;
8262
+ const plan = document2 ? caseDebugPlan(document2, String(command.args["baseUrl"] ?? "")) : null;
8263
+ const carried = plan?.flow ?? command.args["flow"];
8124
8264
  if (!carried && !project.root) return NO_CHECKOUT;
8125
8265
  if (!debugging) {
8126
8266
  return { ok: false, reason: "failed", detail: "this agent cannot debug \u2014 no browser is configured" };
8127
8267
  }
8128
8268
  try {
8129
- const started = await debugging.start(String(command.args["flowId"] ?? ""), {
8269
+ const started = await debugging.start(document2?.id ?? String(command.args["flowId"] ?? ""), {
8130
8270
  root: project.root,
8271
+ retainCompleted: Boolean(document2),
8131
8272
  ...command.args["breakpoints"] ? { breakpoints: command.args["breakpoints"] } : {},
8132
8273
  ...command.args["watch"] === void 0 ? {} : { watch: Boolean(command.args["watch"]) },
8133
8274
  ...carried ? {
@@ -8139,6 +8280,8 @@ function debuggingHandlers(ctx) {
8139
8280
  }
8140
8281
  } : {}
8141
8282
  });
8283
+ sessionProject = project.id;
8284
+ caseSession = document2 && plan ? { document: document2, changeId: command.args["caseChangeId"], owners: plan.owners } : null;
8142
8285
  onActivity?.({ kind: "debugging", projectId: project.id, detail: started.flowId });
8143
8286
  return { ok: true, result: started };
8144
8287
  } catch (err) {
@@ -8150,11 +8293,12 @@ function debuggingHandlers(ctx) {
8150
8293
  },
8151
8294
  // A read, so it stays out of EFFECTFUL: it is how a dashboard opened
8152
8295
  // mid-session catches up rather than showing a stale half-run.
8153
- "debug.status": async () => ({
8296
+ "debug.status": async (_command, project) => ({
8154
8297
  ok: true,
8155
- result: debugging?.status() ?? { flowId: null, state: null }
8298
+ result: sessionProject === null || sessionProject === project.id ? debugging?.status() ?? { flowId: null, state: null } : { flowId: null, state: null }
8156
8299
  }),
8157
- "debug.command": async (command) => {
8300
+ "debug.command": async (command, project) => {
8301
+ if (sessionProject !== null && sessionProject !== project.id) return { ok: false, reason: "not_authorised", detail: "this debug session belongs to another project" };
8158
8302
  if (!debugging) return { ok: false, reason: "failed", detail: "no debug session is running" };
8159
8303
  try {
8160
8304
  const state = await debugging.command(String(command.args["command"] ?? ""), {
@@ -8168,11 +8312,17 @@ function debuggingHandlers(ctx) {
8168
8312
  return { ok: false, reason: "failed", detail: String(err.message) };
8169
8313
  }
8170
8314
  },
8171
- "debug.end": async (command) => {
8315
+ "debug.end": async (command, project) => {
8316
+ if (command.args["caseId"] !== void 0 && command.args["caseId"] !== caseSession?.document.id) return { ok: false, reason: "failed", detail: "this debug session belongs to another test" };
8317
+ if (sessionProject !== null && sessionProject !== project.id) return { ok: false, reason: "not_authorised", detail: "this debug session belongs to another project" };
8172
8318
  if (!debugging) return { ok: false, reason: "failed", detail: "no debug session is running" };
8173
8319
  try {
8174
8320
  const res = await debugging.end(command.args["commit"] !== false);
8175
8321
  if (!res.ok) return { ok: false, reason: "failed", detail: res.error ?? "no debug session is running" };
8322
+ if (caseSession && res.flow) {
8323
+ const testCase = restoreDebugCase(caseSession.document, caseSession.owners, FlowDocumentSchema.parse(res.flow).steps);
8324
+ return { ok: true, result: { ...res, flow: void 0, testCase, caseChangeId: caseSession.changeId } };
8325
+ }
8176
8326
  return { ok: true, result: res };
8177
8327
  } finally {
8178
8328
  onActivity?.(null);
@@ -8194,13 +8344,15 @@ function buildHandlers(deps) {
8194
8344
  artifactLimit: deps.artifactLimitBytes ?? 25 * 1024 * 1024,
8195
8345
  cancels: /* @__PURE__ */ new Map()
8196
8346
  };
8347
+ const debug = debuggingHandlers(context);
8197
8348
  return {
8349
+ "case.debug": debug["debug.start"],
8198
8350
  ...repositoryHandlers(context),
8199
8351
  ...bindingHandlers(context),
8200
8352
  ...historyHandlers(context),
8201
8353
  ...executionHandlers(context),
8202
8354
  ...recordingHandlers(context),
8203
- ...debuggingHandlers(context)
8355
+ ...debug
8204
8356
  };
8205
8357
  }
8206
8358
  function createAgentRuntime(options) {
@@ -8217,7 +8369,7 @@ function createAgentRuntime(options) {
8217
8369
  lease,
8218
8370
  store: new Store(projectRoot),
8219
8371
  emit: (event) => {
8220
- if (event.type === "recording.stopped") lastRecording = { ok: true, flow: event.flow };
8372
+ if (event.type === "recording.stopped") lastRecording = { ok: true, flow: event.flow, secrets: {} };
8221
8373
  journal?.append({ name: `recording.${event.type.replace(/^recording\./, "")}`, data: event });
8222
8374
  }
8223
8375
  });
@@ -8274,7 +8426,6 @@ function createAgentRuntime(options) {
8274
8426
  end: async (commit) => {
8275
8427
  if (!debuggingIn) return { ok: false, error: "no debug session is running" };
8276
8428
  const res = await debuggingIn.end(commit);
8277
- debuggingIn = null;
8278
8429
  return res;
8279
8430
  },
8280
8431
  active: () => Boolean(debuggingIn?.active())
@@ -8305,8 +8456,9 @@ function createAgentRuntime(options) {
8305
8456
  },
8306
8457
  stop: async (projectId) => {
8307
8458
  if (projectId && recordingProject && projectId !== recordingProject) return { ok: false, error: "This recording belongs to another project" };
8308
- if (lastRecording?.ok) return lastRecording;
8309
- if (!recordingIn) return lastRecording ?? { ok: false, error: "no recording is in progress on this machine" };
8459
+ const remembered = recordingIn?.lastStop() ?? lastRecording;
8460
+ if (remembered?.ok) return remembered;
8461
+ if (!recordingIn) return remembered ?? { ok: false, error: "no recording is in progress on this machine" };
8310
8462
  const result = await recordingIn.stop();
8311
8463
  lastRecording = result;
8312
8464
  return result;
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  resolveCheckouts,
9
9
  start,
10
10
  status
11
- } from "./chunk-O5HHREII.js";
11
+ } from "./chunk-YIZ6E6DB.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-O5HHREII.js";
12
+ } from "./chunk-YIZ6E6DB.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.10",
3
+ "version": "0.1.0-beta.12",
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",