Skip to content

test(hub): TriggerAgentTask success + error path coverage (audit T-M1) - #1476

Merged
DeliciousBuding merged 1 commit into
masterfrom
test/dispatch-trigger-success
Jul 29, 2026
Merged

test(hub): TriggerAgentTask success + error path coverage (audit T-M1)#1476
DeliciousBuding merged 1 commit into
masterfrom
test/dispatch-trigger-success

Conversation

@DeliciousBuding

Copy link
Copy Markdown
Collaborator

Audit reference

Covers audit finding T-M1 (highest remaining ROI — critical path coverage gap) from the 2026-07-29 comprehensive AgentHub audit.

What's covered

New DB-backed unit test for DispatchService.TriggerAgentTask (hub-server/internal/service/agent_dispatch.go:28) covering the success path + four key error paths.

Success path — TestTriggerAgentTask_SuccessAndTurnInProgressGate

Seeds a live session (not dissolved), an active SessionMember (left_at nil), an AgentInstance invited by that user, and a text trigger Message. Calls TriggerAgentTask with empty targetID (skips validateDispatchTarget), exercising the real orchestration end-to-end:

GetMessageByIDGetSessionByID (#116 dissolved check) → ListAgentInstancesByInviterSelectAgentInstanceIsMemberActiveNewQueuedPendingTaskCreatePendingTaskUnlessActive (#1430 per-agent_instance TurnInProgress gate) → go s.dispatchTask(...).

Asserts:

  • no error; returned task non-nil
  • task.AgentInstanceID, task.Status == model.TaskStatusQueued, task.TriggeredByUserID, task.TriggerMessageID match
  • task.TargetID and task.EdgeDeviceID empty (empty targetID path)
  • task.ID non-empty (uuidv7 BeforeCreate hook), task.ExpireAt non-zero (TTL injected)
  • the pending_agent_tasks row exists in DB with non-terminal queued status

#1430 TurnInProgress gate (anti-cheat)

A second TriggerAgentTask call for the same agent_instance is rejected with errcode.TurnInProgress (HTTP 409); asserts the gate does not create a duplicate row. The first task stays queued because the dead Edge URL + mock cache route the dispatch through the offline no-op path without a status transition, so FindActivePendingTaskByAgentInstance deterministically observes it.

Error paths — TestTriggerAgentTask_ErrorPaths (table-driven subtests)

Each returns before dispatchTask fires (no goroutine concern):

path sentinel ref
dissolved session errcode.SessionDissolved #116
no agent instances invited by user errcode.AgentNotFound
inactive member (left_at set) errcode.SessionNotMember
message not found errcode.MsgNotFound

Race-handling decision

TriggerAgentTask launches go s.dispatchTask(...) after CreatePendingTaskUnlessActive commits the row synchronously. Verified dispatchTask is fully nil-safe with all ports nil:

  • cachePort() falls back to cache.NoOpCache{} via resolveDispatchCache (cache_fallback.go:29) — GetRoute/PushPendingTask are no-ops, never NPE.
  • recordDelivery/markDeliverySent are nil-safe outbox wrappers (agent_dispatch_ports.go:156/164) — short-circuit when outbox nil.
  • s.mgr.FindByConnID/PushToConn only run when dispatch.ManagerPortAvailable(s.mgr != nil) is true; with mgr=nil the route falls to the offline/cache branch.
  • With targetID=""RouteHTTPdispatchToEdgeHTTP (dead Edge URL → returns "") → cacheClient.GetRoute (NoOp → "") → RouteOfflinePushPendingTask (NoOp) → return. The goroutine completes cleanly with no NPE and no observable side effect on the task row.

Therefore the success-path assertions read the committed DB row (written synchronously by CreatePendingTaskUnlessActive before the goroutine fires) and the returned task. No time.Sleep, no goroutine sync — the DB read is race-free.

Schema choice

Explicit DDL matching the newAgentTaskTargetContractDB pattern (agent_test.go:1700). AutoMigrate maps type:timestamptz to TEXT on sqlite, which the driver cannot scan back into time.Time (the existing contract DB pattern avoids AutoMigrate for the same reason). SetMaxOpenConns(1) serializes sqlite writes so the #1430 transactional lock (a no-op UpdateColumn on sqlite) plus the active-status check serialize concurrent triggers in-process, matching production postgres FOR UPDATE semantics closely enough for the gate to fire.

Verification

cd hub-server && go test ./internal/service/ -run TestTriggerAgentTask -count=1
ok  github.com/agenthub/hub-server/internal/service  0.070s

Also go vet ./internal/service/ clean, full go test ./internal/service/ clean, and 50x repeat of the success-path test clean.

Refs: audit T-M1, #1430, #116, #100. No production code changes — test-only.

Add DB-backed unit test for DispatchService.TriggerAgentTask covering the
success path and four key error paths — the highest remaining ROI coverage
gap from the 2026-07-29 comprehensive audit (audit finding T-M1).

Success path (TestTriggerAgentTask_SuccessAndTurnInProgressGate):
- Empty targetID skips validateDispatchTarget, so the test exercises the
  real orchestration: GetMessageByID → GetSessionByID (#116 dissolved check)
  → ListAgentInstancesByInviter → SelectAgentInstance → IsMemberActive →
  NewQueuedPendingTask → CreatePendingTaskUnlessActive (#1430 per-
  agent_instance TurnInProgress gate).
- Asserts returned task fields (AgentInstanceID, Status==queued, ids,
  TargetID/EdgeDeviceID empty) and that the pending_agent_task row is
  committed synchronously by CreatePendingTaskUnlessActive BEFORE the
  dispatch goroutine fires, so the DB read is race-free without syncing.
- Second trigger for the same agent_instance is rejected with
  errcode.TurnInProgress (HTTP 409); anti-cheat asserts no duplicate row.

Error paths (TestTriggerAgentTask_ErrorPaths, table-driven subtests):
- dissolved session       → errcode.SessionDissolved  (#116)
- no agent instances       → errcode.AgentNotFound
- inactive member (left_at set) → errcode.SessionNotMember
- message not found       → errcode.MsgNotFound

Race handling: dispatchTask is nil-safe with all ports nil — cachePort()
falls back to cache.NoOpCache, outbox wrappers short-circuit, mgr is gated
by ManagerPortAvailable. A dead AGENTHUB_EDGE_URL routes the unbound task
through the offline no-op path without a status transition, so the first
task stays queued deterministically and the second trigger hits the
TurnInProgress gate. No time.Sleep, no goroutine sync — assertions read the
synchronously committed DB row.

Schema: explicit DDL matching the newAgentTaskTargetContractDB pattern
(agent_test.go:1700) — sqlite maps timestamptz to TEXT which the driver
cannot scan back into time.Time, so AutoMigrate is avoided. SetMaxOpenConns(1)
serializes sqlite writes so the #1430 transactional lock + status check
serialize in-process.

Refs: audit T-M1, #1430, #116, #100. No production code changes.
Copilot AI review requested due to automatic review settings July 29, 2026 16:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@DeliciousBuding, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0b2e2074-c372-4dbc-818a-9d3ac6e3c856

📥 Commits

Reviewing files that changed from the base of the PR and between 53ce53a and 685cf65.

📒 Files selected for processing (1)
  • hub-server/internal/service/agent_dispatch_trigger_test.go

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@DeliciousBuding
DeliciousBuding merged commit 31c482c into master Jul 29, 2026
20 checks passed
@DeliciousBuding
DeliciousBuding deleted the test/dispatch-trigger-success branch July 29, 2026 16:52
DeliciousBuding added a commit that referenced this pull request Jul 29, 2026
Lane A #1476: TriggerAgentTask 成功路径 + 4 错误路径 + #1430 TurnInProgress
409 二次触发 gate 直测(320 行,T-M1 覆盖缺口闭环)。
Lane B #1475: 全仓 TS 自等扫仅 1 处真 tautology(oidc-login
expect(true).toBe(true)),审计「52 处」高估→澄清并修正为 DOM 渲染断言。
综合审计 top10 全部落地 10/10。master tip 31c482c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants