mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-23 11:46:28 +00:00
Restore Patrol Assistant next-step links
This commit is contained in:
@@ -341,10 +341,10 @@ runtime cost control, and shared AI transport surfaces.
|
||||
handoffs. That summary may include the handoff kind, finding ID, resource
|
||||
and Patrol run ID, safe run type/status/runtime-failure flags, resource and
|
||||
action counts, a primary resource label, last-known approval/action status,
|
||||
risk level, timestamp, and Patrol assessment recommended next-step/action
|
||||
labels plus the safe recommendation action kind only when they can be
|
||||
safely extracted from the stored assessment handoff, but it must not expose
|
||||
model-only handoff text,
|
||||
risk level, timestamp, and Patrol recommended next-step/action labels plus
|
||||
the safe recommendation action kind or whitelisted app-route href only when
|
||||
they can be safely extracted from the stored Patrol handoff, but it must not
|
||||
expose model-only handoff text,
|
||||
runtime failure detail, action preflight/result bodies, remediation
|
||||
descriptions, raw commands, or approval command payloads. Its
|
||||
`requires_approval` field is a current operator-decision flag only: pending
|
||||
@@ -371,10 +371,11 @@ runtime cost control, and shared AI transport surfaces.
|
||||
The Assistant drawer must also fetch that current session list before
|
||||
opening the session picker instead of presenting mount-time cached
|
||||
summaries as the operator's decision surface. For restored Patrol
|
||||
assessment sessions, that picker must present the safe recommended
|
||||
next-step/action label from `handoff_summary` when one is available and
|
||||
restore the safe recommendation action kind as context metadata instead of
|
||||
reducing the saved session to generic assessment context.
|
||||
assessment or finding sessions, that picker must present the safe
|
||||
recommended next-step/action label from `handoff_summary` when one is
|
||||
available and restore the safe recommendation action kind or route-owned
|
||||
href as context metadata instead of reducing the saved session to generic
|
||||
context.
|
||||
Live Patrol assessment handoffs that include a currently unavailable
|
||||
Patrol-owned recommendation action must carry the bounded disabled reason in
|
||||
the model-only handoff and visible briefing so Assistant explains the
|
||||
|
||||
@@ -2036,9 +2036,10 @@ The `/api/ai/sessions` response may expose `handoff_summary` for sessions that
|
||||
carry private Assistant model-context metadata, but that payload is a safe
|
||||
reload marker only. It may carry the handoff kind, finding ID, Patrol run ID,
|
||||
safe run type/status/runtime-failure flags, counts, primary-resource label,
|
||||
last-known approval/action status, risk level, Patrol assessment recommended
|
||||
next-step/action labels plus the safe recommendation action kind when safely
|
||||
extractable from the stored assessment handoff, and summary timestamp; it must
|
||||
last-known approval/action status, risk level, Patrol recommended
|
||||
next-step/action labels plus the safe recommendation action kind or whitelisted
|
||||
app-route href when safely extractable from the stored Patrol handoff, and
|
||||
summary timestamp; it must
|
||||
not serialize the model-only `handoff_context`, runtime failure detail, action
|
||||
preflight/result bodies, remediation descriptions, raw commands, or approval
|
||||
command payloads.
|
||||
|
||||
@@ -198,16 +198,18 @@ work extends shared components instead of creating new local variants.
|
||||
review, but `handoffContext`, `handoffResources`, and `handoffActions` for
|
||||
run-history context must stay absent from the browser request so the backend
|
||||
can rebuild model-bound context from the stored Patrol run. Restored
|
||||
Patrol assessment and Patrol configuration-failure sessions follow the same
|
||||
safe-summary rule: the drawer may restore source label, title, target type,
|
||||
status badge, action label, and suggested prompts from `handoff_summary`, but
|
||||
Patrol assessment, Patrol finding, and Patrol configuration-failure sessions
|
||||
follow the same safe-summary rule: the drawer may restore source label,
|
||||
title, target type, status badge, action label, and suggested prompts from
|
||||
`handoff_summary`, but
|
||||
it must not infer a finding target from bounded action references or
|
||||
reconstruct hidden model context, provider details, retry payloads, commands,
|
||||
preflight output, or action results in the browser. If the safe summary
|
||||
includes a Patrol assessment recommended next step, action label, or safe
|
||||
action kind, the session picker plus restored drawer briefing and action
|
||||
copy must use that recommendation and may carry the safe action kind as
|
||||
context metadata rather than falling back to generic assessment copy.
|
||||
includes a Patrol recommended next step, action label, safe action kind, or
|
||||
whitelisted app-route href, the session picker plus restored drawer briefing
|
||||
and action copy must use that recommendation and may carry the safe action
|
||||
kind or href as context metadata rather than falling back to generic handoff
|
||||
copy.
|
||||
Session-load and new-conversation transitions must be success-bound: if the
|
||||
underlying session operation fails, the shared drawer store must not clear or
|
||||
replace the current scoped handoff context.
|
||||
|
||||
@@ -271,11 +271,11 @@ Patrol-specific presentation helpers.
|
||||
`handoff_metadata.kind=patrol_assessment` so saved Assistant sessions restore
|
||||
as current-assessment context instead of becoming generic scoped context or
|
||||
an accidental single-finding session because one bounded action reference
|
||||
names a finding. Saved assessment sessions may expose the Patrol-owned
|
||||
recommended next step/action through the safe `handoff_summary` only after
|
||||
command-like and secret-like text is withheld; the browser must use those
|
||||
fields for restored drawer copy without receiving the private model-only
|
||||
handoff context.
|
||||
names a finding. Saved assessment and finding sessions may expose the
|
||||
Patrol-owned recommended next step/action and whitelisted app-route href
|
||||
through the safe `handoff_summary` only after command-like and secret-like
|
||||
text is withheld; the browser must use those fields for restored drawer copy
|
||||
without receiving the private model-only handoff context.
|
||||
When the current Patrol assessment is coverage-incomplete with no active
|
||||
infrastructure finding, the same handoff model must frame the briefing as a
|
||||
verification gap: the prompt leads with what scoped activity did and did not
|
||||
|
||||
@@ -53,6 +53,7 @@ describe('AIChatAPI', () => {
|
||||
recommended_next_step: 'Review pending Patrol approval',
|
||||
recommended_next_step_action: 'Review approvals',
|
||||
recommended_next_step_action_kind: 'review_approvals',
|
||||
recommended_next_step_action_href: '/patrol',
|
||||
updated_at: '2026-05-06T12:08:00Z',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface ChatSessionHandoffSummary {
|
||||
recommended_next_step?: string;
|
||||
recommended_next_step_action?: string;
|
||||
recommended_next_step_action_kind?: string;
|
||||
recommended_next_step_action_href?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1100,6 +1100,9 @@ describe('AIChat', () => {
|
||||
},
|
||||
action_count: 0,
|
||||
requires_approval: false,
|
||||
recommended_next_step: 'Open Patrol provider settings',
|
||||
recommended_next_step_action: 'Open Patrol provider settings',
|
||||
recommended_next_step_action_href: '/settings/system-ai',
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -1113,6 +1116,7 @@ describe('AIChat', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Pulse Patrol')).toBeInTheDocument();
|
||||
expect(screen.getByText('Context attached')).toBeInTheDocument();
|
||||
expect(screen.getByText('Open Patrol provider settings')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Context-only Patrol follow-up'));
|
||||
@@ -1126,11 +1130,15 @@ describe('AIChat', () => {
|
||||
context: expect.objectContaining({
|
||||
actionCount: 0,
|
||||
requiresApproval: false,
|
||||
recommendedNextStep: 'Open Patrol provider settings',
|
||||
recommendedNextStepAction: 'Open Patrol provider settings',
|
||||
recommendedNextStepActionHref: '/settings/system-ai',
|
||||
}),
|
||||
briefing: expect.objectContaining({
|
||||
sourceLabel: 'Pulse Patrol',
|
||||
title: 'Patrol finding on web-server',
|
||||
actionLabel: undefined,
|
||||
actionLabel: 'Open Patrol provider settings',
|
||||
actionHref: '/settings/system-ai',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -146,13 +146,32 @@ const getSessionHandoffBadgeLabel = (summary: ChatSessionHandoffSummary) => {
|
||||
};
|
||||
|
||||
const formatSessionHandoffRecommendationLabel = (summary: ChatSessionHandoffSummary) => {
|
||||
if (!isPatrolAssessmentSessionHandoff(summary)) return '';
|
||||
if (!isPatrolSessionHandoff(summary)) return '';
|
||||
const action = summary.recommended_next_step_action?.trim();
|
||||
if (action) return `Recommended: ${action}`;
|
||||
if (action) return isPatrolFindingSessionHandoff(summary) ? action : `Recommended: ${action}`;
|
||||
const nextStep = summary.recommended_next_step?.trim();
|
||||
return nextStep ? `Recommended: ${nextStep}` : '';
|
||||
};
|
||||
|
||||
const getSessionHandoffRecommendedActionHref = (
|
||||
summary: ChatSessionHandoffSummary,
|
||||
): string | undefined => {
|
||||
const summaryHref = summary.recommended_next_step_action_href?.trim();
|
||||
if (summaryHref === '/settings/system-ai' || summaryHref === '/patrol') {
|
||||
return summaryHref;
|
||||
}
|
||||
|
||||
switch (summary.recommended_next_step_action_kind?.trim()) {
|
||||
case 'open_provider_settings':
|
||||
return '/settings/system-ai';
|
||||
case 'review_approvals':
|
||||
case 'review_findings':
|
||||
return '/patrol';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const buildSessionHandoffContext = (session?: ChatSession): AIChatContext | undefined => {
|
||||
const summary = session?.handoff_summary;
|
||||
if (!summary) return undefined;
|
||||
@@ -164,6 +183,7 @@ const buildSessionHandoffContext = (session?: ChatSession): AIChatContext | unde
|
||||
const recommendedNextStep = summary.recommended_next_step?.trim() || '';
|
||||
const recommendedNextStepAction = summary.recommended_next_step_action?.trim() || '';
|
||||
const recommendedNextStepActionKind = summary.recommended_next_step_action_kind?.trim() || '';
|
||||
const recommendedNextStepActionHref = getSessionHandoffRecommendedActionHref(summary);
|
||||
const actionCount = summary.action_count ?? 0;
|
||||
const resourceCount = summary.resource_count ?? 0;
|
||||
const findingId = summary.finding_id?.trim() || undefined;
|
||||
@@ -225,6 +245,7 @@ const buildSessionHandoffContext = (session?: ChatSession): AIChatContext | unde
|
||||
recommendedNextStep,
|
||||
recommendedNextStepAction,
|
||||
recommendedNextStepActionKind,
|
||||
recommendedNextStepActionHref,
|
||||
updatedAt: summary.updated_at,
|
||||
},
|
||||
findingId,
|
||||
@@ -274,9 +295,19 @@ const buildSessionHandoffContext = (session?: ChatSession): AIChatContext | unde
|
||||
? 'Review Patrol runtime issue'
|
||||
: isPatrolRun
|
||||
? 'Review Patrol run'
|
||||
: actionCount > 0
|
||||
? 'Governed action context'
|
||||
: undefined,
|
||||
: isPatrolFinding && recommendedNextStepAction
|
||||
? recommendedNextStepAction
|
||||
: actionCount > 0
|
||||
? 'Governed action context'
|
||||
: undefined,
|
||||
actionHref:
|
||||
!summary.requires_approval &&
|
||||
recommendedNextStepActionHref &&
|
||||
(isPatrolFinding ||
|
||||
(isPatrolAssessment &&
|
||||
Boolean(recommendedNextStepAction || recommendedNextStep)))
|
||||
? recommendedNextStepActionHref
|
||||
: undefined,
|
||||
commandSummary: statusLabel
|
||||
? isPatrolRun
|
||||
? `Run state: ${statusLabel}`
|
||||
|
||||
@@ -440,6 +440,38 @@ describe('patrolInvestigationContextModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('links route-owned Patrol assessment recommendations in Assistant briefing', () => {
|
||||
const handoff = buildPatrolAssessmentAssistantHandoff({
|
||||
assessment: {
|
||||
title: 'Patrol runtime issue',
|
||||
description: 'Patrol coverage is incomplete.',
|
||||
},
|
||||
overallHealth: {
|
||||
grade: 'C',
|
||||
score: 60,
|
||||
factors: [{ category: 'coverage' }],
|
||||
},
|
||||
recommendedNextStep: {
|
||||
title: 'Restore Patrol visibility',
|
||||
description: 'Fix the Patrol runtime issue before treating the assessment as current.',
|
||||
actionLabel: 'Open Patrol provider settings',
|
||||
actionKind: 'open_provider_settings',
|
||||
},
|
||||
activeFindings: [],
|
||||
});
|
||||
|
||||
expect(handoff.context.briefing).toMatchObject({
|
||||
actionLabel: 'Recommended: Open Patrol provider settings',
|
||||
actionHref: '/settings/system-ai',
|
||||
});
|
||||
expect(handoff.context.handoffContext).toContain(
|
||||
'Recommended Next Step Action: Open Patrol provider settings (open_provider_settings)',
|
||||
);
|
||||
expect(handoff.context.context).toMatchObject({
|
||||
recommendedNextStepActionKind: 'open_provider_settings',
|
||||
});
|
||||
});
|
||||
|
||||
it('marks unavailable recommended Patrol actions in assessment handoffs', () => {
|
||||
const handoff = buildPatrolAssessmentAssistantHandoff({
|
||||
assessment: {
|
||||
|
||||
@@ -857,6 +857,7 @@ function buildPatrolAssessmentAssistantBriefing(
|
||||
.slice(0, 5),
|
||||
evidence: [...findingEvidence.slice(0, 3), ...supportingEvidence].slice(0, 5),
|
||||
actionLabel: actionPosture.actionLabel,
|
||||
actionHref: actionPosture.actionHref,
|
||||
safetyNote: actionPosture.safetyNote,
|
||||
suggestedPrompts: buildPatrolAssessmentSuggestedPrompts(input, {
|
||||
findings,
|
||||
@@ -935,14 +936,30 @@ function formatAssessmentRecommendedNextStepDetailLine(
|
||||
);
|
||||
}
|
||||
|
||||
function getAssessmentRecommendedNextStepActionHref(
|
||||
recommendedNextStep?: NormalizedPatrolAssessmentRecommendedNextStep,
|
||||
): string | undefined {
|
||||
switch (recommendedNextStep?.actionKind) {
|
||||
case 'open_provider_settings':
|
||||
return '/settings/system-ai';
|
||||
case 'review_approvals':
|
||||
case 'review_findings':
|
||||
return '/patrol';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function buildPatrolAssessmentActionPosture(
|
||||
input: PatrolAssessmentAssistantHandoffInput,
|
||||
handoffActions: AIChatHandoffAction[],
|
||||
): { actionLabel: string; safetyNote: string } {
|
||||
): { actionLabel: string; actionHref?: string; safetyNote: string } {
|
||||
const pendingApprovalCount = normalizeAssessmentPendingApprovalCount(input.activeFindings);
|
||||
const actionCount = handoffActions.length;
|
||||
const hasCoverageGap = assessmentHasCoverageGap(input);
|
||||
const recommendedNextStep = normalizeAssessmentRecommendedNextStep(input.recommendedNextStep);
|
||||
const recommendedNextStepActionHref =
|
||||
getAssessmentRecommendedNextStepActionHref(recommendedNextStep);
|
||||
const hasDryRunPosture = handoffActions.some((action) =>
|
||||
Boolean(normalizeText(action.actionDryRunSummary) || normalizeText(action.actionPreflight)),
|
||||
);
|
||||
@@ -986,6 +1003,7 @@ function buildPatrolAssessmentActionPosture(
|
||||
actionLabel: recommendedNextStep?.actionLabel
|
||||
? `Recommended: ${recommendedNextStep.actionLabel}`
|
||||
: 'Review coverage gap',
|
||||
actionHref: recommendedNextStep?.actionLabel ? recommendedNextStepActionHref : undefined,
|
||||
safetyNote: formatAssessmentRecommendationSafetyNote(
|
||||
'Assistant can explain the gap; full Patrol runs, diagnostics, and remediation remain operator-controlled.',
|
||||
recommendedNextStep,
|
||||
@@ -996,6 +1014,7 @@ function buildPatrolAssessmentActionPosture(
|
||||
if (recommendedNextStep?.actionLabel || recommendedNextStep?.title) {
|
||||
return {
|
||||
actionLabel: `Recommended: ${recommendedNextStep.actionLabel || recommendedNextStep.title}`,
|
||||
actionHref: recommendedNextStep.actionLabel ? recommendedNextStepActionHref : undefined,
|
||||
safetyNote: formatAssessmentRecommendationSafetyNote(
|
||||
'Assistant can explain the Patrol recommendation; Patrol runs, settings changes, diagnostics, and remediation remain operator-controlled.',
|
||||
recommendedNextStep,
|
||||
|
||||
@@ -394,6 +394,8 @@ export interface AIChatSessionHandoffSummary {
|
||||
last_known_action_risk?: string;
|
||||
recommended_next_step?: string;
|
||||
recommended_next_step_action?: string;
|
||||
recommended_next_step_action_kind?: string;
|
||||
recommended_next_step_action_href?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -297,6 +297,64 @@ func safePatrolAssessmentRecommendationAction(value string) (string, string) {
|
||||
return action, ""
|
||||
}
|
||||
|
||||
func patrolFindingNextStepSummary(kind string, handoffContext string) (string, string) {
|
||||
if kind != sessionHandoffKindPatrolFinding {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(handoffContext), "\n")
|
||||
if len(lines) == 0 {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
sawFindingContext := false
|
||||
sawPatrolSource := false
|
||||
var nextStepAction string
|
||||
var nextStepActionHref string
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "[Patrol Finding Context]" {
|
||||
sawFindingContext = true
|
||||
continue
|
||||
}
|
||||
if !sawFindingContext {
|
||||
continue
|
||||
}
|
||||
|
||||
label, value, ok := strings.Cut(line, ":")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
label = strings.ToLower(strings.TrimSpace(label))
|
||||
value = strings.TrimSpace(value)
|
||||
switch label {
|
||||
case "source":
|
||||
if strings.EqualFold(value, "Pulse Patrol finding handoff") {
|
||||
sawPatrolSource = true
|
||||
}
|
||||
case "patrol next step":
|
||||
nextStepAction = safeSessionHandoffSummaryText(value, 120)
|
||||
case "patrol next step route":
|
||||
nextStepActionHref = safePatrolHandoffRoute(value)
|
||||
}
|
||||
}
|
||||
|
||||
if !sawFindingContext || !sawPatrolSource || nextStepAction == "" {
|
||||
return "", ""
|
||||
}
|
||||
return nextStepAction, nextStepActionHref
|
||||
}
|
||||
|
||||
func safePatrolHandoffRoute(value string) string {
|
||||
route := trimHandoffMetadataField(value, 160)
|
||||
switch route {
|
||||
case "/patrol", "/settings/system-ai":
|
||||
return route
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func safeSessionHandoffSummaryText(value string, maxRunes int) string {
|
||||
normalized := trimHandoffMetadataField(value, maxRunes)
|
||||
if normalized == "" || sessionHandoffSummaryTextShouldBeWithheld(normalized) {
|
||||
@@ -404,6 +462,16 @@ func modelContextHandoffSummary(modelContext *sessionModelContext) *SessionHando
|
||||
kind,
|
||||
modelContext.HandoffContext,
|
||||
)
|
||||
recommendedNextStepActionHref := ""
|
||||
if recommendedNextStepAction == "" {
|
||||
recommendedNextStepAction, recommendedNextStepActionHref = patrolFindingNextStepSummary(
|
||||
kind,
|
||||
modelContext.HandoffContext,
|
||||
)
|
||||
if recommendedNextStep == "" && recommendedNextStepAction != "" {
|
||||
recommendedNextStep = recommendedNextStepAction
|
||||
}
|
||||
}
|
||||
|
||||
summary := &SessionHandoffSummary{
|
||||
Kind: kind,
|
||||
@@ -418,6 +486,7 @@ func modelContextHandoffSummary(modelContext *sessionModelContext) *SessionHando
|
||||
RecommendedNextStep: recommendedNextStep,
|
||||
RecommendedNextStepAction: recommendedNextStepAction,
|
||||
RecommendedNextStepActionKind: recommendedNextStepActionKind,
|
||||
RecommendedNextStepActionHref: recommendedNextStepActionHref,
|
||||
}
|
||||
if kind != sessionHandoffKindPatrolRun {
|
||||
summary.RunID = ""
|
||||
|
||||
@@ -588,6 +588,89 @@ func TestSessionStore_ListKeepsPatrolAssessmentHandoffIdentity(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionStore_ListKeepsSafePatrolFindingNextStepRoute(t *testing.T) {
|
||||
store, err := NewSessionStore(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create session store: %v", err)
|
||||
}
|
||||
|
||||
session, err := store.Create()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create session: %v", err)
|
||||
}
|
||||
|
||||
handoffContext := strings.Join([]string{
|
||||
"[Patrol Finding Context]",
|
||||
"Source: Pulse Patrol finding handoff",
|
||||
"Finding ID: finding-provider-settings",
|
||||
"Patrol Next Step: Open Patrol provider settings",
|
||||
"Patrol Next Step Route: /settings/system-ai",
|
||||
}, "\n")
|
||||
if err := store.SetModelHandoffEnvelope(session.ID, "finding-provider-settings", handoffContext, nil, nil, HandoffMetadata{}); err != nil {
|
||||
t.Fatalf("SetModelHandoffEnvelope failed: %v", err)
|
||||
}
|
||||
|
||||
sessions, err := store.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List failed: %v", err)
|
||||
}
|
||||
if len(sessions) != 1 || sessions[0].HandoffSummary == nil {
|
||||
t.Fatalf("sessions = %#v, want one session with handoff summary", sessions)
|
||||
}
|
||||
|
||||
summary := sessions[0].HandoffSummary
|
||||
if summary.Kind != sessionHandoffKindPatrolFinding {
|
||||
t.Fatalf("handoff kind = %q, want %q", summary.Kind, sessionHandoffKindPatrolFinding)
|
||||
}
|
||||
if summary.FindingID != "finding-provider-settings" {
|
||||
t.Fatalf("finding ID = %q, want finding-provider-settings", summary.FindingID)
|
||||
}
|
||||
if summary.RecommendedNextStep != "Open Patrol provider settings" ||
|
||||
summary.RecommendedNextStepAction != "Open Patrol provider settings" ||
|
||||
summary.RecommendedNextStepActionHref != "/settings/system-ai" {
|
||||
t.Fatalf("next step summary = %#v, want safe route-owned Patrol next step", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionStore_ListWithholdsUnsafePatrolFindingNextStepRoute(t *testing.T) {
|
||||
store, err := NewSessionStore(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create session store: %v", err)
|
||||
}
|
||||
|
||||
session, err := store.Create()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create session: %v", err)
|
||||
}
|
||||
|
||||
handoffContext := strings.Join([]string{
|
||||
"[Patrol Finding Context]",
|
||||
"Source: Pulse Patrol finding handoff",
|
||||
"Finding ID: finding-provider-settings",
|
||||
"Patrol Next Step: Open Patrol provider settings",
|
||||
"Patrol Next Step Route: javascript:alert(1)",
|
||||
}, "\n")
|
||||
if err := store.SetModelHandoffEnvelope(session.ID, "finding-provider-settings", handoffContext, nil, nil, HandoffMetadata{}); err != nil {
|
||||
t.Fatalf("SetModelHandoffEnvelope failed: %v", err)
|
||||
}
|
||||
|
||||
sessions, err := store.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List failed: %v", err)
|
||||
}
|
||||
if len(sessions) != 1 || sessions[0].HandoffSummary == nil {
|
||||
t.Fatalf("sessions = %#v, want one session with handoff summary", sessions)
|
||||
}
|
||||
|
||||
summary := sessions[0].HandoffSummary
|
||||
if summary.RecommendedNextStepAction != "Open Patrol provider settings" {
|
||||
t.Fatalf("next step action = %q, want safe label preserved", summary.RecommendedNextStepAction)
|
||||
}
|
||||
if summary.RecommendedNextStepActionHref != "" {
|
||||
t.Fatalf("next step href = %q, want unsafe route withheld", summary.RecommendedNextStepActionHref)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionStore_ListWithholdsUnsafePatrolAssessmentRecommendationSummary(t *testing.T) {
|
||||
store, err := NewSessionStore(t.TempDir())
|
||||
if err != nil {
|
||||
|
||||
@@ -42,6 +42,7 @@ type SessionHandoffSummary struct {
|
||||
RecommendedNextStep string `json:"recommended_next_step,omitempty"`
|
||||
RecommendedNextStepAction string `json:"recommended_next_step_action,omitempty"`
|
||||
RecommendedNextStepActionKind string `json:"recommended_next_step_action_kind,omitempty"`
|
||||
RecommendedNextStepActionHref string `json:"recommended_next_step_action_href,omitempty"`
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user