feat(cli): per-app docs admin read (pic docs ls/get --app)

Fills the one per-app admin-read gap kv/files already covered. New
docs_api.rs mirrors kv_api (GET /apps/{id}/docs[/{collection}/{doc_id}],
AppDocsRead capability, DocsRepo::list/get) with the group_blobs_api docs
response shape so the CLI shares one deserialize. DocsCmd gains optional
--app/--group (mutually exclusive, like KvCmd) dispatched via
require_one_owner; new client docs_list/docs_get. Read-only by design,
matching kv/files. Pinned by a collections journey: a script writes the
app's own docs collection, the operator lists + fetches it with no script.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-15 20:53:11 +02:00
parent 8fc78cd07f
commit e4a58dc140
7 changed files with 366 additions and 38 deletions

View File

@@ -749,3 +749,79 @@ fn operator_reads_shared_docs_and_group_dead_letters_via_cli() {
.assert()
.success();
}
/// B4: the per-app docs admin read surface (`pic docs ls/get --app`), the
/// per-app counterpart to the group read above. A script writes to the app's
/// OWN docs collection (no group); the operator then lists + fetches it with
/// no script, mirroring `pic kv --app`.
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn operator_reads_per_app_docs_via_cli() {
let Some(fx) = common::fixture_or_skip() else {
return;
};
let env = common::admin_env(fx);
let app = common::unique_slug("appdocs");
let _a = AppGuard::new(&env.url, &env.token, &app);
common::pic_as(&env)
.args(["apps", "create", &app])
.assert()
.success();
// A script creates a document in the app's own docs collection.
let dir = manifest_dir();
fs::write(
dir.path().join("scripts/dw.rhai"),
r#"docs::collection("notes").create(#{ title: "per-app-hi" })"#,
)
.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 app's docs ids — no script.
let ls = String::from_utf8(
common::pic_as(&env)
.args(["docs", "ls", "--app", &app, "--collection", "notes"])
.output()
.unwrap()
.stdout,
)
.unwrap();
assert!(
ls.contains(&doc_id),
"docs ls --app 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",
"--app",
&app,
"--collection",
"notes",
&doc_id,
])
.output()
.unwrap()
.stdout,
)
.unwrap();
assert!(
get.contains("per-app-hi"),
"docs get --app must return the document data:\n{get}"
);
}