@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.
- package/dist/{chunk-O5HHREII.js → chunk-YIZ6E6DB.js} +204 -52
- package/dist/cli.js +1 -1
- package/dist/engine.js +1 -1
- package/package.json +1 -1
|
@@ -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.
|
|
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-
|
|
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
|
|
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
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
const
|
|
6409
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6425
|
-
|
|
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)
|
|
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 {
|
|
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
|
|
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
|
-
...
|
|
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
|
-
|
|
8309
|
-
if (
|
|
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
package/dist/engine.js
CHANGED
package/package.json
CHANGED