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:
parent
9ad32d9978
commit
7609c548c5
4 changed files with 88 additions and 72 deletions
|
|
@ -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'],
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue