use axon_frontend::lexer::Lexer;
use axon_frontend::parser::Parser;
use axon_frontend::type_checker::TypeChecker;
fn errors(src: &str) -> Vec<String> {
let tokens = Lexer::new(src, "<test>").tokenize().expect("lex");
let prog = Parser::new(tokens).parse().expect("parse");
TypeChecker::new(&prog)
.check()
.iter()
.map(|e| e.message.clone())
.collect()
}
fn has_t927(errs: &[String]) -> bool {
errs.iter().any(|e| e.contains("axon-T927"))
}
#[test]
fn query_is_in_the_closed_method_catalog() {
let src = "flow Search() -> Unit { step S { ask: \"find\" } }\n\
axonendpoint E { method: QUERY path: \"/search\" execute: Search backend: stub }";
let errs = errors(src);
assert!(
!errs.iter().any(|e| e.contains("Unknown HTTP method")),
"QUERY (RFC 10008) must be a valid axonendpoint method. Got: {errs:?}"
);
}
#[test]
fn query_is_declarable_in_cors_allow_methods() {
let src = "cors Api { allow_origins: [\"https://app.example\"] allow_methods: [QUERY] }\n\
flow Search() -> Unit { step S { ask: \"find\" } }\n\
axonendpoint E { method: QUERY path: \"/search\" execute: Search backend: stub cors: Api }";
let errs = errors(src);
assert!(
!errs.iter().any(|e| e.contains("axon-T855")),
"QUERY must be declarable in cors allow_methods (T855 reuses the catalog). Got: {errs:?}"
);
}
#[test]
fn query_over_a_read_only_flow_compiles_clean() {
let src = "axonstore mem { backend: in_memory }\n\
flow Search() -> Unit {\n\
retrieve mem { where: \"kind = 'lead'\" as: hits }\n\
}\n\
axonendpoint E { method: QUERY path: \"/search\" execute: Search backend: stub }";
let errs = errors(src);
assert!(
!has_t927(&errs),
"a read-only QUERY flow must compile — T927 only refuses WRITES. Got: {errs:?}"
);
}
#[test]
fn t927_query_flow_that_persists_is_refused() {
let src = "axonstore mem { backend: in_memory }\n\
flow Search() -> Unit {\n\
retrieve mem { where: \"kind = 'lead'\" as: hits }\n\
persist into mem { kind: \"audit\" content: \"searched\" }\n\
}\n\
axonendpoint E { method: QUERY path: \"/search\" execute: Search backend: stub }";
let errs = errors(src);
assert!(has_t927(&errs), "a QUERY flow that persists must be refused: {errs:?}");
assert!(
errs.iter().any(|e| e.contains("persist")),
"the diagnostic must NAME the offending write verb: {errs:?}"
);
}
#[test]
fn t927_query_flow_that_emits_is_refused() {
let src = "channel Bus { message: Text }\n\
flow Search() -> Unit {\n\
step S { ask: \"find\" }\n\
emit Bus(S)\n\
}\n\
axonendpoint E { method: QUERY path: \"/search\" execute: Search backend: stub }";
let errs = errors(src);
assert!(has_t927(&errs), "a QUERY flow that emits must be refused: {errs:?}");
}
#[test]
fn t927_catches_a_write_nested_in_a_conditional() {
let src = "axonstore mem { backend: in_memory }\n\
flow Search(hot: Bool) -> Unit {\n\
retrieve mem { where: \"kind = 'lead'\" as: hits }\n\
if hot {\n\
persist into mem { kind: \"audit\" content: \"hot\" }\n\
}\n\
}\n\
axonendpoint E { method: QUERY path: \"/search\" execute: Search backend: stub }";
let errs = errors(src);
assert!(
has_t927(&errs),
"a write NESTED in an `if` must still be caught — otherwise the proof is worthless: {errs:?}"
);
}
#[test]
fn t927_query_in_a_program_declaring_a_deliver_is_refused() {
let src = "deliver PushLead { target: crm secret: k effects: <web>\n\
upsert_contact { key: lead_email email: lead_email }\n\
}\n\
flow Search() -> Unit { step S { ask: \"find\" } }\n\
axonendpoint E { method: QUERY path: \"/search\" execute: Search backend: stub }";
let errs = errors(src);
assert!(
has_t927(&errs),
"a QUERY endpoint cannot coexist with a `deliver` (it fires for every flow): {errs:?}"
);
}
#[test]
fn the_same_write_under_post_is_fine() {
let src = "axonstore mem { backend: in_memory }\n\
flow Save() -> Unit {\n\
persist into mem { kind: \"audit\" content: \"x\" }\n\
}\n\
axonendpoint E { method: POST path: \"/save\" execute: Save backend: stub }";
let errs = errors(src);
assert!(
!has_t927(&errs),
"T927 governs QUERY only — a POST is free to change state: {errs:?}"
);
}