diff --git a/docs/tests/inbox/README.md b/docs/tests/inbox/README.md new file mode 100644 index 0000000..3721956 --- /dev/null +++ b/docs/tests/inbox/README.md @@ -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 diff --git a/docs/tests/inbox/ROADMAP.md b/docs/tests/inbox/ROADMAP.md index 0b08498..b125c35 100644 --- a/docs/tests/inbox/ROADMAP.md +++ b/docs/tests/inbox/ROADMAP.md @@ -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 diff --git a/docs/tests/inbox/_shared/README.md b/docs/tests/inbox/_shared/README.md new file mode 100644 index 0000000..80ca409 --- /dev/null +++ b/docs/tests/inbox/_shared/README.md @@ -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. diff --git a/docs/tests/inbox/cancel/README.md b/docs/tests/inbox/cancel/README.md new file mode 100644 index 0000000..d2c00a2 --- /dev/null +++ b/docs/tests/inbox/cancel/README.md @@ -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` 不会为缺失线程隐式创建控制消息 diff --git a/docs/tests/inbox/claim/README.md b/docs/tests/inbox/claim/README.md new file mode 100644 index 0000000..a69f359 --- /dev/null +++ b/docs/tests/inbox/claim/README.md @@ -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` 存在 + +### 断言结论 + +- 请求的租约时长不是仅用于内部计算,也会被持久化到事件消息中 diff --git a/docs/tests/inbox/done/README.md b/docs/tests/inbox/done/README.md new file mode 100644 index 0000000..02b8f20 --- /dev/null +++ b/docs/tests/inbox/done/README.md @@ -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` 对终态线程是幂等失败,而不是重复成功 diff --git a/docs/tests/inbox/fail/README.md b/docs/tests/inbox/fail/README.md new file mode 100644 index 0000000..da013de --- /dev/null +++ b/docs/tests/inbox/fail/README.md @@ -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` 对终态线程不会重复成功 diff --git a/docs/tests/inbox/fetch/README.md b/docs/tests/inbox/fetch/README.md new file mode 100644 index 0000000..1f738de --- /dev/null +++ b/docs/tests/inbox/fetch/README.md @@ -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` + +### 断言结论 + +- 空结果不是成功空数组,而是显式的“无匹配工作”信号 diff --git a/docs/tests/inbox/init/README.md b/docs/tests/inbox/init/README.md new file mode 100644 index 0000000..526a4fb --- /dev/null +++ b/docs/tests/inbox/init/README.md @@ -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 的重复初始化不应引入额外迁移失败或状态漂移 diff --git a/docs/tests/inbox/list/README.md b/docs/tests/inbox/list/README.md new file mode 100644 index 0000000..aec2382 --- /dev/null +++ b/docs/tests/inbox/list/README.md @@ -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 是硬上限,不会返回超量结果 diff --git a/docs/tests/inbox/renew/README.md b/docs/tests/inbox/renew/README.md new file mode 100644 index 0000000..fea88bf --- /dev/null +++ b/docs/tests/inbox/renew/README.md @@ -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 diff --git a/docs/tests/inbox/reply/README.md b/docs/tests/inbox/reply/README.md new file mode 100644 index 0000000..20e5ad9 --- /dev/null +++ b/docs/tests/inbox/reply/README.md @@ -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 校验 diff --git a/docs/tests/inbox/send/README.md b/docs/tests/inbox/send/README.md new file mode 100644 index 0000000..9b8faff --- /dev/null +++ b/docs/tests/inbox/send/README.md @@ -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 文档统一说明 diff --git a/docs/tests/inbox/show/README.md b/docs/tests/inbox/show/README.md new file mode 100644 index 0000000..913f9ff --- /dev/null +++ b/docs/tests/inbox/show/README.md @@ -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` 或命令参数传入 diff --git a/docs/tests/inbox/update/README.md b/docs/tests/inbox/update/README.md new file mode 100644 index 0000000..e9bd049 --- /dev/null +++ b/docs/tests/inbox/update/README.md @@ -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 个 artifact,kind 为 `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 所属者 diff --git a/docs/tests/inbox/wait-reply/README.md b/docs/tests/inbox/wait-reply/README.md new file mode 100644 index 0000000..4c65a16 --- /dev/null +++ b/docs/tests/inbox/wait-reply/README.md @@ -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 diff --git a/docs/tests/inbox/watch/README.md b/docs/tests/inbox/watch/README.md new file mode 100644 index 0000000..170a7d7 --- /dev/null +++ b/docs/tests/inbox/watch/README.md @@ -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` 默认从“当前时刻之后”开始等待,不会回放既有事件 diff --git a/docs/tests/inbox/workflows/README.md b/docs/tests/inbox/workflows/README.md new file mode 100644 index 0000000..11faddd --- /dev/null +++ b/docs/tests/inbox/workflows/README.md @@ -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 来说,答复消费与未读清理是同一条用户契约链路