test_a2a_tools_inbox_wrappers.py's _run() helper used
asyncio.get_event_loop().run_until_complete() to run coroutines from
sync test methods. When pytest-asyncio is active in OTHER test files in
the same suite, get_event_loop() can return the shared pytest-asyncio
loop, and run_until_complete() raises "loop already running" errors.
Fix: replace with asyncio.run(), which creates a fresh loop each call.
Result (full suite, 14→0 for inbox wrappers):
Without fix: 10 failures (6 TestToolWaitForMessage + 4 delegation)
With fix: 4 failures (all pre-existing delegation polling)
Closes#307.
The Canvas template-deploy path returned HTTP 500 with raw pq error
when a user clicked a template card twice in quick succession. Root
cause: migration 20260506000000 added the partial-unique index
`workspaces_parent_name_uniq` on (COALESCE(parent_id, sentinel), name)
WHERE status != 'removed' to close TOCTOU on /org/import (#2872). The
org-import handler resolves the constraint via ON CONFLICT DO NOTHING
+ idempotent re-select. The Canvas Create handler did not — it
bubbled the pq violation as a generic 500.
Fix: auto-suffix the user-typed name on collision via a small retry
helper that pins on SQLSTATE 23505 + constraint name (so unrelated
unique indexes still fail loud), retries with " (2)", " (3)" up to
N=20, and threads the actually-persisted name back into the response
+ broadcast payload (so the canvas displays what the DB actually
holds). Exhaustion maps to a clean 409 Conflict instead of a 500.
#2872 protection is preserved unchanged — the index stays in place,
and /org/import's ON CONFLICT path is unaffected. The bundle-import
INSERT (handlers/bundle.go) is a separate code path and is not
touched here; if it surfaces the same UX issue a follow-up can adopt
the same helper.
Verification (against running localhost:8080 platform):
Three back-to-back POSTs with name="ManualVerify-1778459812":
POST #1 -> 201, id=db2dacf7-…, persisted name="ManualVerify-1778459812"
POST #2 -> 201, id=f468083d-…, persisted name="ManualVerify-1778459812 (2)"
POST #3 -> 201, id=5f5ae905-…, persisted name="ManualVerify-1778459812 (3)"
Log lines: "name collision auto-suffix \"…\" -> \"… (N)\""
Tests:
- workspace_create_name_test.go — 4 unit tests via sqlmock pin the
retry contract (happy path no-suffix, single-collision -> " (2)",
non-retryable error pass-through, exhaustion -> errWorkspaceNameExhausted).
- workspace_create_name_integration_test.go — 2 real-Postgres tests
(build tag `integration`) confirm the partial-unique index
behaviour AND the WHERE status != 'removed' tombstone exemption.
- Watch-it-fail confirmed: temporarily removing the
`fmt.Sprintf("%s (%d)", baseName, attempt+1)` candidate-naming
line makes TestInsertWorkspaceWithNameRetry_SecondAttemptSuffixed
fail with the expected argument-mismatch from sqlmock.
Pre-existing test failures in handlers/ (TestExecuteDelegation_…,
TestMCPHandler_CommitMemory_GlobalScope_Blocked) reproduce on
unmodified staging and are NOT caused by this change.
Adds _sanitize_a2a.py (from PR #346) and integrates sanitize_a2a_result()
into read_delegation_results() so peer-supplied summary and response_preview
fields are escaped before being injected into the agent prompt.
Output is wrapped in [A2A_RESULT_FROM_PEER]...[/A2A_RESULT_FROM_PEER]
boundary markers so content after the block is clearly not from a peer.
Fixes:
- test_a2a_executor.py: correct mock patch path to executor_helpers
- test_executor_helpers.py: fix boundary-injection test assertion to match
_strip_closed_blocks behaviour (closes marker, removes following text)
Follow-up to PR #346 (OFFSEC-003 boundary escape) which noted
"read_delegation_results() path still needs sanitization" as a gap.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Plugin adapters in molecule-skill-* repos do:
from plugins_registry.builtins import AgentskillsAdaptor as Adaptor
But _load_module_from_path() used exec_module() with a fresh module
namespace that did NOT have plugins_registry or its submodules in sys.modules,
causing:
ModuleNotFoundError: No module named 'plugins_registry'
Fix: before exec_module(), import and register plugins_registry + all three
submodules (builtins, protocol, raw_drop) in sys.modules so adapter imports
resolve correctly. Follows the Option 1 recommendation from issue #296.
Also adds test_resolve_plugin.py verifying the fix for both the
AgentskillsAdaptor import and the full InstallContext/resolve/protocol import.
Closes#296.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The A2A proxy can return three error shapes:
{"error": "plain string"}
{"error": {"message": "...", "code": ...}}
{"error": {"message": {"nested": "object"}}} ← value at .message is a string
builtin_tools/a2a_tools.py:72 called data["error"].get("message")
without guarding against error being a string, which raised:
AttributeError: 'str' object has no attribute 'get'
This broke every delegation attempt through the legacy a2a_tools path
(the LangChain-wrapped version used by adapter templates). The
SSOT parser a2a_response.py already handled string errors; the
legacy inline sniffer in a2a_tools.py did not.
Fix: branch on isinstance(err, dict/str/other) before calling .get().
Also update both publish-workflow files to remove the dead
`staging` branch trigger — trunk-based migration (PR #109,
2026-05-08) removed the staging branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>