b7fea83ed6
Deliver the initial local bridge, Noctalia v4/v5 adapters, desktop client, service unit, tests, and implementation documentation for persistent Pi status and control.
107 lines
2.4 KiB
JavaScript
107 lines
2.4 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { mkdtemp, readFile, stat, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import test from "node:test";
|
|
import {
|
|
BridgeAlreadyRunningError,
|
|
BridgeLockError,
|
|
acquireBridgeLock,
|
|
lockMode,
|
|
} from "../src/bridge/instance-lock.js";
|
|
|
|
async function paths() {
|
|
const directory = await mkdtemp(join(tmpdir(), "pi-status-bridge-lock-"));
|
|
return {
|
|
lockPath: join(directory, "bridge.lock"),
|
|
socketPath: join(directory, "bridge.sock"),
|
|
};
|
|
}
|
|
|
|
test("acquires an owner-only lock and releases only its own token", async () => {
|
|
const { lockPath, socketPath } = await paths();
|
|
const lock = await acquireBridgeLock({
|
|
lockPath,
|
|
socketPath,
|
|
pid: 101,
|
|
now: () => new Date("2026-01-02T03:04:05.000Z"),
|
|
});
|
|
const lockContents = await readFile(lockPath, "utf8");
|
|
let metadata;
|
|
try {
|
|
metadata = JSON.parse(lockContents);
|
|
} catch (error) {
|
|
assert.fail(
|
|
error instanceof Error ? error.message : "lock file was not JSON",
|
|
);
|
|
}
|
|
|
|
const lockStats = await stat(lockPath);
|
|
assert.equal(metadata.pid, 101);
|
|
assert.equal(metadata.socketPath, socketPath);
|
|
assert.equal(lockStats.mode & 0o777, 0o600);
|
|
assert.equal(lockMode, 0o600);
|
|
assert.equal(await lock.release(), true);
|
|
assert.equal(await lock.release(), false);
|
|
});
|
|
|
|
test("refuses to replace a live owner", async () => {
|
|
const { lockPath, socketPath } = await paths();
|
|
await writeFile(
|
|
lockPath,
|
|
JSON.stringify({
|
|
pid: 202,
|
|
socketPath,
|
|
startedAt: "2026-01-02T03:04:05.000Z",
|
|
token: "live-owner",
|
|
}),
|
|
);
|
|
|
|
await assert.rejects(
|
|
acquireBridgeLock({
|
|
lockPath,
|
|
socketPath,
|
|
pid: 303,
|
|
processAlive: (pid) => pid === 202,
|
|
}),
|
|
BridgeAlreadyRunningError,
|
|
);
|
|
});
|
|
|
|
test("recovers only a proven-stale lock", async () => {
|
|
const { lockPath, socketPath } = await paths();
|
|
await writeFile(
|
|
lockPath,
|
|
JSON.stringify({
|
|
pid: 404,
|
|
socketPath,
|
|
startedAt: "2026-01-02T03:04:05.000Z",
|
|
token: "stale-owner",
|
|
}),
|
|
);
|
|
|
|
const lock = await acquireBridgeLock({
|
|
lockPath,
|
|
socketPath,
|
|
pid: 505,
|
|
processAlive: () => false,
|
|
});
|
|
assert.equal(lock.metadata.pid, 505);
|
|
await lock.release();
|
|
});
|
|
|
|
test("does not reclaim a malformed lock", async () => {
|
|
const { lockPath, socketPath } = await paths();
|
|
await writeFile(lockPath, "broken");
|
|
|
|
await assert.rejects(
|
|
acquireBridgeLock({
|
|
lockPath,
|
|
socketPath,
|
|
pid: 606,
|
|
processAlive: () => false,
|
|
}),
|
|
BridgeLockError,
|
|
);
|
|
});
|