diff --git a/agent-teams-controller/test/controller.test.js b/agent-teams-controller/test/controller.test.js index 9e828cf8..a17fba61 100644 --- a/agent-teams-controller/test/controller.test.js +++ b/agent-teams-controller/test/controller.test.js @@ -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.'); }); diff --git a/agent-teams-controller/test/crossTeam.test.js b/agent-teams-controller/test/crossTeam.test.js index 74133b6c..b4b67f43 100644 --- a/agent-teams-controller/test/crossTeam.test.js +++ b/agent-teams-controller/test/crossTeam.test.js @@ -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', () => { diff --git a/src/main/utils/atomicWrite.ts b/src/main/utils/atomicWrite.ts index bf6fd237..23d908f0 100644 --- a/src/main/utils/atomicWrite.ts +++ b/src/main/utils/atomicWrite.ts @@ -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 { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function renameWithRetry(src: string, dest: string): Promise { + 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 { 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; diff --git a/src/renderer/components/team/kanban/KanbanTaskCard.tsx b/src/renderer/components/team/kanban/KanbanTaskCard.tsx index f7a52357..2d37fa2c 100644 --- a/src/renderer/components/team/kanban/KanbanTaskCard.tsx +++ b/src/renderer/components/team/kanban/KanbanTaskCard.tsx @@ -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 ? ( diff --git a/test/main/services/team/TeamMcpConfigBuilder.test.ts b/test/main/services/team/TeamMcpConfigBuilder.test.ts index 9dd3cda1..b18e40b9 100644 --- a/test/main/services/team/TeamMcpConfigBuilder.test.ts +++ b/test/main/services/team/TeamMcpConfigBuilder.test.ts @@ -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'), ], }); });