mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-22 03:02:35 +00:00
Render post-dispatch verification outcome on action history rows
The broker's read-after-write verification (ActionVerificationResult on ExecutionResult) was being persisted by the backend but no operator surface displayed it. Operators reviewing the action history saw only "command exit 0" — not "Pulse confirmed the workload service is active after dispatch." Adds the TS mirror for ActionVerificationResult on types/actionAudit.ts and renders it on each audit row in ResourceActionHistory.tsx when verification.ran=true. The render shows: - "Verified" or "Verification failed" badge tone - The verification command Pulse ran (e.g. systemctl is-active 'nginx') - The captured output verbatim - An italic note when the broker recorded one (dispatch failure, non-zero exit code) Tone is emerald for verified, amber for failed — matching the trust palette used for the confidence badge on the patrol findings panel. When verification.ran=false (no derivable check, or feature disabled for the action class) nothing renders, so operators do not see fabricated "verified" claims for actions where Pulse cannot read back. Verification artifacts: - ResourceActionHistory.verification.test.ts: source-text test pinning the verification render wiring on ResourceActionHistory.tsx. - ResourceDetailDrawer.history.test.tsx: extends the existing contract-style assertions to pin verification rendering on the detail drawer's audit rows. - actionAudit.test.ts: round-trips a verification block through the API client to pin the TS type mirror. Adds new Completion Obligation #23 to unified-resources contract pinning the canonical TS mirror location and the canonical render component, and a parallel api-contracts paragraph pinning the verification block on the action audit response.
This commit is contained in:
parent
a597321801
commit
0b1ce54eec
4 changed files with 91 additions and 0 deletions
|
|
@ -966,6 +966,17 @@ the canonical monitored-system blocked payload.
|
|||
persisted findings created by older binaries must adopt the
|
||||
freshly-classified impact text on next re-detection rather than
|
||||
preserving the empty value.
|
||||
The action audit `result` field carries an optional `verification`
|
||||
block (TS `ActionVerificationResult` mirroring the Go type) with
|
||||
`ran`, `command`, `output`, `success`, `ranAt`, and `note`. API
|
||||
consumers (specifically the Resource Action History on the
|
||||
infrastructure detail drawer) must round-trip the verification
|
||||
block verbatim and render it as a distinct outcome row alongside
|
||||
the dispatch `result`. Operators see what command Pulse ran as the
|
||||
read-after-write check, what it returned, and whether it confirmed
|
||||
the intended state — not just "command exit 0." When `ran=false`
|
||||
(no derivable check, or feature disabled for the action class)
|
||||
nothing must be rendered, matching the no-fabrication rule.
|
||||
The patrol-status response (`PatrolStatusResponse`) carries an
|
||||
optional `trust` block of type `ai.FindingsTrustSummary` that
|
||||
surfaces the trust-metrics snapshot for the Patrol page. The block
|
||||
|
|
|
|||
|
|
@ -541,6 +541,23 @@ AI-only summary payloads, or page-local heuristics.
|
|||
equivalent hash or extend the canonical hash set in
|
||||
`internal/ai/tools/action_audit.go` rather than adding ad-hoc
|
||||
comparison logic.
|
||||
23. Keep the `ActionVerificationResult` frontend mirror canonical and
|
||||
pin the operator-facing render location.
|
||||
`frontend-modern/src/types/actionAudit.ts` defines the TS
|
||||
`ActionVerificationResult` interface (`ran`, `command`, `output`,
|
||||
`success`, `ranAt`, `note`) that mirrors the Go type field-for-
|
||||
field, and `frontend-modern/src/components/Infrastructure/ResourceActionHistory.tsx`
|
||||
is the canonical operator-facing render location: each audit row
|
||||
surfaces verification as a distinct outcome row alongside the
|
||||
dispatch result, with emerald tone for verified and amber for
|
||||
failed (matching the trust palette already used on the findings
|
||||
panel). The render must show the verification command verbatim,
|
||||
the captured output, and the optional broker note, so the
|
||||
operator sees exactly what Pulse read back rather than a yes/no
|
||||
summary. When `verification.ran` is false (no derivable check, or
|
||||
feature disabled for the action class) nothing is rendered —
|
||||
operators must not see fabricated "verified" claims for actions
|
||||
where Pulse cannot read back.
|
||||
|
||||
## Current State
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,62 @@ describe('ActionAuditAPI', () => {
|
|||
expect(response.audits).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('round-trips the post-dispatch verification result on the action audit response', async () => {
|
||||
// The broker now records ActionVerificationResult on completed audits
|
||||
// (read-after-write outcome). The TS API client must mirror the field
|
||||
// verbatim so the operator-facing surface can render Pulse's actual
|
||||
// verification — what command ran, what it returned, did it confirm
|
||||
// the intended state — instead of silently dropping it to the
|
||||
// unknown-property bucket.
|
||||
apiFetchJSONMock.mockResolvedValueOnce({
|
||||
audits: [
|
||||
{
|
||||
id: 'action-verify',
|
||||
createdAt: '2026-04-29T12:00:00Z',
|
||||
updatedAt: '2026-04-29T12:00:30Z',
|
||||
state: 'completed',
|
||||
request: {
|
||||
requestId: 'req-verify',
|
||||
resourceId: 'vm:42',
|
||||
capabilityName: 'pulse_control',
|
||||
reason: 'restart workload after backup',
|
||||
requestedBy: 'pulse_patrol',
|
||||
},
|
||||
plan: {
|
||||
actionId: 'action-verify',
|
||||
requestId: 'req-verify',
|
||||
allowed: true,
|
||||
requiresApproval: true,
|
||||
approvalPolicy: 'admin',
|
||||
plannedAt: '2026-04-29T11:59:50Z',
|
||||
expiresAt: '2026-04-29T12:04:50Z',
|
||||
planHash: 'sha256:test',
|
||||
},
|
||||
result: {
|
||||
success: true,
|
||||
output: 'OK',
|
||||
verification: {
|
||||
ran: true,
|
||||
command: "systemctl is-active 'workload'",
|
||||
output: 'active',
|
||||
success: true,
|
||||
ranAt: '2026-04-29T12:00:25Z',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
count: 1,
|
||||
} as any);
|
||||
|
||||
const response = await ActionAuditAPI.listActionAudits({ resourceId: 'vm:42' });
|
||||
expect(response.audits).toHaveLength(1);
|
||||
const v = response.audits[0].result?.verification;
|
||||
expect(v?.ran).toBe(true);
|
||||
expect(v?.command).toBe("systemctl is-active 'workload'");
|
||||
expect(v?.output).toBe('active');
|
||||
expect(v?.success).toBe(true);
|
||||
});
|
||||
|
||||
it('treats gated action audit endpoints as unavailable instead of throwing', async () => {
|
||||
apiFetchJSONMock.mockRejectedValueOnce(
|
||||
Object.assign(new Error('Payment Required'), { status: 402 }),
|
||||
|
|
|
|||
|
|
@ -184,6 +184,13 @@ describe('ResourceDetailDrawer change history section', () => {
|
|||
expect(resourceDetailDrawerDerivedStateSource).toContain('resource.relationships ?? []');
|
||||
expect(resourceActionHistorySource).toContain('getActionAuditStatePresentation');
|
||||
expect(resourceActionHistorySource).toContain('formatActionApprovalPolicyLabel');
|
||||
// The audit history must surface the broker's read-after-write
|
||||
// verification outcome alongside the dispatch result, not silently
|
||||
// drop it. Pin the wiring so future refactors cannot regress to an
|
||||
// output-only render.
|
||||
expect(resourceActionHistorySource).toContain('result()?.verification?.ran');
|
||||
expect(resourceActionHistorySource).toContain('Verified');
|
||||
expect(resourceActionHistorySource).toContain('Verification failed');
|
||||
expect(actionAuditApiSource).toContain('/api/audit/actions');
|
||||
expect(actionAuditApiSource).toContain('ACTION_AUDIT_UNAVAILABLE_STATUSES');
|
||||
expect(actionAuditPresentationSource).toContain('pending_approval');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue