flowstride 1.0.19 → 1.0.20-rc.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.
Files changed (54) hide show
  1. package/NATIVE_CI.md +179 -0
  2. package/dist/adapters/web/playwright.d.ts +1 -0
  3. package/dist/adapters/web/playwright.js +24 -18
  4. package/dist/ci/config.d.ts +3 -0
  5. package/dist/ci/config.js +77 -0
  6. package/dist/ci/deadline.d.ts +2 -0
  7. package/dist/ci/deadline.js +16 -0
  8. package/dist/ci/init.d.ts +1 -0
  9. package/dist/ci/init.js +21 -0
  10. package/dist/ci/runtime.d.ts +19 -0
  11. package/dist/ci/runtime.js +203 -0
  12. package/dist/cli/commands/run.js +17 -35
  13. package/dist/cli/index.js +78 -54
  14. package/dist/cli/manual_server/workspace_server.js +44 -32
  15. package/dist/cli/utils/cloudSession.d.ts +7 -0
  16. package/dist/cli/utils/cloudSession.js +37 -0
  17. package/dist/cli/utils/proInstaller.d.ts +5 -0
  18. package/dist/cli/utils/proInstaller.js +141 -0
  19. package/dist/game/bridge.d.ts +13 -0
  20. package/dist/game/bridge.js +15 -0
  21. package/dist/game/sdk.d.ts +27 -0
  22. package/dist/game/sdk.js +256 -0
  23. package/dist/game/three-bridge.d.ts +18 -0
  24. package/dist/game/three-bridge.js +59 -0
  25. package/dist/index.d.ts +5 -0
  26. package/dist/index.js +9 -0
  27. package/dist/manual/security.d.ts +183 -0
  28. package/dist/manual/security.js +106 -0
  29. package/dist/runner/data_generators.d.ts +1 -0
  30. package/dist/runner/data_generators.js +3 -0
  31. package/dist/runner/dom_stripper.js +3 -0
  32. package/dist/runner/flow_orchestrator.d.ts +5 -1
  33. package/dist/runner/flow_orchestrator.js +34 -19
  34. package/dist/runner/flow_worker.d.ts +1 -0
  35. package/dist/runner/flow_worker.js +56 -16
  36. package/dist/runner/persistent_vault.js +2 -1
  37. package/dist/runner/plugin_manager.d.ts +1 -0
  38. package/dist/runner/plugin_manager.js +35 -1
  39. package/dist/security/localDashboard.d.ts +2 -0
  40. package/dist/security/localDashboard.js +22 -0
  41. package/dist/security/redaction.d.ts +4 -0
  42. package/dist/security/redaction.js +49 -0
  43. package/dist/types/index.d.ts +43 -0
  44. package/dist/utils/cloud.d.ts +6 -0
  45. package/dist/utils/cloud.js +33 -0
  46. package/dist/utils/projectPaths.d.ts +11 -0
  47. package/dist/utils/projectPaths.js +90 -0
  48. package/examples/native-game/flows/canvas.flow +13 -0
  49. package/examples/native-game/flowstride.config.json +12 -0
  50. package/examples/native-game/server.mjs +12 -0
  51. package/package.json +27 -11
  52. package/src/ui/dist/assets/index-DVwp0K-J.js +462 -0
  53. package/src/ui/dist/index.html +1 -1
  54. package/src/ui/dist/assets/index-DIC22ZK_.js +0 -638
@@ -0,0 +1,183 @@
1
+ import { z } from "zod";
2
+ export declare function sanitizeManualHtml(value: unknown): string;
3
+ export declare const manualRunSchema: z.ZodObject<{
4
+ name: z.ZodString;
5
+ status: z.ZodEnum<{
6
+ PASS: "PASS";
7
+ FAIL: "FAIL";
8
+ INCOMPLETE: "INCOMPLETE";
9
+ "NO STEPS": "NO STEPS";
10
+ }>;
11
+ metrics: z.ZodObject<{
12
+ total: z.ZodNumber;
13
+ passed: z.ZodNumber;
14
+ failed: z.ZodNumber;
15
+ pending: z.ZodNumber;
16
+ }, z.core.$strip>;
17
+ canvasData: z.ZodObject<{
18
+ nodes: z.ZodDefault<z.ZodArray<z.ZodObject<{
19
+ id: z.ZodNumber;
20
+ title: z.ZodOptional<z.ZodString>;
21
+ visual_step: z.ZodOptional<z.ZodString>;
22
+ description: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
23
+ steps: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
24
+ expected_result: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
25
+ references: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
26
+ defect_report: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
27
+ defect_media: z.ZodOptional<z.ZodString>;
28
+ assigned_to: z.ZodOptional<z.ZodString>;
29
+ priority: z.ZodOptional<z.ZodString>;
30
+ x: z.ZodOptional<z.ZodNumber>;
31
+ y: z.ZodOptional<z.ZodNumber>;
32
+ width: z.ZodOptional<z.ZodNumber>;
33
+ height: z.ZodOptional<z.ZodNumber>;
34
+ is_active: z.ZodOptional<z.ZodBoolean>;
35
+ }, z.core.$loose>>>;
36
+ groups: z.ZodDefault<z.ZodArray<z.ZodObject<{
37
+ id: z.ZodNumber;
38
+ title: z.ZodOptional<z.ZodString>;
39
+ x: z.ZodOptional<z.ZodNumber>;
40
+ y: z.ZodOptional<z.ZodNumber>;
41
+ width: z.ZodOptional<z.ZodNumber>;
42
+ height: z.ZodOptional<z.ZodNumber>;
43
+ is_active: z.ZodOptional<z.ZodBoolean>;
44
+ }, z.core.$loose>>>;
45
+ texts: z.ZodDefault<z.ZodArray<z.ZodObject<{
46
+ id: z.ZodNumber;
47
+ content: z.ZodOptional<z.ZodString>;
48
+ x: z.ZodOptional<z.ZodNumber>;
49
+ y: z.ZodOptional<z.ZodNumber>;
50
+ width: z.ZodOptional<z.ZodNumber>;
51
+ height: z.ZodOptional<z.ZodNumber>;
52
+ is_active: z.ZodOptional<z.ZodBoolean>;
53
+ }, z.core.$loose>>>;
54
+ connections: z.ZodDefault<z.ZodArray<z.ZodObject<{
55
+ id: z.ZodNumber;
56
+ }, z.core.$loose>>>;
57
+ executionReport: z.ZodDefault<z.ZodArray<z.ZodObject<{
58
+ status: z.ZodEnum<{
59
+ fail: "fail";
60
+ passed: "passed";
61
+ failed: "failed";
62
+ skipped: "skipped";
63
+ success: "success";
64
+ Pending: "Pending";
65
+ Current: "Current";
66
+ Passed: "Passed";
67
+ Failed: "Failed";
68
+ Blocked: "Blocked";
69
+ Skipped: "Skipped";
70
+ Pass: "Pass";
71
+ Fail: "Fail";
72
+ Success: "Success";
73
+ pass: "pass";
74
+ pending: "pending";
75
+ blocked: "blocked";
76
+ }>;
77
+ group_title: z.ZodOptional<z.ZodString>;
78
+ id: z.ZodNumber;
79
+ title: z.ZodOptional<z.ZodString>;
80
+ visual_step: z.ZodOptional<z.ZodString>;
81
+ description: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
82
+ steps: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
83
+ expected_result: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
84
+ references: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
85
+ defect_report: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
86
+ defect_media: z.ZodOptional<z.ZodString>;
87
+ assigned_to: z.ZodOptional<z.ZodString>;
88
+ priority: z.ZodOptional<z.ZodString>;
89
+ x: z.ZodOptional<z.ZodNumber>;
90
+ y: z.ZodOptional<z.ZodNumber>;
91
+ width: z.ZodOptional<z.ZodNumber>;
92
+ height: z.ZodOptional<z.ZodNumber>;
93
+ is_active: z.ZodOptional<z.ZodBoolean>;
94
+ }, z.core.$loose>>>;
95
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
96
+ }, z.core.$loose>;
97
+ }, z.core.$strip>;
98
+ export declare function sanitizeStoredCanvas(value: unknown): Record<string, unknown>;
99
+ export declare const manualCanvasSchema: z.ZodObject<{
100
+ nodes: z.ZodDefault<z.ZodArray<z.ZodObject<{
101
+ id: z.ZodNumber;
102
+ title: z.ZodOptional<z.ZodString>;
103
+ visual_step: z.ZodOptional<z.ZodString>;
104
+ description: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
105
+ steps: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
106
+ expected_result: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
107
+ references: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
108
+ defect_report: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
109
+ defect_media: z.ZodOptional<z.ZodString>;
110
+ assigned_to: z.ZodOptional<z.ZodString>;
111
+ priority: z.ZodOptional<z.ZodString>;
112
+ x: z.ZodOptional<z.ZodNumber>;
113
+ y: z.ZodOptional<z.ZodNumber>;
114
+ width: z.ZodOptional<z.ZodNumber>;
115
+ height: z.ZodOptional<z.ZodNumber>;
116
+ is_active: z.ZodOptional<z.ZodBoolean>;
117
+ }, z.core.$loose>>>;
118
+ groups: z.ZodDefault<z.ZodArray<z.ZodObject<{
119
+ id: z.ZodNumber;
120
+ title: z.ZodOptional<z.ZodString>;
121
+ x: z.ZodOptional<z.ZodNumber>;
122
+ y: z.ZodOptional<z.ZodNumber>;
123
+ width: z.ZodOptional<z.ZodNumber>;
124
+ height: z.ZodOptional<z.ZodNumber>;
125
+ is_active: z.ZodOptional<z.ZodBoolean>;
126
+ }, z.core.$loose>>>;
127
+ texts: z.ZodDefault<z.ZodArray<z.ZodObject<{
128
+ id: z.ZodNumber;
129
+ content: z.ZodOptional<z.ZodString>;
130
+ x: z.ZodOptional<z.ZodNumber>;
131
+ y: z.ZodOptional<z.ZodNumber>;
132
+ width: z.ZodOptional<z.ZodNumber>;
133
+ height: z.ZodOptional<z.ZodNumber>;
134
+ is_active: z.ZodOptional<z.ZodBoolean>;
135
+ }, z.core.$loose>>>;
136
+ connections: z.ZodDefault<z.ZodArray<z.ZodObject<{
137
+ id: z.ZodNumber;
138
+ }, z.core.$loose>>>;
139
+ executionReport: z.ZodDefault<z.ZodArray<z.ZodObject<{
140
+ status: z.ZodEnum<{
141
+ fail: "fail";
142
+ passed: "passed";
143
+ failed: "failed";
144
+ skipped: "skipped";
145
+ success: "success";
146
+ Pending: "Pending";
147
+ Current: "Current";
148
+ Passed: "Passed";
149
+ Failed: "Failed";
150
+ Blocked: "Blocked";
151
+ Skipped: "Skipped";
152
+ Pass: "Pass";
153
+ Fail: "Fail";
154
+ Success: "Success";
155
+ pass: "pass";
156
+ pending: "pending";
157
+ blocked: "blocked";
158
+ }>;
159
+ group_title: z.ZodOptional<z.ZodString>;
160
+ id: z.ZodNumber;
161
+ title: z.ZodOptional<z.ZodString>;
162
+ visual_step: z.ZodOptional<z.ZodString>;
163
+ description: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
164
+ steps: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
165
+ expected_result: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
166
+ references: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
167
+ defect_report: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
168
+ defect_media: z.ZodOptional<z.ZodString>;
169
+ assigned_to: z.ZodOptional<z.ZodString>;
170
+ priority: z.ZodOptional<z.ZodString>;
171
+ x: z.ZodOptional<z.ZodNumber>;
172
+ y: z.ZodOptional<z.ZodNumber>;
173
+ width: z.ZodOptional<z.ZodNumber>;
174
+ height: z.ZodOptional<z.ZodNumber>;
175
+ is_active: z.ZodOptional<z.ZodBoolean>;
176
+ }, z.core.$loose>>>;
177
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
178
+ }, z.core.$loose>;
179
+ export declare class ManualInputError extends Error {
180
+ status: number;
181
+ constructor(status: number, message: string);
182
+ }
183
+ export declare function readManualPayload(req: import("http").IncomingMessage): Promise<any>;
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ManualInputError = exports.manualCanvasSchema = exports.manualRunSchema = void 0;
7
+ exports.sanitizeManualHtml = sanitizeManualHtml;
8
+ exports.sanitizeStoredCanvas = sanitizeStoredCanvas;
9
+ exports.readManualPayload = readManualPayload;
10
+ const sanitize_html_1 = __importDefault(require("sanitize-html"));
11
+ const zod_1 = require("zod");
12
+ const MANUAL_HTML_TAGS = ["b", "i", "em", "strong", "a", "ul", "ol", "li", "br", "div", "p", "font"];
13
+ function sanitizeManualHtml(value) {
14
+ return (0, sanitize_html_1.default)(typeof value === "string" ? value : "", {
15
+ allowedTags: MANUAL_HTML_TAGS,
16
+ allowedAttributes: { a: ["href", "target", "rel"], font: ["size"] },
17
+ allowedSchemes: ["http", "https", "mailto"], allowProtocolRelative: false,
18
+ transformTags: { a: (tagName, attribs) => ({ tagName, attribs: {
19
+ ...attribs, target: attribs.target === "_blank" ? "_blank" : "_self", rel: "noopener noreferrer",
20
+ } }) },
21
+ });
22
+ }
23
+ const label = zod_1.z.string().max(2000);
24
+ const rich = zod_1.z.string().max(100000).transform(sanitizeManualHtml);
25
+ const numeric = zod_1.z.number().finite();
26
+ const media = zod_1.z.string().max(8192).refine(value => !value || /^(https?:\/\/|\/(?!\/))[^\s\\]*$/i.test(value), "Invalid media URL");
27
+ const nodeFields = {
28
+ id: zod_1.z.number().int().nonnegative(), title: label.optional(), visual_step: label.optional(),
29
+ description: rich.optional(), steps: rich.optional(), expected_result: rich.optional(), references: rich.optional(),
30
+ defect_report: rich.optional(), defect_media: media.optional(), assigned_to: label.optional(), priority: label.optional(),
31
+ x: numeric.optional(), y: numeric.optional(), width: numeric.optional(), height: numeric.optional(), is_active: zod_1.z.boolean().optional(),
32
+ };
33
+ const node = zod_1.z.looseObject(nodeFields);
34
+ const step = zod_1.z.looseObject({ ...nodeFields, status: zod_1.z.enum(["Pending", "Current", "Passed", "Failed", "Blocked", "Skipped", "Pass", "Fail", "Success", "passed", "failed", "pass", "fail", "success", "pending", "skipped", "blocked"]), group_title: label.optional() });
35
+ const group = zod_1.z.looseObject({ id: zod_1.z.number().int().nonnegative(), title: label.optional(), x: numeric.optional(), y: numeric.optional(), width: numeric.optional(), height: numeric.optional(), is_active: zod_1.z.boolean().optional() });
36
+ const text = zod_1.z.looseObject({ id: zod_1.z.number().int().nonnegative(), content: label.optional(), x: numeric.optional(), y: numeric.optional(), width: numeric.optional(), height: numeric.optional(), is_active: zod_1.z.boolean().optional() });
37
+ const connection = zod_1.z.looseObject({ id: zod_1.z.number().int().nonnegative() });
38
+ exports.manualRunSchema = zod_1.z.object({
39
+ name: zod_1.z.string().trim().min(1).max(255),
40
+ status: zod_1.z.enum(["PASS", "FAIL", "INCOMPLETE", "NO STEPS"]),
41
+ metrics: zod_1.z.object({ total: zod_1.z.number().int().nonnegative(), passed: zod_1.z.number().int().nonnegative(), failed: zod_1.z.number().int().nonnegative(), pending: zod_1.z.number().int().nonnegative() }).refine(m => m.total === m.passed + m.failed + m.pending, "Inconsistent metrics"),
42
+ canvasData: zod_1.z.looseObject({ nodes: zod_1.z.array(node).max(10000).default([]), groups: zod_1.z.array(group).max(10000).default([]), texts: zod_1.z.array(text).max(10000).default([]), connections: zod_1.z.array(connection).max(20000).default([]), executionReport: zod_1.z.array(step).max(10000).default([]), metadata: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional() }),
43
+ });
44
+ // Sanitize older records without trusting their original ingestion path.
45
+ function sanitizeStoredCanvas(value) {
46
+ if (!value || typeof value !== "object" || Array.isArray(value))
47
+ return {};
48
+ const canvas = { ...value };
49
+ for (const key of ["nodes", "executionReport"]) {
50
+ if (!Array.isArray(canvas[key])) {
51
+ canvas[key] = [];
52
+ continue;
53
+ }
54
+ canvas[key] = canvas[key].filter(item => item && typeof item === "object").map(item => {
55
+ const result = { ...item };
56
+ for (const field of ["description", "steps", "expected_result", "references", "defect_report"]) {
57
+ if (field in result)
58
+ result[field] = sanitizeManualHtml(result[field]);
59
+ }
60
+ return result;
61
+ });
62
+ }
63
+ return canvas;
64
+ }
65
+ exports.manualCanvasSchema = exports.manualRunSchema.shape.canvasData;
66
+ class ManualInputError extends Error {
67
+ status;
68
+ constructor(status, message) {
69
+ super(message);
70
+ this.status = status;
71
+ }
72
+ }
73
+ exports.ManualInputError = ManualInputError;
74
+ async function readManualPayload(req) {
75
+ const limit = 2 * 1024 * 1024;
76
+ const length = req.headers["content-length"];
77
+ if (length && (!/^\d+$/.test(length) || !Number.isSafeInteger(Number(length))))
78
+ throw new ManualInputError(400, "Invalid request length");
79
+ if (length && Number(length) > limit)
80
+ throw new ManualInputError(413, "Manual payload exceeds 2 MiB");
81
+ const buffer = Buffer.allocUnsafe(length ? Number(length) : limit);
82
+ let bytes = 0;
83
+ for await (const chunk of req.iterator({ destroyOnReturn: false })) {
84
+ bytes += chunk.length;
85
+ if (bytes > limit || (length && bytes > Number(length))) {
86
+ req.resume();
87
+ throw new ManualInputError(413, "Manual payload exceeds allowed size");
88
+ }
89
+ buffer.set(chunk, bytes - chunk.length);
90
+ }
91
+ if (length && bytes !== Number(length))
92
+ throw new ManualInputError(400, "Request length mismatch");
93
+ let payload;
94
+ try {
95
+ payload = JSON.parse(buffer.subarray(0, bytes).toString("utf8"));
96
+ }
97
+ catch {
98
+ throw new ManualInputError(400, "Invalid manual JSON");
99
+ }
100
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
101
+ throw new ManualInputError(400, "Invalid manual payload");
102
+ const canvas = exports.manualCanvasSchema.safeParse(payload.canvasData);
103
+ if (!canvas.success)
104
+ throw new ManualInputError(400, "Invalid manual canvas");
105
+ return { ...payload, canvasData: canvas.data };
106
+ }
@@ -3,6 +3,7 @@ export declare class DataGenerators {
3
3
  private runIdContext;
4
4
  private workerIdContext;
5
5
  constructor(runId?: string, workerId?: string);
6
+ sensitiveValues(): string[];
6
7
  resolve(template: string): string;
7
8
  private pickRandom;
8
9
  uuid: () => `${string}-${string}-${string}-${string}-${string}`;
@@ -19,6 +19,9 @@ class DataGenerators {
19
19
  this.runIdContext = runId;
20
20
  this.workerIdContext = workerId;
21
21
  }
22
+ sensitiveValues() {
23
+ return [...this.memoryBank.entries()].filter(([key]) => /password|passcode|token|otp|pin|secret/i.test(key)).map(([, value]) => value);
24
+ }
22
25
  resolve(template) {
23
26
  if (!template || typeof template !== "string")
24
27
  return template;
@@ -44,6 +44,9 @@ class DOMStripper {
44
44
  el.removeAttribute("src");
45
45
  }
46
46
  });
47
+ clone.querySelectorAll('input').forEach(input => input.removeAttribute('value'));
48
+ clone.querySelectorAll('textarea').forEach(input => { input.textContent = '[REDACTED]'; });
49
+ clone.querySelectorAll('[contenteditable]').forEach(input => { input.textContent = '[REDACTED]'; });
47
50
  let rawHtml = clone.outerHTML;
48
51
  rawHtml = rawHtml.replace(/[\n\r\t]+/g, " ");
49
52
  rawHtml = rawHtml.replace(/\s{2,}/g, " ");
@@ -4,6 +4,8 @@ export declare class FlowOrchestrator {
4
4
  private worker;
5
5
  private workerCount;
6
6
  private config?;
7
+ static readonly nativeCIProtocol = 1;
8
+ readonly nativeEventProtocol = 1;
7
9
  private wss;
8
10
  private httpServer;
9
11
  private globalTestScenarios;
@@ -13,6 +15,8 @@ export declare class FlowOrchestrator {
13
15
  constructor(worker: FlowWorker, workerCount?: number, config?: FlowstrideConfig | undefined);
14
16
  private forwardToDashboard;
15
17
  private executeTestRun;
16
- start(targetPath: string): Promise<void>;
18
+ start(targetPath: string): Promise<boolean>;
19
+ abort(): void;
20
+ close(): Promise<void>;
17
21
  private handleExportPdfRequest;
18
22
  }
@@ -37,6 +37,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.FlowOrchestrator = void 0;
40
+ const localDashboard_1 = require("../security/localDashboard");
41
+ const projectPaths_1 = require("../utils/projectPaths");
40
42
  const fs = __importStar(require("fs"));
41
43
  const path = __importStar(require("path"));
42
44
  const http = __importStar(require("http"));
@@ -48,6 +50,8 @@ class FlowOrchestrator {
48
50
  worker;
49
51
  workerCount;
50
52
  config;
53
+ static nativeCIProtocol = 1;
54
+ nativeEventProtocol = 1;
51
55
  wss = null;
52
56
  httpServer = null;
53
57
  globalTestScenarios = [];
@@ -64,6 +68,7 @@ class FlowOrchestrator {
64
68
  });
65
69
  }
66
70
  forwardToDashboard(type, payload) {
71
+ this.config?.onRunEvent?.({ type, payload });
67
72
  if (this.wss) {
68
73
  this.wss.clients.forEach((client) => {
69
74
  if (client.readyState === 1)
@@ -73,17 +78,9 @@ class FlowOrchestrator {
73
78
  }
74
79
  async executeTestRun(targetPath) {
75
80
  this.forwardToDashboard("SUITE_START", {});
76
- let filesToProcess = [];
77
- const stats = fs.statSync(targetPath);
78
- if (stats.isDirectory()) {
79
- filesToProcess = fs
80
- .readdirSync(targetPath)
81
- .filter((f) => f.endsWith(".flow"))
82
- .map((f) => path.join(targetPath, f));
83
- }
84
- else {
85
- filesToProcess = [targetPath];
86
- }
81
+ const filesToProcess = (0, projectPaths_1.discoverFlows)(targetPath);
82
+ if (this.config)
83
+ this.config.flowRoot = fs.statSync(targetPath).isDirectory() ? targetPath : path.dirname(targetPath);
87
84
  this.globalTestScenarios = [];
88
85
  this.stepStatusRegistry.clear();
89
86
  if (this.workerCount > 1) {
@@ -103,12 +100,21 @@ class FlowOrchestrator {
103
100
  this.worker.on("worker_event", listener);
104
101
  await this.worker.runBatch(filesToProcess);
105
102
  this.worker.off("worker_event", listener);
106
- this.forwardToDashboard("RUN_COMPLETE", { status: "success" });
103
+ const passed = ![...this.stepStatusRegistry.values()].some(status => ['failed', 'running', 'not_tested'].includes(status));
104
+ this.forwardToDashboard("RUN_COMPLETE", { status: passed ? 'success' : 'failed' });
105
+ return passed;
107
106
  }
108
107
  async start(targetPath) {
109
108
  try {
109
+ if (this.config?.ci)
110
+ return await this.executeTestRun(targetPath);
110
111
  this.httpServer = http.createServer((req, res) => {
111
- res.setHeader("Access-Control-Allow-Origin", "*");
112
+ if (!(0, localDashboard_1.isLocalDashboardRequest)(req.headers.host, req.headers.origin)) {
113
+ res.writeHead(403);
114
+ res.end('Forbidden');
115
+ return;
116
+ }
117
+ res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
112
118
  if (req.url && req.url.startsWith("/videos/")) {
113
119
  let urlObj;
114
120
  try {
@@ -221,7 +227,7 @@ class FlowOrchestrator {
221
227
  console.error(chalk_1.default.red(`\n[Error]: Failed to start local server: ${e.message}`));
222
228
  process.exit(1);
223
229
  });
224
- this.wss = new ws_1.WebSocketServer({ server: this.httpServer });
230
+ this.wss = new ws_1.WebSocketServer({ server: this.httpServer, verifyClient: (info) => Boolean(info.origin) && (0, localDashboard_1.isLocalDashboardRequest)(info.req.headers.host, info.origin) });
225
231
  this.wss.on("connection", (ws) => {
226
232
  if (this.disconnectTimer) {
227
233
  clearTimeout(this.disconnectTimer);
@@ -277,7 +283,7 @@ class FlowOrchestrator {
277
283
  });
278
284
  });
279
285
  return new Promise((resolve, reject) => {
280
- this.httpServer.listen(0, async () => {
286
+ this.httpServer.listen(0, '127.0.0.1', async () => {
281
287
  try {
282
288
  const address = this.httpServer?.address();
283
289
  const port = typeof address === "object" && address !== null
@@ -304,8 +310,8 @@ class FlowOrchestrator {
304
310
  else {
305
311
  console.log(chalk_1.default.gray("[Dashboard]: Headless mode active. Bypassing UI auto-launch..."));
306
312
  }
307
- await this.executeTestRun(targetPath);
308
- resolve(undefined);
313
+ const result = await this.executeTestRun(targetPath);
314
+ resolve(result);
309
315
  }
310
316
  catch (err) {
311
317
  reject(err);
@@ -317,11 +323,20 @@ class FlowOrchestrator {
317
323
  throw e;
318
324
  }
319
325
  }
326
+ abort() {
327
+ this.worker.abort();
328
+ }
329
+ async close() {
330
+ if (this.disconnectTimer)
331
+ clearTimeout(this.disconnectTimer);
332
+ this.wss?.clients.forEach(client => client.terminate());
333
+ this.wss?.close();
334
+ this.httpServer?.close();
335
+ }
320
336
  async handleExportPdfRequest(testScenariosFromUI) {
321
337
  try {
322
- const spinner = (0, ora_1.default)(chalk_1.default.blue("Compiling high-fidelity PDF report...")).start();
338
+ const spinner = (0, ora_1.default)(chalk_1.default.blue("Compiling high fidelity PDF report...")).start();
323
339
  const pdfPath = await this.pdfGenerator.generateTestcasePdf(testScenariosFromUI);
324
- // Convert the saved PDF to base64 to trigger the native browser download UI
325
340
  const pdfBuffer = fs.readFileSync(pdfPath);
326
341
  const base64Data = pdfBuffer.toString("base64");
327
342
  this.forwardToDashboard("EXPORT_PDF_RES", { base64: base64Data });
@@ -21,6 +21,7 @@ export declare class FlowWorker extends EventEmitter {
21
21
  private pluginManager?;
22
22
  private isSilent;
23
23
  private isAborted;
24
+ private bailTriggered;
24
25
  private isDebugHalted;
25
26
  private currentStepContext;
26
27
  private currentFileName;