mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-22 23:22:09 +03:00
fix(autopilot): show real suggestion count and link dashboard (#11102)
Validated on the combined batch board over release/v3.8.50 tip d91238b7: static gates clean, typecheck:core clean, focused tests green.
Real suggestionCount replaces the conflated link count (deprecated alias kept), dashboard deep-link fixed. Thank you @maxmad64bis!
This commit is contained in:
1
changelog.d/fixes/11102-combo-suggestion-count.md
Normal file
1
changelog.d/fixes/11102-combo-suggestion-count.md
Normal file
@@ -0,0 +1 @@
|
||||
- **Autopilot suggestion counter:** the combo health autopilot summary now reports `suggestionCount` (the real number of suggested actions across all issues) instead of conflating it with link counts, while keeping `actionableCount` as a deprecated alias for backward compatibility. The `run_combo_test` action now links to the dashboard with the combo id (`/dashboard/combos?test=<comboId>`) rather than the read-only API route, so operators can actually trigger a test from the UI ([#11102](https://github.com/diegosouzapw/OmniRoute/pull/11102)).
|
||||
@@ -291,7 +291,9 @@ function ComboAutopilotPanel({ report }: { report: ComboAutopilotReport }) {
|
||||
icon="monitor_heart"
|
||||
label={t("comboHealthIssues")}
|
||||
value={report.summary.issueCount.toLocaleString()}
|
||||
subValue={t("comboHealthActionable", { count: report.summary.actionableCount })}
|
||||
subValue={t("comboHealthActionable", {
|
||||
count: report.summary.suggestionCount ?? report.summary.actionableCount ?? 0,
|
||||
})}
|
||||
/>
|
||||
<MetricBlock
|
||||
icon="error"
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
ComboForecastMetrics,
|
||||
ComboForecastResponse,
|
||||
ComboForecastRiskLevel,
|
||||
ProviderAutopilotReport,
|
||||
ComboHealthMetrics,
|
||||
ComboHealthResponse,
|
||||
ComboRecord,
|
||||
@@ -34,6 +35,7 @@ export interface ComboHealthAutopilotOptions {
|
||||
combos?: ComboRecord[];
|
||||
healthResponse?: ComboHealthResponse;
|
||||
forecastResponse?: ComboForecastResponse;
|
||||
providerHealthResponse?: ProviderAutopilotReport;
|
||||
}
|
||||
|
||||
type ProviderIssueView = {
|
||||
@@ -103,7 +105,12 @@ function actionSet(
|
||||
case "open_combo_editor":
|
||||
return action(type, "Open combo editor", target, "/dashboard/combos");
|
||||
case "run_combo_test":
|
||||
return action(type, "Run combo test", target, "/dashboard/combos");
|
||||
return action(
|
||||
type,
|
||||
"Run combo test",
|
||||
target,
|
||||
`/dashboard/combos?test=${encodeURIComponent(target.comboId)}`
|
||||
);
|
||||
case "open_provider_health_autopilot":
|
||||
return action(type, "Open provider autopilot", target, "/dashboard/health");
|
||||
case "review_quota_limits":
|
||||
@@ -447,7 +454,8 @@ export async function buildComboHealthAutopilotReport(
|
||||
now: options.now,
|
||||
combos: combosSnapshot,
|
||||
}),
|
||||
buildProviderHealthAutopilotReport({ includeHealthy: false, includeActions: false }),
|
||||
options.providerHealthResponse ??
|
||||
buildProviderHealthAutopilotReport({ includeHealthy: false, includeActions: false }),
|
||||
]);
|
||||
|
||||
const forecastsByComboId = new Map(forecast.combos.map((entry) => [entry.comboId, entry]));
|
||||
@@ -470,7 +478,7 @@ export async function buildComboHealthAutopilotReport(
|
||||
const degradedCount = allCombos.filter((combo) => combo.state === "degraded").length;
|
||||
const healthyCount = allCombos.filter((combo) => combo.state === "healthy").length;
|
||||
const issueCount = allCombos.reduce((sum, combo) => sum + combo.issues.length, 0);
|
||||
const actionableCount = allCombos.reduce(
|
||||
const suggestionCount = allCombos.reduce(
|
||||
(sum, combo) =>
|
||||
sum + combo.issues.reduce((issueSum, issue) => issueSum + issue.actions.length, 0),
|
||||
0
|
||||
@@ -487,7 +495,8 @@ export async function buildComboHealthAutopilotReport(
|
||||
degradedCount,
|
||||
downCount,
|
||||
issueCount,
|
||||
actionableCount,
|
||||
suggestionCount,
|
||||
actionableCount: suggestionCount,
|
||||
},
|
||||
combos,
|
||||
};
|
||||
|
||||
@@ -260,7 +260,9 @@ export interface ComboAutopilotReport {
|
||||
degradedCount: number;
|
||||
downCount: number;
|
||||
issueCount: number;
|
||||
actionableCount: number;
|
||||
suggestionCount: number;
|
||||
/** @deprecated Use suggestionCount instead. Kept as an alias for backward compatibility; remove after 2 releases. */
|
||||
actionableCount?: number;
|
||||
};
|
||||
combos: ComboAutopilotCombo[];
|
||||
}
|
||||
|
||||
108
tests/unit/combo-health-autopilot-counter.test.ts
Normal file
108
tests/unit/combo-health-autopilot-counter.test.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import type {
|
||||
ComboForecastResponse,
|
||||
ComboHealthResponse,
|
||||
ProviderAutopilotReport,
|
||||
} from "../../src/shared/types/utilization.ts";
|
||||
import { buildComboHealthAutopilotReport } from "../../src/lib/monitoring/comboHealthAutopilot.ts";
|
||||
|
||||
function healthResponse(): ComboHealthResponse {
|
||||
return {
|
||||
timeRange: "24h",
|
||||
combos: [
|
||||
{
|
||||
comboId: "c1",
|
||||
comboName: "my-combo",
|
||||
strategy: "fallback",
|
||||
models: [],
|
||||
targetHealth: [
|
||||
{
|
||||
executionKey: "e1",
|
||||
stepId: "s1",
|
||||
model: "m",
|
||||
provider: "p",
|
||||
connectionId: null,
|
||||
label: null,
|
||||
requests: 5,
|
||||
successRate: 90,
|
||||
avgLatencyMs: 100,
|
||||
lastStatus: "error",
|
||||
lastUsedAt: null,
|
||||
quotaRemainingPct: 50,
|
||||
quotaIsExhausted: false,
|
||||
quotaTrend: "stable",
|
||||
quotaScope: "provider",
|
||||
},
|
||||
],
|
||||
quotaHealth: { providers: [], worstRemainingPct: 100 },
|
||||
usageSkew: { modelDistribution: [], giniCoefficient: 0 },
|
||||
performance: { avgLatencyMs: 100, successRate: 1.0, totalRequests: 10 },
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function forecastResponse(): ComboForecastResponse {
|
||||
return {
|
||||
timeRange: "24h",
|
||||
horizon: "30d",
|
||||
asOf: new Date(0).toISOString(),
|
||||
method: "linear_history",
|
||||
combos: [],
|
||||
};
|
||||
}
|
||||
|
||||
function providerHealthResponse(): ProviderAutopilotReport {
|
||||
return { providers: [] } as unknown as ProviderAutopilotReport;
|
||||
}
|
||||
|
||||
function buildOptions() {
|
||||
return {
|
||||
range: "24h" as const,
|
||||
horizon: "30d" as const,
|
||||
healthResponse: healthResponse(),
|
||||
forecastResponse: forecastResponse(),
|
||||
providerHealthResponse: providerHealthResponse(),
|
||||
};
|
||||
}
|
||||
|
||||
describe("combo health autopilot counter", () => {
|
||||
it("exposes suggestionCount and keeps actionableCount alias", async () => {
|
||||
const report = await buildComboHealthAutopilotReport(buildOptions());
|
||||
assert.equal(typeof report.summary.suggestionCount, "number");
|
||||
assert.equal(report.summary.actionableCount, report.summary.suggestionCount);
|
||||
const expected = report.combos.reduce(
|
||||
(sum, combo) =>
|
||||
sum + combo.issues.reduce((issueSum, issue) => issueSum + issue.actions.length, 0),
|
||||
0
|
||||
);
|
||||
assert.equal(report.summary.suggestionCount, expected);
|
||||
});
|
||||
|
||||
it("run_combo_test action links the dashboard with the combo id", async () => {
|
||||
const report = await buildComboHealthAutopilotReport(buildOptions());
|
||||
const actions = report.combos.flatMap((combo) => combo.issues.flatMap((i) => i.actions));
|
||||
const runTest = actions.find((a) => a.type === "run_combo_test");
|
||||
assert.ok(runTest, "run_combo_test action should exist");
|
||||
assert.equal(typeof runTest.href, "string");
|
||||
assert.ok(runTest.href?.includes("c1"), "href must carry the combo id");
|
||||
assert.equal(
|
||||
runTest.href?.includes("/api/combos/test?comboId="),
|
||||
false,
|
||||
"href must not target the GET-only API route (405)"
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps every action in manual mode", async () => {
|
||||
const report = await buildComboHealthAutopilotReport(buildOptions());
|
||||
for (const combo of report.combos) {
|
||||
for (const issue of combo.issues) {
|
||||
for (const action of issue.actions) {
|
||||
assert.equal(action.mode, "manual");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user