test(cli): shared-files journey + .gitignore + docs (§11.6 files C5)

- collections.rs: shared_files_collection_is_read_write_across_the_subtree
  — a group declares kv+docs+files in one string-or-table manifest; authed
  app A creates a blob via files::shared_collection("assets").create(...),
  app B lists + gets the SAME bytes back across the subtree, a
  sibling-subtree app gets CollectionNotShared, collections ls shows all
  three kinds. Live-verified the bytes land under
  files/groups/<group_id>/assets/<id[0:2]>/<id>.
- .gitignore: ignore /crates/picloud-cli/data (the CLI journey harness
  spawns the server from that dir; the files journey is the first CLI test
  to write blobs).
- docs: design §11.6 (KV+DOCS+FILES shipped; topics/queue still deferred),
  sdk-shape (files::shared_collection), CLAUDE.md current-focus.

Full journey suite 117/117; schema blessed (55 migrations); workspace
clippy -D clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-30 19:56:47 +02:00
parent 17221a2683
commit 9c6d690792
5 changed files with 179 additions and 30 deletions

View File

@@ -394,6 +394,141 @@ fn shared_docs_collection_is_read_write_across_the_subtree() {
);
}
/// §11.6 files slice: a group declares a `files`-kind shared collection (mixed
/// with kv + docs in one string-or-table manifest); an authenticated app A
/// `create`s a blob and app B `list`s + `get`s the SAME bytes back across the
/// subtree (the group-keyed disk path under `files/groups/<group_id>/...`); a
/// sibling-subtree app gets `CollectionNotShared`; `collections ls` shows all
/// three kinds.
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn shared_files_collection_is_read_write_across_the_subtree() {
let Some(fx) = common::fixture_or_skip() else {
return;
};
let env = common::admin_env(fx);
let group = common::unique_slug("fcoll-grp");
let sibling = common::unique_slug("fcoll-sib");
let app_a = common::unique_slug("fcoll-a");
let app_b = common::unique_slug("fcoll-b");
let foreign = common::unique_slug("fcoll-f");
let _g = GroupGuard::new(&env.url, &env.token, &group);
let _gs = GroupGuard::new(&env.url, &env.token, &sibling);
common::pic_as(&env)
.args(["groups", "create", &group])
.assert()
.success();
common::pic_as(&env)
.args(["groups", "create", &sibling])
.assert()
.success();
// The group declares kv + docs + files collections in one manifest.
let dir = manifest_dir();
let gmanifest = format!(
"[group]\nslug = \"{group}\"\nname = \"FColl\"\n\n\
collections = [\"catalog\", {{ name = \"articles\", kind = \"docs\" }}, \
{{ name = \"assets\", kind = \"files\" }}]\n"
);
let gpath = dir.path().join("group.toml");
fs::write(&gpath, &gmanifest).unwrap();
common::pic_as(&env)
.args(["apply", "--file"])
.arg(&gpath)
.assert()
.success();
// `collections ls` shows all three kinds.
let ls = String::from_utf8(
common::pic_as(&env)
.args(["collections", "ls", "--group", &group])
.output()
.unwrap()
.stdout,
)
.unwrap();
assert!(
ls.contains("assets") && ls.contains("files"),
"ls should list the files collection:\n{ls}"
);
// Re-apply is a no-op (all three markers already exist).
common::pic_as(&env)
.args(["apply", "--file"])
.arg(&gpath)
.assert()
.success();
let _a = AppGuard::new(&env.url, &env.token, &app_a);
let _b = AppGuard::new(&env.url, &env.token, &app_b);
let _f = AppGuard::new(&env.url, &env.token, &foreign);
for (app, grp) in [(&app_a, &group), (&app_b, &group), (&foreign, &sibling)] {
common::pic_as(&env)
.args(["apps", "create", app, "--group", grp])
.assert()
.success();
}
// App A creates a blob in the shared files collection (authenticated).
// base64("hi") = "aGk=".
fs::write(
dir.path().join("scripts/fwriter.rhai"),
r#"files::shared_collection("assets").create(#{ name: "a.txt", content_type: "text/plain", data: base64::decode("aGk=") })"#,
)
.unwrap();
common::pic_as(&env)
.args(["scripts", "deploy"])
.arg(dir.path().join("scripts/fwriter.rhai"))
.args(["--app", &app_a, "--name", "fwriter"])
.assert()
.success();
common::pic_as(&env)
.args(["scripts", "invoke", &app_script_id(&env, &app_a, "fwriter")])
.assert()
.success();
// App B lists the shared collection and reads the SAME bytes back —
// cross-app blob sharing via the group-keyed disk path.
fs::write(
dir.path().join("scripts/freader.rhai"),
r#"let c = files::shared_collection("assets"); let p = c.list(); base64::encode(c.get(p.files[0].id))"#,
)
.unwrap();
common::pic_as(&env)
.args(["scripts", "deploy"])
.arg(dir.path().join("scripts/freader.rhai"))
.args(["--app", &app_b, "--name", "freader"])
.assert()
.success();
assert_eq!(
invoke_body(&env, &app_script_id(&env, &app_b, "freader")),
serde_json::json!("aGk="),
"app B reads the blob app A wrote to the shared files collection"
);
// The foreign app (sibling subtree) → CollectionNotShared.
fs::write(
dir.path().join("scripts/fpeek.rhai"),
r#"files::shared_collection("assets").list()"#,
)
.unwrap();
common::pic_as(&env)
.args(["scripts", "deploy"])
.arg(dir.path().join("scripts/fpeek.rhai"))
.args(["--app", &foreign, "--name", "fpeek"])
.assert()
.success();
let out = common::pic_as(&env)
.args(["scripts", "invoke", &app_script_id(&env, &foreign, "fpeek")])
.output()
.expect("invoke");
assert!(
!out.status.success(),
"a foreign app must not resolve another subtree's shared files collection"
);
}
/// Id of the named script in an app (`pic scripts ls --app <a>`).
fn app_script_id(env: &common::TestEnv, app: &str, name: &str) -> String {
let ls = common::pic_as(env)