feat(cli): add pic docs/dead-letters ls group read mirrors
Two read-only operator surfaces existed server-side but the CLI was app-only:
- `pic dead-letters ls --group <slug>` — mirrors the app listing onto a group's
shared-queue dead-letters (§11.6 D3, `GET /groups/{id}/dead-letters`), via the
existing `require_one_owner`/`OwnerRef` dispatch. List-only (show/replay/
resolve stay app-only, matching the server's operator surface); a group DL row
shows its `collection` and carries no trigger, so it renders its own columns.
- `pic docs ls/get --group <slug>` — a new `Docs` command (`cmds/docs.rs`)
wrapping `GET /groups/{id}/docs[/{collection}/{id}]`, mirroring `pic kv`.
Group-only: per-app docs have no admin read route yet (unlike kv/files), so
there is no `--app` variant — noted for a later pass.
Read-only by design (writes go through the SDK). New client methods +
`GroupDocsListDto`/`GroupDeadLetterDto`. Pinned by a new
`operator_reads_shared_docs_and_group_dead_letters_via_cli` journey
(152/152 pass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -646,3 +646,106 @@ fn operator_reads_shared_kv_via_admin_api() {
|
||||
"kv get --group must return the value:\n{get}"
|
||||
);
|
||||
}
|
||||
|
||||
/// §11.6: the read-only operator CLI mirrors for a group's shared DOCS
|
||||
/// collection (`pic docs ls/get --group`) and its shared-queue DEAD-LETTERS
|
||||
/// (`pic dead-letters ls --group`). A script creates a doc; the operator then
|
||||
/// lists + fetches it with no script, and the group dead-letters listing
|
||||
/// succeeds (empty — no failed shared-queue consumer here).
|
||||
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
|
||||
#[test]
|
||||
fn operator_reads_shared_docs_and_group_dead_letters_via_cli() {
|
||||
let Some(fx) = common::fixture_or_skip() else {
|
||||
return;
|
||||
};
|
||||
let env = common::admin_env(fx);
|
||||
let group = common::unique_slug("gblob-grp");
|
||||
let app = common::unique_slug("gblob-app");
|
||||
|
||||
let _g = GroupGuard::new(&env.url, &env.token, &group);
|
||||
common::pic_as(&env)
|
||||
.args(["groups", "create", &group])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// The group declares a docs-kind shared collection.
|
||||
let dir = manifest_dir();
|
||||
let gmanifest = format!(
|
||||
"[group]\nslug = \"{group}\"\nname = \"GBlob\"\n\n\
|
||||
collections = [{{ name = \"articles\", kind = \"docs\" }}]\n"
|
||||
);
|
||||
let gpath = dir.path().join("group.toml");
|
||||
fs::write(&gpath, &gmanifest).unwrap();
|
||||
common::pic_as(&env)
|
||||
.args(["apply", "--file"])
|
||||
.arg(&gpath)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let _a = AppGuard::new(&env.url, &env.token, &app);
|
||||
common::pic_as(&env)
|
||||
.args(["apps", "create", &app, "--group", &group])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// A script creates a document; `create` returns the new doc id.
|
||||
fs::write(
|
||||
dir.path().join("scripts/dw.rhai"),
|
||||
r#"docs::shared_collection("articles").create(#{ title: "hello-ops" })"#,
|
||||
)
|
||||
.unwrap();
|
||||
common::pic_as(&env)
|
||||
.args(["scripts", "deploy"])
|
||||
.arg(dir.path().join("scripts/dw.rhai"))
|
||||
.args(["--app", &app, "--name", "dw"])
|
||||
.assert()
|
||||
.success();
|
||||
let body = invoke_body(&env, &app_script_id(&env, &app, "dw"));
|
||||
let doc_id = body
|
||||
.as_str()
|
||||
.map(str::to_string)
|
||||
.or_else(|| body.get("id").and_then(|v| v.as_str().map(str::to_string)))
|
||||
.unwrap_or_else(|| panic!("create should return a doc id, got: {body}"));
|
||||
|
||||
// Operator lists the shared docs collection's ids — no script.
|
||||
let ls = String::from_utf8(
|
||||
common::pic_as(&env)
|
||||
.args(["docs", "ls", "--group", &group, "--collection", "articles"])
|
||||
.output()
|
||||
.unwrap()
|
||||
.stdout,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
ls.contains(&doc_id),
|
||||
"docs ls --group must list the new doc id `{doc_id}`:\n{ls}"
|
||||
);
|
||||
|
||||
// Operator fetches the document body.
|
||||
let get = String::from_utf8(
|
||||
common::pic_as(&env)
|
||||
.args([
|
||||
"docs",
|
||||
"get",
|
||||
"--group",
|
||||
&group,
|
||||
"--collection",
|
||||
"articles",
|
||||
&doc_id,
|
||||
])
|
||||
.output()
|
||||
.unwrap()
|
||||
.stdout,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
get.contains("hello-ops"),
|
||||
"docs get --group must return the document data:\n{get}"
|
||||
);
|
||||
|
||||
// The group dead-letters listing works (empty — no failed shared consumer).
|
||||
common::pic_as(&env)
|
||||
.args(["dead-letters", "ls", "--group", &group])
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user