thinkncollab-shell 0.0.12 → 0.0.14
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/Readme.md +40 -8
- package/package.json +1 -1
- package/src/commands/github/commits.js +58 -0
- package/src/commands/github/issue-create.js +67 -0
- package/src/commands/github/issues.js +57 -0
- package/src/commands/github/pulls.js +58 -0
- package/src/commands/github/statusgh.js +50 -0
- package/src/commands/index.js +30 -26
- package/src/commands/room/leave.js +6 -15
- package/src/commands/shell/status.js +5 -5
- package/src/commands/tasks/complete.js +399 -26
- package/src/commands/tasks/loadtest.js +73 -0
- package/src/commands/tasks/task-status.js +63 -52
- package/src/commands/tasks/task.js +11 -1
- package/src/commands/terminal/share.js +1 -1
- package/src/core/geekload.js +524 -0
- package/src/core/notification-window.js +25 -5
- package/src/core/shell.js +3 -2
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const crypto = require('crypto');
|
|
5
|
+
const cp = require('child_process');
|
|
6
|
+
const path = require('path');
|
|
5
7
|
|
|
6
8
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
7
9
|
|
|
@@ -381,46 +383,177 @@ async function runBrowserTest(testConfig, testUser, boardAuth = {}) {
|
|
|
381
383
|
}
|
|
382
384
|
}
|
|
383
385
|
|
|
386
|
+
// Helper to run a local command and capture exitCode & stdout/stderr
|
|
387
|
+
function runLocalCommand(command) {
|
|
388
|
+
return new Promise((resolve) => {
|
|
389
|
+
cp.exec(command, (error, stdout, stderr) => {
|
|
390
|
+
const exitCode = error ? (error.code || 1) : 0;
|
|
391
|
+
resolve({
|
|
392
|
+
exitCode,
|
|
393
|
+
stdout: stdout.toString(),
|
|
394
|
+
stderr: stderr.toString()
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
384
400
|
// ─── Local Judge ──────────────────────────────────────────────────────────────
|
|
385
401
|
function judgeLocally(testConfig, result) {
|
|
386
|
-
const { type, expect } = testConfig;
|
|
402
|
+
const { type, value, condition, expect } = testConfig;
|
|
387
403
|
const failures = [];
|
|
388
404
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
405
|
+
try {
|
|
406
|
+
switch (type) {
|
|
407
|
+
case 'exitCode': {
|
|
408
|
+
const passed = result.exitCode === value;
|
|
409
|
+
return {
|
|
410
|
+
passed,
|
|
411
|
+
reason: passed
|
|
412
|
+
? `Exit code matched: ${value}`
|
|
413
|
+
: `Expected exit code ${value}, got ${result.exitCode}`
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
case 'stdout': {
|
|
418
|
+
const actualOut = (result.stdout || '').trim();
|
|
419
|
+
const expectedOut = (value || '').trim();
|
|
420
|
+
const passed = actualOut === expectedOut;
|
|
421
|
+
return {
|
|
422
|
+
passed,
|
|
423
|
+
reason: passed ? `Output matched` : `Output mismatch`,
|
|
424
|
+
diff: passed ? undefined : { expected: expectedOut, actual: actualOut }
|
|
425
|
+
};
|
|
402
426
|
}
|
|
427
|
+
|
|
428
|
+
case 'regex': {
|
|
429
|
+
const pattern = new RegExp(value);
|
|
430
|
+
const target = result.stdout || result.output || '';
|
|
431
|
+
const passed = pattern.test(target);
|
|
432
|
+
return {
|
|
433
|
+
passed,
|
|
434
|
+
reason: passed ? `Regex matched: ${value}` : `Regex did not match: ${value}`
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
case 'http': {
|
|
439
|
+
if (expect?.status !== undefined && result.status !== expect.status) {
|
|
440
|
+
failures.push(`Status: expected ${expect.status}, got ${result.status}`);
|
|
441
|
+
}
|
|
442
|
+
if (expect?.body) {
|
|
443
|
+
for (const [key, condition] of Object.entries(expect.body)) {
|
|
444
|
+
const actualVal = getNestedValue(result.body || {}, key);
|
|
445
|
+
if (condition?.exists) {
|
|
446
|
+
if (actualVal === undefined || actualVal === null)
|
|
447
|
+
failures.push(`body.${key}: expected to exist`);
|
|
448
|
+
} else if (condition?.eq !== undefined) {
|
|
449
|
+
if (actualVal !== condition.eq)
|
|
450
|
+
failures.push(`body.${key}: expected "${condition.eq}", got "${actualVal}"`);
|
|
451
|
+
} else if (condition?.contains !== undefined) {
|
|
452
|
+
if (!String(actualVal).includes(String(condition.contains)))
|
|
453
|
+
failures.push(`body.${key}: expected to contain "${condition.contains}"`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
case 'json': {
|
|
461
|
+
const actualData = result.output || result.json || {};
|
|
462
|
+
if (condition) {
|
|
463
|
+
return judgeWithConditions(condition, actualData);
|
|
464
|
+
} else {
|
|
465
|
+
const passed = JSON.stringify(value) === JSON.stringify(actualData);
|
|
466
|
+
return {
|
|
467
|
+
passed,
|
|
468
|
+
reason: passed ? `JSON matched` : `JSON mismatch`,
|
|
469
|
+
diff: passed ? undefined : { expected: value, actual: actualData }
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
case 'loadtest':
|
|
474
|
+
case 'geekload': {
|
|
475
|
+
const actualData = result.output || result.json || {};
|
|
476
|
+
const cond = condition || expect?.condition || expect || {};
|
|
477
|
+
return judgeWithConditions(cond, actualData);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
case 'browser': {
|
|
481
|
+
if (expect?.element && !result.elementExists) {
|
|
482
|
+
failures.push(`Element "${expect.element}" not found on page`);
|
|
483
|
+
}
|
|
484
|
+
if (expect?.text && result.elementText) {
|
|
485
|
+
if (!result.elementText.includes(expect.text))
|
|
486
|
+
failures.push(`Element text: expected to contain "${expect.text}", got "${result.elementText}"`);
|
|
487
|
+
}
|
|
488
|
+
if (expect?.redirectUrl) {
|
|
489
|
+
try {
|
|
490
|
+
const actualPath = new URL(result.url).pathname;
|
|
491
|
+
if (!actualPath.includes(expect.redirectUrl))
|
|
492
|
+
failures.push(`URL: expected "${expect.redirectUrl}", got "${actualPath}"`);
|
|
493
|
+
} catch {
|
|
494
|
+
failures.push(`Invalid URL in result: "${result.url}"`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
case 'tnc-build': {
|
|
501
|
+
const passed = result.exitCode === 0;
|
|
502
|
+
return {
|
|
503
|
+
passed,
|
|
504
|
+
reason: passed ? 'TNC Local CI pipeline passed' : 'TNC Local CI pipeline failed'
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
default:
|
|
509
|
+
return { passed: false, reason: `Unknown test type: ${type}` };
|
|
403
510
|
}
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
passed: failures.length === 0,
|
|
514
|
+
reason: failures.length === 0 ? 'All checks passed' : failures.join(' | '),
|
|
515
|
+
failures,
|
|
516
|
+
};
|
|
517
|
+
} catch (err) {
|
|
518
|
+
return { passed: false, reason: `Judge error: ${err.message}` };
|
|
404
519
|
}
|
|
520
|
+
}
|
|
405
521
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
failures.push(`Element "${expect.element}" not found on page`);
|
|
522
|
+
function judgeWithConditions(conditions, actual) {
|
|
523
|
+
const failures = [];
|
|
409
524
|
|
|
410
|
-
|
|
411
|
-
|
|
525
|
+
for (const [key, ops] of Object.entries(conditions)) {
|
|
526
|
+
const actualVal = actual[key];
|
|
412
527
|
|
|
413
|
-
if (
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
528
|
+
if (actualVal === undefined) {
|
|
529
|
+
failures.push(`Missing key: "${key}"`);
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
for (const [op, expected] of Object.entries(ops)) {
|
|
534
|
+
let pass = false;
|
|
535
|
+
switch (op) {
|
|
536
|
+
case 'gte': pass = actualVal >= expected; break;
|
|
537
|
+
case 'lte': pass = actualVal <= expected; break;
|
|
538
|
+
case 'gt': pass = actualVal > expected; break;
|
|
539
|
+
case 'lt': pass = actualVal < expected; break;
|
|
540
|
+
case 'eq': pass = actualVal === expected; break;
|
|
541
|
+
case 'contains': pass = String(actualVal).includes(String(expected)); break;
|
|
542
|
+
case 'regex': pass = new RegExp(expected).test(String(actualVal)); break;
|
|
543
|
+
default: pass = false;
|
|
544
|
+
}
|
|
545
|
+
if (!pass) {
|
|
546
|
+
failures.push(`"${key}" failed: ${actualVal} ${op} ${expected}`);
|
|
547
|
+
}
|
|
417
548
|
}
|
|
418
549
|
}
|
|
419
550
|
|
|
420
551
|
return {
|
|
421
|
-
passed:
|
|
422
|
-
reason:
|
|
423
|
-
|
|
552
|
+
passed: failures.length === 0,
|
|
553
|
+
reason: failures.length === 0
|
|
554
|
+
? `All conditions passed`
|
|
555
|
+
: `Failed conditions: ${failures.join('; ')}`,
|
|
556
|
+
diff: failures.length > 0 ? { conditions, actual } : undefined
|
|
424
557
|
};
|
|
425
558
|
}
|
|
426
559
|
|
|
@@ -525,6 +658,246 @@ const authConfig = { ...boardAuth, ...(testConfig.auth || {}) };
|
|
|
525
658
|
if (result.elementText)
|
|
526
659
|
console.log(chalk.dim(` → Text : "${result.elementText.trim().slice(0, 60)}"`));
|
|
527
660
|
|
|
661
|
+
} else if (testConfig.type === 'exitCode' || testConfig.type === 'stdout' || testConfig.type === 'regex') {
|
|
662
|
+
const cmd = testConfig.command || testConfig.script;
|
|
663
|
+
if (!cmd) {
|
|
664
|
+
throw new Error(`testConfig type "${testConfig.type}" requires a "command" or "script" to execute.`);
|
|
665
|
+
}
|
|
666
|
+
console.log(chalk.dim(` → Executing local command: "${cmd}"`));
|
|
667
|
+
const runRes = await runLocalCommand(cmd);
|
|
668
|
+
result = {
|
|
669
|
+
exitCode: runRes.exitCode,
|
|
670
|
+
stdout: runRes.stdout,
|
|
671
|
+
stderr: runRes.stderr
|
|
672
|
+
};
|
|
673
|
+
console.log(chalk.dim(` → Exit Code: ${result.exitCode}`));
|
|
674
|
+
if (result.stdout) console.log(chalk.dim(` → Stdout : ${result.stdout.trim().slice(0, 80)}`));
|
|
675
|
+
|
|
676
|
+
} else if (testConfig.type === 'json') {
|
|
677
|
+
const cmd = testConfig.command || testConfig.script;
|
|
678
|
+
if (cmd) {
|
|
679
|
+
console.log(chalk.dim(` → Executing local command: "${cmd}"`));
|
|
680
|
+
const runRes = await runLocalCommand(cmd);
|
|
681
|
+
let parsedJson = {};
|
|
682
|
+
try {
|
|
683
|
+
parsedJson = JSON.parse(runRes.stdout.trim());
|
|
684
|
+
} catch (e) {
|
|
685
|
+
throw new Error(`Command executed but stdout was not valid JSON: ${runRes.stdout}`);
|
|
686
|
+
}
|
|
687
|
+
result = {
|
|
688
|
+
exitCode: runRes.exitCode,
|
|
689
|
+
stdout: runRes.stdout,
|
|
690
|
+
stderr: runRes.stderr,
|
|
691
|
+
json: parsedJson,
|
|
692
|
+
output: parsedJson
|
|
693
|
+
};
|
|
694
|
+
} else {
|
|
695
|
+
throw new Error('testConfig type "json" requires a "command" or "script" to output JSON.');
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
} else if (testConfig.type === 'loadtest' || testConfig.type === 'geekload') {
|
|
699
|
+
const script = testConfig.script || testConfig.command;
|
|
700
|
+
if (!script) {
|
|
701
|
+
throw new Error(`testConfig type "${testConfig.type}" requires a "script" or "command" specifying the load test script path.`);
|
|
702
|
+
}
|
|
703
|
+
const scriptPath = path.resolve(process.cwd(), script);
|
|
704
|
+
console.log(chalk.dim(` → Running GeekLoad test script: ${script}`));
|
|
705
|
+
const { runLoadTest } = require('../../core/geekload');
|
|
706
|
+
const loadtestResult = await runLoadTest(scriptPath);
|
|
707
|
+
|
|
708
|
+
const Table = require('cli-table3');
|
|
709
|
+
const table = new Table({
|
|
710
|
+
head: [chalk.cyan('Metric'), chalk.cyan('Value')],
|
|
711
|
+
colWidths: [25, 20]
|
|
712
|
+
});
|
|
713
|
+
table.push(
|
|
714
|
+
['Virtual Users', loadtestResult.virtual_users],
|
|
715
|
+
['Duration (sec)', loadtestResult.duration_sec],
|
|
716
|
+
['Total Requests', loadtestResult.total_requests],
|
|
717
|
+
['Failed Requests', loadtestResult.failed_requests],
|
|
718
|
+
['Failure Rate', `${(loadtestResult.failure_rate * 100).toFixed(2)}%`],
|
|
719
|
+
['p95 Latency (ms)', loadtestResult.p95_latency_ms]
|
|
720
|
+
);
|
|
721
|
+
console.log('\n' + table.toString() + '\n');
|
|
722
|
+
|
|
723
|
+
result = {
|
|
724
|
+
exitCode: loadtestResult.failed_requests > 0 ? 1 : 0,
|
|
725
|
+
output: loadtestResult,
|
|
726
|
+
json: loadtestResult
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
} else if (testConfig.type === 'tnc-build') {
|
|
730
|
+
const fs = require('fs');
|
|
731
|
+
const path = require('path');
|
|
732
|
+
const { spawn } = require('child_process');
|
|
733
|
+
|
|
734
|
+
const projectRoot = process.cwd();
|
|
735
|
+
const workflowsDir = path.join(projectRoot, '.tnc/workflows');
|
|
736
|
+
|
|
737
|
+
if (!fs.existsSync(workflowsDir)) {
|
|
738
|
+
throw new Error('No .tnc/workflows directory found in project root.');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const files = fs.readdirSync(workflowsDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
|
|
742
|
+
if (files.length === 0) {
|
|
743
|
+
throw new Error('No workflow files found in .tnc/workflows/');
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const workflowFile = files[0];
|
|
747
|
+
console.log(chalk.dim(` → Found workflow file: ${workflowFile}`));
|
|
748
|
+
const content = fs.readFileSync(path.join(workflowsDir, workflowFile), 'utf8');
|
|
749
|
+
|
|
750
|
+
// Parse steps
|
|
751
|
+
const lines = content.split('\n');
|
|
752
|
+
let workflowName = workflowFile;
|
|
753
|
+
let steps = [];
|
|
754
|
+
let currentStepName = '';
|
|
755
|
+
|
|
756
|
+
for (let i = 0; i < lines.length; i++) {
|
|
757
|
+
const line = lines[i].trim();
|
|
758
|
+
if (line.startsWith('name:')) {
|
|
759
|
+
workflowName = line.substring(5).trim().replace(/['"]/g, '');
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
if (line.startsWith('-')) {
|
|
763
|
+
const cleaned = line.substring(1).trim();
|
|
764
|
+
if (cleaned.startsWith('name:')) {
|
|
765
|
+
currentStepName = cleaned.substring(5).trim().replace(/['"]/g, '');
|
|
766
|
+
} else if (cleaned.startsWith('run:')) {
|
|
767
|
+
let runCmd = cleaned.substring(4).trim();
|
|
768
|
+
if ((runCmd.startsWith('"') && runCmd.endsWith('"')) || (runCmd.startsWith("'") && runCmd.endsWith("'"))) {
|
|
769
|
+
runCmd = runCmd.substring(1, runCmd.length - 1);
|
|
770
|
+
}
|
|
771
|
+
if (runCmd === '|') {
|
|
772
|
+
let block = [];
|
|
773
|
+
let j = i + 1;
|
|
774
|
+
while (j < lines.length) {
|
|
775
|
+
const next = lines[j];
|
|
776
|
+
const match = next.match(/^(\s+)(.*)/);
|
|
777
|
+
if (match && match[1].length > 4) {
|
|
778
|
+
block.push(next.trim());
|
|
779
|
+
j++;
|
|
780
|
+
} else {
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
runCmd = block.join('\n');
|
|
785
|
+
i = j - 1;
|
|
786
|
+
}
|
|
787
|
+
steps.push({
|
|
788
|
+
name: currentStepName || `run: ${runCmd.substring(0, 30)}`,
|
|
789
|
+
run: runCmd
|
|
790
|
+
});
|
|
791
|
+
currentStepName = '';
|
|
792
|
+
}
|
|
793
|
+
} else if (line.startsWith('name:')) {
|
|
794
|
+
currentStepName = line.substring(5).trim().replace(/['"]/g, '');
|
|
795
|
+
} else if (line.startsWith('run:')) {
|
|
796
|
+
let runCmd = line.substring(4).trim();
|
|
797
|
+
if ((runCmd.startsWith('"') && runCmd.endsWith('"')) || (runCmd.startsWith("'") && runCmd.endsWith("'"))) {
|
|
798
|
+
runCmd = runCmd.substring(1, runCmd.length - 1);
|
|
799
|
+
}
|
|
800
|
+
if (runCmd === '|') {
|
|
801
|
+
let block = [];
|
|
802
|
+
let j = i + 1;
|
|
803
|
+
while (j < lines.length) {
|
|
804
|
+
const next = lines[j];
|
|
805
|
+
const match = next.match(/^(\s+)(.*)/);
|
|
806
|
+
if (match && match[1].length > 4) {
|
|
807
|
+
block.push(next.trim());
|
|
808
|
+
j++;
|
|
809
|
+
} else {
|
|
810
|
+
break;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
runCmd = block.join('\n');
|
|
814
|
+
i = j - 1;
|
|
815
|
+
}
|
|
816
|
+
steps.push({
|
|
817
|
+
name: currentStepName || `run: ${runCmd.substring(0, 30)}`,
|
|
818
|
+
run: runCmd
|
|
819
|
+
});
|
|
820
|
+
currentStepName = '';
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (steps.length === 0) {
|
|
825
|
+
throw new Error('No steps found in the workflow file.');
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
console.log(chalk.cyan(`\n🚀 [TNC Actions] Starting Local Workflow: "${workflowName}"\n`));
|
|
829
|
+
|
|
830
|
+
const stepResults = [];
|
|
831
|
+
let overallPassed = true;
|
|
832
|
+
let logsBuffer = '';
|
|
833
|
+
|
|
834
|
+
for (let s = 0; s < steps.length; s++) {
|
|
835
|
+
const step = steps[s];
|
|
836
|
+
console.log(chalk.cyan(`⚙️ [Step ${s+1}/${steps.length}] "${step.name}"`));
|
|
837
|
+
console.log(chalk.gray(`$ ${step.run}`));
|
|
838
|
+
|
|
839
|
+
// Run command shell execution
|
|
840
|
+
const exitCode = await new Promise((resolve) => {
|
|
841
|
+
const shellCmd = process.platform === 'win32' ? 'cmd.exe' : '/bin/sh';
|
|
842
|
+
const shellArgs = process.platform === 'win32' ? ['/s', '/c', step.run] : ['-c', step.run];
|
|
843
|
+
|
|
844
|
+
const proc = spawn(shellCmd, shellArgs, {
|
|
845
|
+
cwd: projectRoot,
|
|
846
|
+
env: { ...process.env, PATH: process.env.PATH, PUPPETEER_SKIP_DOWNLOAD: 'true' }
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
proc.stdout.on('data', (chunk) => {
|
|
850
|
+
const chunkStr = chunk.toString();
|
|
851
|
+
process.stdout.write(chunkStr);
|
|
852
|
+
logsBuffer += chunkStr;
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
proc.stderr.on('data', (chunk) => {
|
|
856
|
+
const chunkStr = chunk.toString();
|
|
857
|
+
process.stderr.write(chalk.red(chunkStr));
|
|
858
|
+
logsBuffer += chunkStr;
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
proc.on('close', (code) => {
|
|
862
|
+
resolve(code);
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
const stepPassed = (exitCode === 0);
|
|
867
|
+
stepResults.push({
|
|
868
|
+
name: step.name,
|
|
869
|
+
command: step.run,
|
|
870
|
+
exitCode,
|
|
871
|
+
passed: stepPassed
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
if (!stepPassed) {
|
|
875
|
+
overallPassed = false;
|
|
876
|
+
console.log(chalk.red(`\n❌ Step "${step.name}" failed with exit code: ${exitCode}. Aborting.\n`));
|
|
877
|
+
break;
|
|
878
|
+
} else {
|
|
879
|
+
console.log(chalk.green(`\n✅ Step "${step.name}" passed successfully.\n`));
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
result = {
|
|
884
|
+
exitCode: overallPassed ? 0 : 1,
|
|
885
|
+
stdout: logsBuffer,
|
|
886
|
+
stderr: '',
|
|
887
|
+
output: {
|
|
888
|
+
workflow: workflowName,
|
|
889
|
+
status: overallPassed ? 'passed' : 'failed',
|
|
890
|
+
stepsCount: steps.length,
|
|
891
|
+
stepsRun: stepResults
|
|
892
|
+
},
|
|
893
|
+
json: {
|
|
894
|
+
workflow: workflowName,
|
|
895
|
+
status: overallPassed ? 'passed' : 'failed',
|
|
896
|
+
stepsCount: steps.length,
|
|
897
|
+
stepsRun: stepResults
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
|
|
528
901
|
} else {
|
|
529
902
|
console.log(chalk.red(`❌ Unsupported test type: ${testConfig.type}`));
|
|
530
903
|
return;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const { runLoadTest } = require('../../core/geekload');
|
|
5
|
+
const Table = require('cli-table3');
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
name: 'loadtest',
|
|
9
|
+
description: 'Run a local load test script using GeekLoad',
|
|
10
|
+
aliases: ['geekload'],
|
|
11
|
+
requiresAuth: false,
|
|
12
|
+
|
|
13
|
+
async execute(args, shell) {
|
|
14
|
+
if (!args[0]) {
|
|
15
|
+
console.log(chalk.red('Usage: loadtest <script-path>'));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const scriptPath = path.resolve(process.cwd(), args[0]);
|
|
20
|
+
if (!fs.existsSync(scriptPath)) {
|
|
21
|
+
console.log(chalk.red(`❌ Script not found: ${args[0]}`));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(chalk.cyan(`\n📋 Loading test script: ${args[0]}`));
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const results = await runLoadTest(scriptPath);
|
|
29
|
+
|
|
30
|
+
const table = new Table({
|
|
31
|
+
head: [chalk.green('Metric'), chalk.green('Value')],
|
|
32
|
+
colWidths: [25, 20]
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
table.push(
|
|
36
|
+
['Virtual Users', results.virtual_users],
|
|
37
|
+
['Duration (sec)', results.duration_sec],
|
|
38
|
+
['Total Requests', results.total_requests],
|
|
39
|
+
['Failed Requests', results.failed_requests],
|
|
40
|
+
['Requests/sec (RPS)', results.requests_per_sec],
|
|
41
|
+
['Failure Rate', `${(results.failure_rate * 100).toFixed(2)}%`],
|
|
42
|
+
['Average Latency (ms)', results.avg_latency_ms],
|
|
43
|
+
['p95 Latency (ms)', results.p95_latency_ms],
|
|
44
|
+
['p99 Latency (ms)', results.p99_latency_ms]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
console.log(table.toString());
|
|
48
|
+
|
|
49
|
+
if (results.failures.length > 0) {
|
|
50
|
+
console.log(chalk.yellow(`\n⚠️ Recorded Failures (${results.failures.length}):`));
|
|
51
|
+
results.failures.slice(0, 5).forEach((f, idx) => {
|
|
52
|
+
console.log(chalk.dim(` ${idx + 1}. [${f.method}] ${f.url}`));
|
|
53
|
+
f.failures.forEach(msg => {
|
|
54
|
+
console.log(chalk.red(` - ${msg}`));
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
if (results.failures.length > 5) {
|
|
58
|
+
console.log(chalk.dim(` ... and ${results.failures.length - 5} more failures`));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (results.errors.length > 0) {
|
|
63
|
+
console.log(chalk.red(`\n❌ Execution Errors (${results.errors.length}):`));
|
|
64
|
+
results.errors.slice(0, 5).forEach((e, idx) => {
|
|
65
|
+
console.log(chalk.red(` VU ${e.vuId} Error: ${e.message}`));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.log(chalk.red(`❌ Load test execution failed: ${err.message}`));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
@@ -2,60 +2,71 @@
|
|
|
2
2
|
const chalk = require('chalk');
|
|
3
3
|
|
|
4
4
|
const STATUS_MAP = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
accept: 'accepted',
|
|
6
|
+
start: 'inprogress',
|
|
7
|
+
complete: 'completed',
|
|
8
|
+
reject: 'rejected',
|
|
9
|
+
done: 'completed',
|
|
10
|
+
'task-status': null, // requires explicit arg
|
|
10
11
|
};
|
|
11
12
|
|
|
12
13
|
module.exports = {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
14
|
+
name: 'task-status',
|
|
15
|
+
description: 'Update your task status',
|
|
16
|
+
aliases: ['accept', 'start', 'complete', 'reject', 'done'],
|
|
17
|
+
requiresAuth: true,
|
|
18
|
+
requiresRoom: true,
|
|
19
|
+
|
|
20
|
+
async execute(args, shell, invokedAs) {
|
|
21
|
+
const roomId = shell.ws.getCurrentRoom(); // ← top pe, ek baar
|
|
22
|
+
const taskId = args[0];
|
|
23
|
+
const verb = invokedAs || shell._lastCommand || 'task-status';
|
|
24
|
+
const status = STATUS_MAP[verb] ?? args[1];
|
|
25
|
+
|
|
26
|
+
if (!taskId) {
|
|
27
|
+
console.log(chalk.yellow(' Usage: accept <taskId> | start <taskId> | complete <taskId>'));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (!status) {
|
|
31
|
+
console.log(chalk.yellow(' Usage: task-status <taskId> <accepted|inprogress|completed|rejected>'));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const result = await shell.api._request(
|
|
37
|
+
'POST',
|
|
38
|
+
`/thinknsh/${roomId}/tasks/${taskId}/status`,
|
|
39
|
+
{ status }
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const statusColors = {
|
|
43
|
+
accepted: chalk.green,
|
|
44
|
+
inprogress: chalk.blue,
|
|
45
|
+
completed: chalk.green,
|
|
46
|
+
rejected: chalk.red,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const col = statusColors[status] || chalk.white;
|
|
50
|
+
console.log(col(`\n ✓ Task "${result.taskTitle}" → ${status}`));
|
|
51
|
+
|
|
52
|
+
if (result.allCompleted) {
|
|
53
|
+
console.log(chalk.green(' 🎉 All assignees done — task fully completed!\n'));
|
|
54
|
+
} else {
|
|
55
|
+
console.log('');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Socket emit
|
|
59
|
+
if (shell.ws.isConnected?.() && shell.ws.socket) {
|
|
60
|
+
shell.ws.socket.emit('task:' + (status === 'inprogress' ? 'started' : status), {
|
|
61
|
+
roomId,
|
|
62
|
+
taskId,
|
|
63
|
+
taskTitle: result.taskTitle,
|
|
64
|
+
userId: shell.api.getUser()?._id,
|
|
65
|
+
});
|
|
60
66
|
}
|
|
67
|
+
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.log(chalk.red(`\n ❌ ${err.message}\n`));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
61
72
|
};
|