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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user