Files
elizaos--eliza/plugins/plugin-sql/src/__tests__/integration/postgres/pglite-manager-lock.real.test.ts
T
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

204 lines
7.1 KiB
TypeScript

/**
* Tests for `PGliteClientManager`'s file-backed single-writer lock: rejects
* a second manager while the first is confirmed live, reclaims a lock left
* by a dead or reused PID, and reclaims unconditionally in mobile embedded
* mode where the liveness probe is unusable. Writes real lock files to a
* temp dir and exercises the real manager constructor — no mocked lock
* logic, only `readLinuxProcessStartedAtMs` is spied for the PID-reuse case.
*/
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { PGLITE_ERROR_CODES } from "../../../pglite/errors";
import { PGliteClientManager } from "../../../pglite/manager";
const lockPathFor = (dataDir: string) => path.join(dataDir, "eliza-pglite.lock");
const readLinuxBootId = (): string | null => {
try {
const bootId = readFileSync("/proc/sys/kernel/random/boot_id", "utf-8").trim();
return bootId.length > 0 ? bootId : null;
} catch {
return null;
}
};
const readLinuxProcStartTicks = (pid: number | "self"): string | null => {
try {
const stat = readFileSync(`/proc/${pid}/stat`, "utf-8");
const commEnd = stat.lastIndexOf(")");
if (commEnd === -1) {
return null;
}
const fieldsAfterComm = stat
.slice(commEnd + 2)
.trim()
.split(/\s+/);
const startTicks = fieldsAfterComm[19];
return startTicks && /^\d+$/.test(startTicks) ? startTicks : null;
} catch {
return null;
}
};
const currentProcessIdentity = (): { bootId?: string; processStartTicks?: string } => {
const bootId = readLinuxBootId();
const processStartTicks = readLinuxProcStartTicks("self");
return {
...(bootId ? { bootId } : {}),
...(processStartTicks ? { processStartTicks } : {}),
};
};
type PGliteManagerInternals = {
readLinuxProcessStartedAtMs(pid: number): number | null;
};
describe("PGliteClientManager file lock", () => {
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
});
it("rejects a second manager for the same file-backed data dir", async () => {
const dataDir = mkdtempSync(path.join(tmpdir(), "eliza-pglite-lock-"));
tempDirs.push(dataDir);
const first = new PGliteClientManager({ dataDir });
try {
let error: unknown;
try {
new PGliteClientManager({ dataDir });
} catch (err) {
error = err;
}
expect((error as { code?: string }).code).toBe(PGLITE_ERROR_CODES.ACTIVE_LOCK);
} finally {
await first.close();
}
const second = new PGliteClientManager({ dataDir });
await second.close();
});
it("honors a confirmed-live lock regardless of how old its createdAt is", async () => {
// A long-running agent (days/weeks of uptime) holds a lock recording its
// own live PID with an ancient createdAt. Single-writer safety must win:
// a confirmed-alive PID is honored unconditionally, so a second manager is
// rejected rather than reclaiming the lock and opening a dual-writer window.
// (The staleness window only applies to UNCONFIRMABLE liveness — EPERM /
// non-ESRCH — never to a PID we can positively confirm is running.)
const dataDir = mkdtempSync(path.join(tmpdir(), "eliza-pglite-lock-"));
tempDirs.push(dataDir);
const ancientCreatedAt = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
writeFileSync(
lockPathFor(dataDir),
`${JSON.stringify({
pid: process.pid,
createdAt: ancientCreatedAt,
dataDir,
...currentProcessIdentity(),
})}\n`
);
let error: unknown;
try {
new PGliteClientManager({ dataDir });
} catch (err) {
error = err;
}
expect((error as { code?: string }).code).toBe(PGLITE_ERROR_CODES.ACTIVE_LOCK);
// The live lock must be left intact, not reclaimed.
expect(existsSync(lockPathFor(dataDir))).toBe(true);
});
it("reclaims a legacy live-pid lock when proc start time proves PID reuse", async () => {
const dataDir = mkdtempSync(path.join(tmpdir(), "eliza-pglite-lock-"));
tempDirs.push(dataDir);
const lockCreatedAtMs = Date.now() - 60_000;
const processStartedAfterLockMs = lockCreatedAtMs + 10_000;
const startTimeSpy = vi
.spyOn(
PGliteClientManager.prototype as unknown as PGliteManagerInternals,
"readLinuxProcessStartedAtMs"
)
.mockReturnValue(processStartedAfterLockMs);
try {
// The PID is alive, but the lock predates that PID's process generation.
// A process cannot create a lock before it starts, so the PID was reused.
// This is the pid-1-in-container failure mode from #11222 generalized to
// the current test runner PID.
writeFileSync(
lockPathFor(dataDir),
`${JSON.stringify({
pid: process.pid,
createdAt: new Date(lockCreatedAtMs).toISOString(),
dataDir,
})}\n`
);
const manager = new PGliteClientManager({ dataDir });
await manager.close();
expect(existsSync(lockPathFor(dataDir))).toBe(false);
} finally {
startTimeSpy.mockRestore();
}
});
it("reclaims a lock owned by a non-running PID", async () => {
const dataDir = mkdtempSync(path.join(tmpdir(), "eliza-pglite-lock-"));
tempDirs.push(dataDir);
// PID that cannot exist on Linux/macOS (above the configured pid_max).
writeFileSync(
lockPathFor(dataDir),
`${JSON.stringify({
pid: 2_147_483_646,
createdAt: new Date().toISOString(),
dataDir,
})}\n`
);
const manager = new PGliteClientManager({ dataDir });
await manager.close();
expect(existsSync(lockPathFor(dataDir))).toBe(false);
});
it("mobile embedded mode reclaims ANY leftover lock — even one recording a confirmed-live PID (#11030)", async () => {
// iOS/Android local backend is single-tenant: Bun runs as a thread inside
// the one app process and ElizaBunRuntime serializes engine starts, so a
// leftover lock is stale by definition. The liveness probe is unusable
// there: a prior LAUNCH's PID probes EPERM in the iOS sandbox (honored
// for 7 days -> every relaunch bricked with "PGlite data dir is already
// in use"), and a prior Bun THREAD's recorded PID equals the CURRENT app
// PID (probes alive forever). This is the exact on-device #11030
// post-engine-fix failure shape: lock pid == process.pid, recent
// createdAt, and the app must still boot.
const dataDir = mkdtempSync(path.join(tmpdir(), "eliza-pglite-lock-"));
tempDirs.push(dataDir);
writeFileSync(
lockPathFor(dataDir),
`${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), dataDir })}\n`
);
process.env.ELIZA_IOS_LOCAL_BACKEND = "1";
try {
const manager = new PGliteClientManager({ dataDir });
await manager.close();
} finally {
delete process.env.ELIZA_IOS_LOCAL_BACKEND;
}
// Reclaimed, re-acquired, and released on close.
expect(existsSync(lockPathFor(dataDir))).toBe(false);
});
});