Merge branch 'main' into fix/canvas-confirm-dialog-backdrop-a11y-v2
Some checks failed
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 13s
Harness Replays / detect-changes (pull_request) Failing after 11s
Harness Replays / Harness Replays (pull_request) Has been skipped
CI / Detect changes (pull_request) Successful in 43s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 17s
E2E API Smoke Test / detect-changes (pull_request) Successful in 47s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 43s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 40s
sop-tier-check / tier-check (pull_request) Successful in 23s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 42s
CI / Platform (Go) (pull_request) Successful in 9s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 7s
CI / Python Lint & Test (pull_request) Successful in 7s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 10s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 7s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 8s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 8m29s
CI / Canvas (Next.js) (pull_request) Failing after 10m29s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
Some checks failed
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 13s
Harness Replays / detect-changes (pull_request) Failing after 11s
Harness Replays / Harness Replays (pull_request) Has been skipped
CI / Detect changes (pull_request) Successful in 43s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 17s
E2E API Smoke Test / detect-changes (pull_request) Successful in 47s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 43s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 40s
sop-tier-check / tier-check (pull_request) Successful in 23s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 42s
CI / Platform (Go) (pull_request) Successful in 9s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 7s
CI / Python Lint & Test (pull_request) Successful in 7s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 10s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 7s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 8s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 8m29s
CI / Canvas (Next.js) (pull_request) Failing after 10m29s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
This commit is contained in:
commit
63c3475f93
@ -90,7 +90,11 @@ export function ConsoleModal({ workspaceId, workspaceName, open, onClose }: Prop
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center">
|
||||
<div aria-hidden="true" className="absolute inset-0 bg-black/70 backdrop-blur-sm" onClick={onClose} />
|
||||
<div
|
||||
className="absolute inset-0 bg-black/70 backdrop-blur-sm cursor-pointer"
|
||||
onClick={onClose}
|
||||
aria-label="Close terminal"
|
||||
/>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
|
||||
@ -81,7 +81,11 @@ export function DeleteCascadeConfirmDialog({
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div aria-hidden="true" className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onCancel} />
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm cursor-pointer"
|
||||
onClick={onCancel}
|
||||
aria-label="Dismiss dialog"
|
||||
/>
|
||||
|
||||
{/* Dialog */}
|
||||
<div
|
||||
|
||||
@ -98,10 +98,10 @@ describe("ConsoleModal — WCAG 2.1 dialog accessibility", () => {
|
||||
expect(titleEl?.textContent?.trim()).toBe("EC2 console output");
|
||||
});
|
||||
|
||||
it("backdrop div has aria-hidden='true' so screen readers skip it (WCAG 4.1.2)", async () => {
|
||||
it("backdrop div has aria-label for screen readers (WCAG 2.4.6)", async () => {
|
||||
mockGet.mockResolvedValueOnce({ output: "" });
|
||||
render(<ConsoleModal workspaceId="ws-1" open={true} onClose={() => {}} />);
|
||||
const backdrop = document.querySelector('[aria-hidden="true"]');
|
||||
const backdrop = document.querySelector('[aria-label="Close terminal"]');
|
||||
expect(backdrop).toBeTruthy();
|
||||
expect(backdrop?.className).toContain("bg-black");
|
||||
});
|
||||
|
||||
@ -99,9 +99,9 @@ describe("DeleteCascadeConfirmDialog — WCAG 2.1 dialog accessibility", () => {
|
||||
expect(titleEl?.textContent?.trim()).toBe("Delete Workspace and Children");
|
||||
});
|
||||
|
||||
it("backdrop div has aria-hidden='true' so screen readers skip it (WCAG 4.1.2)", () => {
|
||||
it("backdrop div has aria-label for screen readers (WCAG 2.4.6)", () => {
|
||||
renderDialog();
|
||||
const backdrop = document.querySelector('[aria-hidden="true"]');
|
||||
const backdrop = document.querySelector('[aria-label="Dismiss dialog"]');
|
||||
expect(backdrop).toBeTruthy();
|
||||
expect(backdrop?.className).toContain("bg-black");
|
||||
});
|
||||
|
||||
@ -668,6 +668,31 @@ async def main(): # pragma: no cover
|
||||
if heartbeat.active_tasks > 0:
|
||||
continue
|
||||
|
||||
# Issue #381 fix: skip the idle prompt if there are unconsumed
|
||||
# delegation results waiting. The heartbeat sends a self-message
|
||||
# for every new result batch, so sending the idle prompt here would
|
||||
# race: the agent would compose a stale tick BEFORE processing the
|
||||
# results notification, producing repeated identical asks (peer sends
|
||||
# correction, we respond with stale state, peer asks again).
|
||||
# By skipping the idle prompt when results are pending, we let the
|
||||
# heartbeat's own self-message wake the agent after results are
|
||||
# written. The agent then sees the results in _prepare_prompt()
|
||||
# and processes them before composing.
|
||||
from heartbeat import DELEGATION_RESULTS_FILE as _DRF
|
||||
try:
|
||||
with open(_DRF) as _rf:
|
||||
_rf.seek(0)
|
||||
_content = _rf.read().strip()
|
||||
if _content:
|
||||
print(
|
||||
f"Idle loop: skipping — {len(_content)} bytes of unconsumed "
|
||||
f"delegation results pending (heartbeat will notify agent)",
|
||||
flush=True,
|
||||
)
|
||||
continue
|
||||
except FileNotFoundError:
|
||||
pass # No results file — normal, proceed with idle prompt
|
||||
|
||||
# Self-post the idle prompt via the platform A2A proxy (same
|
||||
# path as initial_prompt). The agent's own concurrency control
|
||||
# rejects if the workspace becomes busy between this check and
|
||||
|
||||
80
workspace/tests/test_idle_loop_pending_check.py
Normal file
80
workspace/tests/test_idle_loop_pending_check.py
Normal file
@ -0,0 +1,80 @@
|
||||
"""Tests for issue #381: idle loop must not fire when delegation results are pending.
|
||||
|
||||
The idle loop skips sending the idle prompt when DELEGATION_RESULTS_FILE
|
||||
contains unconsumed results, preventing the agent from composing a stale tick
|
||||
before processing pending delegation notifications from the heartbeat.
|
||||
|
||||
Source: workspace/main.py:_run_idle_loop() pending-results guard.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def check_results_pending(file_path: str) -> bool:
|
||||
"""Mirror the guard logic from workspace/main.py:_run_idle_loop().
|
||||
|
||||
Returns True if the results file exists and is non-empty,
|
||||
meaning the idle loop should skip this tick.
|
||||
"""
|
||||
try:
|
||||
with open(file_path) as rf:
|
||||
rf.seek(0)
|
||||
content = rf.read().strip()
|
||||
return bool(content)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
|
||||
class TestIdleLoopPendingCheck:
|
||||
"""Tests for the idle-loop pending-delegation-results guard."""
|
||||
|
||||
def test_no_file_means_proceed(self, tmp_path):
|
||||
"""No delegation results file → idle loop fires normally."""
|
||||
results_file = tmp_path / "delegation_results.jsonl"
|
||||
assert not check_results_pending(str(results_file))
|
||||
|
||||
def test_empty_file_means_proceed(self, tmp_path):
|
||||
"""Empty file → no pending results → idle loop fires."""
|
||||
results_file = tmp_path / "delegation_results.jsonl"
|
||||
results_file.write_text("", encoding="utf-8")
|
||||
assert not check_results_pending(str(results_file))
|
||||
|
||||
def test_whitespace_only_file_means_proceed(self, tmp_path):
|
||||
"""File with only whitespace → treated as empty → idle loop fires."""
|
||||
results_file = tmp_path / "delegation_results.jsonl"
|
||||
results_file.write_text(" \n ", encoding="utf-8")
|
||||
assert not check_results_pending(str(results_file))
|
||||
|
||||
def test_single_result_means_skip(self, tmp_path):
|
||||
"""File with one delegation result → skip idle tick."""
|
||||
results_file = tmp_path / "delegation_results.jsonl"
|
||||
results_file.write_text(
|
||||
json.dumps({
|
||||
"status": "completed",
|
||||
"delegation_id": "del-abc",
|
||||
"summary": "Done",
|
||||
}) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert check_results_pending(str(results_file))
|
||||
|
||||
def test_multiple_results_means_skip(self, tmp_path):
|
||||
"""File with multiple delegation results → skip idle tick."""
|
||||
results_file = tmp_path / "delegation_results.jsonl"
|
||||
results_file.write_text(
|
||||
json.dumps({"status": "completed", "delegation_id": "del-1", "summary": "A"})
|
||||
+ "\n"
|
||||
+ json.dumps({"status": "failed", "delegation_id": "del-2", "summary": "B"})
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert check_results_pending(str(results_file))
|
||||
|
||||
def test_file_with_only_newline_means_proceed(self, tmp_path):
|
||||
"""File with only a newline character → stripped to empty → fires."""
|
||||
results_file = tmp_path / "delegation_results.jsonl"
|
||||
results_file.write_text("\n", encoding="utf-8")
|
||||
assert not check_results_pending(str(results_file))
|
||||
Loading…
Reference in New Issue
Block a user