Files
Codeman/test/desktop-keep-awake-lid.test.ts
T
Codeman maintainer 79a6c03363 feat(desktop): keep the Mac awake while agents work, lid closed included
Codeman > Keep This Mac Awake in the macOS app: While Agents Work (an
agent at work, plus 5 minutes after the last one stops) or Always While
Codeman Is Open. Off by default, and only for a Codeman on this Mac.

- Idle sleep needs no password: the app holds an NSProcessInfo activity
  with IdleSystemSleepDisabled, which the kernel drops with the process.
  While it only watches for work, an activity that still allows sleep
  keeps App Nap from slowing its look at the server.
- On a MacBook, Only on Power Adapter (on by default) pauses it on
  battery, and Also With the Lid Closed installs a small LaunchDaemon
  behind macOS's administrator prompt. It sets pmset disablesleep only
  while the app keeps a fresh request file with its live pid, only on
  the power adapter, and undoes only what it set itself. The app writes
  the script and plist as root from the copies built into it.
- A status line at the bottom of the submenu says what it does now.

This brings the macOS part of the keep-awake idea from PR #595 into the
app; the server-side setting and the Linux route are left out for now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 04:51:39 +01:00

235 lines
6.8 KiB
TypeScript

/**
* @fileoverview Runs the real lid helper of the Codeman app for Mac
* (packages/desktop/macos/keep-awake-lid.sh) in bash against a stub `pmset` that
* records what it was asked to do. The helper runs as root from a LaunchDaemon, so the
* cases that matter are the ones where it must NOT act: a stale, dead, foreign or
* symlinked request, a Mac on battery, and an administrator's own `disablesleep 1`,
* which it must never switch off.
*
* Only the BSD/GNU-portable tools the script uses (find -mmin, ps -o uid=, ls -ln) run
* here, so this passes on Linux and macOS alike. The app embeds the same file
* (include_str! in packages/desktop/src/keep_awake.rs), so this is the script that
* gets installed. Port: none.
*/
import { spawnSync } from 'node:child_process';
import {
chmodSync,
existsSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
utimesSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
const HELPER = fileURLToPath(new URL('../packages/desktop/macos/keep-awake-lid.sh', import.meta.url));
let dir: string;
let stateDir: string;
let request: string;
/**
* Stub pmset: `-g` prints the stored SleepDisabled, `-g batt` the stored power source,
* `-a disablesleep N` stores N and logs it.
*/
function writeStubPmset(initial: '0' | '1' | null, power: 'AC Power' | 'Battery Power' | null = 'AC Power') {
const value = join(dir, 'sleepdisabled');
if (initial !== null) writeFileSync(value, initial);
const source = join(dir, 'power');
if (power !== null) writeFileSync(source, power);
const stub = join(dir, 'pmset');
writeFileSync(
stub,
`#!/bin/bash
if [ "$1" = "-g" ] && [ "$2" = "batt" ]; then
[ -f "${source}" ] || exit 1
echo "Now drawing from '$(cat "${source}")'"
exit 0
fi
if [ "$1" = "-g" ]; then
echo "System-wide power settings:"
[ -f "${value}" ] && printf ' SleepDisabled\\t\\t%s\\n' "$(cat "${value}")"
echo "Currently in use:"
echo " sleep 1"
exit 0
fi
if [ "$1" = "-a" ] && [ "$2" = "disablesleep" ]; then
printf '%s' "$3" > "${value}"
echo "disablesleep $3" >> "${join(dir, 'pmset.log')}"
exit 0
fi
exit 1
`
);
chmodSync(stub, 0o755);
return stub;
}
const setPower = (power: 'AC Power' | 'Battery Power') => writeFileSync(join(dir, 'power'), power);
function run() {
const result = spawnSync('bash', [HELPER, request], {
encoding: 'utf-8',
env: {
...process.env,
CODEMAN_KEEPAWAKE_PMSET: join(dir, 'pmset'),
CODEMAN_KEEPAWAKE_STATE_DIR: stateDir,
},
});
expect(result.status, result.stderr).toBe(0);
}
const calls = () => (existsSync(join(dir, 'pmset.log')) ? readFileSync(join(dir, 'pmset.log'), 'utf-8') : '');
const sleepDisabled = () => readFileSync(join(dir, 'sleepdisabled'), 'utf-8');
const owned = () => existsSync(join(stateDir, 'keep-awake.owned'));
/** What the app writes: its own pid and a newline. This test process stands in for it. */
const requestFromLiveApp = () => writeFileSync(request, `${process.pid}\n`);
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'keep-awake-lid-'));
stateDir = join(dir, 'state');
request = join(dir, 'keep-awake-lid.pid');
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
});
describe('keep-awake-lid.sh', () => {
it('does nothing without a request', () => {
writeStubPmset('0');
run();
expect(calls()).toBe('');
expect(owned()).toBe(false);
});
it('applies disablesleep for a fresh request from a live process, then undoes it when the request goes', () => {
writeStubPmset('0');
requestFromLiveApp();
run();
expect(sleepDisabled()).toBe('1');
expect(owned()).toBe(true);
run(); // steady state: no repeated pmset call
expect(calls()).toBe('disablesleep 1\n');
rmSync(request);
run();
expect(sleepDisabled()).toBe('0');
expect(owned()).toBe(false);
expect(calls()).toBe('disablesleep 1\ndisablesleep 0\n');
});
it('never acts on battery, and lets go when the adapter is pulled', () => {
writeStubPmset('0', 'Battery Power');
requestFromLiveApp();
run();
expect(calls()).toBe('');
setPower('AC Power');
run();
expect(sleepDisabled()).toBe('1');
setPower('Battery Power');
run();
expect(sleepDisabled()).toBe('0');
expect(owned()).toBe(false);
});
it('reads an unanswerable power source as battery', () => {
writeStubPmset('0', null);
requestFromLiveApp();
run();
expect(calls()).toBe('');
});
it("never switches off an administrator's own disablesleep", () => {
writeStubPmset('1');
requestFromLiveApp();
run();
expect(calls()).toBe('');
expect(owned()).toBe(false);
rmSync(request);
run();
expect(sleepDisabled()).toBe('1');
expect(calls()).toBe('');
});
it("leaves an administrator's own disablesleep on when the adapter is pulled", () => {
writeStubPmset('1', 'Battery Power');
requestFromLiveApp();
run();
expect(calls()).toBe('');
expect(sleepDisabled()).toBe('1');
});
it('treats an absent SleepDisabled line as 0', () => {
writeStubPmset(null);
requestFromLiveApp();
run();
expect(sleepDisabled()).toBe('1');
expect(owned()).toBe(true);
});
it('re-applies after a reboot or OS update reset it, while the request stands', () => {
writeStubPmset('0');
requestFromLiveApp();
run();
writeFileSync(join(dir, 'sleepdisabled'), '0'); // the reset
run();
expect(sleepDisabled()).toBe('1');
});
it('ignores a stale request (an app that hangs) and releases', () => {
writeStubPmset('0');
requestFromLiveApp();
run();
expect(sleepDisabled()).toBe('1');
const threeMinutesAgo = (Date.now() - 3 * 60_000) / 1000;
utimesSync(request, threeMinutesAgo, threeMinutesAgo);
run();
expect(sleepDisabled()).toBe('0');
expect(owned()).toBe(false);
});
it('releases at the next run once the app is gone, however fresh its file', () => {
writeStubPmset('0');
requestFromLiveApp();
run();
expect(sleepDisabled()).toBe('1');
const gone = spawnSync('true').pid;
writeFileSync(request, `${gone}\n`);
run();
expect(sleepDisabled()).toBe('0');
expect(owned()).toBe(false);
});
it('ignores a request whose pid is not running', () => {
writeStubPmset('0');
const gone = spawnSync('true').pid;
writeFileSync(request, `${gone}\n`);
run();
expect(calls()).toBe('');
});
it('ignores garbage and refuses a symlinked request', () => {
writeStubPmset('0');
writeFileSync(request, 'not-a-pid\n');
run();
expect(calls()).toBe('');
rmSync(request);
const real = join(dir, 'elsewhere.pid');
writeFileSync(real, `${process.pid}\n`);
symlinkSync(real, request);
run();
expect(calls()).toBe('');
});
});