fix(ci): restore green workspace checks
This commit is contained in:
parent
4869bb35da
commit
fb21b982c6
15 changed files with 182 additions and 80 deletions
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
|
@ -53,7 +53,7 @@ jobs:
|
|||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
|
@ -88,7 +88,7 @@ jobs:
|
|||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
|
|
|||
2
.github/workflows/landing.yml
vendored
2
.github/workflows/landing.yml
vendored
|
|
@ -21,7 +21,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
|
|
|
|||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
|
|
@ -21,7 +21,7 @@ jobs:
|
|||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
|
@ -178,7 +178,7 @@ jobs:
|
|||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
|
@ -287,7 +287,7 @@ jobs:
|
|||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
|
@ -391,7 +391,7 @@ jobs:
|
|||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
|
|
|||
|
|
@ -1,14 +1,41 @@
|
|||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
function parseJsonToolResult(result: unknown) {
|
||||
const text = (result as { content?: Array<{ text?: string }> }).content?.[0]?.text;
|
||||
const response = result as {
|
||||
content?: Array<{ text?: string }>;
|
||||
isError?: boolean;
|
||||
};
|
||||
const text = response.content?.[0]?.text;
|
||||
if (response.isError) {
|
||||
throw new Error(text ?? 'Tool returned an unspecified error');
|
||||
}
|
||||
return JSON.parse(text ?? 'null');
|
||||
}
|
||||
|
||||
async function writeTeamConfig(claudeDir: string, teamName: string) {
|
||||
const teamDir = path.join(claudeDir, 'teams', teamName);
|
||||
await mkdir(teamDir, { recursive: true });
|
||||
await writeFile(
|
||||
path.join(teamDir, 'config.json'),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: teamName,
|
||||
members: [
|
||||
{ name: 'team-lead', agentType: 'team-lead' },
|
||||
{ name: 'alice', agentType: 'teammate', role: 'developer' },
|
||||
],
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
'utf8'
|
||||
);
|
||||
}
|
||||
|
||||
class McpStdIoClient {
|
||||
private readonly child: ChildProcessWithoutNullStreams;
|
||||
private stdoutBuffer = '';
|
||||
|
|
@ -102,6 +129,7 @@ describe('agent-teams-mcp stdio e2e', () => {
|
|||
});
|
||||
|
||||
it('boots over stdio, lists task tools, and executes task lifecycle calls', async () => {
|
||||
await writeTeamConfig(claudeDir, 'e2e-team');
|
||||
const client = new McpStdIoClient(serverPath, workspaceRoot);
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -174,7 +174,8 @@ export class ChangeExtractorService {
|
|||
teamName,
|
||||
taskId,
|
||||
effectiveOptions,
|
||||
effectiveStateBucket
|
||||
effectiveStateBucket,
|
||||
version
|
||||
);
|
||||
|
||||
if (options?.forceFresh !== true) {
|
||||
|
|
@ -407,9 +408,10 @@ export class ChangeExtractorService {
|
|||
teamName: string,
|
||||
taskId: string,
|
||||
options: TaskChangeEffectiveOptions,
|
||||
stateBucket: TaskChangeStateBucket
|
||||
stateBucket: TaskChangeStateBucket,
|
||||
version: number
|
||||
): string {
|
||||
return `${teamName}:${taskId}:${this.buildTaskSignature(options, stateBucket)}`;
|
||||
return `${teamName}:${taskId}:v${version}:${this.buildTaskSignature(options, stateBucket)}`;
|
||||
}
|
||||
|
||||
private normalizeFilePathKey(filePath: string): string {
|
||||
|
|
|
|||
|
|
@ -9,14 +9,14 @@ import {
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { getTeamFsWorkerClient } from './TeamFsWorkerClient';
|
||||
import { TeamMembersMetaStore } from './TeamMembersMetaStore';
|
||||
import { TeamMetaStore } from './TeamMetaStore';
|
||||
import {
|
||||
choosePreferredLaunchSnapshot,
|
||||
readBootstrapLaunchSnapshot,
|
||||
} from './TeamBootstrapStateReader';
|
||||
import { getTeamFsWorkerClient } from './TeamFsWorkerClient';
|
||||
import { normalizePersistedLaunchSnapshot } from './TeamLaunchStateEvaluator';
|
||||
import { TeamMembersMetaStore } from './TeamMembersMetaStore';
|
||||
import { TeamMetaStore } from './TeamMetaStore';
|
||||
|
||||
import type { TeamConfig, TeamMember, TeamSummary, TeamSummaryMember } from '@shared/types';
|
||||
|
||||
|
|
@ -48,18 +48,19 @@ interface LaunchStateSummary {
|
|||
async function readLaunchStateSummary(teamDir: string): Promise<LaunchStateSummary | null> {
|
||||
const bootstrapSnapshot = await readBootstrapLaunchSnapshot(path.basename(teamDir));
|
||||
const launchStatePath = path.join(teamDir, TEAM_LAUNCH_STATE_FILE);
|
||||
let launchSnapshot = null;
|
||||
try {
|
||||
const stat = await fs.promises.stat(launchStatePath);
|
||||
if (!stat.isFile() || stat.size > MAX_LAUNCH_STATE_BYTES) {
|
||||
launchSnapshot = null;
|
||||
} else {
|
||||
const launchSnapshot = await (async () => {
|
||||
try {
|
||||
const stat = await fs.promises.stat(launchStatePath);
|
||||
if (!stat.isFile() || stat.size > MAX_LAUNCH_STATE_BYTES) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const raw = await readFileUtf8WithTimeout(launchStatePath, PER_TEAM_READ_TIMEOUT_MS);
|
||||
launchSnapshot = normalizePersistedLaunchSnapshot(path.basename(teamDir), JSON.parse(raw));
|
||||
return normalizePersistedLaunchSnapshot(path.basename(teamDir), JSON.parse(raw));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
launchSnapshot = null;
|
||||
}
|
||||
})();
|
||||
|
||||
const snapshot = choosePreferredLaunchSnapshot(bootstrapSnapshot, launchSnapshot);
|
||||
if (!snapshot) {
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@ import {
|
|||
wrapAgentBlock,
|
||||
} from '@shared/constants/agentBlocks';
|
||||
import { getMemberColorByName } from '@shared/constants/memberColors';
|
||||
import { classifyIdleNotificationText } from '@shared/utils/idleNotificationSemantics';
|
||||
import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection';
|
||||
import { createLogger } from '@shared/utils/logger';
|
||||
import { classifyIdleNotificationText } from '@shared/utils/idleNotificationSemantics';
|
||||
import { getKanbanColumnFromReviewState, normalizeReviewState } from '@shared/utils/reviewState';
|
||||
import { buildStandaloneSlashCommandMeta } from '@shared/utils/slashCommands';
|
||||
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
||||
|
|
@ -31,13 +31,13 @@ import * as path from 'path';
|
|||
|
||||
import { gitIdentityResolver } from '../parsing/GitIdentityResolver';
|
||||
|
||||
import { atomicWriteAsync } from './atomicWrite';
|
||||
import {
|
||||
areLeadSessionFileSignaturesEqual,
|
||||
LeadSessionParseCache,
|
||||
type LeadSessionFileSignature,
|
||||
LeadSessionParseCache,
|
||||
type LeadSessionParseCacheKey,
|
||||
} from './cache/LeadSessionParseCache';
|
||||
import { atomicWriteAsync } from './atomicWrite';
|
||||
import { extractLeadSessionMessagesFromJsonl } from './leadSessionMessageExtractor';
|
||||
import { buildTaskChangePresenceDescriptor } from './taskChangePresenceUtils';
|
||||
import { TeamConfigReader } from './TeamConfigReader';
|
||||
|
|
@ -62,9 +62,9 @@ import type {
|
|||
CreateTaskRequest,
|
||||
GlobalTask,
|
||||
InboxMessage,
|
||||
MessagesPage,
|
||||
KanbanColumnId,
|
||||
KanbanState,
|
||||
MessagesPage,
|
||||
ResolvedTeamMember,
|
||||
SendMessageRequest,
|
||||
SendMessageResult,
|
||||
|
|
@ -134,11 +134,11 @@ function normalizePassiveUserReplyLinkText(value: string | undefined): string {
|
|||
|
||||
function extractPassiveUserPeerSummaryBody(text: string): string | null {
|
||||
const classified = classifyIdleNotificationText(text);
|
||||
if (!classified || classified.primaryKind !== 'heartbeat' || !classified.peerSummary) {
|
||||
if (classified?.primaryKind !== 'heartbeat' || !classified.peerSummary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const match = classified.peerSummary.match(/^\[to\s+user\]\s*(.*)$/i);
|
||||
const match = /^\[to\s+user\]\s*(.*)$/i.exec(classified.peerSummary);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -677,7 +677,7 @@ export class TeamDataService {
|
|||
const runWithConcurrencyLimit = (() => {
|
||||
const limit = 2;
|
||||
let active = 0;
|
||||
const queue: Array<() => void> = [];
|
||||
const queue: (() => void)[] = [];
|
||||
const releaseNext = (): void => {
|
||||
if (active >= limit) return;
|
||||
const next = queue.shift();
|
||||
|
|
@ -811,8 +811,8 @@ export class TeamDataService {
|
|||
if (metaMembersStepResult.warning) warnings.push(metaMembersStepResult.warning);
|
||||
if (kanbanStateStepResult.warning) warnings.push(kanbanStateStepResult.warning);
|
||||
|
||||
let tasks: TeamTask[] = tasksStepResult.value;
|
||||
let inboxNames: string[] = inboxNamesStepResult.value;
|
||||
const tasks: TeamTask[] = tasksStepResult.value;
|
||||
const inboxNames: string[] = inboxNamesStepResult.value;
|
||||
let messages: InboxMessage[] = messagesStepResult.value;
|
||||
const leadTexts: InboxMessage[] = leadTextsStepResult.value;
|
||||
const sentMessages: InboxMessage[] = sentMessagesStepResult.value;
|
||||
|
|
@ -2064,8 +2064,37 @@ export class TeamDataService {
|
|||
return true;
|
||||
}
|
||||
|
||||
return /^(принято|принял|приняла|ок|ok|okay|на связи|понял|поняла|roger|ack)(?:[ ,.-]+(на связи|остаюсь на связи|жду(?: [^.!?]+)?|ждём(?: [^.!?]+)?|готов(?:а)?(?: [^.!?]+)?|буду ждать(?: [^.!?]+)?))?$/.test(
|
||||
normalized
|
||||
const startsWithAckPrefix = Array.from(exactMatches).find((prefix) => {
|
||||
if (!normalized.startsWith(prefix)) {
|
||||
return false;
|
||||
}
|
||||
const remainder = normalized.slice(prefix.length);
|
||||
return remainder.length > 0 && /^[ ,.-]+/.test(remainder);
|
||||
});
|
||||
if (!startsWithAckPrefix) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const qualifier = normalized
|
||||
.slice(startsWithAckPrefix.length)
|
||||
.replace(/^[ ,.-]+/, '')
|
||||
.trim();
|
||||
if (!qualifier) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const matchesQualifierWithOptionalDetail = (phrase: string): boolean =>
|
||||
qualifier === phrase ||
|
||||
(qualifier.startsWith(`${phrase} `) && !/[.!?]/.test(qualifier.slice(phrase.length + 1)));
|
||||
|
||||
return (
|
||||
qualifier === 'на связи' ||
|
||||
qualifier === 'остаюсь на связи' ||
|
||||
matchesQualifierWithOptionalDetail('жду') ||
|
||||
matchesQualifierWithOptionalDetail('ждём') ||
|
||||
matchesQualifierWithOptionalDetail('готов') ||
|
||||
matchesQualifierWithOptionalDetail('готова') ||
|
||||
matchesQualifierWithOptionalDetail('буду ждать')
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1450,12 +1450,16 @@ async function writeDeterministicBootstrapSpecFile(spec: RuntimeBootstrapSpec):
|
|||
return filePath;
|
||||
}
|
||||
|
||||
async function removeDeterministicBootstrapSpecFile(filePath: string | null): Promise<void> {
|
||||
async function removeDeterministicBootstrapTempFile(filePath: string | null): Promise<void> {
|
||||
if (!filePath) return;
|
||||
await fs.promises.rm(filePath, { force: true }).catch(() => {});
|
||||
await fs.promises.rmdir(path.dirname(filePath)).catch(() => {});
|
||||
}
|
||||
|
||||
async function removeDeterministicBootstrapSpecFile(filePath: string | null): Promise<void> {
|
||||
await removeDeterministicBootstrapTempFile(filePath);
|
||||
}
|
||||
|
||||
async function writeDeterministicBootstrapUserPromptFile(prompt: string): Promise<string> {
|
||||
const tempDir = await fs.promises.mkdtemp(
|
||||
path.join(os.tmpdir(), 'agent-teams-bootstrap-prompt-')
|
||||
|
|
@ -1469,9 +1473,7 @@ async function writeDeterministicBootstrapUserPromptFile(prompt: string): Promis
|
|||
}
|
||||
|
||||
async function removeDeterministicBootstrapUserPromptFile(filePath: string | null): Promise<void> {
|
||||
if (!filePath) return;
|
||||
await fs.promises.rm(filePath, { force: true }).catch(() => {});
|
||||
await fs.promises.rmdir(path.dirname(filePath)).catch(() => {});
|
||||
await removeDeterministicBootstrapTempFile(filePath);
|
||||
}
|
||||
|
||||
function buildTeamCtlOpsInstructions(teamName: string, leadName: string): string {
|
||||
|
|
@ -1577,7 +1579,7 @@ function buildTeamCtlOpsInstructions(teamName: string, leadName: string): string
|
|||
function buildLeadRosterContextBlock(
|
||||
teamName: string,
|
||||
leadName: string,
|
||||
teammates: Array<{ name: string; role?: string }>
|
||||
teammates: { name: string; role?: string }[]
|
||||
): string | null {
|
||||
if (teammates.length === 0) return null;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {
|
|||
areStringMapsEqual,
|
||||
} from '@renderer/utils/messageRenderEquality';
|
||||
import { toMessageKey } from '@renderer/utils/teamMessageKey';
|
||||
import { createLogger } from '@shared/utils/logger';
|
||||
import { Layers } from 'lucide-react';
|
||||
|
||||
import { ActivityItem, isNoiseMessage } from './ActivityItem';
|
||||
|
|
@ -81,9 +80,6 @@ const COMPACT_MESSAGES_WIDTH_PX = 400;
|
|||
const EMPTY_TEAM_NAMES: string[] = [];
|
||||
const EMPTY_TEAM_COLOR_MAP = new Map<string, string>();
|
||||
const DEFAULT_COLLAPSE_MODE = 'default' as const;
|
||||
const logger = createLogger('Component:ActivityTimeline');
|
||||
const ACTIVITY_TIMELINE_SLICE_WARN_MS = 6;
|
||||
const ACTIVITY_TIMELINE_GROUP_WARN_MS = 8;
|
||||
|
||||
interface ItemCollapseProps {
|
||||
collapseMode: 'default' | 'managed';
|
||||
|
|
@ -339,7 +335,6 @@ export const ActivityTimeline = React.memo(function ActivityTimeline({
|
|||
// Pagination counts only significant (non-thought) messages so that lead thoughts
|
||||
// don't consume the page limit — they collapse into a single visual group anyway.
|
||||
const { visibleMessages, hiddenCount } = useMemo(() => {
|
||||
const startedAt = performance.now();
|
||||
const total = messages.length;
|
||||
if (total === 0) return { visibleMessages: messages, hiddenCount: 0 };
|
||||
|
||||
|
|
@ -359,31 +354,14 @@ export const ActivityTimeline = React.memo(function ActivityTimeline({
|
|||
significantSeen +
|
||||
(cutoff < total ? messages.slice(cutoff).filter((m) => !isLeadThought(m)).length : 0);
|
||||
const hidden = Math.max(0, significantTotal - visibleCount);
|
||||
const result = {
|
||||
return {
|
||||
visibleMessages: cutoff < total ? messages.slice(0, cutoff) : messages,
|
||||
hiddenCount: hidden,
|
||||
};
|
||||
const ms = performance.now() - startedAt;
|
||||
if (ms >= ACTIVITY_TIMELINE_SLICE_WARN_MS) {
|
||||
logger.warn(
|
||||
`[perf] paginate team=${teamName} ms=${ms.toFixed(1)} total=${messages.length} visible=${result.visibleMessages.length} hidden=${result.hiddenCount} pageSize=${visibleCount}`
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}, [messages, visibleCount]);
|
||||
|
||||
// Group consecutive lead thoughts into collapsible blocks.
|
||||
const timelineItems = useMemo(() => {
|
||||
const startedAt = performance.now();
|
||||
const result = groupTimelineItems(visibleMessages);
|
||||
const ms = performance.now() - startedAt;
|
||||
if (ms >= ACTIVITY_TIMELINE_GROUP_WARN_MS) {
|
||||
logger.warn(
|
||||
`[perf] groupTimeline team=${teamName} ms=${ms.toFixed(1)} visibleMessages=${visibleMessages.length} groups=${result.length}`
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}, [teamName, visibleMessages]);
|
||||
const timelineItems = useMemo(() => groupTimelineItems(visibleMessages), [visibleMessages]);
|
||||
|
||||
// Zebra striping is anchored from the bottom of the visible list so prepending
|
||||
// new live messages at the top does not recolor every existing card.
|
||||
|
|
|
|||
|
|
@ -21,6 +21,13 @@ const { mockAddTeamNotification } = vi.hoisted(() => ({
|
|||
const { mockGetMembersMeta } = vi.hoisted(() => ({
|
||||
mockGetMembersMeta: vi.fn(),
|
||||
}));
|
||||
const { mockTeamDataWorkerClient } = vi.hoisted(() => ({
|
||||
mockTeamDataWorkerClient: {
|
||||
isAvailable: vi.fn(),
|
||||
getTeamData: vi.fn(),
|
||||
findLogsForTask: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock('@main/services/infrastructure/NotificationManager', () => ({
|
||||
NotificationManager: {
|
||||
getInstance: vi.fn().mockReturnValue({
|
||||
|
|
@ -33,6 +40,9 @@ vi.mock('@main/services/team/TeamMembersMetaStore', () => ({
|
|||
getMembers: mockGetMembersMeta,
|
||||
})),
|
||||
}));
|
||||
vi.mock('@main/services/team/TeamDataWorkerClient', () => ({
|
||||
getTeamDataWorkerClient: () => mockTeamDataWorkerClient,
|
||||
}));
|
||||
|
||||
import {
|
||||
TEAM_ALIVE_LIST,
|
||||
|
|
@ -182,6 +192,9 @@ describe('ipc teams handlers', () => {
|
|||
vi.clearAllMocks();
|
||||
mockGetMembersMeta.mockReset();
|
||||
mockGetMembersMeta.mockResolvedValue([]);
|
||||
mockTeamDataWorkerClient.isAvailable.mockReturnValue(false);
|
||||
mockTeamDataWorkerClient.getTeamData.mockReset();
|
||||
mockTeamDataWorkerClient.findLogsForTask.mockReset();
|
||||
initializeTeamHandlers(service as never, provisioningService as never);
|
||||
registerTeamHandlers(ipcMain as never);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -247,17 +247,23 @@ describe('FileWatcher', () => {
|
|||
|
||||
// Simulate having previously processed the file by directly setting tracking state
|
||||
const watcherAny = watcher as unknown as {
|
||||
isWatching: boolean;
|
||||
lastProcessedLineCount: Map<string, number>;
|
||||
lastProcessedSize: Map<string, number>;
|
||||
activeSessionFiles: Map<string, { projectId: string; sessionId: string }>;
|
||||
activeSessionFiles: Map<
|
||||
string,
|
||||
{ projectId: string; sessionId: string; lastObservedAt: number }
|
||||
>;
|
||||
runCatchUpScan: () => Promise<void>;
|
||||
};
|
||||
const initialSize = fs.statSync(filePath).size;
|
||||
watcherAny.isWatching = true;
|
||||
watcherAny.lastProcessedLineCount.set(filePath, 1);
|
||||
watcherAny.lastProcessedSize.set(filePath, initialSize);
|
||||
watcherAny.activeSessionFiles.set(filePath, {
|
||||
projectId: 'test-project',
|
||||
sessionId: 'session-1',
|
||||
lastObservedAt: Date.now(),
|
||||
});
|
||||
|
||||
// Append new data WITHOUT triggering fs.watch (simulating a missed event)
|
||||
|
|
@ -301,17 +307,23 @@ describe('FileWatcher', () => {
|
|||
watcher.setNotificationManager(notificationManager);
|
||||
|
||||
const watcherAny = watcher as unknown as {
|
||||
isWatching: boolean;
|
||||
lastProcessedLineCount: Map<string, number>;
|
||||
lastProcessedSize: Map<string, number>;
|
||||
activeSessionFiles: Map<string, { projectId: string; sessionId: string }>;
|
||||
activeSessionFiles: Map<
|
||||
string,
|
||||
{ projectId: string; sessionId: string; lastObservedAt: number }
|
||||
>;
|
||||
runCatchUpScan: () => Promise<void>;
|
||||
};
|
||||
const currentSize = fs.statSync(filePath).size;
|
||||
watcherAny.isWatching = true;
|
||||
watcherAny.lastProcessedLineCount.set(filePath, 1);
|
||||
watcherAny.lastProcessedSize.set(filePath, currentSize);
|
||||
watcherAny.activeSessionFiles.set(filePath, {
|
||||
projectId: 'test-project',
|
||||
sessionId: 'session-1',
|
||||
lastObservedAt: Date.now(),
|
||||
});
|
||||
|
||||
vi.mocked(errorDetector.detectErrors).mockClear();
|
||||
|
|
@ -348,13 +360,19 @@ describe('FileWatcher', () => {
|
|||
watcher.setNotificationManager(notificationManager);
|
||||
|
||||
const watcherAny = watcher as unknown as {
|
||||
activeSessionFiles: Map<string, { projectId: string; sessionId: string }>;
|
||||
isWatching: boolean;
|
||||
activeSessionFiles: Map<
|
||||
string,
|
||||
{ projectId: string; sessionId: string; lastObservedAt: number }
|
||||
>;
|
||||
lastProcessedSize: Map<string, number>;
|
||||
runCatchUpScan: () => Promise<void>;
|
||||
};
|
||||
watcherAny.isWatching = true;
|
||||
watcherAny.activeSessionFiles.set(filePath, {
|
||||
projectId: 'test-project',
|
||||
sessionId: 'old-session',
|
||||
lastObservedAt: Date.now(),
|
||||
});
|
||||
watcherAny.lastProcessedSize.set(filePath, 0);
|
||||
|
||||
|
|
@ -378,14 +396,20 @@ describe('FileWatcher', () => {
|
|||
const filePath = '/tmp/projects/test-project/nonexistent.jsonl';
|
||||
|
||||
const watcherAny = watcher as unknown as {
|
||||
activeSessionFiles: Map<string, { projectId: string; sessionId: string }>;
|
||||
isWatching: boolean;
|
||||
activeSessionFiles: Map<
|
||||
string,
|
||||
{ projectId: string; sessionId: string; lastObservedAt: number }
|
||||
>;
|
||||
lastProcessedSize: Map<string, number>;
|
||||
lastProcessedLineCount: Map<string, number>;
|
||||
runCatchUpScan: () => Promise<void>;
|
||||
};
|
||||
watcherAny.isWatching = true;
|
||||
watcherAny.activeSessionFiles.set(filePath, {
|
||||
projectId: 'test-project',
|
||||
sessionId: 'nonexistent',
|
||||
lastObservedAt: Date.now(),
|
||||
});
|
||||
watcherAny.lastProcessedSize.set(filePath, 100);
|
||||
watcherAny.lastProcessedLineCount.set(filePath, 5);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,13 @@ import { EventEmitter } from 'events';
|
|||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
paths: {
|
||||
claudeRoot: '',
|
||||
teamsBase: '',
|
||||
},
|
||||
}));
|
||||
|
||||
let tempClaudeRoot = '';
|
||||
let tempTeamsBase = '';
|
||||
|
||||
|
|
@ -12,9 +19,9 @@ vi.mock('@main/utils/pathDecoder', async (importOriginal) => {
|
|||
const actual = await importOriginal<typeof import('@main/utils/pathDecoder')>();
|
||||
return {
|
||||
...actual,
|
||||
getAutoDetectedClaudeBasePath: () => tempClaudeRoot,
|
||||
getClaudeBasePath: () => tempClaudeRoot,
|
||||
getTeamsBasePath: () => tempTeamsBase,
|
||||
getAutoDetectedClaudeBasePath: () => hoisted.paths.claudeRoot,
|
||||
getClaudeBasePath: () => hoisted.paths.claudeRoot,
|
||||
getTeamsBasePath: () => hoisted.paths.teamsBase,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -25,11 +32,15 @@ describe('TeamProvisioningService idempotent launch guards', () => {
|
|||
vi.clearAllMocks();
|
||||
tempClaudeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-team-launch-'));
|
||||
tempTeamsBase = path.join(tempClaudeRoot, 'teams');
|
||||
hoisted.paths.claudeRoot = tempClaudeRoot;
|
||||
hoisted.paths.teamsBase = tempTeamsBase;
|
||||
fs.mkdirSync(tempTeamsBase, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempClaudeRoot, { recursive: true, force: true });
|
||||
hoisted.paths.claudeRoot = '';
|
||||
hoisted.paths.teamsBase = '';
|
||||
});
|
||||
|
||||
it('reuses the alive run instead of spawning a duplicate launch', async () => {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,14 @@ import * as path from 'path';
|
|||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
paths: {
|
||||
claudeRoot: '',
|
||||
teamsBase: '',
|
||||
tasksBase: '',
|
||||
},
|
||||
}));
|
||||
|
||||
let tempClaudeRoot = '';
|
||||
let tempTeamsBase = '';
|
||||
let tempTasksBase = '';
|
||||
|
|
@ -22,10 +30,10 @@ vi.mock('@main/utils/pathDecoder', async (importOriginal) => {
|
|||
const actual = await importOriginal<typeof import('@main/utils/pathDecoder')>();
|
||||
return {
|
||||
...actual,
|
||||
getAutoDetectedClaudeBasePath: () => tempClaudeRoot,
|
||||
getClaudeBasePath: () => tempClaudeRoot,
|
||||
getTeamsBasePath: () => tempTeamsBase,
|
||||
getTasksBasePath: () => tempTasksBase,
|
||||
getAutoDetectedClaudeBasePath: () => hoisted.paths.claudeRoot,
|
||||
getClaudeBasePath: () => hoisted.paths.claudeRoot,
|
||||
getTeamsBasePath: () => hoisted.paths.teamsBase,
|
||||
getTasksBasePath: () => hoisted.paths.tasksBase,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -112,6 +120,9 @@ describe('TeamProvisioningService post-compact lifecycle', () => {
|
|||
tempClaudeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-team-compact-'));
|
||||
tempTeamsBase = path.join(tempClaudeRoot, 'teams');
|
||||
tempTasksBase = path.join(tempClaudeRoot, 'tasks');
|
||||
hoisted.paths.claudeRoot = tempClaudeRoot;
|
||||
hoisted.paths.teamsBase = tempTeamsBase;
|
||||
hoisted.paths.tasksBase = tempTasksBase;
|
||||
setAppDataBasePath(tempClaudeRoot);
|
||||
fs.mkdirSync(tempTeamsBase, { recursive: true });
|
||||
fs.mkdirSync(tempTasksBase, { recursive: true });
|
||||
|
|
@ -119,6 +130,9 @@ describe('TeamProvisioningService post-compact lifecycle', () => {
|
|||
|
||||
afterEach(() => {
|
||||
setAppDataBasePath(null);
|
||||
hoisted.paths.claudeRoot = '';
|
||||
hoisted.paths.teamsBase = '';
|
||||
hoisted.paths.tasksBase = '';
|
||||
try {
|
||||
fs.rmSync(tempClaudeRoot, { recursive: true, force: true });
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -549,7 +549,7 @@ describe('TeamGraphAdapter particles', () => {
|
|||
'GPT-5.4 Mini · Medium'
|
||||
);
|
||||
expect(graph.nodes.find((node) => node.id === 'member:my-team:alice')?.runtimeLabel).toBe(
|
||||
'Anthropic · sonnet · High'
|
||||
'Anthropic · Sonnet 4.6 · High'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -144,9 +144,9 @@ describe('PROTECTED_CLI_FLAGS', () => {
|
|||
expect(PROTECTED_CLI_FLAGS.has('--verbose')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not contain user-facing flags', () => {
|
||||
expect(PROTECTED_CLI_FLAGS.has('--model')).toBe(false);
|
||||
expect(PROTECTED_CLI_FLAGS.has('--effort')).toBe(false);
|
||||
it('contains app-managed launch flags but not unrelated user flags', () => {
|
||||
expect(PROTECTED_CLI_FLAGS.has('--model')).toBe(true);
|
||||
expect(PROTECTED_CLI_FLAGS.has('--effort')).toBe(true);
|
||||
expect(PROTECTED_CLI_FLAGS.has('--worktree')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue