qqq-cli 0.6.0

Local-first task queue for coding agent sessions
#[allow(dead_code)]
#[path = "../src/errors.rs"]
mod errors;
#[allow(dead_code)]
#[path = "../src/sql_filter/mod.rs"]
mod sql_filter;
#[allow(dead_code)]
#[path = "../src/tags.rs"]
mod tags;

use rusqlite::{Connection, params_from_iter, types::Value};

fn evaluate(source: &str) -> bool {
    let filter = sql_filter::compile(source).unwrap();
    let db = Connection::open_in_memory().unwrap();
    db.execute_batch("CREATE TABLE tasks(id INTEGER,description TEXT,status TEXT,priority INTEGER,archived INTEGER,parent_id INTEGER,created_at TEXT,updated_at TEXT,harness_name TEXT,harness_session TEXT,orchestrator_name TEXT,orchestrator_session TEXT,tags TEXT); INSERT INTO tasks VALUES(1,'Fix auth','new',3,0,NULL,'2026-10-02T00:00:00Z','2026-10-02T00:00:00Z',NULL,NULL,NULL,NULL,'[\"frontend\",\"界 面\"]')").unwrap();
    db.query_row(
        &format!("SELECT {} FROM tasks", filter.sql()),
        params_from_iter(filter.params()),
        |row| row.get(0),
    )
    .unwrap()
}

#[test]
fn has_tag_matches_exact_normalized_literals() {
    for expression in [
        "has_tag('frontend')",
        "has_tag(' frontend ')",
        "has_tag('界 面') and priority == 3 and status == 'new'",
        "has_tag(('frontend'))",
        "not has_tag('Frontend')",
        "not has_tag('front')",
        "not has_tag('auth')",
        "has_tag('absent') or has_tag('frontend')",
        "(if has_tag('frontend') then priority else 0) == 3",
    ] {
        assert!(evaluate(expression), "{expression}");
    }
    let filter = sql_filter::compile("has_tag(\"' OR 1=1 --\")").unwrap();
    assert!(!filter.sql().contains("OR 1=1"));
    assert_eq!(filter.params(), &[Value::Text("' OR 1=1 --".into())]);
}

#[test]
fn has_tag_rejects_nonliteral_or_invalid_labels() {
    for expression in [
        "has_tag()",
        "has_tag('a','b')",
        "has_tag(nil)",
        "has_tag(1)",
        "has_tag(description)",
        "has_tag(lower('UI'))",
        "has_tag('UI' .. ' review')",
        "has_tag('')",
        "has_tag(' ')",
        "has_tag('a,b')",
        "has_tag('[ui]')",
        r"has_tag('a\nb')",
        "has_tag('frontend') + 1",
    ] {
        assert!(sql_filter::compile(expression).is_err(), "{expression}");
    }
}

#[test]
fn literals_are_bound() {
    let filter = sql_filter::compile("description == \"' OR 1=1 --\"").unwrap();
    assert!(!filter.sql().contains("OR 1=1"));
    assert_eq!(filter.params(), &[Value::Text("' OR 1=1 --".into())]);
    assert!(!evaluate("description == \"' OR 1=1 --\""));
}

#[test]
fn luau_precedence_nil_and_truthiness() {
    for expression in [
        "priority + 2 * 3 == 9",
        "parent_id == nil",
        "not parent_id",
        "not false and not nil",
        "(nil and true) == nil",
        "(nil or false) == false",
        "(nullif(archived, false) and true) == nil",
        "parent_id == harness_name",
        "not not 0",
        "not not ''",
        "(0 or 1) == 0",
        "('' or 'fallback') == ''",
        "(parent_id or 0) == 0",
        "(if archived then 0 else priority) == 3",
        "(if priority < 0 then 1 elseif priority == 3 then 2 else 3) == 2",
        "#'é' == 2",
        "priority / 2 == 1.5",
        "'Fix' .. ' auth' == description",
    ] {
        assert!(evaluate(expression), "{expression}");
    }
    for expression in [
        "false == 0",
        "nil ~= parent_id",
        "parent_id > 0",
        "not 0",
        "not ''",
        "false and true",
    ] {
        assert!(!evaluate(expression), "{expression}");
    }
}

#[test]
fn literals_decode_luau_strings_and_numbers() {
    for expression in [
        r#"'a\n\r\t\a\b\f\v' == '\097\010\013\009\007\008\012\011'"#,
        r#"'\x46ix\z   auth' == 'Fixauth'"#,
        r#"'\u{e9}' == 'é'"#,
        "[=[\nFix auth]=] == description",
        "'Fix\\\nauth' == 'Fix\\n' .. 'auth'",
        "0xA == 10 and 0b1_1 == 3 and 1_000 == 1000",
        "1e-1 == 0.1",
        "-- comment\n priority == 3 -- trailing comment",
    ] {
        assert!(evaluate(expression), "{expression}");
    }
    for expression in [
        r#"'\255' == description"#,
        "1e999 == 1",
        "9007199254740993 == id",
    ] {
        assert!(sql_filter::compile(expression).is_err(), "{expression}");
    }
}

#[test]
fn string_escape_edges_follow_luau_lexer() {
    for expression in [
        "[=[a\rb]=] == 'a\\rb'",
        "[=[\ra]=] == '\\ra'",
        "[=[\r\na\r\nb]=] == 'a\\nb'",
        r#"'\/' == '/' and '\q' == 'q'"#,
    ] {
        assert!(evaluate(expression), "{expression}");
    }
}

#[test]
fn rounded_integer_spellings_cannot_bypass_literal_bound() {
    for expression in [
        "9007199254740993e0 == id",
        "9007199254740993.0 == id",
        "9007199254740992.1 == id",
        "9.007199254740993e15 == id",
        "0x20000000000001p0 == id",
    ] {
        assert!(sql_filter::compile(expression).is_err(), "{expression}");
    }
    for expression in [
        "9007199254740992.0 == 9007199254740992",
        "9.007199254740992e15 == 9007199254740992",
        "0x20000000000000 == 9007199254740992",
    ] {
        assert!(evaluate(expression), "{expression}");
    }
}

#[test]
fn sqlite_functions_and_aliases_work() {
    for expression in [
        "like(task_name, '%auth%')",
        "like('a_b', 'a!_b', '!')",
        "glob(description, '*auth')",
        "lower('AUTH') == 'auth' and upper('auth') == 'AUTH'",
        "length('é') == 1 and substr(description, 5, 4) == 'auth'",
        "trim(' x ') == 'x' and ltrim('xxa', 'x') == 'a' and rtrim('axx', 'x') == 'a'",
        "replace(description, 'auth', 'DB') == 'Fix DB' and instr(description, 'auth') == 5",
        "abs(-3) == 3 and round(1.234, 2) == 1.23",
        "coalesce(parent_id, priority, 0) == 3 and nullif(priority, 3) == nil",
        "min(priority, 5) == 3 and max('a', 'b') == 'b'",
        "date(created_time) == '2026-10-02' and time(updated_time) == '00:00:00'",
        "datetime(created_at, '+1 day') == '2026-10-03 00:00:00'",
        "strftime('%Y', created_at) == '2026'",
        "julianday('1970-01-01') == 2440587.5 and unixepoch('1970-01-01') == 0",
        "coalesce(harness_name, '') == '' and not orchestrator_session",
    ] {
        assert!(evaluate(expression), "{expression}");
    }
}

#[test]
fn invalid_or_unsupported_expressions_fail_closed() {
    for expression in [
        "",
        "id = 1",
        "id ==",
        "return true",
        "true); return false --",
        "unknown == 1",
        "claim_key == nil",
        "context_only",
        "os.execute('echo bad')",
        "description:lower() == 'fix auth'",
        "{1,2}",
        "function() return true end",
        "id :: number",
        "`task {id}`",
        "priority % 2 == 1",
        "priority // 2 == 1",
        "2 ^ 3 == 8",
        "like(description)",
        "like(id, '%')",
        "lower(1) == '1'",
        "min(priority)",
        "coalesce(priority, description) == 0",
        "archived + 1 == 1",
        "priority > description",
        "true or 3",
        "if archived then 1 else 'text'",
    ] {
        let error = match sql_filter::compile(expression) {
            Ok(_) => panic!("accepted {expression}"),
            Err(error) => error.to_string(),
        };
        assert!(error.contains("--filter"), "{expression}: {error}");
    }
}

#[test]
fn compiler_bounds_apply_before_recursive_parsing() {
    for expression in [
        "(".repeat(10_000),
        format!("{}true{}", "(".repeat(100), ")".repeat(100)),
        format!("{}true", "not ".repeat(40)),
        format!("'{}' == description", "x".repeat(16_384)),
    ] {
        assert!(sql_filter::compile(&expression).is_err());
    }
    assert!(evaluate("(((priority == 3)))"));
}