use super::query::{NewRule, new_rules_since};
async fn fresh_skills_pool() -> sqlx::SqlitePool {
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.expect("open in-memory sqlite");
sqlx::query(
r"CREATE TABLE skills (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
origin TEXT NOT NULL DEFAULT 'manual',
source_repo TEXT,
status TEXT NOT NULL DEFAULT 'active',
installed_at TEXT DEFAULT (datetime('now')) NOT NULL
)",
)
.execute(&pool)
.await
.expect("create skills");
pool
}
async fn insert(
pool: &sqlx::SqlitePool,
id: &str,
name: &str,
origin: &str,
source_repo: Option<&str>,
status: &str,
installed_at_iso: &str,
) {
sqlx::query(
r"INSERT INTO skills (id, name, origin, source_repo, status, installed_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
)
.bind(id)
.bind(name)
.bind(origin)
.bind(source_repo)
.bind(status)
.bind(installed_at_iso)
.execute(pool)
.await
.expect("insert skill");
}
#[tokio::test]
async fn first_session_with_none_watermark_returns_recent_rules_for_repo() {
let pool = fresh_skills_pool().await;
insert(
&pool,
"r1",
"Return 413 for body size limit errors",
"pr_review",
Some("acme/billing"),
"active",
"2026-05-21T12:00:00Z",
)
.await;
insert(
&pool,
"r2",
"Wrap context cancellation in errgroup",
"extracted",
Some("acme/billing"),
"active",
"2026-05-22T13:00:00Z",
)
.await;
insert(
&pool,
"r3",
"Irrelevant rule from another repo",
"pr_review",
Some("other-org/other-repo"),
"active",
"2026-05-23T14:00:00Z",
)
.await;
let aliases = vec!["acme/billing".to_owned()];
let rows = new_rules_since(&pool, None, &aliases, 5)
.await
.expect("query ok");
assert_eq!(
rows.len(),
2,
"expected 2 rows scoped to acme/billing, got {rows:?}"
);
assert_eq!(rows[0].title, "Wrap context cancellation in errgroup");
assert_eq!(rows[1].title, "Return 413 for body size limit errors");
}
#[tokio::test]
async fn watermark_filters_to_only_rules_newer_than_prev_ts() {
let pool = fresh_skills_pool().await;
insert(
&pool,
"r1",
"Old rule",
"manual",
Some("acme/billing"),
"active",
"2026-05-20T10:00:00Z",
)
.await;
insert(
&pool,
"r2",
"New rule",
"pr_review",
Some("acme/billing"),
"active",
"2026-05-22T10:00:00Z",
)
.await;
let prev_ms = chrono::DateTime::parse_from_rfc3339("2026-05-21T00:00:00Z")
.expect("parse")
.timestamp_millis();
let aliases = vec!["acme/billing".to_owned()];
let rows = new_rules_since(&pool, Some(prev_ms), &aliases, 5)
.await
.expect("query ok");
assert_eq!(rows.len(), 1, "got: {rows:?}");
assert_eq!(rows[0].title, "New rule");
}
#[tokio::test]
async fn pending_and_no_source_repo_rules_are_excluded() {
let pool = fresh_skills_pool().await;
insert(
&pool,
"r1",
"Unverified rule",
"conversation",
Some("acme/billing"),
"pending",
"2026-05-22T10:00:00Z",
)
.await;
insert(
&pool,
"r2",
"Orphan rule",
"manual",
None,
"active",
"2026-05-22T11:00:00Z",
)
.await;
insert(
&pool,
"r3",
"Good rule",
"pr_review",
Some("acme/billing"),
"active",
"2026-05-22T12:00:00Z",
)
.await;
let aliases = vec!["acme/billing".to_owned()];
let rows = new_rules_since(&pool, None, &aliases, 5)
.await
.expect("query ok");
assert_eq!(rows.len(), 1, "got: {rows:?}");
assert_eq!(rows[0].title, "Good rule");
}
#[tokio::test]
async fn empty_alias_list_returns_empty_without_querying() {
let pool = fresh_skills_pool().await;
insert(
&pool,
"r1",
"Should never show up",
"pr_review",
Some("acme/billing"),
"active",
"2026-05-22T10:00:00Z",
)
.await;
let rows: Vec<NewRule> = new_rules_since(&pool, None, &[], 5)
.await
.expect("query ok");
assert!(
rows.is_empty(),
"empty aliases must yield empty result; got {rows:?}"
);
}
#[tokio::test]
async fn case_insensitive_alias_matching() {
let pool = fresh_skills_pool().await;
insert(
&pool,
"r1",
"Cased rule",
"pr_review",
Some("Acme/Billing"),
"active",
"2026-05-22T10:00:00Z",
)
.await;
let aliases = vec!["acme/billing".to_owned()];
let rows = new_rules_since(&pool, None, &aliases, 5)
.await
.expect("query ok");
assert_eq!(rows.len(), 1, "case-insensitive match failed: {rows:?}");
}
#[tokio::test]
async fn limit_caps_returned_rows() {
let pool = fresh_skills_pool().await;
for i in 0..10 {
insert(
&pool,
&format!("r{i}"),
&format!("Rule {i}"),
"pr_review",
Some("acme/billing"),
"active",
&format!("2026-05-{:02}T10:00:00Z", i + 1),
)
.await;
}
let aliases = vec!["acme/billing".to_owned()];
let rows = new_rules_since(&pool, None, &aliases, 5)
.await
.expect("query ok");
assert_eq!(rows.len(), 5, "limit not enforced: {rows:?}");
assert_eq!(rows[0].title, "Rule 9");
}