@arcadialdev/arcality 2.4.24 → 2.4.25
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/.agents/skills/e2e-testing-expert/SKILL.md +28 -28
- package/.agents/skills/security-qa/SKILL.md +254 -0
- package/README.md +95 -95
- package/bin/arcality.mjs +55 -55
- package/package.json +68 -68
- package/playwright.config.js +33 -33
- package/scripts/gen-and-run.mjs +4 -1
- package/scripts/init.mjs +297 -297
- package/scripts/postinstall.mjs +77 -77
- package/scripts/setup.mjs +166 -166
- package/src/configLoader.mjs +179 -179
- package/src/configManager.mjs +172 -172
- package/src/envSetup.ts +205 -205
- package/src/index.ts +25 -25
- package/src/services/collectiveMemoryService.ts +1 -1
- package/src/services/securityScanner.ts +128 -0
- package/tests/_helpers/ArcalityReporter.js +632 -229
- package/tests/_helpers/ArcalityReporter.ts +262 -27
- package/tests/_helpers/agentic-runner.bundle.spec.js +357 -5
- package/tests/_helpers/agentic-runner.spec.ts +37 -1
- package/tests/_helpers/ai-agent-helper.ts +128 -6
- package/tests/_helpers/qa-security-tools.ts +265 -0
|
@@ -43,7 +43,12 @@ class ArcalityReporter implements Reporter {
|
|
|
43
43
|
|
|
44
44
|
// Handle attachments: copy to report dir
|
|
45
45
|
const processedAttachments = result.attachments.map(att => {
|
|
46
|
-
|
|
46
|
+
let extension = '.png';
|
|
47
|
+
if (att.contentType === 'application/json') extension = '.json';
|
|
48
|
+
else if (att.contentType === 'text/plain') extension = '.txt';
|
|
49
|
+
else if (att.contentType === 'text/yaml') extension = '.yaml';
|
|
50
|
+
|
|
51
|
+
const fileName = att.path ? path.basename(att.path) : `${att.name || 'attachment'}-${Date.now()}${extension}`;
|
|
47
52
|
const attachmentsDir = path.join(this.outputDir, 'attachments');
|
|
48
53
|
const destPath = path.join(attachmentsDir, fileName);
|
|
49
54
|
|
|
@@ -67,8 +72,10 @@ class ArcalityReporter implements Reporter {
|
|
|
67
72
|
});
|
|
68
73
|
|
|
69
74
|
if (result.status === 'failed' || result.error) {
|
|
70
|
-
console.log(
|
|
71
|
-
|
|
75
|
+
console.log(`
|
|
76
|
+
❌ [TEST ENGINE FAILURE] ${test.title} failed:`);
|
|
77
|
+
console.log(`${result.error?.message || result.error?.stack || 'Unknown error'}
|
|
78
|
+
`);
|
|
72
79
|
if (result.error?.stack) {
|
|
73
80
|
console.log(result.error.stack);
|
|
74
81
|
}
|
|
@@ -361,6 +368,92 @@ class ArcalityReporter implements Reporter {
|
|
|
361
368
|
.tag { padding: 4px 8px; border-radius: 4px; font-size: 0.7rem; font-weight: 800; text-transform: uppercase; }
|
|
362
369
|
.tag-project { background: rgba(192, 132, 252, 0.2); color: var(--accent); }
|
|
363
370
|
.tag-retry { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
|
|
371
|
+
|
|
372
|
+
/* SECURITY REPORT SECTION */
|
|
373
|
+
.security-section {
|
|
374
|
+
margin: 2rem 0;
|
|
375
|
+
background: var(--bg-card);
|
|
376
|
+
border-radius: 16px;
|
|
377
|
+
overflow: hidden;
|
|
378
|
+
border: 1px solid #4a4a4a
|
|
379
|
+
}
|
|
380
|
+
.security-header {
|
|
381
|
+
padding: 1rem 1.5rem;
|
|
382
|
+
cursor: pointer;
|
|
383
|
+
display: flex;
|
|
384
|
+
align-items: center;
|
|
385
|
+
gap: 12px;
|
|
386
|
+
background: linear-gradient(90deg, rgba(245, 158, 11, 0.1), rgba(245, 158, 11, 0.0) 70%);
|
|
387
|
+
}
|
|
388
|
+
.security-header:hover {
|
|
389
|
+
background: linear-gradient(90deg, rgba(245, 158, 11, 0.15), rgba(245, 158, 11, 0.05) 70%);
|
|
390
|
+
}
|
|
391
|
+
.security-header h3 {
|
|
392
|
+
margin: 0;
|
|
393
|
+
font-size: 1.2rem;
|
|
394
|
+
color: var(--warning);
|
|
395
|
+
font-weight: 700;
|
|
396
|
+
}
|
|
397
|
+
.security-risk-badge {
|
|
398
|
+
padding: 4px 10px;
|
|
399
|
+
border-radius: 6px;
|
|
400
|
+
font-size: 0.8rem;
|
|
401
|
+
font-weight: 800;
|
|
402
|
+
color: white;
|
|
403
|
+
margin-left: auto;
|
|
404
|
+
}
|
|
405
|
+
.security-details {
|
|
406
|
+
padding: 1.5rem;
|
|
407
|
+
background: #0c1322;
|
|
408
|
+
}
|
|
409
|
+
.security-summary {
|
|
410
|
+
display: grid;
|
|
411
|
+
grid-template-columns: repeat(4, 1fr);
|
|
412
|
+
gap: 1rem;
|
|
413
|
+
margin-bottom: 2rem;
|
|
414
|
+
padding-bottom: 1.5rem;
|
|
415
|
+
border-bottom: 1px solid var(--border);
|
|
416
|
+
text-align: center;
|
|
417
|
+
}
|
|
418
|
+
.summary-item .count {
|
|
419
|
+
font-size: 1.5rem;
|
|
420
|
+
font-weight: 800;
|
|
421
|
+
}
|
|
422
|
+
.summary-item .label {
|
|
423
|
+
font-size: 0.7rem;
|
|
424
|
+
text-transform: uppercase;
|
|
425
|
+
color: var(--text-muted);
|
|
426
|
+
}
|
|
427
|
+
.count-Critical { color: var(--error); }
|
|
428
|
+
.count-High { color: #fb923c; }
|
|
429
|
+
.count-Medium { color: var(--warning); }
|
|
430
|
+
.count-Low { color: #38bdf8; }
|
|
431
|
+
|
|
432
|
+
.vuln-list { display: flex; flex-direction: column; gap: 1rem; }
|
|
433
|
+
.vuln-card {
|
|
434
|
+
background: var(--bg-deep);
|
|
435
|
+
border: 1px solid var(--border);
|
|
436
|
+
border-left-width: 4px;
|
|
437
|
+
border-radius: 8px;
|
|
438
|
+
padding: 1.5rem;
|
|
439
|
+
}
|
|
440
|
+
.vuln-card-title { font-weight: 700; margin-bottom: 0.5rem; display: flex; align-items: center; gap: 10px;}
|
|
441
|
+
.vuln-card-desc { color: var(--text-muted); font-size: 0.9rem; margin-bottom: 1rem; }
|
|
442
|
+
.vuln-card-evidence {
|
|
443
|
+
background: #020617;
|
|
444
|
+
padding: 0.75rem;
|
|
445
|
+
border-radius: 4px;
|
|
446
|
+
font-family: 'JetBrains Mono', monospace;
|
|
447
|
+
font-size: 0.8rem;
|
|
448
|
+
white-space: pre-wrap;
|
|
449
|
+
word-break: break-all;
|
|
450
|
+
margin-bottom: 1rem;
|
|
451
|
+
border: 1px solid var(--border);
|
|
452
|
+
max-height: 200px;
|
|
453
|
+
overflow-y: auto;
|
|
454
|
+
}
|
|
455
|
+
.vuln-card-remediation { font-size: 0.9rem; color: var(--success); }
|
|
456
|
+
pre { margin: 0; }
|
|
364
457
|
</style>
|
|
365
458
|
</head>
|
|
366
459
|
<body>
|
|
@@ -389,12 +482,10 @@ class ArcalityReporter implements Reporter {
|
|
|
389
482
|
${this.results.map((r, i) => {
|
|
390
483
|
const statusClass = `status-${r.result.status}`;
|
|
391
484
|
const projectName = r.test.parent.project()?.name || 'default';
|
|
392
|
-
|
|
393
|
-
const traceFile = r.attachments.find(a => a.name === 'trace')?.path;
|
|
394
|
-
|
|
485
|
+
|
|
395
486
|
return `
|
|
396
487
|
<div class="test-entry ${statusClass}">
|
|
397
|
-
<div class="test-header" onclick="toggleDetails(
|
|
488
|
+
<div class="test-header" onclick="toggleDetails('details-${i}')">
|
|
398
489
|
<div class="test-info">
|
|
399
490
|
<span class="status-dot"></span>
|
|
400
491
|
<div>
|
|
@@ -411,14 +502,12 @@ class ArcalityReporter implements Reporter {
|
|
|
411
502
|
</div>
|
|
412
503
|
</div>
|
|
413
504
|
|
|
414
|
-
<div class="test-details" id="details-${i}">
|
|
505
|
+
<div class="test-details" id="details-${i}" style="display: none;">
|
|
415
506
|
${r.result.status === 'passed' ? (() => {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
return '';
|
|
421
|
-
})() : ''}
|
|
507
|
+
const successSummary = r.attachments.find(a => a.name === 'success_summary');
|
|
508
|
+
const text = this.getAttachmentText(successSummary);
|
|
509
|
+
return text ? `<div class="success-block">${this.escapeHtml(text)}</div>` : '';
|
|
510
|
+
})() : ''}
|
|
422
511
|
|
|
423
512
|
${r.result.error ? `
|
|
424
513
|
<div class="steps-section">
|
|
@@ -426,6 +515,8 @@ class ArcalityReporter implements Reporter {
|
|
|
426
515
|
<div class="error-block">${this.escapeHtml(r.result.error.message || 'Unknown Failure')}</div>
|
|
427
516
|
</div>
|
|
428
517
|
` : ''}
|
|
518
|
+
|
|
519
|
+
${this._renderSecuritySection(r.attachments, i)}
|
|
429
520
|
|
|
430
521
|
<div class="steps-section">
|
|
431
522
|
<span class="section-title">🧠 Agent Cognitive Process</span>
|
|
@@ -439,31 +530,31 @@ class ArcalityReporter implements Reporter {
|
|
|
439
530
|
<span class="section-title">Visual Evidence</span>
|
|
440
531
|
<div class="attachments-grid">
|
|
441
532
|
${r.attachments.map(att => {
|
|
442
|
-
|
|
443
|
-
|
|
533
|
+
if (att.contentType.startsWith('image/')) {
|
|
534
|
+
return `
|
|
444
535
|
<div class="attachment-item">
|
|
445
536
|
<img src="${att.path}" alt="${att.name}" onclick="window.open('${att.path}')">
|
|
446
537
|
<a href="${att.path}" target="_blank" class="att-label">${att.name}</a>
|
|
447
538
|
</div>
|
|
448
539
|
`;
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
540
|
+
}
|
|
541
|
+
if (att.contentType.startsWith('video/')) {
|
|
542
|
+
return `
|
|
452
543
|
<div class="attachment-item">
|
|
453
544
|
<video controls src="${att.path}"></video>
|
|
454
545
|
<a href="${att.path}" target="_blank" class="att-label">${att.name}</a>
|
|
455
546
|
</div>
|
|
456
547
|
`;
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
548
|
+
}
|
|
549
|
+
return '';
|
|
550
|
+
}).join('')}
|
|
460
551
|
</div>
|
|
461
552
|
</div>
|
|
462
553
|
` : ''}
|
|
463
554
|
</div>
|
|
464
555
|
</div>
|
|
465
556
|
`;
|
|
466
|
-
|
|
557
|
+
}).join('')}
|
|
467
558
|
</div>
|
|
468
559
|
|
|
469
560
|
<footer>
|
|
@@ -473,17 +564,161 @@ class ArcalityReporter implements Reporter {
|
|
|
473
564
|
|
|
474
565
|
<script>
|
|
475
566
|
function toggleDetails(id) {
|
|
476
|
-
const el = document.getElementById(
|
|
477
|
-
|
|
567
|
+
const el = document.getElementById(id);
|
|
568
|
+
if (el) {
|
|
569
|
+
const isSecurityDetails = id.startsWith('security-details');
|
|
570
|
+
if (isSecurityDetails) {
|
|
571
|
+
// Independent toggle for security section
|
|
572
|
+
el.style.display = (el.style.display === 'none') ? 'block' : 'none';
|
|
573
|
+
} else {
|
|
574
|
+
// Main test details toggle
|
|
575
|
+
el.style.display = (el.style.display === 'none' || el.style.display === '') ? 'block' : 'none';
|
|
576
|
+
}
|
|
577
|
+
}
|
|
478
578
|
}
|
|
479
|
-
// Collapse all by default
|
|
480
|
-
document.querySelectorAll('.test-details').forEach(d => d.style.display = 'none');
|
|
481
579
|
</script>
|
|
482
580
|
</body>
|
|
483
581
|
</html>
|
|
484
582
|
`;
|
|
485
583
|
}
|
|
486
584
|
|
|
585
|
+
private getAttachmentText(att: { body?: Buffer, path?: string } | undefined): string | null {
|
|
586
|
+
if (!att) return null;
|
|
587
|
+
if (att.body) return att.body.toString();
|
|
588
|
+
if (att.path) {
|
|
589
|
+
const actualPath = path.isAbsolute(att.path) ? att.path : path.join(this.outputDir, att.path);
|
|
590
|
+
if (fs.existsSync(actualPath)) {
|
|
591
|
+
return fs.readFileSync(actualPath, 'utf8');
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private _renderSecuritySection(attachments: any[], index: number): string {
|
|
598
|
+
let vulnerabilities: any[] = [];
|
|
599
|
+
|
|
600
|
+
const reportAtt = attachments.find(a => a.name === 'security_report');
|
|
601
|
+
if (reportAtt) {
|
|
602
|
+
const text = this.getAttachmentText(reportAtt);
|
|
603
|
+
if (text) {
|
|
604
|
+
try {
|
|
605
|
+
const parsed = JSON.parse(text);
|
|
606
|
+
if (Array.isArray(parsed)) vulnerabilities.push(...parsed);
|
|
607
|
+
} catch (e) { }
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const findingAtts = attachments.filter(a => a.name === 'security_finding');
|
|
612
|
+
for (const findingAtt of findingAtts) {
|
|
613
|
+
const text = this.getAttachmentText(findingAtt);
|
|
614
|
+
if (text) {
|
|
615
|
+
try {
|
|
616
|
+
const parsed = JSON.parse(text);
|
|
617
|
+
vulnerabilities.push(parsed);
|
|
618
|
+
} catch (e) { }
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (vulnerabilities.length === 0) return '';
|
|
623
|
+
|
|
624
|
+
const severityOrder = { 'Critical': 0, 'High': 1, 'Medium': 2, 'Low': 3, 'Info': 4 };
|
|
625
|
+
type Severity = keyof typeof severityOrder;
|
|
626
|
+
|
|
627
|
+
const severityCounts: Record<Severity, number> = { 'Critical': 0, 'High': 0, 'Medium': 0, 'Low': 0, 'Info': 0 };
|
|
628
|
+
let worstSeverity: Severity = 'Info';
|
|
629
|
+
|
|
630
|
+
vulnerabilities.forEach(v => {
|
|
631
|
+
const severity = v.severity as Severity;
|
|
632
|
+
if (severityCounts[severity] !== undefined) {
|
|
633
|
+
severityCounts[severity]++;
|
|
634
|
+
}
|
|
635
|
+
if (severityOrder[severity] < severityOrder[worstSeverity]) {
|
|
636
|
+
worstSeverity = severity;
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
const riskColor = worstSeverity === 'Critical' ? 'var(--error)' :
|
|
641
|
+
worstSeverity === 'High' ? '#fb923c' :
|
|
642
|
+
worstSeverity === 'Medium' ? 'var(--warning)' :
|
|
643
|
+
'#38bdf8';
|
|
644
|
+
|
|
645
|
+
return `
|
|
646
|
+
<div class="security-section">
|
|
647
|
+
<div class="security-header" onclick="toggleDetails('security-details-${index}')">
|
|
648
|
+
<h3>🛡️ Security Assessment</h3>
|
|
649
|
+
<span class="security-risk-badge" style="background-color: ${riskColor};">${worstSeverity} Risk</span>
|
|
650
|
+
</div>
|
|
651
|
+
<div class="security-details" id="security-details-${index}" style="display: block;">
|
|
652
|
+
<div class="security-summary">
|
|
653
|
+
<div class="summary-item">
|
|
654
|
+
<div class="count count-Critical">${severityCounts.Critical}</div>
|
|
655
|
+
<div class="label">Critical</div>
|
|
656
|
+
</div>
|
|
657
|
+
<div class="summary-item">
|
|
658
|
+
<div class="count count-High">${severityCounts.High}</div>
|
|
659
|
+
<div class="label">High</div>
|
|
660
|
+
</div>
|
|
661
|
+
<div class="summary-item">
|
|
662
|
+
<div class="count count-Medium">${severityCounts.Medium}</div>
|
|
663
|
+
<div class="label">Medium</div>
|
|
664
|
+
</div>
|
|
665
|
+
<div class="summary-item">
|
|
666
|
+
<div class="count count-Low">${severityCounts.Low}</div>
|
|
667
|
+
<div class="label">Low</div>
|
|
668
|
+
</div>
|
|
669
|
+
</div>
|
|
670
|
+
<div class="vuln-list">
|
|
671
|
+
${vulnerabilities.map(v => {
|
|
672
|
+
const sev = v.severity as Severity;
|
|
673
|
+
const cardColor = sev === 'Critical' ? 'var(--error)' :
|
|
674
|
+
sev === 'High' ? '#fb923c' :
|
|
675
|
+
sev === 'Medium' ? 'var(--warning)' :
|
|
676
|
+
'#38bdf8';
|
|
677
|
+
return `
|
|
678
|
+
<div class="vuln-card" style="border-left-color: ${cardColor};">
|
|
679
|
+
<div class="vuln-card-title">
|
|
680
|
+
<span class="tag" style="background-color: ${cardColor}20; color: ${cardColor};">${v.severity}</span>
|
|
681
|
+
<span>${this.escapeHtml(v.category)} ${v.subcategory ? `<span style="opacity:0.7">— ${this.escapeHtml(v.subcategory)}</span>` : ''}</span>
|
|
682
|
+
</div>
|
|
683
|
+
<div style="font-size: 0.8rem; opacity: 0.8; margin-top: 5px; margin-bottom: 10px; display: flex; flex-wrap: wrap; gap: 12px;">
|
|
684
|
+
${v.type ? `<span><strong style="color:var(--accent);">Type:</strong> ${this.escapeHtml(v.type)}</span>` : ''}
|
|
685
|
+
${v.confidence ? `<span><strong>Confidence:</strong> ${this.escapeHtml(v.confidence)}</span>` : ''}
|
|
686
|
+
${v.exploitability ? `<span><strong>Exploitability:</strong> ${this.escapeHtml(v.exploitability)}</span>` : ''}
|
|
687
|
+
</div>
|
|
688
|
+
<p class="vuln-card-desc">${this.escapeHtml(v.description)}</p>
|
|
689
|
+
${v.impact ? `<p class="vuln-card-impact" style="font-size: 0.85rem; color: #fb923c; margin-bottom: 10px;"><strong>Potential Impact:</strong> ${this.escapeHtml(v.impact)}</p>` : ''}
|
|
690
|
+
|
|
691
|
+
<div class="vuln-card-evidence">
|
|
692
|
+
<strong>Evidence:</strong>
|
|
693
|
+
<pre>${this.escapeHtml(JSON.stringify(v.evidence, null, 2))}</pre>
|
|
694
|
+
</div>
|
|
695
|
+
|
|
696
|
+
${v.reproductionSteps && v.reproductionSteps.length > 0 ? `
|
|
697
|
+
<div class="vuln-card-repro" style="margin: 10px 0; background: rgba(0,0,0,0.2); padding: 10px; border-radius: 6px; border-left: 2px solid ${cardColor}80;">
|
|
698
|
+
<strong style="font-size: 0.85rem; color: var(--text-muted);">Reproduction Steps:</strong>
|
|
699
|
+
<ol style="margin: 5px 0 0 20px; font-size: 0.85rem; color: var(--text-main);">
|
|
700
|
+
${v.reproductionSteps.map((s: string) => `<li>${this.escapeHtml(s)}</li>`).join('')}
|
|
701
|
+
</ol>
|
|
702
|
+
</div>` : ''}
|
|
703
|
+
|
|
704
|
+
<div class="vuln-card-remediation">
|
|
705
|
+
<strong>→ Remediation:</strong> ${this.escapeHtml(v.remediation)}
|
|
706
|
+
</div>
|
|
707
|
+
|
|
708
|
+
${v.classification ? `
|
|
709
|
+
<div style="font-size: 0.75rem; opacity: 0.5; margin-top: 12px; display: flex; gap: 15px; font-family: 'JetBrains Mono', monospace;">
|
|
710
|
+
${v.classification.owaspTop10 ? `<span>[OWASP: ${this.escapeHtml(v.classification.owaspTop10)}]</span>` : ''}
|
|
711
|
+
${v.classification.cwe ? `<span>[CWE: ${this.escapeHtml(v.classification.cwe)}]</span>` : ''}
|
|
712
|
+
</div>` : ''}
|
|
713
|
+
</div>
|
|
714
|
+
`;
|
|
715
|
+
}).join('')}
|
|
716
|
+
</div>
|
|
717
|
+
</div>
|
|
718
|
+
</div>
|
|
719
|
+
`;
|
|
720
|
+
}
|
|
721
|
+
|
|
487
722
|
private escapeHtml(unsafe: string | undefined): string {
|
|
488
723
|
if (!unsafe) return '';
|
|
489
724
|
return unsafe
|