docs: add inbox markdown test plans

This commit is contained in:
2026-03-19 10:54:39 +08:00
parent dab0506c5a
commit 9beb7e93eb
18 changed files with 2091 additions and 135 deletions
+70
View File
@@ -0,0 +1,70 @@
# Inbox Markdown Test Plan
## Purpose
This directory contains the human-readable Markdown test plan for the `inbox` CLI.
It complements automated Go tests. The goal is not to restate implementation details, but to preserve the user-visible CLI contract in a form that can be reviewed, extended, and executed manually when needed.
## Directory Rules
- one folder per command or shared area
- one `README.md` per folder
- no one-file-per-case sprawl
- no numeric test IDs
- each case is identified by `path + case slug`
Recommended case heading pattern:
```md
## case: send-rejects-invalid-payload-json
```
## Authoring Principles
- focus on externally visible behavior of the CLI
- prefer stable command examples that a new agent can replay against a temp database
- describe both success shape and failure contract
- when a case already exists in automated Go tests, reuse its scenario rather than inventing a new one
- keep terminology consistent with command flags and JSON fields exposed by the CLI
## Common Execution Model
Most cases in this directory assume the same baseline:
1. create an isolated temporary directory
2. choose a database path such as `TMPDIR/coord.db`
3. run `inbox --db TMPDIR/coord.db --json init`
4. run the target command sequence against that database
Unless a case says otherwise:
- commands should use `--json`
- assertions should check both exit code and JSON payload
- examples may use explicit `--agent`, or rely on the root `--agent` flag when that is the behavior under test
## Folder Map
- `README.md`: global conventions and glossary
- `_shared/README.md`: reusable fixtures, JSON assertions, exit codes, payload rules
- `workflows/README.md`: cross-command end-to-end scenarios
- per-command folders: command-specific cases and edge conditions
## Glossary
- `thread`: the durable coordination unit tracked by `thread_id`
- `message`: an event-bearing entry appended to a thread
- `artifact`: a file attachment associated with a message
- `read cursor`: the per-agent marker used by unread flows
- `lease`: the temporary ownership granted by `claim` and extended by `renew`
- `terminal state`: a thread state such as `done`, `failed`, or `cancelled`
## Relationship To Automated Tests
The current best executable reference is [internal/cli/inbox/integration_test.go](../../../internal/cli/inbox/integration_test.go).
When this Markdown plan is expanded:
- prefer matching an existing automated scenario first
- record any additional manual-only contract coverage explicitly in the relevant command document
- keep `docs/tests/inbox/ROADMAP.md` synchronized with authored files and case slugs
+93 -135
View File
@@ -25,14 +25,14 @@ Current state:
- `inbox` CLI is implemented end-to-end
- automated Go integration tests already exist for the main lifecycle, wait flows, unread behavior, artifacts, and JSON error contracts
- this roadmap now exists under `docs/tests/inbox/ROADMAP.md`
- the command, workflow, and shared Markdown test-plan documents have not been authored yet
- all planned global, shared, workflow, and command-level Markdown test-plan documents have been authored
Progress summary for planned test-plan documents, excluding `ROADMAP.md`:
- planned document files: `16`
- authored document files: `0`
- planned document files: `17`
- authored document files: `17`
- planned case slugs in this roadmap: `61`
- authored case slugs in this roadmap: `0`
- authored case slugs in this roadmap: `61`
## Scope
@@ -105,12 +105,12 @@ Allowed status values in this roadmap:
The Markdown test-plan set starts at zero, but these automated tests already exist and should be used as source material when writing the docs:
- [integration_test.go](/home/kurihada/project/ai-workflow-skill/internal/cli/inbox/integration_test.go#L12) `TestInboxLifecycle`
- [integration_test.go](/home/kurihada/project/ai-workflow-skill/internal/cli/inbox/integration_test.go#L176) `TestInboxFailLifecycle`
- [integration_test.go](/home/kurihada/project/ai-workflow-skill/internal/cli/inbox/integration_test.go#L243) `TestInboxRenewWaitReplyAndCancel`
- [integration_test.go](/home/kurihada/project/ai-workflow-skill/internal/cli/inbox/integration_test.go#L392) `TestInboxWatchListUnreadAndAppend`
- [integration_test.go](/home/kurihada/project/ai-workflow-skill/internal/cli/inbox/integration_test.go#L549) `TestInboxUnreadReadCursor`
- [integration_test.go](/home/kurihada/project/ai-workflow-skill/internal/cli/inbox/integration_test.go#L639) `TestInboxJSONErrorsAndExitCodes`
- [integration_test.go](../../../internal/cli/inbox/integration_test.go#L12) `TestInboxLifecycle`
- [integration_test.go](../../../internal/cli/inbox/integration_test.go#L176) `TestInboxFailLifecycle`
- [integration_test.go](../../../internal/cli/inbox/integration_test.go#L243) `TestInboxRenewWaitReplyAndCancel`
- [integration_test.go](../../../internal/cli/inbox/integration_test.go#L392) `TestInboxWatchListUnreadAndAppend`
- [integration_test.go](../../../internal/cli/inbox/integration_test.go#L549) `TestInboxUnreadReadCursor`
- [integration_test.go](../../../internal/cli/inbox/integration_test.go#L639) `TestInboxJSONErrorsAndExitCodes`
These tests do not remove the need for the Markdown plan. They only reduce discovery work.
@@ -158,23 +158,23 @@ docs/tests/inbox/
| Path | Purpose | Planned Cases | Authored Cases | Status |
| --- | --- | ---: | ---: | --- |
| `docs/tests/inbox/README.md` | Global testing conventions and glossary | 0 | 0 | pending |
| `docs/tests/inbox/_shared/README.md` | Shared fixtures, JSON assertions, exit-code rules | 0 | 0 | pending |
| `docs/tests/inbox/workflows/README.md` | Cross-command scenarios | 8 | 0 | pending |
| `docs/tests/inbox/init/README.md` | `init` command cases | 2 | 0 | pending |
| `docs/tests/inbox/send/README.md` | `send` command cases | 6 | 0 | pending |
| `docs/tests/inbox/fetch/README.md` | `fetch` command cases | 4 | 0 | pending |
| `docs/tests/inbox/claim/README.md` | `claim` command cases | 4 | 0 | pending |
| `docs/tests/inbox/renew/README.md` | `renew` command cases | 3 | 0 | pending |
| `docs/tests/inbox/update/README.md` | `update` command cases | 5 | 0 | pending |
| `docs/tests/inbox/reply/README.md` | `reply` command cases | 4 | 0 | pending |
| `docs/tests/inbox/done/README.md` | `done` command cases | 4 | 0 | pending |
| `docs/tests/inbox/fail/README.md` | `fail` command cases | 4 | 0 | pending |
| `docs/tests/inbox/cancel/README.md` | `cancel` command cases | 3 | 0 | pending |
| `docs/tests/inbox/list/README.md` | `list` command cases | 4 | 0 | pending |
| `docs/tests/inbox/show/README.md` | `show` command cases | 4 | 0 | pending |
| `docs/tests/inbox/watch/README.md` | `watch` command cases | 3 | 0 | pending |
| `docs/tests/inbox/wait-reply/README.md` | `wait-reply` command cases | 3 | 0 | pending |
| `docs/tests/inbox/README.md` | Global testing conventions and glossary | 0 | 0 | done |
| `docs/tests/inbox/_shared/README.md` | Shared fixtures, JSON assertions, exit-code rules | 0 | 0 | done |
| `docs/tests/inbox/workflows/README.md` | Cross-command scenarios | 8 | 8 | done |
| `docs/tests/inbox/init/README.md` | `init` command cases | 2 | 2 | done |
| `docs/tests/inbox/send/README.md` | `send` command cases | 6 | 6 | done |
| `docs/tests/inbox/fetch/README.md` | `fetch` command cases | 4 | 4 | done |
| `docs/tests/inbox/claim/README.md` | `claim` command cases | 4 | 4 | done |
| `docs/tests/inbox/renew/README.md` | `renew` command cases | 3 | 3 | done |
| `docs/tests/inbox/update/README.md` | `update` command cases | 5 | 5 | done |
| `docs/tests/inbox/reply/README.md` | `reply` command cases | 4 | 4 | done |
| `docs/tests/inbox/done/README.md` | `done` command cases | 4 | 4 | done |
| `docs/tests/inbox/fail/README.md` | `fail` command cases | 4 | 4 | done |
| `docs/tests/inbox/cancel/README.md` | `cancel` command cases | 3 | 3 | done |
| `docs/tests/inbox/list/README.md` | `list` command cases | 4 | 4 | done |
| `docs/tests/inbox/show/README.md` | `show` command cases | 4 | 4 | done |
| `docs/tests/inbox/watch/README.md` | `watch` command cases | 3 | 3 | done |
| `docs/tests/inbox/wait-reply/README.md` | `wait-reply` command cases | 3 | 3 | done |
## Authoring Order
@@ -198,121 +198,79 @@ Reason:
## Authored Case Register
No Markdown test cases have been authored yet.
When the first case is written, add rows in this format:
| Path | Case Slug | Coverage Note | Status |
| --- | --- | --- | --- |
| `docs/tests/inbox/send/README.md` | `send-creates-new-thread` | minimal happy path for new thread creation | done |
| `docs/tests/inbox/workflows/README.md` | `thread-lifecycle-happy-path` | end-to-end happy path from send to show after done | done |
| `docs/tests/inbox/workflows/README.md` | `blocked-question-reply-resume-to-done` | blocked thread receives answer and resumes to done | done |
| `docs/tests/inbox/workflows/README.md` | `fail-lifecycle-from-claim-to-terminal` | claimed thread transitions to failed terminal state | done |
| `docs/tests/inbox/workflows/README.md` | `cancel-lifecycle-after-worker-claim` | claimed thread can be cancelled by initiator | done |
| `docs/tests/inbox/workflows/README.md` | `watch-wakes-then-fetch-sees-new-thread` | watch wake-up remains consistent with unread fetch visibility | done |
| `docs/tests/inbox/workflows/README.md` | `artifact-visible-through-send-and-show` | body-file and artifact data survive send and show | done |
| `docs/tests/inbox/workflows/README.md` | `unread-clears-after-mark-read-and-reappears-on-new-message` | read cursor clears unread and new message restores it | done |
| `docs/tests/inbox/workflows/README.md` | `wait-reply-clears-blocked-unread-for-agent` | wait-reply consumes reply and clears blocked unread view | done |
| `docs/tests/inbox/init/README.md` | `init-creates-schema-on-empty-db` | initializes an empty database path and returns initialized status | done |
| `docs/tests/inbox/init/README.md` | `init-is-idempotent-on-existing-db` | repeated init succeeds on the same database path | done |
| `docs/tests/inbox/send/README.md` | `send-creates-new-thread` | creates a pending thread with an initial task message | done |
| `docs/tests/inbox/send/README.md` | `send-appends-message-to-existing-thread` | appends a message to an existing non-terminal thread | done |
| `docs/tests/inbox/send/README.md` | `send-reads-body-from-body-file` | reads message body from a file path | done |
| `docs/tests/inbox/send/README.md` | `send-attaches-artifact-with-metadata` | persists artifact path, kind, and metadata on send | done |
| `docs/tests/inbox/send/README.md` | `send-rejects-invalid-payload-json` | rejects malformed payload JSON with invalid_input | done |
| `docs/tests/inbox/send/README.md` | `send-rejects-invalid-artifact-metadata-json` | rejects malformed artifact metadata JSON | done |
| `docs/tests/inbox/fetch/README.md` | `fetch-returns-pending-thread-for-target-agent` | returns pending candidate work for the target agent | done |
| `docs/tests/inbox/fetch/README.md` | `fetch-respects-status-and-limit-filters` | enforces status filtering and max row count | done |
| `docs/tests/inbox/fetch/README.md` | `fetch-unread-uses-read-cursor` | unread filtering depends on per-agent read cursor state | done |
| `docs/tests/inbox/fetch/README.md` | `fetch-returns-no-matching-work-when-empty` | empty fetch result returns no_matching_work | done |
| `docs/tests/inbox/claim/README.md` | `claim-acquires-thread-lease` | claims a pending thread and records a claim event message | done |
| `docs/tests/inbox/claim/README.md` | `claim-rejects-when-thread-missing` | missing thread returns not_found | done |
| `docs/tests/inbox/claim/README.md` | `claim-rejects-when-thread-already-claimed` | active lease conflict returns lease_conflict | done |
| `docs/tests/inbox/claim/README.md` | `claim-records-requested-lease-duration` | claim event payload records requested lease duration | done |
| `docs/tests/inbox/renew/README.md` | `renew-extends-active-lease` | owner renews an active lease and gets a renewal event | done |
| `docs/tests/inbox/renew/README.md` | `renew-rejects-non-owner` | non-owner renew attempt returns lease_conflict | done |
| `docs/tests/inbox/renew/README.md` | `renew-rejects-without-active-lease` | missing active lease returns invalid_state | done |
| `docs/tests/inbox/update/README.md` | `update-moves-thread-to-in-progress` | owner moves a thread into in_progress with a progress message | done |
| `docs/tests/inbox/update/README.md` | `update-moves-thread-to-blocked-with-payload` | blocked update writes a question message and payload JSON | done |
| `docs/tests/inbox/update/README.md` | `update-accepts-body-file-and-artifact` | update supports body-file input and artifact attachments | done |
| `docs/tests/inbox/update/README.md` | `update-rejects-invalid-payload-json` | malformed update payload returns invalid_input | done |
| `docs/tests/inbox/update/README.md` | `update-rejects-non-owner` | non-owner update attempt returns lease_conflict | done |
| `docs/tests/inbox/reply/README.md` | `reply-adds-answer-message` | default reply kind is answer and thread stays non-terminal | done |
| `docs/tests/inbox/reply/README.md` | `reply-supports-control-kind` | reply can explicitly send control messages | done |
| `docs/tests/inbox/reply/README.md` | `reply-attaches-artifact` | reply persists artifact data on the message | done |
| `docs/tests/inbox/reply/README.md` | `reply-rejects-invalid-payload-json` | malformed reply payload returns invalid_input | done |
| `docs/tests/inbox/done/README.md` | `done-marks-thread-terminal` | owner completes a thread into done terminal state | done |
| `docs/tests/inbox/done/README.md` | `done-persists-result-body-and-artifact` | result body and artifacts remain visible after done | done |
| `docs/tests/inbox/done/README.md` | `done-rejects-non-owner` | non-owner done attempt returns lease_conflict | done |
| `docs/tests/inbox/done/README.md` | `done-rejects-on-terminal-thread` | terminal threads reject repeated done calls | done |
| `docs/tests/inbox/fail/README.md` | `fail-marks-thread-failed` | owner completes a thread into failed terminal state | done |
| `docs/tests/inbox/fail/README.md` | `fail-persists-failure-body-and-artifact` | failure body and artifacts remain visible after fail | done |
| `docs/tests/inbox/fail/README.md` | `fail-rejects-non-owner` | non-owner fail attempt returns lease_conflict | done |
| `docs/tests/inbox/fail/README.md` | `fail-rejects-on-terminal-thread` | terminal threads reject repeated fail calls | done |
| `docs/tests/inbox/cancel/README.md` | `cancel-marks-thread-cancelled` | cancel moves a non-terminal thread to cancelled | done |
| `docs/tests/inbox/cancel/README.md` | `cancel-persists-reason-and-artifact` | cancel records its reason text and attachments | done |
| `docs/tests/inbox/cancel/README.md` | `cancel-rejects-when-thread-missing` | missing thread returns not_found on cancel | done |
| `docs/tests/inbox/list/README.md` | `list-filters-by-status` | status filter limits listed threads | done |
| `docs/tests/inbox/list/README.md` | `list-filters-by-created-by` | created-by filter limits listed threads | done |
| `docs/tests/inbox/list/README.md` | `list-filters-by-assigned-to` | assigned-to filter limits listed threads | done |
| `docs/tests/inbox/list/README.md` | `list-respects-limit` | list returns at most the requested number of rows | done |
| `docs/tests/inbox/show/README.md` | `show-returns-thread-and-message-history` | show returns thread metadata and ordered history | done |
| `docs/tests/inbox/show/README.md` | `show-includes-artifacts-per-message` | show expands message artifacts in detail view | done |
| `docs/tests/inbox/show/README.md` | `show-mark-read-advances-read-cursor` | mark-read changes subsequent unread visibility | done |
| `docs/tests/inbox/show/README.md` | `show-rejects-when-thread-missing` | missing thread returns not_found on show | done |
| `docs/tests/inbox/watch/README.md` | `watch-wakes-on-matching-thread` | watch wakes on a new matching thread event | done |
| `docs/tests/inbox/watch/README.md` | `watch-respects-status-filter` | watch only wakes on events whose resulting thread status matches | done |
| `docs/tests/inbox/watch/README.md` | `watch-times-out-with-no-activity` | watch timeout returns no_matching_work | done |
| `docs/tests/inbox/wait-reply/README.md` | `wait-reply-wakes-on-answer-after-message` | wait-reply resumes from a known message boundary | done |
| `docs/tests/inbox/wait-reply/README.md` | `wait-reply-can-start-from-after-event` | wait-reply resumes from a known event cursor | done |
| `docs/tests/inbox/wait-reply/README.md` | `wait-reply-times-out-when-no-reply` | wait-reply timeout returns no_matching_work | done |
## Pending Case Backlog
### `docs/tests/inbox/workflows/README.md`
No pending case slugs remain in the current plan.
- `pending` `thread-lifecycle-happy-path`
- `pending` `blocked-question-reply-resume-to-done`
- `pending` `fail-lifecycle-from-claim-to-terminal`
- `pending` `cancel-lifecycle-after-worker-claim`
- `pending` `watch-wakes-then-fetch-sees-new-thread`
- `pending` `artifact-visible-through-send-and-show`
- `pending` `unread-clears-after-mark-read-and-reappears-on-new-message`
- `pending` `wait-reply-clears-blocked-unread-for-agent`
When a new CLI contract or workflow needs coverage:
### `docs/tests/inbox/init/README.md`
- `pending` `init-creates-schema-on-empty-db`
- `pending` `init-is-idempotent-on-existing-db`
### `docs/tests/inbox/send/README.md`
- `pending` `send-creates-new-thread`
- `pending` `send-appends-message-to-existing-thread`
- `pending` `send-reads-body-from-body-file`
- `pending` `send-attaches-artifact-with-metadata`
- `pending` `send-rejects-invalid-payload-json`
- `pending` `send-rejects-invalid-artifact-metadata-json`
### `docs/tests/inbox/fetch/README.md`
- `pending` `fetch-returns-pending-thread-for-target-agent`
- `pending` `fetch-respects-status-and-limit-filters`
- `pending` `fetch-unread-uses-read-cursor`
- `pending` `fetch-returns-no-matching-work-when-empty`
### `docs/tests/inbox/claim/README.md`
- `pending` `claim-acquires-thread-lease`
- `pending` `claim-rejects-when-thread-missing`
- `pending` `claim-rejects-when-thread-already-claimed`
- `pending` `claim-records-requested-lease-duration`
### `docs/tests/inbox/renew/README.md`
- `pending` `renew-extends-active-lease`
- `pending` `renew-rejects-non-owner`
- `pending` `renew-rejects-without-active-lease`
### `docs/tests/inbox/update/README.md`
- `pending` `update-moves-thread-to-in-progress`
- `pending` `update-moves-thread-to-blocked-with-payload`
- `pending` `update-accepts-body-file-and-artifact`
- `pending` `update-rejects-invalid-payload-json`
- `pending` `update-rejects-non-owner`
### `docs/tests/inbox/reply/README.md`
- `pending` `reply-adds-answer-message`
- `pending` `reply-supports-control-kind`
- `pending` `reply-attaches-artifact`
- `pending` `reply-rejects-invalid-payload-json`
### `docs/tests/inbox/done/README.md`
- `pending` `done-marks-thread-terminal`
- `pending` `done-persists-result-body-and-artifact`
- `pending` `done-rejects-non-owner`
- `pending` `done-rejects-on-terminal-thread`
### `docs/tests/inbox/fail/README.md`
- `pending` `fail-marks-thread-failed`
- `pending` `fail-persists-failure-body-and-artifact`
- `pending` `fail-rejects-non-owner`
- `pending` `fail-rejects-on-terminal-thread`
### `docs/tests/inbox/cancel/README.md`
- `pending` `cancel-marks-thread-cancelled`
- `pending` `cancel-persists-reason-and-artifact`
- `pending` `cancel-rejects-when-thread-missing`
### `docs/tests/inbox/list/README.md`
- `pending` `list-filters-by-status`
- `pending` `list-filters-by-created-by`
- `pending` `list-filters-by-assigned-to`
- `pending` `list-respects-limit`
### `docs/tests/inbox/show/README.md`
- `pending` `show-returns-thread-and-message-history`
- `pending` `show-includes-artifacts-per-message`
- `pending` `show-mark-read-advances-read-cursor`
- `pending` `show-rejects-when-thread-missing`
### `docs/tests/inbox/watch/README.md`
- `pending` `watch-wakes-on-matching-thread`
- `pending` `watch-respects-status-filter`
- `pending` `watch-times-out-with-no-activity`
### `docs/tests/inbox/wait-reply/README.md`
- `pending` `wait-reply-wakes-on-answer-after-message`
- `pending` `wait-reply-can-start-from-after-event`
- `pending` `wait-reply-times-out-when-no-reply`
1. add the new case to the relevant `README.md`
2. add the new slug to `Authored Case Register`
3. update `Current Snapshot` and `Document Progress`
## Definition Of Done
+130
View File
@@ -0,0 +1,130 @@
# Inbox Shared Test Conventions
## Purpose
This document captures shared assumptions used by multiple `inbox` test-plan documents so command and workflow files can stay focused on behavior rather than repeating setup boilerplate.
## Recommended Fixture Shape
Use an isolated temp workspace per case:
- database path: `TMPDIR/coord.db`
- optional body file: `TMPDIR/body.md`
- optional artifact file: `TMPDIR/artifact.txt`
Recommended bootstrap command:
```bash
inbox --db TMPDIR/coord.db --json init
```
## Global Flags
Root-level flags apply to every subcommand:
- `--db`: SQLite database path, default `.agents/coord.db`
- `--json`: emit machine-readable JSON
- `--agent`: acting agent identity shortcut used by commands that accept agent context
When a command-specific `--agent` or `--from` flag is omitted, the root `--agent` value may be used instead. Cases that verify fallback behavior should state that explicitly.
## Success JSON Contract
Successful JSON output uses this shape:
```json
{
"ok": true,
"command": "send",
"data": {}
}
```
Shared assertion points:
- `ok` is `true`
- `command` matches the invoked subcommand
- `data` contains the command-specific payload
## Error JSON Contract
Failure JSON output uses this shape:
```json
{
"ok": false,
"error": {
"code": "invalid_input",
"message": "..."
}
}
```
Shared assertion points:
- `ok` is `false`
- `error.code` matches the stable contract
- `error.message` is present and human-readable
## Exit Code Contract
The current CLI contract uses these exit codes:
| Exit Code | Meaning | Typical Error Code |
| --- | --- | --- |
| `0` | success | none |
| `10` | no matching work / timeout without match | `no_matching_work` |
| `20` | lease conflict | `lease_conflict` |
| `30` | invalid input, invalid state, usage-style error | `invalid_input` or `invalid_state` |
| `40` | referenced thread or message missing | `not_found` |
| `50` | unexpected internal failure | `internal_error` |
When a case expects no result, assert both the exit code and the JSON error code.
## Body Input Rules
Commands that support `--body` and `--body-file` follow these rules:
- `--body` and `--body-file` are mutually exclusive
- `--body-file` content is read verbatim into the message body
- unreadable `--body-file` should be treated as `invalid_input`
Relevant commands:
- `send`
- `update`
- `reply`
- `done`
- `fail`
## Artifact Rules
Commands with artifact support use these shared rules:
- `--artifact` may be repeated
- `--artifact-kind` may be specified once for all artifacts, or once per artifact
- `--artifact-metadata-json` may be specified once for all artifacts, or once per artifact
- `--artifact-kind` and `--artifact-metadata-json` are invalid without at least one `--artifact`
- an empty artifact path is invalid input
When artifact behavior is under test, assert at least:
- artifact count
- artifact `path`
- artifact `kind`
- metadata presence when supplied
## Read And Unread Assertions
Unread-related cases should verify behavior from the agent's point of view, not only raw message existence.
Recommended checks:
- `fetch --unread` returns a thread before read acknowledgement
- `show --mark-read` clears unread state for that agent
- a new message to the same thread makes the thread unread again
- `wait-reply` may clear blocked unread state for the waiting agent when the reply is consumed
## Workflow Authoring Rule
If a case spans multiple commands, place the end-to-end narrative in `workflows/README.md` first, then add narrower command-level cases only when they introduce behavior that is easier to reason about in isolation.
+87
View File
@@ -0,0 +1,87 @@
# Inbox `cancel` Test Plan
## Scope
This document covers thread cancellation via `inbox cancel`.
Shared conventions live in [../_shared/README.md](../_shared/README.md).
## case: cancel-marks-thread-cancelled
### 用例意义
验证 `cancel` 可以把非终态线程推进到 `cancelled` 终态,并生成控制消息。
### 前置条件
- 已存在一个非终态线程 `THREAD_ID`
### 输入
```bash
inbox --db TMPDIR/coord.db --json cancel --agent leader --thread THREAD_ID --reason "Task superseded by a larger refactor"
```
### 预期输出
- 命令退出码为 `0`
- `thread.status == "cancelled"`
- `message.kind == "control"`
### 断言结论
- `cancel` 是线程级终态转换
- 取消时会释放活跃 lease
## case: cancel-persists-reason-and-artifact
### 用例意义
验证 `cancel` 的原因文本与附件会被完整持久化。
### 前置条件
- 已存在一个非终态线程 `THREAD_ID`
- `TMPDIR/cancel.md` 已存在
### 输入
```bash
inbox --db TMPDIR/coord.db --json cancel --agent leader --thread THREAD_ID --reason "Task superseded by a larger refactor" --artifact TMPDIR/cancel.md --artifact-kind brief
inbox --db TMPDIR/coord.db --json show --thread THREAD_ID
```
### 预期输出
- `cancel` 成功
- 取消消息 `summary``body` 都保留取消原因
- 取消消息包含 1 个 artifact
### 断言结论
- `cancel` 既保留人类可读原因,也支持附带上下文材料
## case: cancel-rejects-when-thread-missing
### 用例意义
验证 `cancel` 对不存在线程返回稳定的 not-found 错误契约。
### 前置条件
- 空数据库已完成 `init`
### 输入
```bash
inbox --db TMPDIR/coord.db --json cancel --agent leader --thread thr_missing
```
### 预期输出
- 退出码为 `40`
- JSON 错误码为 `not_found`
### 断言结论
- `cancel` 不会为缺失线程隐式创建控制消息
+112
View File
@@ -0,0 +1,112 @@
# Inbox `claim` Test Plan
## Scope
This document covers lease acquisition via `inbox claim`.
Shared conventions live in [../_shared/README.md](../_shared/README.md).
## case: claim-acquires-thread-lease
### 用例意义
验证 `claim` 可以把 `pending` 线程切换到 `claimed`,并生成租约事件消息。
### 前置条件
- 已存在一个 `pending` 线程 `THREAD_ID`
### 输入
```bash
inbox --db TMPDIR/coord.db --json claim --agent worker-a --thread THREAD_ID --lease-seconds 300
```
### 预期输出
- 命令退出码为 `0`
- `thread.status == "claimed"`
- `thread.assigned_to == "worker-a"`
- `message.kind == "event"`
- `message.summary == "thread claimed"`
### 断言结论
- `claim` 同时更新线程状态与活跃租约
- 成功领取会附带一条事件消息,而不是静默改状态
## case: claim-rejects-when-thread-missing
### 用例意义
验证 `claim` 对不存在的线程返回稳定的 not-found 错误契约。
### 前置条件
- 空数据库已完成 `init`
### 输入
```bash
inbox --db TMPDIR/coord.db --json claim --agent worker-z --thread thr_missing
```
### 预期输出
- 退出码为 `40`
- JSON 错误码为 `not_found`
### 断言结论
- 缺失线程会被明确区分为引用错误,而不是 lease 冲突
## case: claim-rejects-when-thread-already-claimed
### 用例意义
验证同一线程在已有活跃租约时,其他执行者无法重复领取。
### 前置条件
- `worker-z` 已成功 `claim` 线程 `THREAD_ID`
### 输入
```bash
inbox --db TMPDIR/coord.db --json claim --agent worker-y --thread THREAD_ID
```
### 预期输出
- 退出码为 `20`
- JSON 错误码为 `lease_conflict`
### 断言结论
- 活跃 lease 是 `claim` 的排他条件
## case: claim-records-requested-lease-duration
### 用例意义
验证 `claim --lease-seconds` 的请求值会进入事件消息 payload,便于后续审计。
### 前置条件
- 已存在一个 `pending` 线程 `THREAD_ID`
### 输入
```bash
inbox --db TMPDIR/coord.db --json claim --agent worker-a --thread THREAD_ID --lease-seconds 300
```
### 预期输出
- 命令退出码为 `0`
- `message.payload_json.lease_seconds == 300`
- `message.payload_json.lease_token` 存在
### 断言结论
- 请求的租约时长不是仅用于内部计算,也会被持久化到事件消息中
+112
View File
@@ -0,0 +1,112 @@
# Inbox `done` Test Plan
## Scope
This document covers successful terminal completion via `inbox done`.
Shared conventions live in [../_shared/README.md](../_shared/README.md).
## case: done-marks-thread-terminal
### 用例意义
验证租约拥有者可以将线程推进到 `done` 终态,并生成结果消息。
### 前置条件
- `worker-a` 已成功 `claim` 线程 `THREAD_ID`
### 输入
```bash
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."
```
### 预期输出
- 命令退出码为 `0`
- `thread.status == "done"`
- `message.kind == "result"`
### 断言结论
- `done` 会把线程推进到成功终态
- 完成时会释放活跃 lease
## case: done-persists-result-body-and-artifact
### 用例意义
验证 `done` 能持久化结果正文与附件,并被后续 `show` 读取。
### 前置条件
- `worker-a` 已成功 `claim` 线程 `THREAD_ID`
- `TMPDIR/result.md` 已存在
### 输入
```bash
inbox --db TMPDIR/coord.db --json done --agent worker-a --thread THREAD_ID --summary "Retry policy implemented" --body-file TMPDIR/result.md --artifact TMPDIR/result.md --artifact-kind report
inbox --db TMPDIR/coord.db --json show --thread THREAD_ID
```
### 预期输出
- `done` 成功
- 最终结果消息 `body` 等于文件内容
- 结果消息包含 1 个 `report` artifact
### 断言结论
- `done` 是结果交付命令,不只是状态切换命令
## case: done-rejects-non-owner
### 用例意义
验证非租约拥有者不能代替执行者完成线程。
### 前置条件
- `worker-a` 已成功 `claim` 线程 `THREAD_ID`
### 输入
```bash
inbox --db TMPDIR/coord.db --json done --agent worker-b --thread THREAD_ID --summary "Retry policy implemented"
```
### 预期输出
- 退出码为 `20`
- JSON 错误码为 `lease_conflict`
### 断言结论
- `done` 受活跃 lease 所属者约束
## case: done-rejects-on-terminal-thread
### 用例意义
验证已进入终态的线程不能再次执行 `done`
### 前置条件
- 线程 `THREAD_ID` 已经是 `done``failed``cancelled`
### 输入
```bash
inbox --db TMPDIR/coord.db --json done --agent worker-a --thread THREAD_ID --summary "Retry policy implemented"
```
### 预期输出
- 退出码为 `30`
- JSON 错误码为 `invalid_state`
### 断言结论
- `done` 对终态线程是幂等失败,而不是重复成功
+111
View File
@@ -0,0 +1,111 @@
# Inbox `fail` Test Plan
## Scope
This document covers failure terminal completion via `inbox fail`.
Shared conventions live in [../_shared/README.md](../_shared/README.md).
## case: fail-marks-thread-failed
### 用例意义
验证租约拥有者可以把线程推进到 `failed` 终态,并生成失败结果消息。
### 前置条件
- `worker-b` 已成功 `claim` 线程 `THREAD_ID`
### 输入
```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."
```
### 预期输出
- 命令退出码为 `0`
- `thread.status == "failed"`
- `message.kind == "result"`
### 断言结论
- `fail``done` 共享结果消息模型,但进入的是失败终态
## case: fail-persists-failure-body-and-artifact
### 用例意义
验证 `fail` 能持久化失败说明与附件。
### 前置条件
- `worker-b` 已成功 `claim` 线程 `THREAD_ID`
- `TMPDIR/failure.md` 已存在
### 输入
```bash
inbox --db TMPDIR/coord.db --json fail --agent worker-b --thread THREAD_ID --summary "Migration failed" --body-file TMPDIR/failure.md --artifact TMPDIR/failure.md --artifact-kind report
inbox --db TMPDIR/coord.db --json show --thread THREAD_ID
```
### 预期输出
- `fail` 成功
- 最终结果消息 `body` 等于文件内容
- 结果消息包含 1 个 `report` artifact
### 断言结论
- 失败终态同样要能完整交付排障材料
## case: fail-rejects-non-owner
### 用例意义
验证非租约拥有者不能把线程标记为失败。
### 前置条件
- `worker-b` 已成功 `claim` 线程 `THREAD_ID`
### 输入
```bash
inbox --db TMPDIR/coord.db --json fail --agent worker-x --thread THREAD_ID --summary "Migration failed"
```
### 预期输出
- 退出码为 `20`
- JSON 错误码为 `lease_conflict`
### 断言结论
- `fail``done` 一样受 lease owner 约束
## case: fail-rejects-on-terminal-thread
### 用例意义
验证已进入终态的线程不能再次执行 `fail`
### 前置条件
- 线程 `THREAD_ID` 已经是 `done``failed``cancelled`
### 输入
```bash
inbox --db TMPDIR/coord.db --json fail --agent worker-b --thread THREAD_ID --summary "Migration failed"
```
### 预期输出
- 退出码为 `30`
- JSON 错误码为 `invalid_state`
### 断言结论
- `fail` 对终态线程不会重复成功
+117
View File
@@ -0,0 +1,117 @@
# Inbox `fetch` Test Plan
## Scope
This document covers agent-scoped candidate thread retrieval via `inbox fetch`.
Shared conventions live in [../_shared/README.md](../_shared/README.md).
## case: fetch-returns-pending-thread-for-target-agent
### 用例意义
验证 `fetch` 能按目标执行者拉取待处理线程。
### 前置条件
- `leader` 已向 `worker-a` 发送至少一个 `pending` 线程
### 输入
```bash
inbox --db TMPDIR/coord.db --json fetch --agent worker-a --status pending
```
### 预期输出
- 命令退出码为 `0`
- 返回 `data.threads`
- 至少包含一个 `assigned_to == "worker-a"``status == "pending"` 的线程
### 断言结论
- `fetch` 默认是执行者视角的候选工作列表,不是全局线程扫描
## case: fetch-respects-status-and-limit-filters
### 用例意义
验证 `fetch` 同时遵守状态过滤与返回上限。
### 前置条件
- `worker-a` 拥有多个不同状态的线程
- 其中至少两个线程满足目标状态过滤条件
### 输入
```bash
inbox --db TMPDIR/coord.db --json fetch --agent worker-a --status pending,blocked --limit 1
```
### 预期输出
- 命令退出码为 `0`
- 返回线程数不超过 `1`
- 返回的每条线程都满足 `status in ["pending","blocked"]`
### 断言结论
- `fetch``status``limit` 会同时生效
- 返回顺序按 `updated_at` 倒序,优先暴露最新线程
## case: fetch-unread-uses-read-cursor
### 用例意义
验证 `fetch --unread` 基于 agent 的 read cursor 计算未读,而不是仅按线程是否存在新消息。
### 前置条件
- `leader` 已向 `worker-e` 发送一个 `pending` 线程 `THREAD_ID`
### 输入
```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` 后,第二次 `fetch --unread` 无匹配结果
- 新消息追加后,第三次 `fetch --unread` 再次返回该线程
### 断言结论
- 未读判断依赖 `thread_reads.last_read_message_id`
- 新消息到达会让同线程重新进入未读结果集
## case: fetch-returns-no-matching-work-when-empty
### 用例意义
验证 `fetch` 在没有匹配线程时返回稳定的“无工作”错误契约。
### 前置条件
- 空数据库已完成 `init`
### 输入
```bash
inbox --db TMPDIR/coord.db --json fetch --agent worker-z --status pending
```
### 预期输出
- 退出码为 `10`
- JSON 错误码为 `no_matching_work`
### 断言结论
- 空结果不是成功空数组,而是显式的“无匹配工作”信号
+64
View File
@@ -0,0 +1,64 @@
# Inbox `init` Test Plan
## Scope
This document covers the externally visible behavior of `inbox init`.
Shared conventions live in [../_shared/README.md](../_shared/README.md).
## case: init-creates-schema-on-empty-db
### 用例意义
验证在空数据库路径上执行 `init` 会创建可用的 inbox schema,并返回稳定的初始化响应。
### 前置条件
- 选择一个尚不存在的数据库路径 `TMPDIR/coord.db`
### 输入
```bash
inbox --db TMPDIR/coord.db --json init
```
### 预期输出
- 命令退出码为 `0`
- 返回 `ok=true`
- `command``init`
- `data.db_path` 等于传入路径
- `data.status``initialized`
### 断言结论
- `init` 在空路径上可以直接完成 schema 初始化
- 初始化结果足以让后续 `send``fetch` 等命令继续使用同一数据库
## case: init-is-idempotent-on-existing-db
### 用例意义
验证 `init` 可以对已初始化过的数据库重复执行,而不会报错或破坏已有 schema。
### 前置条件
- `TMPDIR/coord.db` 已经执行过一次 `inbox --db TMPDIR/coord.db --json init`
### 输入
```bash
inbox --db TMPDIR/coord.db --json init
inbox --db TMPDIR/coord.db --json init
```
### 预期输出
- 两次命令都退出码为 `0`
- 两次响应都返回 `data.status == "initialized"`
- 两次响应都返回相同的 `data.db_path`
### 断言结论
- `init` 是幂等操作
- 对已存在 schema 的重复初始化不应引入额外迁移失败或状态漂移
+107
View File
@@ -0,0 +1,107 @@
# Inbox `list` Test Plan
## Scope
This document covers thread listing behavior via `inbox list`.
Shared conventions live in [../_shared/README.md](../_shared/README.md).
## case: list-filters-by-status
### 用例意义
验证 `list --status` 只返回指定状态集合内的线程。
### 前置条件
- 数据库中存在多个不同状态的线程
### 输入
```bash
inbox --db TMPDIR/coord.db --json list --status pending,blocked
```
### 预期输出
- 命令退出码为 `0`
- 返回的每条线程都满足 `status in ["pending","blocked"]`
### 断言结论
- `list` 是全局筛选视角,状态过滤不会被忽略
## case: list-filters-by-created-by
### 用例意义
验证 `list --created-by` 能按线程创建者筛选结果。
### 前置条件
- 至少有两位不同创建者产生的线程
### 输入
```bash
inbox --db TMPDIR/coord.db --json list --created-by leader
```
### 预期输出
- 命令退出码为 `0`
- 返回的每条线程都满足 `created_by == "leader"`
### 断言结论
- `created-by` 过滤条件直接作用在线程元数据上
## case: list-filters-by-assigned-to
### 用例意义
验证 `list --assigned-to` 能按当前指派执行者筛选线程。
### 前置条件
- 数据库中存在多个不同 `assigned_to` 的线程
### 输入
```bash
inbox --db TMPDIR/coord.db --json list --assigned-to worker-d --status pending
```
### 预期输出
- 命令退出码为 `0`
- 返回的每条线程都满足 `assigned_to == "worker-d"`
### 断言结论
- `list` 可用于管理侧查看某位执行者当前承担的线程集合
## case: list-respects-limit
### 用例意义
验证 `list --limit` 会约束返回条数,并按更新时间倒序返回最新线程。
### 前置条件
- 存在多个满足过滤条件的线程
### 输入
```bash
inbox --db TMPDIR/coord.db --json list --assigned-to worker-d --limit 1
```
### 预期输出
- 命令退出码为 `0`
- 返回线程数不超过 `1`
### 断言结论
- `list` 的 limit 是硬上限,不会返回超量结果
+87
View File
@@ -0,0 +1,87 @@
# Inbox `renew` Test Plan
## Scope
This document covers lease renewal behavior via `inbox renew`.
Shared conventions live in [../_shared/README.md](../_shared/README.md).
## case: renew-extends-active-lease
### 用例意义
验证租约拥有者可以对活跃 lease 执行续租,并生成续租事件消息。
### 前置条件
- `worker-c` 已成功 `claim` 线程 `THREAD_ID`
### 输入
```bash
inbox --db TMPDIR/coord.db --json renew --agent worker-c --thread THREAD_ID --lease-seconds 600
```
### 预期输出
- 命令退出码为 `0`
- `thread.status` 保持原状态
- `message.kind == "event"`
- `message.summary == "lease renewed"`
- `message.payload_json.lease_seconds == 600`
### 断言结论
- `renew` 是在原线程上追加续租事件,而不是重新 claim
## case: renew-rejects-non-owner
### 用例意义
验证非租约拥有者不能续租别人的活跃 lease。
### 前置条件
- `worker-c` 已成功 `claim` 线程 `THREAD_ID`
### 输入
```bash
inbox --db TMPDIR/coord.db --json renew --agent worker-x --thread THREAD_ID --lease-seconds 600
```
### 预期输出
- 退出码为 `20`
- JSON 错误码为 `lease_conflict`
### 断言结论
- `renew``claim` 一样受 lease owner 约束
## case: renew-rejects-without-active-lease
### 用例意义
验证线程没有活跃租约时,`renew` 会明确失败。
### 前置条件
- 已存在线程 `THREAD_ID`
- 该线程当前没有活跃 lease
### 输入
```bash
inbox --db TMPDIR/coord.db --json renew --agent worker-c --thread THREAD_ID --lease-seconds 600
```
### 预期输出
- 退出码为 `30`
- JSON 错误码为 `invalid_state`
### 断言结论
- `renew` 依赖已有活跃租约
- 没有 lease 属于状态错误,不是 not-found
+111
View File
@@ -0,0 +1,111 @@
# Inbox `reply` Test Plan
## Scope
This document covers reply behavior within an existing thread via `inbox reply`.
Shared conventions live in [../_shared/README.md](../_shared/README.md).
## case: reply-adds-answer-message
### 用例意义
验证 `reply` 默认会向现有线程追加一条 `answer` 消息,并保持线程状态不变。
### 前置条件
- 已存在一个非终态线程 `THREAD_ID`
### 输入
```bash
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."
```
### 预期输出
- 命令退出码为 `0`
- `message.kind == "answer"`
- `thread.thread_id == THREAD_ID`
- 线程状态保持原值
### 断言结论
- `reply` 是线程内追加消息,而不是状态转换命令
## case: reply-supports-control-kind
### 用例意义
验证 `reply --kind control` 可以发送控制类消息,而不局限于默认 `answer`
### 前置条件
- 已存在一个非终态线程 `THREAD_ID`
### 输入
```bash
inbox --db TMPDIR/coord.db --json reply --from leader --to worker-a --thread THREAD_ID --kind control --summary "Pause rollout" --body "Pause rollout until QA confirms the fix."
```
### 预期输出
- 命令退出码为 `0`
- `message.kind == "control"`
### 断言结论
- `reply` 的消息种类可由调用方显式指定
## case: reply-attaches-artifact
### 用例意义
验证 `reply` 支持追加带附件的答复消息。
### 前置条件
- 已存在一个非终态线程 `THREAD_ID`
- `TMPDIR/decision.md` 已存在
### 输入
```bash
inbox --db TMPDIR/coord.db --json reply --from leader --to worker-a --thread THREAD_ID --summary "Retry read timeouts" --artifact TMPDIR/decision.md --artifact-kind brief --artifact-metadata-json '{"label":"decision"}'
```
### 预期输出
- 命令退出码为 `0`
- `message.artifacts` 长度为 `1`
- artifact 路径、kind、metadata 都可读
### 断言结论
- `reply``send/update/done/fail` 共享附件写入契约
## case: reply-rejects-invalid-payload-json
### 用例意义
验证 `reply` 对非法 `--payload-json` 输入返回稳定错误契约。
### 前置条件
- 已存在一个非终态线程 `THREAD_ID`
### 输入
```bash
inbox --db TMPDIR/coord.db --json reply --from leader --to worker-a --thread THREAD_ID --summary "Retry read timeouts" --payload-json not-json
```
### 预期输出
- 退出码为 `30`
- JSON 错误码为 `invalid_input`
### 断言结论
- `reply` 的 payload 与其他消息写入命令一样需要通过 JSON 校验
+176
View File
@@ -0,0 +1,176 @@
# Inbox `send` Test Plan
## Scope
This document covers thread creation and message append behavior exposed by `inbox send`.
Shared conventions live in [../_shared/README.md](../_shared/README.md).
## case: send-creates-new-thread
### 用例意义
验证 `send` 在未指定既有线程时会创建新线程,并写入首条任务消息。
### 前置条件
- 空数据库已完成 `init`
### 输入
```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
```
### 预期输出
- 命令退出码为 `0`
- 返回 `thread.thread_id`
- `thread.status == "pending"`
- `thread.created_by == "leader"`
- `thread.assigned_to == "worker-a"`
- `message.kind == "task"`
### 断言结论
- `send` 会新建线程而不是只插入孤立消息
- 新线程的默认初始状态是 `pending`
## case: send-appends-message-to-existing-thread
### 用例意义
验证 `send` 在指定既有 `--thread` 时会向原线程追加消息,而不是重建线程。
### 前置条件
- 已存在一个由 `leader` 发给 `worker-d` 的线程 `THREAD_ID`
### 输入
```bash
inbox --db TMPDIR/coord.db --json send --from leader --to worker-d --thread THREAD_ID --summary "Use a markdown editor" --body "Prefer a textarea-based markdown editor for v1."
inbox --db TMPDIR/coord.db --json show --thread THREAD_ID
```
### 预期输出
- `send` 成功,返回的 `thread.thread_id` 仍为 `THREAD_ID`
- 线程状态保持原值,不被强制改写为新状态
- `show` 可见消息数增加
### 断言结论
- 追加消息不会重置线程生命周期
- 线程历史按时间顺序保留旧消息与新消息
## case: send-reads-body-from-body-file
### 用例意义
验证 `send --body-file` 会把文件内容写入消息正文。
### 前置条件
- `TMPDIR/task.md` 已存在,内容为测试正文
### 输入
```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
inbox --db TMPDIR/coord.db --json show --thread THREAD_ID
```
### 预期输出
- `send` 成功
- `show` 首条消息的 `body` 与文件内容一致
### 断言结论
- `body-file` 内容会被原样读取
- 该行为与直接传 `--body` 的最终存储结果等价
## case: send-attaches-artifact-with-metadata
### 用例意义
验证 `send` 支持附带 artifact、kind 和 metadata,并可在返回值或后续 `show` 中读取。
### 前置条件
- `TMPDIR/task.md` 已存在
### 输入
```bash
inbox --db TMPDIR/coord.db --json send --from leader --to worker-d --subject "Build admin editor" --summary "Create the first editor screen" --artifact TMPDIR/task.md --artifact-kind brief --artifact-metadata-json '{"label":"task-brief"}'
```
### 预期输出
- 命令退出码为 `0`
- `message.artifacts` 长度为 `1`
- artifact `path == "TMPDIR/task.md"`
- artifact `kind == "brief"`
- artifact `metadata_json.label == "task-brief"`
### 断言结论
- `send` 可以在创建消息时持久化附件及其结构化元数据
## case: send-rejects-invalid-payload-json
### 用例意义
验证 `send` 对非法 `--payload-json` 输入给出稳定错误契约。
### 前置条件
- 空数据库已完成 `init`
### 输入
```bash
inbox --db TMPDIR/coord.db --json send --from leader --to worker-z --subject "Invalid payload json" --payload-json not-json
```
### 预期输出
- 退出码为 `30`
- JSON 错误码为 `invalid_input`
### 断言结论
- 非法 payload 在写库前就会被拒绝
- 错误归类为输入问题,而不是内部错误
## case: send-rejects-invalid-artifact-metadata-json
### 用例意义
验证 `send` 对非法 artifact metadata JSON 给出稳定错误契约。
### 前置条件
- 空数据库已完成 `init`
### 输入
```bash
inbox --db TMPDIR/coord.db --json send --from leader --to worker-z --subject "Invalid artifact json" --artifact TMPDIR/report.md --artifact-metadata-json not-json
```
### 预期输出
- 退出码为 `30`
- JSON 错误码为 `invalid_input`
### 断言结论
- artifact metadata 会在写入前校验 JSON 合法性
## Notes
- 新建线程时未显式传 `--summary`,会回退到 `--subject`
- `--body``--body-file` 互斥;该约束由 shared 文档统一说明
+115
View File
@@ -0,0 +1,115 @@
# Inbox `show` Test Plan
## Scope
This document covers per-thread detail retrieval via `inbox show`.
Shared conventions live in [../_shared/README.md](../_shared/README.md).
## case: show-returns-thread-and-message-history
### 用例意义
验证 `show` 会返回线程详情和完整消息历史。
### 前置条件
- 已存在一个含多条消息的线程 `THREAD_ID`
### 输入
```bash
inbox --db TMPDIR/coord.db --json show --thread THREAD_ID
```
### 预期输出
- 命令退出码为 `0`
- 返回 `data.thread`
- 返回 `data.messages`
- 消息按创建时间升序排列
### 断言结论
- `show` 是线程详情与时间序历史的读取入口
## case: show-includes-artifacts-per-message
### 用例意义
验证 `show` 返回的每条消息都包含其关联 artifact 列表。
### 前置条件
- 线程 `THREAD_ID` 中至少一条消息附带 artifact
### 输入
```bash
inbox --db TMPDIR/coord.db --json show --thread THREAD_ID
```
### 预期输出
- 命令退出码为 `0`
- 相关消息节点包含 `artifacts`
- artifact 的 `path``kind``metadata_json` 可读
### 断言结论
- `show` 需要把附件一并展开,而不是只返回 message 基本字段
## case: show-mark-read-advances-read-cursor
### 用例意义
验证 `show --mark-read` 会推进调用 agent 的 read cursor,并影响后续 unread 查询。
### 前置条件
- `worker-e` 有一个未读线程 `THREAD_ID`
### 输入
```bash
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
```
### 预期输出
- `show` 成功
- 随后的 `fetch --unread` 对该线程不再返回结果
### 断言结论
- `mark-read` 的副作用是推进该 agent 的 `last_read_message_id`
## case: show-rejects-when-thread-missing
### 用例意义
验证 `show` 对不存在线程返回稳定的 not-found 错误契约。
### 前置条件
- 空数据库已完成 `init`
### 输入
```bash
inbox --db TMPDIR/coord.db --json show --thread thr_missing
```
### 预期输出
- 退出码为 `40`
- JSON 错误码为 `not_found`
### 断言结论
- `show` 不会对缺失线程返回空对象
## Notes
- 使用 `--mark-read` 时必须提供 agent 身份,可通过根级 `--agent` 或命令参数传入
+139
View File
@@ -0,0 +1,139 @@
# Inbox `update` Test Plan
## Scope
This document covers progress and blocked transitions via `inbox update`.
Shared conventions live in [../_shared/README.md](../_shared/README.md).
## case: update-moves-thread-to-in-progress
### 用例意义
验证租约拥有者可以把线程推进到 `in_progress`,并生成进度消息。
### 前置条件
- `worker-a` 已成功 `claim` 线程 `THREAD_ID`
### 输入
```bash
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."
```
### 预期输出
- 命令退出码为 `0`
- `thread.status == "in_progress"`
- `message.kind == "progress"`
- `message.to_agent` 指向线程创建者
### 断言结论
- `update` 会把状态推进和消息追加合并为同一次事务
## case: update-moves-thread-to-blocked-with-payload
### 用例意义
验证 `update --status blocked` 会写入阻塞问题消息,并保留结构化 payload。
### 前置条件
- `worker-a` 已成功 `claim` 线程 `THREAD_ID`
### 输入
```bash
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?"}'
```
### 预期输出
- 命令退出码为 `0`
- `thread.status == "blocked"`
- `message.kind == "question"`
- `message.payload_json.question` 保存提问内容
### 断言结论
- `blocked` 更新会生成面向创建者的问题消息
## case: update-accepts-body-file-and-artifact
### 用例意义
验证 `update` 支持通过 `body-file` 与 artifact 发送结构化进度材料。
### 前置条件
- `worker-a` 已成功 `claim` 线程 `THREAD_ID`
- `TMPDIR/progress.md` 已存在
### 输入
```bash
inbox --db TMPDIR/coord.db --json update --agent worker-a --thread THREAD_ID --status in_progress --summary "Implementation started" --body-file TMPDIR/progress.md --artifact TMPDIR/progress.md --artifact-kind note
inbox --db TMPDIR/coord.db --json show --thread THREAD_ID
```
### 预期输出
- `update` 成功
- 对应消息 `body` 等于文件内容
- 对应消息包含 1 个 artifactkind 为 `note`
### 断言结论
- `update` 的正文与 artifact 支持与 `send/reply/done/fail` 保持一致
## case: update-rejects-invalid-payload-json
### 用例意义
验证 `update` 对非法 `--payload-json` 输入返回稳定错误契约。
### 前置条件
- `worker-a` 已成功 `claim` 线程 `THREAD_ID`
### 输入
```bash
inbox --db TMPDIR/coord.db --json update --agent worker-a --thread THREAD_ID --status blocked --summary "Need timeout decision" --payload-json not-json
```
### 预期输出
- 退出码为 `30`
- JSON 错误码为 `invalid_input`
### 断言结论
- 阻塞问题的 payload 需要满足合法 JSON 约束
## case: update-rejects-non-owner
### 用例意义
验证非租约拥有者不能更新线程状态。
### 前置条件
- `worker-a` 已成功 `claim` 线程 `THREAD_ID`
### 输入
```bash
inbox --db TMPDIR/coord.db --json update --agent worker-b --thread THREAD_ID --status in_progress --summary "Implementation started"
```
### 预期输出
- 退出码为 `20`
- JSON 错误码为 `lease_conflict`
### 断言结论
- `update` 明确依赖活跃 lease 所属者
+93
View File
@@ -0,0 +1,93 @@
# Inbox `wait-reply` Test Plan
## Scope
This document covers blocking reply wait behavior within one thread via `inbox wait-reply`.
Shared conventions live in [../_shared/README.md](../_shared/README.md).
## case: wait-reply-wakes-on-answer-after-message
### 用例意义
验证 `wait-reply` 可以从某条已知消息之后开始等待,并在答复到达后唤醒。
### 前置条件
- `worker-c` 已拥有一个 `blocked` 线程 `THREAD_ID`
- 阻塞消息的 `message_id``BLOCKED_MESSAGE_ID`
### 输入
```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."
```
### 预期输出
- `wait-reply` 退出码为 `0`
- `wait-reply.data.woke == true`
- 返回的 `message.kind == "answer"`
### 断言结论
- `wait-reply` 可以可靠地从既知消息边界之后等待后续答复
## case: wait-reply-can-start-from-after-event
### 用例意义
验证 `wait-reply --after-event` 支持从既知事件游标之后恢复等待。
### 前置条件
- 已通过先前的 `watch``wait-reply` 结果拿到某个 `NEXT_EVENT_ID`
- 线程 `THREAD_ID` 后续还会收到新的回复类消息
### 输入
```bash
inbox --db TMPDIR/coord.db --agent worker-c --json wait-reply --thread THREAD_ID --after-event NEXT_EVENT_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."
```
### 预期输出
- `wait-reply` 在事件游标之后的新回复出现时被唤醒
- 返回新的 `next_event_id`
### 断言结论
- `after-event` 允许等待逻辑在断点之后继续,而不会重复消费旧回复
## case: wait-reply-times-out-when-no-reply
### 用例意义
验证在超时时间内没有匹配回复出现时,`wait-reply` 返回稳定超时契约。
### 前置条件
- 存在一个线程 `THREAD_ID`
- 不会有新的 `answer/control/result` 消息到达
### 输入
```bash
inbox --db TMPDIR/coord.db --agent worker-c --json wait-reply --thread THREAD_ID --timeout-seconds 1
```
### 预期输出
- 退出码为 `10`
- JSON 错误码为 `no_matching_work`
### 断言结论
- `wait-reply` 超时被视为“没有等到匹配回复”
## Notes
- 默认唤醒 kinds 为 `answer,control,result`
- 当返回消息是发给等待 agent 的外来消息时,`wait-reply` 会顺带推进该 agent 的 read cursor
+91
View File
@@ -0,0 +1,91 @@
# Inbox `watch` Test Plan
## Scope
This document covers blocking thread-activity wait behavior via `inbox watch`.
Shared conventions live in [../_shared/README.md](../_shared/README.md).
## case: watch-wakes-on-matching-thread
### 用例意义
验证 `watch` 在新匹配线程到达时会被唤醒,并返回线程、消息与事件信息。
### 前置条件
- `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"
```
### 预期输出
- `watch` 退出码为 `0`
- `watch.data.woke == true`
- 返回 `thread``message``event`
### 断言结论
- `watch` 唤醒结果不仅说明“醒了”,还提供触发该唤醒的具体事件上下文
## case: watch-respects-status-filter
### 用例意义
验证 `watch --status` 只会对匹配状态的后续事件唤醒。
### 前置条件
- 存在一个会被推进到 `blocked` 的线程 `THREAD_ID`
- `watch``--status blocked` 先启动
### 输入
```bash
inbox --db TMPDIR/coord.db --json watch --agent worker-c --status blocked --timeout-seconds 2
inbox --db TMPDIR/coord.db --json update --agent worker-c --thread THREAD_ID --status blocked --summary "Need policy decision"
```
### 预期输出
- `watch` 只在线程进入 `blocked` 后返回
- 返回的 `thread.status == "blocked"`
### 断言结论
- `watch` 的状态过滤作用在“事件发生后的线程状态”上
## case: watch-times-out-with-no-activity
### 用例意义
验证在超时时间内没有匹配活动时,`watch` 返回稳定超时契约。
### 前置条件
- 没有新匹配事件会发生
### 输入
```bash
inbox --db TMPDIR/coord.db --json watch --agent worker-d --status pending --timeout-seconds 1
```
### 预期输出
- 退出码为 `10`
- JSON 错误码为 `no_matching_work`
### 断言结论
- `watch` 超时被归类为“无匹配工作”,而不是内部错误
## Notes
- 未传 `--after-event` 时,`watch` 默认从“当前时刻之后”开始等待,不会回放既有事件
+276
View File
@@ -0,0 +1,276 @@
# 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 来说,答复消费与未读清理是同一条用户契约链路