288 lines
9.3 KiB
TypeScript
288 lines
9.3 KiB
TypeScript
import {
|
|
extractCliArgValues,
|
|
resolveTeamMemberRuntimeLiveness,
|
|
sanitizeProcessCommandForDiagnostics,
|
|
} from '@main/services/team/TeamRuntimeLivenessResolver';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
const NOW = '2026-04-24T12:00:00.000Z';
|
|
|
|
describe('resolveTeamMemberRuntimeLiveness', () => {
|
|
it('classifies tmux shell panes as weak shell-only evidence', () => {
|
|
const result = resolveTeamMemberRuntimeLiveness({
|
|
teamName: 'demo',
|
|
memberName: 'bob',
|
|
agentId: 'agent-bob',
|
|
backendType: 'tmux',
|
|
tmuxPaneId: '%1',
|
|
pane: { paneId: '%1', panePid: 100, currentCommand: 'zsh' },
|
|
processRows: [{ pid: 100, ppid: 1, command: 'zsh' }],
|
|
processTableAvailable: true,
|
|
nowIso: NOW,
|
|
});
|
|
|
|
expect(result.alive).toBe(false);
|
|
expect(result.livenessKind).toBe('shell_only');
|
|
expect(result.pidSource).toBe('tmux_pane');
|
|
expect(result.pid).toBe(100);
|
|
});
|
|
|
|
it('promotes a verified team and agent process to strong runtime evidence', () => {
|
|
const result = resolveTeamMemberRuntimeLiveness({
|
|
teamName: 'demo',
|
|
memberName: 'alice',
|
|
agentId: 'agent-alice',
|
|
backendType: 'tmux',
|
|
processRows: [
|
|
{
|
|
pid: 222,
|
|
ppid: 1,
|
|
command: 'node runtime --team-name demo --agent-id agent-alice',
|
|
},
|
|
],
|
|
processTableAvailable: true,
|
|
nowIso: NOW,
|
|
});
|
|
|
|
expect(result.alive).toBe(true);
|
|
expect(result.livenessKind).toBe('runtime_process');
|
|
expect(result.pidSource).toBe('agent_process_table');
|
|
expect(result.pid).toBe(222);
|
|
});
|
|
|
|
it('uses the newest verified team and agent process without requiring sorted rows', () => {
|
|
const result = resolveTeamMemberRuntimeLiveness({
|
|
teamName: 'demo',
|
|
memberName: 'alice',
|
|
agentId: 'agent-alice',
|
|
backendType: 'tmux',
|
|
processRows: [
|
|
{
|
|
pid: 222,
|
|
ppid: 1,
|
|
command: 'node runtime --team-name demo --agent-id agent-alice',
|
|
},
|
|
{
|
|
pid: 111,
|
|
ppid: 1,
|
|
command: 'node runtime --team-name demo --agent-id agent-alice',
|
|
},
|
|
{
|
|
pid: 333,
|
|
ppid: 1,
|
|
command: 'node runtime --team-name other --agent-id agent-alice',
|
|
},
|
|
],
|
|
processTableAvailable: true,
|
|
nowIso: NOW,
|
|
});
|
|
|
|
expect(result.alive).toBe(true);
|
|
expect(result.livenessKind).toBe('runtime_process');
|
|
expect(result.pidSource).toBe('agent_process_table');
|
|
expect(result.pid).toBe(222);
|
|
});
|
|
|
|
it('keeps a verified process pid visible after bootstrap is confirmed', () => {
|
|
const result = resolveTeamMemberRuntimeLiveness({
|
|
teamName: 'demo',
|
|
memberName: 'alice',
|
|
agentId: 'agent-alice',
|
|
backendType: 'tmux',
|
|
trackedSpawnStatus: {
|
|
status: 'online',
|
|
launchState: 'confirmed_alive',
|
|
agentToolAccepted: true,
|
|
runtimeAlive: true,
|
|
bootstrapConfirmed: true,
|
|
hardFailure: false,
|
|
updatedAt: NOW,
|
|
},
|
|
processRows: [
|
|
{
|
|
pid: 222,
|
|
ppid: 1,
|
|
command: 'node runtime --team-name demo --agent-id agent-alice',
|
|
},
|
|
],
|
|
processTableAvailable: true,
|
|
nowIso: NOW,
|
|
});
|
|
|
|
expect(result.alive).toBe(true);
|
|
expect(result.livenessKind).toBe('runtime_process');
|
|
expect(result.pidSource).toBe('agent_process_table');
|
|
expect(result.pid).toBe(222);
|
|
});
|
|
|
|
it('keeps a non-shell tmux descendant without identity as a candidate', () => {
|
|
const result = resolveTeamMemberRuntimeLiveness({
|
|
teamName: 'demo',
|
|
memberName: 'jack',
|
|
agentId: 'agent-jack',
|
|
backendType: 'tmux',
|
|
tmuxPaneId: '%2',
|
|
pane: { paneId: '%2', panePid: 300, currentCommand: 'zsh' },
|
|
processRows: [
|
|
{ pid: 300, ppid: 1, command: 'zsh' },
|
|
{ pid: 301, ppid: 300, command: 'node helper.js' },
|
|
],
|
|
processTableAvailable: true,
|
|
nowIso: NOW,
|
|
});
|
|
|
|
expect(result.alive).toBe(false);
|
|
expect(result.livenessKind).toBe('runtime_process_candidate');
|
|
expect(result.pidSource).toBe('tmux_child');
|
|
expect(result.pid).toBe(301);
|
|
});
|
|
|
|
it('keeps a live OpenCode runtime pid as candidate until bootstrap is confirmed', () => {
|
|
const result = resolveTeamMemberRuntimeLiveness({
|
|
teamName: 'demo',
|
|
memberName: 'bob',
|
|
providerId: 'opencode',
|
|
persistedRuntimePid: 404,
|
|
persistedRuntimeSessionId: 'session-bob',
|
|
processRows: [{ pid: 404, ppid: 1, command: 'opencode runtime host' }],
|
|
processTableAvailable: true,
|
|
nowIso: NOW,
|
|
});
|
|
|
|
expect(result.alive).toBe(false);
|
|
expect(result.livenessKind).toBe('runtime_process_candidate');
|
|
expect(result.pidSource).toBe('opencode_bridge');
|
|
expect(result.pid).toBe(404);
|
|
expect(result.runtimeDiagnostic).toBe(
|
|
'OpenCode runtime process detected, but teammate bootstrap is not confirmed'
|
|
);
|
|
});
|
|
|
|
it('promotes a live OpenCode runtime pid after bootstrap confirmation', () => {
|
|
const result = resolveTeamMemberRuntimeLiveness({
|
|
teamName: 'demo',
|
|
memberName: 'bob',
|
|
providerId: 'opencode',
|
|
persistedRuntimePid: 404,
|
|
persistedRuntimeSessionId: 'session-bob',
|
|
trackedSpawnStatus: {
|
|
status: 'online',
|
|
launchState: 'confirmed_alive',
|
|
agentToolAccepted: true,
|
|
runtimeAlive: true,
|
|
bootstrapConfirmed: true,
|
|
hardFailure: false,
|
|
updatedAt: NOW,
|
|
},
|
|
processRows: [{ pid: 404, ppid: 1, command: 'opencode runtime host' }],
|
|
processTableAvailable: true,
|
|
nowIso: NOW,
|
|
});
|
|
|
|
expect(result.alive).toBe(true);
|
|
expect(result.livenessKind).toBe('runtime_process');
|
|
expect(result.pidSource).toBe('opencode_bridge');
|
|
expect(result.pid).toBe(404);
|
|
});
|
|
|
|
it('does not trust an OpenCode runtime pid reused by an unrelated process', () => {
|
|
const result = resolveTeamMemberRuntimeLiveness({
|
|
teamName: 'demo',
|
|
memberName: 'bob',
|
|
providerId: 'opencode',
|
|
persistedRuntimePid: 404,
|
|
persistedRuntimeSessionId: 'session-bob',
|
|
processRows: [{ pid: 404, ppid: 1, command: 'node unrelated-worker.js' }],
|
|
processTableAvailable: true,
|
|
nowIso: NOW,
|
|
});
|
|
|
|
expect(result.alive).toBe(false);
|
|
expect(result.livenessKind).toBe('runtime_process_candidate');
|
|
expect(result.pidSource).toBe('opencode_bridge');
|
|
expect(result.runtimeDiagnostic).toBe(
|
|
'OpenCode runtime pid is alive, but process identity is unverified'
|
|
);
|
|
});
|
|
|
|
it('does not let a reused OpenCode runtime pid downgrade committed bootstrap evidence', () => {
|
|
const result = resolveTeamMemberRuntimeLiveness({
|
|
teamName: 'demo',
|
|
memberName: 'bob',
|
|
providerId: 'opencode',
|
|
persistedRuntimePid: 404,
|
|
persistedRuntimeSessionId: 'session-bob',
|
|
trackedSpawnStatus: {
|
|
status: 'online',
|
|
launchState: 'confirmed_alive',
|
|
agentToolAccepted: true,
|
|
runtimeAlive: true,
|
|
bootstrapConfirmed: true,
|
|
hardFailure: false,
|
|
updatedAt: NOW,
|
|
},
|
|
processRows: [{ pid: 404, ppid: 1, command: 'node unrelated-worker.js' }],
|
|
processTableAvailable: true,
|
|
nowIso: NOW,
|
|
});
|
|
|
|
expect(result.alive).toBe(true);
|
|
expect(result.livenessKind).toBe('confirmed_bootstrap');
|
|
expect(result.pidSource).toBe('runtime_bootstrap');
|
|
expect(result.pid).toBeUndefined();
|
|
expect(result.diagnostics).toContain(
|
|
'bootstrap confirmed despite runtime pid identity mismatch'
|
|
);
|
|
});
|
|
|
|
it('does not trust a stale persisted pid without current process identity', () => {
|
|
const result = resolveTeamMemberRuntimeLiveness({
|
|
teamName: 'demo',
|
|
memberName: 'tom',
|
|
persistedRuntimePid: 444,
|
|
processRows: [{ pid: 555, ppid: 1, command: 'node other.js' }],
|
|
processTableAvailable: true,
|
|
nowIso: NOW,
|
|
});
|
|
|
|
expect(result.alive).toBe(false);
|
|
expect(result.livenessKind).toBe('stale_metadata');
|
|
expect(result.pidSource).toBe('persisted_metadata');
|
|
});
|
|
|
|
it('does not treat a persisted pid as stale when the process table is unavailable', () => {
|
|
const result = resolveTeamMemberRuntimeLiveness({
|
|
teamName: 'demo',
|
|
memberName: 'tom',
|
|
persistedRuntimePid: 444,
|
|
processRows: [],
|
|
processTableAvailable: false,
|
|
nowIso: NOW,
|
|
});
|
|
|
|
expect(result.alive).toBe(false);
|
|
expect(result.livenessKind).toBe('registered_only');
|
|
expect(result.pidSource).toBe('persisted_metadata');
|
|
expect(result.diagnostics).toContain('process table unavailable');
|
|
});
|
|
|
|
it('redacts common secret flags in diagnostics commands', () => {
|
|
expect(
|
|
sanitizeProcessCommandForDiagnostics('node runtime --api-key sk-123 --token=abc --safe ok')
|
|
).toBe('node runtime --api-key [redacted] --token=[redacted] --safe ok');
|
|
});
|
|
|
|
it('keeps cached CLI arg extraction immutable for callers', () => {
|
|
const command =
|
|
'node runtime --team-name demo --agent-id "agent alice" --agent-id agent-bob';
|
|
const first = extractCliArgValues(command, '--agent-id');
|
|
first.push('mutated');
|
|
|
|
expect(extractCliArgValues(command, '--agent-id')).toEqual(['agent alice', 'agent-bob']);
|
|
expect(extractCliArgValues(command, '--team-name')).toEqual(['demo']);
|
|
});
|
|
|
|
it('returns no CLI arg values when the flag is absent', () => {
|
|
expect(extractCliArgValues('node runtime --other value', '--agent-id')).toEqual([]);
|
|
});
|
|
});
|