añadido karpathy y refactorizacion del proyecto
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* enrich.js
|
||||
* Shared logic to generate rich metadata (steps, automatable, notYet, dependencies)
|
||||
* for a test case. Used by both the editor and (optionally) the report.
|
||||
*/
|
||||
|
||||
function enrichTestCase(t) {
|
||||
const id = t.id || '';
|
||||
const group = t.group || '';
|
||||
const isCore = t.module === 'core';
|
||||
const isPbx = t.module === 'pbx';
|
||||
const status = t.status || 'manual';
|
||||
|
||||
const isAutomated = status === 'automated';
|
||||
const isPartial = status === 'partial';
|
||||
const isManual = status === 'manual';
|
||||
|
||||
let steps = [];
|
||||
let automatable = [];
|
||||
let notYet = [];
|
||||
let dependencies = [];
|
||||
|
||||
// === STEPS ===
|
||||
steps.push('Open admin portal or repository URL in browser context');
|
||||
|
||||
if (id.includes('repository') || id.includes('documentation')) {
|
||||
steps.push('Navigate to the download / documentation portal');
|
||||
steps.push('Search or locate the relevant file/link by filename pattern');
|
||||
steps.push('Assert presence and visibility of the expected document');
|
||||
} else if (group.includes('security') || group.includes('configuration')) {
|
||||
steps.push('Log in to the admin portal (if required)');
|
||||
steps.push('Navigate to the relevant settings section');
|
||||
steps.push('Read current configuration values via UI or API');
|
||||
} else if (group.includes('calls') || group.includes('codecs')) {
|
||||
steps.push('Ensure SIP endpoints / softphones are registered');
|
||||
steps.push('Initiate or receive call using the required scenario');
|
||||
steps.push('Verify audio path, signaling, and media parameters');
|
||||
} else if (group.includes('upgrade') || group.includes('installation')) {
|
||||
steps.push('Prepare clean or previous-version VM/image');
|
||||
steps.push('Perform installation or upgrade procedure');
|
||||
steps.push('Post-install verification via portal + external tools');
|
||||
} else {
|
||||
steps.push('Execute the main verification or configuration action described in the test');
|
||||
steps.push('Capture evidence (screenshot, log, API response)');
|
||||
steps.push('Assert expected outcome');
|
||||
}
|
||||
|
||||
// === AUTOMATION BREAKDOWN ===
|
||||
if (isAutomated) {
|
||||
automatable.push('Full end-to-end execution via Playwright against the admin portal UI');
|
||||
automatable.push('Navigation, form interaction, and assertions are scriptable');
|
||||
} else if (isPartial) {
|
||||
automatable.push('Portal UI configuration and verification steps (already partially automated)');
|
||||
automatable.push('Data extraction and reporting of current settings');
|
||||
}
|
||||
|
||||
if (isManual || isPartial) {
|
||||
if (t.manualReason) notYet.push(t.manualReason);
|
||||
if (t.partialNotes) notYet.push(t.partialNotes);
|
||||
}
|
||||
|
||||
if (!isAutomated) {
|
||||
notYet.push('Requires external SIP endpoints or physical phones for media/audio validation');
|
||||
}
|
||||
|
||||
if (group.includes('virtualization') || group.includes('availability') || group.includes('performance')) {
|
||||
notYet.push('Hypervisor-level operations (vCenter, libvirt, vSphere) not reachable from browser automation');
|
||||
notYet.push('Long-running load or HA failover scenarios need dedicated lab infrastructure');
|
||||
}
|
||||
|
||||
if (group.includes('calls') || group.includes('codecs')) {
|
||||
notYet.push('Real RTP/SRTP media path and audio quality verification (human ear or PESQ/MOS tools)');
|
||||
notYet.push('External SIP trunk / PSTN connectivity and DID numbers');
|
||||
}
|
||||
|
||||
if (id.includes('security') && (id.includes('cve') || id.includes('vulnerab'))) {
|
||||
notYet.push('Full CVE / dependency scanning (Trivy, Grype, OWASP Dependency-Check) in CI pipeline');
|
||||
}
|
||||
|
||||
// === DEPENDENCIES ===
|
||||
dependencies.push({ label: 'Admin Portal', value: 'https://pbx.local:4443 (configure via $BASE_URL)' });
|
||||
dependencies.push({ label: 'Repository URL', value: '${BASE_URL}/downloads or $REPO_URL' });
|
||||
|
||||
if (isCore) dependencies.push({ label: 'Protocol', value: 'HTTPS (admin portal) + optional SSH/CLI' });
|
||||
if (isPbx) dependencies.push({ label: 'Protocol', value: 'HTTPS + SIP (UDP/TCP/TLS) + RTP/SRTP' });
|
||||
|
||||
if (group.includes('calls') || group.includes('codecs') || group.includes('extensions')) {
|
||||
dependencies.push({ label: 'SIP Endpoints', value: 'At least 2 registered SIP clients (deskphone or softphone: Zoiper, MicroSIP, Yealink, etc.)' });
|
||||
dependencies.push({ label: 'Network', value: 'SIP trunk or local Asterisk/FreeSWITCH simulator for PSTN simulation' });
|
||||
}
|
||||
|
||||
if (group.includes('virtualization')) {
|
||||
dependencies.push({ label: 'Hypervisor Access', value: 'vCenter, Hyper-V Manager, Proxmox, or virsh (SSH/API)' });
|
||||
}
|
||||
|
||||
if (group.includes('performance') || group.includes('availability')) {
|
||||
dependencies.push({ label: 'Load Gen', value: 'SIPp or custom load generator + monitoring (Prometheus/Grafana recommended)' });
|
||||
}
|
||||
|
||||
if (id.includes('siem') || id.includes('addm')) {
|
||||
dependencies.push({ label: 'External Systems', value: 'SIEM server (Splunk, ELK, QRadar) and/or ADDM/ADDM collector' });
|
||||
}
|
||||
|
||||
dependencies.push({ label: 'Credentials', value: 'Admin user with sufficient privileges (portal + SSH where needed)' });
|
||||
dependencies.push({ label: 'Test Data', value: 'Clean test tenant or lab PBX instance (recommended isolated environment)' });
|
||||
|
||||
// Cleanup: remove duplicates and empty
|
||||
const seen = new Set();
|
||||
notYet = notYet.filter(item => {
|
||||
const key = item.trim();
|
||||
if (!key || seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
|
||||
automatable = [...new Set(automatable.filter(Boolean))];
|
||||
steps = [...new Set(steps.filter(Boolean))];
|
||||
|
||||
// Fallbacks
|
||||
if (automatable.length === 0) {
|
||||
automatable.push('Most UI-driven configuration and verification steps via Playwright');
|
||||
}
|
||||
if (notYet.length === 0 && !isAutomated) {
|
||||
notYet.push('No major blockers identified — can be fully automated with current tooling');
|
||||
}
|
||||
|
||||
return {
|
||||
steps,
|
||||
automatable,
|
||||
notYet,
|
||||
dependencies
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { enrichTestCase };
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* load-testcases.js
|
||||
*
|
||||
* ONE-TIME TRANSITION HELPER
|
||||
* Loads all testcases from the new YAML tree (testcases/) and converts them
|
||||
* to the legacy JSON shape that the editor frontend currently expects.
|
||||
*
|
||||
* This allows the UI to keep working unchanged while we migrate.
|
||||
*
|
||||
* TODO: Once the editor is updated to work natively with YAML, this can be removed.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const TESTCASES_ROOT = path.join(__dirname, '..', '..', 'testcases');
|
||||
|
||||
/**
|
||||
* Recursively find all .yaml files under a directory (excluding SCHEMA.md etc.)
|
||||
*/
|
||||
function findYamlFiles(dir) {
|
||||
const results = [];
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...findYamlFiles(fullPath));
|
||||
} else if (entry.name.endsWith('.yaml') && !entry.name.startsWith('SCHEMA')) {
|
||||
results.push(fullPath);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a single YAML testcase object back to the legacy editor shape.
|
||||
*/
|
||||
function yamlToLegacy(yamlTest) {
|
||||
const cf = yamlTest.custom_fields || {};
|
||||
|
||||
const legacy = {
|
||||
id: yamlTest.id,
|
||||
group: yamlTest.group,
|
||||
module: yamlTest.module,
|
||||
action: cf.original_action || 'check',
|
||||
description: yamlTest.description || yamlTest.title || '',
|
||||
status: yamlTest.automation_status || 'manual',
|
||||
file: yamlTest.file || '',
|
||||
steps: (yamlTest.steps || []).map(s => s.action || s),
|
||||
automatable: cf.original_automatable || [],
|
||||
notYet: cf.original_notYet || [],
|
||||
dependencies: cf.original_dependencies || [],
|
||||
};
|
||||
|
||||
// Preserve extra fields that the old data sometimes had
|
||||
if (cf.manualReason) legacy.manualReason = cf.manualReason;
|
||||
if (cf.partialNotes) legacy.partialNotes = cf.partialNotes;
|
||||
|
||||
return legacy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main loader. Returns array of testcases in the old JSON format.
|
||||
*/
|
||||
function loadTestcases() {
|
||||
if (!fs.existsSync(TESTCASES_ROOT)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const yamlFiles = findYamlFiles(TESTCASES_ROOT);
|
||||
const testcases = [];
|
||||
|
||||
for (const filePath of yamlFiles) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const doc = yaml.load(content);
|
||||
if (doc && doc.id) {
|
||||
testcases.push(yamlToLegacy(doc));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load YAML:', filePath, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort for stable output (same order every time)
|
||||
testcases.sort((a, b) => a.id.localeCompare(b.id));
|
||||
|
||||
return testcases;
|
||||
}
|
||||
|
||||
module.exports = { loadTestcases };
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* save-testcases.js
|
||||
*
|
||||
* Writes an array of legacy-shaped testcases back to the individual YAML files.
|
||||
* Tries to preserve existing per-step automation flags and custom data.
|
||||
*
|
||||
* This is part of the transition so that the editor can save directly to YAML.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const TESTCASES_ROOT = path.join(__dirname, '..', '..', 'testcases');
|
||||
|
||||
function getYamlPath(legacyTest) {
|
||||
const project = 'pbx'; // transition: all current data lives under pbx project
|
||||
const module = legacyTest.module || 'unknown';
|
||||
const rawGroup = legacyTest.group || 'unknown';
|
||||
const group = rawGroup.replace(/^core-|^pbx-/, '');
|
||||
return path.join(TESTCASES_ROOT, project, module, group, `${legacyTest.id}.yaml`);
|
||||
}
|
||||
|
||||
function buildCanonicalDoc(legacy, existing = {}) {
|
||||
const cf = legacy.custom_fields || existing.custom_fields || {};
|
||||
|
||||
// Support rich steps from new per-step editor (Fase B+)
|
||||
let steps = [];
|
||||
const incomingSteps = legacy.steps || [];
|
||||
|
||||
if (incomingSteps.length > 0 && typeof incomingSteps[0] === 'object' && incomingSteps[0] !== null) {
|
||||
// Rich steps sent from frontend
|
||||
steps = incomingSteps.map(s => ({
|
||||
action: s.action || '',
|
||||
expected: s.expected || '',
|
||||
automatable: s.automatable !== undefined ? !!s.automatable : (legacy.status !== 'manual'),
|
||||
...(s.reason_not_automatable ? { reason_not_automatable: s.reason_not_automatable } : {})
|
||||
}));
|
||||
} else {
|
||||
// Legacy flat strings
|
||||
steps = incomingSteps.map((action, i) => {
|
||||
const prev = (existing.steps && existing.steps[i]) || {};
|
||||
return {
|
||||
action: action,
|
||||
expected: prev.expected || '',
|
||||
automatable: prev.automatable !== undefined ? prev.automatable : (legacy.status !== 'manual'),
|
||||
...(prev.reason_not_automatable ? { reason_not_automatable: prev.reason_not_automatable } : {})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// If existing had more detailed steps, prefer their automatable flags
|
||||
if (existing.steps && existing.steps.length === steps.length) {
|
||||
steps = existing.steps.map((prev, i) => ({
|
||||
...prev,
|
||||
action: steps[i].action
|
||||
}));
|
||||
}
|
||||
|
||||
const doc = {
|
||||
id: legacy.id,
|
||||
project: 'pbx',
|
||||
module: legacy.module,
|
||||
group: legacy.group,
|
||||
title: legacy.description || legacy.title,
|
||||
description: legacy.description || legacy.title,
|
||||
priority: existing.priority || 'Medium',
|
||||
tags: existing.tags || [],
|
||||
references: existing.references || [],
|
||||
dependencies: legacy.dependencies || existing.dependencies || [],
|
||||
preconditions: existing.preconditions || [],
|
||||
postconditions: existing.postconditions || [],
|
||||
steps,
|
||||
automation_status: legacy.status,
|
||||
file: legacy.file || existing.file || null,
|
||||
custom_fields: {
|
||||
...cf,
|
||||
original_action: legacy.action,
|
||||
}
|
||||
};
|
||||
|
||||
// Carry over extra legacy fields the UI might send
|
||||
if (legacy.manualReason) doc.custom_fields.manualReason = legacy.manualReason;
|
||||
if (legacy.partialNotes) doc.custom_fields.partialNotes = legacy.partialNotes;
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
function saveTestcases(legacyArray) {
|
||||
for (const test of legacyArray) {
|
||||
if (!test || !test.id) continue;
|
||||
|
||||
const filePath = getYamlPath(test);
|
||||
const dir = path.dirname(filePath);
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
let existing = {};
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
existing = yaml.load(raw) || {};
|
||||
} catch (e) {
|
||||
// ignore, we'll overwrite
|
||||
}
|
||||
}
|
||||
|
||||
const doc = buildCanonicalDoc(test, existing);
|
||||
const header = `# Updated via Autotest Editor - ${new Date().toISOString()}\n# Source of truth: this YAML file\n\n`;
|
||||
const content = header + yaml.dump(doc, { lineWidth: 100, noRefs: true });
|
||||
|
||||
fs.writeFileSync(filePath, content, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { saveTestcases, getYamlPath };
|
||||
Reference in New Issue
Block a user