use std::collections::HashMap;
use serde_json::Value;
use tempfile::NamedTempFile;
use tiny_http::Method;
use super::api::{route, Ctx};
use crate::KnowledgeBase;
fn ctx_with(token: Option<&str>) -> (Ctx, NamedTempFile) {
let f = NamedTempFile::new().unwrap();
let kb = KnowledgeBase::open(f.path()).unwrap();
kb.add(
"alpha content",
"note",
Some("when alpha"),
None,
"manual",
Some("skill_a"),
)
.unwrap();
kb.add(
"beta content",
"note",
Some("when beta"),
None,
"manual",
Some("skill_b"),
)
.unwrap();
let ctx = Ctx {
kb,
token: token.map(String::from),
bind: "127.0.0.1".into(),
port: 8788,
};
(ctx, f)
}
fn hdr(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn lists_chunks() {
let (ctx, _f) = ctx_with(Some("secret"));
let r = route(&ctx, &Method::Get, "/api/chunks", "limit=10", &hdr(&[]), "");
assert_eq!(r.status, 200);
let v: Value = serde_json::from_str(&r.body).unwrap();
let chunks = v["chunks"].as_array().unwrap();
assert_eq!(chunks.len(), 2);
assert!(chunks[0].get("content_preview").is_some());
}
#[test]
fn list_state_filter_matches_nothing_for_bogus_state() {
let (ctx, _f) = ctx_with(None);
let r = route(
&ctx,
&Method::Get,
"/api/chunks",
"state=does-not-exist",
&hdr(&[]),
"",
);
assert_eq!(r.status, 200);
let v: Value = serde_json::from_str(&r.body).unwrap();
assert_eq!(v["chunks"].as_array().unwrap().len(), 0);
}
#[test]
fn inspect_is_open() {
let (ctx, _f) = ctx_with(Some("secret"));
let r = route(&ctx, &Method::Get, "/api/inspect", "", &hdr(&[]), "");
assert_eq!(r.status, 200);
}
#[test]
fn governance_queue_is_open() {
let (ctx, _f) = ctx_with(None);
let r = route(&ctx, &Method::Get, "/api/governance", "", &hdr(&[]), "");
assert_eq!(r.status, 200);
let v: Value = serde_json::from_str(&r.body).unwrap();
assert_eq!(v["state"], "pending");
assert!(v["proposals"].is_array());
}
#[test]
fn llm_traces_endpoint_returns_array() {
let (ctx, _f) = ctx_with(None);
let r = route(
&ctx,
&Method::Get,
"/api/llm-traces",
"limit=5",
&hdr(&[]),
"",
);
assert_eq!(r.status, 200);
let v: Value = serde_json::from_str(&r.body).unwrap();
assert!(v["traces"].is_array());
assert_eq!(v["limit"], 5);
}
#[test]
fn serves_index_html() {
let (ctx, _f) = ctx_with(None);
let r = route(&ctx, &Method::Get, "/", "", &hdr(&[]), "");
assert_eq!(r.status, 200);
assert!(r.body.contains("<title>Innate"));
}
#[test]
fn governance_rejected_without_token() {
let (ctx, _f) = ctx_with(Some("secret"));
let id = first_chunk_id(&ctx);
let path = format!("/api/chunk/{id}/approve");
let r = route(&ctx, &Method::Post, &path, "", &hdr(&[]), "{}");
assert_eq!(r.status, 403);
}
#[test]
fn governance_rejected_cross_origin() {
let (ctx, _f) = ctx_with(Some("secret"));
let id = first_chunk_id(&ctx);
let path = format!("/api/chunk/{id}/approve");
let h = hdr(&[
("origin", "http://evil.example.com"),
("x-innate-token", "secret"),
]);
let r = route(&ctx, &Method::Post, &path, "", &h, "{}");
assert_eq!(r.status, 403);
}
#[test]
fn governance_approve_with_token_succeeds() {
let (ctx, _f) = ctx_with(Some("secret"));
let id = first_chunk_id(&ctx);
let path = format!("/api/chunk/{id}/approve");
let h = hdr(&[
("origin", "http://127.0.0.1:8788"),
("x-innate-token", "secret"),
]);
let r = route(&ctx, &Method::Post, &path, "", &h, "{}");
assert_eq!(r.status, 200, "body: {}", r.body);
let v: Value = serde_json::from_str(&r.body).unwrap();
assert_eq!(v["ok"], true);
}
#[test]
fn archive_requires_reason() {
let (ctx, _f) = ctx_with(Some("secret"));
let id = first_chunk_id(&ctx);
let path = format!("/api/chunk/{id}/archive");
let h = hdr(&[("x-innate-token", "secret")]);
let r = route(&ctx, &Method::Post, &path, "", &h, "{}");
assert_eq!(r.status, 400);
}
#[test]
fn is_loopback_classifies_addresses() {
assert!(super::is_loopback("127.0.0.1"));
assert!(super::is_loopback("::1"));
assert!(super::is_loopback("localhost"));
assert!(!super::is_loopback("0.0.0.0"));
assert!(!super::is_loopback("192.168.1.10"));
}
#[test]
fn loopback_reads_need_no_token() {
let (ctx, _f) = ctx_with(Some("secret"));
let r = route(&ctx, &Method::Get, "/api/chunks", "", &hdr(&[]), "");
assert_eq!(r.status, 200);
}
#[test]
fn non_loopback_reads_require_token() {
let (mut ctx, _f) = ctx_with(Some("secret"));
ctx.bind = "0.0.0.0".into();
let denied = route(&ctx, &Method::Get, "/api/chunks", "", &hdr(&[]), "");
assert_eq!(
denied.status, 403,
"remote read without token must be denied"
);
let h = hdr(&[("x-innate-token", "secret")]);
let ok = route(&ctx, &Method::Get, "/api/chunks", "", &h, "");
assert_eq!(ok.status, 200, "remote read with token must succeed");
}
#[test]
fn non_loopback_static_assets_stay_public() {
let (mut ctx, _f) = ctx_with(Some("secret"));
ctx.bind = "0.0.0.0".into();
let r = route(&ctx, &Method::Get, "/", "", &hdr(&[]), "");
assert_eq!(r.status, 200);
}
#[test]
fn no_token_mode_allows_governance() {
let (ctx, _f) = ctx_with(None);
let id = first_chunk_id(&ctx);
let path = format!("/api/chunk/{id}/approve");
let r = route(&ctx, &Method::Post, &path, "", &hdr(&[]), "{}");
assert_eq!(r.status, 200, "body: {}", r.body);
}
#[test]
fn governance_queue_is_open_and_lists_proposals() {
let (ctx, _f) = ctx_with(None);
let chunk_id = first_chunk_id(&ctx);
let now = crate::utils::utc_now_iso();
ctx.kb
.storage
.upsert_governance_proposal(
&crate::utils::gen_uuid(),
&chunk_id,
"review_applicability",
"Weighted negative feedback",
3,
2.5,
2,
&now,
)
.unwrap();
let r = route(&ctx, &Method::Get, "/api/governance", "", &hdr(&[]), "");
assert_eq!(r.status, 200, "body: {}", r.body);
let v: Value = serde_json::from_str(&r.body).unwrap();
let proposals = v["proposals"].as_array().unwrap();
assert_eq!(proposals.len(), 1);
let p = &proposals[0];
assert_eq!(p["chunk_id"], chunk_id);
assert_eq!(p["actor_count"], 2);
assert!(p.get("content_preview").is_some());
assert!(p.get("skill_name").is_some());
}
#[test]
fn governance_queue_empty_by_default() {
let (ctx, _f) = ctx_with(None);
let r = route(&ctx, &Method::Get, "/api/governance", "", &hdr(&[]), "");
assert_eq!(r.status, 200);
let v: Value = serde_json::from_str(&r.body).unwrap();
assert_eq!(v["proposals"].as_array().unwrap().len(), 0);
}
fn first_chunk_id(ctx: &Ctx) -> String {
let rows = ctx.kb.storage.list_chunks(None, None, 1, 0).unwrap();
rows[0]["id"].as_str().unwrap().to_string()
}