test(integration): restart managed runtime for verify

This commit is contained in:
rcourtman 2026-03-28 23:36:49 +00:00
parent 60e902ab9a
commit d877763fde
4 changed files with 123 additions and 2 deletions

View file

@ -200,6 +200,11 @@ across pretest, Playwright, and posttest. `scripts/hot-dev.sh` must honor that
lock by suppressing source-triggered rebuilds and manual `pulse` binary restart
churn while the owning proof process is still alive. Stale verify locks must
clear themselves automatically once the owning process exits.
That same verification contract also applies before Playwright attaches: if a
managed hot-dev session is already running when the verify lock is active, the
integration launcher must restart that session instead of silently attaching to
an old frontend process, so browser proof reflects the current branch-tip
source rather than whatever Vite shell happened to be left alive.
That same launcher boundary also owns its CLI contract: managed commands such
as `start --takeover` and `restart --takeover` must preserve the takeover flag
through the actual script entrypoint instead of silently dropping second-arg

View file

@ -243,6 +243,36 @@ test_verify_bg_holds_runtime_lock_for_proof_duration() {
assert_contains "verify clears the runtime lock after proofs finish" "${output}" "lock_after=no"
}
test_managed_dev_runtime_restarts_existing_session_for_verification() {
local output
output="$(
ROOT_DIR="${ROOT_DIR}" \
node --input-type=module <<'EOF'
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
const rootDir = process.env.ROOT_DIR;
const modulePath = path.join(rootDir, 'tests', 'integration', 'scripts', 'managed-dev-runtime.mjs');
const { shouldRestartManagedDevRuntimeForVerification } = await import(`file://${modulePath}`);
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'pulse-managed-dev-verify-'));
const lockPath = path.join(tempDir, 'hot-dev.verify.lock');
await fs.writeFile(lockPath, `pid=${process.pid}\ncreated_at=2026-03-28T23:00:00Z\n`, 'utf8');
console.log(
`running=${shouldRestartManagedDevRuntimeForVerification({ env: { HOT_DEV_VERIFY_LOCK_FILE: lockPath }, wasRunning: true })}`,
);
console.log(
`stopped=${shouldRestartManagedDevRuntimeForVerification({ env: { HOT_DEV_VERIFY_LOCK_FILE: lockPath }, wasRunning: false })}`,
);
EOF
)"
assert_contains "managed dev runtime restarts existing sessions during verification" "${output}" "running=true"
assert_contains "managed dev runtime does not restart absent prior session" "${output}" "stopped=false"
}
test_takeover_avoids_killing_current_shell_lineage() {
local output
output="$(
@ -865,6 +895,7 @@ main() {
test_verify_command_injects_managed_runtime_env
test_default_verify_command_runs_runtime_and_layout_proofs
test_verify_bg_holds_runtime_lock_for_proof_duration
test_managed_dev_runtime_restarts_existing_session_for_verification
test_takeover_avoids_killing_current_shell_lineage
test_launchd_session_supervises_managed_runtime
test_start_bg_reports_browser_entrypoint

View file

@ -1,3 +1,4 @@
import fs from 'node:fs';
import { spawn } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@ -18,6 +19,10 @@ function hotDevBgScriptPath(env = process.env) {
return path.join(repoRootFromEnv(env), 'scripts', 'hot-dev-bg.sh');
}
function managedVerifyLockPath(env = process.env) {
return trim(env.HOT_DEV_VERIFY_LOCK_FILE) || path.join(repoRootFromEnv(env), 'tmp', 'hot-dev.verify.lock');
}
function hotDevBrowserURL(env = process.env) {
const host = trim(env.FRONTEND_DEV_HOST) || '127.0.0.1';
const port = trim(env.FRONTEND_DEV_PORT) || '5173';
@ -63,6 +68,32 @@ function statusReportsRunning(output) {
return output.includes('[hot-dev-bg] Running');
}
export function managedVerifyLockActive(env = process.env) {
try {
const raw = fs.readFileSync(managedVerifyLockPath(env), 'utf8');
const ownerPid = Number.parseInt(
raw
.split('\n')
.find((line) => line.startsWith('pid='))?.slice(4) || '',
10,
);
if (!Number.isInteger(ownerPid) || ownerPid <= 0) {
return false;
}
process.kill(ownerPid, 0);
return true;
} catch {
return false;
}
}
export function shouldRestartManagedDevRuntimeForVerification({
env = process.env,
wasRunning,
}) {
return Boolean(wasRunning) && managedVerifyLockActive(env);
}
async function managedRuntimeStatusOutput(env = process.env) {
const status = await runHotDevBg(['status'], env);
if (status.code !== 0) {
@ -231,14 +262,21 @@ export async function startManagedDevRuntime({
throw new Error(`hot-dev-bg status failed before start: ${statusBefore.stderr || statusBefore.stdout}`);
}
const wasRunning = statusReportsRunning(`${statusBefore.stdout}${statusBefore.stderr}`);
const shouldRestartForVerification = shouldRestartManagedDevRuntimeForVerification({
env,
wasRunning,
});
const startArgs = ['start'];
const startArgs = [shouldRestartForVerification ? 'restart' : 'start'];
if (truthy(env.PULSE_E2E_HOT_DEV_TAKEOVER)) {
startArgs.push('--takeover');
}
if (shouldRestartForVerification) {
logger.log('[integration] Restarting managed dev runtime before browser verification to pick up current source changes');
}
const startResult = await runHotDevBg(startArgs, env);
if (startResult.code !== 0) {
throw new Error(`hot-dev-bg start failed: ${startResult.stderr || startResult.stdout}`);
throw new Error(`hot-dev-bg ${startArgs[0]} failed: ${startResult.stderr || startResult.stdout}`);
}
await waitForStableManagedRuntime({ env });

View file

@ -0,0 +1,47 @@
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import {
managedVerifyLockActive,
shouldRestartManagedDevRuntimeForVerification,
} from './managed-dev-runtime.mjs';
test('managedVerifyLockActive returns true for a live verify lock owner', async () => {
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'pulse-managed-dev-runtime-'));
const lockPath = path.join(rootDir, 'hot-dev.verify.lock');
await fs.writeFile(lockPath, `pid=${process.pid}\ncreated_at=2026-03-28T23:00:00Z\n`, 'utf8');
assert.equal(managedVerifyLockActive({ HOT_DEV_VERIFY_LOCK_FILE: lockPath }), true);
});
test('managedVerifyLockActive clears false when the verify lock owner is stale', async () => {
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'pulse-managed-dev-runtime-'));
const lockPath = path.join(rootDir, 'hot-dev.verify.lock');
await fs.writeFile(lockPath, 'pid=999999\ncreated_at=2026-03-28T23:00:00Z\n', 'utf8');
assert.equal(managedVerifyLockActive({ HOT_DEV_VERIFY_LOCK_FILE: lockPath }), false);
});
test('shouldRestartManagedDevRuntimeForVerification only restarts existing sessions under verify lock', async () => {
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'pulse-managed-dev-runtime-'));
const lockPath = path.join(rootDir, 'hot-dev.verify.lock');
await fs.writeFile(lockPath, `pid=${process.pid}\ncreated_at=2026-03-28T23:00:00Z\n`, 'utf8');
assert.equal(
shouldRestartManagedDevRuntimeForVerification({
env: { HOT_DEV_VERIFY_LOCK_FILE: lockPath },
wasRunning: true,
}),
true,
);
assert.equal(
shouldRestartManagedDevRuntimeForVerification({
env: { HOT_DEV_VERIFY_LOCK_FILE: lockPath },
wasRunning: false,
}),
false,
);
});