fix: resolve CI failures across all platforms
- Fix TS error: use kanbanTaskState?.reviewer instead of task.reviewer (reviewer exists on KanbanTaskState, not TeamTask) - Fix crossTeam tests: update assertions to match XML tag protocol format (CROSS_TEAM_PREFIX_TAG → CROSS_TEAM_TAG_NAME with XML attributes) - Fix controller test: use 'bob' instead of 'alice' as commenter (alice is the lead = same as task owner, so no notification fires) - Fix TeamMcpConfigBuilder test: use path.join() for cross-platform paths (forward slashes fail on Windows where path.join uses backslashes) - Fix atomicWrite EPERM on Windows: add retry logic for rename operations (Windows CI antivirus can briefly hold files, causing EPERM on rename)
This commit is contained in:
parent
7c6837eacd
commit
729a756f29
5 changed files with 43 additions and 24 deletions
|
|
@ -661,14 +661,14 @@ describe('agent-teams-controller API', () => {
|
|||
});
|
||||
|
||||
controller.tasks.addTaskComment(task.id, {
|
||||
from: 'alice',
|
||||
from: 'bob',
|
||||
text: 'Need your decision here.',
|
||||
});
|
||||
|
||||
const inboxPath = path.join(claudeDir, 'teams', 'my-team', 'inboxes', 'team-lead.json');
|
||||
const rows = JSON.parse(fs.readFileSync(inboxPath, 'utf8'));
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].from).toBe('alice');
|
||||
expect(rows[0].from).toBe('bob');
|
||||
expect(rows[0].text).toContain('Need your decision here.');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ const os = require('os');
|
|||
const path = require('path');
|
||||
|
||||
const { createController } = require('../src/index.js');
|
||||
const { CROSS_TEAM_SOURCE, CROSS_TEAM_PREFIX_TAG } = require('../src/internal/crossTeamProtocol.js');
|
||||
const { CROSS_TEAM_SOURCE, CROSS_TEAM_TAG_NAME } = require('../src/internal/crossTeamProtocol.js');
|
||||
|
||||
describe('crossTeam module', () => {
|
||||
function makeClaudeDir(teams = {}) {
|
||||
|
|
@ -60,9 +60,9 @@ describe('crossTeam module', () => {
|
|||
expect(inbox).toHaveLength(1);
|
||||
expect(inbox[0].source).toBe(CROSS_TEAM_SOURCE);
|
||||
expect(inbox[0].from).toBe('team-a.lead');
|
||||
expect(inbox[0].text).toContain(`[${CROSS_TEAM_PREFIX_TAG} team-a.lead | depth:0`);
|
||||
expect(inbox[0].text).toContain(`<${CROSS_TEAM_TAG_NAME} from="team-a.lead" depth="0"`);
|
||||
expect(inbox[0].conversationId).toBeTruthy();
|
||||
expect(inbox[0].text).toContain(`conversation:${inbox[0].conversationId}`);
|
||||
expect(inbox[0].text).toContain(`conversationId="${inbox[0].conversationId}"`);
|
||||
});
|
||||
|
||||
it('records outbox entry', () => {
|
||||
|
|
@ -121,8 +121,8 @@ describe('crossTeam module', () => {
|
|||
const inbox = JSON.parse(fs.readFileSync(inboxPath, 'utf8'));
|
||||
expect(inbox[0].conversationId).toBe('conv-123');
|
||||
expect(inbox[0].replyToConversationId).toBe('conv-123');
|
||||
expect(inbox[0].text).toContain('conversation:conv-123');
|
||||
expect(inbox[0].text).toContain('replyTo:conv-123');
|
||||
expect(inbox[0].text).toContain('conversationId="conv-123"');
|
||||
expect(inbox[0].text).toContain('replyToConversationId="conv-123"');
|
||||
});
|
||||
|
||||
it('deduplicates the same recent cross-team request', () => {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,37 @@ import { randomUUID } from 'crypto';
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const EPERM_MAX_RETRIES = 3;
|
||||
const EPERM_RETRY_DELAY_MS = 50;
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function renameWithRetry(src: string, dest: string): Promise<void> {
|
||||
for (let attempt = 0; attempt <= EPERM_MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
await fs.promises.rename(src, dest);
|
||||
return;
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException).code;
|
||||
if (code === 'EXDEV') {
|
||||
await fs.promises.copyFile(src, dest);
|
||||
await fs.promises.unlink(src).catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
if (code === 'EPERM' && attempt < EPERM_MAX_RETRIES) {
|
||||
await sleep(EPERM_RETRY_DELAY_MS * (attempt + 1));
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Async atomic write: write tmp file then rename over target.
|
||||
* Uses best-effort fsync and EXDEV fallback for safety.
|
||||
* Uses best-effort fsync and EXDEV/EPERM fallback for safety.
|
||||
*/
|
||||
export async function atomicWriteAsync(targetPath: string, data: string): Promise<void> {
|
||||
const dir = path.dirname(targetPath);
|
||||
|
|
@ -24,16 +52,7 @@ export async function atomicWriteAsync(targetPath: string, data: string): Promis
|
|||
await fd?.close();
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.promises.rename(tmpPath, targetPath);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'EXDEV') {
|
||||
await fs.promises.copyFile(tmpPath, targetPath);
|
||||
await fs.promises.unlink(tmpPath).catch(() => undefined);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await renameWithRetry(tmpPath, targetPath);
|
||||
} catch (error) {
|
||||
await fs.promises.unlink(tmpPath).catch(() => undefined);
|
||||
throw error;
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ export const KanbanTaskCard = ({
|
|||
task,
|
||||
teamName,
|
||||
columnId,
|
||||
kanbanTaskState: _kanbanTaskState,
|
||||
kanbanTaskState,
|
||||
hasReviewers,
|
||||
compact,
|
||||
taskMap,
|
||||
|
|
@ -266,7 +266,7 @@ export const KanbanTaskCard = ({
|
|||
taskChangeRequestOptions,
|
||||
]);
|
||||
|
||||
const isReviewManual = columnId === 'review' && !hasReviewers && !task.reviewer;
|
||||
const isReviewManual = columnId === 'review' && !hasReviewers && !kanbanTaskState?.reviewer;
|
||||
const metaActions = (
|
||||
<>
|
||||
{canDisplay && taskHasChanges === true ? (
|
||||
|
|
|
|||
|
|
@ -52,10 +52,10 @@ describe('TeamMcpConfigBuilder', () => {
|
|||
expect(server?.command).toBe('pnpm');
|
||||
expect(server?.args).toEqual([
|
||||
'--dir',
|
||||
`${process.cwd()}/mcp-server`,
|
||||
path.join(process.cwd(), 'mcp-server'),
|
||||
'exec',
|
||||
'tsx',
|
||||
`${process.cwd()}/mcp-server/src/index.ts`,
|
||||
path.join(process.cwd(), 'mcp-server', 'src', 'index.ts'),
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -178,10 +178,10 @@ describe('TeamMcpConfigBuilder', () => {
|
|||
command: 'pnpm',
|
||||
args: [
|
||||
'--dir',
|
||||
`${process.cwd()}/mcp-server`,
|
||||
path.join(process.cwd(), 'mcp-server'),
|
||||
'exec',
|
||||
'tsx',
|
||||
`${process.cwd()}/mcp-server/src/index.ts`,
|
||||
path.join(process.cwd(), 'mcp-server', 'src', 'index.ts'),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue