scripts/test-kv-accumulation.mjs
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env node
/**
* Test de non-régression : vérifie que le dev-server accumule recentChecks
* au lieu de les écraser.
*
* Régression historique : KV.put stocke 'monitors' en string JSON. Si un lecteur
* (runChecks) ne reparse pas cette string, existingData[monitorId] est undefined,
* previousChecks = [], et chaque check écrase l'historique -> recentChecks reste
* bloqué à 1 entrée. Ce test seed N checks, lance UN cycle de check via le
* dev-server, et asserte que recentChecks contient N+1 entrées.
*
* Usage : node scripts/test-kv-accumulation.mjs
* Exit 0 si OK, 1 si régression détectée.
*/
import { readFileSync, writeFileSync, existsSync, copyFileSync, rmSync } from 'fs'
import { spawn } from 'child_process'
import { createServer } from 'http'
import { fileURLToPath } from 'url'
import { dirname, join } from 'path'
const __dirname = dirname(fileURLToPath(import.meta.url))
const root = join(__dirname, '..')
const KV_FILE = join(root, 'local-kv.json')
const BACKUP = join(root, 'local-kv.json.testbak')
const MONITORS_FILE = join(root, 'monitors.json')
const MONITORS_BACKUP = join(root, 'monitors.json.testbak')
const SEED_COUNT = 5
const MONITOR_ID = 'uptimeworker-status'
const TARGET_PORT = 3998
const API_PORT = 3999
function seed() {
const now = Date.now()
const checks = []
for (let i = SEED_COUNT; i >= 1; i--) {
checks.push({ t: new Date(now - i * 60000).toISOString(), s: i % 2 === 0 ? 'operational' : 'degraded' })
}
const mon = {
[MONITOR_ID]: {
operational: false, status: 'degraded',
lastCheck: checks[checks.length - 1].t, responseTime: 50,
startDate: checks[0].t, uptime: 0,
recentChecks: checks,
dailyHistory: [{ date: new Date(now).toISOString().split('T')[0], status: 'degraded' }],
},
}
writeFileSync(KV_FILE, JSON.stringify({ monitors: JSON.stringify(mon), lastUpdate: checks[checks.length - 1].t }, null, 2))
}
function readChecks() {
const data = JSON.parse(readFileSync(KV_FILE, 'utf-8'))
const monitors = JSON.parse(data.monitors)
return monitors[MONITOR_ID]?.recentChecks ?? []
}
async function main() {
// Sauvegarde les fichiers locaux pour les restaurer après le test.
const hadKv = existsSync(KV_FILE)
const hadMonitors = existsSync(MONITORS_FILE)
if (hadKv) copyFileSync(KV_FILE, BACKUP)
if (hadMonitors) copyFileSync(MONITORS_FILE, MONITORS_BACKUP)
let child
const target = createServer((_, response) => {
response.writeHead(200, { 'content-type': 'text/plain' })
response.end('ok')
})
try {
await new Promise((resolve, reject) => {
target.once('error', reject)
target.listen(TARGET_PORT, '127.0.0.1', resolve)
})
writeFileSync(MONITORS_FILE, JSON.stringify([{
id: MONITOR_ID,
name: 'KV accumulation test',
url: `http://127.0.0.1:${TARGET_PORT}/health`,
method: 'GET',
acceptedStatusCodes: ['200'],
}], null, 2))
seed()
const before = readChecks().length
if (before !== SEED_COUNT) {
throw new Error(`Seed raté : attendu ${SEED_COUNT}, obtenu ${before}`)
}
// Lance le dev-server : runChecks() s'exécute une fois au démarrage.
child = spawn('node', ['dev-server.js'], {
cwd: root,
env: { ...process.env, CRON_CHECK_INTERVAL: '59', API_PORT: String(API_PORT) },
stdio: ['ignore', 'pipe', 'pipe'],
})
let output = ''
child.stdout.on('data', (chunk) => { output += chunk.toString() })
child.stderr.on('data', (chunk) => { output += chunk.toString() })
const deadline = Date.now() + 10000
let after = before
while (Date.now() < deadline && after < before + 1) {
await new Promise((resolve) => setTimeout(resolve, 100))
after = readChecks().length
}
if (!output.includes('Monitoring checks every 59 minutes')) {
throw new Error('CRON_CHECK_INTERVAL n\'est pas appliqué par le serveur local')
}
if (after >= before + 1) {
console.log(`✓ Accumulation OK : ${before} seedés -> ${after} après 1 check (pas d'écrasement)`)
process.exitCode = 0
} else {
console.error(`✗ Aucun contrôle ajouté : ${before} seedés -> ${after}.`)
if (output.trim()) console.error(output.trim())
process.exitCode = 1
}
} catch (error) {
console.error(`✗ Test KV impossible : ${error.message}`)
process.exitCode = 1
} finally {
child?.kill()
await new Promise((resolve) => target.close(resolve))
// Restaure les fichiers d'origine.
if (hadKv) {
copyFileSync(BACKUP, KV_FILE)
rmSync(BACKUP, { force: true })
} else if (existsSync(KV_FILE)) {
rmSync(KV_FILE, { force: true })
}
if (hadMonitors) {
copyFileSync(MONITORS_BACKUP, MONITORS_FILE)
rmSync(MONITORS_BACKUP, { force: true })
} else if (existsSync(MONITORS_FILE)) {
rmSync(MONITORS_FILE, { force: true })
}
// Nettoie le handshake de port écrit par le dev-server du test.
const portFile = join(root, '.dev-api-port')
if (existsSync(portFile)) rmSync(portFile, { force: true })
}
}
main()