fix(team): stabilize graph preview validation
This commit is contained in:
parent
b9f82f8162
commit
30a6e36976
12 changed files with 224 additions and 13 deletions
|
|
@ -33,6 +33,7 @@ export interface GraphConfigPort {
|
||||||
|
|
||||||
// ─── Filters (show/hide node kinds) ────────────────────────────────────
|
// ─── Filters (show/hide node kinds) ────────────────────────────────────
|
||||||
showActivity?: boolean;
|
showActivity?: boolean;
|
||||||
|
showLogs?: boolean;
|
||||||
showTasks?: boolean;
|
showTasks?: boolean;
|
||||||
showProcesses?: boolean;
|
showProcesses?: boolean;
|
||||||
showCompletedTasks?: boolean;
|
showCompletedTasks?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
Activity,
|
Activity,
|
||||||
Columns3,
|
Columns3,
|
||||||
Expand,
|
Expand,
|
||||||
|
FileText,
|
||||||
Settings2,
|
Settings2,
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
|
|
@ -29,6 +30,7 @@ import type { GraphLayoutMode } from '../ports/types';
|
||||||
|
|
||||||
export interface GraphFilterState {
|
export interface GraphFilterState {
|
||||||
showActivity: boolean;
|
showActivity: boolean;
|
||||||
|
showLogs: boolean;
|
||||||
showTasks: boolean;
|
showTasks: boolean;
|
||||||
showProcesses: boolean;
|
showProcesses: boolean;
|
||||||
showEdges: boolean;
|
showEdges: boolean;
|
||||||
|
|
@ -269,6 +271,13 @@ export function GraphControls({
|
||||||
label="Activity"
|
label="Activity"
|
||||||
block
|
block
|
||||||
/>
|
/>
|
||||||
|
<ToolbarToggle
|
||||||
|
active={filters.showLogs}
|
||||||
|
onClick={() => toggle('showLogs')}
|
||||||
|
icon={<FileText size={13} />}
|
||||||
|
label="Logs"
|
||||||
|
block
|
||||||
|
/>
|
||||||
<ToolbarToggle
|
<ToolbarToggle
|
||||||
active={filters.showTasks}
|
active={filters.showTasks}
|
||||||
onClick={() => toggle('showTasks')}
|
onClick={() => toggle('showTasks')}
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,7 @@ export function GraphView({
|
||||||
const [interactionLocked, setInteractionLocked] = useState(false);
|
const [interactionLocked, setInteractionLocked] = useState(false);
|
||||||
const [filters, setFilters] = useState<GraphFilterState>({
|
const [filters, setFilters] = useState<GraphFilterState>({
|
||||||
showActivity: config?.showActivity ?? true,
|
showActivity: config?.showActivity ?? true,
|
||||||
|
showLogs: config?.showLogs ?? config?.showActivity ?? true,
|
||||||
showTasks: config?.showTasks ?? true,
|
showTasks: config?.showTasks ?? true,
|
||||||
showProcesses: config?.showProcesses ?? true,
|
showProcesses: config?.showProcesses ?? true,
|
||||||
showEdges: true,
|
showEdges: true,
|
||||||
|
|
@ -138,10 +139,10 @@ export function GraphView({
|
||||||
? {
|
? {
|
||||||
...data.layout,
|
...data.layout,
|
||||||
showActivity: filters.showActivity,
|
showActivity: filters.showActivity,
|
||||||
showLogs: filters.showActivity,
|
showLogs: filters.showLogs,
|
||||||
}
|
}
|
||||||
: data.layout,
|
: data.layout,
|
||||||
[data.layout, filters.showActivity]
|
[data.layout, filters.showActivity, filters.showLogs]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ref mirror of selectedNodeId — read by RAF loop to avoid recreating animate on selection change
|
// Ref mirror of selectedNodeId — read by RAF loop to avoid recreating animate on selection change
|
||||||
|
|
|
||||||
|
|
@ -416,7 +416,7 @@ export const GraphMemberLogPreviewHud = ({
|
||||||
key={item.id}
|
key={item.id}
|
||||||
type="button"
|
type="button"
|
||||||
className={[
|
className={[
|
||||||
'block h-14 min-h-14 w-full min-w-0 overflow-hidden rounded-md border px-2.5 py-1.5 text-left text-slate-400 transition-[border-color,background-color,box-shadow] duration-500 hover:border-white/20 hover:bg-[rgba(12,20,40,0.78)]',
|
'block h-16 min-h-16 w-full min-w-0 overflow-hidden rounded-md border px-2.5 py-1.5 text-left text-slate-400 transition-[border-color,background-color,box-shadow] duration-500 hover:border-white/20 hover:bg-[rgba(12,20,40,0.78)]',
|
||||||
isHighlighted
|
isHighlighted
|
||||||
? 'border-sky-300/70 bg-[rgba(14,34,62,0.74)] shadow-[0_0_0_1px_rgba(125,211,252,0.30),0_0_18px_rgba(56,189,248,0.22)]'
|
? 'border-sky-300/70 bg-[rgba(14,34,62,0.74)] shadow-[0_0_0_1px_rgba(125,211,252,0.30),0_0_18px_rgba(56,189,248,0.22)]'
|
||||||
: 'border-white/10 bg-[rgba(8,14,28,0.52)]',
|
: 'border-white/10 bg-[rgba(8,14,28,0.52)]',
|
||||||
|
|
@ -430,15 +430,15 @@ export const GraphMemberLogPreviewHud = ({
|
||||||
>
|
>
|
||||||
{itemIcon(item)}
|
{itemIcon(item)}
|
||||||
</span>
|
</span>
|
||||||
<span className="align-top text-[11px] font-medium leading-5 text-slate-200">
|
<span className="align-top text-[11px] font-medium leading-4 text-slate-200">
|
||||||
{displayTitle}
|
{displayTitle}
|
||||||
</span>
|
</span>
|
||||||
{relativeTime ? (
|
{relativeTime ? (
|
||||||
<span className="ml-1 align-top text-[9px] font-normal leading-5 text-slate-500">
|
<span className="ml-1 align-top text-[9px] font-normal leading-4 text-slate-500">
|
||||||
{relativeTime}
|
{relativeTime}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
<span className="ml-1 break-words align-top text-[10px] leading-5 text-slate-300/85">
|
<span className="ml-1 break-words align-top text-[10px] leading-4 text-slate-300/85">
|
||||||
{previewText}
|
{previewText}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -494,7 +494,7 @@ export const GraphMemberLogPreviewHud = ({
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
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"
|
className="flex h-16 min-h-16 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)}
|
onClick={() => openLogs(memberName)}
|
||||||
>
|
>
|
||||||
{resolveEmptyText(preview, loading, error)}
|
{resolveEmptyText(preview, loading, error)}
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,7 @@ export const TeamGraphOverlay = ({
|
||||||
worldToScreen={extraHudProps.worldToScreen}
|
worldToScreen={extraHudProps.worldToScreen}
|
||||||
getViewportSize={getViewportSize}
|
getViewportSize={getViewportSize}
|
||||||
focusNodeIds={focusNodeIds}
|
focusNodeIds={focusNodeIds}
|
||||||
enabled={filters?.showActivity ?? true}
|
enabled={filters?.showLogs ?? true}
|
||||||
onOpenMemberProfile={onOpenMemberProfile}
|
onOpenMemberProfile={onOpenMemberProfile}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -224,7 +224,7 @@ export const TeamGraphTab = ({
|
||||||
worldToScreen={extraHudProps.worldToScreen}
|
worldToScreen={extraHudProps.worldToScreen}
|
||||||
getViewportSize={getViewportSize}
|
getViewportSize={getViewportSize}
|
||||||
focusNodeIds={focusNodeIds}
|
focusNodeIds={focusNodeIds}
|
||||||
enabled={isActive && (filters?.showActivity ?? true)}
|
enabled={isActive && (filters?.showLogs ?? true)}
|
||||||
onOpenMemberProfile={dispatchOpenProfile}
|
onOpenMemberProfile={dispatchOpenProfile}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -645,6 +645,62 @@ Reply to this comment using MCP tool task_add_comment.
|
||||||
expect(result.items[0]?.preview).not.toContain('[{');
|
expect(result.items[0]?.preview).not.toContain('[{');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('formats sourceToolUseID result wrappers with text-block content arrays', () => {
|
||||||
|
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: 'source-result',
|
||||||
|
type: 'user',
|
||||||
|
role: 'user',
|
||||||
|
timestamp: '2026-04-01T10:01:00.000Z',
|
||||||
|
content: '',
|
||||||
|
sourceToolUseID: 'tool-list',
|
||||||
|
toolUseResult: {
|
||||||
|
toolUseId: 'tool-list',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify([
|
||||||
|
{
|
||||||
|
id: '4499fbe5-1fee-42a5-8584-851fbfc4adcd',
|
||||||
|
displayId: '4499fbe5',
|
||||||
|
subject: 'Fix contact form route',
|
||||||
|
status: 'todo',
|
||||||
|
owner: 'bob',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.items[0]).toMatchObject({
|
||||||
|
kind: 'tool_result',
|
||||||
|
title: 'Task list',
|
||||||
|
preview: '1 task - #4499fbe5: Fix contact form route, status todo, owner bob',
|
||||||
|
});
|
||||||
|
expect(result.items[0]?.preview).not.toContain('toolUseId');
|
||||||
|
expect(result.items[0]?.preview).not.toContain('content');
|
||||||
|
});
|
||||||
|
|
||||||
it('formats common board and cross-team tool previews compactly', () => {
|
it('formats common board and cross-team tool previews compactly', () => {
|
||||||
const result = extractMemberLogPreviewItems({
|
const result = extractMemberLogPreviewItems({
|
||||||
provider: 'opencode_runtime',
|
provider: 'opencode_runtime',
|
||||||
|
|
|
||||||
|
|
@ -240,8 +240,10 @@ function recordFromUnknownWithWrapper(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof record.content === 'string') {
|
const nestedContent =
|
||||||
const nested = recordFromUnknownWithWrapper(record.content);
|
typeof record.content === 'string' ? record.content : textFromTextContentBlocks(record.content);
|
||||||
|
if (nestedContent) {
|
||||||
|
const nested = recordFromUnknownWithWrapper(nestedContent);
|
||||||
if (nested) {
|
if (nested) {
|
||||||
return nested;
|
return nested;
|
||||||
}
|
}
|
||||||
|
|
@ -984,7 +986,10 @@ function previewUnknownValue(
|
||||||
if (textBlocks) {
|
if (textBlocks) {
|
||||||
return previewUnknownValue(textBlocks, limit, priorityKeys, toolContext);
|
return previewUnknownValue(textBlocks, limit, priorityKeys, toolContext);
|
||||||
}
|
}
|
||||||
const knownCollection = formatTaskCollectionArrayPayload(value, toolContext?.canonicalName);
|
const knownCollection = formatTaskCollectionArrayPayload(
|
||||||
|
value,
|
||||||
|
toolContext?.canonicalName ?? null
|
||||||
|
);
|
||||||
if (knownCollection) {
|
if (knownCollection) {
|
||||||
return { ...truncatePreview(knownCollection.text, limit), title: knownCollection.title };
|
return { ...truncatePreview(knownCollection.text, limit), title: knownCollection.title };
|
||||||
}
|
}
|
||||||
|
|
@ -1000,6 +1005,13 @@ function previewUnknownValue(
|
||||||
if (known) {
|
if (known) {
|
||||||
return { ...truncatePreview(known.text, limit), title: known.title };
|
return { ...truncatePreview(known.text, limit), title: known.title };
|
||||||
}
|
}
|
||||||
|
for (const key of ['content', 'message', 'result'] as const) {
|
||||||
|
if (!(key in record)) continue;
|
||||||
|
const nested = previewUnknownValue(record[key], limit, priorityKeys, toolContext);
|
||||||
|
if (nested.preview.trim().length > 0) {
|
||||||
|
return nested;
|
||||||
|
}
|
||||||
|
}
|
||||||
const priority = findPriorityValue(record, priorityKeys);
|
const priority = findPriorityValue(record, priorityKeys);
|
||||||
if (priority) {
|
if (priority) {
|
||||||
return truncatePreview(priority, limit);
|
return truncatePreview(priority, limit);
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ describe('GraphControls', () => {
|
||||||
React.createElement(GraphControls, {
|
React.createElement(GraphControls, {
|
||||||
filters: {
|
filters: {
|
||||||
showActivity: true,
|
showActivity: true,
|
||||||
|
showLogs: true,
|
||||||
showTasks: true,
|
showTasks: true,
|
||||||
showProcesses: true,
|
showProcesses: true,
|
||||||
showEdges: true,
|
showEdges: true,
|
||||||
|
|
@ -90,6 +91,7 @@ describe('GraphControls', () => {
|
||||||
React.createElement(GraphControls, {
|
React.createElement(GraphControls, {
|
||||||
filters: {
|
filters: {
|
||||||
showActivity: true,
|
showActivity: true,
|
||||||
|
showLogs: true,
|
||||||
showTasks: true,
|
showTasks: true,
|
||||||
showProcesses: true,
|
showProcesses: true,
|
||||||
showEdges: true,
|
showEdges: true,
|
||||||
|
|
@ -126,6 +128,7 @@ describe('GraphControls', () => {
|
||||||
React.createElement(GraphControls, {
|
React.createElement(GraphControls, {
|
||||||
filters: {
|
filters: {
|
||||||
showActivity: true,
|
showActivity: true,
|
||||||
|
showLogs: true,
|
||||||
showTasks: true,
|
showTasks: true,
|
||||||
showProcesses: true,
|
showProcesses: true,
|
||||||
showEdges: true,
|
showEdges: true,
|
||||||
|
|
@ -161,6 +164,67 @@ describe('GraphControls', () => {
|
||||||
|
|
||||||
expect(onFiltersChange).toHaveBeenCalledWith({
|
expect(onFiltersChange).toHaveBeenCalledWith({
|
||||||
showActivity: false,
|
showActivity: false,
|
||||||
|
showLogs: true,
|
||||||
|
showTasks: true,
|
||||||
|
showProcesses: true,
|
||||||
|
showEdges: true,
|
||||||
|
paused: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toggles log preview visibility from graph settings independently of activity', async () => {
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
const onFiltersChange = vi.fn();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(GraphControls, {
|
||||||
|
filters: {
|
||||||
|
showActivity: true,
|
||||||
|
showLogs: true,
|
||||||
|
showTasks: true,
|
||||||
|
showProcesses: true,
|
||||||
|
showEdges: true,
|
||||||
|
paused: false,
|
||||||
|
},
|
||||||
|
onFiltersChange,
|
||||||
|
onZoomIn: vi.fn(),
|
||||||
|
onZoomOut: vi.fn(),
|
||||||
|
onZoomToFit: vi.fn(),
|
||||||
|
teamName: 'demo-team',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
const settingsButton = host.querySelector('button[aria-label="Graph settings"]');
|
||||||
|
expect(settingsButton).not.toBeNull();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
settingsButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
const logsButton = Array.from(host.querySelectorAll('button')).find((button) =>
|
||||||
|
button.textContent?.includes('Logs')
|
||||||
|
);
|
||||||
|
expect(logsButton).not.toBeUndefined();
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
logsButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onFiltersChange).toHaveBeenCalledWith({
|
||||||
|
showActivity: true,
|
||||||
|
showLogs: false,
|
||||||
showTasks: true,
|
showTasks: true,
|
||||||
showProcesses: true,
|
showProcesses: true,
|
||||||
showEdges: true,
|
showEdges: true,
|
||||||
|
|
@ -184,6 +248,7 @@ describe('GraphControls', () => {
|
||||||
React.createElement(GraphControls, {
|
React.createElement(GraphControls, {
|
||||||
filters: {
|
filters: {
|
||||||
showActivity: true,
|
showActivity: true,
|
||||||
|
showLogs: true,
|
||||||
showTasks: true,
|
showTasks: true,
|
||||||
showProcesses: true,
|
showProcesses: true,
|
||||||
showEdges: true,
|
showEdges: true,
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,8 @@ describe('GraphMemberLogPreviewHud', () => {
|
||||||
expect(row).not.toBeUndefined();
|
expect(row).not.toBeUndefined();
|
||||||
expect(row?.querySelector('.float-left')).not.toBeNull();
|
expect(row?.querySelector('.float-left')).not.toBeNull();
|
||||||
expect(row?.querySelector('.line-clamp-3')).toBeNull();
|
expect(row?.querySelector('.line-clamp-3')).toBeNull();
|
||||||
|
expect(row?.className).toContain('h-16');
|
||||||
|
expect(row?.querySelector('span.text-slate-200')?.className).toContain('leading-4');
|
||||||
expect(row?.textContent).toContain('pnpm test');
|
expect(row?.textContent).toContain('pnpm test');
|
||||||
|
|
||||||
const errorRow = Array.from(host.querySelectorAll('button')).find((button) =>
|
const errorRow = Array.from(host.querySelectorAll('button')).find((button) =>
|
||||||
|
|
|
||||||
|
|
@ -513,7 +513,7 @@ describe('GraphView pan interactions', () => {
|
||||||
expect(onOwnerSlotDrop).not.toHaveBeenCalled();
|
expect(onOwnerSlotDrop).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes activity filter state to renderHud and updates it through graph controls', async () => {
|
it('passes activity and log filter state to renderHud and updates it through graph controls', async () => {
|
||||||
const renderHud = vi.fn(() => null);
|
const renderHud = vi.fn(() => null);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
|
|
@ -539,6 +539,7 @@ describe('GraphView pan interactions', () => {
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
filters: expect.objectContaining({
|
filters: expect.objectContaining({
|
||||||
showActivity: false,
|
showActivity: false,
|
||||||
|
showLogs: false,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -546,6 +547,7 @@ describe('GraphView pan interactions', () => {
|
||||||
const controlsProps = hoisted.graphControlsProps as {
|
const controlsProps = hoisted.graphControlsProps as {
|
||||||
filters: {
|
filters: {
|
||||||
showActivity: boolean;
|
showActivity: boolean;
|
||||||
|
showLogs: boolean;
|
||||||
showTasks: boolean;
|
showTasks: boolean;
|
||||||
showProcesses: boolean;
|
showProcesses: boolean;
|
||||||
showEdges: boolean;
|
showEdges: boolean;
|
||||||
|
|
@ -553,6 +555,7 @@ describe('GraphView pan interactions', () => {
|
||||||
};
|
};
|
||||||
onFiltersChange: (filters: {
|
onFiltersChange: (filters: {
|
||||||
showActivity: boolean;
|
showActivity: boolean;
|
||||||
|
showLogs: boolean;
|
||||||
showTasks: boolean;
|
showTasks: boolean;
|
||||||
showProcesses: boolean;
|
showProcesses: boolean;
|
||||||
showEdges: boolean;
|
showEdges: boolean;
|
||||||
|
|
@ -573,6 +576,25 @@ describe('GraphView pan interactions', () => {
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
filters: expect.objectContaining({
|
filters: expect.objectContaining({
|
||||||
showActivity: true,
|
showActivity: true,
|
||||||
|
showLogs: false,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
controlsProps?.onFiltersChange({
|
||||||
|
...controlsProps!.filters,
|
||||||
|
showActivity: true,
|
||||||
|
showLogs: true,
|
||||||
|
});
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(renderHud).toHaveBeenLastCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
filters: expect.objectContaining({
|
||||||
|
showActivity: true,
|
||||||
|
showLogs: true,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -408,6 +408,49 @@ describe('stable slot layout planner', () => {
|
||||||
expect(frame?.kanbanBandRect.left).toBe(frame?.boardBandRect.left);
|
expect(frame?.kanbanBandRect.left).toBe(frame?.boardBandRect.left);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps the reserved log column when logs are shown and activity is hidden', () => {
|
||||||
|
const teamName = 'team-logs-without-activity-slot';
|
||||||
|
const lead = createLead(teamName);
|
||||||
|
const alice = createMember(teamName, 'agent-alice', 'alice');
|
||||||
|
const layout: GraphLayoutPort = {
|
||||||
|
version: 'stable-slots-v1',
|
||||||
|
showActivity: false,
|
||||||
|
showLogs: true,
|
||||||
|
ownerOrder: [alice.id],
|
||||||
|
slotAssignments: {
|
||||||
|
[alice.id]: { ringIndex: 0, sectorIndex: 1 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const [footprint] = computeOwnerFootprints([lead, alice], layout);
|
||||||
|
const snapshot = buildStableSlotLayoutSnapshot({
|
||||||
|
teamName,
|
||||||
|
nodes: [lead, alice],
|
||||||
|
layout,
|
||||||
|
});
|
||||||
|
const frame = snapshot?.memberSlotFrames[0];
|
||||||
|
|
||||||
|
expect(footprint).toBeDefined();
|
||||||
|
expect(footprint?.activityColumnWidth).toBe(0);
|
||||||
|
expect(footprint?.activityColumnHeight).toBe(0);
|
||||||
|
expect(footprint?.logColumnWidth).toBe(260);
|
||||||
|
expect(footprint?.logColumnHeight).toBe(
|
||||||
|
ACTIVITY_LANE.headerHeight +
|
||||||
|
ACTIVITY_LANE.maxVisibleItems * ACTIVITY_LANE.rowHeight +
|
||||||
|
ACTIVITY_LANE.overflowHeight
|
||||||
|
);
|
||||||
|
expect(snapshot).not.toBeNull();
|
||||||
|
expect(validateStableSlotLayout(snapshot!)).toEqual({ valid: true });
|
||||||
|
expect(frame?.activityColumnRect.width).toBe(0);
|
||||||
|
expect(frame?.activityColumnRect.height).toBe(0);
|
||||||
|
expect(frame?.logColumnRect.width).toBe(260);
|
||||||
|
expect(frame?.logColumnRect.height).toBe(
|
||||||
|
ACTIVITY_LANE.headerHeight +
|
||||||
|
ACTIVITY_LANE.maxVisibleItems * ACTIVITY_LANE.rowHeight +
|
||||||
|
ACTIVITY_LANE.overflowHeight
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps diagonal ring-zero sectors closer than the legacy coarse central box radius', () => {
|
it('keeps diagonal ring-zero sectors closer than the legacy coarse central box radius', () => {
|
||||||
const teamName = 'team-directional-radius';
|
const teamName = 'team-directional-radius';
|
||||||
const lead = createLead(teamName);
|
const lead = createLead(teamName);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue