# Inbox Workflow Test Plan

## Scope

This document tracks cross-command scenarios where the main value is the interaction between multiple `inbox` subcommands.

All examples assume:

- isolated temp database
- `inbox --db TMPDIR/coord.db --json init` already executed
- assertions follow the shared rules in [../_shared/README.md](../_shared/README.md)

## case: thread-lifecycle-happy-path

### 用例意义

验证 `send -> fetch -> claim -> update(in_progress) -> update(blocked) -> reply -> done -> show` 的主干链路可用，且线程与消息历史一致。

### 前置条件

- 空数据库已完成 `init`
- 发送方为 `leader`
- 执行方为 `worker-a`

### 输入

```bash
inbox --db TMPDIR/coord.db --json send --from leader --to worker-a --subject "Implement feature X" --summary "Add retry policy" --body "Implement retry handling for the HTTP client." --run run_blog_001 --task T1
inbox --db TMPDIR/coord.db --json fetch --agent worker-a --status pending
inbox --db TMPDIR/coord.db --json claim --agent worker-a --thread THREAD_ID --lease-seconds 300
inbox --db TMPDIR/coord.db --json update --agent worker-a --thread THREAD_ID --status in_progress --summary "Implementation started" --body "Scanning current HTTP client usage."
inbox --db TMPDIR/coord.db --json update --agent worker-a --thread THREAD_ID --status blocked --summary "Need timeout decision" --payload-json '{"question":"Should retries apply to read timeouts?"}'
inbox --db TMPDIR/coord.db --json reply --from leader --to worker-a --thread THREAD_ID --summary "Retry read timeouts" --body "Yes, include read timeouts in the retry policy."
inbox --db TMPDIR/coord.db --json done --agent worker-a --thread THREAD_ID --summary "Retry policy implemented" --body "The HTTP client now retries the selected transient failures."
inbox --db TMPDIR/coord.db --json show --thread THREAD_ID
```

### 预期输出

- `send` 返回新建线程，线程状态为 `pending`
- `fetch` 返回唯一匹配线程
- `claim` 后线程状态为 `claimed`
- 第一次 `update` 后线程状态为 `in_progress`
- 第二次 `update` 后线程状态为 `blocked`
- `reply` 返回一条 `kind=answer` 的消息
- `done` 后线程状态为 `done`
- `show` 返回线程状态 `done`，并包含完整消息历史

### 断言结论

- 全链路所有命令退出码为 `0`
- `show.data.thread.status == "done"`
- `show.data.messages` 长度为 `6`
- 历史中的状态推进顺序与执行顺序一致，不出现丢消息或状态回退

## case: blocked-question-reply-resume-to-done

### 用例意义

验证被阻塞线程在收到答复后可以继续推进，并最终进入完成态。

### 前置条件

- 已存在由 `leader` 发给 `worker-c` 的线程
- `worker-c` 已经成功 `claim`

### 输入

```bash
inbox --db TMPDIR/coord.db --json update --agent worker-c --thread THREAD_ID --status blocked --summary "Need policy decision" --body "Should guest users be redirected to login or shown a 403 page?"
inbox --db TMPDIR/coord.db --agent worker-c --json wait-reply --thread THREAD_ID --after-message BLOCKED_MESSAGE_ID --timeout-seconds 2
inbox --db TMPDIR/coord.db --json reply --from leader --to worker-c --thread THREAD_ID --summary "Redirect to login" --body "Redirect guests to login for the MVP."
inbox --db TMPDIR/coord.db --json done --agent worker-c --thread THREAD_ID --summary "Policy applied" --body "The flow now redirects guests to login."
```

### 预期输出

- `update` 将线程推进到 `blocked`
- `wait-reply` 在答复出现后唤醒
- 唤醒结果包含答复消息
- `done` 成功将线程推进到 `done`

### 断言结论

- `wait-reply.data.woke == true`
- `wait-reply.data.message.kind == "answer"`
- 最终 `done.data.thread.status == "done"`
- 该用例强调“阻塞后可恢复”，不是单纯验证 reply 本身

## case: fail-lifecycle-from-claim-to-terminal

### 用例意义

验证线程在被领取后可以直接进入失败终态，并且 `show` 对终态读取一致。

### 前置条件

- 空数据库已完成 `init`
- `leader` 已向 `worker-b` 发送任务
- `worker-b` 已 `claim` 该线程

### 输入

```bash
inbox --db TMPDIR/coord.db --json fail --agent worker-b --thread THREAD_ID --summary "Migration failed" --body "The migration cannot proceed because the prior schema is inconsistent."
inbox --db TMPDIR/coord.db --json show --thread THREAD_ID
```

### 预期输出

- `fail` 返回线程状态 `failed`
- `show` 返回相同终态

### 断言结论

- `fail.data.thread.status == "failed"`
- `show.data.thread.status == "failed"`
- 失败消息保留在线程历史中，可被后续排障读取

## case: cancel-lifecycle-after-worker-claim

### 用例意义

验证线程在执行者已领取后，发起方仍可以取消任务，并进入 `cancelled` 终态。

### 前置条件

- `leader` 已向 `worker-c` 发送任务
- `worker-c` 已成功 `claim`

### 输入

```bash
inbox --db TMPDIR/coord.db --json cancel --agent leader --thread THREAD_ID --reason "Task superseded by a larger refactor"
```

### 预期输出

- `cancel` 成功
- 返回线程状态 `cancelled`
- 返回的消息记录取消原因

### 断言结论

- `cancel.data.thread.status == "cancelled"`
- 取消属于终态转换，不要求执行者先主动释放 lease
- 原因字段可被后续 `show` 或审计场景消费

## case: watch-wakes-then-fetch-sees-new-thread

### 用例意义

验证 `watch` 的等待语义与 `fetch --unread` 的可见性一致，确保新线程到达时执行者既会被唤醒，也能随后拉到未读任务。

### 前置条件

- `worker-d` 尚无匹配 `pending` 线程
- `watch` 先于 `send` 启动

### 输入

```bash
inbox --db TMPDIR/coord.db --json watch --agent worker-d --status pending --timeout-seconds 2
inbox --db TMPDIR/coord.db --json send --from leader --to worker-d --subject "Build admin editor" --summary "Create the first editor screen" --body-file TMPDIR/task.md --artifact TMPDIR/task.md --artifact-kind brief --artifact-metadata-json '{"label":"task-brief"}' --run run_blog_004 --task T4
inbox --db TMPDIR/coord.db --json fetch --agent worker-d --status pending --unread
```

### 预期输出

- `watch` 因新线程到达而唤醒
- 唤醒结果中的 `thread_id` 与 `send` 返回值一致
- 随后 `fetch --unread` 仍能看到该 `pending` 线程

### 断言结论

- `watch.data.woke == true`
- `watch.data.thread.thread_id == send.data.thread.thread_id`
- `fetch.data.threads` 长度为 `1`
- `watch` 唤醒不应提前消费掉线程的未读可见性

## case: artifact-visible-through-send-and-show

### 用例意义

验证 `send` 写入的 body-file 与 artifact 信息能被后续 `show` 完整读回。

### 前置条件

- `TMPDIR/task.md` 已存在，内容为测试任务正文
- 空数据库已完成 `init`

### 输入

```bash
inbox --db TMPDIR/coord.db --json send --from leader --to worker-d --subject "Build admin editor" --summary "Create the first editor screen" --body-file TMPDIR/task.md --artifact TMPDIR/task.md --artifact-kind brief --artifact-metadata-json '{"label":"task-brief"}' --run run_blog_004 --task T4
inbox --db TMPDIR/coord.db --json show --thread THREAD_ID
```

### 预期输出

- `send` 成功创建线程并附带一条 artifact
- `show` 的首条消息包含从文件读取的正文与 artifact 列表

### 断言结论

- 首条消息 `body` 等于 `TMPDIR/task.md` 的文件内容
- 首条消息 `artifacts` 长度为 `1`
- 首个 artifact 的 `path` 等于 `TMPDIR/task.md`
- 首个 artifact 的 `kind` 等于 `brief`

## case: unread-clears-after-mark-read-and-reappears-on-new-message

### 用例意义

验证 read cursor 的最关键用户感知行为：未读任务可被显式清空，并会在同线程新消息到达后重新出现。

### 前置条件

- `leader` 已向 `worker-e` 发送一个 `pending` 线程

### 输入

```bash
inbox --db TMPDIR/coord.db --json fetch --agent worker-e --status pending --unread
inbox --db TMPDIR/coord.db --agent worker-e --json show --thread THREAD_ID --mark-read
inbox --db TMPDIR/coord.db --json fetch --agent worker-e --status pending --unread
inbox --db TMPDIR/coord.db --json send --from leader --to worker-e --thread THREAD_ID --summary "Use sentence case" --body "Keep the nav labels in sentence case."
inbox --db TMPDIR/coord.db --json fetch --agent worker-e --status pending --unread
```

### 预期输出

- 第一次 `fetch --unread` 返回该线程
- `show --mark-read` 成功推进 `worker-e` 的 read cursor
- 第二次 `fetch --unread` 无匹配结果
- 新消息追加后，第三次 `fetch --unread` 再次返回该线程

### 断言结论

- 第一次 `fetch` 返回 1 条线程
- 第二次 `fetch` 退出码为 `10`，错误码为 `no_matching_work`
- 追加消息后第三次 `fetch` 再次返回 1 条线程
- 未读状态是按 agent 视角计算，而不是线程级布尔值

## case: wait-reply-clears-blocked-unread-for-agent

### 用例意义

验证等待答复的消费者在收到答复后，其阻塞线程未读状态会被消费，避免“已经处理过回复但列表仍显示未读”的错觉。

### 前置条件

- `worker-c` 已拥有一个 `blocked` 线程
- 该线程阻塞消息对应的 `message_id` 已知
- `worker-c` 使用 `wait-reply` 等待答复

### 输入

```bash
inbox --db TMPDIR/coord.db --agent worker-c --json wait-reply --thread THREAD_ID --after-message BLOCKED_MESSAGE_ID --timeout-seconds 2
inbox --db TMPDIR/coord.db --json reply --from leader --to worker-c --thread THREAD_ID --summary "Redirect to login" --body "Redirect guests to login for the MVP."
inbox --db TMPDIR/coord.db --agent worker-c --json fetch --status blocked --unread
```

### 预期输出

- `wait-reply` 在答复后唤醒
- 唤醒结果携带 `answer` 消息
- 随后的 `fetch --status blocked --unread` 不再返回该线程

### 断言结论

- `wait-reply.data.woke == true`
- `wait-reply.data.message.kind == "answer"`
- 后续 `fetch` 退出码为 `10`
- 对等待中的 agent 来说，答复消费与未读清理是同一条用户契约链路
