Files
MikrotikManager/backend/scripts/test-source-capture-adopted-postgres.mjs
T

88 lines
6.1 KiB
JavaScript

// Isolated regression only: no application config, configured DB URL or startup.
import { mkdtemp, rm, readFile, writeFile, mkdir } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve, relative, isAbsolute, basename } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { createRequire } from 'node:module'
import { randomBytes, createHash } from 'node:crypto'
import { createServer } from 'node:net'
import { spawn } from 'node:child_process'
const args = process.argv.slice(2)
if (args.length > 1 || args[0] && !args[0].startsWith('--source-root=')) throw Error('Only --source-root=PATH review override is supported')
const sourceRoot = args[0] ? resolve(args[0].slice('--source-root='.length)) : fileURLToPath(new URL('../../', import.meta.url))
const require = createRequire(join(sourceRoot, 'backend/package.json'))
const { default: EmbeddedPostgres } = await import(pathToFileURL(require.resolve('embedded-postgres')).href)
const sources = ['backend/src/services/partition-write-transaction.ts', 'backend/src/db/sqlite-import-control-writes.ts',
'backend/src/db/migration-schema-inventory.ts', 'backend/src/db/migration-data-inventory.ts', 'backend/src/db/schema.ts', 'backend/src/db/index.ts']
const ownFiles = [fileURLToPath(import.meta.url), fileURLToPath(new URL('../src/db/database-source-capture-adopted.sql-helper.ts', import.meta.url)),
fileURLToPath(new URL('../src/db/database-source-capture-adopted.sql.test.ts', import.meta.url))]
async function pins() {
const files = [...sources.map(path => ({ path, absolute: join(sourceRoot, path) })), ...ownFiles.map(absolute => ({ path: 'executed-fixture/' + basename(absolute), absolute }))]
return Promise.all(files.map(async ({ path, absolute }) => ({ path, sha256: createHash('sha256').update(await readFile(absolute)).digest('hex') })))
}
const before = await pins()
const directory = await mkdtemp(join(tmpdir(), 'mm-source-capture-'))
const password = randomBytes(24).toString('hex'), ownerToken = randomBytes(24).toString('hex')
let postgres, failure, phase = 'port-and-owner-manifest', failedPhase, childExitCode = 1, unchanged = false, clusterStopped = false, directoryRemoved = false
try {
const listener = createServer()
await new Promise((done, reject) => { listener.once('error', reject); listener.listen(0, '127.0.0.1', done) })
const port = listener.address().port
await new Promise((done, reject) => listener.close(error => error ? reject(error) : done()))
await writeFile(join(directory, 'owned-source-capture.json'), JSON.stringify({ ownerToken, port, username: 'source_operator' }), { flag: 'wx' })
postgres = new EmbeddedPostgres({ databaseDir: join(directory, 'pg'), port, user: 'source_operator', password,
persistent: true, createPostgresUser: false, initdbFlags: ['--encoding=UTF8'], postgresFlags: ['-h', '127.0.0.1'], onLog: () => {}, onError: () => {} })
phase = 'initialise-owned-postgres'; await postgres.initialise()
phase = 'start-owned-postgres'; await postgres.start()
phase = 'owned-sql-child'
childExitCode = await new Promise((done, reject) => {
const child = spawn(process.execPath, ['--import', pathToFileURL(require.resolve('tsx')).href, ownFiles[2]], {
cwd: directory, windowsHide: true, stdio: 'inherit', env: {
...Object.fromEntries(['SystemRoot', 'WINDIR', 'TEMP', 'TMP', 'PATH', 'Path'].filter(key => process.env[key]).map(key => [key, process.env[key]])),
MMAPP_OWN_SOURCE_CAPTURE: 'true', MMAPP_OWN_SOURCE_CAPTURE_DIRECTORY: directory, MMAPP_OWN_SOURCE_CAPTURE_ROOT: sourceRoot,
MMAPP_OWN_SOURCE_CAPTURE_TOKEN: ownerToken, MMAPP_OWN_SOURCE_CAPTURE_ADMIN_URL: `postgresql://source_operator:${password}@127.0.0.1:${port}/postgres`,
},
})
// No child subprocesses are spawned by the SQL fixture. Kill only this
// exact Node child on timeout, then await exit before stopping own cluster.
let timedOut = false
const timer = setTimeout(() => { timedOut = true; child.kill(); }, 120000)
child.once('error', error => { clearTimeout(timer); reject(error) })
child.once('exit', code => { clearTimeout(timer); done(timedOut ? 1 : code ?? 1) })
})
} catch (error) { failure = error; failedPhase = phase }
finally {
try {
if (postgres) await postgres.stop()
clusterStopped = true
} catch (error) { failure ??= error }
// Delete only when stop succeeded, retaining failed-stop files for diagnosis.
if (clusterStopped) {
try {
const root = resolve(tmpdir()), target = resolve(directory), rel = relative(root, target)
if (!rel || rel.startsWith('..') || isAbsolute(rel) || !basename(target).startsWith('mm-source-capture-')) throw Error('Unsafe owned source capture cleanup path')
await rm(target, { recursive: true, force: true }); directoryRemoved = true
} catch (error) { failure ??= error }
}
try {
const after = await pins()
unchanged = JSON.stringify(before) === JSON.stringify(after)
await mkdir(join(sourceRoot, 'tmp'), { recursive: true })
const evidencePath = join(sourceRoot, 'tmp', basename(directory) + '-pins.json')
await writeFile(evidencePath, JSON.stringify({ format: 'adopted-source-capture-owned-pins-v1', date: new Date().toISOString(),
before, after, unchanged, childExitCode, clusterStopped, directoryRemoved,
allWriterDrainAuthenticated: false, sourceGenerationAuthenticated: false, readyAuthorized: false }, null, 2))
console.log('Owned source capture evidence:', evidencePath)
} catch (error) { failure ??= error }
}
console.log(JSON.stringify({ childExitCode, sourcePinsUnchanged: unchanged, clusterStopped, directoryRemoved,
allWriterDrainAuthenticated: false, sourceGenerationAuthenticated: false, readyAuthorized: false }))
// Never print connection strings or credentials in fixture error output.
if (failure) {
const text = String(failure)
const knownCode = ['ENOMEM', 'EACCES', 'EPERM', 'ENOENT', 'EADDRINUSE', 'ENOSPC'].find(code => text.includes(code)) ?? 'UNCLASSIFIED'
console.error(JSON.stringify({ ownedRunnerFailure: true, phase: failedPhase ?? 'pins-or-cleanup', knownCode }))
}
process.exitCode = failure || !unchanged || !clusterStopped || !directoryRemoved ? 1 : childExitCode