diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index a98df276fd..48bc844e21 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -6794,6 +6794,11 @@ or run controls, while settled historical records must not be mislabeled as
expired actionable reviews.
For an aged, receipt-pending execution, the same responsive review dialog may
expose an audit-recovery disclosure to an eligible local administrator. It must
+show the receipt-pending state to every viewer as an unknown outcome, with a
+non-mutating in-dialog re-read of the same action and a warning not to create a
+second plan while the first outcome is unknown. The re-read remains available
+before the recovery window and in read-only sessions; a failed read retains the
+unknown state and never implies that the action was sent again. Recovery must
first instruct the operator to check the resource's actual state outside the
action record, then require a written reason and an explicit acknowledgement
before a fresh action read and guarded force-fail call. Desktop and phone
diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md
index 8e70cef3a7..4701e6cbba 100644
--- a/docs/release-control/v6/internal/subsystems/unified-resources.md
+++ b/docs/release-control/v6/internal/subsystems/unified-resources.md
@@ -2964,7 +2964,10 @@ rollback or automatic retry.
An executing action whose durable dispatch attempt remains `receipt_pending`
for more than the server's one-hour reconciliation window is still an unknown
-operational outcome. The Actions review may offer the already-authorized local
+operational outcome. At any age, the Actions review labels that wait as receipt
+pending, warns against a new plan, and can re-read the same action by GET without
+dispatching or closing it; this is available even to read-only viewers. The
+Actions review may offer the already-authorized local
audit force-fail recourse only after direct resource-state checking, a reason,
acknowledgement and a fresh detail read. This terminalizes the audit row
inconclusively; it does not stop or retry the agent operation. The shared
diff --git a/frontend-modern/browser-tests/action-receipt-wait.cjs b/frontend-modern/browser-tests/action-receipt-wait.cjs
new file mode 100644
index 0000000000..c0ec48ae07
--- /dev/null
+++ b/frontend-modern/browser-tests/action-receipt-wait.cjs
@@ -0,0 +1,142 @@
+// Browser proof for the production action dialog's non-mutating receipt re-read.
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const { chromium } = require('playwright');
+
+(async () => {
+ const root = '/workspace/frontend-modern';
+ const artifacts = path.join(root, 'browser-tests', 'action-receipt-proof');
+ fs.mkdirSync(artifacts, { recursive: true });
+ process.chdir(root);
+ const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js'));
+ const server = await createServer({
+ root,
+ configFile: path.join(root, 'vite.config.ts'),
+ server: { host: '127.0.0.1', port: 5202, strictPort: true },
+ });
+ let browser;
+ try {
+ await server.listen();
+ browser = await chromium.launch({ headless: true, channel: 'chromium', args: ['--no-sandbox'] });
+ const results = [];
+ for (const width of [1365, 390]) {
+ let mode = 'pending';
+ const page = await browser.newPage({ viewport: { width, height: width === 390 ? 844 : 900 } });
+ const pageErrors = [];
+ const actionRequests = [];
+ page.on('pageerror', (error) => pageErrors.push(error.message));
+ await page.route('**/*', (route) => {
+ const url = new URL(route.request().url());
+ if (url.origin !== 'http://127.0.0.1:5202') return route.abort();
+ if (url.pathname === '/api/security/status') {
+ return route.fulfill({ json: {
+ hasAuthentication: true,
+ requiresAuth: true,
+ settingsCapabilities: { authenticationWrite: true },
+ } });
+ }
+ if (url.pathname === '/api/actions/action-fixture-1') {
+ actionRequests.push({ method: route.request().method(), mode });
+ if (mode === 'error') return route.fulfill({ status: 503, json: { error: 'fixture read failed' } });
+ if (mode === 'complete') {
+ return route.fulfill({ json: {
+ audit: {
+ id: 'action-fixture-1',
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ state: 'completed',
+ decisionRevision: 1,
+ request: {
+ requestId: 'request-fixture-1', resourceId: 'docker:container:edge',
+ capabilityName: 'update', reason: 'Update edge to its latest image.', requestedBy: 'operator',
+ },
+ plan: {
+ actionId: 'action-fixture-1', requestId: 'request-fixture-1', allowed: true,
+ requiresApproval: false, rollbackAvailable: false,
+ expiresAt: new Date(Date.now() + 30 * 60_000).toISOString(),
+ planHash: 'sha256:fixture-plan', policyDecision: { status: 'resolved', authorities: [] },
+ },
+ result: {
+ success: true,
+ actionResultV2: {
+ execution: { status: 'succeeded' },
+ verification: { status: 'verified', evidenceClass: 'agent' },
+ compensation: { status: 'not_attempted', support: 'unavailable' },
+ },
+ },
+ },
+ events: [],
+ attempt: { id: 'attempt-fixture-1', actionId: 'action-fixture-1', state: 'receipt_recorded',
+ createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), dispatchCount: 1 },
+ receipt: { receivedAt: new Date().toISOString() },
+ } });
+ }
+ return route.fulfill({ json: {
+ audit: {
+ id: 'action-fixture-1', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
+ state: 'executing', decisionRevision: 1,
+ request: { requestId: 'request-fixture-1', resourceId: 'docker:container:edge',
+ capabilityName: 'update', reason: 'Update edge to its latest image.', requestedBy: 'operator' },
+ plan: { actionId: 'action-fixture-1', requestId: 'request-fixture-1', allowed: true,
+ requiresApproval: false, rollbackAvailable: false, expiresAt: new Date(Date.now() + 30 * 60_000).toISOString(),
+ planHash: 'sha256:fixture-plan', policyDecision: { status: 'resolved', authorities: [] } },
+ },
+ events: [],
+ attempt: { id: 'attempt-fixture-1', actionId: 'action-fixture-1', state: 'receipt_pending',
+ createdAt: new Date(Date.now() - 5 * 60_000).toISOString(),
+ updatedAt: new Date(Date.now() - 5 * 60_000).toISOString(), dispatchCount: 1 },
+ } });
+ }
+ if (url.pathname.startsWith('/api/')) return route.abort();
+ return route.continue();
+ });
+ await page.goto('http://127.0.0.1:5202/browser-tests/action-receipt-wait.html', {
+ waitUntil: 'domcontentloaded', timeout: 120_000,
+ });
+ const dialog = page.getByRole('dialog', { name: 'Update' });
+ await dialog.waitFor();
+ await page.getByText('Receipt pending', { exact: true }).waitFor();
+ assert.match(await page.getByTestId('action-receipt-waiting').innerText(), /Do not start the action again/);
+ assert.equal(await page.getByTestId('action-stuck-recovery').count(), 0);
+ await page.waitForTimeout(400); // Let the dialog entrance transition finish before capture.
+ await page.screenshot({ path: path.join(artifacts, `pending-${width}.png`) });
+
+ const refresh = page.getByRole('button', { name: 'Check for receipt' });
+ if (width === 390) {
+ await refresh.focus();
+ await page.keyboard.press('Enter');
+ } else {
+ await refresh.click();
+ }
+ await page.waitForFunction(() => document.querySelector('[data-testid="action-receipt-waiting"]') !== null);
+ assert.equal(actionRequests.length, 1);
+ mode = 'error';
+ await refresh.click();
+ await page.getByRole('alert').filter({ hasText: 'No new action was sent' }).waitFor();
+ await page.waitForTimeout(400);
+ await page.screenshot({ path: path.join(artifacts, `read-error-${width}.png`) });
+
+ mode = 'complete';
+ await refresh.click();
+ await page.getByText('Completed', { exact: true }).waitFor();
+ assert.equal(await page.getByTestId('action-receipt-waiting').count(), 0);
+ await page.waitForTimeout(400);
+ await page.screenshot({ path: path.join(artifacts, `completed-${width}.png`) });
+ assert.equal(actionRequests.length, 3);
+ assert.ok(actionRequests.every((request) => request.method === 'GET'), JSON.stringify(actionRequests));
+ assert.deepEqual(pageErrors, []);
+ const dimensions = await page.evaluate(() => ({ scroll: document.documentElement.scrollWidth, inner: innerWidth }));
+ assert.ok(dimensions.scroll <= dimensions.inner + 1, JSON.stringify(dimensions));
+ results.push({ width, actionRequests, pageErrors, dimensions });
+ await page.close();
+ }
+ console.log(JSON.stringify({ result: 'passed', browser: browser.version(), playwright: '1.56.1', results }));
+ } finally {
+ if (browser) await browser.close();
+ await server.close();
+ }
+})().catch((error) => {
+ console.error(error);
+ process.exitCode = 1;
+});
diff --git a/frontend-modern/browser-tests/action-receipt-wait.html b/frontend-modern/browser-tests/action-receipt-wait.html
new file mode 100644
index 0000000000..87ed8c4775
--- /dev/null
+++ b/frontend-modern/browser-tests/action-receipt-wait.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend-modern/browser-tests/action-receipt-wait.tsx b/frontend-modern/browser-tests/action-receipt-wait.tsx
new file mode 100644
index 0000000000..f5bf6d820b
--- /dev/null
+++ b/frontend-modern/browser-tests/action-receipt-wait.tsx
@@ -0,0 +1,73 @@
+// Mount the production action dialog with a synthetic, recent receipt wait.
+// The browser runner supplies only bounded read-only API responses.
+import { createSignal } from 'solid-js';
+import { render } from 'solid-js/web';
+import { ActionReviewDialog } from '../src/features/actions/ActionReviewDialog';
+import type { ActionDetailResponse } from '../src/types/actionAudit';
+import '../src/index.css';
+
+const now = Date.now();
+const initial: ActionDetailResponse = {
+ audit: {
+ id: 'action-fixture-1',
+ createdAt: new Date(now - 6 * 60_000).toISOString(),
+ updatedAt: new Date(now - 5 * 60_000).toISOString(),
+ state: 'executing',
+ decisionRevision: 1,
+ request: {
+ requestId: 'request-fixture-1',
+ resourceId: 'docker:container:edge',
+ capabilityName: 'update',
+ reason: 'Update edge to its latest image.',
+ requestedBy: 'operator',
+ },
+ plan: {
+ actionId: 'action-fixture-1',
+ requestId: 'request-fixture-1',
+ allowed: true,
+ requiresApproval: false,
+ approvalPolicy: 'none',
+ approvalRequirement: {
+ version: 1,
+ floor: 'none',
+ quorum: 0,
+ disallowRequester: false,
+ },
+ rollbackAvailable: false,
+ expiresAt: new Date(now + 30 * 60_000).toISOString(),
+ planHash: 'sha256:fixture-plan',
+ policyDecision: {
+ version: 1,
+ status: 'resolved',
+ decisionId: 'fixture-policy',
+ actionId: 'action-fixture-1',
+ scope: { orgId: 'org-fixture', resourceId: 'docker:container:edge', capabilityName: 'update' },
+ authorities: [],
+ approvalRequirement: {
+ version: 1,
+ floor: 'none',
+ quorum: 0,
+ disallowRequester: false,
+ },
+ planningAllowed: true,
+ requiresApproval: false,
+ },
+ },
+ verificationOutcome: { status: 'unknown' },
+ },
+ events: [],
+ attempt: {
+ id: 'attempt-fixture-1',
+ actionId: 'action-fixture-1',
+ state: 'receipt_pending',
+ createdAt: new Date(now - 5 * 60_000).toISOString(),
+ updatedAt: new Date(now - 5 * 60_000).toISOString(),
+ dispatchCount: 1,
+ },
+};
+
+const [selected, setSelected] = createSignal(initial);
+render(
+ () => {}} onChanged={setSelected} />,
+ document.getElementById('root')!,
+);
diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json
index 497683d9b3..dcc2cbd298 100644
--- a/frontend-modern/browser-verification.json
+++ b/frontend-modern/browser-verification.json
@@ -1,18 +1,16 @@
{
"version": 1,
- "base_sha": "498e2b12a22510520f9df7a510105d6775ea0522",
- "verified_at": "2026-09-29T16:42:43Z",
+ "base_sha": "c2f7aa8b471d6cbb858e8ec71b4e664fded6549c",
+ "verified_at": "2026-09-29T19:09:52Z",
"result": "passed",
"changed_paths": [
- "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx",
- "frontend-modern/src/types/resource.ts"
+ "frontend-modern/src/features/actions/ActionReviewDialog.tsx"
],
"content_sha256": {
- "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx": "fc3c71f792c91abe62475be2a341fbedc729dd079b31e2245b5769f969fa6b12",
- "frontend-modern/src/types/resource.ts": "4e3b64a3799aad0d357b9d8898dd2daf9a557e05c529df3650d2f6d03eb8ca56"
+ "frontend-modern/src/features/actions/ActionReviewDialog.tsx": "9a4af522484b42e37b118e8bfde7c1673122037a54b8bfed249818ba644e6b81"
},
"routes": [
- "/browser-tests/pbs-mixed-source-1723.html (mock-backed current ProxmoxBackupServersTable)"
+ "/browser-tests/action-receipt-wait.html (mock-backed production ActionReviewDialog)"
],
"viewports": [
{
@@ -25,26 +23,75 @@
}
],
"states": [
- "PBS API + corroborated Agent with token-auth nodeName absent",
- "same-host PVE API joined alongside a PVE-only name collision",
- "correlated host row temporarily omitted",
- "correlated host row restored"
+ "fresh executing/receipt_pending, including after a same-action re-read",
+ "failed status re-read with unknown outcome preserved",
+ "completed action after a recorded receipt"
],
"interactions": [
- "desktop disclosure-button keyboard focus and Enter; phone row click",
- "open History and select 24h, 6h, 1h and 12h across topology changes",
- "join PVE API, omit host row, restore host row"
+ "desktop pointer and phone keyboard Check for receipt",
+ "GET same action under pending, read error and completion; no POST",
+ "desktop and phone layout and horizontal-overflow checks"
],
- "command": "pulse-worker-browser frontend-modern/node_modules/proof-pbs-1723.cjs from assigned workspace root; Vite 6.4.3, Chromium 141.0.7390.37, Playwright 1.56.1",
+ "command": "pulse-worker-browser frontend-modern/browser-tests/action-receipt-wait.cjs from assigned workspace root; Vite 6.4.3, Chromium 141.0.7390.37, Playwright 1.56.1",
"artifacts": [
- "/var/lib/pulse-maintainer/worker-outputs/core-runtime-8wlxxg93/proof-pbs-1723.cjs",
- "/var/lib/pulse-maintainer/worker-outputs/core-runtime-8wlxxg93/browser-run.log",
- "/var/lib/pulse-maintainer/worker-outputs/core-runtime-8wlxxg93/browser-proof-1723/result.json",
- "/var/lib/pulse-maintainer/worker-outputs/core-runtime-8wlxxg93/browser-proof-1723/joined-1365.png",
- "/var/lib/pulse-maintainer/worker-outputs/core-runtime-8wlxxg93/browser-proof-1723/joined-390.png"
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-kp_huwb7/action-receipt-browser.log",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-kp_huwb7/action-receipt-proof/pending-1365.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-kp_huwb7/action-receipt-proof/read-error-1365.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-kp_huwb7/action-receipt-proof/completed-1365.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-kp_huwb7/action-receipt-proof/pending-390.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-kp_huwb7/action-receipt-proof/read-error-390.png",
+ "/var/lib/pulse-maintainer/worker-outputs/web-product-kp_huwb7/action-receipt-proof/completed-390.png"
],
- "notes": "Fresh offline browser run on supplied main 498e2b12a2 plus unchanged #1723 runtime repair. At desktop and phone widths, History stayed selected with eight plotted paths through initial PBS + Agent, same-host PVE join, transient host omission and restoration. All eight captured history requests targeted agent:agent-uuid, never the PBS service key; joined screenshots were visually inspected. Mock-backed proof is not installed, second-topology or reporter acceptance.",
+ "notes": "Production dialog mounted with synthetic recent action data and mocked same-action GET. Both desktop and phone showed explicit receipt-pending/unknown outcome guidance, a non-mutating status refresh, a safe read error, and recorded completion; screenshots inspected. This is not an installed action or reporter retest.",
"prior_verifications": [
+ {
+ "version": 1,
+ "base_sha": "498e2b12a22510520f9df7a510105d6775ea0522",
+ "verified_at": "2026-09-29T16:42:43Z",
+ "result": "passed",
+ "changed_paths": [
+ "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx",
+ "frontend-modern/src/types/resource.ts"
+ ],
+ "content_sha256": {
+ "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx": "fc3c71f792c91abe62475be2a341fbedc729dd079b31e2245b5769f969fa6b12",
+ "frontend-modern/src/types/resource.ts": "4e3b64a3799aad0d357b9d8898dd2daf9a557e05c529df3650d2f6d03eb8ca56"
+ },
+ "routes": [
+ "/browser-tests/pbs-mixed-source-1723.html (mock-backed current ProxmoxBackupServersTable)"
+ ],
+ "viewports": [
+ {
+ "width": 1365,
+ "height": 900
+ },
+ {
+ "width": 390,
+ "height": 844
+ }
+ ],
+ "states": [
+ "PBS API + corroborated Agent with token-auth nodeName absent",
+ "same-host PVE API joined alongside a PVE-only name collision",
+ "correlated host row temporarily omitted",
+ "correlated host row restored"
+ ],
+ "interactions": [
+ "desktop disclosure-button keyboard focus and Enter; phone row click",
+ "open History and select 24h, 6h, 1h and 12h across topology changes",
+ "join PVE API, omit host row, restore host row"
+ ],
+ "command": "pulse-worker-browser frontend-modern/node_modules/proof-pbs-1723.cjs from assigned workspace root; Vite 6.4.3, Chromium 141.0.7390.37, Playwright 1.56.1",
+ "artifacts": [
+ "/var/lib/pulse-maintainer/worker-outputs/core-runtime-8wlxxg93/proof-pbs-1723.cjs",
+ "/var/lib/pulse-maintainer/worker-outputs/core-runtime-8wlxxg93/browser-run.log",
+ "/var/lib/pulse-maintainer/worker-outputs/core-runtime-8wlxxg93/browser-proof-1723/result.json",
+ "/var/lib/pulse-maintainer/worker-outputs/core-runtime-8wlxxg93/browser-proof-1723/joined-1365.png",
+ "/var/lib/pulse-maintainer/worker-outputs/core-runtime-8wlxxg93/browser-proof-1723/joined-390.png"
+ ],
+ "notes": "Fresh offline browser run on supplied main 498e2b12a2 plus unchanged #1723 runtime repair. At desktop and phone widths, History stayed selected with eight plotted paths through initial PBS + Agent, same-host PVE join, transient host omission and restoration. All eight captured history requests targeted agent:agent-uuid, never the PBS service key; joined screenshots were visually inspected. Mock-backed proof is not installed, second-topology or reporter acceptance.",
+ "preserved_candidate_source": "25d6ab2e7a23adf3c74e0c071dfd40e799d2d83d"
+ },
{
"version": 1,
"base_sha": "fcbb7e58ad514ac146f666f49075039d40d829e2",
@@ -112,6 +159,5 @@
"Loaded /alerts/notifications, /settings/system-general, /settings/system-relay and /settings/system/billing/plan on the Community stack and checked nav items, panels and links"
]
}
- ],
- "preserved_candidate_source": "25d6ab2e7a23adf3c74e0c071dfd40e799d2d83d"
+ ]
}
diff --git a/frontend-modern/src/features/actions/ActionReviewDialog.tsx b/frontend-modern/src/features/actions/ActionReviewDialog.tsx
index c340468348..7b1a26319a 100644
--- a/frontend-modern/src/features/actions/ActionReviewDialog.tsx
+++ b/frontend-modern/src/features/actions/ActionReviewDialog.tsx
@@ -43,12 +43,15 @@ export const ActionReviewDialog: Component<{
}> = (props) => {
const [busy, setBusy] = createSignal(false);
const [error, setError] = createSignal('');
+ const [receiptRefreshError, setReceiptRefreshError] = createSignal('');
const [clock, setClock] = createSignal(Date.now());
const [canForceFail, setCanForceFail] = createSignal(false);
const [recoveryOpen, setRecoveryOpen] = createSignal(false);
const [recoveryReason, setRecoveryReason] = createSignal('');
const [recoveryConfirmed, setRecoveryConfirmed] = createSignal(false);
const audit = () => props.detail?.audit;
+ const receiptWaiting = () =>
+ audit()?.state === 'executing' && props.detail?.attempt?.state === 'receipt_pending';
const originDestination = () => getActionOriginDestination(audit()?.origin);
const resource = createMemo(() => {
const record = audit();
@@ -64,6 +67,7 @@ export const ActionReviewDialog: Component<{
);
createEffect(() => {
const detail = props.detail;
+ setReceiptRefreshError('');
setRecoveryOpen(false);
setRecoveryReason('');
setRecoveryConfirmed(false);
@@ -177,6 +181,21 @@ export const ActionReviewDialog: Component<{
await props.onChanged?.(detail);
};
+ const refreshReceipt = async () => {
+ const actionId = audit()?.id;
+ if (!actionId || !receiptWaiting() || busy()) return;
+ setBusy(true);
+ setReceiptRefreshError('');
+ try {
+ const latest = await ResourceActionsAPI.getAction(actionId);
+ await props.onChanged?.(latest);
+ } catch {
+ setReceiptRefreshError('Could not re-read this action. No new action was sent.');
+ } finally {
+ setBusy(false);
+ }
+ };
+
const decide = async (outcome: 'approved' | 'rejected') => {
const action = audit();
if (!action || busy()) return;
@@ -369,8 +388,14 @@ export const ActionReviewDialog: Component<{
{formatActionName(record().request.capabilityName)}
-
- {getActionAuditStatePresentation(record()).label}
+
+ {receiptWaiting()
+ ? 'Receipt pending'
+ : getActionAuditStatePresentation(record()).label}
@@ -401,6 +426,36 @@ export const ActionReviewDialog: Component<{
+
+
+
+ Waiting for the agent receipt
+
+
+ Pulse sent this action once, but its outcome is not known yet. The agent may
+ still be working or may have finished. Do not start the action again from a new
+ plan. Check the actual resource if the wait persists.
+
+ void refreshReceipt()}
+ >
+ Check for receipt
+
+
+
+ {receiptRefreshError()}
+
+
+
+
{
+ it('re-reads a fresh receipt wait without dispatching again and shows a recorded completion', async () => {
+ const current = waitingDetail(new Date(Date.now() - 5 * 60 * 1000).toISOString());
+ const completed = {
+ ...current,
+ audit: { ...current.audit, state: 'completed' as const },
+ attempt: { ...current.attempt!, state: 'receipt_recorded' as const },
+ };
+ vi.mocked(ResourceActionsAPI.getAction).mockResolvedValueOnce(completed);
+ const [selected, setSelected] = createSignal(current);
+ render(() => (
+
+ ));
+
+ expect(screen.getByText('Receipt pending')).toBeVisible();
+ expect(screen.getByTestId('action-receipt-waiting')).toHaveTextContent(
+ 'Do not start the action again from a new plan.',
+ );
+ fireEvent.click(screen.getByRole('button', { name: 'Check for receipt' }));
+
+ await waitFor(() => expect(screen.getByText('Completed', { exact: true })).toBeVisible());
+ expect(screen.queryByTestId('action-receipt-waiting')).toBeNull();
+ expect(ResourceActionsAPI.getAction).toHaveBeenCalledWith('action-1');
+ expect(ResourceActionsAPI.executeAction).not.toHaveBeenCalled();
+ expect(ResourceActionsAPI.forceFailAction).not.toHaveBeenCalled();
+ });
+
+ it('keeps an unknown outcome visible when receipt re-read fails', async () => {
+ const current = waitingDetail(new Date(Date.now() - 5 * 60 * 1000).toISOString());
+ vi.mocked(ResourceActionsAPI.getAction).mockRejectedValueOnce(new Error('offline'));
+ render(() => );
+
+ fireEvent.click(screen.getByRole('button', { name: 'Check for receipt' }));
+ expect(await screen.findByRole('alert')).toHaveTextContent(
+ 'Could not re-read this action. No new action was sent.',
+ );
+ expect(screen.getByText('Receipt pending')).toBeVisible();
+ expect(ResourceActionsAPI.executeAction).not.toHaveBeenCalled();
+ });
+
it('lets an admin close an aged receipt wait only after a direct-check reason and acknowledgement', async () => {
const current = waitingDetail();
const terminalAudit: ActionAuditRecord = {
@@ -210,6 +249,7 @@ describe('ActionReviewDialog trust gates', () => {
const fresh = waitingDetail(new Date(Date.now() - 5 * 60 * 1000).toISOString());
const freshView = render(() => );
await waitFor(() => expect(SecurityAPI.getStatus).toHaveBeenCalled());
+ expect(screen.getByTestId('action-receipt-waiting')).toBeVisible();
expect(screen.queryByTestId('action-stuck-recovery')).toBeNull();
freshView.unmount();
@@ -221,6 +261,7 @@ describe('ActionReviewDialog trust gates', () => {
const readOnly = { ...waitingDetail(), readOnly: true };
const readOnlyView = render(() => );
+ expect(screen.getByRole('button', { name: 'Check for receipt' })).toBeEnabled();
expect(screen.queryByTestId('action-stuck-recovery')).toBeNull();
readOnlyView.unmount();