fix(tests): resolve pre-existing test failures on non-standard environments

- TeamProvisioningServiceRelay: add missing stat fields (mode, dev, ino,
  mtimeMs, ctimeMs, birthtimeMs) to fs mock so new fingerprint-based
  TeamConfigReader cache can read config in tests
- TeamMcpConfigBuilder: export clearResolvedNodePathForTests() to reset
  module-level node path cache between tests; restore execFileMock
  implementation in beforeEach after vi.restoreAllMocks() clears it;
  broaden node binary regex to accept versioned names (node-22, node-20)
  common on Fedora/RHEL systems
- ScheduledTaskExecutor: strip CLAUDECODE at spawn site as last defence
  so nested-session detection is prevented even when buildProviderAwareCliEnv
  merges it back in from the outer process environment
This commit is contained in:
Mike 2026-05-02 20:10:42 +05:00
parent 9ad32d9978
commit 7609c548c5
4 changed files with 88 additions and 72 deletions

View file

@ -178,7 +178,9 @@ export class ScheduledTaskExecutor {
cwd: request.config.cwd,
// shellEnv spread after buildEnrichedEnv ensures freshly-resolved values
// take precedence over the cached snapshot inside buildEnrichedEnv.
env,
// CLAUDECODE stripped last to prevent nested-session detection regardless
// of what buildProviderAwareCliEnv merges in.
env: { ...env, CLAUDECODE: undefined },
stdio: ['ignore', 'pipe', 'pipe'],
});

View file

@ -154,6 +154,10 @@ async function hasValidServerCopy(dir: string): Promise<boolean> {
let _resolvedNodePath: string | undefined;
export function clearResolvedNodePathForTests(): void {
_resolvedNodePath = undefined;
}
/**
* Find the real `node` binary path. In Electron, process.execPath is the
* Electron binary NOT node so we must resolve node separately.

View file

@ -46,7 +46,10 @@ vi.mock('@main/utils/pathDecoder', async (importOriginal) => {
});
import { setAppDataBasePath, setClaudeBasePathOverride } from '@main/utils/pathDecoder';
import { TeamMcpConfigBuilder } from '@main/services/team/TeamMcpConfigBuilder';
import {
TeamMcpConfigBuilder,
clearResolvedNodePathForTests,
} from '@main/services/team/TeamMcpConfigBuilder';
describe('TeamMcpConfigBuilder', () => {
const createdPaths: string[] = [];
@ -93,7 +96,7 @@ describe('TeamMcpConfigBuilder', () => {
entry: string
): void {
expect(server?.args).toEqual([entry]);
expect(server?.command).toMatch(/(^node$|[\\/]node(?:\.exe)?$)/);
expect(server?.command).toMatch(/(^node(?:-\d+)?$|[\\/]node(?:-\d+)?(?:\.exe)?$)/);
}
function expectNodeTsxSourceEntry(
@ -102,7 +105,7 @@ describe('TeamMcpConfigBuilder', () => {
sourceEntry: string
): void {
expect(server?.args).toEqual([tsxCli, sourceEntry]);
expect(server?.command).toMatch(/(^node$|[\\/]node(?:\.exe)?$)/);
expect(server?.command).toMatch(/(^node(?:-\d+)?$|[\\/]node(?:-\d+)?(?:\.exe)?$)/);
}
function getBuiltWorkspaceEntry(): string {
@ -165,6 +168,7 @@ describe('TeamMcpConfigBuilder', () => {
}
beforeEach(() => {
clearResolvedNodePathForTests();
originalResourcesPath = (process as NodeJS.Process & { resourcesPath?: string }).resourcesPath;
tempAppData = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-appdata-'));
createdDirs.push(tempAppData);

View file

@ -16,9 +16,16 @@ const hoisted = vi.hoisted(() => {
error.code = 'ENOENT';
throw error;
}
const size = Buffer.byteLength(data, 'utf8');
return {
isFile: () => true,
size: Buffer.byteLength(data, 'utf8'),
size,
mode: 0o644,
dev: 0,
ino: 0,
mtimeMs: 0,
ctimeMs: 0,
birthtimeMs: 0,
};
});
@ -54,22 +61,20 @@ const hoisted = vi.hoisted(() => {
files.set(sentMessagesPath, JSON.stringify(rows));
return message;
}),
sendInboxMessage: vi.fn(
(teamName: string, message: Record<string, unknown>) => {
const member =
typeof message.member === 'string'
? message.member
: typeof message.to === 'string'
? message.to
: 'unknown';
const p = `/mock/teams/${teamName}/inboxes/${member}.json`;
const current = files.get(p);
const rows = current ? (JSON.parse(current) as unknown[]) : [];
rows.push(message);
files.set(p, JSON.stringify(rows));
return { deliveredToInbox: true, messageId: 'mock-id', message };
}
),
sendInboxMessage: vi.fn((teamName: string, message: Record<string, unknown>) => {
const member =
typeof message.member === 'string'
? message.member
: typeof message.to === 'string'
? message.to
: 'unknown';
const p = `/mock/teams/${teamName}/inboxes/${member}.json`;
const current = files.get(p);
const rows = current ? (JSON.parse(current) as unknown[]) : [];
rows.push(message);
files.set(p, JSON.stringify(rows));
return { deliveredToInbox: true, messageId: 'mock-id', message };
}),
setAtomicWriteShouldFail: (next: boolean) => {
atomicWriteShouldFail = next;
},
@ -371,7 +376,9 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
const payload = String(writeSpy.mock.calls[0]?.[0] ?? '');
expect(payload).toContain('Source: system_notification');
expect(payload).toContain('summary looks like \\"Comment on #...\\"');
expect(payload).toContain('reply via task_add_comment only when you have a substantive board update');
expect(payload).toContain(
'reply via task_add_comment only when you have a substantive board update'
);
expect(payload).toContain('Do NOT post acknowledgement-only task comments');
(service as any).handleStreamJsonMessage(run, {
@ -492,9 +499,13 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
runId: 'run-old',
});
const inboxDeferred = createDeferred<typeof inboxMessages>();
const inboxReader = (service as unknown as {
inboxReader: { getMessagesFor: (team: string, member: string) => Promise<typeof inboxMessages> };
}).inboxReader;
const inboxReader = (
service as unknown as {
inboxReader: {
getMessagesFor: (team: string, member: string) => Promise<typeof inboxMessages>;
};
}
).inboxReader;
const inboxSpy = vi
.spyOn(inboxReader, 'getMessagesFor')
.mockImplementationOnce(async () => await inboxDeferred.promise)
@ -538,14 +549,13 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
const { runId: oldRunId } = attachAliveRun(service, teamName, { runId: 'run-old' });
const inboxDeferred = createDeferred<[typeof permissionMessage]>();
const inboxReader = (service as unknown as {
inboxReader: {
getMessagesFor: (
team: string,
member: string
) => Promise<[typeof permissionMessage]>;
};
}).inboxReader;
const inboxReader = (
service as unknown as {
inboxReader: {
getMessagesFor: (team: string, member: string) => Promise<[typeof permissionMessage]>;
};
}
).inboxReader;
const inboxSpy = vi
.spyOn(inboxReader, 'getMessagesFor')
.mockImplementationOnce(async () => await inboxDeferred.promise)
@ -654,7 +664,9 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
const payload = String(writeSpy.mock.calls[0]?.[0] ?? '');
expect(payload).toContain('Source: cross_team');
expect(payload).toContain('Cross-team conversationId: conv-explicit');
expect(payload).toContain('Call the MCP tool named cross_team_send with toTeam=\\"other-team\\"');
expect(payload).toContain(
'Call the MCP tool named cross_team_send with toTeam=\\"other-team\\"'
);
expect(payload).toContain('replyToConversationId=\\"conv-explicit\\"');
expect(payload).toContain('NEVER set recipient/to to \\"cross_team_send\\"');
@ -905,7 +917,11 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
attachAliveRun(service, teamName);
const run = (service as unknown as { runs: Map<string, unknown> }).runs.get('run-1') as {
silentUserDmForward: { target: string; startedAt: string; mode: 'user_dm' | 'member_inbox_relay' } | null;
silentUserDmForward: {
target: string;
startedAt: string;
mode: 'user_dm' | 'member_inbox_relay';
} | null;
};
run.silentUserDmForward = {
target: 'alice',
@ -1072,9 +1088,13 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
runId: 'run-old',
});
const inboxDeferred = createDeferred<typeof inboxMessages>();
const inboxReader = (service as unknown as {
inboxReader: { getMessagesFor: (team: string, member: string) => Promise<typeof inboxMessages> };
}).inboxReader;
const inboxReader = (
service as unknown as {
inboxReader: {
getMessagesFor: (team: string, member: string) => Promise<typeof inboxMessages>;
};
}
).inboxReader;
const inboxSpy = vi
.spyOn(inboxReader, 'getMessagesFor')
.mockImplementationOnce(async () => await inboxDeferred.promise)
@ -1284,11 +1304,7 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
await (service as any).markInboxMessagesRead(teamName, 'alice', [
{
messageId: buildLegacyInboxMessageId(
legacyRow.from,
legacyRow.timestamp,
legacyRow.text
),
messageId: buildLegacyInboxMessageId(legacyRow.from, legacyRow.timestamp, legacyRow.text),
},
]);
@ -1684,9 +1700,7 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
taskRefs: [{ teamName, taskId: 'task-1', displayId: 'abcd1234' }],
})
);
const rows = JSON.parse(
hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]'
);
const rows = JSON.parse(hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]');
expect(rows[0].read).toBe(true);
});
@ -1732,9 +1746,7 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
failed: 0,
lastDelivery: { delivered: true, responsePending: true },
});
const rows = JSON.parse(
hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]'
);
const rows = JSON.parse(hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]');
expect(rows[0].read).toBe(false);
});
@ -1866,9 +1878,7 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
teamName,
expect.objectContaining({ messageId: 'opencode-terminal-new' })
);
const rows = JSON.parse(
hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]'
);
const rows = JSON.parse(hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]');
expect(rows.map((row: { read?: boolean }) => row.read)).toEqual([false, true]);
});
@ -1952,9 +1962,7 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
'opencode_attachments_not_supported_for_secondary_runtime'
);
vi.mocked(console.warn).mockClear();
const rows = JSON.parse(
hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]'
);
const rows = JSON.parse(hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]');
expect(rows[0].read).toBe(false);
expect(records[0]).toMatchObject({
inboxMessageId: 'opencode-attachment-1',
@ -1979,7 +1987,10 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
],
})
);
const identity = await (service as any).resolveOpenCodeMemberDeliveryIdentity(teamName, 'jack');
const identity = await (service as any).resolveOpenCodeMemberDeliveryIdentity(
teamName,
'jack'
);
expect(identity.ok).toBe(true);
const laneId = identity.laneId;
const records: any[] = [];
@ -2000,7 +2011,12 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
return record;
}),
markAcceptanceUnknown: vi.fn(
async (input: { id: string; reason: string; nextAttemptAt: string; markedAt: string }) => {
async (input: {
id: string;
reason: string;
nextAttemptAt: string;
markedAt: string;
}) => {
const record = records.find((candidate) => candidate.id === input.id);
Object.assign(record, {
status: 'failed_retryable',
@ -2132,9 +2148,7 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
teamName,
expect.objectContaining({ messageId: 'opencode-inflight-new' })
);
const rows = JSON.parse(
hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]'
);
const rows = JSON.parse(hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]');
expect(rows.map((row: { read?: boolean }) => row.read)).toEqual([true, true]);
});
@ -2211,9 +2225,7 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
const relay = await service.relayInboxFileToLiveRecipient(teamName, 'jack');
expect(relay).toMatchObject({ kind: 'opencode_member', relayed: 1 });
const rows = JSON.parse(
hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]'
);
const rows = JSON.parse(hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]');
expect(rows[0].read).toBe(true);
});
@ -2303,9 +2315,7 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
'OpenCode inbox relay failed for jack/opencode-relay-failed-1'
);
vi.mocked(console.warn).mockClear();
const rows = JSON.parse(
hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]'
);
const rows = JSON.parse(hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]');
expect(rows[0].read).toBe(false);
});
@ -2337,9 +2347,7 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
delivered: true,
diagnostics: [],
});
vi.spyOn(service as any, 'markInboxMessagesRead').mockRejectedValue(
new Error('write failed')
);
vi.spyOn(service as any, 'markInboxMessagesRead').mockRejectedValue(new Error('write failed'));
const relay = await service.relayOpenCodeMemberInboxMessages(teamName, 'jack');
@ -2360,9 +2368,7 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
'opencode_inbox_mark_read_failed_after_delivery'
);
vi.mocked(console.warn).mockClear();
const rows = JSON.parse(
hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]'
);
const rows = JSON.parse(hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]');
expect(rows[0].read).toBe(false);
});
});