fix(tests): clear platform_auth cache before each test
Fixes issue #160: workspace tests fail when MOLECULE_WORKSPACE_TOKEN
is set in the environment.
The bug: platform_auth._cached_token is populated at module import or
first get_token() call and persists for the process lifetime. Tests
that use monkeypatch.delenv("MOLECULE_WORKSPACE_TOKEN") to simulate "no
token in env" were failing because delenv removes the env var but not
the module-level cache — subsequent get_token() calls returned the
stale cached value.
Fix: add a function-scoped autouse fixture in conftest.py that calls
platform_auth.clear_cache() before every test. The import is inside the
fixture to avoid collection-time import issues when platform_auth is
not yet available.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix: Treat delivery-confirmed proxy errors as delegation success
Two-part fix for issue #159 — successful delegation responses were
rendered as error banners:
PART 1 — a2a_proxy.go: When io.ReadAll fails mid-stream (e.g., TCP
connection drops after the agent sent its 200 OK response), the prior
code returned (0, nil, BadGateway) discarding both the HTTP status code
and any partial body bytes already received. Fix: return
(resp.StatusCode, respBody, error) so callers can inspect what was
delivered even when the body read failed.
PART 2 — delegation.go: New condition in executeDelegation after the
transient-error retry block:
if proxyErr != nil && len(respBody) > 0 && status >= 200 && status < 300 {
goto handleSuccess
}
When proxyA2ARequest returns a delivery-confirmed error (status 2xx +
non-empty partial body), route to success instead of failure. This
prevents the retry-storm pattern where the canvas shows "error" with
a Restart-workspace suggestion even though the delegation actually
completed and the response is available.
Regression tests (delegation_test.go):
- TestExecuteDelegation_DeliveryConfirmedProxyError_TreatsAsSuccess:
server sends 200 + partial body then closes; second attempt succeeds.
Verifies the new condition fires for delivery-confirmed 2xx responses.
- TestExecuteDelegation_ProxyErrorNon2xx_RemainsFailed: server sends
500 + partial body then closes. Verifies non-2xx routes to failure.
- TestExecuteDelegation_ProxyErrorEmptyBody_RemainsFailed: server
returns 502 Bad Gateway (empty body, transient). Verifies empty-body
errors still route to failure (condition len(respBody) > 0 guards it).
- TestExecuteDelegation_CleanProxyResponse_Unchanged: clean 200 OK.
Verifies baseline (proxyErr == nil path) is unaffected.
Fixes issue #159.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(template_import): Remove silent template-dir fallback in ReplaceFiles offline path
When the workspace container is offline and writeViaEphemeral fails
(docker unavailable), ReplaceFiles previously fell back to writing
to the host-side template directory. This silently returned 200 with
"source: template" while the file change was invisible after restart
because the restart handler reads from the Docker volume, not the
template dir (issue #151).
Now returns 503 Service Unavailable with a message telling the caller
to retry after the workspace starts. The ephemeral write path is
the only correct mechanism for offline-container updates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mark PR #175 (keyboard shortcuts dialog) as ✅ done.
Note that screen reader announcements (HIGH) is in progress by Core-FE.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Issue #160: workspace tests fail when MOLECULE_WORKSPACE_TOKEN is set in
the test environment (or when /configs/.auth_token exists on disk, as it
does in a container CI runner).
Root cause:
- test_resolve_token_returns_none_when_missing: monkeypatch.delenv()
removes the env var, but _resolve_token() falls through to
configs_dir.resolve()/.auth_token which exists in the container.
- Multi-workspace tests: clear_cache() resets _cached_token, but
get_token() immediately re-reads /configs/.auth_token and caches
the real token before the env var is even checked.
Fix:
- test_mcp_doctor: patch configs_dir.resolve() to return a bare tmp_path
so the disk-file fallback finds nothing.
- Multi-workspace tests: patch platform_auth._token_file() to return a
non-existent path (via tmp_path) alongside clear_cache(), ensuring
the env var wins as intended.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix: Treat delivery-confirmed proxy errors as delegation success
When proxyA2ARequest returns an error but we have a non-empty
response body with a 2xx status code, the agent completed the work
successfully. The error is a delivery/transport error (e.g., connection
reset after response was received).
Previously, executeDelegation would mark these as "failed" even though
the work was done, causing:
- Retry storms (canvas suggests restart, user retries)
- "error" rendering in canvas even though result is available
- Data loss risk from unnecessary restarts
Now we check for valid response data before marking as failed.
Fixes issue #159.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Issue #152: claude-code workspace plugin adapter import fails with
'No module named plugins_registry'. Plugin adapter code
(workspace-template-*) uses bare `from plugins_registry import ...`
but molecule-runtime only shipped it at
molecule_runtime/plugins_registry/ (the package namespace path).
Fix: copy workspace/plugins_registry/ to the top level of the wheel
in addition to molecule_runtime/plugins_registry/. Both copies coexist
— the top-level one satisfies bare imports from plugin adapters,
the nested one satisfies the rewritten
`from molecule_runtime.plugins_registry import ...` in adapter_base.py.
pyproject.toml updated to include plugins_registry* in the packages find
directive so setuptools ships it from the wheel root.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes the "no keyboard shortcut help dialog" audit gap (MEDIUM).
Changes:
- Add KeyboardShortcutsDialog component: portal-based, accessible
dialog listing all canvas + navigation + agent shortcuts grouped by
category. WCAG 2.1 compliant (focus trap, Esc close, aria-modal,
aria-labelledby, focus restoration on close).
- Add global ? shortcut: opens the dialog when pressed outside any
input field and no modal is already open.
- Add "See all shortcuts →" link in the Toolbar quick-start popup
linking to the dialog.
Test plan:
- [x] npx vitest run (182 tests pass)
- [x] tsc --noEmit (no type errors)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Issue: HIGH priority item from canvas accessibility audit (2026-05-09).
Screen reader users had no way to know when workspace status changed
— the canvas updated visually but no announcement was made.
Changes:
- canvas.ts: add `liveAnnouncement: string` + `setLiveAnnouncement` to
CanvasState so the store can hold the current announcement text.
- canvas-events.ts: set `liveAnnouncement` in handleCanvasEvent for 6
key status transitions: ONLINE, OFFLINE, PAUSED, DEGRADED, PROVISIONING,
REMOVED, PROVISION_FAILED. Names are looked up from store nodes so
announcements are human-readable ("Alpha is now online" not "ws-1").
TASK_UPDATED and AGENT_MESSAGE are intentionally excluded — they fire
on every heartbeat and would overwhelm the user.
- Canvas.tsx: subscribe to `liveAnnouncement` from the store; render a
visually-hidden `aria-live="polite" aria-atomic="true"` region that
speaks the announcement then clears it after 500 ms so the same
message doesn't re-announce on re-render. Fallback still announces
workspace count on initial load.
- canvas-events.test.ts: 12 new test cases covering announcement
content for all 6 event types, empty/no-announcement cases, and
payload-name fallback when a node isn't yet in the store.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Issue #159: successful delegation responses were rendered as error
banners because extractResponseText() only handled the A2A result
format (body.result.parts[].text) but delegation.go stores
response_body as {text: "...", delegation_id: "..."}. The error
status was set when the HTTP transport failed even though the actual
agent response was received.
Fixes:
1. extractResponseText: check body.text before the result path so
delegation response_body.text is extracted correctly
2. extractResponseText: also check body.response_preview (WS event shape
from DELEGATION_COMPLETE handler)
3. GroupedCommsView: render NormalMessage when status=error but
responseText is populated (delegation succeeded, transport failed)
instead of burying the content in an error banner
Tests: 8 new cases (4 extractResponseText + 2 extractRequestText
regression + 2 render tests). 189 tests pass across 10 files.
Closes#159.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[core-lead-agent] Closes Core-Security audit finding (2026-05-09 audit cycle, MEDIUM):
1. workspace-server/internal/handlers/workspace_crud.go:335
`DELETE /workspaces/:id` returned `err.Error()` verbatim in the 500
body, leaking wrapped lib/pq driver strings (schema column names,
index hints) to HTTP clients. Replaced with sanitized message;
raw error already logged server-side via the existing log.Printf
immediately above.
2. workspace-server/internal/handlers/org.go:610
`OrgImport` echoed the user-supplied `body.Dir` verbatim in the 404
"org template not found: %s" response. Path traversal is already
blocked by resolveInsideRoot earlier in the handler, but echoing
raw input back lets a client probe filesystem layout (404-with-echo
vs. 400-from-resolve is itself a signal). Dropped the input from the
client-facing message; preserved full context in a new log.Printf
(orgFile path + the requested body.Dir) for operator triage.
Both fixes preserve operator-side diagnostics (logs unchanged in
content, only client-facing JSON sanitized). No behavior change for
legitimate clients — error type, status code, and JSON shape all stay
the same.
Tier: low. Defensive hardening only; reduces info-disclosure surface
without altering control-flow or auth gates.
Agent Comms tab rendered outbound delegations as blank bubbles because
extractRequestText only checked the A2A JSON-RPC format
(body.params.message.parts[].text) while delegation.go stores
request_body as {"task": "...", "delegation_id": "..."}.
Fix: check body.task first for delegation activities, then fall back to
the A2A format. Add six test cases covering the delegation shape,
precedence over A2A params when both present, empty-string guard, and
non-string type guard.
Closes#158.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>