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