fix(ci): restore green workspace checks

This commit is contained in:
777genius 2026-04-12 00:02:59 +03:00
parent 4869bb35da
commit fb21b982c6
15 changed files with 182 additions and 80 deletions

View file

@ -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

View file

@ -21,7 +21,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v7
- uses: actions/setup-node@v6
with:
node-version: 22

View file

@ -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

View file

@ -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 {

View file

@ -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 {

View file

@ -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) {

View file

@ -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('буду ждать')
);
}

View file

@ -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;

View file

@ -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.

View file

@ -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);
});

View file

@ -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);

View file

@ -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 () => {

View file

@ -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 {

View file

@ -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'
);
});
});

View file

@ -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);
});
});