flowstride 1.0.20 → 1.0.22
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/NATIVE_BROWSER.md +381 -0
- package/NATIVE_CI.md +17 -5
- package/NATIVE_WORKFLOWS.md +44 -0
- package/README.md +11 -4
- package/dist/adapters/api/undici.d.ts +11 -1
- package/dist/adapters/api/undici.js +28 -3
- package/dist/adapters/web/playwright.d.ts +22 -2
- package/dist/adapters/web/playwright.js +254 -107
- package/dist/ci/config.js +14 -1
- package/dist/ci/distribution.d.ts +38 -0
- package/dist/ci/distribution.js +134 -0
- package/dist/ci/init.d.ts +4 -1
- package/dist/ci/init.js +13 -2
- package/dist/ci/runtime.js +33 -6
- package/dist/cli/commands/run.js +17 -4
- package/dist/cli/index.js +24 -5
- package/dist/cli/manual_server/workspace_server.js +90 -74
- package/dist/manual/security.d.ts +6 -6
- package/dist/native/accessibility.d.ts +4 -0
- package/dist/native/accessibility.js +84 -0
- package/dist/native/assertions.d.ts +3 -0
- package/dist/native/assertions.js +223 -0
- package/dist/native/compatibility.d.ts +3 -0
- package/dist/native/compatibility.js +16 -0
- package/dist/native/components.d.ts +12 -0
- package/dist/native/components.js +122 -0
- package/dist/native/dashboard.d.ts +7 -0
- package/dist/native/dashboard.js +32 -0
- package/dist/native/deadline.d.ts +2 -0
- package/dist/native/deadline.js +13 -0
- package/dist/native/events.d.ts +35 -0
- package/dist/native/events.js +308 -0
- package/dist/native/evidence.d.ts +3 -0
- package/dist/native/evidence.js +31 -0
- package/dist/native/functions.d.ts +10 -0
- package/dist/native/functions.js +131 -0
- package/dist/native/runtime.d.ts +17 -0
- package/dist/native/runtime.js +160 -0
- package/dist/native/validation.d.ts +107 -0
- package/dist/native/validation.js +84 -0
- package/dist/parser/assertion_contract.d.ts +12 -0
- package/dist/parser/assertion_contract.js +56 -0
- package/dist/parser/lexer.js +15 -4
- package/dist/parser/parser.js +15 -7
- package/dist/reporting/assertion_result.d.ts +36 -0
- package/dist/reporting/assertion_result.js +104 -0
- package/dist/reporting/pdf_generator.js +25 -20
- package/dist/reporting/runtime_failure.d.ts +10 -0
- package/dist/reporting/runtime_failure.js +64 -0
- package/dist/reporting/step_status.d.ts +10 -0
- package/dist/reporting/step_status.js +44 -0
- package/dist/runner/flow_orchestrator.d.ts +5 -0
- package/dist/runner/flow_orchestrator.js +30 -8
- package/dist/runner/flow_worker.d.ts +15 -1
- package/dist/runner/flow_worker.js +219 -42
- package/dist/runner/plugin_manager.js +3 -0
- package/dist/runner/temporary_store.d.ts +1 -0
- package/dist/runner/temporary_store.js +3 -0
- package/dist/sdk/api_proxy.d.ts +3 -0
- package/dist/sdk/api_proxy.js +16 -0
- package/dist/security/localWorkspace.d.ts +2 -0
- package/dist/security/localWorkspace.js +17 -0
- package/dist/security/redaction.js +1 -1
- package/dist/types/index.d.ts +13 -0
- package/examples/native-workflows/README.md +144 -0
- package/examples/native-workflows/api-and-ui/README.md +80 -0
- package/examples/native-workflows/api-and-ui/flows/api-and-ui.flow +27 -0
- package/examples/native-workflows/api-and-ui/negative/wrong-backend-state.flow +27 -0
- package/examples/native-workflows/api-ui-otp/README.md +140 -0
- package/examples/native-workflows/api-ui-otp/templates/api-ui-otp.flow +31 -0
- package/examples/native-workflows/api-ui-otp/templates/existing-account-otp.flow +20 -0
- package/examples/native-workflows/audio-to-text/README.md +131 -0
- package/examples/native-workflows/audio-to-text/fixtures/microphone-tone.wav +0 -0
- package/examples/native-workflows/audio-to-text/flows/microphone-demo.flow +23 -0
- package/examples/native-workflows/audio-to-text/negative/wrong-transcript.flow +23 -0
- package/examples/native-workflows/audio-to-text/templates/real-transcription.flow +25 -0
- package/examples/native-workflows/iframes/README.md +116 -0
- package/examples/native-workflows/iframes/flows/explicit-frame-plugin.flow +20 -0
- package/examples/native-workflows/iframes/flows/readable-frame.flow +13 -0
- package/examples/native-workflows/iframes/negative/wrong-frame-result.flow +13 -0
- package/examples/native-workflows/iframes/plugins/frame-actions.ts +7 -0
- package/examples/native-workflows/manual-testing/README.md +85 -0
- package/examples/native-workflows/manual-testing/fixtures/profile-review-mismatch.json +93 -0
- package/examples/native-workflows/manual-testing/fixtures/profile-review.json +93 -0
- package/examples/native-workflows/shared/server.mjs +80 -0
- package/examples/native-workflows/tables-and-cells/README.md +118 -0
- package/examples/native-workflows/tables-and-cells/flows/coordinates.flow +17 -0
- package/examples/native-workflows/tables-and-cells/flows/search-extract-reuse.flow +27 -0
- package/examples/native-workflows/tables-and-cells/negative/wrong-cell-value.flow +27 -0
- package/examples/native-workflows/variables-and-reuse/README.md +140 -0
- package/examples/native-workflows/variables-and-reuse/flows/reuse.flow +9 -0
- package/examples/native-workflows/variables-and-reuse/flows/setup.first.flow +27 -0
- package/examples/native-workflows/variables-and-reuse/negative/wrong-reused-value.flow +27 -0
- package/examples/native-workflows/variables-and-reuse/sessions/restore.flow +10 -0
- package/examples/native-workflows/variables-and-reuse/sessions/save.flow +29 -0
- package/package.json +23 -9
- package/src/ui/dist/assets/index-s0WYkGPt.js +636 -0
- package/src/ui/dist/index.html +1 -1
- package/src/ui/dist/assets/index-B7MuBTWm.js +0 -460
|
@@ -0,0 +1,134 @@
|
|
|
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.parseShard = parseShard;
|
|
7
|
+
exports.executionPlan = executionPlan;
|
|
8
|
+
exports.mergeResults = mergeResults;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const node_crypto_1 = require("node:crypto");
|
|
12
|
+
const projectPaths_1 = require("../utils/projectPaths");
|
|
13
|
+
function parseShard(value) {
|
|
14
|
+
if (value === undefined)
|
|
15
|
+
return { index: 1, total: 1 };
|
|
16
|
+
const match = /^(\d+)\/(\d+)$/.exec(value);
|
|
17
|
+
if (!match)
|
|
18
|
+
throw new Error('shard must be index/total, e.g. 1/4.');
|
|
19
|
+
const index = Number(match[1]), total = Number(match[2]);
|
|
20
|
+
if (!Number.isSafeInteger(index) || !Number.isSafeInteger(total) || index < 1 || index > total || total > 256)
|
|
21
|
+
throw new Error('Shard index must be between 1 and total (maximum 256).');
|
|
22
|
+
return { index, total };
|
|
23
|
+
}
|
|
24
|
+
function executionPlan(target, config) {
|
|
25
|
+
const root = config?.projectRoot || process.cwd(), files = (0, projectPaths_1.discoverFlows)(target, root);
|
|
26
|
+
const flowRoot = node_fs_1.default.statSync(target).isDirectory() ? target : node_path_1.default.dirname(target);
|
|
27
|
+
const shard = parseShard(config?.shard);
|
|
28
|
+
const identity = (file) => (0, projectPaths_1.flowIdentity)(file, flowRoot);
|
|
29
|
+
const suiteHash = (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(files.map(file => [identity(file), (0, node_crypto_1.createHash)('sha256').update(node_fs_1.default.readFileSync(file)).digest('hex')]))).digest('hex');
|
|
30
|
+
// Configuration affects compatibility, but only a hash leaves this process.
|
|
31
|
+
const configHash = (0, node_crypto_1.createHash)('sha256').update(JSON.stringify([require('../../package.json').name, require('../../package.json').version, config?.browser || 'chromium', config?.device || 'desktop', config?.browserOptions || {}, config?.retries || 0, config?.baseUrl || '', config?.timeout || 30000, config?.headless, config?.browserErrors, config?.failOnFlaky, config?.recordVideo, config?.trace, config?.har])).digest('hex');
|
|
32
|
+
let history = {};
|
|
33
|
+
if (config?.shardHistory) {
|
|
34
|
+
history = JSON.parse(node_fs_1.default.readFileSync((0, projectPaths_1.projectPath)(root, config.shardHistory), 'utf8'));
|
|
35
|
+
if (!history || Array.isArray(history) || typeof history !== 'object' || Object.values(history).some(value => typeof value !== 'number' || !Number.isFinite(value) || value < 0))
|
|
36
|
+
throw new Error('Shard history must map file names to nonnegative durations.');
|
|
37
|
+
}
|
|
38
|
+
const setup = files.filter(file => file.endsWith('.first.flow'));
|
|
39
|
+
const standard = files.filter(file => !file.endsWith('.first.flow'));
|
|
40
|
+
const bins = Array.from({ length: shard.total }, () => []), weights = bins.map(() => 0);
|
|
41
|
+
for (const file of [...standard].sort((a, b) => (history[identity(b)] ?? 1) - (history[identity(a)] ?? 1) || identity(a).localeCompare(identity(b)))) {
|
|
42
|
+
const index = weights.indexOf(Math.min(...weights));
|
|
43
|
+
bins[index].push(file);
|
|
44
|
+
weights[index] += history[identity(file)] ?? 1;
|
|
45
|
+
}
|
|
46
|
+
const assigned = new Set([...setup, ...bins[shard.index - 1]]);
|
|
47
|
+
return { files: files.filter(file => assigned.has(file)), allFiles: files.map(identity), setupFiles: setup.map(identity), suiteHash, configHash, shard, flowRoot };
|
|
48
|
+
}
|
|
49
|
+
function mergeResults(summaries, output = 'flowstride-merged') {
|
|
50
|
+
const root = process.cwd(), out = (0, projectPaths_1.projectPath)(root, output);
|
|
51
|
+
if (node_fs_1.default.existsSync(out))
|
|
52
|
+
throw new Error('Merge output already exists; choose a new directory.');
|
|
53
|
+
if (!summaries.length)
|
|
54
|
+
throw new Error('Provide one summary per shard.');
|
|
55
|
+
const inputs = summaries.map(file => { const absolute = (0, projectPaths_1.projectPath)(root, file); return { file: absolute, report: JSON.parse(node_fs_1.default.readFileSync(absolute, 'utf8')) }; });
|
|
56
|
+
const first = inputs[0].report;
|
|
57
|
+
if (first.protocol !== 1 || !first.distribution?.suiteHash)
|
|
58
|
+
throw new Error('Only native distributed reports can be merged.');
|
|
59
|
+
const total = first.distribution.shard.total, seen = new Set(), seenFiles = new Set(), setup = new Set(first.distribution.setupFiles);
|
|
60
|
+
const errors = [], steps = [], attempts = [], timings = {};
|
|
61
|
+
for (const { report } of inputs) {
|
|
62
|
+
const dist = report.distribution;
|
|
63
|
+
if (report.protocol !== 1 || report.engine !== first.engine || !dist || dist.suiteHash !== first.distribution.suiteHash || dist.configHash !== first.distribution.configHash || dist.shard.total !== total || JSON.stringify(dist.allFiles) !== JSON.stringify(first.distribution.allFiles) || JSON.stringify(dist.setupFiles) !== JSON.stringify(first.distribution.setupFiles))
|
|
64
|
+
throw new Error('Shards belong to different suites or browser configurations.');
|
|
65
|
+
const { index } = parseShard(`${dist.shard.index}/${dist.shard.total}`);
|
|
66
|
+
if (seen.has(index))
|
|
67
|
+
throw new Error('Duplicate shard.');
|
|
68
|
+
seen.add(index);
|
|
69
|
+
if (!['passed', 'failed', 'interrupted'].includes(report.status) || !Array.isArray(report.steps) || !Array.isArray(report.files) || !Array.isArray(report.errors))
|
|
70
|
+
throw new Error('Invalid shard report.');
|
|
71
|
+
if (!report.metrics || ['total', 'passed', 'failed', 'skipped', 'incomplete'].some(key => !Number.isSafeInteger(report.metrics[key]) || report.metrics[key] < 0) || report.metrics.passed + report.metrics.failed + report.metrics.skipped + report.metrics.incomplete !== report.metrics.total)
|
|
72
|
+
throw new Error('Inconsistent shard metrics.');
|
|
73
|
+
if (report.status === 'passed' && (report.errors.length || report.metrics.failed || report.metrics.incomplete || report.steps.some((step) => !['passed', 'skipped', 'not_tested'].includes(step.status))))
|
|
74
|
+
throw new Error('A passing shard contains failures or unfinished work.');
|
|
75
|
+
const duplicateSetup = new Set();
|
|
76
|
+
for (const file of report.files) {
|
|
77
|
+
if (!dist.allFiles.includes(file))
|
|
78
|
+
throw new Error('Unexpected file in shard.');
|
|
79
|
+
if (seenFiles.has(file)) {
|
|
80
|
+
if (!setup.has(file))
|
|
81
|
+
throw new Error('File appears in more than one shard.');
|
|
82
|
+
duplicateSetup.add(file);
|
|
83
|
+
}
|
|
84
|
+
seenFiles.add(file);
|
|
85
|
+
}
|
|
86
|
+
for (const step of report.steps)
|
|
87
|
+
steps.push({ ...step, shard: index });
|
|
88
|
+
attempts.push(...(report.attempts || []).map((attempt) => ({ ...attempt, shard: index })));
|
|
89
|
+
errors.push(...report.errors.map((error) => `Shard ${index}: ${error}`));
|
|
90
|
+
for (const [file, duration] of Object.entries(report.timings || {}))
|
|
91
|
+
if (typeof duration === 'number' && duration >= 0)
|
|
92
|
+
timings[file] = Math.max(timings[file] || 0, duration);
|
|
93
|
+
}
|
|
94
|
+
if (seen.size !== total)
|
|
95
|
+
throw new Error(`Missing shards: expected ${total}, received ${seen.size}.`);
|
|
96
|
+
if (first.distribution.allFiles.some((file) => !seenFiles.has(file)))
|
|
97
|
+
throw new Error('Merged reports omit test files.');
|
|
98
|
+
const statuses = inputs.map(input => input.report.status), status = statuses.includes('interrupted') ? 'interrupted' : statuses.includes('failed') || errors.length ? 'failed' : 'passed';
|
|
99
|
+
const metrics = inputs.reduce((all, { report }) => { for (const key of ['total', 'passed', 'failed', 'skipped', 'incomplete']) {
|
|
100
|
+
const n = report.metrics?.[key];
|
|
101
|
+
if (!Number.isSafeInteger(n) || n < 0)
|
|
102
|
+
throw new Error('Invalid shard metrics.');
|
|
103
|
+
all[key] += n;
|
|
104
|
+
} return all; }, { total: 0, passed: 0, failed: 0, skipped: 0, incomplete: 0 });
|
|
105
|
+
// Preserve shard-level metrics (setup executes independently on each machine).
|
|
106
|
+
const merged = { protocol: 1, engine: first.engine, browser: first.browser, browsers: inputs.flatMap(({ report }) => (report.browsers || []).map((info) => ({ ...info, shard: report.distribution.shard.index }))), status, metrics, steps, attempts, errors, timings, files: [...seenFiles], duration: Math.max(...inputs.map(input => input.report.duration)), distribution: { ...first.distribution, merged: true, setupRunsPerShard: true } };
|
|
107
|
+
node_fs_1.default.mkdirSync(out, { recursive: true, mode: 0o700 });
|
|
108
|
+
try {
|
|
109
|
+
for (const { file, report } of inputs) {
|
|
110
|
+
const source = (0, projectPaths_1.projectPath)(root, node_path_1.default.join(node_path_1.default.dirname(file), node_path_1.default.basename(report.artifactsDir || '')));
|
|
111
|
+
if (!node_fs_1.default.existsSync(source) || !node_fs_1.default.statSync(source).isDirectory())
|
|
112
|
+
throw new Error('Shard evidence directory is missing. Preserve each summary alongside its run directory.');
|
|
113
|
+
const destination = node_path_1.default.join(out, `shard-${report.distribution.shard.index}`);
|
|
114
|
+
node_fs_1.default.mkdirSync(destination);
|
|
115
|
+
let bytes = 0;
|
|
116
|
+
for (const name of node_fs_1.default.readdirSync(source)) {
|
|
117
|
+
const item = (0, projectPaths_1.projectPath)(root, node_path_1.default.join(source, name)), stat = node_fs_1.default.lstatSync(item);
|
|
118
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
119
|
+
throw new Error('Shard evidence must contain regular files only.');
|
|
120
|
+
bytes += stat.size;
|
|
121
|
+
if (bytes > 500 * 1024 * 1024)
|
|
122
|
+
throw new Error('Shard evidence exceeds the 500 MiB merge limit.');
|
|
123
|
+
node_fs_1.default.copyFileSync(item, node_path_1.default.join(destination, name));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(out, 'summary.json'), JSON.stringify(merged, null, 2));
|
|
127
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(out, 'timings.json'), JSON.stringify(timings, null, 2));
|
|
128
|
+
return merged;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
node_fs_1.default.rmSync(out, { recursive: true, force: true });
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
package/dist/ci/init.d.ts
CHANGED
package/dist/ci/init.js
CHANGED
|
@@ -7,15 +7,26 @@ exports.initCI = initCI;
|
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const config_1 = require("./config");
|
|
10
|
-
function initCI(provider) {
|
|
10
|
+
function initCI(provider, options = {}) {
|
|
11
11
|
if (provider !== 'github')
|
|
12
12
|
throw new Error('Supported provider: github.');
|
|
13
|
+
const browsers = (options.browsers || 'chromium').split(',');
|
|
14
|
+
const shards = Number(options.shards || 1);
|
|
15
|
+
if (!browsers.length || new Set(browsers).size !== browsers.length || browsers.some(browser => !['chromium', 'firefox', 'webkit'].includes(browser)))
|
|
16
|
+
throw new Error('Choose unique browser names separated by commas.');
|
|
17
|
+
if (!Number.isSafeInteger(shards) || shards < 1 || shards > 256)
|
|
18
|
+
throw new Error('shards must be 1..256.');
|
|
19
|
+
const matrix = browsers.length > 1 || shards > 1;
|
|
20
|
+
const strategy = matrix ? `\n strategy:\n fail-fast: false\n matrix:\n browser: ${JSON.stringify(browsers)}\n shard: ${JSON.stringify(Array.from({ length: shards }, (_, index) => index + 1))}` : '';
|
|
21
|
+
const selection = matrix ? ' --browser ${{ matrix.browser }} --shard ${{ matrix.shard }}/' + shards : browsers[0] !== 'chromium' ? ' --browser ' + browsers[0] : '';
|
|
22
|
+
const install = matrix ? ' ${{ matrix.browser }}' : ' ' + browsers[0];
|
|
23
|
+
const artifact = matrix ? 'flowstride-results-${{ matrix.browser }}-${{ matrix.shard }}' : 'flowstride-results';
|
|
13
24
|
const file = node_path_1.default.resolve('.github/workflows/flowstride.yml');
|
|
14
25
|
if (node_fs_1.default.existsSync(file))
|
|
15
26
|
throw new Error('Flowstride workflow already exists; it was not overwritten.');
|
|
16
27
|
const artifacts = node_path_1.default.relative(process.cwd(), (0, config_1.loadNativeConfig)(process.cwd(), {}).artifactsDir);
|
|
17
28
|
const parent = node_path_1.default.dirname(file);
|
|
18
29
|
node_fs_1.default.mkdirSync(parent, { recursive: true });
|
|
19
|
-
node_fs_1.default.writeFileSync(file, `name: Flowstride\non:\n push:\n pull_request:\n workflow_dispatch:\npermissions:\n contents: read\njobs:\n test:\n runs-on: ubuntu-latest\n timeout-minutes: 20\n steps:\n - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262\n with:\n persist-credentials: false\n - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020\n with:\n node-version: 24\n cache: npm\n - run: npm ci\n - name: Install matching browser dependencies\n run: npx --no-install flowstride install --with-deps\n - name: Run native Flowstride\n run: npx --no-install flowstride run --ci\n - name: Retain test evidence\n if: always()\n uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02\n with:\n name:
|
|
30
|
+
node_fs_1.default.writeFileSync(file, `name: Flowstride\non:\n push:\n pull_request:\n workflow_dispatch:\npermissions:\n contents: read\njobs:\n test:\n runs-on: ubuntu-latest\n timeout-minutes: 20${strategy}\n steps:\n - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262\n with:\n persist-credentials: false\n - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020\n with:\n node-version: 24\n cache: npm\n - run: npm ci\n - name: Install matching browser dependencies\n run: npx --no-install flowstride install --with-deps${install}\n - name: Run native Flowstride\n run: npx --no-install flowstride run --ci${selection}\n - name: Retain test evidence\n if: always()\n uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02\n with:\n name: ${artifact}\n path: ${JSON.stringify(artifacts + "/")}\n retention-days: 14\n`, { flag: 'wx' });
|
|
20
31
|
console.log(`Created ${file}. Commit it when ready. This template uses the project-locked Flowstride installation.`);
|
|
21
32
|
}
|
package/dist/ci/runtime.js
CHANGED
|
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.runNativeCI = runNativeCI;
|
|
7
|
+
const assertion_result_1 = require("../reporting/assertion_result");
|
|
8
|
+
const distribution_1 = require("./distribution");
|
|
7
9
|
const node_crypto_1 = require("node:crypto");
|
|
8
10
|
const projectPaths_1 = require("../utils/projectPaths");
|
|
9
11
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
@@ -20,6 +22,11 @@ async function runNativeCI(worker, orchestrator, target, config, engine) {
|
|
|
20
22
|
node_fs_1.default.mkdirSync(artifactRoot, { recursive: true });
|
|
21
23
|
const directory = node_fs_1.default.mkdtempSync(node_path_1.default.join(artifactRoot, 'run-'));
|
|
22
24
|
config.artifactsDir = directory;
|
|
25
|
+
const plan = (0, distribution_1.executionPlan)(target, config);
|
|
26
|
+
const browsers = new Map();
|
|
27
|
+
const attempts = [];
|
|
28
|
+
const attemptSteps = new Map();
|
|
29
|
+
const timings = {};
|
|
23
30
|
const steps = new Map();
|
|
24
31
|
const completed = new Set();
|
|
25
32
|
const errors = [];
|
|
@@ -34,8 +41,18 @@ async function runNativeCI(worker, orchestrator, target, config, engine) {
|
|
|
34
41
|
if (!data?.payload)
|
|
35
42
|
return;
|
|
36
43
|
const p = data.payload;
|
|
44
|
+
if (data.type === 'BROWSER_INFO')
|
|
45
|
+
browsers.set(p.fileName, { ...p });
|
|
37
46
|
if (data.type === 'BAIL')
|
|
38
47
|
bailed = true;
|
|
48
|
+
if (data.type === 'SCENARIO_ATTEMPT')
|
|
49
|
+
attemptSteps.set(p.fileName, []);
|
|
50
|
+
if (data.type === 'SCENARIO_RESULT')
|
|
51
|
+
attempts.push({ ...p, steps: [...(attemptSteps.get(p.fileName) || [])] });
|
|
52
|
+
if (data.type === 'FILE_COMPLETE' || data.type === 'FILE_FAILED')
|
|
53
|
+
timings[p.fileName] = p.duration;
|
|
54
|
+
if (data.type === 'STEP_UPDATE')
|
|
55
|
+
attemptSteps.get(p.fileName)?.push({ ...p, screenshot: undefined });
|
|
39
56
|
if (data.type === 'LOG_EVENT' && p.status === 'error') {
|
|
40
57
|
const active = [...steps.values()].reverse().find(step => step.fileName === p.fileName && step.status === 'running');
|
|
41
58
|
if (active)
|
|
@@ -69,9 +86,9 @@ async function runNativeCI(worker, orchestrator, target, config, engine) {
|
|
|
69
86
|
process.on('SIGINT', interrupt);
|
|
70
87
|
process.on('SIGTERM', interrupt);
|
|
71
88
|
try {
|
|
72
|
-
files =
|
|
89
|
+
files = plan.files;
|
|
73
90
|
config.flowRoot = node_fs_1.default.statSync(target).isDirectory() ? target : node_path_1.default.dirname(target);
|
|
74
|
-
if (!
|
|
91
|
+
if (!plan.allFiles.length)
|
|
75
92
|
throw new Error('No .flow tests found.');
|
|
76
93
|
for (const file of files) {
|
|
77
94
|
const features = (0, projectPaths_1.selectedFeatures)(new parser_1.Parser(new lexer_1.Lexer(node_fs_1.default.readFileSync(file, 'utf8')).tokenise()).parse());
|
|
@@ -83,7 +100,7 @@ async function runNativeCI(worker, orchestrator, target, config, engine) {
|
|
|
83
100
|
}
|
|
84
101
|
expected += features.reduce((n, feature) => n + feature.scenarios.reduce((m, scenario) => m + scenario.steps.length, 0), 0);
|
|
85
102
|
}
|
|
86
|
-
if (!expected)
|
|
103
|
+
if (files.length && !expected)
|
|
87
104
|
throw new Error('No executable test steps found.');
|
|
88
105
|
if (config.webServer) {
|
|
89
106
|
const settings = config.webServer;
|
|
@@ -175,8 +192,10 @@ async function runNativeCI(worker, orchestrator, target, config, engine) {
|
|
|
175
192
|
errors.push('Engine produced an unexpected step count.');
|
|
176
193
|
const metrics = { total: expected, passed: results.filter(s => s.status === 'passed').length,
|
|
177
194
|
failed: results.filter(s => s.status === 'failed').length, skipped: results.filter(s => ['skipped', 'not_tested'].includes(s.status)).length, incomplete };
|
|
178
|
-
|
|
179
|
-
|
|
195
|
+
if (config.failOnFlaky && attempts.some(attempt => attempt.flaky))
|
|
196
|
+
errors.push('Flaky scenarios are configured to fail CI.');
|
|
197
|
+
const result = { cloudSync: config.cloudSyncResult, browser: config.browser || 'chromium', browsers: [...browsers.values()], protocol: 1, engine, status: interrupted ? 'interrupted' : errors.length || metrics.failed || incomplete ? 'failed' : 'passed',
|
|
198
|
+
duration: Date.now() - started, artifactsDir: directory, files: files.map(file => (0, projectPaths_1.flowIdentity)(file, config.flowRoot)), steps: results, errors, metrics, attempts, timings, distribution: { ...plan, files: undefined, flowRoot: undefined } };
|
|
180
199
|
// Existing workers finalize videos into these locations. Copy only run files,
|
|
181
200
|
// never the project directory or credentials.
|
|
182
201
|
if (config.recordVideo !== false)
|
|
@@ -190,8 +209,9 @@ async function runNativeCI(worker, orchestrator, target, config, engine) {
|
|
|
190
209
|
}
|
|
191
210
|
}
|
|
192
211
|
config.artifactsDir = artifactRoot;
|
|
212
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(artifactRoot, 'timings.json'), JSON.stringify(timings, null, 2));
|
|
193
213
|
node_fs_1.default.writeFileSync(node_path_1.default.join(artifactRoot, 'summary.json'), JSON.stringify(result, null, 2));
|
|
194
|
-
const cases = results.map(step => `<testcase classname="${xml(step.fileName)}" name="${xml(step.description || step.id)}">${step.status === 'failed' ? `<failure message="${xml(step.error || 'Test step failed')}"/>` : ['skipped', 'not_tested'].includes(step.status) ? '<skipped/>' : ''}</testcase>`);
|
|
214
|
+
const cases = results.map(step => `<testcase classname="${xml(step.fileName)}" name="${xml(step.description || step.id)}">${step.status === 'failed' ? `<failure message="${xml(step.error || 'Test step failed')}"/>` : ['skipped', 'not_tested'].includes(step.status) ? '<skipped/>' : ''}${step.assertion ? `<system-out>${xml((0, assertion_result_1.assertionText)(step.assertion))}</system-out>` : ''}</testcase>`);
|
|
195
215
|
for (const error of errors)
|
|
196
216
|
cases.push(`<testcase name="execution"><error message="${xml(error)}"/></testcase>`);
|
|
197
217
|
if (incomplete)
|
|
@@ -199,5 +219,12 @@ async function runNativeCI(worker, orchestrator, target, config, engine) {
|
|
|
199
219
|
node_fs_1.default.writeFileSync(node_path_1.default.join(artifactRoot, 'junit.xml'), `<?xml version="1.0"?><testsuite name="Flowstride" tests="${cases.length}" failures="${metrics.failed + (incomplete ? 1 : 0)}" errors="${errors.length}" skipped="${metrics.skipped}">${cases.join('')}</testsuite>`);
|
|
200
220
|
if (process.env.GITHUB_STEP_SUMMARY)
|
|
201
221
|
node_fs_1.default.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `\n### Flowstride · ${config.device}\nResult: **${result.status}** (${engine})\n\nPassed: ${metrics.passed}; failed: ${metrics.failed}; skipped: ${metrics.skipped}; incomplete: ${metrics.incomplete}.\n`);
|
|
222
|
+
if (process.env.GITHUB_STEP_SUMMARY) {
|
|
223
|
+
const failures = results.filter(step => step.status === 'failed').map(step => `<details><summary>${xml(step.commandLabel || step.command || 'Step')} · ${xml(step.fileName)}</summary><pre>${xml(step.error || (step.assertion ? (0, assertion_result_1.assertionText)(step.assertion) : 'Failed without an adapter diagnostic.'))}</pre></details>`);
|
|
224
|
+
for (const error of errors)
|
|
225
|
+
failures.push(`<pre>${xml(error)}</pre>`);
|
|
226
|
+
if (failures.length)
|
|
227
|
+
node_fs_1.default.appendFileSync(process.env.GITHUB_STEP_SUMMARY, '\n' + failures.join('\n') + '\n');
|
|
228
|
+
}
|
|
202
229
|
return result;
|
|
203
230
|
}
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.runAction = runAction;
|
|
37
|
+
const runtime_failure_1 = require("../../reporting/runtime_failure");
|
|
38
|
+
const compatibility_1 = require("../../native/compatibility");
|
|
37
39
|
const projectPaths_1 = require("../../utils/projectPaths");
|
|
38
40
|
const config_1 = require("../../ci/config");
|
|
39
41
|
const runtime_1 = require("../../ci/runtime");
|
|
@@ -121,12 +123,16 @@ async function runAction(targetPath, options) {
|
|
|
121
123
|
const proOrchModule = await import((0, url_1.pathToFileURL)(proOrchPath).href);
|
|
122
124
|
if (config.ci && proOrchModule.FlowOrchestrator.nativeCIProtocol !== 1)
|
|
123
125
|
throw new Error('Installed Pro engine does not support native CI protocol 1; install the matching Pro candidate.');
|
|
126
|
+
if ((0, compatibility_1.needsNativeBrowserProtocol)(resolvedPath, config) && (proWorkerModule.FlowWorker.nativeBrowserProtocol !== 2 || proOrchModule.FlowOrchestrator.nativeBrowserProtocol !== 2))
|
|
127
|
+
throw new Error('Installed Cloud engine does not support these native browser capabilities. Install the matching Cloud archive; this run will use OS fallback.');
|
|
128
|
+
if ((0, compatibility_1.needsNativeBrowserProtocol)(resolvedPath, config) && (proWorkerModule.FlowWorker.nativeAssertionProtocol !== 1 || proOrchModule.FlowOrchestrator.nativeAssertionProtocol !== 1))
|
|
129
|
+
throw new Error('Installed Cloud engine predates structured assertion reporting; this run will use OS fallback. Install the matching Cloud archive to update.');
|
|
124
130
|
ActiveFlowWorker = proWorkerModule.FlowWorker;
|
|
125
131
|
ActiveFlowOrchestrator = proOrchModule.FlowOrchestrator;
|
|
126
132
|
engineMode = "Enterprise (Pro)";
|
|
127
133
|
}
|
|
128
134
|
catch (err) {
|
|
129
|
-
console.warn(`\n[Warning]: Found @flowstride/pro but failed to boot it. Falling back to Open Source engine. (${
|
|
135
|
+
console.warn(`\n[Warning]: Found @flowstride/pro but failed to boot it. Falling back to Open Source engine. (${(0, runtime_failure_1.runtimeFailure)("run.setup", { path: resolvedPath }, err, { file: resolvedPath })})`);
|
|
130
136
|
}
|
|
131
137
|
}
|
|
132
138
|
console.log(`\n[Engine]: Booting Flowstride in ${engineMode} mode...`);
|
|
@@ -137,9 +143,16 @@ async function runAction(targetPath, options) {
|
|
|
137
143
|
console.log(`[Flowstride]: ${result.status}; reports: ${config.artifactsDir}`);
|
|
138
144
|
process.exit(result.status === 'passed' ? 0 : result.status === 'interrupted' ? 130 : 1);
|
|
139
145
|
}
|
|
146
|
+
const previousRunEvent = config.onRunEvent;
|
|
147
|
+
config.onRunEvent = event => {
|
|
148
|
+
previousRunEvent?.(event);
|
|
149
|
+
if (event.type === 'RUN_COMPLETE')
|
|
150
|
+
process.exitCode = event.payload?.status === 'success' ? 0 : ['aborted', 'interrupted'].includes(event.payload?.status) ? 130 : 1;
|
|
151
|
+
};
|
|
140
152
|
try {
|
|
141
|
-
await orchestrator.start(resolvedPath);
|
|
142
|
-
|
|
153
|
+
const isSuccess = await orchestrator.start(resolvedPath);
|
|
154
|
+
process.exitCode = isSuccess ? 0 : 1;
|
|
155
|
+
console.log(isSuccess ? "\n[Flowstride]: Execution suite completed successfully." : "\n[Flowstride]: Execution suite failed.");
|
|
143
156
|
if (isCI) {
|
|
144
157
|
const artifactsDir = path.resolve(root, "flowstride-artifacts");
|
|
145
158
|
if (!fs.existsSync(artifactsDir)) {
|
|
@@ -154,7 +167,7 @@ async function runAction(targetPath, options) {
|
|
|
154
167
|
}
|
|
155
168
|
}
|
|
156
169
|
catch (error) {
|
|
157
|
-
console.error("\n[Fatal Error]:",
|
|
170
|
+
console.error("\n[Fatal Error]:", (0, runtime_failure_1.runtimeFailure)("run.setup", { path: resolvedPath }, error, { file: resolvedPath }));
|
|
158
171
|
if (isCI) {
|
|
159
172
|
console.log(`[CI Alert]: Terminating pipeline with exit code 1 to block deployment.`);
|
|
160
173
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -37,6 +37,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
38
|
};
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
const distribution_1 = require("../ci/distribution");
|
|
41
|
+
const projectPaths_1 = require("../utils/projectPaths");
|
|
40
42
|
const cloud_1 = require("../utils/cloud");
|
|
41
43
|
const cloudSession_1 = require("./utils/cloudSession");
|
|
42
44
|
const config_1 = require("../ci/config");
|
|
@@ -63,7 +65,7 @@ let API_BASE = (0, cloud_1.cloudApiBase)(process.env);
|
|
|
63
65
|
program
|
|
64
66
|
.name("flowstride")
|
|
65
67
|
.description("Flowstride: The Enterprise Flow-First Automation CLI")
|
|
66
|
-
.version("
|
|
68
|
+
.version(require("../../package.json").version);
|
|
67
69
|
program
|
|
68
70
|
.command("init")
|
|
69
71
|
.description("Initialise a new Flowstride workspace in the current directory")
|
|
@@ -258,6 +260,10 @@ program
|
|
|
258
260
|
.option("--workers <number>", "Number of parallel workers to spawn (Pro feature)")
|
|
259
261
|
.option('--ci', 'Run without a dashboard and write CI reports')
|
|
260
262
|
.option('--device <profile>', 'desktop, phone, or landscape')
|
|
263
|
+
.option('--browser <engine>', 'chromium, firefox, or webkit')
|
|
264
|
+
.option('--shard <index/total>', 'Run one deterministic CI shard, e.g. 1/4')
|
|
265
|
+
.option('--shard-history <file>', 'Use merged timing history for shard balancing')
|
|
266
|
+
.option('--update-baselines', 'Explicitly update ARIA baselines locally')
|
|
261
267
|
.option('--config <file>', 'Path to the Flowstride configuration')
|
|
262
268
|
.option('--env <name>', 'Environment profile')
|
|
263
269
|
.action(async (targetPath, options) => {
|
|
@@ -394,15 +400,28 @@ program
|
|
|
394
400
|
await (0, open_1.openAction)();
|
|
395
401
|
});
|
|
396
402
|
program.command('install')
|
|
397
|
-
.description('Install
|
|
403
|
+
.description('Install browsers matching this Flowstride engine')
|
|
404
|
+
.argument('[browsers...]', 'chromium, firefox, or webkit')
|
|
398
405
|
.option('--with-deps', 'Install operating-system browser dependencies')
|
|
399
|
-
.action((options) => {
|
|
406
|
+
.action((browsers, options) => {
|
|
407
|
+
if (browsers.some(browser => !['chromium', 'firefox', 'webkit'].includes(browser)))
|
|
408
|
+
throw new Error('Unknown browser.');
|
|
400
409
|
const { execFileSync } = require('node:child_process');
|
|
401
|
-
execFileSync(process.execPath, [path.join(path.dirname(require.resolve('playwright/package.json')), 'cli.js'), 'install', ...(options.withDeps ? ['--with-deps'] : []), 'chromium'], { stdio: 'inherit', env: { ...process.env, PLAYWRIGHT_SKIP_BROWSER_GC: '1' } });
|
|
410
|
+
execFileSync(process.execPath, [path.join(path.dirname(require.resolve('playwright/package.json')), 'cli.js'), 'install', ...(options.withDeps ? ['--with-deps'] : []), ...(browsers.length ? browsers : ['chromium'])], { stdio: 'inherit', env: { ...process.env, PLAYWRIGHT_SKIP_BROWSER_GC: '1' } });
|
|
411
|
+
console.log(`[Browsers]: Matching binaries installed: ${(browsers.length ? browsers : ['chromium']).join(', ')}. Use run --browser <engine> to select one.`);
|
|
402
412
|
});
|
|
403
413
|
program.command('ci')
|
|
404
414
|
.description('Set up native Flowstride CI')
|
|
405
415
|
.command('init <provider>')
|
|
406
416
|
.description('Generate a GitHub Actions workflow without overwriting existing files')
|
|
407
|
-
.
|
|
417
|
+
.option('--browsers <names>', 'Comma-separated browser matrix', 'chromium')
|
|
418
|
+
.option('--shards <total>', 'Number of CI machines per browser', '1')
|
|
419
|
+
.action((provider, options) => (0, init_1.initCI)(provider, options));
|
|
420
|
+
program.command('merge <summaries...>')
|
|
421
|
+
.description('Merge complete native shard results and evidence')
|
|
422
|
+
.option('--output <directory>', 'New project directory for merged results', 'flowstride-merged')
|
|
423
|
+
.action((summaries, options) => { const result = (0, distribution_1.mergeResults)(summaries, options.output); console.log(`Merged Flowstride result: ${result.status}`); process.exitCode = result.status === 'passed' ? 0 : 1; });
|
|
424
|
+
program.command('trace <file>')
|
|
425
|
+
.description('Open a local Playwright trace without uploading it')
|
|
426
|
+
.action((file) => { const { execFileSync } = require('node:child_process'); const local = (0, projectPaths_1.projectPath)(process.cwd(), file); execFileSync(process.execPath, [path.join(path.dirname(require.resolve('playwright/package.json')), 'cli.js'), 'show-trace', local], { stdio: 'inherit' }); });
|
|
408
427
|
program.parse(process.argv);
|
|
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.startManualWorkspaceServer = startManualWorkspaceServer;
|
|
40
|
+
const localWorkspace_1 = require("../../security/localWorkspace");
|
|
40
41
|
const projectPaths_1 = require("../../utils/projectPaths");
|
|
41
42
|
const security_1 = require("../../manual/security");
|
|
42
43
|
const http = __importStar(require("http"));
|
|
@@ -44,6 +45,72 @@ const path = __importStar(require("path"));
|
|
|
44
45
|
const fs = __importStar(require("fs"));
|
|
45
46
|
const ws_1 = require("ws");
|
|
46
47
|
const chalk_1 = __importDefault(require("chalk"));
|
|
48
|
+
/** Complete local persistence before publishing a usable media URL. */
|
|
49
|
+
function receiveManualUpload(req, res, filePath, maxSize, limitMessage, onSaved, onChunk) {
|
|
50
|
+
let size = 0;
|
|
51
|
+
let saved = false;
|
|
52
|
+
let failure;
|
|
53
|
+
const output = fs.createWriteStream(filePath);
|
|
54
|
+
const sendFailure = () => {
|
|
55
|
+
if (!res.destroyed && !res.writableEnded && !res.headersSent) {
|
|
56
|
+
res.writeHead(failure?.status || 500, { 'Content-Type': 'application/json' });
|
|
57
|
+
res.end(JSON.stringify({ error: failure?.message || 'Upload stream failed' }));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const fail = (status, message) => {
|
|
61
|
+
if (saved || failure)
|
|
62
|
+
return;
|
|
63
|
+
failure = { status, message };
|
|
64
|
+
output.destroy();
|
|
65
|
+
// Drain the rejected request without buffering it so the error can be sent.
|
|
66
|
+
req.resume();
|
|
67
|
+
};
|
|
68
|
+
output.on('error', () => fail(500, 'Upload stream failed'));
|
|
69
|
+
output.once('close', () => {
|
|
70
|
+
if (saved)
|
|
71
|
+
return;
|
|
72
|
+
// An early destroy may race the asynchronous file open. Cleanup belongs
|
|
73
|
+
// after close, when a partial file can no longer be recreated or written.
|
|
74
|
+
try {
|
|
75
|
+
fs.unlinkSync(filePath);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error.code !== 'ENOENT')
|
|
79
|
+
console.warn('[Upload]: Could not remove a partial media file.');
|
|
80
|
+
}
|
|
81
|
+
sendFailure();
|
|
82
|
+
});
|
|
83
|
+
output.once('finish', () => {
|
|
84
|
+
if (failure)
|
|
85
|
+
return;
|
|
86
|
+
saved = true;
|
|
87
|
+
Promise.resolve().then(() => onSaved(size)).catch(() => {
|
|
88
|
+
failure = { status: 500, message: 'Upload completion failed' };
|
|
89
|
+
sendFailure();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
req.on('data', (chunk) => {
|
|
93
|
+
if (failure || saved)
|
|
94
|
+
return;
|
|
95
|
+
size += chunk.length;
|
|
96
|
+
if (size > maxSize) {
|
|
97
|
+
fail(413, limitMessage);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
onChunk?.(chunk);
|
|
101
|
+
if (!output.write(chunk)) {
|
|
102
|
+
req.pause();
|
|
103
|
+
output.once('drain', () => { if (!failure)
|
|
104
|
+
req.resume(); });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
req.once('end', () => { if (!failure)
|
|
108
|
+
output.end(); });
|
|
109
|
+
req.once('aborted', () => fail(400, 'Upload aborted'));
|
|
110
|
+
req.once('error', () => fail(500, 'Upload stream failed'));
|
|
111
|
+
res.once('close', () => { if (!res.writableFinished)
|
|
112
|
+
fail(400, 'Upload aborted'); });
|
|
113
|
+
}
|
|
47
114
|
async function startManualWorkspaceServer() {
|
|
48
115
|
const cliInstallPath = path.resolve(__dirname, "../../..");
|
|
49
116
|
const uiDistPath = path.join(cliInstallPath, "src", "ui", "dist");
|
|
@@ -74,9 +141,18 @@ async function startManualWorkspaceServer() {
|
|
|
74
141
|
".webm": "video/webm",
|
|
75
142
|
};
|
|
76
143
|
const httpServer = http.createServer(async (req, res) => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
144
|
+
const origin = typeof req.headers.origin === 'string' ? req.headers.origin : undefined;
|
|
145
|
+
const fetchSite = typeof req.headers['sec-fetch-site'] === 'string' ? req.headers['sec-fetch-site'] : undefined;
|
|
146
|
+
if (!(0, localWorkspace_1.isLocalWorkspaceRequest)(req.headers.host, origin, req.socket.localPort, fetchSite)) {
|
|
147
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
148
|
+
res.end('Forbidden');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (origin)
|
|
152
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
153
|
+
res.setHeader('Vary', 'Origin');
|
|
154
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
155
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-File-Extension');
|
|
80
156
|
if (req.method === "OPTIONS") {
|
|
81
157
|
res.writeHead(204);
|
|
82
158
|
return res.end();
|
|
@@ -95,43 +171,11 @@ async function startManualWorkspaceServer() {
|
|
|
95
171
|
}
|
|
96
172
|
const safeFilename = `media_${Date.now()}_${Math.random().toString(36).substring(2, 9)}${ext.toLowerCase()}`;
|
|
97
173
|
const filePath = (0, projectPaths_1.projectPath)(process.cwd(), path.join(mediaDir, safeFilename));
|
|
98
|
-
|
|
99
|
-
let uploadedSize = 0;
|
|
100
|
-
let isAborted = false;
|
|
101
|
-
const writeStream = fs.createWriteStream(filePath);
|
|
102
|
-
req.on("data", (chunk) => {
|
|
103
|
-
if (isAborted)
|
|
104
|
-
return;
|
|
105
|
-
uploadedSize += chunk.length;
|
|
106
|
-
if (uploadedSize > MAX_SIZE) {
|
|
107
|
-
isAborted = true;
|
|
108
|
-
writeStream.destroy();
|
|
109
|
-
if (fs.existsSync(filePath))
|
|
110
|
-
fs.unlinkSync(filePath);
|
|
111
|
-
res.writeHead(413, { "Content-Type": "application/json" });
|
|
112
|
-
res.end(JSON.stringify({ error: "Payload Too Large. Max size is 100MB." }));
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
writeStream.write(chunk);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
req.on("end", () => {
|
|
119
|
-
if (isAborted)
|
|
120
|
-
return;
|
|
121
|
-
writeStream.end();
|
|
174
|
+
receiveManualUpload(req, res, filePath, 104857600, 'Payload Too Large. Max size is 100MB.', (uploadedSize) => {
|
|
122
175
|
console.log(chalk_1.default.blue(`\n[Reference Uploaded]: Securely saved ${safeFilename} (${(uploadedSize / 1024 / 1024).toFixed(2)} MB)`));
|
|
123
|
-
res.writeHead(200, {
|
|
176
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
124
177
|
res.end(JSON.stringify({ url: `/media/${safeFilename}` }));
|
|
125
178
|
});
|
|
126
|
-
req.on("error", (err) => {
|
|
127
|
-
if (!isAborted) {
|
|
128
|
-
writeStream.destroy();
|
|
129
|
-
if (fs.existsSync(filePath))
|
|
130
|
-
fs.unlinkSync(filePath);
|
|
131
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
132
|
-
res.end(JSON.stringify({ error: "Upload stream failed" }));
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
179
|
return;
|
|
136
180
|
}
|
|
137
181
|
if (req.method === "DELETE" && pathname === "/api/media") {
|
|
@@ -216,44 +260,12 @@ async function startManualWorkspaceServer() {
|
|
|
216
260
|
}
|
|
217
261
|
const safeFilename = `defect_${Date.now()}_${Math.random().toString(36).substring(2, 9)}${ext.toLowerCase()}`;
|
|
218
262
|
const filePath = (0, projectPaths_1.projectPath)(process.cwd(), path.join(defectMediaDir, safeFilename));
|
|
219
|
-
|
|
220
|
-
let uploadedSize = 0;
|
|
221
|
-
let isAborted = false;
|
|
222
|
-
const writeStream = fs.createWriteStream(filePath);
|
|
223
|
-
req.on("data", (chunk) => {
|
|
224
|
-
if (isAborted)
|
|
225
|
-
return;
|
|
226
|
-
uploadedSize += chunk.length;
|
|
227
|
-
if (uploadedSize > MAX_SIZE) {
|
|
228
|
-
isAborted = true;
|
|
229
|
-
writeStream.destroy();
|
|
230
|
-
if (fs.existsSync(filePath))
|
|
231
|
-
fs.unlinkSync(filePath);
|
|
232
|
-
res.writeHead(413, { "Content-Type": "application/json" });
|
|
233
|
-
res.end(JSON.stringify({ error: "Payload Too Large. Max size is 40MB." }));
|
|
234
|
-
}
|
|
235
|
-
else {
|
|
236
|
-
writeStream.write(chunk);
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
req.on("end", async () => {
|
|
240
|
-
if (isAborted)
|
|
241
|
-
return;
|
|
242
|
-
writeStream.end();
|
|
263
|
+
receiveManualUpload(req, res, filePath, 41943040, 'Payload Too Large. Max size is 40MB.', () => {
|
|
243
264
|
const finalUrl = `/defect_media/${safeFilename}`;
|
|
244
265
|
console.log(chalk_1.default.green(`\n[Vault Success]: Defect Media secured locally at -> ${finalUrl}`));
|
|
245
|
-
res.writeHead(200, {
|
|
266
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
246
267
|
res.end(JSON.stringify({ url: finalUrl }));
|
|
247
268
|
});
|
|
248
|
-
req.on("error", (err) => {
|
|
249
|
-
if (!isAborted) {
|
|
250
|
-
writeStream.destroy();
|
|
251
|
-
if (fs.existsSync(filePath))
|
|
252
|
-
fs.unlinkSync(filePath);
|
|
253
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
254
|
-
res.end(JSON.stringify({ error: "Upload stream failed" }));
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
269
|
return;
|
|
258
270
|
}
|
|
259
271
|
if (req.method === "DELETE" && pathname === "/api/defect-media") {
|
|
@@ -727,7 +739,11 @@ async function startManualWorkspaceServer() {
|
|
|
727
739
|
}
|
|
728
740
|
}
|
|
729
741
|
});
|
|
730
|
-
const wss = new ws_1.WebSocketServer({
|
|
742
|
+
const wss = new ws_1.WebSocketServer({
|
|
743
|
+
server: httpServer,
|
|
744
|
+
verifyClient: (info) => Boolean(info.origin) &&
|
|
745
|
+
(0, localWorkspace_1.isLocalWorkspaceRequest)(info.req.headers.host, info.origin, info.req.socket.localPort, typeof info.req.headers['sec-fetch-site'] === 'string' ? info.req.headers['sec-fetch-site'] : undefined),
|
|
746
|
+
});
|
|
731
747
|
wss.on("connection", (ws) => {
|
|
732
748
|
ws.send(JSON.stringify({ type: "CLI_CONTEXT", payload: { command: "open" } }));
|
|
733
749
|
ws.on("error", (err) => {
|
|
@@ -738,7 +754,7 @@ async function startManualWorkspaceServer() {
|
|
|
738
754
|
console.error(chalk_1.default.red(`\n[Error]: Failed to start manual workspace server: ${e.message}`));
|
|
739
755
|
process.exit(1);
|
|
740
756
|
});
|
|
741
|
-
httpServer.listen(0, async () => {
|
|
757
|
+
httpServer.listen(0, '127.0.0.1', async () => {
|
|
742
758
|
try {
|
|
743
759
|
const address = httpServer.address();
|
|
744
760
|
const port = typeof address === "object" && address !== null ? address.port : null;
|
|
@@ -746,7 +762,7 @@ async function startManualWorkspaceServer() {
|
|
|
746
762
|
console.error(chalk_1.default.red("\n[Error]: Could not bind to an available port."));
|
|
747
763
|
process.exit(1);
|
|
748
764
|
}
|
|
749
|
-
const localUrl = `http://
|
|
765
|
+
const localUrl = `http://127.0.0.1:${port}`;
|
|
750
766
|
console.log(chalk_1.default.yellow(`\n[Workspace]: Manual Design server running at ${localUrl}`));
|
|
751
767
|
const { default: open } = await import("open");
|
|
752
768
|
await open(localUrl);
|