mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 22:12:23 +00:00
Carry safe action metadata into Patrol handoffs
This commit is contained in:
@@ -320,8 +320,9 @@ the canonical monitored-system blocked payload.
|
||||
governed action posture without raw command text. Structured handoff action
|
||||
references may use the current live Patrol investigation-fix approval for
|
||||
the finding when that approval is newer than the approval ID on the durable
|
||||
record, but the payload may carry only IDs, status/risk/target metadata, and
|
||||
fix/action references, never the approval command payload. Patrol
|
||||
record, but the payload may carry only IDs, status/risk/target metadata,
|
||||
safe generated approval summaries, command counts, and fix/action
|
||||
references, never the approval command payload. Patrol
|
||||
remediation-plan handoffs must use the same boundary for frontend-authored
|
||||
prompts: plan status, risk, step labels, and command counts are allowed,
|
||||
while raw command and rollback command payloads remain in governed action
|
||||
@@ -341,7 +342,11 @@ the canonical monitored-system blocked payload.
|
||||
recovery handoffs may use a still-available structured proposed-fix payload
|
||||
only as safe metadata: description, target, risk, rationale, destructive
|
||||
posture, and command count may enter the briefing, while raw command text
|
||||
remains owned by governed remediation or approval surfaces. Direct
|
||||
remains owned by governed remediation or approval surfaces. If the unified
|
||||
finding list lacks a full investigation record, frontend finding-discussion
|
||||
handoffs may hydrate the latest investigation session for the same safe
|
||||
proposed-fix metadata, but they must not paste raw proposed-fix command text
|
||||
into the authored prompt or visible briefing. Direct
|
||||
alert-investigation API handoffs through `internal/api/ai_handlers.go` must
|
||||
enforce that same request-scoped boundary by setting
|
||||
`ai.ExecuteRequest.AutonomousMode` to
|
||||
@@ -453,8 +458,8 @@ the canonical monitored-system blocked payload.
|
||||
and that same Patrol investigation-context owner, so visible Assistant
|
||||
drawer handoffs may include live pending-approval metadata only as safe
|
||||
operator context: approval ID, status, risk, requested/expiry timestamps,
|
||||
and target label are allowed, while approval command payloads stay inside
|
||||
governed approval/remediation surfaces
|
||||
target label, generated approval summary, and command count are allowed, while
|
||||
approval command payloads stay inside governed approval/remediation surfaces
|
||||
and the dedicated `frontend-modern/src/stores/aiIntelligenceSummaryModel.ts` owner, so recent-change counts and governed policy-posture fallbacks normalize once at the shared store boundary instead of as Patrol-hook-local payload repair
|
||||
and the shared `frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx` card, so learned correlations and correlation context stay rendered through one governed frontend card instead of separate page-local list loops
|
||||
and the same shared correlation card's ordering and truncation rule, so callers pass raw correlations instead of encoding their own top-N sort behavior
|
||||
|
||||
@@ -840,9 +840,13 @@ frontend primitive boundary.
|
||||
yet, rather than opening the shared drawer as empty generic chat.
|
||||
When the feature helper adds live approval state to the generic drawer
|
||||
briefing, it may pass only safe approval metadata into
|
||||
`AIChatContextBriefing`; raw approval commands remain owned by the governed
|
||||
approval/remediation panels. The shared approval-required drawer banner must
|
||||
derive its subject from that briefing or structured finding context, so
|
||||
`AIChatContextBriefing`, including generated approval summaries and command
|
||||
counts when available; raw approval commands remain owned by the governed
|
||||
approval/remediation panels. If the generic finding-level helper hydrates
|
||||
latest investigation detail to recover proposed-fix context, it may pass only
|
||||
safe summary fields and command counts into the drawer briefing. The shared
|
||||
approval-required drawer banner must derive its subject from that briefing or
|
||||
structured finding context, so
|
||||
Patrol handoffs render as Patrol handoffs or Patrol findings, and alert
|
||||
handoffs render as alert investigations, rather than generic dashboard
|
||||
briefs. Patrol approval-row Assistant prompts must
|
||||
|
||||
@@ -156,14 +156,16 @@ Patrol-specific presentation helpers.
|
||||
generic empty Assistant drawer. When a live pending Patrol approval exists
|
||||
for that finding, the visible Assistant briefing may include only safe
|
||||
approval metadata such as approval ID, pending status, risk, requested time,
|
||||
expiry, and target label; it must not copy the approval command payload into
|
||||
Assistant drawer prose. The model-only runtime briefing must apply that same
|
||||
expiry, target label, generated approval summary, and command count; it must
|
||||
not copy the approval command payload into Assistant drawer prose. The
|
||||
model-only runtime briefing must apply that same
|
||||
recovered approval reference when framing the operator decision and action
|
||||
posture. Inline Patrol approval actions that open Assistant must follow the
|
||||
same rule: pass approval ID/status/risk/target as review context, force the
|
||||
request-local approval-required mode, attach the Patrol-owned visible drawer
|
||||
briefing for the pending approval, and never paste the approval command or
|
||||
proposed-fix command text into the chat prompt. Remediation-plan Assistant
|
||||
same rule: pass approval ID/status/risk/target plus safe summary/count
|
||||
metadata as review context, force the request-local approval-required mode,
|
||||
attach the Patrol-owned visible drawer briefing for the pending approval, and
|
||||
never paste the approval command or proposed-fix command text into the chat
|
||||
prompt. Remediation-plan Assistant
|
||||
handoffs follow the same boundary: step labels, plan status, risk, and command
|
||||
counts are allowed, safe suggested prompts may ask about plan risk,
|
||||
prerequisites, rollback, and verification, while command and rollback command
|
||||
@@ -187,6 +189,12 @@ Patrol-specific presentation helpers.
|
||||
and command count. Raw command text remains in the governed remediation or
|
||||
approval panel, while Assistant gets enough context to explain approval
|
||||
recovery and risk without becoming an execution surface.
|
||||
Generic finding-level Assistant handoffs must use that same safe metadata
|
||||
boundary when the list response lacks a full investigation record: they may
|
||||
hydrate the latest investigation session to recover proposed-fix summary,
|
||||
risk, target, rationale, destructive posture, and command count, but they must
|
||||
still keep command text out of both the user-authored prompt and visible
|
||||
Assistant briefing.
|
||||
If the referenced finding is no longer current, Assistant must drop the
|
||||
stored handoff instead of continuing from stale Patrol context. Assistant
|
||||
handoff context must also carry the unified
|
||||
|
||||
@@ -20,13 +20,16 @@ import { aiChatStore } from '@/stores/aiChat';
|
||||
import {
|
||||
buildPatrolAssistantFindingBriefing,
|
||||
buildPatrolAssistantFindingPrompt,
|
||||
buildPatrolAssistantProposedFixBriefingInput,
|
||||
buildPatrolRemediationPlanAssistantBriefing,
|
||||
buildPatrolRemediationPlanAssistantPrompt,
|
||||
patrolAssistantFindingHandoffRequiresApprovalMode,
|
||||
type PatrolAssistantApprovalBriefingInput,
|
||||
type PatrolAssistantProposedFixBriefingInput,
|
||||
} from '@/features/patrol/patrolInvestigationContextModel';
|
||||
import { useResources } from '@/hooks/useResources';
|
||||
import { InvestigationSection, ApprovalSection } from '@/components/patrol';
|
||||
import type { RemediationPlan } from '@/api/ai';
|
||||
import { AIAPI, type ApprovalRequest, type RemediationPlan } from '@/api/ai';
|
||||
import type { PatrolRunRecord, PatrolRuntimeState } from '@/api/patrol';
|
||||
import { buildResolvedResourceSurfaceLinks } from '@/routing/resourceLinks';
|
||||
import { getApprovalRiskPresentation } from '@/utils/approvalRiskPresentation';
|
||||
@@ -420,6 +423,55 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
setEditingNoteId(null);
|
||||
};
|
||||
|
||||
const buildLiveApprovalProposedFixBriefing = (
|
||||
approval: ApprovalRequest | undefined,
|
||||
): PatrolAssistantProposedFixBriefingInput | undefined =>
|
||||
buildPatrolAssistantProposedFixBriefingInput(
|
||||
approval
|
||||
? {
|
||||
description: approval.context,
|
||||
riskLevel: approval.riskLevel,
|
||||
targetHost: approval.targetName,
|
||||
commandCount: approval.command ? 1 : 0,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
|
||||
const loadLatestInvestigationProposedFixBriefing = async (
|
||||
finding: UnifiedFinding,
|
||||
pendingApprovalBriefing: PatrolAssistantApprovalBriefingInput | undefined,
|
||||
): Promise<PatrolAssistantProposedFixBriefingInput | undefined> => {
|
||||
if (finding.investigationRecord?.proposed_fix) {
|
||||
return undefined;
|
||||
}
|
||||
const hasInvestigationPointer = Boolean(
|
||||
finding.investigationOutcome ||
|
||||
finding.investigationSessionId ||
|
||||
finding.lastInvestigatedAt ||
|
||||
pendingApprovalBriefing?.id,
|
||||
);
|
||||
if (!hasInvestigationPointer) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
!patrolAssistantFindingHandoffRequiresApprovalMode({
|
||||
investigationOutcome: finding.investigationOutcome,
|
||||
remediationId: finding.remediationPlanId,
|
||||
pendingApproval: pendingApprovalBriefing,
|
||||
investigationRecord: finding.investigationRecord,
|
||||
})
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const investigation = await AIAPI.getInvestigation(finding.id);
|
||||
return buildPatrolAssistantProposedFixBriefingInput(investigation?.proposed_fix);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDiscussWithAssistant = async (finding: UnifiedFinding, e: Event) => {
|
||||
e.stopPropagation();
|
||||
await aiIntelligenceStore.loadPendingApprovals();
|
||||
@@ -438,6 +490,12 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
targetName: pendingApproval.targetName,
|
||||
}
|
||||
: undefined;
|
||||
const latestInvestigationProposedFix = await loadLatestInvestigationProposedFixBriefing(
|
||||
finding,
|
||||
pendingApprovalBriefing,
|
||||
);
|
||||
const proposedFix =
|
||||
latestInvestigationProposedFix || buildLiveApprovalProposedFixBriefing(pendingApproval);
|
||||
const prompt = buildPatrolAssistantFindingPrompt({
|
||||
title,
|
||||
subject,
|
||||
@@ -456,6 +514,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
||||
lastRegressionAt: finding.lastRegressionAt,
|
||||
remediationId: finding.remediationPlanId,
|
||||
pendingApproval: pendingApprovalBriefing,
|
||||
proposedFix,
|
||||
investigationRecord: finding.investigationRecord,
|
||||
});
|
||||
const requiresApprovalMode = patrolAssistantFindingHandoffRequiresApprovalMode({
|
||||
|
||||
@@ -45,9 +45,12 @@ const patrolWorkspaceSource = readFileSync(
|
||||
describe('FindingsPanel assistant handoff', () => {
|
||||
it('routes Patrol investigation records into the Assistant briefing context', () => {
|
||||
expect(findingsPanelSource).toContain('buildPatrolAssistantFindingBriefing');
|
||||
expect(findingsPanelSource).toContain('buildPatrolAssistantProposedFixBriefingInput');
|
||||
expect(findingsPanelSource).toContain('briefing,');
|
||||
expect(findingsPanelSource).toContain('investigationRecord: finding.investigationRecord');
|
||||
expect(findingsPanelSource).toContain('pendingApproval: pendingApproval');
|
||||
expect(findingsPanelSource).toContain('proposedFix,');
|
||||
expect(findingsPanelSource).toContain('AIAPI.getInvestigation(finding.id)');
|
||||
expect(findingsPanelSource).toContain('await aiIntelligenceStore.loadPendingApprovals()');
|
||||
expect(findingsPanelSource).toContain('patrolAssistantFindingHandoffRequiresApprovalMode');
|
||||
expect(findingsPanelSource).toContain(
|
||||
|
||||
@@ -12,7 +12,11 @@ import { aiChatStore } from '@/stores/aiChat';
|
||||
import { hasFeature } from '@/stores/license';
|
||||
import { AIAPI, type ApprovalRequest, type ApprovalExecutionResult } from '@/api/ai';
|
||||
import { getApprovalRiskPresentation } from '@/utils/approvalRiskPresentation';
|
||||
import { buildPatrolAssistantFindingBriefing } from '@/features/patrol/patrolInvestigationContextModel';
|
||||
import {
|
||||
buildPatrolAssistantFindingBriefing,
|
||||
buildPatrolAssistantProposedFixBriefingInput,
|
||||
type PatrolAssistantProposedFixBriefingSource,
|
||||
} from '@/features/patrol/patrolInvestigationContextModel';
|
||||
import { RemediationStatus } from './RemediationStatus';
|
||||
|
||||
interface ApprovalSectionProps {
|
||||
@@ -24,15 +28,6 @@ interface ApprovalSectionProps {
|
||||
resourceId?: string;
|
||||
}
|
||||
|
||||
interface AssistantBriefingFixSource {
|
||||
description?: string | null;
|
||||
commands?: string[] | null;
|
||||
target_host?: string | null;
|
||||
risk_level?: string | null;
|
||||
rationale?: string | null;
|
||||
destructive?: boolean | null;
|
||||
}
|
||||
|
||||
export const ApprovalSection: Component<ApprovalSectionProps> = (props) => {
|
||||
const [actionLoading, setActionLoading] = createSignal<string | null>(null);
|
||||
const [executionResult, setExecutionResult] = createSignal<ApprovalExecutionResult | null>(null);
|
||||
@@ -50,7 +45,7 @@ export const ApprovalSection: Component<ApprovalSectionProps> = (props) => {
|
||||
|
||||
const approvalBriefing = (
|
||||
approval: ApprovalRequest | null,
|
||||
fix?: AssistantBriefingFixSource | null,
|
||||
fix?: PatrolAssistantProposedFixBriefingSource | null,
|
||||
) =>
|
||||
buildPatrolAssistantFindingBriefing({
|
||||
title: props.findingTitle || 'Patrol finding',
|
||||
@@ -68,21 +63,22 @@ export const ApprovalSection: Component<ApprovalSectionProps> = (props) => {
|
||||
targetName: approval.targetName,
|
||||
}
|
||||
: null,
|
||||
proposedFix: fix
|
||||
? {
|
||||
description: fix.description,
|
||||
riskLevel: fix.risk_level,
|
||||
targetHost: fix.target_host,
|
||||
rationale: fix.rationale,
|
||||
commandCount: fix.commands?.length ?? 0,
|
||||
destructive: fix.destructive,
|
||||
}
|
||||
: null,
|
||||
proposedFix: buildPatrolAssistantProposedFixBriefingInput(
|
||||
fix ||
|
||||
(approval
|
||||
? {
|
||||
description: approval.context,
|
||||
riskLevel: approval.riskLevel,
|
||||
targetHost: approval.targetName,
|
||||
commandCount: approval.command ? 1 : 0,
|
||||
}
|
||||
: null),
|
||||
),
|
||||
});
|
||||
|
||||
const handleFixWithAssistant = (
|
||||
approval: ApprovalRequest | null,
|
||||
fix: AssistantBriefingFixSource | null,
|
||||
fix: PatrolAssistantProposedFixBriefingSource | null,
|
||||
e: Event,
|
||||
) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -197,10 +197,19 @@ describe('ApprovalSection', () => {
|
||||
statusLabel: 'Pending approval · High risk',
|
||||
detailLines: expect.arrayContaining([
|
||||
expect.stringContaining('live approval pending'),
|
||||
expect.stringContaining('Proposed fix: Restart the workload service'),
|
||||
expect.stringContaining('1 command recorded for approval context'),
|
||||
expect.stringContaining('Review live governed approval approval-1 before execution'),
|
||||
]),
|
||||
actionLabel: 'Approval approval-1',
|
||||
safetyNote: 'Execution requires the governed approval flow.',
|
||||
actionLabel: 'Restart the workload service',
|
||||
commandSummary: '1 command recorded for approval context',
|
||||
safetyNote:
|
||||
'Command details stay in approval context; execution requires the governed approval flow.',
|
||||
suggestedPrompts: [
|
||||
'Review approval risk and next step',
|
||||
'Explain current finding status',
|
||||
'Summarize remediation without command text',
|
||||
],
|
||||
}),
|
||||
autonomousMode: false,
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
buildPatrolAssessmentAssistantHandoff,
|
||||
buildPatrolAssistantFindingBriefing,
|
||||
buildPatrolAssistantFindingPrompt,
|
||||
buildPatrolAssistantProposedFixBriefingInput,
|
||||
buildPatrolInvestigationContextSummary,
|
||||
buildPatrolInvestigationRecordPresentation,
|
||||
buildPatrolRemediationPlanAssistantBriefing,
|
||||
@@ -322,6 +323,27 @@ describe('patrolInvestigationContextModel', () => {
|
||||
expect(JSON.stringify(presentation)).not.toContain('systemctl restart workload.service');
|
||||
});
|
||||
|
||||
it('normalizes safe proposed-fix briefing metadata without command text', () => {
|
||||
const briefing = buildPatrolAssistantProposedFixBriefingInput({
|
||||
description: 'Restart the workload service',
|
||||
commands: ['systemctl restart workload.service'],
|
||||
risk_level: 'high',
|
||||
target_host: 'node-1',
|
||||
rationale: 'Service stayed wedged after IO pressure.',
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
expect(briefing).toEqual({
|
||||
description: 'Restart the workload service',
|
||||
riskLevel: 'high',
|
||||
targetHost: 'node-1',
|
||||
rationale: 'Service stayed wedged after IO pressure.',
|
||||
commandCount: 1,
|
||||
destructive: true,
|
||||
});
|
||||
expect(JSON.stringify(briefing)).not.toContain('systemctl restart workload.service');
|
||||
});
|
||||
|
||||
it('frames Assistant handoff around the structured Patrol record when one exists', () => {
|
||||
expect(
|
||||
buildPatrolAssistantFindingPrompt({
|
||||
|
||||
@@ -77,6 +77,18 @@ export interface PatrolAssistantProposedFixBriefingInput {
|
||||
destructive?: boolean | null;
|
||||
}
|
||||
|
||||
export interface PatrolAssistantProposedFixBriefingSource {
|
||||
description?: string | null;
|
||||
riskLevel?: string | null;
|
||||
risk_level?: string | null;
|
||||
targetHost?: string | null;
|
||||
target_host?: string | null;
|
||||
rationale?: string | null;
|
||||
commandCount?: number | null;
|
||||
commands?: readonly string[] | null;
|
||||
destructive?: boolean | null;
|
||||
}
|
||||
|
||||
export interface PatrolAssistantFindingBriefingInput {
|
||||
title: string;
|
||||
subject: string;
|
||||
@@ -225,16 +237,9 @@ export function buildPatrolInvestigationRecordPresentation(
|
||||
};
|
||||
}
|
||||
|
||||
const proposedFix = record.proposed_fix
|
||||
? {
|
||||
description: normalizeText(record.proposed_fix.description),
|
||||
riskLabel: formatIdentifierLabel(record.proposed_fix.risk_level),
|
||||
targetHost: normalizeText(record.proposed_fix.target_host),
|
||||
rationale: normalizeText(record.proposed_fix.rationale),
|
||||
commandSummary: formatCommandSummary(record.proposed_fix.commands?.length ?? 0),
|
||||
destructive: Boolean(record.proposed_fix.destructive),
|
||||
}
|
||||
: undefined;
|
||||
const proposedFix = normalizeProposedFixBriefing(
|
||||
buildPatrolAssistantProposedFixBriefingInput(record.proposed_fix),
|
||||
);
|
||||
|
||||
return {
|
||||
hasRecord: true,
|
||||
@@ -264,6 +269,39 @@ export function buildPatrolInvestigationRecordPresentation(
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPatrolAssistantProposedFixBriefingInput(
|
||||
source?: PatrolAssistantProposedFixBriefingSource | null,
|
||||
): PatrolAssistantProposedFixBriefingInput | undefined {
|
||||
if (!source) return undefined;
|
||||
const commandCount =
|
||||
typeof source.commandCount === 'number'
|
||||
? source.commandCount
|
||||
: Array.isArray(source.commands)
|
||||
? source.commands.length
|
||||
: null;
|
||||
const briefing = {
|
||||
description: normalizeText(source.description),
|
||||
riskLevel: normalizeText(source.riskLevel || source.risk_level),
|
||||
targetHost: normalizeText(source.targetHost || source.target_host),
|
||||
rationale: normalizeText(source.rationale),
|
||||
commandCount: normalizeNonNegativeCount(commandCount),
|
||||
destructive: typeof source.destructive === 'boolean' ? source.destructive : null,
|
||||
};
|
||||
|
||||
if (
|
||||
!briefing.description &&
|
||||
!briefing.riskLevel &&
|
||||
!briefing.targetHost &&
|
||||
!briefing.rationale &&
|
||||
!briefing.commandCount &&
|
||||
briefing.destructive !== true
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return briefing;
|
||||
}
|
||||
|
||||
export function buildPatrolAssistantFindingPrompt(
|
||||
input: PatrolAssistantFindingPromptInput,
|
||||
): string {
|
||||
@@ -1389,6 +1427,9 @@ function buildPatrolAssistantSafetyNote(
|
||||
if (hasCommands && isDestructive) {
|
||||
return 'Command details stay in approval context; destructive actions require governed approval.';
|
||||
}
|
||||
if (hasCommands && pendingApproval?.id) {
|
||||
return 'Command details stay in approval context; execution requires the governed approval flow.';
|
||||
}
|
||||
if (hasCommands) {
|
||||
return 'Command details stay in approval context.';
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ test.describe("Patrol Assistant operator briefing", () => {
|
||||
const approvalRequestedAt = new Date(Date.now() - 60_000).toISOString();
|
||||
const approvalExpiresAt = new Date(Date.now() + 10 * 60_000).toISOString();
|
||||
let includePendingApproval = true;
|
||||
let includeUnifiedInvestigationRecord = true;
|
||||
let includeInvestigationProposedFix = false;
|
||||
|
||||
await page.route("**/api/security/status", async (route) => {
|
||||
@@ -262,42 +263,46 @@ test.describe("Patrol Assistant operator briefing", () => {
|
||||
investigation_status: "completed",
|
||||
investigation_outcome: "fix_queued",
|
||||
investigation_attempts: 1,
|
||||
investigation_record: {
|
||||
id: "record-1",
|
||||
finding_id: "finding-operator-briefing",
|
||||
subject: {
|
||||
resource_id: "host:web-server",
|
||||
resource_name: "web-server",
|
||||
resource_type: "host",
|
||||
},
|
||||
trigger: {
|
||||
detected_at: "2026-05-06T12:00:00Z",
|
||||
title: "High CPU usage",
|
||||
},
|
||||
status: "completed",
|
||||
outcome: "fix_queued",
|
||||
confidence: "high",
|
||||
conclusion: "Backup job saturated CPU.",
|
||||
recommended_action:
|
||||
"Approve a controlled restart after the backup completes.",
|
||||
evidence: [
|
||||
{
|
||||
kind: "metrics",
|
||||
summary: "CPU stayed above 95% for 10 minutes",
|
||||
},
|
||||
],
|
||||
proposed_fix: {
|
||||
id: "fix-1",
|
||||
description: "Restart the workload service",
|
||||
commands: ["systemctl restart workload.service"],
|
||||
risk_level: "medium",
|
||||
destructive: true,
|
||||
},
|
||||
verification: ["CPU returned below 50%"],
|
||||
tools_used: [],
|
||||
started_at: "2026-05-06T12:00:00Z",
|
||||
approval_id: "approval-1",
|
||||
},
|
||||
...(includeUnifiedInvestigationRecord
|
||||
? {
|
||||
investigation_record: {
|
||||
id: "record-1",
|
||||
finding_id: "finding-operator-briefing",
|
||||
subject: {
|
||||
resource_id: "host:web-server",
|
||||
resource_name: "web-server",
|
||||
resource_type: "host",
|
||||
},
|
||||
trigger: {
|
||||
detected_at: "2026-05-06T12:00:00Z",
|
||||
title: "High CPU usage",
|
||||
},
|
||||
status: "completed",
|
||||
outcome: "fix_queued",
|
||||
confidence: "high",
|
||||
conclusion: "Backup job saturated CPU.",
|
||||
recommended_action:
|
||||
"Approve a controlled restart after the backup completes.",
|
||||
evidence: [
|
||||
{
|
||||
kind: "metrics",
|
||||
summary: "CPU stayed above 95% for 10 minutes",
|
||||
},
|
||||
],
|
||||
proposed_fix: {
|
||||
id: "fix-1",
|
||||
description: "Restart the workload service",
|
||||
commands: ["systemctl restart workload.service"],
|
||||
risk_level: "medium",
|
||||
destructive: true,
|
||||
},
|
||||
verification: ["CPU returned below 50%"],
|
||||
tools_used: [],
|
||||
started_at: "2026-05-06T12:00:00Z",
|
||||
approval_id: "approval-1",
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
],
|
||||
count: 1,
|
||||
@@ -510,6 +515,7 @@ test.describe("Patrol Assistant operator briefing", () => {
|
||||
queuedAssistantContext.getByText("systemctl restart workload.service"),
|
||||
).toHaveCount(0);
|
||||
|
||||
includeUnifiedInvestigationRecord = false;
|
||||
includeInvestigationProposedFix = true;
|
||||
await page.reload({ waitUntil: "domcontentloaded" });
|
||||
await expect(page.getByRole("button", { name: "Findings" })).toBeVisible();
|
||||
@@ -517,6 +523,32 @@ test.describe("Patrol Assistant operator briefing", () => {
|
||||
await page.getByText("High CPU usage").click();
|
||||
const expiredFinding = page.locator("#finding-finding-operator-briefing");
|
||||
await expect(expiredFinding.getByText("approval expired")).toBeVisible();
|
||||
await expiredFinding
|
||||
.getByRole("button", { name: "Discuss with Assistant" })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
const hydratedFindingAssistantContext =
|
||||
page.getByLabel("Assistant context");
|
||||
await expect(hydratedFindingAssistantContext).toBeVisible();
|
||||
await expect(hydratedFindingAssistantContext).toContainText(
|
||||
"Operator briefing attached",
|
||||
);
|
||||
await expect(hydratedFindingAssistantContext).toContainText("Fix Queued");
|
||||
await expect(hydratedFindingAssistantContext).toContainText(
|
||||
"Proposed fix: Restart the workload service; target web-server; high risk; 1 command recorded for approval context; destructive proposed fix; rationale Workload service stayed wedged after backup pressure.",
|
||||
);
|
||||
await expect(
|
||||
hydratedFindingAssistantContext.getByRole("button", {
|
||||
name: "Summarize remediation without command text",
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
hydratedFindingAssistantContext.getByText(
|
||||
"systemctl restart workload.service",
|
||||
),
|
||||
).toHaveCount(0);
|
||||
|
||||
await expiredFinding
|
||||
.getByRole("button", { name: "Fix with Assistant" })
|
||||
.last()
|
||||
|
||||
Reference in New Issue
Block a user