Files
buzz/desktop/src-tauri/src/commands/workflows_tests.rs
Wes f8692fa9b5 test(desktop): cover exact workflow batch limit (#6168)
## 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>
2026-08-17 16:47:07 -07:00

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": [] })
);
}