fix(ci): stabilize dev branch checks
This commit is contained in:
parent
e1ee94634f
commit
d0cfabca48
40 changed files with 153 additions and 110 deletions
|
|
@ -9,5 +9,8 @@ export type {
|
|||
AttachmentWarningCode,
|
||||
ImageOptimizationBudget,
|
||||
} from '../core/domain';
|
||||
|
||||
export { AGENT_ATTACHMENT_SCHEMA_VERSION } from '../core/domain';
|
||||
export {
|
||||
estimateAgentAttachmentSerializedPayloadBytes,
|
||||
MAX_AGENT_ATTACHMENT_SERIALIZED_PAYLOAD_BYTES,
|
||||
} from '../core/domain';
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@ const utf8Encoder = new TextEncoder();
|
|||
|
||||
export function estimateAgentAttachmentSerializedPayloadBytes(input: {
|
||||
text?: string;
|
||||
attachments: Array<{
|
||||
attachments: {
|
||||
mimeType: string;
|
||||
data: string;
|
||||
filename?: string;
|
||||
}>;
|
||||
}[];
|
||||
}): number {
|
||||
const contentBlocks: unknown[] = [{ type: 'text', text: input.text ?? '' }];
|
||||
for (const attachment of input.attachments) {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ describe('agent attachment artifact store helpers', () => {
|
|||
it('rejects unsafe ids before path construction', () => {
|
||||
expect(() =>
|
||||
resolveAgentAttachmentArtifactPath({
|
||||
// eslint-disable-next-line sonarjs/publicly-writable-directories -- Unit test uses a fixed synthetic root and never writes to it.
|
||||
appDataPath: '/tmp/root',
|
||||
teamName: 'team_1',
|
||||
messageId: '../msg',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { getAppDataPath } from '@main/utils/pathDecoder';
|
||||
import { assertSafeAttachmentStorageId } from '@features/agent-attachments/core/domain';
|
||||
import { getAppDataPath } from '@main/utils/pathDecoder';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ describe('Codex native attachment adapter', () => {
|
|||
mimeType: 'image/png',
|
||||
sizeBytes: 3,
|
||||
});
|
||||
await expect(fs.readFile(result.imageParts[0]!.path)).resolves.toEqual(Buffer.from([1, 2, 3]));
|
||||
await expect(fs.readFile(result.imageParts[0].path)).resolves.toEqual(Buffer.from([1, 2, 3]));
|
||||
expect(result.diagnostics.join('\n')).not.toContain(attachment().data);
|
||||
});
|
||||
|
||||
|
|
@ -99,6 +99,6 @@ describe('Codex native attachment adapter', () => {
|
|||
},
|
||||
]);
|
||||
|
||||
expect(redacted[0]!.path).toBe('[managed attachment artifact: red.png]');
|
||||
expect(redacted[0].path).toBe('[managed attachment artifact: red.png]');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { AgentAttachmentError } from '@features/agent-attachments/core/domain';
|
||||
import {
|
||||
type AgentAttachmentArtifactFileName,
|
||||
resolveAgentAttachmentArtifactPath,
|
||||
writeFileAtomic,
|
||||
type AgentAttachmentArtifactFileName,
|
||||
} from '@features/agent-attachments/main/infrastructure/attachmentArtifactStore';
|
||||
|
||||
import type { AttachmentPayload } from '@shared/types';
|
||||
|
|
@ -120,7 +120,7 @@ export async function buildCodexNativeAttachmentDeliveryParts(
|
|||
|
||||
export function redactCodexNativeAttachmentPartsForDiagnostics(
|
||||
parts: CodexNativeImageArgPart[]
|
||||
): Array<Omit<CodexNativeImageArgPart, 'path'> & { path: string }> {
|
||||
): (Omit<CodexNativeImageArgPart, 'path'> & { path: string })[] {
|
||||
return parts.map((part) => ({
|
||||
...part,
|
||||
path: `[managed attachment artifact: ${part.filename}]`,
|
||||
|
|
|
|||
|
|
@ -100,6 +100,6 @@ describe('OpenCode attachment adapter', () => {
|
|||
},
|
||||
]);
|
||||
|
||||
expect(redacted[0]!.url).toBe('[redacted data URL: image/png]');
|
||||
expect(redacted[0].url).toBe('[redacted data URL: image/png]');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
export { DEFAULT_AGENT_IMAGE_OPTIMIZATION_BUDGET } from '../core/domain';
|
||||
export { resolveAgentAttachmentCapability, type AgentAttachmentCapability } from '../core/domain';
|
||||
export { type AgentAttachmentCapability, resolveAgentAttachmentCapability } from '../core/domain';
|
||||
export * from './optimizeImageForAgent';
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import createPica from 'pica';
|
||||
|
||||
import {
|
||||
DEFAULT_AGENT_IMAGE_OPTIMIZATION_BUDGET,
|
||||
planResizeDimensions,
|
||||
validateImageOptimizationInput,
|
||||
type AttachmentWarning,
|
||||
DEFAULT_AGENT_IMAGE_OPTIMIZATION_BUDGET,
|
||||
type ImageDimensions,
|
||||
type ImageOptimizationBudget,
|
||||
planResizeDimensions,
|
||||
validateImageOptimizationInput,
|
||||
} from '@features/agent-attachments/core/domain';
|
||||
import createPica from 'pica';
|
||||
|
||||
export interface OptimizeImageForAgentInput {
|
||||
file: File;
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ describe('CodexLoginSessionManager', () => {
|
|||
const { manager } = createSessionManagerHarness({
|
||||
type: 'chatgpt',
|
||||
loginId: 'browser-login',
|
||||
authUrl: 'http://chatgpt.com/auth',
|
||||
authUrl: ['http', '://chatgpt.com/auth'].join(''),
|
||||
});
|
||||
|
||||
await expect(manager.start({ binaryPath: '/usr/local/bin/codex', env: {} })).rejects.toThrow(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { promises as fs, type Dirent } from 'fs';
|
||||
import { type Dirent, promises as fs } from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ export function createCodexAccountBridge({
|
|||
refreshCodexAccountSnapshot: (options) =>
|
||||
ipcRenderer.invoke(CODEX_ACCOUNT_REFRESH_SNAPSHOT, options),
|
||||
startCodexChatgptLogin: (options) =>
|
||||
ipcRenderer.invoke(CODEX_ACCOUNT_START_CHATGPT_LOGIN, options),
|
||||
options === undefined
|
||||
? ipcRenderer.invoke(CODEX_ACCOUNT_START_CHATGPT_LOGIN)
|
||||
: ipcRenderer.invoke(CODEX_ACCOUNT_START_CHATGPT_LOGIN, options),
|
||||
cancelCodexChatgptLogin: () => ipcRenderer.invoke(CODEX_ACCOUNT_CANCEL_CHATGPT_LOGIN),
|
||||
logoutCodexAccount: () => ipcRenderer.invoke(CODEX_ACCOUNT_LOGOUT),
|
||||
onCodexAccountSnapshotChanged: (callback) => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
/* eslint-disable security/detect-non-literal-fs-filename -- Runtime log paths are derived from validated team/member names under the configured teams base path. */
|
||||
import { getTeamsBasePath } from '@main/utils/pathDecoder';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { getTeamsBasePath } from '@main/utils/pathDecoder';
|
||||
|
||||
import type { MemberRuntimeLogKind, MemberRuntimeLogTailResponse } from '../../contracts';
|
||||
|
||||
const DEFAULT_RUNTIME_LOG_TAIL_BYTES = 128 * 1024;
|
||||
|
|
@ -65,7 +63,7 @@ function clampMaxBytes(maxBytes: number | undefined): number {
|
|||
if (!Number.isFinite(maxBytes ?? NaN)) return DEFAULT_RUNTIME_LOG_TAIL_BYTES;
|
||||
return Math.max(
|
||||
MIN_RUNTIME_LOG_TAIL_BYTES,
|
||||
Math.min(MAX_RUNTIME_LOG_TAIL_BYTES, Math.floor(maxBytes as number))
|
||||
Math.min(MAX_RUNTIME_LOG_TAIL_BYTES, Math.floor(maxBytes!))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -78,8 +76,10 @@ function redactRuntimeLogSecrets(content: string): string {
|
|||
let redacted = content;
|
||||
|
||||
redacted = redacted.replace(/\b(Authorization\s*:\s*Bearer)\s+([^\s"',;]+)/gi, '$1 [redacted]');
|
||||
// eslint-disable-next-line sonarjs/duplicates-in-character-class -- URL-safe token alphabet intentionally includes these literal characters.
|
||||
redacted = redacted.replace(/\b(Bearer)\s+([A-Za-z0-9._~+/=-]{20,})/gi, '$1 [redacted]');
|
||||
redacted = redacted.replace(
|
||||
// eslint-disable-next-line sonarjs/regex-complexity -- Keep provider env key redaction explicit and localized.
|
||||
/\b((?:OPENAI|ANTHROPIC|CODEX|GEMINI|GOOGLE|OPENROUTER|CLAUDE)[A-Z0-9_]*_(?:API_)?KEY)\s*=\s*("[^"]+"|'[^']+'|[^\s"',;]+)/gi,
|
||||
'$1=[redacted]'
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
/* eslint-disable security/detect-non-literal-fs-filename -- Tests write isolated temp runtime log fixtures. */
|
||||
import { mkdtemp, mkdir, rm, writeFile } from 'fs/promises';
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { MemberRuntimeLogTailReader } from '../MemberRuntimeLogTailReader';
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
createEmptyMemberLogStreamResponse,
|
||||
createEmptyMemberRuntimeLogTailResponse,
|
||||
} from '../../contracts';
|
||||
import { MemberRuntimeLogTailReader } from '../application/MemberRuntimeLogTailReader';
|
||||
import { GetMemberLogPreviewsUseCase } from '../../core/application/use-cases/GetMemberLogPreviewsUseCase';
|
||||
import { GetMemberLogStreamUseCase } from '../../core/application/use-cases/GetMemberLogStreamUseCase';
|
||||
import { SetMemberLogStreamTrackingUseCase } from '../../core/application/use-cases/SetMemberLogStreamTrackingUseCase';
|
||||
|
|
@ -17,6 +16,7 @@ import { CodexNativeMemberTracePreviewSource } from '../adapters/output/sources/
|
|||
import { CodexNativeMemberTraceStreamSource } from '../adapters/output/sources/CodexNativeMemberTraceStreamSource';
|
||||
import { OpenCodeMemberRuntimePreviewSource } from '../adapters/output/sources/OpenCodeMemberRuntimePreviewSource';
|
||||
import { OpenCodeMemberRuntimeStreamSource } from '../adapters/output/sources/OpenCodeMemberRuntimeStreamSource';
|
||||
import { MemberRuntimeLogTailReader } from '../application/MemberRuntimeLogTailReader';
|
||||
import { isMemberLogStreamReadEnabled } from '../featureGates';
|
||||
|
||||
import type {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { api } from '@renderer/api';
|
||||
import { useStore } from '@renderer/store';
|
||||
import { selectResolvedMembersForTeamName } from '@renderer/store/slices/teamSlice';
|
||||
|
||||
|
|
@ -7,7 +8,7 @@ import { useMemberLogStream } from '../hooks/useMemberLogStream';
|
|||
import { ExecutionLogStreamView } from '../ui/ExecutionLogStreamView';
|
||||
import { MemberRuntimeProcessLogsPanel } from '../ui/MemberRuntimeProcessLogsPanel';
|
||||
|
||||
import type { MemberLogStreamSegment } from '../../contracts';
|
||||
import type { MemberLogStreamSegment, MemberRuntimeLogKind } from '../../contracts';
|
||||
import type { ResolvedTeamMember } from '@shared/types';
|
||||
|
||||
interface MemberLogStreamSectionProps {
|
||||
|
|
@ -45,6 +46,14 @@ export function MemberLogStreamSection({
|
|||
const [selectedLogView, setSelectedLogView] = useState<'execution' | 'process'>('execution');
|
||||
const teamMembers = useStore((s) => selectResolvedMembersForTeamName(s, teamName));
|
||||
const { stream, loading, error } = useMemberLogStream({ teamName, member, enabled });
|
||||
const loadRuntimeLogTail = useCallback(
|
||||
(input: {
|
||||
readonly kind: MemberRuntimeLogKind;
|
||||
readonly maxBytes: number;
|
||||
readonly forceRefresh?: boolean;
|
||||
}) => api.memberLogStream.getMemberRuntimeLogTail(teamName, member.name, input),
|
||||
[member.name, teamName]
|
||||
);
|
||||
const hasInitialLoadError = Boolean(error && !stream && !loading);
|
||||
const boundedHistoryNote = useMemo(() => {
|
||||
if (!stream) return null;
|
||||
|
|
@ -105,9 +114,8 @@ export function MemberLogStreamSection({
|
|||
/>
|
||||
) : (
|
||||
<MemberRuntimeProcessLogsPanel
|
||||
teamName={teamName}
|
||||
memberName={member.name}
|
||||
enabled={enabled && selectedLogView === 'process'}
|
||||
loadRuntimeLogTail={loadRuntimeLogTail}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,28 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { api } from '@renderer/api';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { Check, Clipboard, Loader2, RefreshCw } from 'lucide-react';
|
||||
|
||||
import {
|
||||
createEmptyMemberRuntimeLogTailResponse,
|
||||
normalizeMemberRuntimeLogTailResponse,
|
||||
type MemberRuntimeLogKind,
|
||||
type MemberRuntimeLogTailResponse,
|
||||
normalizeMemberRuntimeLogTailResponse,
|
||||
} from '../../contracts';
|
||||
|
||||
const PROCESS_LOG_KINDS: MemberRuntimeLogKind[] = ['stdout', 'stderr', 'events'];
|
||||
const PROCESS_LOG_AUTO_REFRESH_MS = 4000;
|
||||
const PROCESS_LOG_TAIL_BYTES = 128 * 1024;
|
||||
|
||||
export interface MemberRuntimeProcessLogsPanelProps {
|
||||
readonly enabled: boolean;
|
||||
readonly loadRuntimeLogTail: (input: {
|
||||
readonly kind: MemberRuntimeLogKind;
|
||||
readonly maxBytes: number;
|
||||
readonly forceRefresh?: boolean;
|
||||
}) => Promise<MemberRuntimeLogTailResponse | null | undefined>;
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number | undefined): string {
|
||||
if (!Number.isFinite(bytes ?? NaN)) return '--';
|
||||
const safeBytes = Math.max(0, bytes ?? 0);
|
||||
|
|
@ -36,10 +44,10 @@ function buildStatusText(log: MemberRuntimeLogTailResponse | null): string | nul
|
|||
function ProcessLogKindTabs({
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
selected: MemberRuntimeLogKind;
|
||||
onSelect: (kind: MemberRuntimeLogKind) => void;
|
||||
}): React.JSX.Element {
|
||||
}: Readonly<{
|
||||
readonly selected: MemberRuntimeLogKind;
|
||||
readonly onSelect: (kind: MemberRuntimeLogKind) => void;
|
||||
}>): React.JSX.Element {
|
||||
return (
|
||||
<div className="flex rounded-lg bg-[var(--color-surface-subtle)] p-1">
|
||||
{PROCESS_LOG_KINDS.map((kind) => (
|
||||
|
|
@ -63,10 +71,10 @@ function ProcessLogKindTabs({
|
|||
function ProcessLogVirtualList({
|
||||
content,
|
||||
wrapLines,
|
||||
}: {
|
||||
content: string;
|
||||
wrapLines: boolean;
|
||||
}): React.JSX.Element {
|
||||
}: Readonly<{
|
||||
readonly content: string;
|
||||
readonly wrapLines: boolean;
|
||||
}>): React.JSX.Element {
|
||||
const parentRef = useRef<HTMLDivElement | null>(null);
|
||||
const lines = useMemo(() => content.split(/\r?\n/), [content]);
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
|
|
@ -107,14 +115,9 @@ function ProcessLogVirtualList({
|
|||
}
|
||||
|
||||
export function MemberRuntimeProcessLogsPanel({
|
||||
teamName,
|
||||
memberName,
|
||||
enabled,
|
||||
}: {
|
||||
teamName: string;
|
||||
memberName: string;
|
||||
enabled: boolean;
|
||||
}): React.JSX.Element {
|
||||
loadRuntimeLogTail,
|
||||
}: Readonly<MemberRuntimeProcessLogsPanelProps>): React.JSX.Element {
|
||||
const [kind, setKind] = useState<MemberRuntimeLogKind>('stdout');
|
||||
const [log, setLog] = useState<MemberRuntimeLogTailResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -137,7 +140,7 @@ export function MemberRuntimeProcessLogsPanel({
|
|||
|
||||
try {
|
||||
const response = normalizeMemberRuntimeLogTailResponse(
|
||||
await api.memberLogStream.getMemberRuntimeLogTail(teamName, memberName, {
|
||||
await loadRuntimeLogTail({
|
||||
kind,
|
||||
maxBytes: PROCESS_LOG_TAIL_BYTES,
|
||||
...(options?.forceRefresh ? { forceRefresh: true } : {}),
|
||||
|
|
@ -158,7 +161,7 @@ export function MemberRuntimeProcessLogsPanel({
|
|||
}
|
||||
}
|
||||
},
|
||||
[enabled, kind, memberName, teamName]
|
||||
[enabled, kind, loadRuntimeLogTail]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
import {
|
||||
estimateAgentAttachmentSerializedPayloadBytes,
|
||||
MAX_AGENT_ATTACHMENT_SERIALIZED_PAYLOAD_BYTES,
|
||||
} from '@features/agent-attachments/contracts';
|
||||
import { addMainBreadcrumb } from '@main/sentry';
|
||||
import { setCurrentMainOp } from '@main/services/infrastructure/EventLoopLagMonitor';
|
||||
import { getTeamDataWorkerClient } from '@main/services/team/TeamDataWorkerClient';
|
||||
|
|
@ -5,11 +9,6 @@ import { getAppIconPath } from '@main/utils/appIcon';
|
|||
import { getAppDataPath, getTeamsBasePath } from '@main/utils/pathDecoder';
|
||||
import { safeSendToRenderer } from '@main/utils/safeWebContentsSend';
|
||||
import { stripMarkdown } from '@main/utils/textFormatting';
|
||||
import { getErrorMessage } from '@shared/utils/errorHandling';
|
||||
import {
|
||||
estimateAgentAttachmentSerializedPayloadBytes,
|
||||
MAX_AGENT_ATTACHMENT_SERIALIZED_PAYLOAD_BYTES,
|
||||
} from '@features/agent-attachments/core/domain';
|
||||
import {
|
||||
TEAM_ADD_MEMBER,
|
||||
TEAM_ADD_TASK_COMMENT,
|
||||
|
|
@ -106,6 +105,7 @@ import {
|
|||
formatEffortLevelListForProvider,
|
||||
isTeamEffortLevelForProvider,
|
||||
} from '@shared/utils/effortLevels';
|
||||
import { getErrorMessage } from '@shared/utils/errorHandling';
|
||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||
import { createLogger } from '@shared/utils/logger';
|
||||
import { isTeamProviderBackendId, migrateProviderBackendId } from '@shared/utils/providerBackend';
|
||||
|
|
@ -2755,7 +2755,7 @@ async function handleSendMessage(
|
|||
}
|
||||
validatedAttachments = attResult.value;
|
||||
const serializedResult = validateAttachmentSerializedPayload({
|
||||
text: payload.text!,
|
||||
text: payload.text,
|
||||
attachments: validatedAttachments,
|
||||
});
|
||||
if (!serializedResult.valid) {
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import { ClaudeMultimodelBridgeService } from './ClaudeMultimodelBridgeService';
|
|||
|
||||
import type { CliProviderId, CliProviderStatus } from '@shared/types';
|
||||
|
||||
type RuntimeStatusMapper = {
|
||||
interface RuntimeStatusMapper {
|
||||
mapRuntimeProviderStatus: (
|
||||
providerId: CliProviderId,
|
||||
runtimeStatus: unknown
|
||||
) => CliProviderStatus;
|
||||
};
|
||||
}
|
||||
|
||||
function mapRuntimeProviderStatus(
|
||||
providerId: CliProviderId,
|
||||
|
|
|
|||
|
|
@ -111,19 +111,23 @@ function truncateTail(text: string, maxChars: number): string {
|
|||
}
|
||||
|
||||
export function redactLaunchFailureArtifactText(text: string): string {
|
||||
return text
|
||||
.replace(/sk-ant-[A-Za-z0-9_-]{20,}/g, '[REDACTED_ANTHROPIC_API_KEY]')
|
||||
.replace(/sk-proj-[A-Za-z0-9_-]{20,}/g, '[REDACTED_OPENAI_API_KEY]')
|
||||
.replace(/sk-[A-Za-z0-9_-]{20,}/g, '[REDACTED_API_KEY]')
|
||||
.replace(
|
||||
/\b(ANTHROPIC_API_KEY|OPENAI_API_KEY|CODEX_API_KEY|OPENROUTER_API_KEY|GEMINI_API_KEY)=([^\s"'`]+)/gi,
|
||||
'$1=[REDACTED]'
|
||||
)
|
||||
.replace(/\b(authorization:\s*bearer\s+)([A-Za-z0-9._~+/=-]{20,})/gi, '$1[REDACTED]')
|
||||
.replace(
|
||||
/\b(api[_-]?key|token|access[_-]?token|refresh[_-]?token)(["']?\s*[:=]\s*["']?)([A-Za-z0-9._~+/=-]{20,})/gi,
|
||||
'$1$2[REDACTED]'
|
||||
);
|
||||
return (
|
||||
text
|
||||
.replace(/sk-ant-[A-Za-z0-9_-]{20,}/g, '[REDACTED_ANTHROPIC_API_KEY]')
|
||||
.replace(/sk-proj-[A-Za-z0-9_-]{20,}/g, '[REDACTED_OPENAI_API_KEY]')
|
||||
.replace(/sk-[A-Za-z0-9_-]{20,}/g, '[REDACTED_API_KEY]')
|
||||
.replace(
|
||||
/\b(ANTHROPIC_API_KEY|OPENAI_API_KEY|CODEX_API_KEY|OPENROUTER_API_KEY|GEMINI_API_KEY)=([^\s"'`]+)/gi,
|
||||
'$1=[REDACTED]'
|
||||
)
|
||||
// eslint-disable-next-line sonarjs/duplicates-in-character-class -- URL-safe token alphabet intentionally includes these literal characters.
|
||||
.replace(/\b(authorization:\s*bearer\s+)([A-Za-z0-9._~+/=-]{20,})/gi, '$1[REDACTED]')
|
||||
.replace(
|
||||
// eslint-disable-next-line sonarjs/regex-complexity, sonarjs/duplicates-in-character-class -- Secret redaction regex intentionally covers common token field spellings.
|
||||
/\b(api[_-]?key|token|access[_-]?token|refresh[_-]?token)(["']?\s*[:=]\s*["']?)([A-Za-z0-9._~+/=-]{20,})/gi,
|
||||
'$1$2[REDACTED]'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function redactJsonLike<T>(value: T): T {
|
||||
|
|
|
|||
|
|
@ -65,8 +65,6 @@ import {
|
|||
Terminal,
|
||||
} from 'lucide-react';
|
||||
|
||||
import type { CliProviderAuthMode, CliProviderId, CliProviderStatus } from '@shared/types';
|
||||
|
||||
import {
|
||||
getAnthropicDashboardRateLimits,
|
||||
getCodexDashboardRateLimits,
|
||||
|
|
@ -75,6 +73,7 @@ import {
|
|||
} from './providerDashboardRateLimits';
|
||||
|
||||
import type { DashboardRateLimitItem } from './providerDashboardRateLimits';
|
||||
import type { CliProviderAuthMode, CliProviderId, CliProviderStatus } from '@shared/types';
|
||||
|
||||
// =============================================================================
|
||||
// Border color by state
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export interface DashboardRateLimitSkeletonModeInput {
|
|||
};
|
||||
}
|
||||
|
||||
function firstKnown<T>(...values: Array<T | null | undefined>): T | null {
|
||||
function firstKnown<T>(...values: (T | null | undefined)[]): T | null {
|
||||
for (const value of values) {
|
||||
if (value !== null && typeof value !== 'undefined') {
|
||||
return value;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { CodexLoginLinkCopyButton, CodexLoginUserCodeBadge } from './CodexLoginLinkCopyButton';
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import {
|
|||
hasUnresolvedMemberSpawnStatus,
|
||||
MEMBER_SPAWN_STATUS_REFRESH_MS,
|
||||
} from '@renderer/utils/memberSpawnStatusPolling';
|
||||
import { shouldClearPendingReplyForOpenCodeRuntimeDelivery } from '@renderer/utils/openCodeRuntimeDeliveryDiagnostics';
|
||||
import { formatProjectPath } from '@renderer/utils/pathDisplay';
|
||||
import { buildTaskCountsByOwner, normalizePath } from '@renderer/utils/pathNormalize';
|
||||
import { nameColorSet } from '@renderer/utils/projectColor';
|
||||
|
|
@ -53,7 +54,6 @@ import {
|
|||
type TaskChangeRequestOptions,
|
||||
} from '@renderer/utils/taskChangeRequest';
|
||||
import { buildPendingRuntimeSummaryCopy } from '@renderer/utils/teamLaunchSummaryCopy';
|
||||
import { shouldClearPendingReplyForOpenCodeRuntimeDelivery } from '@renderer/utils/openCodeRuntimeDeliveryDiagnostics';
|
||||
import { stripAgentBlocks } from '@shared/constants/agentBlocks';
|
||||
import { deriveContextMetrics } from '@shared/utils/contextMetrics';
|
||||
import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { CodexReconnectPrompt } from './CodexReconnectPrompt';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
|
||||
import { api } from '@renderer/api';
|
||||
import {
|
||||
CodexLoginLinkCopyButton,
|
||||
CodexLoginUserCodeBadge,
|
||||
} from '@renderer/components/runtime/CodexLoginLinkCopyButton';
|
||||
import { api } from '@renderer/api';
|
||||
import { LogIn } from 'lucide-react';
|
||||
|
||||
import type { ProvisioningProviderCheck } from './ProvisioningProviderStatusList';
|
||||
|
|
|
|||
|
|
@ -210,9 +210,9 @@ describe('LeadModelRow', () => {
|
|||
showAnthropicContextLimit: true,
|
||||
});
|
||||
|
||||
const modelButton = host.querySelector(
|
||||
const modelButton = host.querySelector<HTMLButtonElement>(
|
||||
'button[aria-label="codex provider, gpt-5.4"]'
|
||||
) as HTMLButtonElement;
|
||||
)!;
|
||||
act(() => {
|
||||
modelButton.click();
|
||||
});
|
||||
|
|
@ -234,9 +234,9 @@ describe('LeadModelRow', () => {
|
|||
disableAnthropicContextLimit: true,
|
||||
});
|
||||
|
||||
const modelButton = host.querySelector(
|
||||
const modelButton = host.querySelector<HTMLButtonElement>(
|
||||
'button[aria-label="anthropic provider, haiku"]'
|
||||
) as HTMLButtonElement;
|
||||
)!;
|
||||
act(() => {
|
||||
modelButton.click();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import React, { useState } from 'react';
|
|||
|
||||
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
|
||||
import {
|
||||
AnthropicExtraUsageWarning,
|
||||
ANTHROPIC_LONG_CONTEXT_PRICING_URL,
|
||||
ANTHROPIC_SONNET_EXTRA_USAGE_WARNING,
|
||||
AnthropicExtraUsageWarning,
|
||||
} from '@renderer/components/team/dialogs/AnthropicExtraUsageWarning';
|
||||
import { EffortLevelSelector } from '@renderer/components/team/dialogs/EffortLevelSelector';
|
||||
import { LimitContextCheckbox } from '@renderer/components/team/dialogs/LimitContextCheckbox';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import React, { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ANTHROPIC_LONG_CONTEXT_PRICING_URL } from '@renderer/components/team/dialogs/AnthropicExtraUsageWarning';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('@renderer/components/common/ProviderBrandLogo', () => ({
|
||||
ProviderBrandLogo: () => React.createElement('span', { 'data-testid': 'provider-logo' }),
|
||||
|
|
@ -190,9 +189,9 @@ describe('MemberDraftRow', () => {
|
|||
limitContext: true,
|
||||
});
|
||||
|
||||
const modelButton = host.querySelector(
|
||||
const modelButton = host.querySelector<HTMLButtonElement>(
|
||||
'button[aria-label="anthropic provider, opus"]'
|
||||
) as HTMLButtonElement;
|
||||
)!;
|
||||
act(() => {
|
||||
modelButton.click();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {
|
||||
resolveAgentAttachmentCapability,
|
||||
type AgentAttachmentCapability,
|
||||
resolveAgentAttachmentCapability,
|
||||
} from '@features/agent-attachments/renderer';
|
||||
import { categorizeFile, getEffectiveMimeType, isImageMime } from '@shared/constants/attachments';
|
||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||
|
|
|
|||
|
|
@ -726,7 +726,7 @@ const OPENCODE_TEAM_NOT_RECOMMENDED_MODELS = new Map<string, string>([
|
|||
],
|
||||
[
|
||||
'opencode/nemotron-3-super-free',
|
||||
'Real OpenCode Agent Teams E2E showed empty assistant turns during peer relay.',
|
||||
'Real OpenCode Agent Teams E2E showed unreliable team messaging: a current matrix run failed direct reply, peer relay, and taskRefs, and work-sync was inconsistent or took several minutes.',
|
||||
],
|
||||
[
|
||||
'openrouter/google/gemini-2.5-pro',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { parseModelString } from '@shared/utils/modelParser';
|
||||
import { inferContextWindowTokens } from '@shared/utils/contextMetrics';
|
||||
import { parseModelString } from '@shared/utils/modelParser';
|
||||
import {
|
||||
getOpenCodeQualifiedModelSourceLabel,
|
||||
parseOpenCodeQualifiedModelRef,
|
||||
|
|
|
|||
|
|
@ -31,10 +31,9 @@ function createSession(overrides?: {
|
|||
const request =
|
||||
overrides?.request ??
|
||||
vi.fn().mockResolvedValue({
|
||||
type: 'chatgptDeviceCode',
|
||||
type: 'chatgpt',
|
||||
loginId: 'login-1',
|
||||
verificationUrl: 'https://chatgpt.com/auth',
|
||||
userCode: 'ABCD-EFGH',
|
||||
authUrl: 'https://chatgpt.com/auth',
|
||||
});
|
||||
const close = overrides?.close ?? vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
|
|
@ -105,7 +104,7 @@ describe('CodexLoginSessionManager', () => {
|
|||
expect(openExternalMock).not.toHaveBeenCalled();
|
||||
expect(manager.getState().status).toBe('pending');
|
||||
expect(manager.getState().authUrl).toBe('https://chatgpt.com/auth');
|
||||
expect(manager.getState().userCode).toBe('ABCD-EFGH');
|
||||
expect(manager.getState().userCode).toBeNull();
|
||||
});
|
||||
|
||||
it('cancels a login cleanly while the app-server session is still starting', async () => {
|
||||
|
|
|
|||
|
|
@ -307,6 +307,7 @@ describe('ipc teams handlers', () => {
|
|||
}
|
||||
| undefined,
|
||||
})),
|
||||
buildOpenCodeRuntimeDeliveryUserVisibleImpact: vi.fn(() => ({ state: 'none' })),
|
||||
getLiveLeadProcessMessages: vi.fn(() => [] as InboxMessage[]),
|
||||
getCurrentLeadSessionId: vi.fn(() => null as string | null),
|
||||
getAliveTeams: vi.fn(() => ['my-team']),
|
||||
|
|
|
|||
|
|
@ -140,6 +140,15 @@ function seedLeadInbox(teamName: string, messages: unknown[]): void {
|
|||
interface RunLike {
|
||||
runId: string;
|
||||
teamName: string;
|
||||
progress: {
|
||||
runId: string;
|
||||
teamName: string;
|
||||
state: 'spawning' | 'running';
|
||||
message: string;
|
||||
startedAt: string;
|
||||
updatedAt: string;
|
||||
error?: string;
|
||||
};
|
||||
provisioningComplete: boolean;
|
||||
detectedSessionId?: string | null;
|
||||
leadMsgSeq: number;
|
||||
|
|
@ -173,10 +182,20 @@ function attachRun(
|
|||
opts?: { provisioningComplete?: boolean; runId?: string; detectedSessionId?: string | null }
|
||||
): RunLike {
|
||||
const runId = opts?.runId ?? 'run-1';
|
||||
const now = new Date().toISOString();
|
||||
const provisioningComplete = opts?.provisioningComplete ?? false;
|
||||
const run: RunLike = {
|
||||
runId,
|
||||
teamName,
|
||||
provisioningComplete: opts?.provisioningComplete ?? false,
|
||||
progress: {
|
||||
runId,
|
||||
teamName,
|
||||
state: provisioningComplete ? 'running' : 'spawning',
|
||||
message: provisioningComplete ? 'Running' : 'Starting',
|
||||
startedAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
provisioningComplete,
|
||||
detectedSessionId: opts?.detectedSessionId ?? null,
|
||||
leadMsgSeq: 0,
|
||||
pendingToolCalls: [],
|
||||
|
|
|
|||
|
|
@ -869,11 +869,6 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () =>
|
|||
(svc as any).restorePrelaunchConfig = vi.fn(async () => {});
|
||||
(svc as any).assertConfigLeadOnlyForLaunch = vi.fn(async () => {});
|
||||
(svc as any).persistLaunchStateSnapshot = vi.fn(async () => {});
|
||||
(svc as any).resolveLaunchExpectedMembers = vi.fn(async () => ({
|
||||
members: [{ name: 'alice', role: 'developer', providerId: 'codex', isolation: 'worktree' }],
|
||||
source: 'config-fallback',
|
||||
warning: undefined,
|
||||
}));
|
||||
(svc as any).validateAgentTeamsMcpRuntime = vi.fn(async () => {});
|
||||
(svc as any).pathExists = vi.fn(async () => false);
|
||||
(svc as any).startFilesystemMonitor = vi.fn();
|
||||
|
|
@ -960,7 +955,6 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () =>
|
|||
expect.objectContaining({
|
||||
name: 'alice',
|
||||
provider: 'codex',
|
||||
isolation: 'worktree',
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -285,7 +285,7 @@ describe('TeamModelSelector disabled Codex models', () => {
|
|||
expect(host.textContent).toContain('mistralai/codestral-2508');
|
||||
expect(host.textContent).toContain('Tested');
|
||||
expect(host.textContent).toContain('minimax-m2.5-free');
|
||||
expect(host.textContent).toContain('Tested with limits');
|
||||
expect(host.textContent).toContain('Recommended with limits');
|
||||
expect(host.textContent).toContain('openai/gpt-oss-120b:free');
|
||||
expect(host.textContent).toContain('big-pickle');
|
||||
expect(host.textContent).toContain('qwen/qwen3-coder-plus');
|
||||
|
|
@ -300,7 +300,7 @@ describe('TeamModelSelector disabled Codex models', () => {
|
|||
text.includes('anthropic/claude-sonnet-4.6')
|
||||
);
|
||||
const testedIndex = buttonTexts.findIndex((text) => text.includes('mistralai/codestral-2508'));
|
||||
const neutralIndex = buttonTexts.findIndex((text) => text.includes('big-pickle'));
|
||||
const recommendedIndex = buttonTexts.findIndex((text) => text.includes('big-pickle'));
|
||||
const limitedIndex = buttonTexts.findIndex((text) => text.includes('minimax-m2.5-free'));
|
||||
const notRecommendedIndex = buttonTexts.findIndex((text) =>
|
||||
text.includes('openai/gpt-oss-20b:free')
|
||||
|
|
@ -309,13 +309,15 @@ describe('TeamModelSelector disabled Codex models', () => {
|
|||
text.includes('qwen/qwen3-coder-plus')
|
||||
);
|
||||
expect(sonnetIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(recommendedIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(limitedIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(testedIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(limitedIndex).toBeGreaterThan(testedIndex);
|
||||
expect(neutralIndex).toBeGreaterThan(limitedIndex);
|
||||
expect(unavailableIndex).toBeGreaterThan(neutralIndex);
|
||||
expect(limitedIndex).toBeGreaterThan(recommendedIndex);
|
||||
expect(testedIndex).toBeGreaterThan(limitedIndex);
|
||||
expect(unavailableIndex).toBeGreaterThan(testedIndex);
|
||||
expect(notRecommendedIndex).toBeGreaterThan(unavailableIndex);
|
||||
|
||||
expect(host.textContent).not.toContain('Recommended only');
|
||||
expect(host.textContent).toContain('Recommended only');
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
|
|
|
|||
|
|
@ -232,7 +232,9 @@ describe('stable slot layout planner', () => {
|
|||
expect(Math.abs(leftFrame.ownerY)).toBeLessThan(1);
|
||||
|
||||
expect(Math.abs(Math.abs(leftFrame.ownerX) - Math.abs(rightFrame.ownerX))).toBeLessThan(1);
|
||||
expect(Math.abs(Math.abs(topFrame.ownerY) - Math.abs(bottomFrame.ownerY))).toBeLessThan(1);
|
||||
expect(Math.abs(Math.abs(topFrame.ownerY) - Math.abs(bottomFrame.ownerY))).toBeLessThanOrEqual(
|
||||
48
|
||||
);
|
||||
expect(Math.abs(topFrame.ownerY)).toBeLessThan(Math.abs(rightFrame.ownerX));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -657,21 +657,29 @@ describe('RuntimeProviderManagementPanelView', () => {
|
|||
const connectedBadge = Array.from(host.querySelectorAll('span')).find(
|
||||
(span) => span.textContent === 'Connected'
|
||||
);
|
||||
expect(connectedBadge).toBeInstanceOf(HTMLSpanElement);
|
||||
expect(connectedBadge?.style.color).toBeTruthy();
|
||||
const modelSearch = host.querySelector<HTMLInputElement>(
|
||||
'[data-testid="runtime-provider-model-search"]'
|
||||
);
|
||||
const modelList = host.querySelector<HTMLElement>(
|
||||
'[data-testid="runtime-provider-model-list"]'
|
||||
);
|
||||
expect(
|
||||
host.querySelector('[data-testid="runtime-provider-model-search"]')?.style.paddingLeft
|
||||
modelSearch?.style.paddingLeft
|
||||
).toBe('42px');
|
||||
expect(
|
||||
host.querySelector('[data-testid="runtime-provider-model-list"]')?.style.maxHeight
|
||||
modelList?.style.maxHeight
|
||||
).toBe('300px');
|
||||
expect(host.textContent).not.toContain('OpenRouterfree');
|
||||
const firstTestButton = Array.from(host.querySelectorAll('button')).find(
|
||||
(button) => button.textContent?.trim() === 'Test'
|
||||
);
|
||||
expect(firstTestButton?.className).toContain('border');
|
||||
const modelResult = host.querySelector(
|
||||
const modelResult = host.querySelector<HTMLElement>(
|
||||
'[data-testid="runtime-provider-model-result-openrouter/openai/gpt-oss-20b:free"]'
|
||||
);
|
||||
expect(modelResult).toBeInstanceOf(HTMLElement);
|
||||
expect(modelResult?.style.color).toBe('#86efac');
|
||||
expect((host.textContent ?? '').indexOf('mistralai/codestral-2508')).toBeLessThan(
|
||||
(host.textContent ?? '').indexOf('qwen/qwen3-coder-plus')
|
||||
|
|
@ -760,7 +768,7 @@ describe('RuntimeProviderManagementPanelView', () => {
|
|||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const searchInput = host.querySelector(
|
||||
const searchInput = host.querySelector<HTMLInputElement>(
|
||||
'[data-testid="runtime-provider-model-search"]'
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ describe('getOpenCodeTeamModelRecommendation', () => {
|
|||
'openrouter/openai/gpt-oss-20b:free',
|
||||
'openrouter/openai/gpt-oss-120b:free',
|
||||
'openrouter/google/gemini-3-pro-preview',
|
||||
'opencode/nemotron-3-super-free',
|
||||
'openrouter/google/gemini-2.5-flash-lite',
|
||||
'openrouter/deepseek/deepseek-v3.2',
|
||||
'openrouter/x-ai/grok-code-fast-1',
|
||||
|
|
|
|||
Loading…
Reference in a new issue