Files
windmill/backend/tests/ai_shared_sessions.rs
T
Ruben FiszelandClaude Opus 5.5 a9aa2b8229 feat: share AI sessions read-only by link, optionally public (#11799)
* feat: share AI sessions read-only by link, optionally public

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: render shared session snapshots from an allowlist, without mermaid or remote images

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style: make the session share button icon-only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: safe reasoning links, artifact list and full-load exits on shared sessions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 18:51:11 +02:00

121 lines
4.0 KiB
Rust

//! Shared AI sessions: a private share is served only to members of its workspace, a public
//! one to anyone holding the link, and only its author decides which it is.
use serde_json::{json, Value};
use sqlx::{Pool, Postgres};
use windmill_test_utils::*;
const ADMIN: &str = "Bearer SECRET_TOKEN";
const MEMBER: &str = "Bearer SECRET_TOKEN_2";
const OTHER_MEMBER: &str = "Bearer SECRET_TOKEN_3";
const OUTSIDER: &str = "Bearer OUTSIDER_TOKEN";
#[sqlx::test(fixtures("base"))]
async fn private_share_is_for_members_and_public_share_for_anyone(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
initialize_tracing().await;
sqlx::raw_sql(
"INSERT INTO password(email, password_hash, login_type, super_admin, verified, name)
VALUES ('outsider@windmill.dev', 'not-a-real-hash', 'password', false, true, 'Outsider');
INSERT INTO token(token_hash, token_prefix, token, email, label, super_admin)
VALUES (encode(sha256('OUTSIDER_TOKEN'::bytea), 'hex'), 'OUTSIDER_T', 'OUTSIDER_TOKEN',
'outsider@windmill.dev', 'outsider token', false);",
)
.execute(&db)
.await?;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let authed = format!("http://localhost:{port}/api/w/test-workspace/ai/shared_sessions");
let get = |id: &str| {
format!("http://localhost:{port}/api/w/test-workspace/ai_u/shared_sessions/get/{id}")
};
let client = reqwest::Client::new();
let resp = client
.post(format!("{authed}/share"))
.header("Authorization", MEMBER)
.json(&json!({
"session_id": "session-1",
"name": "Debugging",
"message_count": 1,
"snapshot": { "messages": [{ "role": "user", "content": "hi" }], "artifacts": [] },
}))
.send()
.await?;
assert_eq!(resp.status(), 200, "{}", resp.text().await?);
let share: Value = resp.json().await?;
assert_eq!(share["is_public"], false, "a new share must start private");
let id = share["id"].as_str().unwrap().to_string();
let status = |token: Option<&'static str>| {
let mut req = client.get(get(&id));
if let Some(token) = token {
req = req.header("Authorization", token);
}
async move { anyhow::Ok(req.send().await?.status().as_u16()) }
};
assert_eq!(status(Some(OTHER_MEMBER)).await?, 200);
assert_eq!(
status(None).await?,
401,
"a private share was served anonymously"
);
assert_eq!(
status(Some(OUTSIDER)).await?,
401,
"a private share was served to a non-member"
);
let resp = client
.post(format!("{authed}/set_public/{id}"))
.header("Authorization", OTHER_MEMBER)
.json(&json!({ "is_public": true }))
.send()
.await?;
assert_eq!(
resp.status(),
404,
"a member who is not the author made the share public"
);
let resp = client
.post(format!("{authed}/set_public/{id}"))
.header("Authorization", MEMBER)
.json(&json!({ "is_public": true }))
.send()
.await?;
assert_eq!(resp.status(), 200, "{}", resp.text().await?);
let resp = client.get(get(&id)).send().await?;
assert_eq!(
resp.status(),
200,
"a public share was not served anonymously"
);
let body: Value = resp.json().await?;
assert_eq!(body["snapshot"]["messages"][0]["content"], "hi");
assert_eq!(body["can_unshare"], false);
let delete = |token: &'static str| {
client
.delete(format!("{authed}/delete/{id}"))
.header("Authorization", token)
.send()
};
assert_eq!(
delete(OTHER_MEMBER).await?.status(),
404,
"a member who is neither the author nor an admin took the share down"
);
assert_eq!(
delete(ADMIN).await?.status(),
200,
"an admin could not take the share down"
);
assert_eq!(status(Some(OTHER_MEMBER)).await?, 404);
Ok(())
}