test: pin a teammate's carried draft; name the kind that refuses a draft move

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-09-14 21:55:24 +02:00
co-authored by Claude Opus 5
parent 68cd7a4420
commit 733ef9c3b8
5 changed files with 62 additions and 13 deletions
@@ -1,12 +1,12 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n EXISTS(SELECT 1 FROM draft WHERE workspace_id = $1 AND path = $3\n AND typ::text = ANY($6::text[]) AND email = $4) as \"at_target!\",\n EXISTS(SELECT 1 FROM draft WHERE workspace_id = $1 AND path = $5\n AND typ = $2 AND email = $4\n AND position(chr(92) || 'u0000' in replace(value::text, chr(92) || chr(92), '')) > 0\n ) as \"poisoned!\" ",
"query": "SELECT\n (SELECT typ::text FROM draft WHERE workspace_id = $1 AND path = $3\n AND typ::text = ANY($6::text[]) AND email = $4 LIMIT 1) as \"at_target\",\n EXISTS(SELECT 1 FROM draft WHERE workspace_id = $1 AND path = $5\n AND typ = $2 AND email = $4\n AND position(chr(92) || 'u0000' in replace(value::text, chr(92) || chr(92), '')) > 0\n ) as \"poisoned!\" ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "at_target!",
"type_info": "Bool"
"name": "at_target",
"type_info": "Text"
},
{
"ordinal": 1,
@@ -63,5 +63,5 @@
null
]
},
"hash": "9b574bf93759d822066ac1629744a8df58840c463d8aa412871bd9774d978bae"
"hash": "ba312a2cade7e7e3bca1788e151e23cf3a3b7cea9a676b1cedc2ae36290aab40"
}
+15 -5
View File
@@ -94,7 +94,10 @@ async fn test_draft_move_refuses_the_other_app_kind(db: Pool<Postgres>) -> anyho
let status = resp.status();
let body = resp.text().await?;
assert_eq!(status, 400, "move onto a classic app draft was allowed: {body}");
assert!(body.contains("already have a draft"), "unexpected refusal: {body}");
assert!(
body.contains("already have a app draft"),
"the refusal did not name the occupying kind: {body}"
);
// Both drafts are untouched.
let list: Vec<Value> = reqwest::Client::new()
@@ -106,12 +109,19 @@ async fn test_draft_move_refuses_the_other_app_kind(db: Pool<Postgres>) -> anyho
.await?
.json()
.await?;
let mut kinds = list
let mut at = list
.iter()
.filter(|d| matches!(d["kind"].as_str(), Some("app") | Some("raw_app")))
.filter_map(|d| d["kind"].as_str())
.filter_map(|d| Some((d["kind"].as_str()?, d["path"].as_str()?)))
.collect::<Vec<_>>();
kinds.sort();
assert_eq!(kinds, vec!["app", "raw_app"], "{list:?}");
at.sort();
assert_eq!(
at,
vec![
("app", "u/test-user/mvtaken_app"),
("raw_app", "u/test-user/mvtaken_raw")
],
"{list:?}"
);
Ok(())
}
+30
View File
@@ -239,3 +239,33 @@ async fn test_move_back_ends_the_record(db: Pool<Postgres>) -> anyhow::Result<()
assert_eq!(own_draft_paths(port).await?, vec!["u/test-user/follow_a"]);
Ok(())
}
/// A rename carries a teammate's row too: both its path keys follow, and the version
/// it forked from does not move. A restamp there would clear their out-of-date prompt
/// and let them deploy over the mover's version believing they were current.
#[sqlx::test(fixtures("base", "drafts_save_follows_move"))]
async fn test_a_teammates_draft_follows_with_its_base(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
rename(port, HEAD_HASH, "u/test-user/follow_b").await?;
// Read from the pool: the teammate's row is another user's, and this asserts on
// `base`, which no endpoint exposes for someone else's draft.
let row: (String, String, Option<String>) = sqlx::query_as(
"SELECT value::jsonb ->> 'path', value::jsonb ->> 'draft_path', base
FROM draft WHERE workspace_id = 'test-workspace' AND typ = 'script'
AND email = 'test2@windmill.dev'",
)
.fetch_one(&db)
.await?;
assert_eq!(row.0, "u/test-user/follow_b", "typed path did not follow");
assert_eq!(row.1, "u/test-user/follow_b", "mirror did not follow");
assert_eq!(
row.2.as_deref(),
Some(HEAD_HASH),
"the teammate's base was restamped by someone else's rename"
);
Ok(())
}
+7
View File
@@ -20,3 +20,10 @@ INSERT INTO draft (workspace_id, path, typ, value, email)
VALUES ('test-workspace', 'u/test-user/draft_store', 'script',
'{"path": "u/test-user/friendly", "draft_path": "u/test-user/friendly", "summary": "D", "content": "draft"}',
'test@windmill.dev');
-- A teammate's draft on the same deployed script, forked from the same head. The
-- rename must carry it too, without touching the version it forked from.
INSERT INTO draft (workspace_id, path, typ, value, email, base)
VALUES ('test-workspace', 'u/test-user/follow_a', 'script',
'{"path": "u/test-user/follow_a", "draft_path": "u/test-user/follow_a", "parent_hash": "0000000000001b76", "summary": "A", "content": "teammate draft"}',
'test2@windmill.dev', '0000000000001b76');
+6 -4
View File
@@ -757,8 +757,8 @@ async fn move_draft(
if moved.is_none() {
let row = sqlx::query!(
r#"SELECT
EXISTS(SELECT 1 FROM draft WHERE workspace_id = $1 AND path = $3
AND typ::text = ANY($6::text[]) AND email = $4) as "at_target!",
(SELECT typ::text FROM draft WHERE workspace_id = $1 AND path = $3
AND typ::text = ANY($6::text[]) AND email = $4 LIMIT 1) as "at_target",
EXISTS(SELECT 1 FROM draft WHERE workspace_id = $1 AND path = $5
AND typ = $2 AND email = $4
AND position(chr(92) || 'u0000' in replace(value::text, chr(92) || chr(92), '')) > 0
@@ -780,8 +780,10 @@ async fn move_draft(
"'{path}' contains a NUL character and predates the sanitizer, so it cannot be \
{attempted}. Reopen it, re-save to rewrite it cleanly, then retry."
)
} else if row.at_target && new_path != path {
format!("You already have a draft at '{new_path}'")
} else if let Some(occupant) = row.at_target.filter(|_| new_path != path) {
// Naming the kind matters for the app pair: a classic-app draft refusing a
// raw-app move is invisible in the raw-app list the caller is looking at.
format!("You already have a {} draft at '{new_path}'", occupant.replace('_', " "))
} else {
format!("You have no draft at '{path}'")
}));