@flowriki/agent 0.1.0-beta.57 → 0.1.0-beta.59
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.
|
@@ -68,7 +68,7 @@ import {
|
|
|
68
68
|
textIndependent,
|
|
69
69
|
validateActors,
|
|
70
70
|
webCloudOf
|
|
71
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-MJXAVVGP.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.
|
|
79
|
+
var AGENT_VERSION = "0.1.0-beta.59";
|
|
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-
|
|
139
|
+
const { mobileDoctor: mobileDoctor2 } = await import("./dist-BB5LTG3H.js");
|
|
140
140
|
return {
|
|
141
141
|
agentVersion: AGENT_VERSION,
|
|
142
142
|
node: process.versions.node,
|
|
@@ -9806,6 +9806,8 @@ function assistantHandlers({ deps }) {
|
|
|
9806
9806
|
...linked.length ? { folders: linked } : {},
|
|
9807
9807
|
...mode ? { mode } : {},
|
|
9808
9808
|
...brief ? { brief } : {},
|
|
9809
|
+
...command.args["platform"] === "web" || command.args["platform"] === "android" || command.args["platform"] === "ios" ? { platform: command.args["platform"] } : {},
|
|
9810
|
+
...command.args["reuse"] === false ? { reuse: false } : {},
|
|
9809
9811
|
...typeof sessionId === "string" ? { sessionId } : {},
|
|
9810
9812
|
emit
|
|
9811
9813
|
});
|
|
@@ -10175,7 +10177,8 @@ function buildAskArgs(opts) {
|
|
|
10175
10177
|
const brief = opts.brief?.trim();
|
|
10176
10178
|
const guidance = [
|
|
10177
10179
|
`You are the Flowriki assistant for the project "${opts.projectName}" (id ${opts.projectId}). Use the Flowriki tools, and pass projectId "${opts.projectId}" wherever a tool takes one. ` + (brief ? "The project brief below is this project's memory for this job: read it first." : "Start with project_brief: it is this project's memory."),
|
|
10178
|
-
ask ? "ASK MODE: answer the question; change nothing. Cite what you rely on as [[testCase:<its id, tc_\u2026>]], [[knowledge:features/checkout.md]] or [[file:path/in/repo.ts]]." : "DO MODE: do what is asked, on Claude's working branch, for a person to review. Ask in the chat and wait before publishing, deleting, or sending anything to another system.
|
|
10180
|
+
ask ? "ASK MODE: answer the question; change nothing. Cite what you rely on as [[testCase:<its id, tc_\u2026>]], [[knowledge:features/checkout.md]] or [[file:path/in/repo.ts]]." : "DO MODE: do what is asked, on Claude's working branch, for a person to review. Ask in the chat and wait before publishing, deleting, or sending anything to another system. Work quietly: the person sees what you build and your final answer, so keep words between tool calls to a minimum and end with a short result \u2014 what was saved, and anything that needs them. When you saved something, write a short summary with write_knowledge (memory/sessions/<date>-<topic>.md, append) so the next session knows.",
|
|
10181
|
+
"Use only the tools you have: a tool is used by calling it, not by writing the call out as text. If Flowriki's tools are missing, say so in one sentence and stop.",
|
|
10179
10182
|
folders.length ? `The project's code is in ${folders.join(" and ")}. Read it with Read, Grep and Glob when it helps; you cannot change it.` : "",
|
|
10180
10183
|
brief ? `
|
|
10181
10184
|
${brief}` : ""
|
|
@@ -10207,6 +10210,16 @@ ${brief}` : ""
|
|
|
10207
10210
|
...opts.sessionId ? ["--resume", opts.sessionId] : []
|
|
10208
10211
|
];
|
|
10209
10212
|
}
|
|
10213
|
+
function clearOld(dir) {
|
|
10214
|
+
try {
|
|
10215
|
+
for (const name of fs43.readdirSync(dir)) {
|
|
10216
|
+
if (!/^mcp-.+\.json$/.test(name)) continue;
|
|
10217
|
+
const file2 = path42.join(dir, name);
|
|
10218
|
+
if (Date.now() - fs43.statSync(file2).mtimeMs > 24 * 60 * 6e4) fs43.rmSync(file2, { force: true });
|
|
10219
|
+
}
|
|
10220
|
+
} catch {
|
|
10221
|
+
}
|
|
10222
|
+
}
|
|
10210
10223
|
function eventsFromLine(askId, line) {
|
|
10211
10224
|
let parsed;
|
|
10212
10225
|
try {
|
|
@@ -10215,8 +10228,10 @@ function eventsFromLine(askId, line) {
|
|
|
10215
10228
|
return [];
|
|
10216
10229
|
}
|
|
10217
10230
|
const out = [];
|
|
10218
|
-
if (parsed.type === "system" && parsed.subtype === "init"
|
|
10219
|
-
|
|
10231
|
+
if (parsed.type === "system" && parsed.subtype === "init") {
|
|
10232
|
+
const ours = (parsed.mcp_servers ?? []).find((s2) => s2.name === MCP_SERVER_NAME);
|
|
10233
|
+
if (ours) out.push({ name: "assistant.tools", data: { askId, status: ours.status ?? "unknown" } });
|
|
10234
|
+
if (parsed.session_id) out.push({ name: "assistant.session", data: { askId, sessionId: parsed.session_id } });
|
|
10220
10235
|
}
|
|
10221
10236
|
if (parsed.type === "assistant") {
|
|
10222
10237
|
for (const block of parsed.message?.content ?? []) {
|
|
@@ -10299,6 +10314,35 @@ function createClaudeCli(options) {
|
|
|
10299
10314
|
child.on("close", (code) => finish(code, false));
|
|
10300
10315
|
});
|
|
10301
10316
|
}
|
|
10317
|
+
const LANE_FRESH_MS = 60 * 6e4;
|
|
10318
|
+
const LANE_MAX_TOKENS = 15e4;
|
|
10319
|
+
const laneFile = (dir) => path42.join(dir, "lane-sessions.json");
|
|
10320
|
+
const readLanes = (dir) => {
|
|
10321
|
+
try {
|
|
10322
|
+
return JSON.parse(fs43.readFileSync(laneFile(dir), "utf8"));
|
|
10323
|
+
} catch {
|
|
10324
|
+
return {};
|
|
10325
|
+
}
|
|
10326
|
+
};
|
|
10327
|
+
const writeLane = (dir, platform, record) => {
|
|
10328
|
+
const all = readLanes(dir);
|
|
10329
|
+
if (record) all[platform] = record;
|
|
10330
|
+
else delete all[platform];
|
|
10331
|
+
try {
|
|
10332
|
+
fs43.writeFileSync(laneFile(dir), JSON.stringify(all), { mode: 384 });
|
|
10333
|
+
} catch {
|
|
10334
|
+
}
|
|
10335
|
+
};
|
|
10336
|
+
function laneSession(ask, dir) {
|
|
10337
|
+
if (ask.sessionId || !ask.platform) return ask.sessionId;
|
|
10338
|
+
if (ask.reuse === false) {
|
|
10339
|
+
writeLane(dir, ask.platform, null);
|
|
10340
|
+
return void 0;
|
|
10341
|
+
}
|
|
10342
|
+
const kept = readLanes(dir)[ask.platform];
|
|
10343
|
+
if (!kept || Date.now() - kept.updatedAt > LANE_FRESH_MS || kept.tokens > LANE_MAX_TOKENS) return void 0;
|
|
10344
|
+
return kept.sessionId;
|
|
10345
|
+
}
|
|
10302
10346
|
return {
|
|
10303
10347
|
async status() {
|
|
10304
10348
|
const version = await capture(["--version"]);
|
|
@@ -10344,13 +10388,14 @@ function createClaudeCli(options) {
|
|
|
10344
10388
|
fs43.mkdirSync(scratchRoot2, { recursive: true, mode: 448 });
|
|
10345
10389
|
const dir = path42.join(scratchRoot2, `project-${ask.projectId.replace(/[^A-Za-z0-9_-]/g, "_")}`);
|
|
10346
10390
|
fs43.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
10347
|
-
|
|
10391
|
+
clearOld(dir);
|
|
10392
|
+
const mcpConfigPath = path42.join(dir, `mcp-${ask.askId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 80)}.json`);
|
|
10348
10393
|
fs43.writeFileSync(
|
|
10349
10394
|
mcpConfigPath,
|
|
10350
|
-
JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: { command: options.mcp.command, args: options.mcp.args } } }),
|
|
10395
|
+
JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: { command: options.mcp.command, args: options.mcp.args, env: { FLOWRIKI_ASK_ID: ask.askId } } } }),
|
|
10351
10396
|
{ mode: 384 }
|
|
10352
10397
|
);
|
|
10353
|
-
return launch(ask, dir, mcpConfigPath, ask
|
|
10398
|
+
return launch(ask, dir, mcpConfigPath, laneSession(ask, dir));
|
|
10354
10399
|
},
|
|
10355
10400
|
cancel(askId) {
|
|
10356
10401
|
const child = running.get(askId);
|
|
@@ -10366,7 +10411,7 @@ function createClaudeCli(options) {
|
|
|
10366
10411
|
return running.size >= MAX_AT_ONCE;
|
|
10367
10412
|
}
|
|
10368
10413
|
};
|
|
10369
|
-
function launch(ask, dir, mcpConfigPath, sessionId) {
|
|
10414
|
+
function launch(ask, dir, mcpConfigPath, sessionId, attempt = 1) {
|
|
10370
10415
|
let child;
|
|
10371
10416
|
try {
|
|
10372
10417
|
child = spawn2(
|
|
@@ -10390,8 +10435,21 @@ function createClaudeCli(options) {
|
|
|
10390
10435
|
running.set(ask.askId, child);
|
|
10391
10436
|
let buffer = "", stderr = "", sawDone = false, ended = false;
|
|
10392
10437
|
let silentFailure = false;
|
|
10438
|
+
let toolsMissing = false;
|
|
10439
|
+
let session;
|
|
10440
|
+
let spent = 0;
|
|
10393
10441
|
const handleLine = (line) => {
|
|
10442
|
+
if (toolsMissing) return;
|
|
10394
10443
|
for (const event of eventsFromLine(ask.askId, line)) {
|
|
10444
|
+
if (event.name === "assistant.tools") {
|
|
10445
|
+
if (event.data["status"] !== "connected") {
|
|
10446
|
+
toolsMissing = true;
|
|
10447
|
+
child.kill("SIGTERM");
|
|
10448
|
+
return;
|
|
10449
|
+
}
|
|
10450
|
+
continue;
|
|
10451
|
+
}
|
|
10452
|
+
if (event.name === "assistant.session") session = String(event.data["sessionId"] ?? "") || session;
|
|
10395
10453
|
if (event.name === "assistant.done" && event.data["ok"] === false && !String(event.data["result"] ?? "").trim()) {
|
|
10396
10454
|
silentFailure = true;
|
|
10397
10455
|
continue;
|
|
@@ -10399,6 +10457,14 @@ function createClaudeCli(options) {
|
|
|
10399
10457
|
if (event.name === "assistant.done") sawDone = true;
|
|
10400
10458
|
ask.emit(event.name, event.data);
|
|
10401
10459
|
}
|
|
10460
|
+
try {
|
|
10461
|
+
const parsed = JSON.parse(line);
|
|
10462
|
+
if (parsed.type === "result" && parsed.usage) {
|
|
10463
|
+
const u = parsed.usage;
|
|
10464
|
+
spent += (u.input_tokens ?? 0) + (u.output_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
|
|
10465
|
+
}
|
|
10466
|
+
} catch {
|
|
10467
|
+
}
|
|
10402
10468
|
};
|
|
10403
10469
|
const end = (ok, detail) => {
|
|
10404
10470
|
if (ended) return;
|
|
@@ -10406,10 +10472,26 @@ function createClaudeCli(options) {
|
|
|
10406
10472
|
clearTimeout(timer);
|
|
10407
10473
|
if (buffer.trim()) handleLine(buffer);
|
|
10408
10474
|
running.delete(ask.askId);
|
|
10475
|
+
if (toolsMissing && cancelled !== ask.askId) {
|
|
10476
|
+
if (attempt < 3) {
|
|
10477
|
+
setTimeout(() => {
|
|
10478
|
+
const again = launch(ask, dir, mcpConfigPath, sessionId, attempt + 1);
|
|
10479
|
+
if (!again.started) ask.emit("assistant.done", { askId: ask.askId, ok: false, result: again.detail ?? "The assistant could not be started." });
|
|
10480
|
+
}, 1500 * attempt + Math.floor(Math.random() * 1e3)).unref?.();
|
|
10481
|
+
return;
|
|
10482
|
+
}
|
|
10483
|
+
ask.emit("assistant.done", { askId: ask.askId, ok: false, result: "Flowriki's tools did not connect to the assistant on this computer after three tries, so nothing was done. Send it again in a moment; if it keeps happening, restart the agent." });
|
|
10484
|
+
return;
|
|
10485
|
+
}
|
|
10409
10486
|
if (silentFailure && sessionId && /no conversation found/i.test(stderr) && cancelled !== ask.askId) {
|
|
10487
|
+
if (ask.platform) writeLane(dir, ask.platform, null);
|
|
10410
10488
|
const again = launch(ask, dir, mcpConfigPath, void 0);
|
|
10411
10489
|
if (again.started) return;
|
|
10412
10490
|
}
|
|
10491
|
+
if (ask.platform && session && ok && !silentFailure) {
|
|
10492
|
+
const before = sessionId && readLanes(dir)[ask.platform]?.sessionId === sessionId ? readLanes(dir)[ask.platform].tokens : 0;
|
|
10493
|
+
writeLane(dir, ask.platform, { sessionId: session, updatedAt: Date.now(), tokens: Math.max(before, spent) });
|
|
10494
|
+
}
|
|
10413
10495
|
if (!sawDone) {
|
|
10414
10496
|
const why = detail || stderr.trim().split("\n").filter((l) => l && !/^Warning:/.test(l)).slice(-3).join(" ") || "The assistant stopped without saying why.";
|
|
10415
10497
|
ask.emit("assistant.done", { askId: ask.askId, ok: ok && !silentFailure, result: ok && !silentFailure ? "" : why });
|
|
@@ -687,7 +687,13 @@ var ExecutionConfigSchema = z3.object({
|
|
|
687
687
|
var BUSINESS_KEYWORDS = ["Given", "When", "Then", "And", "But"];
|
|
688
688
|
var BusinessStepSchema = z22.object({
|
|
689
689
|
keyword: z22.enum(BUSINESS_KEYWORDS),
|
|
690
|
-
text: z22.string().min(1).max(2e3)
|
|
690
|
+
text: z22.string().min(1).max(2e3),
|
|
691
|
+
/** THE CASE STEPS THAT CARRY IT OUT, once it is automated — each as
|
|
692
|
+
* `<occurrenceId>/<stepId>`, so a person reads the case line by line with
|
|
693
|
+
* the steps under the words they prove. Said by whoever automated it (the
|
|
694
|
+
* assistant, when it builds the case). A step whose row is gone is simply
|
|
695
|
+
* not found, and shows with the steps no line claims. */
|
|
696
|
+
steps: z22.array(z22.string().min(1).max(200)).max(500).optional()
|
|
691
697
|
}).strict();
|
|
692
698
|
var TestCaseOriginSchema = z22.object({
|
|
693
699
|
kind: z22.enum(["import", "assistant"]),
|
package/dist/cli.js
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
runtimeValue,
|
|
25
25
|
start,
|
|
26
26
|
status
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-HXUFFE2E.js";
|
|
28
28
|
import {
|
|
29
29
|
CLOUD_PROVIDERS,
|
|
30
30
|
CLOUD_PROVIDER_NAME,
|
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
slugify,
|
|
52
52
|
stepRefusedOn,
|
|
53
53
|
targetFor
|
|
54
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-MJXAVVGP.js";
|
|
55
55
|
|
|
56
56
|
// src/cli.ts
|
|
57
57
|
import { parseArgs } from "util";
|
|
@@ -137,6 +137,7 @@ function serve(tools, info, io = {}) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// src/mcp/service-client.ts
|
|
140
|
+
var ASK_ID = (process.env["FLOWRIKI_ASK_ID"] ?? "").trim().slice(0, 200);
|
|
140
141
|
var ServiceClient = class {
|
|
141
142
|
constructor(connection, fetcher = fetch) {
|
|
142
143
|
this.connection = connection;
|
|
@@ -159,7 +160,10 @@ var ServiceClient = class {
|
|
|
159
160
|
headers: {
|
|
160
161
|
"x-flowriki-device": this.connection.deviceId,
|
|
161
162
|
"x-flowriki-device-secret": this.connection.secret,
|
|
162
|
-
"content-type": "application/json"
|
|
163
|
+
"content-type": "application/json",
|
|
164
|
+
// The dashboard's question these tools are answering, when they are:
|
|
165
|
+
// what they build is filed on it, for the person to watch.
|
|
166
|
+
...ASK_ID ? { "x-flowriki-ask": ASK_ID } : {}
|
|
163
167
|
}
|
|
164
168
|
});
|
|
165
169
|
const text2 = await res.text();
|
|
@@ -255,6 +259,12 @@ var ServiceClient = class {
|
|
|
255
259
|
);
|
|
256
260
|
}
|
|
257
261
|
/** Build a test case from flows — as a dry run, or saved. */
|
|
262
|
+
/** The flow being recorded, as it stands, for the dashboard's job. Only
|
|
263
|
+
* when these tools answer a question asked in the dashboard. */
|
|
264
|
+
async reportDraft(projectId, draft) {
|
|
265
|
+
if (!ASK_ID) return;
|
|
266
|
+
await this.call(`/api/agent/jobs/draft?projectId=${encodeURIComponent(projectId)}`, { method: "POST", body: JSON.stringify({ draft }) });
|
|
267
|
+
}
|
|
258
268
|
buildCase(projectId, spec, dryRun) {
|
|
259
269
|
return this.call(`/api/agent/cases?projectId=${encodeURIComponent(projectId)}`, { method: "POST", body: JSON.stringify({ ...spec, dryRun }) });
|
|
260
270
|
}
|
|
@@ -497,11 +507,21 @@ function deviceFrom(said, platform, devices, mobile) {
|
|
|
497
507
|
async function devicesSummary() {
|
|
498
508
|
return listDevices().catch(() => []);
|
|
499
509
|
}
|
|
500
|
-
function describeRows(rows) {
|
|
501
|
-
return rows.map((r) =>
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
510
|
+
function describeRows(rows, testCase) {
|
|
511
|
+
return rows.map((r, i) => {
|
|
512
|
+
const steps = testCase?.occurrences[i]?.steps;
|
|
513
|
+
if (r.steps?.length && steps?.length === r.steps.length) {
|
|
514
|
+
const on = new Map(r.checks.map((c) => [c.n, c.on]));
|
|
515
|
+
return [
|
|
516
|
+
`Row ${r.row}: "${r.flow}" (${r.browser})`,
|
|
517
|
+
...r.steps.map((st, j) => ` step ${st.n}. ${describe(steps[j])}${st.check !== void 0 ? ` \u2014 check ${st.check} ${on.get(st.check) ? "[on]" : "[off]"}` : ""}`)
|
|
518
|
+
].join("\n");
|
|
519
|
+
}
|
|
520
|
+
return [
|
|
521
|
+
`Row ${r.row}: "${r.flow}" (${r.browser})`,
|
|
522
|
+
...r.checks.length ? r.checks.map((c) => ` ${c.on ? "[on] " : "[off]"} ${c.n}. ${c.check}`) : [" (no checks in this flow)"]
|
|
523
|
+
].join("\n");
|
|
524
|
+
}).join("\n");
|
|
505
525
|
}
|
|
506
526
|
function describe(step) {
|
|
507
527
|
const target = (step.name ? `${step.role || "element"} "${step.name}"` : step.role || "") + (step.nth !== void 0 ? ` (nth ${step.nth})` : "");
|
|
@@ -634,6 +654,7 @@ var AuthoringSession = class _AuthoringSession {
|
|
|
634
654
|
const left = opts.fresh ? null : this.readDraft(input.projectId, input.name);
|
|
635
655
|
if (opts.fresh) this.dropDraft(input.projectId, input.name);
|
|
636
656
|
const opened = isMobile(input.platform) ? await this.startMobile(input.platform, catalog) : await this.startWeb(input, catalog);
|
|
657
|
+
this.report();
|
|
637
658
|
if (!left?.steps.length) return opened;
|
|
638
659
|
return this.resume(left.steps, opened);
|
|
639
660
|
}
|
|
@@ -686,6 +707,7 @@ ${view}`;
|
|
|
686
707
|
const draft = this.draft;
|
|
687
708
|
if (!draft) return;
|
|
688
709
|
this.verified = null;
|
|
710
|
+
this.report();
|
|
689
711
|
try {
|
|
690
712
|
const file = _AuthoringSession.draftFile(draft.projectId, draft.name);
|
|
691
713
|
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 448 });
|
|
@@ -694,6 +716,36 @@ ${view}`;
|
|
|
694
716
|
} catch {
|
|
695
717
|
}
|
|
696
718
|
}
|
|
719
|
+
/** The draft as it stands, to the dashboard question this answers — at
|
|
720
|
+
* most every half second, a recording keeps a step a second. */
|
|
721
|
+
reportTimer = null;
|
|
722
|
+
report() {
|
|
723
|
+
if (this.reportTimer) return;
|
|
724
|
+
this.reportTimer = setTimeout(() => void this.sendReport(), 500);
|
|
725
|
+
this.reportTimer.unref?.();
|
|
726
|
+
}
|
|
727
|
+
/** Whatever is waiting to be reported, now — before a save or a close, so
|
|
728
|
+
* the dashboard ends on the flow as it was saved. */
|
|
729
|
+
async sendReport() {
|
|
730
|
+
if (this.reportTimer) {
|
|
731
|
+
clearTimeout(this.reportTimer);
|
|
732
|
+
this.reportTimer = null;
|
|
733
|
+
}
|
|
734
|
+
{
|
|
735
|
+
const draft = this.draft;
|
|
736
|
+
if (!draft) return;
|
|
737
|
+
const featureName = draft.featureName ?? this.catalog?.features?.find((f) => f.id === draft.featureId)?.name ?? "";
|
|
738
|
+
if (typeof this.service.reportDraft !== "function") return;
|
|
739
|
+
await this.service.reportDraft(draft.projectId, {
|
|
740
|
+
name: draft.name,
|
|
741
|
+
feature: featureName,
|
|
742
|
+
platform: draft.platform ?? "web",
|
|
743
|
+
after: (draft.after ?? []).map((a) => a.name),
|
|
744
|
+
signsIn: Boolean(draft.signIn),
|
|
745
|
+
steps: withoutTyped(draft.steps).map((st) => st.secret ? { ...st, value: "" } : st)
|
|
746
|
+
}).catch(() => void 0);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
697
749
|
readDraft(projectId, name) {
|
|
698
750
|
try {
|
|
699
751
|
const raw = JSON.parse(fs.readFileSync(_AuthoringSession.draftFile(projectId, name), "utf8"));
|
|
@@ -1019,6 +1071,7 @@ Fix the step that failed (remove_step, then act again) and save again \u2014 or
|
|
|
1019
1071
|
replayed = " It was replayed from the start first and passed.";
|
|
1020
1072
|
}
|
|
1021
1073
|
const checksOff = draft.steps.map((step) => step.type === "assert" ? { ...step, proposals: step.proposals.map((p) => ({ ...p, enabled: false })) } : step);
|
|
1074
|
+
if (this.reportTimer) await this.sendReport();
|
|
1022
1075
|
const checkCount = checksOff.filter((step) => step.type === "assert").length;
|
|
1023
1076
|
const secretValues = {};
|
|
1024
1077
|
for (const step of checksOff) {
|
|
@@ -1052,7 +1105,7 @@ Review and publish it here: ${review}${note}`;
|
|
|
1052
1105
|
async previewCase(projectId, spec) {
|
|
1053
1106
|
const built = await this.service.buildCase(projectId, spec, true);
|
|
1054
1107
|
return `"${spec.name}" in ${spec.folder} would be:
|
|
1055
|
-
${describeRows(built.rows)}`;
|
|
1108
|
+
${describeRows(built.rows, built.testCase)}`;
|
|
1056
1109
|
}
|
|
1057
1110
|
/** RUN THE CASE, THEN SAVE IT. Built as a dry run, replayed in a fresh
|
|
1058
1111
|
* browser exactly as a test run does it, and saved onto Claude's working
|
|
@@ -1102,7 +1155,7 @@ ${describeRows(built.rows)}`;
|
|
|
1102
1155
|
return `The case did not pass, so it was NOT saved.
|
|
1103
1156
|
${lines.join("\n")}
|
|
1104
1157
|
|
|
1105
|
-
${describeRows(built.rows)}`;
|
|
1158
|
+
${describeRows(built.rows, testCase)}`;
|
|
1106
1159
|
}
|
|
1107
1160
|
const saved = await this.service.buildCase(projectId, spec, false);
|
|
1108
1161
|
const review = `${this.service.origin}${saved.path}`;
|
|
@@ -1111,6 +1164,7 @@ ${lines.join("\n")}
|
|
|
1111
1164
|
Review and publish it here: ${review}`;
|
|
1112
1165
|
}
|
|
1113
1166
|
async close() {
|
|
1167
|
+
if (this.reportTimer) await this.sendReport();
|
|
1114
1168
|
const run = this.run;
|
|
1115
1169
|
const mobile = this.mobile;
|
|
1116
1170
|
this.run = null;
|
|
@@ -1445,10 +1499,18 @@ HOW TO WORK:
|
|
|
1445
1499
|
catalog, then type {{its saveAs name}} into the field that needs it.
|
|
1446
1500
|
5. Add checks for what each step proves (a heading, a message, a value, the
|
|
1447
1501
|
URL). replay_flow, then save_flow. A flow must record at least one check.
|
|
1448
|
-
6. Build each test case: preview_case lists its rows and every
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1502
|
+
6. Build each test case: preview_case lists its rows and every step, numbered
|
|
1503
|
+
across the case, with each check's number in its row; save_case with the
|
|
1504
|
+
checks to switch on runs the whole case and saves it only if it passed.
|
|
1505
|
+
Pass businessSteps to both: the case's Given / When / Then lines (its own,
|
|
1506
|
+
from read_test_cases, when it has them) each with the step numbers that
|
|
1507
|
+
carry it out, so the person reads every line with its steps under it.
|
|
1508
|
+
Give the person the review link.
|
|
1509
|
+
7. Saving the same name again replaces it on the working branch. ONE VERSION
|
|
1510
|
+
of each scenario per platform: redo a flow or case under its existing name
|
|
1511
|
+
(project_catalog lists them) rather than saving another beside it. When the
|
|
1512
|
+
project tests several platforms, name flows "<scenario> \u2014 Web", "\u2014 Android",
|
|
1513
|
+
"\u2014 iOS". Old branches
|
|
1452
1514
|
of yours? gather_branches puts them into one; discard_branch deletes one \u2014
|
|
1453
1515
|
only after the person asks or agrees.
|
|
1454
1516
|
Test data you invent (a new user's email or password for a sign-up) is fine to
|
|
@@ -1480,6 +1542,19 @@ var CASE_INPUT = {
|
|
|
1480
1542
|
additionalProperties: false
|
|
1481
1543
|
}
|
|
1482
1544
|
},
|
|
1545
|
+
businessSteps: {
|
|
1546
|
+
type: "array",
|
|
1547
|
+
description: 'The case in Given / When / Then lines, in order, each with the step numbers from preview_case that carry it out \u2014 e.g. {"step": "Given the user is on the sign-in page", "steps": [1, 2]}. A case that already has business steps keeps its own words: give the same lines.',
|
|
1548
|
+
items: {
|
|
1549
|
+
type: "object",
|
|
1550
|
+
properties: {
|
|
1551
|
+
step: { type: "string", description: "One line, starting Given, When, Then, And or But." },
|
|
1552
|
+
steps: { type: "array", items: { type: "number" }, description: "The step numbers (across the case) that carry it out." }
|
|
1553
|
+
},
|
|
1554
|
+
required: ["step"],
|
|
1555
|
+
additionalProperties: false
|
|
1556
|
+
}
|
|
1557
|
+
},
|
|
1483
1558
|
branch: { type: "string", description: "Optional: save onto this branch (chg_\u2026) instead of the working one." },
|
|
1484
1559
|
projectId: { type: "string" }
|
|
1485
1560
|
},
|
|
@@ -1491,6 +1566,7 @@ var caseSpec = (args) => ({
|
|
|
1491
1566
|
folder: String(args["folder"] ?? ""),
|
|
1492
1567
|
...args["description"] ? { description: String(args["description"]) } : {},
|
|
1493
1568
|
rows: Array.isArray(args["rows"]) ? args["rows"] : [],
|
|
1569
|
+
...Array.isArray(args["businessSteps"]) ? { businessSteps: args["businessSteps"] } : {},
|
|
1494
1570
|
...args["branch"] ? { changeId: String(args["branch"]) } : {}
|
|
1495
1571
|
});
|
|
1496
1572
|
var text = (value, isError = false) => ({ content: [{ type: "text", text: value }], ...isError ? { isError } : {} });
|
package/dist/engine.js
CHANGED
package/package.json
CHANGED