mirror of
https://github.com/block/buzz.git
synced 2026-11-01 06:51:17 +00:00
## Summary - retain explicit regression coverage for the exact 128-channel relay request limit - cover the 129-channel split into 128 + 1 filters The workflow-listing implementation originally carried by this PR landed through #6009. This branch is now rebased onto current `main`, so the remaining diff is only the boundary test that #6009 did not include. Fixes #6116 ## Test plan - `cargo test --manifest-path desktop/src-tauri/Cargo.toml workflow_queries_respect_relay_explicit_channel_limit` - pre-push hook: Desktop checks, Desktop tests, Desktop Tauri checks, and path-scoped Rust tests Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
315 lines
9.6 KiB
Rust
315 lines
9.6 KiB
Rust
// Tests for commands/workflows.rs — split into a sibling file to keep
|
|
// workflows.rs focused. These exercise the pure helpers (no relay): event →
|
|
// wire conversion, YAML definition parsing, name derivation, and the
|
|
// create/update record shaping.
|
|
|
|
use super::*;
|
|
use nostr::{EventBuilder, Keys, Kind, Tag};
|
|
|
|
/// Build a signed kind:30620 workflow definition event with the given YAML
|
|
/// content and d/h tags.
|
|
fn wf_event(d: &str, h: &str, yaml: &str) -> nostr::Event {
|
|
let keys = Keys::generate();
|
|
let tags: Vec<Tag> = [vec!["d", d], vec!["h", h]]
|
|
.into_iter()
|
|
.map(|t| Tag::parse(t).expect("parse tag"))
|
|
.collect();
|
|
EventBuilder::new(Kind::Custom(30620), yaml)
|
|
.tags(tags)
|
|
.sign_with_keys(&keys)
|
|
.expect("sign")
|
|
}
|
|
|
|
const CHAN: &str = "11111111-1111-1111-1111-111111111111";
|
|
const WF: &str = "22222222-2222-2222-2222-222222222222";
|
|
|
|
const YAML: &str = "\
|
|
name: Greet on join
|
|
description: Says hi
|
|
enabled: true
|
|
trigger:
|
|
on: message_posted
|
|
filter: hello
|
|
steps:
|
|
- id: reply
|
|
action: post_message
|
|
";
|
|
|
|
#[test]
|
|
fn workflow_from_event_maps_all_fields() {
|
|
let ev = wf_event(WF, CHAN, YAML);
|
|
let wf = workflow_from_event(&ev);
|
|
|
|
assert_eq!(wf.id, WF);
|
|
assert_eq!(wf.revision, ev.id.to_hex());
|
|
assert_eq!(wf.channel_id.as_deref(), Some(CHAN));
|
|
assert_eq!(wf.owner_pubkey, ev.pubkey.to_hex());
|
|
assert_eq!(wf.name, "Greet on join");
|
|
assert_eq!(wf.status, "active");
|
|
assert_eq!(wf.created_at, ev.created_at.as_secs() as i64);
|
|
assert_eq!(wf.updated_at, ev.created_at.as_secs() as i64);
|
|
}
|
|
|
|
#[test]
|
|
fn definition_is_parsed_into_object_with_nested_fields() {
|
|
let ev = wf_event(WF, CHAN, YAML);
|
|
let wf = workflow_from_event(&ev);
|
|
|
|
// The whole YAML document is preserved as a free-form object.
|
|
let def = wf.definition.as_object().expect("definition is an object");
|
|
assert_eq!(
|
|
def.get("description").and_then(Value::as_str),
|
|
Some("Says hi")
|
|
);
|
|
assert_eq!(def.get("enabled").and_then(Value::as_bool), Some(true));
|
|
assert_eq!(
|
|
wf.definition.pointer("/trigger/on").and_then(Value::as_str),
|
|
Some("message_posted")
|
|
);
|
|
assert_eq!(
|
|
wf.definition
|
|
.pointer("/steps/0/action")
|
|
.and_then(Value::as_str),
|
|
Some("post_message")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn name_falls_back_to_id_when_missing() {
|
|
let yaml = "trigger:\n on: schedule\n cron: '* * * * *'\n";
|
|
let ev = wf_event(WF, CHAN, yaml);
|
|
let wf = workflow_from_event(&ev);
|
|
assert_eq!(wf.name, WF);
|
|
}
|
|
|
|
#[test]
|
|
fn name_falls_back_to_id_when_blank() {
|
|
let yaml = "name: ' '\ntrigger:\n on: schedule\n";
|
|
let ev = wf_event(WF, CHAN, yaml);
|
|
let wf = workflow_from_event(&ev);
|
|
assert_eq!(wf.name, WF);
|
|
}
|
|
|
|
#[test]
|
|
fn malformed_yaml_yields_empty_object_not_error() {
|
|
// A broken workflow must not break the whole list — definition falls back
|
|
// to an empty object and the name falls back to the id. (YAML is permissive,
|
|
// so this uses an unterminated flow mapping that genuinely fails to parse.)
|
|
let ev = wf_event(WF, CHAN, "{ name: oops, unterminated: [1, 2");
|
|
let wf = workflow_from_event(&ev);
|
|
assert_eq!(wf.definition, Value::Object(serde_json::Map::new()));
|
|
assert_eq!(wf.name, WF);
|
|
}
|
|
|
|
#[test]
|
|
fn scalar_yaml_document_yields_empty_object() {
|
|
// A bare scalar parses as valid YAML but isn't an object; treat as empty.
|
|
let ev = wf_event(WF, CHAN, "just a string");
|
|
let wf = workflow_from_event(&ev);
|
|
assert_eq!(wf.definition, Value::Object(serde_json::Map::new()));
|
|
}
|
|
|
|
#[test]
|
|
fn tag_value_reads_d_and_h_and_misses_absent() {
|
|
let ev = wf_event(WF, CHAN, YAML);
|
|
assert_eq!(tag_value(&ev, "d").as_deref(), Some(WF));
|
|
assert_eq!(tag_value(&ev, "h").as_deref(), Some(CHAN));
|
|
assert_eq!(tag_value(&ev, "z"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn workflow_record_shapes_save_inputs() {
|
|
let wf = workflow_record(
|
|
WF.to_string(),
|
|
"revision-1".to_string(),
|
|
Some(CHAN.to_string()),
|
|
"deadbeef".to_string(),
|
|
YAML,
|
|
100,
|
|
200,
|
|
);
|
|
assert_eq!(wf.id, WF);
|
|
assert_eq!(wf.name, "Greet on join");
|
|
assert_eq!(wf.owner_pubkey, "deadbeef");
|
|
assert_eq!(wf.channel_id.as_deref(), Some(CHAN));
|
|
assert_eq!(wf.created_at, 100);
|
|
assert_eq!(wf.updated_at, 200);
|
|
assert_eq!(wf.status, "active");
|
|
}
|
|
|
|
#[test]
|
|
fn save_wire_serializes_flat_with_optional_secret() {
|
|
let workflow = workflow_record(
|
|
WF.to_string(),
|
|
"revision-1".to_string(),
|
|
Some(CHAN.to_string()),
|
|
"deadbeef".to_string(),
|
|
YAML,
|
|
1,
|
|
1,
|
|
);
|
|
|
|
// With a secret: present, flattened alongside the workflow fields.
|
|
let with = WorkflowSaveWire {
|
|
workflow: workflow.clone(),
|
|
webhook_secret: Some("s3cr3t".to_string()),
|
|
};
|
|
let v = serde_json::to_value(&with).expect("serialize");
|
|
assert_eq!(v.get("id").and_then(Value::as_str), Some(WF));
|
|
assert_eq!(v.get("name").and_then(Value::as_str), Some("Greet on join"));
|
|
assert_eq!(
|
|
v.get("webhook_secret").and_then(Value::as_str),
|
|
Some("s3cr3t")
|
|
);
|
|
|
|
// Without a secret: the key is omitted entirely (frontend treats as null).
|
|
let without = WorkflowSaveWire {
|
|
workflow,
|
|
webhook_secret: None,
|
|
};
|
|
let v = serde_json::to_value(&without).expect("serialize");
|
|
assert!(v.get("webhook_secret").is_none());
|
|
assert_eq!(v.get("id").and_then(Value::as_str), Some(WF));
|
|
}
|
|
|
|
#[test]
|
|
fn workflow_wire_serializes_with_snake_case_keys() {
|
|
// Guard the wire contract the frontend's RawWorkflow depends on.
|
|
let ev = wf_event(WF, CHAN, YAML);
|
|
let v = serde_json::to_value(workflow_from_event(&ev)).expect("serialize");
|
|
for key in [
|
|
"id",
|
|
"revision",
|
|
"name",
|
|
"owner_pubkey",
|
|
"channel_id",
|
|
"definition",
|
|
"status",
|
|
"created_at",
|
|
"updated_at",
|
|
] {
|
|
assert!(v.get(key).is_some(), "missing wire key: {key}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn multi_channel_workflow_query_uses_one_filter_per_channel() {
|
|
let other_channel = "33333333-3333-3333-3333-333333333333";
|
|
let filters = channel_workflow_filters(vec![CHAN.to_string(), other_channel.to_string()])
|
|
.expect("valid channels");
|
|
|
|
assert_eq!(filters.len(), 2);
|
|
assert_eq!(
|
|
filters[0],
|
|
serde_json::json!({
|
|
"kinds": [30620],
|
|
"#h": [CHAN],
|
|
})
|
|
);
|
|
assert_eq!(
|
|
filters[1],
|
|
serde_json::json!({
|
|
"kinds": [30620],
|
|
"#h": [other_channel],
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn workflow_queries_respect_relay_explicit_channel_limit() {
|
|
for (channel_count, expected_batch_sizes) in [
|
|
(WORKFLOW_QUERY_CHANNEL_BATCH_SIZE, vec![128]),
|
|
(WORKFLOW_QUERY_CHANNEL_BATCH_SIZE + 1, vec![128, 1]),
|
|
] {
|
|
let channel_ids = (0..channel_count)
|
|
.map(|index| uuid::Uuid::from_u128(index as u128 + 1).to_string())
|
|
.collect();
|
|
let batches = channel_workflow_filter_batches(channel_ids).expect("valid channels");
|
|
|
|
assert_eq!(
|
|
batches.iter().map(Vec::len).collect::<Vec<_>>(),
|
|
expected_batch_sizes
|
|
);
|
|
assert!(batches.iter().flatten().all(|filter| filter["#h"]
|
|
.as_array()
|
|
.is_some_and(|values| values.len() == 1)));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn workflow_query_results_are_deduplicated_by_event_id() {
|
|
let first = wf_event(WF, CHAN, YAML);
|
|
let second_workflow = "33333333-3333-3333-3333-333333333333";
|
|
let second = wf_event(second_workflow, CHAN, YAML);
|
|
let mut workflows = Vec::new();
|
|
let mut seen_event_ids = HashSet::new();
|
|
|
|
append_unique_workflows(
|
|
&mut workflows,
|
|
&mut seen_event_ids,
|
|
&[first.clone(), second.clone()],
|
|
);
|
|
append_unique_workflows(&mut workflows, &mut seen_event_ids, &[first, second]);
|
|
|
|
assert_eq!(workflows.len(), 2);
|
|
assert_eq!(workflows[0].id, WF);
|
|
assert_eq!(workflows[1].id, second_workflow);
|
|
}
|
|
|
|
#[test]
|
|
fn channel_workflow_filters_reject_malformed_or_blank_channel_ids() {
|
|
for channel_id in ["not-a-uuid", "", " "] {
|
|
let error = channel_workflow_filters(vec![channel_id.to_string()])
|
|
.expect_err("malformed channel id must fail before querying the relay");
|
|
assert_eq!(error, "invalid channel id");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn channel_workflow_filters_accepts_empty_input() {
|
|
assert_eq!(
|
|
channel_workflow_filters(Vec::new()).expect("empty input is valid"),
|
|
Vec::<Value>::new()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn trigger_response_uses_persisted_run_id_contract() {
|
|
let wire = trigger_wire_from_message(
|
|
WF.to_string(),
|
|
"response:{\"run_id\":\"33333333-3333-3333-3333-333333333333\"}",
|
|
)
|
|
.expect("parse trigger response");
|
|
|
|
assert_eq!(wire.run_id, "33333333-3333-3333-3333-333333333333");
|
|
assert_eq!(wire.workflow_id, WF);
|
|
assert_eq!(wire.status, "pending");
|
|
let value = serde_json::to_value(wire).expect("serialize trigger response");
|
|
assert!(value.get("event_id").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn trigger_response_rejects_missing_or_empty_run_id() {
|
|
assert!(trigger_wire_from_message(WF.to_string(), "response:{}").is_err());
|
|
assert!(trigger_wire_from_message(WF.to_string(), "response:{\"run_id\":\" \"}",).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn run_reads_serialize_to_backend_envelopes() {
|
|
let runs = WorkflowRunsWire {
|
|
runs: Vec::new(),
|
|
next: None,
|
|
};
|
|
let approvals = WorkflowApprovalsWire {
|
|
approvals: Vec::new(),
|
|
};
|
|
assert_eq!(
|
|
serde_json::to_value(runs).expect("serialize runs"),
|
|
serde_json::json!({ "runs": [], "next": null })
|
|
);
|
|
assert_eq!(
|
|
serde_json::to_value(approvals).expect("serialize approvals"),
|
|
serde_json::json!({ "approvals": [] })
|
|
);
|
|
}
|