Files
memind/release-goosed-remount-guard.test.mjs
T
john 4e6db07123
Memind CI / Test, build, and release guards (pull_request) Successful in 2m26s
fix: verify MindSpace service replacement
2026-07-21 18:55:23 +08:00

70 lines
3.4 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
const releaseScript = fs.readFileSync(
new URL('./scripts/release-portal-runtime-prod.sh', import.meta.url),
'utf8',
);
const mindSpaceReleaseScript = fs.readFileSync(
new URL('./scripts/release-mindspace-service-prod.sh', import.meta.url),
'utf8',
);
test('103 release remounts goosed after swapping the live directory', () => {
assert.match(releaseScript, /remount_goosed_after_live_swap\(\)/);
assert.match(releaseScript, /docker-compose\.prod\.yml/);
assert.match(releaseScript, /up -d --force-recreate/);
assert.match(releaseScript, /MindSpace\/\.goosed-remount-/);
assert.match(releaseScript, /containers\[@\].*!= 9/);
assert.match(releaseScript, /seq 18006 18014/);
assert.match(releaseScript, /\/opt\/portal\/mindspace-sandbox-mcp\.mjs/);
const swapIndex = releaseScript.indexOf('mv "${RUNTIME_DIR}" "${APP_DIR}"');
const remountIndex = releaseScript.indexOf('remount_goosed_after_live_swap', swapIndex);
const portalStartIndex = releaseScript.indexOf('say "启动新的 Portal runtime"');
assert.ok(swapIndex >= 0, 'live directory swap must exist');
assert.ok(remountIndex > swapIndex, 'goosed remount must happen after the live swap');
assert.ok(portalStartIndex > remountIndex, 'Portal must start only after goosed remount succeeds');
});
test('MindSpace release preserves runtime data and returns it during rollback', () => {
assert.match(mindSpaceReleaseScript, /PERSISTED_DATA_MOVED=0/);
assert.match(mindSpaceReleaseScript, /PERSISTED_USERS_MOVED=0/);
assert.match(mindSpaceReleaseScript, /mv "\$\{OLD_APP_ARCHIVE\}\/data" "\$\{APP_DIR\}\/data"/);
assert.match(mindSpaceReleaseScript, /mv "\$\{OLD_APP_ARCHIVE\}\/users" "\$\{APP_DIR\}\/users"/);
assert.match(mindSpaceReleaseScript, /mv "\$\{APP_DIR\}\/data" "\$\{OLD_APP_ARCHIVE\}\/data"/);
assert.match(mindSpaceReleaseScript, /mv "\$\{APP_DIR\}\/users" "\$\{OLD_APP_ARCHIVE\}\/users"/);
});
test('MindSpace release waits for the old listener and verifies the new launchd job', () => {
const bootoutIndex = mindSpaceReleaseScript.indexOf(
'launchctl bootout "${LAUNCHD_GUI}/${SERVICE_LABEL}"',
);
const listenerWaitIndex = mindSpaceReleaseScript.indexOf('lsof -nP -iTCP:8082', bootoutIndex);
const swapIndex = mindSpaceReleaseScript.indexOf('mv "${NEW_DIR}" "${APP_DIR}"');
const bootstrapIndex = mindSpaceReleaseScript.indexOf(
'launchctl bootstrap "${LAUNCHD_GUI}" "${SERVICE_PLIST}"',
swapIndex,
);
const portalRestartIndex = mindSpaceReleaseScript.indexOf(
'launchctl kickstart -k "${LAUNCHD_GUI}/${PORTAL_LABEL}"',
bootstrapIndex,
);
const finalServiceCheckIndex = mindSpaceReleaseScript.indexOf('service_is_running', portalRestartIndex);
assert.ok(bootoutIndex >= 0, 'old MindSpace launchd job must be stopped');
assert.ok(listenerWaitIndex > bootoutIndex, 'release must wait for the old 8082 listener to exit');
assert.ok(swapIndex > listenerWaitIndex, 'runtime swap must happen after the old listener exits');
assert.ok(bootstrapIndex > swapIndex, 'new launchd job must start after the runtime swap');
assert.doesNotMatch(
mindSpaceReleaseScript.slice(bootstrapIndex, bootstrapIndex + 120),
/\|\| true/,
'launchctl bootstrap failures must not be ignored',
);
assert.ok(
finalServiceCheckIndex > portalRestartIndex,
'MindSpace launchd state must be rechecked after Portal restarts',
);
});