Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion src/mount/relayfile-binary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {

async function writeState(
dir: string,
state: { workspaceId?: string; lastReconcileAt?: string; pid?: number },
state: { workspaceId?: string; lastReconcileAt?: string; pid?: number; daemon?: { pid?: number } },
): Promise<string> {
const statePath = join(dir, 'state.json')
await writeFile(statePath, JSON.stringify(state), 'utf8')
Expand Down Expand Up @@ -170,6 +170,62 @@ describe('checkMountStaleness', () => {
})
})

it('treats a fresh pid-less state as healthy (CLI-daemonized mount records no pid)', async () => {
await withTempDir(async (dir) => {
const statePath = await writeState(dir, {
workspaceId: 'rw_test',
lastReconcileAt: new Date().toISOString(),
// no pid: relayfile start --background does not always record one
})

expect(checkMountStaleness(statePath, 'rw_test')).toEqual({ stale: false })
})
})

it('uses daemon.pid for liveness when the top-level pid is absent (CLI-daemonized mount)', async () => {
await withTempDir(async (dir) => {
const statePath = await writeState(dir, {
workspaceId: 'rw_test',
lastReconcileAt: new Date().toISOString(),
daemon: { pid: process.pid },
})

expect(checkMountStaleness(statePath, 'rw_test')).toEqual({ stale: false, pid: process.pid })
})
})

it('marks a stale mount via a dead daemon.pid even within the reconcile window', async () => {
await withTempDir(async (dir) => {
const statePath = await writeState(dir, {
workspaceId: 'rw_test',
lastReconcileAt: new Date().toISOString(),
daemon: { pid: 12345 },
})
vi.spyOn(process, 'kill').mockImplementation(() => {
throw Object.assign(new Error('not found'), { code: 'ESRCH' })
})

expect(checkMountStaleness(statePath, 'rw_test')).toEqual({
stale: true,
reason: 'mount process (pid 12345) is not running',
pid: 12345,
})
})
})

it('still marks a pid-less state stale when the reconcile timestamp is old', async () => {
await withTempDir(async (dir) => {
const statePath = await writeState(dir, {
workspaceId: 'rw_test',
lastReconcileAt: new Date(Date.now() - 16 * 60 * 1000).toISOString(),
})

const result = checkMountStaleness(statePath, 'rw_test')
expect(result.stale).toBe(true)
expect(result.reason).toMatch(/^last reconcile 16m ago$/u)
})
})

it('returns non-stale with the mount pid when state is fresh and the process is alive', async () => {
await withTempDir(async (dir) => {
const statePath = await writeState(dir, {
Expand Down
20 changes: 16 additions & 4 deletions src/mount/relayfile-binary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@ const NOT_FOUND_ERROR = '[factory] relayfile-mount binary not found. Install dep
type MountState = {
workspaceId?: unknown
lastReconcileAt?: unknown
// The mount process pid. Older mounts wrote a top-level `pid`; the
// CLI-daemonized mount (`relayfile start --background`) records it under
// `daemon.pid` instead. Either may be absent.
pid?: unknown
daemon?: { pid?: unknown }
}

function coercePid(value: unknown): number | undefined {
return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : undefined
}

function canExecute(filePath: string | undefined): filePath is string {
Expand Down Expand Up @@ -155,9 +163,8 @@ export function checkMountStaleness(
}
}

const pid = typeof parsed.pid === 'number' && Number.isInteger(parsed.pid) && parsed.pid > 0
? parsed.pid
: undefined
// Prefer the top-level pid; fall back to the CLI-daemonized mount's daemon.pid.
const pid = coercePid(parsed.pid) ?? coercePid(parsed.daemon?.pid)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Accept daemon.pid during mount readiness checks

When a missing or refreshed mount is started via relayfile start --background, this new daemon.pid support is not used by the readiness path: ensureLocalMount still waits in waitForStateFile, whose isValidMountState only accepts a top-level pid (src/mount/local-mount-preflight.ts lines 178-185). In the same CLI-daemonized state shape this commit is fixing, first startup or auto-refresh will therefore time out even though checkMountStaleness would now consider the state healthy; the readiness validator needs the same daemon.pid/pid-less handling or the factory still fails whenever it has to spawn the mount.

Useful? React with 👍 / 👎.


const lastReconcileAt = typeof parsed.lastReconcileAt === 'string'
? Date.parse(parsed.lastReconcileAt)
Expand All @@ -176,7 +183,12 @@ export function checkMountStaleness(
}

if (pid === undefined) {
return { stale: true, reason: 'mount process pid is missing' }
// Neither a top-level pid nor daemon.pid was recorded. The fresh
// lastReconcileAt validated above is itself proof the mount is live and
// reconciling, so fall back to that rather than declaring a healthy mount
// stale and forcing a spurious refresh (which previously tore down the live
// mount and then failed to re-spawn).
return { stale: false }
}

try {
Expand Down