mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 15:09:42 +02:00
fix(self-update): stop a stalled status from blocking every later update
A Homebrew node upgrade under a long-running server deletes the versioned Cellar path the server passes as --node, so every status write from the updater failed. The update itself still built and restarted (npm and the build use node from PATH), but update-status.json stayed "queued" forever. The boot reconcile ran one minute after the restart, inside its 15 min window, and isInFlight() had no age limit, so "An update is already in progress." blocked every later update until the next server restart. - self-update.sh falls back to node on PATH when --node is not executable. - expireStalledStatus() (pure) fails an in-flight status whose last write is older than the stale window; applied on every read (start + status poll) and persisted. The live updater heartbeats every few seconds, so a running update never trips it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
isValidReleaseTag,
|
||||
parseGitHubRepo,
|
||||
reconcileStatusDecision,
|
||||
expireStalledStatus,
|
||||
} from '../src/web/self-update.js';
|
||||
import type { UpdateStatus } from '../src/types/update.js';
|
||||
|
||||
@@ -167,3 +168,40 @@ describe('reconcileStatusDecision (boot handoff state machine)', () => {
|
||||
expect(reconcileStatusDecision(noTarget, '0.9.4', NOW)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('expireStalledStatus (runtime staleness backstop)', () => {
|
||||
const NOW = 1_000_000_000_000;
|
||||
const MIN = 60 * 1000;
|
||||
const base = (over: Partial<UpdateStatus>): UpdateStatus => ({
|
||||
updateId: 'u1',
|
||||
phase: 'queued',
|
||||
message: '',
|
||||
fromVersion: '1.24.7',
|
||||
toVersion: '1.29.0',
|
||||
startedAt: NOW - 5_000,
|
||||
updatedAt: NOW - 5_000,
|
||||
...over,
|
||||
});
|
||||
|
||||
it('leaves a heartbeating update alone', () => {
|
||||
expect(
|
||||
expireStalledStatus(base({ phase: 'installing', startedAt: NOW - 60 * MIN, updatedAt: NOW - 3_000 }), NOW)
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('fails a status that stopped heartbeating (queued forever: status writes were failing)', () => {
|
||||
const out = expireStalledStatus(
|
||||
base({ startedAt: NOW - 8 * 24 * 60 * MIN, updatedAt: NOW - 8 * 24 * 60 * MIN }),
|
||||
NOW
|
||||
);
|
||||
expect(out?.phase).toBe('failed');
|
||||
expect(out?.error).toContain('queued');
|
||||
expect(out?.updatedAt).toBe(NOW);
|
||||
});
|
||||
|
||||
it('never touches terminal phases or a missing status', () => {
|
||||
expect(expireStalledStatus(null, NOW)).toBeNull();
|
||||
expect(expireStalledStatus(base({ phase: 'completed', updatedAt: NOW - 60 * MIN }), NOW)).toBeNull();
|
||||
expect(expireStalledStatus(base({ phase: 'failed', updatedAt: NOW - 60 * MIN }), NOW)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user