import assert from "node:assert/strict"; import { spawn } from "node:child_process"; import { randomUUID } from "node:crypto"; import { fileURLToPath } from "node:url"; import pg from "pg"; const { Pool } = pg; const clients = positiveInteger(process.env.TASKTREE_MIGRATION_SMOKE_CLIENTS, 8); const rounds = positiveInteger(process.env.TASKTREE_MIGRATION_SMOKE_ROUNDS, 3); const adminUrl = process.env.TASKTREE_MIGRATION_SMOKE_DATABASE_URL || "postgresql:///postgres?host=/var/run/postgresql&user=root"; const databaseName = `tasktree_smoke_${randomUUID().replaceAll("-", "")}`; const cliPath = fileURLToPath(new URL("../tools/hwlab-cli/bin/hwlab-cli.ts", import.meta.url)); const scopedUrl = new URL(adminUrl); scopedUrl.pathname = `/${databaseName}`; scopedUrl.searchParams.set("sslmode", "disable"); scopedUrl.searchParams.set("options", "-clock_timeout=1500ms"); const databaseUrl = scopedUrl.toString(); const admin = new Pool({ connectionString: adminUrl, max: 2 }); const scoped = new Pool({ connectionString: databaseUrl, max: 4 }); try { await admin.query(`CREATE DATABASE ${databaseName}`); const fresh = await Promise.all(Array.from({ length: clients }, () => runCli(["group", "list"]))); fresh.forEach((result) => assertEnvelope(result, "group.list")); await assertMigrations(); const createdGroup = await runCli(["group", "create", "--name", `Concurrency smoke ${Date.now()}`]); assertEnvelope(createdGroup, "group.create"); const groupId = createdGroup.data.id; const createdTask = await runCli(["task", "create", "--group", groupId, "--title", "Read envelope seed"]); assertEnvelope(createdTask, "task.create"); const taskId = createdTask.data.id; const report = await runCli(["report", "write", "--task", taskId, "--title", "Smoke", "--body", "passed"]); assertEnvelope(report, "report.write"); assert.equal(report.data.mutation, true); await scoped.query( "DELETE FROM tasktree_schema_migrations WHERE migration_id = ANY($1::text[])", [["tasktree-20260717-v2-subsubtask", "tasktree-20260718-v3-github-backup"]], ); await scoped.query("ALTER TABLE tasktree_tasks DROP CONSTRAINT tasktree_tasks_kind_check"); await scoped.query( "INSERT INTO tasktree_tasks (id,group_id,kind,title) VALUES ($1,$2,$3,$4)", ["tt_invalid_migration_smoke", groupId, "invalid", "Migration rollback sentinel"], ); const failedMigration = await runCli(["group", "list"], true); assert.equal(failedMigration.ok, false); assert.equal(failedMigration.error.code, "23514"); assert.deepEqual(failedMigration.error.detail, { migrationId: "tasktree-20260717-v2-subsubtask", sqlState: "23514", }); const afterFailure = await scoped.query("SELECT migration_id FROM tasktree_schema_migrations ORDER BY migration_id"); assert.deepEqual(afterFailure.rows.map((row) => row.migration_id), ["tasktree-20260716-v1"]); await scoped.query("DELETE FROM tasktree_tasks WHERE id=$1", ["tt_invalid_migration_smoke"]); assertEnvelope(await runCli(["group", "list"]), "group.list"); await assertMigrations(); const blocker = await scoped.connect(); try { await blocker.query("BEGIN"); await blocker.query("SELECT id FROM tasktree_tasks LIMIT 1"); const commands = [ ["group", "list"], ["task", "list", "--group", groupId], ["report", "list", "--task", taskId], ]; for (let round = 0; round < rounds; round += 1) { const results = await Promise.all(Array.from({ length: clients }, (_, index) => runCli(commands[index % commands.length]))); results.forEach((result, index) => assertEnvelope(result, ["group.list", "task.list", "report.list"][index % commands.length])); } await blocker.query("COMMIT"); } catch (error) { await blocker.query("ROLLBACK").catch(() => {}); throw error; } finally { blocker.release(); } console.log(JSON.stringify({ ok: true, operation: "tasktree.schema-concurrency-smoke", data: { clients, rounds, freshCliEnvelopes: fresh.length, existingCliEnvelopes: clients * rounds, migrationRollbackVerified: true, existingReadsWithTableLockVerified: true, migrationIds: await migrationIds(), }, }, null, 2)); } finally { await scoped.end().catch(() => {}); await admin.query(`DROP DATABASE IF EXISTS ${databaseName} WITH (FORCE)`).catch(() => {}); await admin.end().catch(() => {}); } async function runCli(args, allowFailure = false) { const child = spawn(process.execPath, [cliPath, "tasktree", ...args], { cwd: fileURLToPath(new URL("..", import.meta.url)), env: { ...process.env, TASKTREE_DATABASE_URL: databaseUrl, DATABASE_URL: "" }, stdio: ["ignore", "pipe", "pipe"], }); let stdout = ""; let stderr = ""; child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout += chunk; }); child.stderr.setEncoding("utf8").on("data", (chunk) => { stderr += chunk; }); const exitCode = await new Promise((resolve, reject) => { child.once("error", reject); child.once("close", resolve); }); assert.equal(stderr.trim(), "", `CLI stderr must stay empty: ${stderr}`); assert.ok(stdout.trim(), "CLI must return one JSON envelope on stdout"); const result = JSON.parse(stdout); if (!allowFailure) assert.equal(exitCode, 0, JSON.stringify(result)); return result; } function assertEnvelope(result, operation) { assert.equal(result.ok, true, JSON.stringify(result)); assert.equal(result.operation, operation); assert.ok(Object.hasOwn(result, "data"), "successful CLI envelope must contain data"); } async function assertMigrations() { assert.deepEqual(await migrationIds(), [ "tasktree-20260716-v1", "tasktree-20260717-v2-subsubtask", "tasktree-20260718-v3-github-backup", ]); const constraint = await scoped.query( "SELECT COUNT(*)::int AS count FROM pg_constraint WHERE conrelid='tasktree_tasks'::regclass AND conname='tasktree_tasks_kind_check'", ); assert.equal(constraint.rows[0].count, 1); } async function migrationIds() { const result = await scoped.query("SELECT migration_id FROM tasktree_schema_migrations ORDER BY migration_id"); return result.rows.map((row) => row.migration_id); } function positiveInteger(value, fallback) { const parsed = Number(value ?? fallback); assert.ok(Number.isInteger(parsed) && parsed > 0, "smoke concurrency parameters must be positive integers"); return parsed; }