test(workflows): pin soft-deleted revision retention

Historical runs execute the exact signed revision persisted at run
creation; NIP-33 replacement soft-deletes the superseded kind-30620
event, so load_run_definition must read through soft deletion. Pin
that invariant: the test fails against get_event_by_id and passes
against get_event_by_id_including_deleted (mutation-verified on the
validation workstation).

Co-authored-by: Wakko <0999168a3b1a4dabdf398cf43ba2893bb29b74dc06d0b31c2d3b51dbebea1612@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
Logan Johnson
2026-08-25 09:38:04 -04:00
parent cb2261011c
commit 83be4ed5d3

View File

@@ -2193,4 +2193,89 @@ steps:
"channel owner's call_webhook workflow fires"
);
}
/// A run is bound to the exact signed revision persisted at creation.
/// Replacing the definition (NIP-33) soft-deletes the old kind-30620
/// event, but historical runs must keep loading their bound revision —
/// `load_run_definition` reads through soft deletion.
#[tokio::test]
#[ignore = "requires Postgres"]
async fn run_definition_loads_after_revision_soft_deleted() {
let db = setup_db().await;
let owner_keys = nostr::Keys::generate();
let owner = owner_keys.public_key().to_bytes().to_vec();
let member = nostr::Keys::generate().public_key().to_bytes().to_vec();
let (community, channel_id) = setup_channel(&db, &owner, &member).await;
let workflow_id = Uuid::new_v4();
let yaml = concat!(
"name: revision-history\n",
"trigger:\n on: message_posted\n",
"steps:\n - id: s1\n action: send_message\n text: hi\n",
);
let definition_event =
nostr::EventBuilder::new(nostr::Kind::Custom(KIND_WORKFLOW_DEF as u16), yaml)
.tags([
nostr::Tag::parse(["d", &workflow_id.to_string()]).expect("d tag"),
nostr::Tag::parse(["h", &channel_id.to_string()]).expect("h tag"),
])
.sign_with_keys(&owner_keys)
.expect("sign definition");
db.insert_event(community, &definition_event, Some(channel_id))
.await
.expect("store definition event");
let (def, _) = WorkflowEngine::parse_yaml(yaml).expect("parse yaml");
let def_json = serde_json::to_string(&def).expect("serialize definition");
let mut tx = db.begin_transaction().await.expect("begin tx");
db.upsert_workflow(
&mut tx,
community,
workflow_id,
Some(channel_id),
&owner,
"revision-history",
&def_json,
&[0u8; 32],
definition_event.id.as_bytes(),
)
.await
.expect("create workflow");
tx.commit().await.expect("commit workflow");
let run_id = db
.create_workflow_run(
community,
workflow_id,
definition_event.id.as_bytes(),
None,
None,
)
.await
.expect("create run");
let engine = Arc::new(WorkflowEngine::new(db.clone(), WorkflowConfig::default()));
// Bound revision loads while the event is live.
engine
.load_run_definition(community, run_id)
.await
.expect("load definition before deletion");
// Definition replacement soft-deletes the superseded revision event.
assert!(
db.soft_delete_event(community, definition_event.id.as_bytes())
.await
.expect("soft delete revision"),
"revision event must exist to be soft-deleted"
);
// The historical run still resolves its exact signed revision.
let (run, loaded) = engine
.load_run_definition(community, run_id)
.await
.expect("load definition after soft deletion");
assert_eq!(run.id, run_id);
assert_eq!(loaded.name, "revision-history");
}
}