feat(FN-2598): merge fusion/fn-2598 (auto-resolved)
- test(FN-2598): complete Step 3 — update collapsed model header coverage - fix(FN-2598): normalize expand button spacing token - feat(FN-2598): complete Step 2 — style compact model header - feat(FN-2598): complete Step 1 — compact model header structure
This commit is contained in:
@@ -4,7 +4,7 @@ import { useRef, useEffect, useState, useCallback, useLayoutEffect, useMemo } fr
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import type { Components } from "react-markdown";
|
||||
import { Maximize2, Minimize2, Loader2 } from "lucide-react";
|
||||
import { Maximize2, Minimize2, Loader2, ChevronDown, ChevronRight } from "lucide-react";
|
||||
|
||||
function formatTimestamp(iso: string): string {
|
||||
const date = new Date(iso);
|
||||
@@ -127,6 +127,7 @@ export function AgentLogViewer({
|
||||
const previousNewestEntryKeyRef = useRef<string | null>(null);
|
||||
const [renderMarkdown, setRenderMarkdown] = useState(true);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [modelHeaderExpanded, setModelHeaderExpanded] = useState(false);
|
||||
|
||||
const chronologicalEntryKeys = useMemo(
|
||||
() => buildEntryRenderKeys(entries),
|
||||
@@ -208,6 +209,39 @@ export function AgentLogViewer({
|
||||
const hasValidatorOverride = validatorModel?.provider && validatorModel?.modelId;
|
||||
const hasPlanningOverride = planningModel?.provider && planningModel?.modelId;
|
||||
|
||||
const modelProviders = useMemo(() => {
|
||||
const providers: Array<{ role: string; provider: string; modelId?: string }> = [];
|
||||
if (hasExecutorOverride) {
|
||||
providers.push({
|
||||
role: "Executor",
|
||||
provider: executorModel!.provider!,
|
||||
modelId: executorModel!.modelId,
|
||||
});
|
||||
}
|
||||
if (hasValidatorOverride) {
|
||||
providers.push({
|
||||
role: "Validator",
|
||||
provider: validatorModel!.provider!,
|
||||
modelId: validatorModel!.modelId,
|
||||
});
|
||||
}
|
||||
if (hasPlanningOverride) {
|
||||
providers.push({
|
||||
role: "Planning",
|
||||
provider: planningModel!.provider!,
|
||||
modelId: planningModel!.modelId,
|
||||
});
|
||||
}
|
||||
return providers;
|
||||
}, [
|
||||
hasExecutorOverride,
|
||||
executorModel,
|
||||
hasValidatorOverride,
|
||||
validatorModel,
|
||||
hasPlanningOverride,
|
||||
planningModel,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
@@ -216,39 +250,26 @@ export function AgentLogViewer({
|
||||
>
|
||||
{/* Model info header */}
|
||||
<div className="agent-log-model-header" data-testid="agent-log-model-header">
|
||||
<div className="agent-log-model-group">
|
||||
<span className="agent-log-model-label">Executor:</span>
|
||||
{hasExecutorOverride ? (
|
||||
<span className="agent-log-model-value">
|
||||
<ProviderIcon provider={executorModel.provider!} size="sm" />
|
||||
<span>{executorModel.provider}/{executorModel.modelId}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="model-badge-default">Using default</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="agent-log-model-group">
|
||||
<span className="agent-log-model-label">Validator:</span>
|
||||
{hasValidatorOverride ? (
|
||||
<span className="agent-log-model-value">
|
||||
<ProviderIcon provider={validatorModel.provider!} size="sm" />
|
||||
<span>{validatorModel.provider}/{validatorModel.modelId}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="model-badge-default">Using default</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="agent-log-model-group">
|
||||
<span className="agent-log-model-label">Planning:</span>
|
||||
{hasPlanningOverride ? (
|
||||
<span className="agent-log-model-value">
|
||||
<ProviderIcon provider={planningModel.provider!} size="sm" />
|
||||
<span>{planningModel.provider}/{planningModel.modelId}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="model-badge-default">Using default</span>
|
||||
)}
|
||||
<div className="agent-log-model-icons">
|
||||
{modelProviders.map((modelProvider) => (
|
||||
<ProviderIcon
|
||||
key={`${modelProvider.role}-${modelProvider.provider}-${modelProvider.modelId ?? "default"}`}
|
||||
provider={modelProvider.provider}
|
||||
size="sm"
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
className="agent-log-model-expand-btn"
|
||||
onClick={() => setModelHeaderExpanded((prev) => !prev)}
|
||||
aria-label={modelHeaderExpanded ? "Collapse model details" : "Expand model details"}
|
||||
aria-expanded={modelHeaderExpanded}
|
||||
aria-controls="agent-log-model-details"
|
||||
data-testid="agent-log-model-expand"
|
||||
>
|
||||
{modelHeaderExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Markdown render toggle */}
|
||||
<div className="agent-log-model-header-toggle">
|
||||
<button
|
||||
@@ -271,6 +292,44 @@ export function AgentLogViewer({
|
||||
{isFullscreen ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{modelHeaderExpanded && (
|
||||
<div id="agent-log-model-details" className="agent-log-model-details">
|
||||
<div className="agent-log-model-group">
|
||||
<span className="agent-log-model-label">Executor:</span>
|
||||
{hasExecutorOverride ? (
|
||||
<span className="agent-log-model-value">
|
||||
<ProviderIcon provider={executorModel.provider!} size="sm" />
|
||||
<span>{executorModel.provider}/{executorModel.modelId}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="model-badge-default">Using default</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="agent-log-model-group">
|
||||
<span className="agent-log-model-label">Validator:</span>
|
||||
{hasValidatorOverride ? (
|
||||
<span className="agent-log-model-value">
|
||||
<ProviderIcon provider={validatorModel.provider!} size="sm" />
|
||||
<span>{validatorModel.provider}/{validatorModel.modelId}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="model-badge-default">Using default</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="agent-log-model-group">
|
||||
<span className="agent-log-model-label">Planning/Triage:</span>
|
||||
{hasPlanningOverride ? (
|
||||
<span className="agent-log-model-value">
|
||||
<ProviderIcon provider={planningModel.provider!} size="sm" />
|
||||
<span>{planningModel.provider}/{planningModel.modelId}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="model-badge-default">Using default</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pagination summary */}
|
||||
|
||||
@@ -251,16 +251,56 @@
|
||||
.agent-log-model-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 12px;
|
||||
gap: var(--space-lg);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
margin-bottom: var(--space-md);
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.agent-log-model-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.agent-log-model-expand-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-xs);
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
.agent-log-model-expand-btn:hover {
|
||||
color: var(--text);
|
||||
background: var(--card-hover);
|
||||
}
|
||||
|
||||
.agent-log-model-expand-btn:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring-strong);
|
||||
}
|
||||
|
||||
.agent-log-model-details {
|
||||
display: flex;
|
||||
flex-basis: 100%;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-lg);
|
||||
padding-top: var(--space-sm);
|
||||
margin-top: var(--space-sm);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.agent-log-model-group {
|
||||
|
||||
@@ -7,7 +7,10 @@ import type { AgentLogEntry } from "@fusion/core";
|
||||
vi.mock("lucide-react", () => ({
|
||||
Maximize2: () => null,
|
||||
Minimize2: () => null,
|
||||
Loader2: () => null,
|
||||
Cpu: () => null,
|
||||
ChevronDown: () => null,
|
||||
ChevronRight: () => null,
|
||||
}));
|
||||
|
||||
function makeEntry(overrides: Partial<AgentLogEntry> = {}): AgentLogEntry {
|
||||
@@ -275,38 +278,34 @@ describe("AgentLogViewer", () => {
|
||||
entries={entries}
|
||||
loading={false}
|
||||
executorModel={{ provider: "anthropic", modelId: "claude-sonnet-4-5" }}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
expect(container.querySelector('[data-provider="anthropic"]')).toBeTruthy();
|
||||
expect(header!.textContent).not.toContain("Executor:");
|
||||
|
||||
fireEvent.click(screen.getByTestId("agent-log-model-expand"));
|
||||
expect(header!.textContent).toContain("Executor:");
|
||||
expect(header!.textContent).toContain("anthropic/claude-sonnet-4-5");
|
||||
expect(header!.textContent).toContain("Validator:");
|
||||
expect(header!.textContent).toContain("Using default");
|
||||
// Verify ProviderIcon is rendered for executor
|
||||
expect(container.querySelector('[data-provider="anthropic"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders 'Using default' when no executor model override is set", () => {
|
||||
const entries = [makeEntry()];
|
||||
const { container } = render(
|
||||
<AgentLogViewer entries={entries} loading={false} executorModel={null} />
|
||||
<AgentLogViewer entries={entries} loading={false} executorModel={null} />,
|
||||
);
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
expect(header!.textContent).toContain("Executor:");
|
||||
expect(header!.textContent).toContain("Using default");
|
||||
expect(header!.textContent).not.toContain("Using default");
|
||||
});
|
||||
|
||||
it("renders 'Using default' when executorModel is undefined", () => {
|
||||
const entries = [makeEntry()];
|
||||
const { container } = render(
|
||||
<AgentLogViewer entries={entries} loading={false} />
|
||||
);
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
expect(header!.textContent).toContain("Executor:");
|
||||
expect(header!.textContent).toContain("Using default");
|
||||
expect(header!.textContent).not.toContain("Using default");
|
||||
});
|
||||
|
||||
it("renders model info header with validator model when set", () => {
|
||||
@@ -316,27 +315,26 @@ describe("AgentLogViewer", () => {
|
||||
entries={entries}
|
||||
loading={false}
|
||||
validatorModel={{ provider: "openai", modelId: "gpt-4o" }}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
expect(container.querySelector('[data-provider="openai"]')).toBeTruthy();
|
||||
expect(header!.textContent).not.toContain("Validator:");
|
||||
|
||||
fireEvent.click(screen.getByTestId("agent-log-model-expand"));
|
||||
expect(header!.textContent).toContain("Validator:");
|
||||
expect(header!.textContent).toContain("openai/gpt-4o");
|
||||
expect(header!.textContent).toContain("Executor:");
|
||||
expect(header!.textContent).toContain("Using default");
|
||||
// Verify ProviderIcon is rendered for validator
|
||||
expect(container.querySelector('[data-provider="openai"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders 'Using default' when no validator model override is set", () => {
|
||||
const entries = [makeEntry()];
|
||||
const { container } = render(
|
||||
<AgentLogViewer entries={entries} loading={false} validatorModel={null} />
|
||||
<AgentLogViewer entries={entries} loading={false} validatorModel={null} />,
|
||||
);
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
expect(header!.textContent).toContain("Validator:");
|
||||
expect(header!.textContent).toContain("Using default");
|
||||
expect(header!.textContent).not.toContain("Using default");
|
||||
});
|
||||
|
||||
it("renders both models when both are configured", () => {
|
||||
@@ -347,30 +345,26 @@ describe("AgentLogViewer", () => {
|
||||
loading={false}
|
||||
executorModel={{ provider: "anthropic", modelId: "claude-opus-4" }}
|
||||
validatorModel={{ provider: "openai", modelId: "gpt-4o" }}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
expect(header!.textContent).toContain("Executor:");
|
||||
expect(header!.textContent).toContain("anthropic/claude-opus-4");
|
||||
expect(header!.textContent).toContain("Validator:");
|
||||
expect(header!.textContent).toContain("openai/gpt-4o");
|
||||
// Verify both ProviderIcons are rendered
|
||||
expect(container.querySelector('[data-provider="anthropic"]')).toBeTruthy();
|
||||
expect(container.querySelector('[data-provider="openai"]')).toBeTruthy();
|
||||
expect(header!.textContent).not.toContain("anthropic/claude-opus-4");
|
||||
expect(header!.textContent).not.toContain("openai/gpt-4o");
|
||||
|
||||
fireEvent.click(screen.getByTestId("agent-log-model-expand"));
|
||||
expect(header!.textContent).toContain("anthropic/claude-opus-4");
|
||||
expect(header!.textContent).toContain("openai/gpt-4o");
|
||||
});
|
||||
|
||||
it("renders header with 'Using default' for both models when both are null/undefined", () => {
|
||||
const entries = [makeEntry()];
|
||||
const { container } = render(
|
||||
<AgentLogViewer entries={entries} loading={false} />
|
||||
);
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
expect(header!.textContent).toContain("Executor:");
|
||||
expect(header!.textContent).toContain("Using default");
|
||||
expect(header!.textContent).toContain("Validator:");
|
||||
expect(header!.textContent).toContain("Using default");
|
||||
expect(header!.textContent).not.toContain("Using default");
|
||||
});
|
||||
|
||||
it("shows 'Using default' when executorModel has only provider but no modelId", () => {
|
||||
@@ -380,12 +374,11 @@ describe("AgentLogViewer", () => {
|
||||
entries={entries}
|
||||
loading={false}
|
||||
executorModel={{ provider: "anthropic" }}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
expect(header!.textContent).toContain("Executor:");
|
||||
expect(header!.textContent).toContain("Using default");
|
||||
expect(header!.textContent).not.toContain("Using default");
|
||||
});
|
||||
|
||||
it("shows 'Using default' when executorModel has only modelId but no provider", () => {
|
||||
@@ -395,12 +388,11 @@ describe("AgentLogViewer", () => {
|
||||
entries={entries}
|
||||
loading={false}
|
||||
executorModel={{ modelId: "claude-sonnet-4-5" }}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
expect(header!.textContent).toContain("Executor:");
|
||||
expect(header!.textContent).toContain("Using default");
|
||||
expect(header!.textContent).not.toContain("Using default");
|
||||
});
|
||||
|
||||
it("renders model info header with planning model when set", () => {
|
||||
@@ -410,36 +402,34 @@ describe("AgentLogViewer", () => {
|
||||
entries={entries}
|
||||
loading={false}
|
||||
planningModel={{ provider: "anthropic", modelId: "claude-opus-4" }}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
expect(header!.textContent).toContain("Planning:");
|
||||
expect(header!.textContent).toContain("anthropic/claude-opus-4");
|
||||
// Verify ProviderIcon is rendered for planning
|
||||
expect(container.querySelector('[data-provider="anthropic"]')).toBeTruthy();
|
||||
expect(header!.textContent).not.toContain("Planning/Triage:");
|
||||
|
||||
fireEvent.click(screen.getByTestId("agent-log-model-expand"));
|
||||
expect(header!.textContent).toContain("Planning/Triage:");
|
||||
expect(header!.textContent).toContain("anthropic/claude-opus-4");
|
||||
});
|
||||
|
||||
it("renders 'Using default' for planning when no planning model is set", () => {
|
||||
const entries = [makeEntry()];
|
||||
const { container } = render(
|
||||
<AgentLogViewer entries={entries} loading={false} planningModel={null} />
|
||||
<AgentLogViewer entries={entries} loading={false} planningModel={null} />,
|
||||
);
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
expect(header!.textContent).toContain("Planning:");
|
||||
expect(header!.textContent).toContain("Using default");
|
||||
expect(header!.textContent).not.toContain("Using default");
|
||||
});
|
||||
|
||||
it("renders 'Using default' for planning when planningModel is undefined", () => {
|
||||
const entries = [makeEntry()];
|
||||
const { container } = render(
|
||||
<AgentLogViewer entries={entries} loading={false} />
|
||||
);
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
expect(header!.textContent).toContain("Planning:");
|
||||
expect(header!.textContent).toContain("Using default");
|
||||
expect(header!.textContent).not.toContain("Using default");
|
||||
});
|
||||
|
||||
it("renders all three models when all are configured", () => {
|
||||
@@ -451,20 +441,21 @@ describe("AgentLogViewer", () => {
|
||||
executorModel={{ provider: "anthropic", modelId: "claude-opus-4" }}
|
||||
validatorModel={{ provider: "openai", modelId: "gpt-4o" }}
|
||||
planningModel={{ provider: "google", modelId: "gemini-pro" }}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
expect(header!.textContent).toContain("Executor:");
|
||||
expect(header!.textContent).toContain("anthropic/claude-opus-4");
|
||||
expect(header!.textContent).toContain("Validator:");
|
||||
expect(header!.textContent).toContain("openai/gpt-4o");
|
||||
expect(header!.textContent).toContain("Planning:");
|
||||
expect(header!.textContent).toContain("google/gemini-pro");
|
||||
// Verify all three ProviderIcons are rendered
|
||||
expect(container.querySelector('[data-provider="anthropic"]')).toBeTruthy();
|
||||
expect(container.querySelector('[data-provider="openai"]')).toBeTruthy();
|
||||
expect(container.querySelector('[data-provider="google"]')).toBeTruthy();
|
||||
expect(header!.textContent).not.toContain("anthropic/claude-opus-4");
|
||||
expect(header!.textContent).not.toContain("openai/gpt-4o");
|
||||
expect(header!.textContent).not.toContain("google/gemini-pro");
|
||||
|
||||
fireEvent.click(screen.getByTestId("agent-log-model-expand"));
|
||||
expect(header!.textContent).toContain("anthropic/claude-opus-4");
|
||||
expect(header!.textContent).toContain("openai/gpt-4o");
|
||||
expect(header!.textContent).toContain("google/gemini-pro");
|
||||
});
|
||||
|
||||
it("shows 'Using default' for planning when planningModel has only provider but no modelId", () => {
|
||||
@@ -474,12 +465,101 @@ describe("AgentLogViewer", () => {
|
||||
entries={entries}
|
||||
loading={false}
|
||||
planningModel={{ provider: "anthropic" }}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
expect(header!.textContent).toContain("Planning:");
|
||||
expect(header!.textContent).toContain("Using default");
|
||||
expect(header!.textContent).not.toContain("Using default");
|
||||
});
|
||||
});
|
||||
|
||||
describe("model header expand/collapse", () => {
|
||||
it("shows only provider icons in collapsed state, hides model text", () => {
|
||||
const entries = [makeEntry()];
|
||||
const { container } = render(
|
||||
<AgentLogViewer
|
||||
entries={entries}
|
||||
loading={false}
|
||||
executorModel={{ provider: "anthropic", modelId: "claude-sonnet-4-5" }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector('[data-provider="anthropic"]')).toBeTruthy();
|
||||
expect(screen.getByTestId("agent-log-model-expand")).toBeTruthy();
|
||||
expect(container.textContent).not.toContain("Executor:");
|
||||
expect(container.textContent).not.toContain("claude-sonnet-4-5");
|
||||
});
|
||||
|
||||
it("shows model details when expand button is clicked", () => {
|
||||
const entries = [makeEntry()];
|
||||
const { container } = render(
|
||||
<AgentLogViewer
|
||||
entries={entries}
|
||||
loading={false}
|
||||
executorModel={{ provider: "anthropic", modelId: "claude-sonnet-4-5" }}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId("agent-log-model-expand"));
|
||||
expect(container.textContent).toContain("Executor:");
|
||||
expect(container.textContent).toContain("anthropic/claude-sonnet-4-5");
|
||||
});
|
||||
|
||||
it("collapses model details when expand button is clicked again", () => {
|
||||
const entries = [makeEntry()];
|
||||
const { container } = render(
|
||||
<AgentLogViewer
|
||||
entries={entries}
|
||||
loading={false}
|
||||
executorModel={{ provider: "anthropic", modelId: "claude-sonnet-4-5" }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const button = screen.getByTestId("agent-log-model-expand");
|
||||
fireEvent.click(button);
|
||||
expect(container.textContent).toContain("Executor:");
|
||||
fireEvent.click(button);
|
||||
expect(container.textContent).not.toContain("Executor:");
|
||||
});
|
||||
|
||||
it("shows no provider icons when no model overrides are set", () => {
|
||||
const entries = [makeEntry()];
|
||||
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
|
||||
|
||||
expect(container.querySelector("[data-provider]")).toBeNull();
|
||||
});
|
||||
|
||||
it("has aria-expanded=false when collapsed and aria-expanded=true when expanded", () => {
|
||||
const entries = [makeEntry()];
|
||||
render(
|
||||
<AgentLogViewer
|
||||
entries={entries}
|
||||
loading={false}
|
||||
executorModel={{ provider: "anthropic", modelId: "claude-sonnet-4-5" }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const button = screen.getByTestId("agent-log-model-expand");
|
||||
expect(button.getAttribute("aria-expanded")).toBe("false");
|
||||
fireEvent.click(button);
|
||||
expect(button.getAttribute("aria-expanded")).toBe("true");
|
||||
});
|
||||
|
||||
it("renders multiple provider icons for multiple overrides", () => {
|
||||
const entries = [makeEntry()];
|
||||
const { container } = render(
|
||||
<AgentLogViewer
|
||||
entries={entries}
|
||||
loading={false}
|
||||
executorModel={{ provider: "anthropic", modelId: "claude-opus-4" }}
|
||||
validatorModel={{ provider: "openai", modelId: "gpt-4o" }}
|
||||
planningModel={{ provider: "google", modelId: "gemini-pro" }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector('[data-provider="anthropic"]')).toBeTruthy();
|
||||
expect(container.querySelector('[data-provider="openai"]')).toBeTruthy();
|
||||
expect(container.querySelector('[data-provider="google"]')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -48,9 +48,11 @@ vi.mock("lucide-react", () => ({
|
||||
MessageSquare: () => null,
|
||||
ChevronUp: () => null,
|
||||
ChevronDown: () => null,
|
||||
ChevronRight: () => null,
|
||||
X: () => null,
|
||||
Maximize2: () => null,
|
||||
Minimize2: () => null,
|
||||
Loader2: () => null,
|
||||
Bot: () => null,
|
||||
CircleDot: () => null,
|
||||
XCircle: () => null,
|
||||
@@ -1706,23 +1708,32 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
}
|
||||
|
||||
it("shows resolved executor from settings when task has no explicit executor override", async () => {
|
||||
const { container } = await setupModelTest({
|
||||
defaultProvider: "anthropic",
|
||||
defaultModelId: "claude-sonnet-4-5",
|
||||
});
|
||||
|
||||
async function openAgentLogAndExpandModelDetails(container: HTMLElement) {
|
||||
fireEvent.click(screen.getByText("Logs"));
|
||||
fireEvent.click(screen.getByText("Agent Log"));
|
||||
|
||||
await waitFor(() => {
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
expect(header!.textContent).toContain("anthropic/claude-sonnet-4-5");
|
||||
});
|
||||
|
||||
const expandButton = screen.getByTestId("agent-log-model-expand") as HTMLButtonElement;
|
||||
if (expandButton.getAttribute("aria-expanded") !== "true") {
|
||||
fireEvent.click(expandButton);
|
||||
}
|
||||
|
||||
return container.querySelector("[data-testid='agent-log-model-header']") as HTMLElement;
|
||||
}
|
||||
|
||||
it("shows resolved executor from settings when task has no explicit executor override", async () => {
|
||||
const { container } = await setupModelTest({
|
||||
defaultProvider: "anthropic",
|
||||
defaultModelId: "claude-sonnet-4-5",
|
||||
});
|
||||
|
||||
const header = await openAgentLogAndExpandModelDetails(container);
|
||||
|
||||
// Validator should also fall back to the default
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
|
||||
expect(header.textContent).toContain("anthropic/claude-sonnet-4-5");
|
||||
});
|
||||
|
||||
@@ -1734,16 +1745,7 @@ describe("TaskDetailModal", () => {
|
||||
validatorModelId: "gpt-4o",
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("Logs"));
|
||||
fireEvent.click(screen.getByText("Agent Log"));
|
||||
|
||||
await waitFor(() => {
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
expect(header!.textContent).toContain("openai/gpt-4o");
|
||||
});
|
||||
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
|
||||
const header = await openAgentLogAndExpandModelDetails(container);
|
||||
// Executor falls back to default
|
||||
expect(header.textContent).toContain("anthropic/claude-sonnet-4-5");
|
||||
// Validator uses the validator-specific setting
|
||||
@@ -1757,18 +1759,9 @@ describe("TaskDetailModal", () => {
|
||||
// No validatorProvider or validatorModelId
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("Logs"));
|
||||
fireEvent.click(screen.getByText("Agent Log"));
|
||||
|
||||
await waitFor(() => {
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
// Both executor and validator should resolve to the default
|
||||
expect(header!.textContent).toContain("anthropic/claude-sonnet-4-5");
|
||||
});
|
||||
const header = await openAgentLogAndExpandModelDetails(container);
|
||||
|
||||
// Count occurrences - should appear three times (once for executor, once for validator, once for planning)
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
|
||||
const matches = header.textContent!.match(/anthropic\/claude-sonnet-4-5/g);
|
||||
expect(matches).toHaveLength(3);
|
||||
});
|
||||
@@ -1779,18 +1772,9 @@ describe("TaskDetailModal", () => {
|
||||
{ defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5" },
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Logs"));
|
||||
fireEvent.click(screen.getByText("Agent Log"));
|
||||
|
||||
await waitFor(() => {
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
// Task override should win
|
||||
expect(header!.textContent).toContain("openai/gpt-4o");
|
||||
});
|
||||
const header = await openAgentLogAndExpandModelDetails(container);
|
||||
|
||||
// Default model should not appear for executor
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
|
||||
expect(header.textContent).toContain("openai/gpt-4o");
|
||||
// Validator falls back to default
|
||||
expect(header.textContent).toContain("anthropic/claude-sonnet-4-5");
|
||||
@@ -1802,17 +1786,7 @@ describe("TaskDetailModal", () => {
|
||||
{ defaultProvider: "anthropic", defaultModelId: "claude-sonnet-4-5", validatorProvider: "openai", validatorModelId: "gpt-4o" },
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Logs"));
|
||||
fireEvent.click(screen.getByText("Agent Log"));
|
||||
|
||||
await waitFor(() => {
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
// Task validator override should win
|
||||
expect(header!.textContent).toContain("google/gemini-pro");
|
||||
});
|
||||
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
|
||||
const header = await openAgentLogAndExpandModelDetails(container);
|
||||
// Executor falls back to default
|
||||
expect(header.textContent).toContain("anthropic/claude-sonnet-4-5");
|
||||
// Settings validator should not appear (task override wins)
|
||||
@@ -1824,15 +1798,7 @@ describe("TaskDetailModal", () => {
|
||||
// No defaultProvider/defaultModelId
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("Logs"));
|
||||
fireEvent.click(screen.getByText("Agent Log"));
|
||||
|
||||
await waitFor(() => {
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
});
|
||||
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
|
||||
const header = await openAgentLogAndExpandModelDetails(container);
|
||||
expect(header.textContent).toContain("Using default");
|
||||
// Should show "Using default" for executor, validator, and planning
|
||||
const defaultBadges = header.querySelectorAll(".model-badge-default");
|
||||
@@ -1862,16 +1828,8 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Logs"));
|
||||
fireEvent.click(screen.getByText("Agent Log"));
|
||||
|
||||
// Wait for the failed fetch to settle
|
||||
await waitFor(() => {
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
});
|
||||
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
|
||||
const header = await openAgentLogAndExpandModelDetails(container);
|
||||
expect(header.textContent).toContain("Using default");
|
||||
const defaultBadges = header.querySelectorAll(".model-badge-default");
|
||||
expect(defaultBadges).toHaveLength(3);
|
||||
@@ -1892,16 +1850,7 @@ describe("TaskDetailModal", () => {
|
||||
},
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Logs"));
|
||||
fireEvent.click(screen.getByText("Agent Log"));
|
||||
|
||||
await waitFor(() => {
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
expect(header!.textContent).toContain("google/gemini-pro");
|
||||
});
|
||||
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
|
||||
const header = await openAgentLogAndExpandModelDetails(container);
|
||||
// Executor uses task override
|
||||
expect(header.textContent).toContain("google/gemini-pro");
|
||||
// Validator uses settings-specific validator
|
||||
@@ -1943,19 +1892,11 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Navigate to Agent Log subview
|
||||
fireEvent.click(screen.getByText("Logs"));
|
||||
fireEvent.click(screen.getByText("Agent Log"));
|
||||
const header = await openAgentLogAndExpandModelDetails(container);
|
||||
|
||||
await waitFor(() => {
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
// Planning should show the runtime triage marker, not settings default
|
||||
expect(header!.textContent).toContain("Planning:");
|
||||
expect(header!.textContent).toContain("google/gemini-pro");
|
||||
});
|
||||
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
|
||||
// Planning should show the runtime triage marker, not settings default
|
||||
expect(header.textContent).toContain("Planning/Triage:");
|
||||
expect(header.textContent).toContain("google/gemini-pro");
|
||||
// Executor/Validator should still show settings default
|
||||
expect(header.textContent).toContain("anthropic/claude-sonnet-4-5");
|
||||
});
|
||||
@@ -1992,19 +1933,11 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Navigate to Agent Log subview
|
||||
fireEvent.click(screen.getByText("Logs"));
|
||||
fireEvent.click(screen.getByText("Agent Log"));
|
||||
const header = await openAgentLogAndExpandModelDetails(container);
|
||||
|
||||
await waitFor(() => {
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
// Planning should use planningProvider/planningModelId from settings
|
||||
expect(header!.textContent).toContain("Planning:");
|
||||
expect(header!.textContent).toContain("openai/gpt-4o");
|
||||
});
|
||||
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
|
||||
// Planning should use planningProvider/planningModelId from settings
|
||||
expect(header.textContent).toContain("Planning/Triage:");
|
||||
expect(header.textContent).toContain("openai/gpt-4o");
|
||||
// Executor/Validator should show default
|
||||
expect(header.textContent).toContain("anthropic/claude-sonnet-4-5");
|
||||
// Planning should NOT show the default
|
||||
@@ -2017,19 +1950,12 @@ describe("TaskDetailModal", () => {
|
||||
defaultModelId: "claude-sonnet-4-5",
|
||||
});
|
||||
|
||||
// Navigate to Agent Log subview
|
||||
fireEvent.click(screen.getByText("Logs"));
|
||||
fireEvent.click(screen.getByText("Agent Log"));
|
||||
const header = await openAgentLogAndExpandModelDetails(container);
|
||||
|
||||
await waitFor(() => {
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
expect(header!.textContent).toContain("Planning:");
|
||||
expect(header!.textContent).toContain("anthropic/claude-sonnet-4-5");
|
||||
});
|
||||
expect(header.textContent).toContain("Planning/Triage:");
|
||||
expect(header.textContent).toContain("anthropic/claude-sonnet-4-5");
|
||||
|
||||
// Planning falls back to default - same as executor/validator
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
|
||||
const matches = header.textContent!.match(/anthropic\/claude-sonnet-4-5/g);
|
||||
expect(matches).toHaveLength(3); // executor, validator, planning
|
||||
});
|
||||
@@ -2039,17 +1965,8 @@ describe("TaskDetailModal", () => {
|
||||
// No defaultProvider/defaultModelId
|
||||
});
|
||||
|
||||
// Navigate to Agent Log subview
|
||||
fireEvent.click(screen.getByText("Logs"));
|
||||
fireEvent.click(screen.getByText("Agent Log"));
|
||||
|
||||
await waitFor(() => {
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
});
|
||||
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
|
||||
expect(header.textContent).toContain("Planning:");
|
||||
const header = await openAgentLogAndExpandModelDetails(container);
|
||||
expect(header.textContent).toContain("Planning/Triage:");
|
||||
const defaultBadges = header.querySelectorAll(".model-badge-default");
|
||||
// 3 default badges: executor, validator, planning
|
||||
expect(defaultBadges).toHaveLength(3);
|
||||
@@ -2091,19 +2008,10 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Navigate to Agent Log subview
|
||||
fireEvent.click(screen.getByText("Logs"));
|
||||
fireEvent.click(screen.getByText("Agent Log"));
|
||||
|
||||
await waitFor(() => {
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
// Per-task override should take precedence over settings
|
||||
expect(header!.textContent).toContain("Planning:");
|
||||
expect(header!.textContent).toContain("google/gemini-2.5-pro");
|
||||
});
|
||||
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
|
||||
const header = await openAgentLogAndExpandModelDetails(container);
|
||||
// Per-task override should take precedence over settings
|
||||
expect(header.textContent).toContain("Planning/Triage:");
|
||||
expect(header.textContent).toContain("google/gemini-2.5-pro");
|
||||
// Should NOT show the settings planning model
|
||||
expect(header.textContent).not.toContain("openai/gpt-4o");
|
||||
});
|
||||
@@ -2143,18 +2051,9 @@ describe("TaskDetailModal", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Navigate to Agent Log subview
|
||||
fireEvent.click(screen.getByText("Logs"));
|
||||
fireEvent.click(screen.getByText("Agent Log"));
|
||||
|
||||
await waitFor(() => {
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']");
|
||||
expect(header).toBeTruthy();
|
||||
// Runtime marker should win over planning settings
|
||||
expect(header!.textContent).toContain("google/gemini-pro");
|
||||
});
|
||||
|
||||
const header = container.querySelector("[data-testid='agent-log-model-header']")!;
|
||||
const header = await openAgentLogAndExpandModelDetails(container);
|
||||
// Runtime marker should win over planning settings
|
||||
expect(header.textContent).toContain("google/gemini-pro");
|
||||
// Should NOT show the planning settings model
|
||||
expect(header.textContent).not.toContain("openai/gpt-4o");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user