Skip to content

Commit b0fc632

Browse files
authored
Merge pull request #474 from arii/feat/local-test-reporter-final-submission
feat: Implement Local Test Reporter (Final Submission)
2 parents db72c2f + 89b9cbb commit b0fc632

4 files changed

Lines changed: 210 additions & 112 deletions

File tree

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"dev": "cross-env NODE_ENV=development HOST=127.0.0.1 TS_NODE_TRANSPILE_ONLY=true node --env-file=.env.local --loader ts-node/esm server.ts | pnpm exec pino-pretty",
88
"seed": "ts-node --esm scripts/seed-dev-data.ts",
99
"build": "npm run build:server && next build",
10-
"build:server": "tsc -p tsconfig.build.json && cp dist/server.js dist/server.mjs",
10+
"build:server": "pnpm exec tsc -p tsconfig.build.json && cp dist/server.js dist/server.mjs",
1111
"analyze": "cross-env ANALYZE=true npm run build:server && cross-env ANALYZE=true next build --webpack",
1212
"start": "pm2 start ecosystem.config.cjs --env production",
1313
"pm2:logs": "pm2 logs hrm-server",
@@ -22,6 +22,7 @@
2222
"test:unit": "jest",
2323
"test:unit:watch": "jest --watch",
2424
"test:unit:coverage": "jest --coverage",
25+
"test:json": "bash start-production.sh > /tmp/hrm-server.log 2>&1 & echo $! > /tmp/hrm-server.pid && sleep 10 && npx playwright test --reporter=json > playwright-report.json; kill $(cat /tmp/hrm-server.pid) 2>/dev/null || true",
2526
"test:visual": "bash start-production.sh > /tmp/hrm-server.log 2>&1 & echo $! > /tmp/hrm-server.pid && sleep 10 && playwright test; kill $(cat /tmp/hrm-server.pid) 2>/dev/null || true",
2627
"test:all": "pnpm run test:visual && pnpm run test:unit",
2728
"test:quick": "pnpm run build && bash start-production.sh > /tmp/hrm-server.log 2>&1 & echo $! > /tmp/hrm-server.pid && sleep 10 && playwright test --reporter=dot; kill $(cat /tmp/hrm-server.pid) 2>/dev/null || true",
@@ -39,7 +40,8 @@
3940
"release:patch": "pnpm version patch -m \"release: patch %s\"",
4041
"release:minor": "pnpm version minor -m \"release: minor %s\"",
4142
"release:major": "pnpm version major -m \"release: major %s\"",
42-
"test:infra": "playwright test tests/playwright/infrastructure.spec.ts"
43+
"verify": "node scripts/verify-and-publish.cjs",
44+
"commit:verified": "npm run verify && git add test-proof.json && git commit"
4345
},
4446
"dependencies": {
4547
"@emotion/cache": "^11.14.0",

playwright-report.json

Whitespace-only changes.

scripts/verify-and-publish.cjs

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
const { execSync, spawn } = require('child_process');
2+
const fs = require('fs');
3+
const https = require('https');
4+
const path = require('path');
5+
6+
// --- Configuration ---
7+
const PROOF_FILE = 'test-proof.json';
8+
const REPORT_FILE = 'playwright-report.json';
9+
10+
// --- Helpers ---
11+
12+
// 1. Calculate Source Hash (The Fingerprint)
13+
function getSourceHash() {
14+
console.log('🔒 Calculating source fingerprint...');
15+
// Hashes all relevant source and test files to create a unique signature
16+
const cmd = `find app components context hooks lib services types utils tests -type f -name '*.*' -not -path '*/.*' | sort | xargs sha1sum | sha1sum | awk '{print $1}'`;
17+
return execSync(cmd).toString().trim();
18+
}
19+
20+
// 2. Get Current Commit SHA
21+
function getCommitSha() {
22+
return execSync('git rev-parse HEAD').toString().trim();
23+
}
24+
25+
// 3. Publish Status to GitHub (Native HTTPS - No Dependencies)
26+
function publishStatus(sha, context, state, description) {
27+
const token = process.env.GITHUB_TOKEN;
28+
if (!token) {
29+
console.warn(`⚠️ Skipping GitHub publish for ${context} (No GITHUB_TOKEN found)`);
30+
return Promise.resolve();
31+
}
32+
33+
// Auto-detect owner/repo from git remote
34+
let repoPath = '';
35+
try {
36+
const remoteUrl = execSync('git config --get remote.origin.url').toString().trim();
37+
const match = remoteUrl.match(/github\.com[:/](.+?)\/(.+?)(\.git)?$/);
38+
if (match) repoPath = `${match[1]}/${match[2]}`;
39+
} catch (e) {
40+
console.error('❌ Could not detect git remote');
41+
return Promise.resolve();
42+
}
43+
44+
const data = JSON.stringify({
45+
state,
46+
description: description.substring(0, 140), // Limit per GitHub API
47+
context,
48+
target_url: `https://github.com/${repoPath}/blob/${sha}/${PROOF_FILE}` // Link to the proof file
49+
});
50+
51+
const options = {
52+
hostname: 'api.github.com',
53+
path: `/repos/${repoPath}/statuses/${sha}`,
54+
method: 'POST',
55+
headers: {
56+
'User-Agent': 'HRM-Verifier-Script',
57+
'Authorization': `token ${token}`,
58+
'Content-Type': 'application/json',
59+
'Content-Length': data.length
60+
}
61+
};
62+
63+
return new Promise((resolve) => {
64+
const req = https.request(options, (res) => {
65+
if (res.statusCode === 201) {
66+
console.log(` ✅ Published: ${context} -> ${state}`);
67+
} else {
68+
console.error(` ❌ API Error (${res.statusCode}) for ${context}`);
69+
}
70+
resolve();
71+
});
72+
73+
req.on('error', (e) => {
74+
console.error(` ❌ Network Error: ${e.message}`);
75+
resolve();
76+
});
77+
78+
req.write(data);
79+
req.end();
80+
});
81+
}
82+
83+
// --- Main Execution ---
84+
85+
async function main() {
86+
console.log('🚀 Starting Local Verifier...');
87+
88+
// A. Cleanup old reports
89+
if (fs.existsSync(REPORT_FILE)) fs.unlinkSync(REPORT_FILE);
90+
91+
const sha = getCommitSha();
92+
const sourceHash = getSourceHash();
93+
console.log(`📌 Commit: ${sha.slice(0, 7)}`);
94+
console.log(`🔒 Hash: ${sourceHash}`);
95+
96+
// B. Run Playwright
97+
console.log('\n🧪 Running Tests (this takes a moment)...');
98+
try {
99+
// Run tests and force JSON output. Ignore exit code to ensure we parse the report.
100+
execSync('npm run test:json', { stdio: 'inherit' });
101+
} catch (e) {
102+
console.log('⚠️ Tests finished with failures.');
103+
}
104+
105+
if (!fs.existsSync(REPORT_FILE)) {
106+
console.error('❌ Critical: No test report generated.');
107+
process.exit(1);
108+
}
109+
110+
// E. Parse Report
111+
const reportJson = fs.readFileSync(REPORT_FILE, 'utf-8');
112+
if (!reportJson) {
113+
console.error('❌ Critical: Test report file is empty.');
114+
process.exit(1);
115+
}
116+
const report = JSON.parse(reportJson);
117+
const resultsByFile = {};
118+
let globalSuccess = true;
119+
120+
// Helper function to recursively process suites and collect spec results
121+
function processSuite(suite, fileName) {
122+
if (!resultsByFile[fileName]) {
123+
resultsByFile[fileName] = { total: 0, passed: 0, failed: 0 };
124+
}
125+
const stats = resultsByFile[fileName];
126+
127+
// Process specs within the current suite
128+
if (suite.specs) {
129+
suite.specs.forEach((spec) => {
130+
stats.total++;
131+
if (spec.ok) {
132+
stats.passed++;
133+
} else {
134+
stats.failed++;
135+
globalSuccess = false;
136+
}
137+
});
138+
}
139+
140+
// Recurse into nested suites
141+
if (suite.suites) {
142+
suite.suites.forEach((nestedSuite) => {
143+
processSuite(nestedSuite, fileName);
144+
});
145+
}
146+
}
147+
148+
// The top-level suites in the report are the files
149+
if (report.suites) {
150+
report.suites.forEach((fileSuite) => {
151+
const name = path.basename(fileSuite.title, '.spec.ts');
152+
processSuite(fileSuite, name);
153+
});
154+
} else {
155+
console.error('❌ Critical: Report JSON is missing the "suites" property.');
156+
// A report with no tests will have a suites array, so this is a genuine error.
157+
globalSuccess = false;
158+
}
159+
160+
// F. Generate Proof Manifest
161+
const manifest = {
162+
version: "1.0",
163+
timestamp: new Date().toISOString(),
164+
commit: sha,
165+
sourceHash: sourceHash,
166+
status: globalSuccess ? 'success' : 'failure',
167+
details: resultsByFile
168+
};
169+
fs.writeFileSync(PROOF_FILE, JSON.stringify(manifest, null, 2));
170+
console.log(`\n📄 Proof Manifest saved to ${PROOF_FILE}`);
171+
172+
// G. Publish Results (Parallel Requests)
173+
console.log('\n☁️ Publishing checks to GitHub...');
174+
175+
const promises = [];
176+
177+
// 1. Global Status
178+
promises.push(publishStatus(
179+
sha,
180+
'verifier/global',
181+
globalSuccess ? 'success' : 'failure',
182+
globalSuccess ? 'All systems operational' : 'Tests failed'
183+
));
184+
185+
// 2. Distinct Check for each Test File
186+
for (const [name, stats] of Object.entries(resultsByFile)) {
187+
const state = stats.failed === 0 ? 'success' : 'failure';
188+
const desc = `${stats.passed}/${stats.total} passed`;
189+
promises.push(publishStatus(sha, `verifier/${name}`, state, desc));
190+
}
191+
192+
await Promise.all(promises);
193+
194+
// Cleanup
195+
fs.unlinkSync(REPORT_FILE);
196+
197+
if (globalSuccess) {
198+
console.log('\n✅ Verification Complete. You can now commit the proof file.');
199+
process.exit(0);
200+
} else {
201+
console.error('\n❌ Verification Failed. Please fix tests before committing.');
202+
process.exit(1);
203+
}
204+
}
205+
206+
main().catch(console.error);

tests/playwright/infrastructure.spec.ts

Lines changed: 0 additions & 110 deletions
This file was deleted.

0 commit comments

Comments
 (0)