* feat(core): relay unsolicited agent events between user turns
Add a background goroutine per interactive session that consumes agent
events produced between user-initiated turns (e.g. Claude Code background
task completions via run_in_background). Previously these events piled up
in the buffered channel and were unconditionally discarded by drainEvents()
when the next user message arrived.
Key changes:
- Add startUnsolicitedReader/stopUnsolicitedReader with context-based
lifecycle management and bounded handoff to foreground turns
- Replace unconditional drainEvents with conditional resync via
eventsNeedResync flag (default true, cleared on clean EventResult)
- Integrate unsolicited reader teardown into all cleanup paths:
cleanupInteractiveState, stopInteractiveSession, session recycle,
/compress, and idle reaper
- Track BeginTurn/EndTurn for unsolicited turns so idle reaper does
not kill workspaces with active background processing
- Make workspace pool idle timeout configurable via
workspace_idle_timeout_mins in project config (was hardcoded 15min)
- Export SetWorkspaceIdleTimeout and DefaultWorkspaceIdleTimeout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(core): add unit tests for unsolicited events feature
Add 10 tests covering the unsolicited events implementation:
- TestUnsolicitedReader_RelaysEventResult: verifies EventResult relay
- TestUnsolicitedReader_StopsOnCancel: verifies clean goroutine shutdown
- TestUnsolicitedReader_SetsResyncOnChannelClose: abnormal exit path
- TestUnsolicitedReader_SetsResyncOnEventError: error event handling
- TestUnsolicitedReader_PermissionDeny: permission auto-deny when no user
- TestEventsNeedResync_DefaultTrue: constructor initialization
- TestEventsNeedResync_ClearedOnCleanResult: clean EventResult path
- TestCleanupInteractiveState_StopsUnsolicitedReader: cleanup teardown
- TestWorkspaceIdleTimeout_Configurable: timeout config API
- TestReapIdle_DisabledWhenZeroTimeout: zero timeout disables reaping
Tests use event-driven synchronization (channel waits with timeouts)
instead of fixed time.Sleep to avoid CI flakiness.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(core): stop unsolicited reader in drainOrphanedQueue to prevent race
drainOrphanedQueue calls drainPendingMessages which reads from the
Events() channel. Without stopping the unsolicited reader first, two
goroutines can concurrently read from the same channel — a data race.
Also restart the unsolicited reader after draining completes so
background events continue to be relayed.
Found during self-review concurrency audit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(integration): add end-to-end tests for unsolicited events
Add two integration tests covering the full flow:
TestIntegration_UnsolicitedEventsEndToEnd — verifies the happy path:
1. User sends msg → agent turn completes (EventResult)
2. Agent emits new events later (simulating run_in_background completion)
→ unsolicited reader relays them to platform
3. User sends second msg → conditional drain skipped (clean exit) →
new turn response delivered correctly
TestIntegration_StaleEventsDrainedAfterAbnormalExit — verifies the safety
net: after EventError sets eventsNeedResync=true, buffered leftover events
are drained on the next user message (not relayed as unsolicited, not
attributed to the new turn).
Uses a persistentEventsSession that keeps the Events() channel open across
turns (unlike FakeAgentSession which closes per-call). Synchronization is
event-driven via waitForMessage and waitForPromptCount — no fixed sleeps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(core): address PR #608 review comments
Addresses all five review comments from @chenhg5 on PR #608:
1. **[blocker]** Promote workspace_idle_timeout_mins to a global config.
Adds top-level Config.WorkspaceIdleTimeoutMins; per-project
ProjectConfig.WorkspaceIdleTimeoutMins retains precedence so existing
configs keep working. main.go picks project > global > default.
2. Close the concurrent-reader window around stopUnsolicitedReader.
After cancel + bounded 5s wait, the reader now double-checks ctx
after reading an event — if cancellation happened between the outer
select firing and the check, the event is dropped and resync is
requested, preventing the old reader from silently consuming an event
that belongs to the incoming foreground turn. The wait stays bounded
because getOrCreateInteractiveStateWith and stopInteractiveSession
call this while holding interactiveMu, and an unbounded wait there
would stall unrelated sessions.
3. Notify the user on auto-deny. When approveAll=false, the reader now
sends a localized MsgBackgroundAutoDenied message (EN/ZH/ZH-TW/JA/ES)
identifying the requested tool so a silently blocked background task
isn't invisible. RespondPermission is dispatched on a detached
goroutine so the slow adapter call cannot block reader shutdown.
4. Accumulate and log tool events. EventToolUse / EventToolResult now
record tool names and emit debug logs. If the channel closes mid-turn
with buffered context, a warning log captures the tools/text that
were lost — removes the misleading "accumulate silently" comments.
5. Document Session.AddHistory thread-safety. Session.AddHistory is
already internally locked; stopUnsolicitedReader's bounded wait
orders unsolicited-vs-foreground history writes. Added an explanatory
comment so the invariant is explicit.
Verified with go test -race ./... and go test -race -tags=integration
./tests/integration/. Reviewed end-to-end by Codex (gpt-5.3-codex, high
reasoning effort) — no regressions flagged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(config): make workspace_idle_timeout_mins global-only
Addresses the remaining blocker on PR #608: the maintainer asked for
workspace_idle_timeout_mins to be a global config rather than per-project.
Changes:
- Remove ProjectConfig.WorkspaceIdleTimeoutMins as a supported per-project
knob; the canonical setting is now the existing top-level
Config.WorkspaceIdleTimeoutMins.
- Keep parsing the legacy per-project key (renamed to
WorkspaceIdleTimeoutMinsLegacy in code) so existing configs continue to
work. When the top-level field is unset, the legacy per-project value is
honored as a fallback and a deprecation warning is logged at startup.
This avoids silently breaking deployments that used `0` to disable
reaping or a custom timeout.
- Clarify the comment on the global field to state that per-project
configuration is intentionally not supported.
Also commits the unsolicited-reader test additions in core/engine_test.go
(437 lines covering reader relay, cancel/stop, channel close, error
handling, permission deny, resync flag defaults/clearing, cleanup, and
workspace idle timeout configurability) that landed alongside the prior
review-fix commit but were not previously committed.
Verified via `codex exec review --uncommitted` (gpt-5.3-codex). Build
clean, all relevant tests pass; the 3 known macOS-specific pre-existing
failures (TestProcessInteractiveEvents_AppendsReplyFooter*,
TestResolveLocalDirPath_AcceptsSubdir) are out of scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Create e2e_session_test.go covering /list /new /switch /name /delete
/stop /status /provider commands with real agent calls
- Create e2e_helpers_test.go with shared test utilities
- Update filter_sessions_test.go to reference new E2E test location
- Add config.test.toml to .gitignore for test isolation
Made-with: Cursor
The E2E tests (Codex + ClaudeCode full session lifecycle) now load
provider credentials from /root/.cc-connect/config.toml via a new
setupE2EEngine helper, removing the OPENAI_API_KEY / ANTHROPIC_API_KEY
env-var requirement. Both tests pass with real LLM round-trips (~8s each).
Also improved resilience: auth/balance errors cause skip instead of fail,
and project names are overridable via E2E_CODEX_PROJECT / E2E_CLAUDECODE_PROJECT
environment variables.
Made-with: Cursor
Integration tests using real codex.Agent and claudecode.Agent adapters
with JSONL fixture files on disk. Tests the full pipeline: agent reads
real session files → ListSessions → Engine applySessionFilter → command
output. Covers /list, /switch, /delete under both filter modes plus
dynamic runtime toggle, for both Codex and Claude Code agents.
Made-with: Cursor
- Add skipUnlessAgentReady() that checks both CLI binary and API key
env vars (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.) before running
agent integration tests, preventing false-negative timeouts in CI
- Fix multi_workspace_shared_test.go: replace immediately-closed events
channel with buffered channel to prevent engine's processInteractiveEvents
from exiting prematurely
- Fix TestNewSessionClearsContext: adjust expectation since Claude Code
memory is workspace-scoped (persists across /new), verify session
functionality instead of memory erasure
Made-with: Cursor
Add a new integration test suite that verifies real agent (Claude Code, Codex,
Cursor, Gemini, OpenCode) interactions with a mock platform. Tests are gated
by //go:build integration tag so they're excluded from normal CI.
core/engine.go:
- Add public ReceiveMessage(p, msg) method to allow test callers to
inject messages into the engine
tests/integration/agent_integration_test.go:
- mockPlatform implementing core.Platform interface for test verification
- agentPool for reusing agent instances across tests
- Session tests: new session, /list, /switch, /stop, /new context clear, /history
- Agent tests: claude code, codex, cursor, gemini (skip if quota), opencode (skip if no gitlab auth)
- Command tests: /shell (skip if admin_from not set), /provider list
- i18n: /lang zh language switch
- Concurrency: session isolation, long text chunking, empty message handling
docs/plans/2026-03-24-integration-tests.md:
- Test plan listing all implemented and planned integration test cases
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The changes add error handling for previously ignored operations and introduce logging for critical errors and warnings. This includes:
- Adding error checks for file operations, JSON unmarshaling, system calls
- Logging warnings/errors for failed operations using slog
- Properly handling return values from functions that were previously ignored
- Adding context to error messages for better debugging
- Cleaning up unused code and variables
generated by llmgit
Co-Authored-By: Claude <noreply@anthropic.com>
Updated the GitHub CI workflow to use the correct golangci-lint installation path. Enhanced test mocks with proper mutex protection in TestMessageHandler and cleaned up FakeAgentSession implementation for better thread safety and performance.
generated by llmgit
Co-Authored-By: Claude <noreply@anthropic.com>
- session.go: Events() now correctly checks if last event
already has Done=true before appending an extra Done event
- message.go: TestLongMessage now uses strings.Repeat instead
of null bytes
- message.go: BatchTestMessages now uses fmt.Sprintf
instead of rune overflow-prone string concatenation
- response.go: TestAgentSessionInfoList now uses fmt.Sprintf
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add:
- T-241: Discord embed format regression test
- T-242: Telegram command parsing regression test
- T-243: DingTalk crypto/regression test
- T-252: Cron job cancellation regression test
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add testify and httpmock dependencies for testing
- Create tests/ directory structure with mocks and fakes
- Add mock_platform.go: mock for core.Platform interface
- Add mock_agent.go: mock for core.Agent/AgentSession interfaces
- Add fake/message.go: test message and event fixtures
- Add fake/session.go: fake agent session implementation
- Add fake/response.go: test helpers and fixtures
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>