feat(FN-5204): add test mode activation across settings, engine, and dashboard
- Add core settings schema/types support for testMode with model-resolution override handling - Enforce engine session lane overrides in test mode with targeted helper coverage - Add dashboard settings toggle plus persistent test-mode banner and related component tests - Update settings documentation and parity/roundtrip tests for the new test mode behavior
This commit is contained in:
committed by
gsxdsm
parent
fbf7e2cb4d
commit
9c2b61eec6
@@ -146,6 +146,10 @@ Per-task opt-out exists: `task.scopeOverride = true` (log the reason).
|
||||
|
||||
When `settings.autoMerge: false`, `in-review` is terminal-until-merged by a human. Lifecycle-mutating self-healing must not move these tasks backward, pause/fail them, or re-enqueue them for execution.
|
||||
|
||||
### Mock provider (test mode)
|
||||
|
||||
`testMode?: boolean` is now available in both project and global settings. If project `testMode === true` (or the resolved default provider is `"mock"` at any tier), every AI lane is forced to `mock/scripted`, overriding per-task and per-lane model selections. The dashboard exposes this via the Settings Modal "Enable test mode" toggle and a persistent "Test mode — no real AI calls" banner.
|
||||
|
||||
---
|
||||
|
||||
## Reference docs (deeper detail)
|
||||
|
||||
@@ -1303,6 +1303,8 @@ Per-task and global script overrides live in `mockScriptRegistry` (`setMockScrip
|
||||
The mock runtime never registers with pi's `ModelRegistry` and is guarded by tests that fail on any `fetch`, `http.request`, or `https.request` usage.
|
||||
Activation UX/settings affordances are handled separately in FN-5204.
|
||||
|
||||
`testMode?: boolean` exists at both global and project scopes. Project `testMode: true` takes precedence and forces planning, executor, reviewer/validator, merger, and heartbeat to `mock/scripted` regardless of per-task or per-lane overrides. The dashboard surfaces this with the Settings Modal "Enable test mode" toggle and the shell banner: "Test mode — no real AI calls".
|
||||
|
||||
## Per-task token budget precedence
|
||||
|
||||
1. `task.tokenBudgetOverride`
|
||||
|
||||
@@ -219,6 +219,15 @@ describe("GlobalSettingsStore", () => {
|
||||
expect(settings.themeMode).toBe("dark"); // preserved default
|
||||
});
|
||||
|
||||
it("round-trips testMode in global settings", async () => {
|
||||
await store.init();
|
||||
|
||||
await store.updateSettings({ testMode: true });
|
||||
|
||||
const settings = await store.getSettings();
|
||||
expect(settings.testMode).toBe(true);
|
||||
});
|
||||
|
||||
it("creates directory if missing", async () => {
|
||||
const nested = join(dir, "auto", "create");
|
||||
const nestedStore = new GlobalSettingsStore(nested);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
applyTestModeOverrides,
|
||||
resolveExecutionSettingsModel,
|
||||
resolvePlanningSettingsModel,
|
||||
resolveProjectDefaultModel,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
resolveTaskValidatorModel,
|
||||
resolveTitleSummarizerSettingsModel,
|
||||
resolveValidatorSettingsModel,
|
||||
TEST_MODE_RESOLVED,
|
||||
} from "../model-resolution.js";
|
||||
|
||||
describe("model-resolution", () => {
|
||||
@@ -116,4 +118,60 @@ describe("model-resolution", () => {
|
||||
),
|
||||
).toEqual({ provider: "openai", modelId: "gpt-4.1" });
|
||||
});
|
||||
|
||||
it("forces every lane to mock when testMode is true", () => {
|
||||
const settings = {
|
||||
testMode: true,
|
||||
executionProvider: "anthropic",
|
||||
executionModelId: "claude-sonnet-4-5",
|
||||
planningProvider: "anthropic",
|
||||
planningModelId: "claude-sonnet-4-5",
|
||||
validatorProvider: "anthropic",
|
||||
validatorModelId: "claude-sonnet-4-5",
|
||||
titleSummarizerProvider: "anthropic",
|
||||
titleSummarizerModelId: "claude-sonnet-4-5",
|
||||
defaultProviderOverride: "anthropic",
|
||||
defaultModelIdOverride: "claude-sonnet-4-5",
|
||||
};
|
||||
const taskOverrides = {
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
validatorModelProvider: "anthropic",
|
||||
validatorModelId: "claude-sonnet-4-5",
|
||||
planningModelProvider: "anthropic",
|
||||
planningModelId: "claude-sonnet-4-5",
|
||||
};
|
||||
|
||||
expect(resolveProjectDefaultModel(settings)).toEqual(TEST_MODE_RESOLVED);
|
||||
expect(resolveExecutionSettingsModel(settings)).toEqual(TEST_MODE_RESOLVED);
|
||||
expect(resolvePlanningSettingsModel(settings)).toEqual(TEST_MODE_RESOLVED);
|
||||
expect(resolveValidatorSettingsModel(settings)).toEqual(TEST_MODE_RESOLVED);
|
||||
expect(resolveTitleSummarizerSettingsModel(settings)).toEqual(TEST_MODE_RESOLVED);
|
||||
expect(resolveTaskExecutionModel(taskOverrides, settings)).toEqual(TEST_MODE_RESOLVED);
|
||||
expect(resolveTaskValidatorModel(taskOverrides, settings)).toEqual(TEST_MODE_RESOLVED);
|
||||
expect(resolveTaskPlanningModel(taskOverrides, settings)).toEqual(TEST_MODE_RESOLVED);
|
||||
});
|
||||
|
||||
it("forces mock when defaultProvider is mock without testMode", () => {
|
||||
const settings = {
|
||||
defaultProvider: "mock",
|
||||
defaultModelId: "anything",
|
||||
executionProvider: "anthropic",
|
||||
executionModelId: "claude-sonnet-4-5",
|
||||
};
|
||||
|
||||
expect(resolveExecutionSettingsModel(settings)).toEqual(TEST_MODE_RESOLVED);
|
||||
expect(resolvePlanningSettingsModel(settings)).toEqual(TEST_MODE_RESOLVED);
|
||||
expect(resolveValidatorSettingsModel(settings)).toEqual(TEST_MODE_RESOLVED);
|
||||
});
|
||||
|
||||
it("passes through when test mode is inactive", () => {
|
||||
const resolved = resolveExecutionSettingsModel({
|
||||
executionProvider: "anthropic",
|
||||
executionModelId: "claude-sonnet-4-5",
|
||||
});
|
||||
|
||||
expect(applyTestModeOverrides(resolved, { testMode: false })).toEqual(resolved);
|
||||
expect(applyTestModeOverrides(resolved, {})).toEqual(resolved);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -299,6 +299,7 @@ describe("settings key parity", () => {
|
||||
const projectKeySet = new Set(PROJECT_SETTINGS_KEYS as readonly string[]);
|
||||
const overlap = (GLOBAL_SETTINGS_KEYS as readonly string[]).filter((key) => projectKeySet.has(key));
|
||||
expect(overlap).toEqual([
|
||||
"testMode",
|
||||
"taskTokenBudget",
|
||||
"githubTrackingDefaultRepo",
|
||||
"worktrunk",
|
||||
@@ -434,6 +435,11 @@ describe("model lane key parity regression (FN-1729)", () => {
|
||||
expect(isGlobalSettingsKey("defaultModelIdOverride")).toBe(false);
|
||||
});
|
||||
|
||||
it("testMode key is recognized in both project and global scopes", () => {
|
||||
expect(isProjectSettingsKey("testMode")).toBe(true);
|
||||
expect(isGlobalSettingsKey("testMode")).toBe(true);
|
||||
});
|
||||
|
||||
it("no model lane provider exists without its corresponding modelId key", () => {
|
||||
const allKeys = new Set([...GLOBAL_SETTINGS_KEYS, ...PROJECT_SETTINGS_KEYS]);
|
||||
|
||||
|
||||
@@ -1465,6 +1465,20 @@ describe("TaskStore", () => {
|
||||
expect(settings.maxConcurrent).toBe(8);
|
||||
});
|
||||
|
||||
it("round-trips testMode in both project and global scopes", async () => {
|
||||
await harness.store().updateGlobalSettings({ testMode: true });
|
||||
let merged = await harness.store().getSettings();
|
||||
expect(merged.testMode).toBe(true);
|
||||
|
||||
await harness.store().updateSettings({ testMode: false });
|
||||
merged = await harness.store().getSettings();
|
||||
expect(merged.testMode).toBe(false);
|
||||
|
||||
const { global, project } = await harness.store().getSettingsByScope();
|
||||
expect(global.testMode).toBe(true);
|
||||
expect(project.testMode).toBe(false);
|
||||
});
|
||||
|
||||
it("updateSettings silently filters out global-only fields", async () => {
|
||||
// themeMode is a global field — should not be persisted to project config
|
||||
await harness.store().updateSettings({ maxConcurrent: 5, themeMode: "light" } as any);
|
||||
|
||||
@@ -581,6 +581,8 @@ export {
|
||||
__resetSummarizeState,
|
||||
} from "./ai-summarize.js";
|
||||
export {
|
||||
applyTestModeOverrides,
|
||||
isTestModeActive,
|
||||
resolveExecutionSettingsModel,
|
||||
resolvePlanningSettingsModel,
|
||||
resolveProjectDefaultModel,
|
||||
@@ -589,6 +591,7 @@ export {
|
||||
resolveTaskValidatorModel,
|
||||
resolveTitleSummarizerSettingsModel,
|
||||
resolveValidatorSettingsModel,
|
||||
TEST_MODE_RESOLVED,
|
||||
} from "./model-resolution.js";
|
||||
export type { ResolvedModelSelection } from "./model-resolution.js";
|
||||
|
||||
|
||||
@@ -5,6 +5,19 @@ export interface ResolvedModelSelection {
|
||||
modelId?: string;
|
||||
}
|
||||
|
||||
export const TEST_MODE_RESOLVED: ResolvedModelSelection = { provider: "mock", modelId: "scripted" };
|
||||
|
||||
export function isTestModeActive(settings?: Partial<Settings>): boolean {
|
||||
return settings?.testMode === true || settings?.defaultProvider?.trim().toLowerCase() === "mock";
|
||||
}
|
||||
|
||||
export function applyTestModeOverrides(
|
||||
resolved: ResolvedModelSelection,
|
||||
settings?: Partial<Settings>,
|
||||
): ResolvedModelSelection {
|
||||
return isTestModeActive(settings) ? TEST_MODE_RESOLVED : resolved;
|
||||
}
|
||||
|
||||
type ModelPair =
|
||||
| ResolvedModelSelection
|
||||
| {
|
||||
@@ -36,75 +49,90 @@ function pickFirstModelPair(...pairs: ModelPair[]): ResolvedModelSelection {
|
||||
}
|
||||
|
||||
export function resolveProjectDefaultModel(settings?: Partial<Settings>): ResolvedModelSelection {
|
||||
return pickFirstModelPair(
|
||||
{
|
||||
provider: settings?.defaultProviderOverride,
|
||||
modelId: settings?.defaultModelIdOverride,
|
||||
},
|
||||
{
|
||||
provider: settings?.defaultProvider,
|
||||
modelId: settings?.defaultModelId,
|
||||
},
|
||||
return applyTestModeOverrides(
|
||||
pickFirstModelPair(
|
||||
{
|
||||
provider: settings?.defaultProviderOverride,
|
||||
modelId: settings?.defaultModelIdOverride,
|
||||
},
|
||||
{
|
||||
provider: settings?.defaultProvider,
|
||||
modelId: settings?.defaultModelId,
|
||||
},
|
||||
),
|
||||
settings,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveExecutionSettingsModel(settings?: Partial<Settings>): ResolvedModelSelection {
|
||||
return pickFirstModelPair(
|
||||
{
|
||||
provider: settings?.executionProvider,
|
||||
modelId: settings?.executionModelId,
|
||||
},
|
||||
{
|
||||
provider: settings?.executionGlobalProvider,
|
||||
modelId: settings?.executionGlobalModelId,
|
||||
},
|
||||
resolveProjectDefaultModel(settings),
|
||||
return applyTestModeOverrides(
|
||||
pickFirstModelPair(
|
||||
{
|
||||
provider: settings?.executionProvider,
|
||||
modelId: settings?.executionModelId,
|
||||
},
|
||||
{
|
||||
provider: settings?.executionGlobalProvider,
|
||||
modelId: settings?.executionGlobalModelId,
|
||||
},
|
||||
resolveProjectDefaultModel(settings),
|
||||
),
|
||||
settings,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolvePlanningSettingsModel(settings?: Partial<Settings>): ResolvedModelSelection {
|
||||
return pickFirstModelPair(
|
||||
{
|
||||
provider: settings?.planningProvider,
|
||||
modelId: settings?.planningModelId,
|
||||
},
|
||||
{
|
||||
provider: settings?.planningGlobalProvider,
|
||||
modelId: settings?.planningGlobalModelId,
|
||||
},
|
||||
resolveProjectDefaultModel(settings),
|
||||
return applyTestModeOverrides(
|
||||
pickFirstModelPair(
|
||||
{
|
||||
provider: settings?.planningProvider,
|
||||
modelId: settings?.planningModelId,
|
||||
},
|
||||
{
|
||||
provider: settings?.planningGlobalProvider,
|
||||
modelId: settings?.planningGlobalModelId,
|
||||
},
|
||||
resolveProjectDefaultModel(settings),
|
||||
),
|
||||
settings,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveValidatorSettingsModel(settings?: Partial<Settings>): ResolvedModelSelection {
|
||||
return pickFirstModelPair(
|
||||
{
|
||||
provider: settings?.validatorProvider,
|
||||
modelId: settings?.validatorModelId,
|
||||
},
|
||||
{
|
||||
provider: settings?.validatorGlobalProvider,
|
||||
modelId: settings?.validatorGlobalModelId,
|
||||
},
|
||||
resolveProjectDefaultModel(settings),
|
||||
return applyTestModeOverrides(
|
||||
pickFirstModelPair(
|
||||
{
|
||||
provider: settings?.validatorProvider,
|
||||
modelId: settings?.validatorModelId,
|
||||
},
|
||||
{
|
||||
provider: settings?.validatorGlobalProvider,
|
||||
modelId: settings?.validatorGlobalModelId,
|
||||
},
|
||||
resolveProjectDefaultModel(settings),
|
||||
),
|
||||
settings,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveTitleSummarizerSettingsModel(settings?: Partial<Settings>): ResolvedModelSelection {
|
||||
return pickFirstModelPair(
|
||||
{
|
||||
provider: settings?.titleSummarizerProvider,
|
||||
modelId: settings?.titleSummarizerModelId,
|
||||
},
|
||||
{
|
||||
provider: settings?.titleSummarizerGlobalProvider,
|
||||
modelId: settings?.titleSummarizerGlobalModelId,
|
||||
},
|
||||
{
|
||||
provider: settings?.planningProvider,
|
||||
modelId: settings?.planningModelId,
|
||||
},
|
||||
resolveProjectDefaultModel(settings),
|
||||
return applyTestModeOverrides(
|
||||
pickFirstModelPair(
|
||||
{
|
||||
provider: settings?.titleSummarizerProvider,
|
||||
modelId: settings?.titleSummarizerModelId,
|
||||
},
|
||||
{
|
||||
provider: settings?.titleSummarizerGlobalProvider,
|
||||
modelId: settings?.titleSummarizerGlobalModelId,
|
||||
},
|
||||
{
|
||||
provider: settings?.planningProvider,
|
||||
modelId: settings?.planningModelId,
|
||||
},
|
||||
resolveProjectDefaultModel(settings),
|
||||
),
|
||||
settings,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -112,12 +140,15 @@ export function resolveTaskExecutionModel(
|
||||
task: TaskModelLike,
|
||||
settings?: Partial<Settings>,
|
||||
): ResolvedModelSelection {
|
||||
return pickFirstModelPair(
|
||||
{
|
||||
provider: task.modelProvider,
|
||||
modelId: task.modelId,
|
||||
},
|
||||
resolveExecutionSettingsModel(settings),
|
||||
return applyTestModeOverrides(
|
||||
pickFirstModelPair(
|
||||
{
|
||||
provider: task.modelProvider,
|
||||
modelId: task.modelId,
|
||||
},
|
||||
resolveExecutionSettingsModel(settings),
|
||||
),
|
||||
settings,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,12 +156,15 @@ export function resolveTaskValidatorModel(
|
||||
task: TaskModelLike,
|
||||
settings?: Partial<Settings>,
|
||||
): ResolvedModelSelection {
|
||||
return pickFirstModelPair(
|
||||
{
|
||||
provider: task.validatorModelProvider,
|
||||
modelId: task.validatorModelId,
|
||||
},
|
||||
resolveValidatorSettingsModel(settings),
|
||||
return applyTestModeOverrides(
|
||||
pickFirstModelPair(
|
||||
{
|
||||
provider: task.validatorModelProvider,
|
||||
modelId: task.validatorModelId,
|
||||
},
|
||||
resolveValidatorSettingsModel(settings),
|
||||
),
|
||||
settings,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -138,11 +172,14 @@ export function resolveTaskPlanningModel(
|
||||
task: TaskModelLike,
|
||||
settings?: Partial<Settings>,
|
||||
): ResolvedModelSelection {
|
||||
return pickFirstModelPair(
|
||||
{
|
||||
provider: task.planningModelProvider,
|
||||
modelId: task.planningModelId,
|
||||
},
|
||||
resolvePlanningSettingsModel(settings),
|
||||
return applyTestModeOverrides(
|
||||
pickFirstModelPair(
|
||||
{
|
||||
provider: task.planningModelProvider,
|
||||
modelId: task.planningModelId,
|
||||
},
|
||||
resolvePlanningSettingsModel(settings),
|
||||
),
|
||||
settings,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export const DEFAULT_GLOBAL_SETTINGS = {
|
||||
dashboardFontScalePct: 100,
|
||||
defaultProvider: undefined,
|
||||
defaultModelId: undefined,
|
||||
testMode: undefined,
|
||||
fallbackProvider: undefined,
|
||||
fallbackModelId: undefined,
|
||||
defaultThinkingLevel: undefined,
|
||||
@@ -192,6 +193,7 @@ export const DEFAULT_PROJECT_SETTINGS = {
|
||||
groupOverlappingFiles: true,
|
||||
overlapIgnorePaths: [],
|
||||
autoMerge: true,
|
||||
testMode: undefined,
|
||||
mergeStrategy: "direct",
|
||||
directMergeCommitStrategy: "auto",
|
||||
mergeIntegrationWorktree: "reuse-task-worktree",
|
||||
|
||||
@@ -2227,6 +2227,10 @@ export interface GlobalSettings {
|
||||
* Must be set together with `defaultProvider`. When both are undefined,
|
||||
* the engine uses pi's automatic model resolution. */
|
||||
defaultModelId?: string;
|
||||
/** When true, force every AI lane onto the deterministic mock provider regardless
|
||||
* of per-task or per-lane overrides. No network calls, zero token cost.
|
||||
* Project `testMode` takes precedence over the global value. */
|
||||
testMode?: boolean;
|
||||
/** Fallback AI model provider used when the primary default model fails due to
|
||||
* transient provider-side issues such as rate limits or overloaded capacity.
|
||||
* Must be set together with `fallbackModelId`. */
|
||||
@@ -2710,6 +2714,9 @@ export interface ProjectSettings {
|
||||
* active in-progress tasks and in-review tasks with unmerged worktrees. */
|
||||
overlapIgnorePaths?: string[];
|
||||
autoMerge: boolean;
|
||||
/** When true, force every AI lane onto the deterministic mock provider regardless
|
||||
* of per-task or per-lane overrides. No network calls, zero token cost. */
|
||||
testMode?: boolean;
|
||||
/** How completed in-review tasks should be finalized when autoMerge is enabled.
|
||||
* - "direct": preserve the existing local squash-merge flow into the current branch
|
||||
* - "pull-request": create or reuse a GitHub PR and wait for GitHub-side checks/reviews
|
||||
|
||||
@@ -24,6 +24,7 @@ import { SessionNotificationBanner } from "./components/SessionNotificationBanne
|
||||
import { CliBinaryInstallBanner } from "./components/CliBinaryInstallBanner";
|
||||
import { SetupWarningBanner } from "./components/SetupWarningBanner";
|
||||
import { CapacityRiskBanner } from "./components/CapacityRiskBanner";
|
||||
import { TestModeBanner } from "./components/TestModeBanner";
|
||||
import { TaskIdIntegrityBanner } from "./components/TaskIdIntegrityBanner";
|
||||
import { DbCorruptionBanner } from "./components/DbCorruptionBanner";
|
||||
import { UpdateAvailableBanner } from "./components/UpdateAvailableBanner";
|
||||
@@ -773,6 +774,7 @@ function AppInner() {
|
||||
autoMerge,
|
||||
globalPaused,
|
||||
enginePaused,
|
||||
isTestMode,
|
||||
taskStuckTimeoutMs,
|
||||
staleHighFanoutBlockerAgeThresholdMs,
|
||||
capacityRiskBannerEnabled,
|
||||
@@ -1777,6 +1779,9 @@ function AppInner() {
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
{viewMode === "project" && currentProject && (
|
||||
<TestModeBanner isActive={isTestMode} />
|
||||
)}
|
||||
{viewMode === "project" && currentProject && !nodesOpen && taskView !== "missions" && !modalManager.isPlanningOpen && !sessionBannersHidden && (
|
||||
<SessionNotificationBanner
|
||||
sessions={sessionsNeedingInput}
|
||||
|
||||
@@ -4522,6 +4522,23 @@ export function SettingsModal({
|
||||
<small>When enabled, tasks that pass review are automatically merged into the main branch</small>
|
||||
</details>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="testMode" className="checkbox-label">
|
||||
<input
|
||||
id="testMode"
|
||||
type="checkbox"
|
||||
checked={form.testMode === true}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, testMode: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
Enable test mode
|
||||
</label>
|
||||
<details className="settings-option-details">
|
||||
<summary>More details</summary>
|
||||
<small>Forces all AI lanes to use the deterministic mock provider. No network calls, zero token cost.</small>
|
||||
</details>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="workflowRevisionForkOnScopeMismatch" className="checkbox-label">
|
||||
<input
|
||||
|
||||
18
packages/dashboard/app/components/TestModeBanner.css
Normal file
18
packages/dashboard/app/components/TestModeBanner.css
Normal file
@@ -0,0 +1,18 @@
|
||||
.test-mode-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
margin-bottom: var(--space-md);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border-radius: var(--radius-md);
|
||||
border-inline-start: var(--space-xs) solid var(--color-warning);
|
||||
background: color-mix(in srgb, var(--color-warning) 18%, transparent);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.test-mode-banner {
|
||||
padding: var(--space-sm);
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
19
packages/dashboard/app/components/TestModeBanner.tsx
Normal file
19
packages/dashboard/app/components/TestModeBanner.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { FlaskConical } from "lucide-react";
|
||||
import "./TestModeBanner.css";
|
||||
|
||||
interface TestModeBannerProps {
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export function TestModeBanner({ isActive }: TestModeBannerProps) {
|
||||
if (!isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="test-mode-banner" role="status" aria-live="polite">
|
||||
<FlaskConical aria-hidden="true" />
|
||||
<span>Test mode — no real AI calls</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { SettingsModal } from "../SettingsModal";
|
||||
|
||||
const mockFetchSettings = vi.fn();
|
||||
const mockFetchSettingsByScope = vi.fn();
|
||||
|
||||
vi.mock("../../api", async (importOriginal) => {
|
||||
const { createDashboardApiMock } = await import("../../test/mockApi");
|
||||
return createDashboardApiMock(() => importOriginal<typeof import("../../api")>(), {
|
||||
fetchSettings: (...args: unknown[]) => mockFetchSettings(...args),
|
||||
fetchSettingsByScope: (...args: unknown[]) => mockFetchSettingsByScope(...args),
|
||||
});
|
||||
});
|
||||
|
||||
vi.mock("../../hooks/useMemoryBackendStatus", () => ({
|
||||
useMemoryBackendStatus: () => ({ status: null, capabilities: null, loading: false, error: null, refresh: vi.fn() }),
|
||||
}));
|
||||
vi.mock("../../hooks/useViewportMode", () => ({ useViewportMode: () => "desktop" }));
|
||||
vi.mock("../../hooks/useMobileKeyboard", () => ({
|
||||
useMobileKeyboard: () => ({ keyboardOverlap: 0, viewportHeight: null, viewportOffsetTop: 0, keyboardOpen: false }),
|
||||
}));
|
||||
vi.mock("../../hooks/useMobileScrollLock", () => ({ useMobileScrollLock: vi.fn() }));
|
||||
vi.mock("../../hooks/useConfirm", () => ({ useConfirm: () => ({ confirm: vi.fn() }) }));
|
||||
vi.mock("../../hooks/useWorkspaceFileBrowser", () => ({
|
||||
useWorkspaceFileBrowser: () => ({ entries: [], currentPath: ".", setPath: vi.fn(), loading: false, error: null, refresh: vi.fn() }),
|
||||
}));
|
||||
vi.mock("../../hooks/useWorktrunkInstallStatus", () => ({
|
||||
useWorktrunkInstallStatus: () => ({ status: "idle", requestInstall: vi.fn() }),
|
||||
}));
|
||||
|
||||
function buildSettings(testMode: boolean) {
|
||||
return {
|
||||
autoMerge: true,
|
||||
testMode,
|
||||
maxConcurrent: 2,
|
||||
maxTriageConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 15000,
|
||||
heartbeatMultiplier: 1,
|
||||
groupOverlappingFiles: true,
|
||||
overlapIgnorePaths: [],
|
||||
mergeStrategy: "direct",
|
||||
mergeIntegrationWorktree: "reuse-task-worktree",
|
||||
recycleWorktrees: false,
|
||||
executorAllowSiblingBranchRename: false,
|
||||
worktreeNaming: "random",
|
||||
worktreesDir: "",
|
||||
worktrunk: { enabled: false, binaryPath: "", onFailure: "fail" },
|
||||
includeTaskIdInCommit: true,
|
||||
ntfyEnabled: false,
|
||||
failureNotificationMode: "sticky-only",
|
||||
failureNotificationDelayMs: 30000,
|
||||
webhookEnabled: false,
|
||||
experimentalFeatures: {},
|
||||
};
|
||||
}
|
||||
|
||||
describe("SettingsModal testMode toggle", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
const merged = buildSettings(true);
|
||||
mockFetchSettings.mockResolvedValue(merged);
|
||||
mockFetchSettingsByScope.mockResolvedValue({ global: {}, project: { testMode: true } });
|
||||
});
|
||||
|
||||
it("renders merge test mode toggle with initial checked state", async () => {
|
||||
render(<SettingsModal onClose={() => {}} addToast={() => {}} initialSection="merge" />);
|
||||
|
||||
const toggle = await screen.findByLabelText("Enable test mode");
|
||||
expect(toggle).toBeChecked();
|
||||
});
|
||||
|
||||
it("flips checkbox state when clicked", async () => {
|
||||
render(<SettingsModal onClose={() => {}} addToast={() => {}} initialSection="merge" />);
|
||||
|
||||
const toggle = await screen.findByLabelText("Enable test mode");
|
||||
expect(toggle).toBeChecked();
|
||||
|
||||
await userEvent.click(toggle);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toggle).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { TestModeBanner } from "../TestModeBanner";
|
||||
|
||||
describe("TestModeBanner", () => {
|
||||
it("renders nothing when inactive", () => {
|
||||
const { container } = render(<TestModeBanner isActive={false} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it("renders status copy when active", () => {
|
||||
render(<TestModeBanner isActive />);
|
||||
expect(screen.getByRole("status")).toHaveTextContent("Test mode — no real AI calls");
|
||||
});
|
||||
});
|
||||
@@ -43,6 +43,8 @@ describe("useAppSettings", () => {
|
||||
expect(result.current.maxConcurrent).toBe(4);
|
||||
expect(result.current.rootDir).toBe("/workspace/project");
|
||||
expect(result.current.autoMerge).toBe(false);
|
||||
expect(result.current.testMode).toBe(false);
|
||||
expect(result.current.isTestMode).toBe(false);
|
||||
expect(result.current.globalPaused).toBe(true);
|
||||
expect(result.current.enginePaused).toBe(false);
|
||||
expect(result.current.prAuthAvailable).toBe(true);
|
||||
@@ -120,6 +122,25 @@ describe("useAppSettings", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("derives isTestMode from defaultProvider=mock", async () => {
|
||||
mockFetchSettings.mockResolvedValueOnce({
|
||||
autoMerge: false,
|
||||
globalPause: true,
|
||||
enginePaused: false,
|
||||
prAuthAvailable: true,
|
||||
taskStuckTimeoutMs: 600000,
|
||||
showQuickChatFAB: false,
|
||||
defaultProvider: "mock",
|
||||
} as never);
|
||||
|
||||
const { result } = renderHook(() => useAppSettings("proj_123"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.testMode).toBe(false);
|
||||
expect(result.current.isTestMode).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("propagates capacity risk settings from fetchSettings", async () => {
|
||||
mockFetchSettings.mockResolvedValueOnce({
|
||||
autoMerge: false,
|
||||
|
||||
@@ -9,6 +9,8 @@ export interface UseAppSettingsResult {
|
||||
maxConcurrent: number;
|
||||
rootDir: string;
|
||||
autoMerge: boolean;
|
||||
testMode: boolean;
|
||||
isTestMode: boolean;
|
||||
globalPaused: boolean;
|
||||
enginePaused: boolean;
|
||||
taskStuckTimeoutMs: number | undefined;
|
||||
@@ -42,6 +44,8 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
const [maxConcurrent, setMaxConcurrent] = useState(2);
|
||||
const [rootDir, setRootDir] = useState<string>(".");
|
||||
const [autoMerge, setAutoMerge] = useState(true);
|
||||
const [testMode, setTestMode] = useState(false);
|
||||
const [isTestMode, setIsTestMode] = useState(false);
|
||||
const [globalPaused, setGlobalPaused] = useState(false);
|
||||
const [enginePaused, setEnginePaused] = useState(false);
|
||||
const [taskStuckTimeoutMs, setTaskStuckTimeoutMs] = useState<number | undefined>(undefined);
|
||||
@@ -78,6 +82,10 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
if (settingsResult.status === "fulfilled") {
|
||||
const settings = settingsResult.value;
|
||||
setAutoMerge(Boolean(settings.autoMerge));
|
||||
const nextTestMode = settings.testMode === true;
|
||||
const nextIsTestMode = nextTestMode || settings.defaultProvider?.trim().toLowerCase() === "mock";
|
||||
setTestMode(nextTestMode);
|
||||
setIsTestMode(nextIsTestMode);
|
||||
setGlobalPaused(Boolean(settings.globalPause));
|
||||
setEnginePaused(Boolean(settings.enginePaused));
|
||||
setPrAuthAvailable(Boolean(settings.prAuthAvailable));
|
||||
@@ -183,6 +191,8 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
maxConcurrent,
|
||||
rootDir,
|
||||
autoMerge,
|
||||
testMode,
|
||||
isTestMode,
|
||||
globalPaused,
|
||||
enginePaused,
|
||||
taskStuckTimeoutMs,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<title>Fusion</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
@@ -3189,6 +3189,13 @@ input[type="range"]:focus-visible {
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.board {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* === Mobile Responsive Overrides ===
|
||||
Cross-cutting mobile rules only. Component-specific overrides have been
|
||||
moved into co-located @media blocks at the bottom of each component's
|
||||
|
||||
@@ -12,7 +12,7 @@ const qualityAppTests = [
|
||||
"app/api/**/*.test.ts",
|
||||
// Representative workflow/component coverage. Exhaustive modal/view suites
|
||||
// stay available in the full `dashboard-app` project.
|
||||
"app/components/__tests__/{ActiveAgentsPanel,ActivityLogModal,AgentMentionPopup,AgentMetricsBar,AgentOnboardingModal,AgentReflectionsTab,AgentTokenStatsPanel,App,AuthTokenRecoveryDialog,Board,board-mobile,board-mobile-view-switch,ChatView,ChatView.autosize,ChatView.chat-input-autosize,ChatView.default-model-icon,ChatView.draft,ChatView.hash-mention,ChatView.rooms,ChatView.scroll-to-top,ChatView.swipe-back,Column,ConfirmDialog,ConversationHistory,DashboardLoader,DevServerView.mobile,DirectoryPicker,DuplicateWarningModal,ErrorBoundary,ExecutorStatusBar,FileBrowser,FileEditor,GitHubBadge,InlineCreateCard,LoginInstructions,MemoryView,MergeAdvanceNotice,MessageComposer,MessageComposer.autosize,MobileNavBar,NewTaskModal,NewTaskModal.shared-cache,NodeCard,NodeHealthDot,NodeStatusIndicator,PlanningModeModal.autosize,PrChecksList,PrCreateModal,PrCreateModal.layout,ProjectCard,ProjectSelector,ProviderIcon,PrPanel,PrPanel.merge,PrPanel.reviews,QuickChatFAB,QuickChatFAB.shared-cache,ReliabilityView,ResearchView,SecretsView,SecretsView.mobile,SettingsModal,SettingsModal.worktrunk,StashConflictModal,StashRecoveryView,TaskCard,TaskCard.badge-height,TaskCard.badge-wrap,TaskCard.footer-wrap,TaskChangesTab,TaskComments,TaskDetailModal,TaskDetailModal.create-pr-e2e,TaskDetailModal.create-pr-integration,TaskDetailModal.github-tracking-header,TaskDetailModal.github-tracking-stale,TaskDetailModal.rebind-banner,TaskDocumentsTab,TaskForm,TaskIdIntegrityBanner,TrackingRepoSelect,WorkflowResultsTab,WorktrunkInstallApprovalDetails}.test.tsx",
|
||||
"app/components/__tests__/{ActiveAgentsPanel,ActivityLogModal,AgentMentionPopup,AgentMetricsBar,AgentOnboardingModal,AgentReflectionsTab,AgentTokenStatsPanel,App,AuthTokenRecoveryDialog,Board,board-mobile,board-mobile-view-switch,ChatView,ChatView.autosize,ChatView.chat-input-autosize,ChatView.default-model-icon,ChatView.draft,ChatView.hash-mention,ChatView.rooms,ChatView.scroll-to-top,ChatView.swipe-back,Column,ConfirmDialog,ConversationHistory,DashboardLoader,DevServerView.mobile,DirectoryPicker,DuplicateWarningModal,ErrorBoundary,ExecutorStatusBar,FileBrowser,FileEditor,GitHubBadge,InlineCreateCard,LoginInstructions,MemoryView,MergeAdvanceNotice,MessageComposer,MessageComposer.autosize,MobileNavBar,NewTaskModal,NewTaskModal.shared-cache,NodeCard,NodeHealthDot,NodeStatusIndicator,PlanningModeModal.autosize,PrChecksList,PrCreateModal,PrCreateModal.layout,ProjectCard,ProjectSelector,ProviderIcon,PrPanel,PrPanel.merge,PrPanel.reviews,QuickChatFAB,QuickChatFAB.shared-cache,ReliabilityView,ResearchView,SecretsView,SecretsView.mobile,SettingsModal,SettingsModal.testMode,SettingsModal.worktrunk,StashConflictModal,StashRecoveryView,TaskCard,TaskCard.badge-height,TaskCard.badge-wrap,TaskCard.footer-wrap,TaskChangesTab,TaskComments,TaskDetailModal,TaskDetailModal.create-pr-e2e,TestModeBanner,TaskDetailModal.create-pr-integration,TaskDetailModal.github-tracking-header,TaskDetailModal.github-tracking-stale,TaskDetailModal.rebind-banner,TaskDocumentsTab,TaskForm,TaskIdIntegrityBanner,TrackingRepoSelect,WorkflowResultsTab,WorktrunkInstallApprovalDetails}.test.tsx",
|
||||
// Hooks and utilities are fast, user-visible state/formatting behavior.
|
||||
"app/context/**/*.test.tsx",
|
||||
"app/hooks/__tests__/{useAgents,useAgentLogs,useAgentLogs.resume-instrumentation,useAppSettings,useAuthOnboarding,useConfirm,useCurrentProject,useNodes,useNodes.resume-instrumentation,useNodeSettingsSync,useProjects,useProjects.resume-instrumentation,useMeshState.resume-instrumentation,useManagedDockerNodes.resume-instrumentation,usePrChecksStream.resume-instrumentation,useDevServerLogs.resume-instrumentation,useResearch.resume-instrumentation,useBackgroundSessions.resume-instrumentation,useQuickChat,useTasks,useTasks.resume-instrumentation,useChatRooms.resume-instrumentation,useTerminalSessions,useTheme,useToast,useUsageData,useViewState}.test.{ts,tsx}",
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
resolveExecutorSessionModel,
|
||||
resolveHeartbeatSessionModels,
|
||||
resolveMergerSessionModel,
|
||||
resolvePlanningSessionModel,
|
||||
} from "../agent-session-helpers.js";
|
||||
|
||||
const assignedAgentRuntimeConfig = {
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
};
|
||||
|
||||
describe("agent-session-helpers test mode overrides", () => {
|
||||
it("forces every resolver lane to mock when settings.testMode is true", () => {
|
||||
const settings = {
|
||||
testMode: true,
|
||||
defaultProvider: "openai",
|
||||
defaultModelId: "gpt-4.1",
|
||||
executionProvider: "openai",
|
||||
executionModelId: "gpt-4.1",
|
||||
planningProvider: "openai",
|
||||
planningModelId: "gpt-4.1",
|
||||
defaultProviderOverride: "openai",
|
||||
defaultModelIdOverride: "gpt-4.1",
|
||||
};
|
||||
|
||||
expect(resolveExecutorSessionModel("openai", "gpt-4.1", settings, assignedAgentRuntimeConfig)).toEqual({
|
||||
provider: "mock",
|
||||
modelId: "scripted",
|
||||
});
|
||||
expect(resolvePlanningSessionModel("openai", "gpt-4.1", settings, assignedAgentRuntimeConfig)).toEqual({
|
||||
provider: "mock",
|
||||
modelId: "scripted",
|
||||
});
|
||||
expect(resolveMergerSessionModel(settings, assignedAgentRuntimeConfig)).toEqual({
|
||||
provider: "mock",
|
||||
modelId: "scripted",
|
||||
});
|
||||
expect(resolveHeartbeatSessionModels(settings, assignedAgentRuntimeConfig)).toEqual({
|
||||
defaultProvider: "mock",
|
||||
defaultModelId: "scripted",
|
||||
fallbackProvider: undefined,
|
||||
fallbackModelId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("forces every resolver lane to mock when defaultProvider is mock", () => {
|
||||
const settings = {
|
||||
defaultProvider: "mock",
|
||||
defaultModelId: "custom",
|
||||
executionProvider: "openai",
|
||||
executionModelId: "gpt-4.1",
|
||||
planningProvider: "openai",
|
||||
planningModelId: "gpt-4.1",
|
||||
defaultProviderOverride: "openai",
|
||||
defaultModelIdOverride: "gpt-4.1",
|
||||
};
|
||||
|
||||
expect(resolveExecutorSessionModel("openai", "gpt-4.1", settings, assignedAgentRuntimeConfig)).toEqual({
|
||||
provider: "mock",
|
||||
modelId: "scripted",
|
||||
});
|
||||
expect(resolvePlanningSessionModel("openai", "gpt-4.1", settings, assignedAgentRuntimeConfig)).toEqual({
|
||||
provider: "mock",
|
||||
modelId: "scripted",
|
||||
});
|
||||
expect(resolveMergerSessionModel(settings, assignedAgentRuntimeConfig)).toEqual({
|
||||
provider: "mock",
|
||||
modelId: "scripted",
|
||||
});
|
||||
expect(resolveHeartbeatSessionModels(settings, assignedAgentRuntimeConfig)).toEqual({
|
||||
defaultProvider: "mock",
|
||||
defaultModelId: "scripted",
|
||||
fallbackProvider: undefined,
|
||||
fallbackModelId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps existing behavior when test mode is inactive", () => {
|
||||
const settings = {
|
||||
executionProvider: "openai",
|
||||
executionModelId: "gpt-4.1",
|
||||
planningProvider: "openai",
|
||||
planningModelId: "gpt-4.1",
|
||||
defaultProviderOverride: "openai",
|
||||
defaultModelIdOverride: "gpt-4.1",
|
||||
defaultProvider: "openai",
|
||||
defaultModelId: "gpt-4.1",
|
||||
};
|
||||
|
||||
expect(resolveExecutorSessionModel("task-provider", "task-model", settings, assignedAgentRuntimeConfig)).toEqual({
|
||||
provider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
});
|
||||
expect(resolvePlanningSessionModel("task-provider", "task-model", settings, assignedAgentRuntimeConfig)).toEqual({
|
||||
provider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
});
|
||||
expect(resolveMergerSessionModel(settings, assignedAgentRuntimeConfig)).toEqual({
|
||||
provider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
});
|
||||
expect(resolveHeartbeatSessionModels(settings, assignedAgentRuntimeConfig)).toEqual({
|
||||
defaultProvider: "anthropic",
|
||||
defaultModelId: "claude-sonnet-4-5",
|
||||
fallbackProvider: "openai",
|
||||
fallbackModelId: "gpt-4.1",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,9 +12,11 @@ import type { SkillSelectionContext } from "./skill-resolver.js";
|
||||
import type { PluginRunner } from "./plugin-runner.js";
|
||||
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
||||
import {
|
||||
isTestModeActive,
|
||||
resolveExecutionSettingsModel,
|
||||
resolveTaskExecutionModel,
|
||||
resolveTaskPlanningModel,
|
||||
TEST_MODE_RESOLVED,
|
||||
type Settings,
|
||||
} from "@fusion/core";
|
||||
import { resolveRuntime, buildRuntimeResolutionContext, isMockProviderId, type SessionPurpose } from "./runtime-resolution.js";
|
||||
@@ -122,6 +124,13 @@ export function resolveExecutorSessionModel(
|
||||
settings: Partial<Settings> | undefined,
|
||||
assignedAgentRuntimeConfig?: Record<string, unknown>,
|
||||
): { provider: string | undefined; modelId: string | undefined } {
|
||||
if (isTestModeActive(settings)) {
|
||||
return {
|
||||
provider: TEST_MODE_RESOLVED.provider,
|
||||
modelId: TEST_MODE_RESOLVED.modelId,
|
||||
};
|
||||
}
|
||||
|
||||
const assignedRuntimeModel = extractRuntimeModel(assignedAgentRuntimeConfig);
|
||||
if (assignedRuntimeModel.provider && assignedRuntimeModel.modelId) {
|
||||
return assignedRuntimeModel;
|
||||
@@ -147,6 +156,13 @@ export function resolvePlanningSessionModel(
|
||||
settings: Partial<Settings> | undefined,
|
||||
assignedAgentRuntimeConfig?: Record<string, unknown>,
|
||||
): { provider: string | undefined; modelId: string | undefined } {
|
||||
if (isTestModeActive(settings)) {
|
||||
return {
|
||||
provider: TEST_MODE_RESOLVED.provider,
|
||||
modelId: TEST_MODE_RESOLVED.modelId,
|
||||
};
|
||||
}
|
||||
|
||||
const assignedRuntimeModel = extractRuntimeModel(assignedAgentRuntimeConfig);
|
||||
if (assignedRuntimeModel.provider && assignedRuntimeModel.modelId) {
|
||||
return assignedRuntimeModel;
|
||||
@@ -175,6 +191,15 @@ export function resolveHeartbeatSessionModels(
|
||||
fallbackProvider: string | undefined;
|
||||
fallbackModelId: string | undefined;
|
||||
} {
|
||||
if (isTestModeActive(settings)) {
|
||||
return {
|
||||
defaultProvider: TEST_MODE_RESOLVED.provider,
|
||||
defaultModelId: TEST_MODE_RESOLVED.modelId,
|
||||
fallbackProvider: undefined,
|
||||
fallbackModelId: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const assignedRuntimeModel = extractRuntimeModel(assignedAgentRuntimeConfig);
|
||||
const executionSettingsModel = resolveExecutionSettingsModel(settings);
|
||||
|
||||
@@ -197,6 +222,13 @@ export function resolveMergerSessionModel(
|
||||
settings: Partial<Settings> | undefined,
|
||||
assignedAgentRuntimeConfig?: Record<string, unknown>,
|
||||
): { provider: string | undefined; modelId: string | undefined } {
|
||||
if (isTestModeActive(settings)) {
|
||||
return {
|
||||
provider: TEST_MODE_RESOLVED.provider,
|
||||
modelId: TEST_MODE_RESOLVED.modelId,
|
||||
};
|
||||
}
|
||||
|
||||
const assignedRuntimeModel = extractRuntimeModel(assignedAgentRuntimeConfig);
|
||||
if (assignedRuntimeModel.provider && assignedRuntimeModel.modelId) {
|
||||
return assignedRuntimeModel;
|
||||
|
||||
Reference in New Issue
Block a user