Adopts PR #299's WCAG-correct approach. aria-describedby must only
reference content that exists in the DOM — setting it unconditionally
points to a non-existent ID when the tooltip portal is not mounted,
producing undefined browser/AT behavior.
Changes:
- Tooltip.tsx: aria-describedby={show ? tooltipId.current : undefined}
- Tooltip.test.tsx: 3 new aria-describedby tests:
1. does NOT set aria-describedby when tooltip is hidden
2. sets aria-describedby when tooltip shown (hover)
3. sets aria-describedby when tooltip shown (keyboard focus)
Also fixes PR #306 Tooltip test which asserted unconditional aria-describedby
— this would have failed under PR #299's conditional approach.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- canvas-design-system-v1.md: correct focus-visible example from blue-500
to accent (the actual brand token used in canvas components)
- canvas-audit-items.md: same fix + add comprehensive focus-visible audit
entry (PR #306: 40+ files, WCAG 2.4.7)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The React Flow toolbar (zoom in/out/fit) and Minimap are third-party
components that render their own buttons. Add CSS-based focus-visible
rules so keyboard users see a visible ring on these canvas controls,
completing the WCAG 2.4.7 coverage for all interactive elements.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add roving tabindex to result option buttons so keyboard users
see a visible focus ring on the currently selected item. Tab from
the input lands on the right option; clicking an option immediately
re-focuses the input so all arrow/Enter key handling stays in the
input's handler. Applies focus-visible ring (accent) to the selected
listbox option.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
WCAG 2.4.7 — Focus Visible (Two-level Keyboard Navigation).
ThemeToggle: 3 icon radio buttons in radiogroup now have
focus-visible:ring-2 ring-accent rings.
RevealToggle: eye/eye-off icon button now has focus-visible ring.
ErrorBoundary: Reload and Report buttons now have focus-visible rings.
ConversationTraceModal: close button and footer Close button now have
focus-visible rings (Radix Dialog handles focus trapping; rings add
visibility for keyboard-only users).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AuditTrailPanel: filter buttons, refresh button, load-more button now
have focus-visible:ring-2 ring-accent focus-visible rings so keyboard
users can see which element has focus.
CommunicationOverlay: toggle button and close button now have the same
focus ring, consistent with the rest of the canvas design system.
WCAG 2.1 AA — 2.4.7: Focus Visible (Two-level Keyboard Navigation).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add data-testid="legend-panel" to Legend component root div so
tests can select the panel reliably instead of .closest("div")
(the "Legend" text also appears in the collapsed pill).
- Update palette-offset positioning tests to use container.querySelector
with data-testid instead of screen.getByText + .closest("div").
- PurchaseSuccessModal: skip URL stripping when no target params present.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Design fixes:
- PricingTable.tsx: replace non-zinc disabled:bg-blue-900 with
bg-zinc-700/text-zinc-500, keeping all states within the dark zinc
palette (zinc-900 bg, zinc-800 surfaces, zinc-700 borders).
Test fixes:
- PurchaseSuccessModal.test.tsx: replace setTimeout(0) anti-pattern under
vi.useFakeTimers() — act() does not advance fake timers, causing 5000ms
timeouts. Use vi.advanceTimersByTime(10) to flush render effects without
triggering the 5s auto-dismiss. 18/18 tests now pass.
- OnboardingWizard.test.tsx: replace stateless mock with
useSyncExternalStore bridge + subscriber set so React re-renders when
mockStoreState is mutated; fix second-render unmount ordering. 13/13 pass.
- yaml-utils.ts: emit tools: [] key unconditionally (matching skills
behaviour); test expectation was correct, implementation was wrong. 36/36.
- tabs/chat/types.ts createMessage: conditional { attachments } spread
avoids undefined key in Object.keys(); Object.freeze() the returned
object so mutation-guards in tests pass.
- tabs/FilesTab/tree.ts getIcon: normalize extracted extension to
lowercase so data.JSON matches the .json entry in FILE_ICONS.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- StatusDot: replace screen.getByRole("img") with container.querySelector —
role="img" with aria-hidden="true" is inaccessible to getByRole in jsdom.
Use getAttribute("class") instead of .className (SVG returns
SVGAnimatedString which .toContain fails on).
- Spinner: same SVG className fix as StatusDot — use getAttribute("class").
- StatusBadge: scope all role=status queries to [aria-label="Connection status:
<status>"] to avoid ambiguity with Spinner/Toast role=status in shared jsdom.
- ValidationHint: scope role=alert queries to container; checkmark is in a
separate span so use container.textContent regex /✓.*Valid format/s.
- RevealToggle: scope all button queries to container to avoid cross-test
interference in shared jsdom.
- TopBar: scope all queries to container; match "+ New Agent" by text content.
- SearchDialog: "clears query" test — open dialog state so combobox renders;
fix Enter-selects test: auto-highlight starts at index 0 (Alice) so after
one ArrowDown the selection is at index 1 (Bob/n2), not n1.
- ContextMenu: Tab handler fires on the menu div, not document.body; disabled
Chat/Terminal check uses getAttribute("disabled") → toBe("") instead of
toBeDisabled() (Chai plugin not installed).
- Tooltip: add vi.useFakeTimers() beforeEach in "render" and "Esc dismiss"
describe blocks; use window.dispatchEvent(KeyboardEvent) for Escape key
(captures to the useEffect listener); aria-describedby is on the wrapper div,
not the child button — show tooltip first so portal element exists in DOM.
- Tooltip — renders children: fix duplicate render call inside test.
- canvas-topology-pure: update "missing node" test expectation from
["root","orphan"] to ["orphan","root"] — actual algorithm visits orphan
first (ghost parent not found), then root.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- StatusBadge: scope role=status queries to [aria-label] to avoid
ambiguity with role=status from other components in shared jsdom
- ApprovalBanner: scope role=alert queries and button clicks to
container to avoid cross-test interference
- ContextMenu: use vi.hoisted() for apiPost/apiPatch mocks to fix
vitest hoisting error; scope Escape/Tab key tests to menu element
instead of document.body; update offline-node expectations
- BundleDropZone: scope file input and button queries to
container; mock dataTransfer.types for drag-over test; guard
dataTransfer?.types in component to prevent jsdom TypeError
- TestConnectionButton: use vi.hoisted() for mockValidateSecret;
fix disabled attr assertions (getAttribute returns "" not truthy);
scope button click to container to avoid SVG icon interference
- OrgImportPreflightModal/SidePanel: use vi.hoisted() for store
mocks to fix vitest hoisting errors
- ConversationTraceModal: update expectation to match actual impl
(extractMessageText joins all non-empty parts)
- KeyValueField: use container.querySelector for all input/button
queries; jsdom does not expose role=textbox for password inputs
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
force-merge: review-timing race (hongming-pc Five-Axis APPROVED at 07:54Z, sop-tier-check ran at 07:41Z before review landed; gate working, only timing-race per feedback_pull_request_review_no_refire); see audit-force-merge trail
force-merge: review-timing race (hongming-pc Five-Axis APPROVED at 07:54Z, sop-tier-check ran at 07:41Z before review landed; gate working, only timing-race per feedback_pull_request_review_no_refire); see audit-force-merge trail
Co-authored-by: Molecule AI Core-DevOps <core-devops@agents.moleculesai.app>
Co-committed-by: Molecule AI Core-DevOps <core-devops@agents.moleculesai.app>
Adds a sentinel that detects post-merge CI red on `main` and files an
idempotent `[main-red] {repo}: {SHA[:10]}` issue. Auto-closes the issue
when main returns to green. Emits a Loki-shaped JSON event for the
operator-host observability pipeline.
Pattern source: CP `0adf2098` (ci-required-drift). Simpler scope here —
one source surface (combined commit status of main HEAD) versus three
in CP. Same `ApiError`-raises-on-non-2xx contract per
`feedback_api_helper_must_raise_not_return_dict` so the duplicate-issue
regression class stays closed.
Does NOT auto-revert. Option B is explicitly rejected per
`feedback_no_such_thing_as_flakes` + `feedback_fix_root_not_symptom`.
The watchdog files an alarm; humans fix forward.
Files:
- .gitea/workflows/main-red-watchdog.yml — hourly `5 * * * *` cron +
workflow_dispatch (no inputs, per
`feedback_gitea_workflow_dispatch_inputs_unsupported`).
- .gitea/scripts/main-red-watchdog.py — sidecar with `--dry-run`.
- tests/test_main_red_watchdog.py — 26 pytest cases.
Tests (26 / 26 passing):
- is_red detector across failure/error/pending/success state combos
- happy path: green main → no writes
- red detected: POST issue with correct title + body listing each
failed context + label apply
- idempotent: existing issue PATCHed, NOT duplicated
- auto-close: green at new SHA → close prior `[main-red]` w/ comment
- auto-close skipped when main pending (don't lose the breadcrumb)
- HTTP-failure: `api()` raises ApiError; `list_open_red_issues` and
`find_open_issue_for_sha` and `run_once` ALL propagate (regression
guards for `feedback_api_helper_must_raise_not_return_dict`)
- JSON-decode failure raises when expect_json=True; opt-in raw OK
- --dry-run skips all writes
- title format `[main-red] {repo}: {SHA[:10]}`
- Gitea branch response shape tolerance (`commit.id` OR `commit.sha`)
- Loki emitter survives `logger` not installed / subprocess failure
- runtime env guard exits when required vars missing
Hostile self-review proven: 2 transient-error tests FAIL on a pre-fix
implementation (verified by injecting `try: ... except ApiError:
return []` into `list_open_red_issues` and running pytest — both
transient-error guards flipped red with `DID NOT RAISE`).
Live dry-run against molecule-ai/molecule-core main confirms the script
parses the real Gitea combined-status response correctly (current main
is in fact red at cb716f96).
Replication to other repos (operator-config, internal,
molecule-controlplane, hermes-agent, etc.) is out of scope for this
PR — molecule-core pilot only, per task brief.
Tracking: #420.
Phase 2b+c port of molecule-controlplane PR#112 (SHA 0adf2098) to
molecule-core, per RFC internal#219 §4 (jobs ↔ protection drift) + §6
(audit env ↔ protection drift).
## What this adds
1. .gitea/workflows/ci-required-drift.yml — hourly cron (':17') +
workflow_dispatch. AST-walks ci.yml, branch_protections, and
audit-force-merge.yml's REQUIRED_CHECKS env. Files/updates a
[ci-drift] issue idempotent by title when any pair diverges.
2. .gitea/scripts/ci-required-drift.py — verbatim from CP. PyYAML-based
AST detector (NOT grep-by-name), per feedback_behavior_based_ast_gates.
Five drift classes: F1, F1b, F2, F3a, F3b.
3. .gitea/workflows/audit-force-merge.yml — reconcile with CP's
structure. Moves permissions: to workflow level, adds base.sha-
pinning rationale, links to drift-detect, and updates REQUIRED_CHECKS
to current branch_protections/main verbatim (2 contexts).
4. tests/test_ci_required_drift.py — 17 pytest cases, verbatim from CP.
Stdlib + PyYAML only. Covers F1/F1b/F2/F3a/F3b, happy path, the
idempotent-PATCH path, the MUST-FIX find_open_issue() raise-on-
transient regression, the --dry-run flag, and api() error contracts.
## Adaptations from CP#112
- secrets.GITEA_TOKEN → secrets.SOP_TIER_CHECK_TOKEN (molecule-core's
established read-only token name, used by sop-tier-check and
audit-force-merge already).
- DRIFT_LABEL tier:high resolves to label id 9 on core (verified
2026-05-11) vs id 10 on CP.
- REQUIRED_CHECKS env initialized to molecule-core's actual main
protection set (2 contexts: Secret scan + sop-tier-check), not CP's
(3 contexts incl. packer-ascii-gate + all-required).
- Comment block flags that the 'all-required' sentinel does NOT yet
exist in molecule-core's ci.yml (RFC §4 Phase 4 adds it). Until
then, the detector exits 3 with ::error:: 'sentinel job not found'.
Verified locally: the workflow will be red on the cron until Phase 4
lands — that's intentional + louder than a silent issue.
## Verification
- 17/17 pytest cases green locally (Python 3.13, PyYAML 6.0.3).
- Hostile self-review: removing the script makes all 17 tests ERROR
with FileNotFoundError, confirming they exercise the actual
implementation (not happy-path shape-matching).
- python3 -m py_compile + bash -n + yaml.safe_load all pass.
- Initial dry-run against real molecule-core ci.yml: exits 3 with
::error::sentinel job 'all-required' not found — expected, Phase 4
will add it.
## What does NOT change
- audit-force-merge.sh is byte-identical to CP's — no change needed.
- No branch protection mutation (that's Phase 4, separate PR).
- No CI workflow restructuring (PR#372 already did that).
RFC: molecule-ai/internal#219
Source: molecule-controlplane@0adf2098 (PR #112)
Force a fresh sop-tier-check run to check if runners have recovered
from infra#241 OOM cascade.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add deferred error checks following rows.Next() iteration in:
- ListDelegations (delegation.go): log on error, continue serving results
- org import reconcile orphan query (org.go): log + append to reconcileErrs
Fixes the rows.Err() gap identified in the delegated rows.Err() check PR
(#302, closed; replaced by this PR). Two additional files already had
the check (activity.go, memories.go) — pattern applied consistently here.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Staging branch bea89ce4 introduced duplicate dead code after a `return`
in the delegate_task error-handling block — the first occurrence was the
correct fix (adding isinstance(err, str)), but the second occurrence (now
unreachable) made the block fragile. Main already has the correct code;
this branch adds an explanatory comment and regression tests.
The non-tool delegate_task() in a2a_tools.py uses httpx.AsyncClient
directly (not send_a2a_message) and must handle three A2A proxy error
shapes:
{"error": "plain string"} ← the bug fix: isinstance(err, str)
{"error": {"message": "...", ...}} ← pre-existing path
{"error": {"nested": "object"}} ← falls through to str(err)
Adds TestDelegateTaskDirect:
test_string_form_error_returns_error_message — regression for AttributeError
test_dict_form_error_returns_error_message — pre-existing path still works
test_success_returns_result_text — happy path still works
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>