config: full APAW agent infrastructure + Phantom project files
- Added all agent definitions (.kile/agents/*.md) - Added commands, rules, skills, shared modules - Added src/, scripts/, tests/, docker/, agent-evolution/ - Extracted 3 archives: website/, workspace/, release/ - Created .env with Gitea creds for UniqueSoft/Phantom - Created docs/ with project-specific guides - Added .gitignore for node_modules
This commit is contained in:
263
tests/scripts/lib/gitea-client.js
Normal file
263
tests/scripts/lib/gitea-client.js
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* Gitea API Client — Lightweight helper for posting test results to Gitea Issues.
|
||||
*
|
||||
* Auth flow: Basic Auth → create token → use token for API calls.
|
||||
*
|
||||
* Usage:
|
||||
* const gitea = require('./lib/gitea-client');
|
||||
* await gitea.postComment(issueNumber, body);
|
||||
* await gitea.uploadAttachment(issueNumber, filePath);
|
||||
*
|
||||
* Environment:
|
||||
* GITEA_API_URL - API base (default: https://git.softuniq.eu/api/v1)
|
||||
* GITEA_TOKEN - Pre-existing API token (skips Basic Auth if set)
|
||||
* GITEA_USER - Username for Basic Auth (default: NW)
|
||||
* GITEA_PASSWORD - Password for Basic Auth (required if no token)
|
||||
* GITEA_REPO - Repository path (default: UniqueSoft/APAW)
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const GITEA_API_URL = process.env.GITEA_API_URL || 'https://git.softuniq.eu/api/v1';
|
||||
const GITEA_USER = process.env.GITEA_USER || '';
|
||||
const GITEA_PASSWORD = process.env.GITEA_PASSWORD || '';
|
||||
const GITEA_REPO = process.env.GITEA_REPO || 'UniqueSoft/APAW';
|
||||
|
||||
let _cachedToken = process.env.GITEA_TOKEN || null;
|
||||
|
||||
function request(urlStr, options, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(urlStr);
|
||||
const mod = url.protocol === 'https:' ? https : http;
|
||||
const opts = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method: options.method || 'GET',
|
||||
headers: options.headers || {},
|
||||
};
|
||||
const req = mod.request(opts, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
try { resolve(JSON.parse(data)); } catch { resolve(data); }
|
||||
} else {
|
||||
reject(new Error(`Gitea API ${res.statusCode}: ${data.slice(0, 300)}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (body) req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function getToken() {
|
||||
if (_cachedToken) return _cachedToken;
|
||||
|
||||
const credentials = Buffer.from(`${GITEA_USER}:${GITEA_PASSWORD}`).toString('base64');
|
||||
const urlStr = `${GITEA_API_URL}/users/${GITEA_USER}/tokens`;
|
||||
const body = JSON.stringify({ name: `vt-${Date.now()}`, scopes: ['all'] });
|
||||
|
||||
const result = await request(urlStr, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Basic ${credentials}`,
|
||||
},
|
||||
}, body);
|
||||
|
||||
_cachedToken = result.sha1;
|
||||
return _cachedToken;
|
||||
}
|
||||
|
||||
async function authHeaders() {
|
||||
const token = await getToken();
|
||||
return { 'Authorization': `token ${token}`, 'Content-Type': 'application/json' };
|
||||
}
|
||||
|
||||
async function postComment(issueNumber, body) {
|
||||
const headers = await authHeaders();
|
||||
const url = `${GITEA_API_URL}/repos/${GITEA_REPO}/issues/${issueNumber}/comments`;
|
||||
return request(url, { method: 'POST', headers }, JSON.stringify({ body }));
|
||||
}
|
||||
|
||||
async function uploadAttachment(issueNumber, filePath) {
|
||||
const token = await getToken();
|
||||
const fileContent = fs.readFileSync(filePath);
|
||||
const filename = path.basename(filePath);
|
||||
const boundary = `----FormBoundary${Date.now()}`;
|
||||
|
||||
let body = `--${boundary}\r\n`.getBytes?.() || Buffer.from(`--${boundary}\r\n`);
|
||||
body = Buffer.concat([
|
||||
Buffer.from(`--${boundary}\r\n`),
|
||||
Buffer.from(`Content-Disposition: form-data; name="attachment"; filename="${filename}"\r\n`),
|
||||
Buffer.from(`Content-Type: image/png\r\n\r\n`),
|
||||
fileContent,
|
||||
Buffer.from(`\r\n--${boundary}--\r\n`),
|
||||
]);
|
||||
|
||||
const url = new URL(`${GITEA_API_URL}/repos/${GITEA_REPO}/issues/${issueNumber}/assets`);
|
||||
const mod = url.protocol === 'https:' ? https : http;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = mod.request({
|
||||
hostname: url.hostname,
|
||||
port: url.port || 443,
|
||||
path: url.pathname,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `token ${token}`,
|
||||
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
||||
'Content-Length': body.length,
|
||||
},
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
try { resolve(JSON.parse(data)); } catch { resolve(data); }
|
||||
} else {
|
||||
reject(new Error(`Gitea upload ${res.statusCode}: ${data.slice(0, 300)}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadAndComment(issueNumber, filePaths, commentBody) {
|
||||
const uuids = [];
|
||||
for (const fp of filePaths) {
|
||||
try {
|
||||
const result = await uploadAttachment(issueNumber, fp);
|
||||
uuids.push({ filename: path.basename(fp), uuid: result.uuid });
|
||||
} catch (err) {
|
||||
console.error(` ⚠️ Upload failed ${path.basename(fp)}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
let fullBody = commentBody;
|
||||
if (uuids.length > 0) {
|
||||
fullBody += '\n\n### 📸 Screenshots\n\n';
|
||||
for (const u of uuids) {
|
||||
fullBody += `\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return postComment(issueNumber, fullBody);
|
||||
}
|
||||
|
||||
function formatVisualReport(report) {
|
||||
const s = report.summary;
|
||||
const lines = [
|
||||
'## 📊 Visual Test Results',
|
||||
'',
|
||||
`**URL**: \`${report.targetUrl}\``,
|
||||
`**Pages**: ${report.pages.join(', ')}`,
|
||||
`**Viewports**: ${report.viewports.join(', ')}`,
|
||||
`**Threshold**: ${report.threshold * 100}%`,
|
||||
'',
|
||||
'### Summary',
|
||||
'',
|
||||
`| Metric | Count |`,
|
||||
`|--------|-------|`,
|
||||
`| Screenshots captured | ${s.screenshotsCaptured} |`,
|
||||
`| Screenshots failed | ${s.screenshotsFailed} |`,
|
||||
`| Comparisons passed | ${s.comparisonsPassed} |`,
|
||||
`| Comparisons failed | ${s.comparisonsFailed} |`,
|
||||
`| UI elements extracted | ${s.totalElements} |`,
|
||||
`| Console errors | ${s.totalConsoleErrors} |`,
|
||||
`| Network errors | ${s.totalNetworkErrors} |`,
|
||||
'',
|
||||
`**Overall**: ${s.overallPassed ? '✅ PASSED' : '❌ FAILED'}`,
|
||||
];
|
||||
|
||||
if (report.comparison?.length) {
|
||||
lines.push('', '### Comparison Details', '');
|
||||
lines.push('| Screenshot | Status | Diff % |');
|
||||
lines.push('|------------|--------|--------|');
|
||||
for (const c of report.comparison) {
|
||||
lines.push(`| ${c.filename} | ${c.status === 'PASS' ? '✅' : '❌'} ${c.status} | ${c.diffPercent || 'N/A'} |`);
|
||||
}
|
||||
}
|
||||
|
||||
if (report.consoleErrors?.length > 0) {
|
||||
lines.push('', '### Console Errors', '');
|
||||
for (const e of report.consoleErrors.slice(0, 5)) {
|
||||
lines.push(`- [${e.page}/${e.viewport}] ${e.error?.slice(0, 120)}`);
|
||||
}
|
||||
if (report.consoleErrors.length > 5) {
|
||||
lines.push(`- ... and ${report.consoleErrors.length - 5} more`);
|
||||
}
|
||||
}
|
||||
|
||||
if (report.networkErrors?.length > 0) {
|
||||
lines.push('', '### Network Errors', '');
|
||||
for (const e of report.networkErrors.slice(0, 5)) {
|
||||
lines.push(`- [${e.page}/${e.viewport}] ${e.status || e.failure} ${e.url?.slice(0, 80)}`);
|
||||
}
|
||||
if (report.networkErrors.length > 5) {
|
||||
lines.push(`- ... and ${report.networkErrors.length - 5} more`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function formatConsoleReport(report) {
|
||||
const s = report.summary;
|
||||
const lines = [
|
||||
'## 📊 Console Error Monitor Results',
|
||||
'',
|
||||
`**URL**: \`${report.targetUrl}\``,
|
||||
`**Pages**: ${report.pages.join(', ')}`,
|
||||
'',
|
||||
'### Summary',
|
||||
'',
|
||||
`| Metric | Count |`,
|
||||
`|--------|-------|`,
|
||||
`| Console errors | ${s.consoleErrors} |`,
|
||||
`| Console warnings | ${s.consoleWarnings} |`,
|
||||
`| Network errors | ${s.networkErrors} |`,
|
||||
`| **Total issues** | **${s.totalIssues}** |`,
|
||||
'',
|
||||
`**Status**: ${s.totalIssues === 0 ? '✅ CLEAN' : '❌ ISSUES FOUND'}`,
|
||||
];
|
||||
|
||||
if (report.consoleErrors?.length > 0) {
|
||||
lines.push('', '### Console Errors', '');
|
||||
for (const e of report.consoleErrors.slice(0, 8)) {
|
||||
lines.push(`- [${e.page}] ${e.text?.slice(0, 120)}`);
|
||||
}
|
||||
if (report.consoleErrors.length > 8) {
|
||||
lines.push(`- ... and ${report.consoleErrors.length - 8} more`);
|
||||
}
|
||||
}
|
||||
|
||||
if (report.networkErrors?.length > 0) {
|
||||
lines.push('', '### Network Errors', '');
|
||||
for (const e of report.networkErrors.slice(0, 8)) {
|
||||
lines.push(`- [${e.page}] ${e.status || e.failure} ${e.url?.slice(0, 80)}`);
|
||||
}
|
||||
if (report.networkErrors.length > 8) {
|
||||
lines.push(`- ... and ${report.networkErrors.length - 8} more`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
postComment,
|
||||
uploadAttachment,
|
||||
uploadAndComment,
|
||||
formatVisualReport,
|
||||
formatConsoleReport,
|
||||
};
|
||||
Reference in New Issue
Block a user