Add monitor-first Patrol browser proof

This commit is contained in:
rcourtman 2026-06-30 11:26:36 +01:00
parent 229867636d
commit d4e4c7ed79
4 changed files with 529 additions and 2 deletions

View file

@ -51,6 +51,7 @@ Patrol-specific presentation helpers.
28. `frontend-modern/src/utils/patrolRuntimeActions.ts`
29. `frontend-modern/src/utils/textPresentation.ts`
30. `tests/integration/tests/73-patrol-assistant-operator-briefing.spec.ts`
31. `tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts`
## Shared Boundaries
@ -1035,6 +1036,16 @@ successes: the Patrol popover must keep the saved provider/model visible,
render a `Patrol control needs attention` inline state with the returned
readiness cause and summary, and hand off to Assistant as a model-only Patrol
control issue rather than as a save failure.
A separate browser proof in
`tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts` keeps the
authenticated launch workbench monitor-first. When the runtime has monitored
Proxmox resources, legacy infrastructure entry points must resolve to the
Proxmox monitoring lens before Patrol or Assistant chrome, while the stable
Patrol tab may still expose the open-work count. Calm-day copy such as `No
current issues` and checked-resource coverage belongs inside the Patrol route
after the operator chooses Patrol; the monitor lens must not become a hidden
Patrol all-clear page, and Assistant launchers must stay scoped to the current
surface.
The readiness contract now applies before Patrol work is admitted, not only
after a page render: recoverable Patrol provider/model settings saves must
persist and echo structured readiness cause metadata, manual run requests must

View file

@ -4956,7 +4956,8 @@
"frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts",
"frontend-modern/src/utils/__tests__/patrolPagePresentation.test.ts",
"tests/integration/tests/18-patrol-runtime-state.spec.ts",
"tests/integration/tests/73-patrol-assistant-operator-briefing.spec.ts"
"tests/integration/tests/73-patrol-assistant-operator-briefing.spec.ts",
"tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts"
]
},
{

View file

@ -23,6 +23,7 @@ PATROL_PAGE_AND_STATE_EXACT_FILES = [
"frontend-modern/src/utils/__tests__/patrolPagePresentation.test.ts",
"tests/integration/tests/18-patrol-runtime-state.spec.ts",
"tests/integration/tests/73-patrol-assistant-operator-briefing.spec.ts",
"tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts",
]
PLATFORM_CONNECTIONS_WORKSPACE_EXACT_FILES = [
@ -2916,7 +2917,7 @@ class SubsystemLookupTest(unittest.TestCase):
{
"heading": "## Shared Boundaries",
"path": "internal/api/access_control_handlers.go",
"line": 1093,
"line": 1099,
"heading_line": 137,
}
],

View file

@ -0,0 +1,514 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { expect, test as base, type Page } from "@playwright/test";
import { createAuthenticatedStorageState } from "./helpers";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
type WorkerFixtures = {
authStorageStatePath: string;
};
type PatrolWorkbenchFixtureOptions = {
findingCount?: number;
resourcesChecked?: number;
runStatus?: "healthy" | "issues_found";
};
const test = base.extend<{}, WorkerFixtures>({
storageState: async ({ authStorageStatePath }, use) => {
await use(authStorageStatePath);
},
authStorageStatePath: [
async ({ browser }, use, workerInfo) => {
const storageStatePath = path.resolve(
__dirname,
"..",
"..",
"tmp",
"playwright-auth",
`monitor-first-patrol-workbench-${workerInfo.project.name}.json`,
);
fs.mkdirSync(path.dirname(storageStatePath), { recursive: true });
await createAuthenticatedStorageState(browser, storageStatePath);
try {
await use(storageStatePath);
} finally {
fs.rmSync(storageStatePath, { force: true });
}
},
{ scope: "worker" },
],
});
const monitoredProxmoxResource = {
id: "node:pve-main",
type: "agent",
name: "pve-main",
displayName: "pve-main",
status: "online",
lastSeen: "2026-06-30T08:00:00Z",
platformType: "proxmox-pve",
sourceType: "agent",
sources: ["agent", "proxmox"],
platformScopes: ["proxmox-pve"],
metrics: {
cpu: { value: 12, unit: "%" },
memory: {
used: 4_294_967_296,
total: 17_179_869_184,
percent: 25,
unit: "bytes",
},
disk: {
used: 68_719_476_736,
total: 274_877_906_944,
percent: 25,
unit: "bytes",
},
},
proxmox: {
nodeName: "pve-main",
clusterName: "homelab",
temperature: 42,
},
agent: {
agentId: "agent-pve-main",
agentVersion: "6.0.0",
hostname: "pve-main",
osName: "Proxmox VE",
uptimeSeconds: 86_400,
},
};
const buildRuntimeState = () => ({
connectedInfrastructure: [],
metrics: [],
performance: {
apiCallDuration: {},
lastPollDuration: 0,
pollingStartTime: "",
totalApiCalls: 0,
failedApiCalls: 0,
cacheHits: 0,
cacheMisses: 0,
},
connectionHealth: {},
stats: {
startTime: "2026-06-30T08:00:00Z",
uptime: 3600,
pollingCycles: 1,
webSocketClients: 1,
version: "6.0.0-rc.7",
},
activeAlerts: [],
recentlyResolved: [],
pveTagColors: {},
pveTagStyles: {},
lastUpdate: Date.parse("2026-06-30T08:00:00Z"),
resources: [monitoredProxmoxResource],
});
const buildRunHistory = (options: Required<PatrolWorkbenchFixtureOptions>) => [
{
id: options.findingCount > 0 ? "run-active-work" : "run-calm-day",
started_at: "2026-06-30T08:05:00Z",
completed_at: "2026-06-30T08:05:42Z",
duration_ms: 42_000,
type: "full",
trigger_reason: "scheduled",
resources_checked: options.resourcesChecked,
nodes_checked: 1,
guests_checked: 0,
docker_checked: 0,
storage_checked: 0,
hosts_checked: 1,
pbs_checked: 0,
pmg_checked: 0,
kubernetes_checked: 0,
new_findings: options.findingCount,
existing_findings: 0,
rejected_findings: 0,
resolved_findings: 0,
auto_fix_count: 0,
findings_summary:
options.findingCount > 0 ? "1 active finding" : "No active findings",
finding_ids: options.findingCount > 0 ? ["finding-active-work"] : [],
error_count: 0,
status: options.runStatus,
triage_flags: 0,
tool_call_count: 1,
},
];
const buildPatrolStatus = (
options: Required<PatrolWorkbenchFixtureOptions>,
) => ({
runtime_state: "active",
running: false,
enabled: true,
last_patrol_at: "2026-06-30T08:05:42Z",
next_patrol_at: "2026-06-30T14:05:42Z",
last_duration_ms: 42_000,
resources_checked: options.resourcesChecked,
findings_count: options.findingCount,
error_count: 0,
healthy: options.findingCount === 0,
interval_ms: 21_600_000,
fixed_count: 0,
blocked_reason: "",
blocked_at: "",
license_required: false,
license_status: "active",
summary: {
critical: 0,
warning: options.findingCount,
watch: 0,
info: 0,
},
});
const buildIntelligenceSummary = (
options: Required<PatrolWorkbenchFixtureOptions>,
) => ({
timestamp: "2026-06-30T08:06:00Z",
overall_health: {
score: options.findingCount > 0 ? 74 : 100,
grade: options.findingCount > 0 ? "C" : "A",
trend: "stable",
factors: [],
prediction:
options.findingCount > 0
? "Patrol found one current issue for review."
: "Patrol checked monitored resources and found no current issues.",
},
findings_count: {
critical: 0,
warning: options.findingCount,
watch: 0,
info: 0,
total: options.findingCount,
},
predictions_count: 0,
recent_changes_count: 0,
recent_changes: [],
learning: {
resources_with_knowledge: 1,
total_notes: 0,
resources_with_baselines: 1,
patterns_detected: 0,
correlations_learned: 0,
incidents_tracked: 0,
},
});
const buildPatrolFindingsResponse = (
options: Required<PatrolWorkbenchFixtureOptions>,
) => {
if (options.findingCount === 0) {
return { findings: [], count: 0, active_count: 0 };
}
return {
findings: [
{
id: "finding-active-work",
severity: "warning",
category: "reliability",
resource_id: monitoredProxmoxResource.id,
resource_name: monitoredProxmoxResource.displayName,
resource_type: monitoredProxmoxResource.type,
title: "High CPU pressure on pve-main",
description:
"Patrol detected sustained CPU pressure on the monitored Proxmox host.",
detected_at: "2026-06-30T08:05:00Z",
last_seen_at: "2026-06-30T08:05:42Z",
auto_resolved: false,
times_raised: 1,
suppressed: false,
investigation_attempts: 0,
},
],
count: 1,
active_count: 1,
};
};
const buildPatrolFindings = (
options: Required<PatrolWorkbenchFixtureOptions>,
) => buildPatrolFindingsResponse(options).findings;
async function mockMonitorFirstPatrolWorkbench(
page: Page,
options: PatrolWorkbenchFixtureOptions = {},
): Promise<void> {
const resolved = {
findingCount: options.findingCount ?? 0,
resourcesChecked: options.resourcesChecked ?? 1,
runStatus:
options.runStatus ?? (options.findingCount ? "issues_found" : "healthy"),
} satisfies Required<PatrolWorkbenchFixtureOptions>;
await page.route("**/api/security/status", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
hasAuthentication: true,
hasHTTPS: true,
apiTokenConfigured: true,
exportProtected: true,
hideLocalLogin: false,
publicAccess: false,
requiresAuth: true,
ssoEnabled: false,
ssoProviders: [],
tokenScopes: ["*"],
sessionCapabilities: {
assistantEnabled: true,
demoMode: false,
},
presentationPolicy: {
demoMode: false,
readOnly: false,
hideCommercial: false,
hideUpgrade: false,
},
}),
});
});
await page.route("**/api/state", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(buildRuntimeState()),
});
});
await page.route("**/api/resources**", async (route) => {
const requestUrl = new URL(route.request().url());
if (requestUrl.pathname !== "/api/resources") {
await route.continue();
return;
}
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
data: [monitoredProxmoxResource],
meta: {
page: 1,
limit: 100,
total: 1,
totalPages: 1,
},
links: {
next: null,
},
}),
});
});
await page.route("**/api/replication/jobs", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([]),
});
});
await page.route("**/api/ai/patrol/status", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(buildPatrolStatus(resolved)),
});
});
await page.route("**/api/ai/patrol/runs*", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(buildRunHistory(resolved)),
});
});
await page.route("**/api/ai/patrol/autonomy", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
autonomy_level: "monitor",
full_mode_unlocked: false,
investigation_budget: 15,
investigation_timeout_sec: 300,
}),
});
});
await page.route("**/api/ai/patrol/findings*", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(buildPatrolFindings(resolved)),
});
});
await page.route("**/api/ai/unified/findings*", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(buildPatrolFindingsResponse(resolved)),
});
});
await page.route("**/api/ai/intelligence", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(buildIntelligenceSummary(resolved)),
});
});
await page.route("**/api/ai/intelligence/correlations*", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ correlations: [], count: 0 }),
});
});
await page.route("**/api/ai/circuit/status", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
state: "closed",
can_patrol: true,
consecutive_failures: 0,
total_successes: 4,
total_failures: 0,
}),
});
});
await page.route("**/api/ai/approvals", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ approvals: [] }),
});
});
await page.route("**/api/settings/ai", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
patrol_enabled: true,
patrol_interval_minutes: 360,
patrol_model: "",
model: "",
alert_triggered_analysis: false,
patrol_alert_triggers_enabled: true,
patrol_anomaly_triggers_enabled: false,
patrol_event_triggers_enabled: true,
patrol_auto_fix: false,
auto_fix_model: "",
}),
});
});
await page.route("**/api/ai/models", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ models: [] }),
});
});
}
test.describe("Monitor-first Patrol workbench browser contract", () => {
test.setTimeout(180_000);
test("legacy infrastructure launch opens the monitor lens before Patrol or Assistant chrome", async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name.startsWith("mobile-"),
"Desktop workbench routing proof",
);
await mockMonitorFirstPatrolWorkbench(page, {
findingCount: 1,
runStatus: "issues_found",
});
await page.goto("/infrastructure", { waitUntil: "domcontentloaded" });
await expect(page).toHaveURL(/\/proxmox\/overview$/, { timeout: 30_000 });
await expect(page.getByTestId("proxmox-page")).toBeVisible({
timeout: 30_000,
});
const desktopNav = page.getByRole("tablist", {
name: "Primary navigation",
});
await expect(
desktopNav.getByRole("tab", { name: "Proxmox" }),
).toBeVisible();
await expect(
desktopNav.getByRole("tab", { name: "Patrol: 1 open work item" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Ask Pulse Assistant about Proxmox" }),
).toBeVisible();
await expect(page.getByRole("heading", { name: "Open work" })).toHaveCount(
0,
);
await expect(
page.getByRole("heading", { name: /^Pulse Assistant$/ }),
).toHaveCount(0);
});
test("calm-day Patrol posture stays inside Patrol context after infrastructure launch", async ({
page,
}, testInfo) => {
test.skip(
testInfo.project.name.startsWith("mobile-"),
"Desktop workbench routing proof",
);
await mockMonitorFirstPatrolWorkbench(page, {
findingCount: 0,
runStatus: "healthy",
});
await page.goto("/infrastructure", { waitUntil: "domcontentloaded" });
await expect(page).toHaveURL(/\/proxmox\/overview$/, { timeout: 30_000 });
await expect(page.getByTestId("proxmox-page")).toBeVisible({
timeout: 30_000,
});
await expect(
page.getByRole("heading", { name: "No current issues" }),
).toHaveCount(0);
await page.getByRole("tab", { name: "Patrol" }).click();
await expect(page).toHaveURL(/\/patrol$/);
await expect(
page.getByRole("heading", { level: 1, name: "Patrol" }),
).toBeVisible();
await expect(
page.getByRole("heading", { level: 2, name: "Open work" }),
).toBeVisible();
await expect(page.getByText("No current issues").first()).toBeVisible();
await expect(page.getByText("Checked 1 resource.")).toBeVisible();
await expect(
page.getByRole("button", { name: "Ask Pulse Assistant about Patrol" }),
).toBeVisible();
await expect(page.getByText(/Nothing needs attention/i)).toHaveCount(0);
});
});