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 - `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 - 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` - 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`: Progress summary for planned test-plan documents, excluding `ROADMAP.md`:
- planned document files: `16` - planned document files: `17`
- authored document files: `0` - authored document files: `17`
- planned case slugs in this roadmap: `61` - planned case slugs in this roadmap: `61`
- authored case slugs in this roadmap: `0` - authored case slugs in this roadmap: `61`
## Scope ## 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: 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](../../../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](../../../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](../../../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](../../../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](../../../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#L639) `TestInboxJSONErrorsAndExitCodes`
These tests do not remove the need for the Markdown plan. They only reduce discovery work. 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 | | Path | Purpose | Planned Cases | Authored Cases | Status |
| --- | --- | ---: | ---: | --- | | --- | --- | ---: | ---: | --- |
| `docs/tests/inbox/README.md` | Global testing conventions and glossary | 0 | 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 | pending | | `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 | 0 | pending | | `docs/tests/inbox/workflows/README.md` | Cross-command scenarios | 8 | 8 | done |
| `docs/tests/inbox/init/README.md` | `init` command cases | 2 | 0 | pending | | `docs/tests/inbox/init/README.md` | `init` command cases | 2 | 2 | done |
| `docs/tests/inbox/send/README.md` | `send` command cases | 6 | 0 | pending | | `docs/tests/inbox/send/README.md` | `send` command cases | 6 | 6 | done |
| `docs/tests/inbox/fetch/README.md` | `fetch` command cases | 4 | 0 | pending | | `docs/tests/inbox/fetch/README.md` | `fetch` command cases | 4 | 4 | done |
| `docs/tests/inbox/claim/README.md` | `claim` command cases | 4 | 0 | pending | | `docs/tests/inbox/claim/README.md` | `claim` command cases | 4 | 4 | done |
| `docs/tests/inbox/renew/README.md` | `renew` command cases | 3 | 0 | pending | | `docs/tests/inbox/renew/README.md` | `renew` command cases | 3 | 3 | done |
| `docs/tests/inbox/update/README.md` | `update` command cases | 5 | 0 | pending | | `docs/tests/inbox/update/README.md` | `update` command cases | 5 | 5 | done |
| `docs/tests/inbox/reply/README.md` | `reply` command cases | 4 | 0 | pending | | `docs/tests/inbox/reply/README.md` | `reply` command cases | 4 | 4 | done |
| `docs/tests/inbox/done/README.md` | `done` command cases | 4 | 0 | pending | | `docs/tests/inbox/done/README.md` | `done` command cases | 4 | 4 | done |
| `docs/tests/inbox/fail/README.md` | `fail` command cases | 4 | 0 | pending | | `docs/tests/inbox/fail/README.md` | `fail` command cases | 4 | 4 | done |
| `docs/tests/inbox/cancel/README.md` | `cancel` command cases | 3 | 0 | pending | | `docs/tests/inbox/cancel/README.md` | `cancel` command cases | 3 | 3 | done |
| `docs/tests/inbox/list/README.md` | `list` command cases | 4 | 0 | pending | | `docs/tests/inbox/list/README.md` | `list` command cases | 4 | 4 | done |
| `docs/tests/inbox/show/README.md` | `show` command cases | 4 | 0 | pending | | `docs/tests/inbox/show/README.md` | `show` command cases | 4 | 4 | done |
| `docs/tests/inbox/watch/README.md` | `watch` command cases | 3 | 0 | pending | | `docs/tests/inbox/watch/README.md` | `watch` command cases | 3 | 3 | done |
| `docs/tests/inbox/wait-reply/README.md` | `wait-reply` command cases | 3 | 0 | pending | | `docs/tests/inbox/wait-reply/README.md` | `wait-reply` command cases | 3 | 3 | done |
## Authoring Order ## Authoring Order
@@ -198,121 +198,79 @@ Reason:
## Authored Case Register ## 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 | | 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 ## Pending Case Backlog
### `docs/tests/inbox/workflows/README.md` No pending case slugs remain in the current plan.
- `pending` `thread-lifecycle-happy-path` When a new CLI contract or workflow needs coverage:
- `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`
### `docs/tests/inbox/init/README.md` 1. add the new case to the relevant `README.md`
2. add the new slug to `Authored Case Register`
- `pending` `init-creates-schema-on-empty-db` 3. update `Current Snapshot` and `Document Progress`
- `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`
## Definition Of Done ## 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 来说,答复消费与未读清理是同一条用户契约链路