diff --git a/src/features/agent-graph/renderer/hooks/useGraphMemberLogPreviews.ts b/src/features/agent-graph/renderer/hooks/useGraphMemberLogPreviews.ts index 39049efc..3ed91b86 100644 --- a/src/features/agent-graph/renderer/hooks/useGraphMemberLogPreviews.ts +++ b/src/features/agent-graph/renderer/hooks/useGraphMemberLogPreviews.ts @@ -32,7 +32,7 @@ function buildRequestKey(input: { .sort((left, right) => left[0].localeCompare(right[0])); return JSON.stringify([ input.teamName, - input.memberNames.map(normalizeMemberName), + input.memberNames.map(normalizeMemberName).sort((left, right) => left.localeCompare(right)), laneEntries, input.maxItemsPerMember, input.textLimit, @@ -143,7 +143,14 @@ export function useGraphMemberLogPreviews(input: { } return result; }, [input.memberNames]); - const memberKey = useMemo(() => memberNames.map(normalizeMemberName).join('|'), [memberNames]); + const memberKey = useMemo( + () => + memberNames + .map(normalizeMemberName) + .sort((left, right) => left.localeCompare(right)) + .join('|'), + [memberNames] + ); const [previewsByMember, setPreviewsByMember] = useState( new Map() ); diff --git a/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx b/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx index 9a298332..9f4e07fe 100644 --- a/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx +++ b/src/features/agent-graph/renderer/ui/GraphMemberLogPreviewHud.tsx @@ -101,8 +101,13 @@ function itemIcon(item: MemberLogPreviewItem): React.JSX.Element { return ; } -function resolveEmptyText(preview: MemberLogPreviewMember | undefined, loading: boolean): string { +function resolveEmptyText( + preview: MemberLogPreviewMember | undefined, + loading: boolean, + error: string | null +): string { if (loading && !preview) return 'Loading logs'; + if (error && !preview) return 'Logs unavailable'; if (preview?.warnings.some((warning) => warning.code === 'codex_member_wide_not_supported')) { return 'Unsupported provider'; } @@ -111,12 +116,45 @@ function resolveEmptyText(preview: MemberLogPreviewMember | undefined, loading: function compactDisplayTitle(item: MemberLogPreviewItem): string { const title = item.title.trim(); + if (title.toLowerCase() === 'tool result') { + return title; + } if (item.kind === 'tool_result' && title.toLowerCase().endsWith(' result')) { return title.slice(0, -' result'.length).trim() || title; } return title; } +function trimRepeatedTitlePrefix(preview: string, title: string): string { + const normalizedPreview = preview.toLowerCase(); + const normalizedTitle = title.toLowerCase(); + if (normalizedPreview.startsWith(`${normalizedTitle} - `)) { + return preview.slice(title.length + 3).trim(); + } + if (normalizedPreview.startsWith(`${normalizedTitle}: `)) { + return preview.slice(title.length + 2).trim(); + } + if (normalizedPreview.startsWith(`${normalizedTitle} `)) { + return preview.slice(title.length + 1).trim(); + } + return preview; +} + +function compactPreviewText(item: MemberLogPreviewItem, displayTitle: string): string { + const preview = item.preview?.trim(); + if (preview) { + const compact = trimRepeatedTitlePrefix(preview, displayTitle); + return compact || preview; + } + if (item.kind === 'tool_result') { + return item.tone === 'error' ? 'No error output' : 'No output'; + } + if (item.kind === 'tool_use') { + return 'No input'; + } + return item.sourceLabel || 'Log event'; +} + function setShellHidden(shell: HTMLDivElement): void { shell.style.opacity = '0'; shell.style.pointerEvents = 'none'; @@ -155,7 +193,7 @@ export const GraphMemberLogPreviewHud = ({ }), [nodes] ); - const { previewsByMember, loading } = useGraphMemberLogPreviews({ + const { previewsByMember, loading, error } = useGraphMemberLogPreviews({ teamName, memberNames: visibleMemberNames, laneIdsByMember, @@ -367,10 +405,10 @@ export const GraphMemberLogPreviewHud = ({ (memberName: string, item: MemberLogPreviewItem) => { const relativeTime = formatRelativeTime(item.timestamp); const displayTitle = compactDisplayTitle(item); - const previewText = item.preview || item.sourceLabel || 'Log event'; + const previewText = compactPreviewText(item, displayTitle); const titleText = relativeTime - ? `${item.title} ${relativeTime} ${previewText}` - : `${item.title} ${previewText}`; + ? `${displayTitle} ${relativeTime} ${previewText}` + : `${displayTitle} ${previewText}`; const isHighlighted = highlightedItemIds.has(item.id); return ( @@ -392,23 +430,15 @@ export const GraphMemberLogPreviewHud = ({ > {itemIcon(item)} - - - {displayTitle} - - {relativeTime ? ( - - {relativeTime} - - ) : null} + + {displayTitle} - + {relativeTime ? ( + + {relativeTime} + + ) : null} + {previewText} @@ -467,7 +497,7 @@ export const GraphMemberLogPreviewHud = ({ className="flex h-14 min-h-14 items-center rounded-md border border-dashed border-white/10 bg-[rgba(8,14,28,0.28)] px-3 text-left text-[11px] text-slate-400/60" onClick={() => openLogs(memberName)} > - {resolveEmptyText(preview, loading)} + {resolveEmptyText(preview, loading, error)} )} {preview && preview.overflowCount > 0 ? ( diff --git a/src/features/member-log-stream/core/application/use-cases/GetMemberLogPreviewsUseCase.ts b/src/features/member-log-stream/core/application/use-cases/GetMemberLogPreviewsUseCase.ts index 92d8fea2..a167584f 100644 --- a/src/features/member-log-stream/core/application/use-cases/GetMemberLogPreviewsUseCase.ts +++ b/src/features/member-log-stream/core/application/use-cases/GetMemberLogPreviewsUseCase.ts @@ -74,9 +74,16 @@ function stableInputKey(input: { textLimit: number; forceRefresh?: boolean; }): string { + const memberKeys = input.members + .map((member) => [normalizeMemberName(member.memberName), member.laneId ?? ''] as const) + .sort((left, right) => { + const byName = left[0].localeCompare(right[0]); + if (byName !== 0) return byName; + return left[1].localeCompare(right[1]); + }); return JSON.stringify([ input.teamName, - input.members.map((member) => [normalizeMemberName(member.memberName), member.laneId ?? '']), + memberKeys, input.maxItems, input.textLimit, input.forceRefresh === true, diff --git a/src/features/member-log-stream/core/application/use-cases/__tests__/GetMemberLogPreviewsUseCase.test.ts b/src/features/member-log-stream/core/application/use-cases/__tests__/GetMemberLogPreviewsUseCase.test.ts index 6d70f931..58c3140f 100644 --- a/src/features/member-log-stream/core/application/use-cases/__tests__/GetMemberLogPreviewsUseCase.test.ts +++ b/src/features/member-log-stream/core/application/use-cases/__tests__/GetMemberLogPreviewsUseCase.test.ts @@ -5,6 +5,7 @@ import { GetMemberLogPreviewsUseCase } from '../GetMemberLogPreviewsUseCase'; import type { MemberLogPreviewSource, MemberLogPreviewSourceInput, + MemberLogPreviewSourceResult, } from '../../ports/MemberLogPreviewSource'; function source( @@ -16,6 +17,38 @@ function source( return { provider, loadPreview }; } +function createDeferred(): { + promise: Promise; + resolve: (value: T) => void; +} { + let resolve!: (value: T) => void; + const promise = new Promise((innerResolve) => { + resolve = innerResolve; + }); + return { promise, resolve }; +} + +function textResult(memberName: string): MemberLogPreviewSourceResult { + return { + provider: 'claude_transcript', + status: 'included', + items: [ + { + id: `item:${memberName}`, + kind: 'text', + provider: 'claude_transcript', + timestamp: '2026-04-01T12:00:00.000Z', + title: 'Assistant', + preview: memberName, + tone: 'neutral', + }, + ], + warnings: [], + truncated: false, + overflowCount: 0, + }; +} + describe('GetMemberLogPreviewsUseCase', () => { it('dedupes members, clamps options, and merges source coverage per member', async () => { const loadPreview = vi.fn(async (input: MemberLogPreviewSourceInput) => ({ @@ -95,4 +128,38 @@ describe('GetMemberLogPreviewsUseCase', () => { expect(loadPreview).toHaveBeenCalledTimes(1); expect(first.members[0]?.warnings[0]?.code).toBe('codex_member_wide_not_supported'); }); + + it('dedupes in-flight batch requests for the same member set in different order', async () => { + const pendingByMember = new Map< + string, + ReturnType> + >(); + const loadPreview = vi.fn((input: MemberLogPreviewSourceInput) => { + const deferred = createDeferred(); + pendingByMember.set(input.memberName, deferred); + return deferred.promise; + }); + const useCase = new GetMemberLogPreviewsUseCase({ + sources: [source('claude_transcript', loadPreview)], + clock: { now: () => Date.parse('2026-04-01T12:01:00.000Z') }, + logger: { warn: vi.fn(), error: vi.fn() }, + }); + + const firstPromise = useCase.execute({ + teamName: 'alpha-team', + memberNames: ['alice', 'bob'], + }); + const secondPromise = useCase.execute({ + teamName: 'alpha-team', + memberNames: ['bob', 'alice'], + }); + + expect(loadPreview).toHaveBeenCalledTimes(2); + pendingByMember.get('alice')?.resolve(textResult('alice')); + pendingByMember.get('bob')?.resolve(textResult('bob')); + + const [first, second] = await Promise.all([firstPromise, secondPromise]); + expect(second).toBe(first); + expect(first.members.map((member) => member.memberName)).toEqual(['alice', 'bob']); + }); }); diff --git a/src/features/member-log-stream/core/domain/policies/__tests__/memberLogPreviewExtractor.test.ts b/src/features/member-log-stream/core/domain/policies/__tests__/memberLogPreviewExtractor.test.ts index a8bc4e24..b59f9a8e 100644 --- a/src/features/member-log-stream/core/domain/policies/__tests__/memberLogPreviewExtractor.test.ts +++ b/src/features/member-log-stream/core/domain/policies/__tests__/memberLogPreviewExtractor.test.ts @@ -571,6 +571,80 @@ Reply to this comment using MCP tool task_add_comment. expect(JSON.stringify(result.items)).not.toContain('agent_teams_task_get_response'); }); + it('formats direct task list arrays without leaking raw array fields', () => { + const result = extractMemberLogPreviewItems({ + provider: 'claude_transcript', + maxItems: 3, + textLimit: 220, + messages: [ + message({ + uuid: 'list-call', + timestamp: '2026-04-01T10:00:00.000Z', + content: [ + { + type: 'tool_use', + id: 'tool-list', + name: 'mcp__agent-teams__task_list', + input: { teamName: 'demo' }, + }, + ], + }), + message({ + uuid: 'list-result', + type: 'user', + role: 'user', + timestamp: '2026-04-01T10:01:00.000Z', + content: [ + { + type: 'tool_result', + tool_use_id: 'tool-list', + content: JSON.stringify([ + { + id: '4499fbe5-1fee-42a5-8584-851fbfc4adcd', + displayId: '4499fbe5', + subject: 'Fix contact form route', + status: 'todo', + owner: 'bob', + }, + { + id: '0276a054-1111-4222-8333-444444444444', + displayId: '0276a054', + title: 'High-confidence bug triage', + status: 'in_progress', + owner: 'alice', + }, + { + id: '8a9e766b-1111-4222-8333-444444444444', + displayId: '8a9e766b', + title: 'Follow-up split', + status: 'done', + owner: 'tom', + }, + { + id: '898a6a3e-1111-4222-8333-444444444444', + displayId: '898a6a3e', + title: 'Regression research', + status: 'done', + owner: 'team-lead', + }, + ]), + }, + ], + }), + ], + }); + + expect(result.items[0]).toMatchObject({ + kind: 'tool_result', + title: 'Task list', + preview: + '4 tasks - #4499fbe5: Fix contact form route, status todo, owner bob; #0276a054: High-confidence bug triage, status in_progress, owner alice; #8a9e766b: Follow-up split, status done, owner tom; +1 more', + }); + expect(result.items).toHaveLength(1); + expect(result.items[0]?.preview).not.toContain('displayId'); + expect(result.items[0]?.preview).not.toContain('[{'); + }); + it('formats common board and cross-team tool previews compactly', () => { const result = extractMemberLogPreviewItems({ provider: 'opencode_runtime', diff --git a/src/features/member-log-stream/core/domain/policies/memberLogPreviewExtractor.ts b/src/features/member-log-stream/core/domain/policies/memberLogPreviewExtractor.ts index fad0ef24..dd89cf51 100644 --- a/src/features/member-log-stream/core/domain/policies/memberLogPreviewExtractor.ts +++ b/src/features/member-log-stream/core/domain/policies/memberLogPreviewExtractor.ts @@ -465,7 +465,56 @@ function countArrayField(payload: Record, keys: readonly string return null; } +function formatTaskCollectionItem(task: Record): string | null { + const taskRef = taskRefFromPayload(task); + const taskSummary = shortTaskSummary(task); + if (taskRef && taskSummary) return `${taskRef}: ${taskSummary}`; + if (taskRef) return taskRef; + return taskSummary; +} + +function formatTaskCollectionArrayPayload( + items: readonly unknown[], + canonicalToolNameValue: string | null +): KnownPayloadPreview | null { + const canonical = canonicalToolNameValue ?? ''; + if (canonical !== 'task_list' && canonical !== 'task_briefing') { + return null; + } + + const tasks = items + .map((item) => asRecord(item)) + .filter((item): item is Record => Boolean(item)); + if (tasks.length === 0) { + return { + title: canonical === 'task_briefing' ? 'Task briefing' : 'Task list', + text: '0 tasks', + }; + } + + const taskSummaries = tasks.slice(0, 3).map(formatTaskCollectionItem).filter(Boolean); + const remainingTaskCount = Math.max(0, tasks.length - taskSummaries.length); + const moreText = remainingTaskCount > 0 ? `; +${remainingTaskCount} more` : ''; + const countText = `${tasks.length} ${tasks.length === 1 ? 'task' : 'tasks'}`; + return { + title: canonical === 'task_briefing' ? 'Task briefing' : 'Task list', + text: + taskSummaries.length > 0 + ? `${countText} - ${taskSummaries.join('; ')}${moreText}` + : countText, + }; +} + function formatTaskCollectionPayload(payload: Record): KnownPayloadPreview | null { + for (const key of ['tasks', 'items', 'actionable'] as const) { + const value = payload[key]; + if (!Array.isArray(value)) continue; + const collection = formatTaskCollectionArrayPayload(value, 'task_list'); + if (collection) { + return collection; + } + } + const taskCount = countArrayField(payload, ['tasks', 'items', 'actionable']); const summary = stringField(payload, 'summary') ?? @@ -935,6 +984,10 @@ function previewUnknownValue( if (textBlocks) { return previewUnknownValue(textBlocks, limit, priorityKeys, toolContext); } + const knownCollection = formatTaskCollectionArrayPayload(value, toolContext?.canonicalName); + if (knownCollection) { + return { ...truncatePreview(knownCollection.text, limit), title: knownCollection.title }; + } const parts = value .slice(0, 3) .map((item) => previewUnknownValue(item, limit, priorityKeys, toolContext).preview) diff --git a/src/features/member-log-stream/renderer/ui/ExecutionLogStreamView.tsx b/src/features/member-log-stream/renderer/ui/ExecutionLogStreamView.tsx index 164503fe..bbf90130 100644 --- a/src/features/member-log-stream/renderer/ui/ExecutionLogStreamView.tsx +++ b/src/features/member-log-stream/renderer/ui/ExecutionLogStreamView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { MemberBadge } from '@renderer/components/team/MemberBadge'; import { MemberExecutionLog } from '@renderer/components/team/members/MemberExecutionLog'; @@ -238,6 +238,7 @@ export function ExecutionLogStreamView({ getSegmentMetaLabel, }: Readonly>): React.JSX.Element { const [selectedParticipantKey, setSelectedParticipantKey] = useState('all'); + const appliedSelectionResetKeyRef = useRef(null); const participants = stream?.participants ?? []; const memberColorMap = useMemo(() => buildMemberColorMap([...teamMembers]), [teamMembers]); const participantVisuals = useMemo( @@ -248,8 +249,13 @@ export function ExecutionLogStreamView({ useEffect(() => { if (!stream) { setSelectedParticipantKey('all'); + appliedSelectionResetKeyRef.current = null; return; } + if (appliedSelectionResetKeyRef.current === selectionResetKey) { + return; + } + appliedSelectionResetKeyRef.current = selectionResetKey; setSelectedParticipantKey(stream.defaultFilter); }, [selectionResetKey, stream]); diff --git a/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx b/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx index 3c04faae..93375cb3 100644 --- a/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx +++ b/src/renderer/components/runtime/ProviderRuntimeSettingsDialog.tsx @@ -21,6 +21,7 @@ import { } from '@features/codex-runtime-profile/renderer'; import { RuntimeProviderManagementPanel } from '@features/runtime-provider-management/renderer'; import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo'; +import { api } from '@renderer/api'; import { Button } from '@renderer/components/ui/button'; import { Dialog, diff --git a/test/features/codex-account/main/createCodexAccountFeature.test.ts b/test/features/codex-account/main/createCodexAccountFeature.test.ts index 3e2d3a44..e6926f33 100644 --- a/test/features/codex-account/main/createCodexAccountFeature.test.ts +++ b/test/features/codex-account/main/createCodexAccountFeature.test.ts @@ -41,7 +41,7 @@ const { error: null as string | null, startedAt: null as string | null, authUrl: null as string | null, - }, + } as CodexLoginStateDto, }, loginStateListeners: new Set<() => void>(), loginSettledListeners: new Set<() => void>(), @@ -242,6 +242,7 @@ describe('createCodexAccountFeature', () => { status: 'idle', error: null, startedAt: null, + authUrl: null, }; loginStateListeners.clear(); loginSettledListeners.clear(); diff --git a/test/renderer/features/agent-graph/GraphMemberLogPreviewHud.test.tsx b/test/renderer/features/agent-graph/GraphMemberLogPreviewHud.test.tsx index a438bf52..1a911c8d 100644 --- a/test/renderer/features/agent-graph/GraphMemberLogPreviewHud.test.tsx +++ b/test/renderer/features/agent-graph/GraphMemberLogPreviewHud.test.tsx @@ -5,8 +5,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { GraphMemberLogPreviewHud } from '@features/agent-graph/renderer/ui/GraphMemberLogPreviewHud'; import type { GraphNode } from '@claude-teams/agent-graph'; +import type { MemberLogPreviewMember } from '@features/member-log-stream/contracts/dto'; -const basePreviewsByMember = new Map([ +const basePreviewsByMember = new Map([ [ 'team-lead', { @@ -329,4 +330,89 @@ describe('GraphMemberLogPreviewHud', () => { root.unmount(); }); }); + + it('keeps compact event text readable without repeating the title prefix', async () => { + mockedPreviewsByMember = new Map([ + [ + 'alice', + { + memberName: 'alice', + items: [ + { + id: 'message-sent-preview', + kind: 'tool_result', + provider: 'claude_transcript', + timestamp: '2026-04-03T00:01:00.000Z', + title: 'Message sent', + preview: 'Message sent to team-lead - #abc done', + tone: 'success', + }, + { + id: 'generic-tool-result-preview', + kind: 'tool_result', + provider: 'claude_transcript', + timestamp: '2026-04-03T00:00:50.000Z', + title: 'Tool result', + preview: 'stored', + tone: 'success', + }, + ], + coverage: [{ provider: 'claude_transcript', status: 'included' }], + warnings: [], + truncated: false, + overflowCount: 0, + generatedAt: '2026-04-03T00:01:00.000Z', + }, + ], + ]); + const node: GraphNode = { + id: 'member:alpha-team:alice', + kind: 'member', + label: 'alice', + state: 'active', + domainRef: { kind: 'member', teamName: 'alpha-team', memberName: 'alice' }, + }; + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render( + ({ + left: 40, + top: 80, + right: 300, + bottom: 372, + width: 260, + height: 292, + })} + getCameraZoom={() => 1} + worldToScreen={(x, y) => ({ x, y })} + getViewportSize={() => ({ width: 1200, height: 800 })} + focusNodeIds={null} + /> + ); + await Promise.resolve(); + }); + + const messageRow = Array.from(host.querySelectorAll('button')).find((button) => + button.textContent?.includes('#abc done') + ); + expect(messageRow?.textContent).toContain('Message sent'); + expect(messageRow?.textContent).toContain('to team-lead - #abc done'); + expect(messageRow?.textContent).not.toContain('Message sentMessage sent'); + expect(messageRow?.textContent).not.toContain('Message sent now Message sent'); + + const genericResultRow = Array.from(host.querySelectorAll('button')).find((button) => + button.textContent?.includes('stored') + ); + expect(genericResultRow?.textContent).toContain('Tool result'); + + act(() => { + root.unmount(); + }); + }); }); diff --git a/test/renderer/features/agent-graph/useGraphMemberLogPreviews.test.tsx b/test/renderer/features/agent-graph/useGraphMemberLogPreviews.test.tsx index 66aefaeb..4bb05590 100644 --- a/test/renderer/features/agent-graph/useGraphMemberLogPreviews.test.tsx +++ b/test/renderer/features/agent-graph/useGraphMemberLogPreviews.test.tsx @@ -57,6 +57,31 @@ function response(memberName: string, generatedAt: string): MemberLogPreviewResp }; } +function batchResponse(memberNames: string[], generatedAt: string): MemberLogPreviewResponse { + return { + generatedAt, + members: memberNames.map((memberName) => ({ + memberName, + items: [ + { + id: `${memberName}:${generatedAt}`, + kind: 'text', + provider: 'claude_transcript', + timestamp: generatedAt, + title: 'Assistant', + preview: memberName, + tone: 'neutral', + }, + ], + coverage: [{ provider: 'claude_transcript', status: 'included' }], + warnings: [], + truncated: false, + overflowCount: 0, + generatedAt, + })), + }; +} + const HookProbe = ({ teamName, memberNames, @@ -250,6 +275,65 @@ describe('useGraphMemberLogPreviews', () => { }); }); + it('does not duplicate preview requests when the same visible members are reordered', async () => { + const firstLoad = createDeferred(); + apiMock.memberLogStream.getMemberLogPreviews.mockReturnValueOnce(firstLoad.promise); + + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render( + undefined} /> + ); + await Promise.resolve(); + }); + await act(async () => { + vi.advanceTimersByTime(700); + await Promise.resolve(); + }); + expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); + expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenLastCalledWith( + 'alpha-team', + ['alice', 'bob'], + expect.any(Object) + ); + + await act(async () => { + root.render( + undefined} /> + ); + await Promise.resolve(); + }); + await act(async () => { + vi.advanceTimersByTime(700); + await Promise.resolve(); + }); + expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); + + await act(async () => { + firstLoad.resolve(batchResponse(['alice', 'bob'], '2026-04-03T00:00:00.000Z')); + await Promise.resolve(); + }); + + await act(async () => { + root.render( + undefined} /> + ); + await Promise.resolve(); + }); + await act(async () => { + vi.advanceTimersByTime(700); + await Promise.resolve(); + }); + expect(apiMock.memberLogStream.getMemberLogPreviews).toHaveBeenCalledTimes(1); + + act(() => { + root.unmount(); + }); + }); + it('reloads visible members on log-source events with force refresh', async () => { let teamChangeListener: | ((event: unknown, data: { teamName: string; type: string }) => void) diff --git a/test/renderer/features/agent-graph/useGraphSimulation.test.ts b/test/renderer/features/agent-graph/useGraphSimulation.test.ts index a921e312..d9a16524 100644 --- a/test/renderer/features/agent-graph/useGraphSimulation.test.ts +++ b/test/renderer/features/agent-graph/useGraphSimulation.test.ts @@ -316,6 +316,9 @@ describe('stable slot layout planner', () => { }); expect(snapshot).not.toBeNull(); + if (!snapshot) { + throw new Error('Expected stable slot layout snapshot'); + } expect(validateStableSlotLayout(snapshot!)).toEqual({ valid: true }); for (const frame of snapshot.memberSlotFrames) { @@ -429,6 +432,9 @@ describe('stable slot layout planner', () => { expect(snapshot).not.toBeNull(); expect(frame).toBeDefined(); expect(footprint).toBeDefined(); + if (!snapshot || !frame || !footprint) { + throw new Error('Expected stable slot frame and footprint'); + } const legacyHorizontalExtent = snapshot.runtimeCentralExclusion.right; const legacyVerticalExtent = Math.abs(snapshot.runtimeCentralExclusion.top); @@ -990,6 +996,9 @@ describe('stable slot layout planner', () => { }); expect(snapshot).not.toBeNull(); + if (!snapshot) { + throw new Error('Expected stable slot layout snapshot'); + } const targetFrame = snapshot.memberSlotFrames[1]; expect(