#![allow(clippy::too_many_lines)]
#[cfg(feature = "sal")]
use crate::models::field_names;
#[cfg(feature = "sal")]
use axum::{
Json,
extract::{Request, State},
http::StatusCode,
middleware::Next,
response::{IntoResponse, Response},
};
#[cfg(feature = "sal")]
use serde_json::json;
#[cfg(feature = "sal")]
use super::{AppState, StorageBackend};
#[cfg(feature = "sal")]
#[must_use]
pub fn postgres_not_implemented(endpoint: &'static str) -> Response {
let remediation = if endpoint == super::routes::FIND_PATHS {
"POST /api/v1/kg/find_paths instead (same handler; the bare /find_paths path is now an alias on v0.7.0+ binaries). Accepts both `source_id`/`target_id` and `from_id`/`to_id` body fields."
} else {
"use sqlite-backed daemon or wait for v0.7.x trait coverage"
};
(
StatusCode::NOT_IMPLEMENTED,
Json(json!({
"error": "endpoint not yet implemented for postgres-backed daemon",
"endpoint": endpoint,
(field_names::STORAGE_BACKEND): "postgres",
"remediation": remediation,
})),
)
.into_response()
}
#[cfg(feature = "sal")]
#[must_use]
pub fn postgres_endpoint_supported(method: &axum::http::Method, path: &str) -> bool {
use axum::http::Method;
if path == super::routes::HEALTH
|| path == super::routes::CAPABILITIES
|| path == super::routes::METRICS_BARE
|| path == super::routes::METRICS
{
return true;
}
if path == super::routes::APPROVALS_STREAM && method == Method::GET {
return true;
}
match (method.as_str(), path) {
("POST", super::routes::MEMORIES) | ("GET", super::routes::MEMORIES) => true,
("GET" | "PUT" | "DELETE", p) if memory_id_path(p) => true,
("GET", super::routes::SEARCH) => true,
("POST", super::routes::LINKS) => true,
("GET", p) if links_id_path(p) => true,
("GET", super::routes::PENDING) => true,
("GET", super::routes::AGENTS) => true,
("PUT", p) if agents_pubkey_path(p) => true,
("GET", super::routes::NAMESPACES) => {
true
}
("POST", super::routes::KG_QUERY)
| ("GET", super::routes::KG_TIMELINE)
| ("POST", super::routes::KG_INVALIDATE) => true,
("POST", super::routes::KG_FIND_PATHS)
| ("POST", super::routes::LINKS_VERIFY)
| ("POST", super::routes::QUOTA_STATUS) => true,
("POST", super::routes::ENTITIES) | ("GET", super::routes::ENTITIES_BY_ALIAS) => true,
("GET", super::routes::STATS) => true,
("POST", super::routes::MEMORIES_BULK) => true,
("GET" | "POST", super::routes::RECALL) => true,
("GET", super::routes::ARCHIVE) => true,
("GET", super::routes::ARCHIVE_STATS) => true,
("GET", super::routes::TAXONOMY) => true,
("POST", super::routes::CHECK_DUPLICATE) => true,
("GET", super::routes::SUBSCRIPTIONS) => true,
("GET", super::routes::INBOX) => true,
("POST", super::routes::SYNC_PUSH) => true,
("GET", super::routes::SYNC_SINCE) => true,
("POST", p) if pending_decide_path(p) => true,
("POST", p) if namespace_standard_post_path(p) => true,
("DELETE", p) if namespace_standard_delete_path(p) => true,
("POST", super::routes::NAMESPACES) => true,
("DELETE", super::routes::NAMESPACES) => true,
("POST", super::routes::FORGET) => true,
("POST", super::routes::CONSOLIDATE) => true,
("GET", super::routes::CONTRADICTIONS) => true,
("POST", super::routes::AUTO_TAG) => true,
("POST", super::routes::EXPAND_QUERY) => true,
("GET", super::routes::TOOLS_LIST) => true,
("POST", super::routes::MEMORY_LOAD_FAMILY) => true,
("POST", super::routes::NOTIFY) => true,
("POST", super::routes::GC) => true,
("POST", super::routes::IMPORT) => true,
("GET", super::routes::EXPORT) => true,
("POST", super::routes::ARCHIVE) => true,
("DELETE", super::routes::ARCHIVE) => true,
("POST", p) if archive_restore_path(p) => true,
("POST", super::routes::AGENTS) => true,
("DELETE", super::routes::LINKS) => true,
("POST", super::routes::SUBSCRIPTIONS) | ("DELETE", super::routes::SUBSCRIPTIONS) => true,
("POST", super::routes::SESSION_START) => true,
("POST", p) if memory_promote_path(p) => true,
("POST", p) if approvals_decide_path(p) => true,
("POST", super::routes::MEMORY_REFLECT) => true,
("POST", super::routes::MEMORY_REFLECTION_ORIGIN) => true,
("POST", super::routes::CAPTURE_TURN) => true,
("POST", super::routes::MEMORY_RECALL_OBSERVATIONS) => true,
_ => false,
}
}
#[cfg(feature = "sal")]
fn memory_promote_path(p: &str) -> bool {
let Some(rest) = p.strip_prefix("/api/v1/memories/") else {
return false;
};
rest.ends_with("/promote") && rest.split('/').count() == 2
}
#[cfg(feature = "sal")]
fn approvals_decide_path(p: &str) -> bool {
let Some(rest) = p.strip_prefix("/api/v1/approvals/") else {
return false;
};
!rest.is_empty() && rest != "stream" && !rest.contains('/')
}
#[cfg(feature = "sal")]
fn archive_restore_path(p: &str) -> bool {
let Some(rest) = p.strip_prefix("/api/v1/archive/") else {
return false;
};
rest.ends_with("/restore") && rest.split('/').count() == 2
}
#[cfg(feature = "sal")]
fn pending_decide_path(p: &str) -> bool {
let Some(rest) = p.strip_prefix("/api/v1/pending/") else {
return false;
};
matches!(rest.split_once('/'), Some((_, "approve" | "reject")))
}
#[cfg(feature = "sal")]
fn namespace_standard_post_path(p: &str) -> bool {
let Some(rest) = p.strip_prefix("/api/v1/namespaces/") else {
return false;
};
rest.ends_with("/standard") && rest.split('/').count() == 2
}
#[cfg(feature = "sal")]
fn namespace_standard_delete_path(p: &str) -> bool {
namespace_standard_post_path(p)
}
#[cfg(feature = "sal")]
fn memory_id_path(p: &str) -> bool {
let Some(rest) = p.strip_prefix("/api/v1/memories/") else {
return false;
};
if rest == "bulk" {
return false;
}
!rest.contains('/')
}
#[cfg(feature = "sal")]
fn links_id_path(p: &str) -> bool {
let Some(rest) = p.strip_prefix("/api/v1/links/") else {
return false;
};
!rest.is_empty() && !rest.contains('/')
}
#[cfg(feature = "sal")]
fn agents_pubkey_path(p: &str) -> bool {
let Some(rest) = p.strip_prefix("/api/v1/agents/") else {
return false;
};
rest.strip_suffix("/pubkey")
.is_some_and(|id| !id.is_empty() && !id.contains('/'))
}
#[cfg(feature = "sal")]
#[must_use]
pub fn path_is_registered_route(method: &axum::http::Method, path: &str) -> bool {
#[allow(clippy::match_same_arms)]
let fixed_match = match (method.as_str(), path) {
("GET", super::routes::HEALTH) => true,
("GET", super::routes::METRICS_BARE) => true,
("GET", super::routes::METRICS) => true,
("GET", super::routes::CAPABILITIES) => true,
("GET", super::routes::MEMORIES) => true,
("POST", super::routes::MEMORIES) => true,
("POST", super::routes::MEMORIES_BULK) => true,
("GET", super::routes::SEARCH) => true,
("GET", super::routes::RECALL) => true,
("POST", super::routes::RECALL) => true,
("POST", super::routes::FORGET) => true,
("POST", super::routes::CONSOLIDATE) => true,
("GET", super::routes::CONTRADICTIONS) => true,
("POST", super::routes::AUTO_TAG) => true,
("POST", super::routes::EXPAND_QUERY) => true,
("POST", super::routes::CAPTURE_TURN) => true,
("GET", super::routes::TOOLS_LIST) => true,
("POST", super::routes::MEMORY_LOAD_FAMILY) => true,
("POST", super::routes::LINKS) => true,
("DELETE", super::routes::LINKS) => true,
("GET", super::routes::NAMESPACES) => true,
("POST", super::routes::NAMESPACES) => true,
("DELETE", super::routes::NAMESPACES) => true,
("GET", super::routes::TAXONOMY) => true,
("POST", super::routes::CHECK_DUPLICATE) => true,
("POST", super::routes::ENTITIES) => true,
("GET", super::routes::ENTITIES_BY_ALIAS) => true,
("GET", super::routes::KG_TIMELINE) => true,
("POST", super::routes::KG_INVALIDATE) => true,
("POST", super::routes::KG_QUERY) => true,
("POST", super::routes::KG_FIND_PATHS) => true,
("POST", super::routes::FIND_PATHS) => true,
("POST", super::routes::LINKS_VERIFY) => true,
("POST", super::routes::QUOTA_STATUS) => true,
("GET", super::routes::STATS) => true,
("POST", super::routes::GC) => true,
("GET", super::routes::EXPORT) => true,
("POST", super::routes::IMPORT) => true,
("GET", super::routes::ARCHIVE) => true,
("POST", super::routes::ARCHIVE) => true,
("DELETE", super::routes::ARCHIVE) => true,
("GET", super::routes::ARCHIVE_STATS) => true,
("GET", super::routes::AGENTS) => true,
("POST", super::routes::AGENTS) => true,
("PUT", p) if agents_pubkey_path(p) => true,
("GET", super::routes::PENDING) => true,
("GET", super::routes::APPROVALS_STREAM) => true,
("POST", super::routes::SYNC_PUSH) => true,
("GET", super::routes::SYNC_SINCE) => true,
("POST", super::routes::NOTIFY) => true,
("GET", super::routes::INBOX) => true,
("POST", super::routes::SUBSCRIPTIONS) => true,
("DELETE", super::routes::SUBSCRIPTIONS) => true,
("GET", super::routes::SUBSCRIPTIONS) => true,
("POST", super::routes::SESSION_START) => true,
("POST", super::routes::SKILL_REGISTER) => true,
("GET", super::routes::SKILL_LIST) => true,
("POST", super::routes::SHARE) => true,
("POST", super::routes::MEMORY_SMART_LOAD) => true,
("POST", super::routes::MEMORY_REFLECT) => true,
("POST", super::routes::MEMORY_RECALL_OBSERVATIONS) => true,
("POST", super::routes::MEMORY_REFLECTION_ORIGIN) => true,
("POST", super::routes::MEMORY_DEPENDENTS_OF_INVALIDATED) => true,
("POST", super::routes::MEMORY_EXPORT_REFLECTION) => true,
("POST", super::routes::MEMORY_ATOMISE) => true,
("POST", super::routes::MEMORY_CALIBRATE_CONFIDENCE) => true,
("POST", super::routes::MEMORY_VERIFY) => true,
("POST", super::routes::MEMORY_REPLAY) => true,
("POST", super::routes::MEMORY_SUBSCRIPTION_REPLAY) => true,
("POST", super::routes::MEMORY_SUBSCRIPTION_DLQ_LIST) => true,
("POST", super::routes::MEMORY_RULE_LIST) => true,
("POST", super::routes::MEMORY_CHECK_AGENT_ACTION) => true,
_ => false,
};
if fixed_match {
return true;
}
match (method.as_str(), path) {
("GET" | "PUT" | "DELETE", p) if memory_id_path(p) => true,
("POST", p) if memory_promote_path(p) => true,
("GET", p) if links_id_path(p) => true,
("GET" | "POST" | "DELETE", p) if namespace_standard_post_path(p) => true,
("POST", p) if archive_restore_path(p) => true,
("POST", p) if pending_decide_path(p) => true,
("POST", p) if approvals_decide_path(p) => true,
("GET", p) if skill_id_path(p) => true,
("GET", p) if skill_id_sub_path(p, "resource") => true,
("POST", p)
if skill_id_sub_path(p, "export")
|| skill_id_sub_path(p, "promote")
|| skill_id_sub_path(p, "compose") =>
{
true
}
_ => false,
}
}
#[cfg(feature = "sal")]
fn skill_id_path(p: &str) -> bool {
let Some(rest) = p.strip_prefix("/api/v1/skill/") else {
return false;
};
if rest == "list" || rest == "register" || rest.is_empty() {
return false;
}
!rest.contains('/')
}
#[cfg(feature = "sal")]
fn skill_id_sub_path(p: &str, suffix: &str) -> bool {
let Some(rest) = p.strip_prefix("/api/v1/skill/") else {
return false;
};
let Some((id, tail)) = rest.split_once('/') else {
return false;
};
!id.is_empty() && tail == suffix
}
#[cfg(feature = "sal")]
pub async fn postgres_route_gate(
State(app): State<AppState>,
req: Request,
next: Next,
) -> Response {
if !matches!(app.storage_backend, StorageBackend::Postgres) {
return next.run(req).await;
}
let method = req.method().clone();
let path = req.uri().path().to_string();
if postgres_endpoint_supported(&method, &path) {
return next.run(req).await;
}
if !path_is_registered_route(&method, &path) {
tracing::debug!(
method = %method,
path = %path,
"postgres-backed daemon: passing unrouted path to Axum 404 handler (#1410)"
);
return next.run(req).await;
}
tracing::debug!(
method = %method,
path = %path,
"postgres-backed daemon: 501 for un-migrated endpoint"
);
(
StatusCode::NOT_IMPLEMENTED,
Json(json!({
"error": "endpoint not yet implemented for postgres-backed daemon",
"endpoint": path,
"method": method.as_str(),
(field_names::STORAGE_BACKEND): "postgres",
"remediation": "use sqlite-backed daemon or wait for v0.7.x trait coverage; \
see docs/postgres-age-guide.md for the supported endpoint inventory",
})),
)
.into_response()
}
#[cfg(feature = "sal")]
#[must_use]
pub fn store_err_to_response(e: crate::store::StoreError) -> Response {
use crate::store::StoreError;
let (status, msg) = match &e {
StoreError::NotFound { .. } => (StatusCode::NOT_FOUND, "not found".to_string()),
StoreError::Conflict { .. } => (
StatusCode::CONFLICT,
sanitize_store_err_message(&e.to_string()),
),
StoreError::PermissionDenied { .. } => (
StatusCode::FORBIDDEN,
sanitize_store_err_message(&e.to_string()),
),
StoreError::InvalidInput { .. } => (
StatusCode::BAD_REQUEST,
sanitize_store_err_message(&e.to_string()),
),
StoreError::LinkRefused { .. } => (
StatusCode::CONFLICT,
sanitize_store_err_message(&e.to_string()),
),
StoreError::UnsupportedCapability { capability } => (
StatusCode::NOT_IMPLEMENTED,
format!("backend does not support capability: {capability}"),
),
StoreError::IntegrityFailed { .. } | StoreError::BackendUnavailable { .. } => {
tracing::error!("store backend error: {e}");
(
StatusCode::SERVICE_UNAVAILABLE,
"storage backend unavailable".to_string(),
)
}
_ => {
tracing::error!("store backend error: {e}");
(
StatusCode::INTERNAL_SERVER_ERROR,
crate::errors::msg::INTERNAL_SERVER_ERROR.to_string(),
)
}
};
(status, Json(json!({"error": msg}))).into_response()
}
#[cfg(feature = "sal")]
const REDACTED_URL_SENTINEL: &str = "[redacted-url]";
#[cfg(feature = "sal")]
const REDACTED_PATH_SENTINEL: &str = "[redacted-path]";
#[cfg(feature = "sal")]
#[must_use]
pub fn sanitize_store_err_message(raw: &str) -> String {
let mut out = String::with_capacity(raw.len());
let bytes = raw.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
if i + 2 < bytes.len() && &bytes[i..i + 3] == b"://" {
let mut scheme_start = i;
while scheme_start > 0 {
let c = bytes[scheme_start - 1];
if c.is_ascii_alphanumeric() || c == b'+' || c == b'-' || c == b'.' {
scheme_start -= 1;
} else {
break;
}
}
let pop = i - scheme_start;
out.truncate(out.len().saturating_sub(pop));
out.push_str(REDACTED_URL_SENTINEL);
i += 3;
while i < bytes.len() {
let c = bytes[i];
if c.is_ascii_whitespace()
|| c == b'"'
|| c == b'\''
|| c == b'`'
|| c == b'{'
|| c == b'}'
|| c == b'('
|| c == b')'
|| c == b','
|| c == b';'
|| c == b'<'
|| c == b'>'
{
break;
}
i += 1;
}
continue;
}
if bytes[i] == b'/'
&& (i == 0
|| matches!(
bytes[i - 1],
b' ' | b'\t' | b'\n' | b'"' | b'\'' | b'(' | b'[' | b'=' | b':'
))
&& i + 1 < bytes.len()
&& (bytes[i + 1].is_ascii_alphanumeric()
|| bytes[i + 1] == b'_'
|| bytes[i + 1] == b'.')
{
out.push_str(REDACTED_PATH_SENTINEL);
i += 1;
while i < bytes.len() {
let c = bytes[i];
if c.is_ascii_alphanumeric() || c == b'/' || c == b'.' || c == b'_' || c == b'-' {
i += 1;
} else {
break;
}
}
continue;
}
out.push(bytes[i] as char);
i += 1;
}
out
}
#[cfg(all(test, feature = "sal"))]
mod store_err_sanitize_tests {
use super::{REDACTED_PATH_SENTINEL, REDACTED_URL_SENTINEL, sanitize_store_err_message};
#[test]
fn sanitize_redacts_postgres_url() {
let leak = "connection failed for postgres://admin:hunter2@db.internal:5432/ai_memory";
let clean = sanitize_store_err_message(leak);
assert!(!clean.contains("postgres://"), "raw scheme leaked: {clean}");
assert!(!clean.contains("hunter2"), "password leaked: {clean}");
assert!(!clean.contains("db.internal"), "host leaked: {clean}");
assert!(
clean.contains(REDACTED_URL_SENTINEL),
"missing sentinel: {clean}"
);
}
#[test]
fn sanitize_redacts_filesystem_path() {
let leak = "open /var/lib/postgresql/data/global/pg_control failed";
let clean = sanitize_store_err_message(leak);
assert!(!clean.contains("/var/lib"), "raw path leaked: {clean}");
assert!(
clean.contains(REDACTED_PATH_SENTINEL),
"missing sentinel: {clean}"
);
}
#[test]
fn sanitize_passes_through_clean_diagnostics() {
let clean_input = "memory not found: abc-123";
let out = sanitize_store_err_message(clean_input);
assert_eq!(out, clean_input);
}
#[test]
fn sanitize_handles_multiple_leaks() {
let leak = "sqlx error at postgres://u:p@h/db touching /etc/secret/key";
let clean = sanitize_store_err_message(leak);
assert!(!clean.contains("postgres://"));
assert!(!clean.contains("/etc/secret"));
assert!(clean.contains(REDACTED_URL_SENTINEL));
assert!(clean.contains(REDACTED_PATH_SENTINEL));
}
#[test]
fn sanitize_preserves_relative_paths() {
let raw = "ratio 1/2 over 3/4";
let out = sanitize_store_err_message(raw);
assert_eq!(out, raw, "fraction-like content must not be redacted");
}
#[test]
fn sanitize_handles_unicode_in_clean_message() {
let raw = "memory not found: \u{1F4DD}-id-with-emoji";
let out = sanitize_store_err_message(raw);
assert!(out.contains(crate::errors::msg::MEMORY_NOT_FOUND));
}
#[test]
fn sanitize_redacts_url_at_start_of_message() {
let leak = "postgres://u:p@h/db is unreachable";
let clean = sanitize_store_err_message(leak);
assert!(clean.starts_with(REDACTED_URL_SENTINEL));
}
}
#[cfg(all(test, feature = "sal"))]
mod transport_postgres_gate_tests {
use super::*;
use axum::http::Method;
#[test]
fn postgres_gate_always_passes_health_and_metrics() {
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::HEALTH
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::CAPABILITIES
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::METRICS_BARE
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::METRICS
));
}
#[test]
fn postgres_gate_passes_core_crud() {
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::MEMORIES
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::MEMORIES
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::SEARCH
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::LINKS
));
}
#[test]
fn postgres_gate_passes_recursive_learning_routes() {
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::MEMORY_REFLECT
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::MEMORY_REFLECTION_ORIGIN
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::MEMORY_RECALL_OBSERVATIONS
));
}
#[test]
fn postgres_gate_passes_memory_id_paths() {
assert!(postgres_endpoint_supported(
&Method::GET,
"/api/v1/memories/abc-123"
));
assert!(postgres_endpoint_supported(
&Method::PUT,
"/api/v1/memories/abc-123"
));
assert!(postgres_endpoint_supported(
&Method::DELETE,
"/api/v1/memories/abc-123"
));
assert!(!postgres_endpoint_supported(
&Method::POST,
"/api/v1/memories/abc-123"
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::MEMORIES_BULK
));
}
#[test]
fn postgres_gate_passes_links_id_paths() {
assert!(postgres_endpoint_supported(
&Method::GET,
"/api/v1/links/link-id-1"
));
assert!(!postgres_endpoint_supported(&Method::GET, "/api/v1/links/"));
}
#[test]
fn postgres_gate_passes_kg_paths() {
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::KG_QUERY
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::KG_TIMELINE
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::KG_INVALIDATE
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::KG_FIND_PATHS
));
}
#[test]
fn postgres_gate_passes_quota_verify_entities() {
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::LINKS_VERIFY
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::QUOTA_STATUS
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::ENTITIES
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::ENTITIES_BY_ALIAS
));
}
#[test]
fn postgres_gate_passes_archive_paths() {
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::ARCHIVE
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::ARCHIVE_STATS
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::ARCHIVE
));
assert!(postgres_endpoint_supported(
&Method::DELETE,
crate::handlers::routes::ARCHIVE
));
assert!(!postgres_endpoint_supported(
&Method::POST,
"/api/v1/archive/purge"
));
assert!(postgres_endpoint_supported(
&Method::POST,
"/api/v1/archive/abc/restore"
));
assert!(!postgres_endpoint_supported(
&Method::POST,
"/api/v1/archive/abc/restore/other"
));
}
#[test]
fn postgres_gate_passes_namespace_standard_paths() {
assert!(postgres_endpoint_supported(
&Method::POST,
"/api/v1/namespaces/proj/standard"
));
assert!(postgres_endpoint_supported(
&Method::DELETE,
"/api/v1/namespaces/proj/standard"
));
assert!(!postgres_endpoint_supported(
&Method::POST,
"/api/v1/namespaces/standard"
));
}
#[test]
fn postgres_gate_passes_pending_decide_paths() {
assert!(postgres_endpoint_supported(
&Method::POST,
"/api/v1/pending/p1/approve"
));
assert!(postgres_endpoint_supported(
&Method::POST,
"/api/v1/pending/p1/reject"
));
assert!(!postgres_endpoint_supported(
&Method::POST,
"/api/v1/pending/p1/foo"
));
}
#[test]
fn postgres_gate_passes_approvals_decide_paths() {
assert!(postgres_endpoint_supported(
&Method::POST,
"/api/v1/approvals/abc-123"
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::APPROVALS_STREAM
));
}
#[test]
fn postgres_gate_passes_memory_promote_path() {
assert!(postgres_endpoint_supported(
&Method::POST,
"/api/v1/memories/abc/promote"
));
assert!(!postgres_endpoint_supported(
&Method::POST,
"/api/v1/memories/abc/promote/extra"
));
}
#[test]
fn postgres_gate_passes_remaining_write_paths() {
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::FORGET
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::CONSOLIDATE
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::CONTRADICTIONS
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::AUTO_TAG
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::EXPAND_QUERY
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::TOOLS_LIST
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::MEMORY_LOAD_FAMILY
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::NOTIFY
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::GC
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::IMPORT
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::EXPORT
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::AGENTS
));
assert!(postgres_endpoint_supported(
&Method::DELETE,
crate::handlers::routes::LINKS
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::SUBSCRIPTIONS
));
assert!(postgres_endpoint_supported(
&Method::DELETE,
crate::handlers::routes::SUBSCRIPTIONS
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::SESSION_START
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::SYNC_PUSH
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::SYNC_SINCE
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::PENDING
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::AGENTS
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::NAMESPACES
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::NAMESPACES
));
assert!(postgres_endpoint_supported(
&Method::DELETE,
crate::handlers::routes::NAMESPACES
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::STATS
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::TAXONOMY
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::CHECK_DUPLICATE
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::SUBSCRIPTIONS
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::INBOX
));
assert!(postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::RECALL
));
assert!(postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::RECALL
));
}
#[test]
fn postgres_gate_rejects_unknown_paths() {
assert!(!postgres_endpoint_supported(
&Method::POST,
"/api/v1/this/is/not/a/real/endpoint"
));
assert!(!postgres_endpoint_supported(
&Method::POST,
"/api/v1/unknown"
));
}
#[test]
fn unknown_path_not_a_registered_route_1410() {
assert!(!path_is_registered_route(
&Method::GET,
"/api/v1/this-does-not-exist"
));
assert!(!path_is_registered_route(
&Method::POST,
"/api/v1/federation/peers"
));
assert!(!path_is_registered_route(
&Method::POST,
"/api/v1/totally/fake/path"
));
assert!(!path_is_registered_route(
&Method::PATCH,
crate::handlers::routes::MEMORIES
));
}
#[test]
fn registered_route_table_covers_every_router_registration_1410() {
assert!(path_is_registered_route(
&Method::GET,
crate::handlers::routes::HEALTH
));
assert!(path_is_registered_route(
&Method::POST,
crate::handlers::routes::CAPTURE_TURN
));
assert!(
postgres_endpoint_supported(&Method::POST, crate::handlers::routes::CAPTURE_TURN),
"#1619: capture_turn is fully SAL-routed and must be postgres-supported"
);
assert!(path_is_registered_route(
&Method::GET,
crate::handlers::routes::METRICS_BARE
));
assert!(path_is_registered_route(
&Method::POST,
crate::handlers::routes::MEMORIES
));
assert!(path_is_registered_route(
&Method::GET,
crate::handlers::routes::MEMORIES
));
assert!(path_is_registered_route(
&Method::POST,
crate::handlers::routes::MEMORIES_BULK
));
assert!(path_is_registered_route(
&Method::GET,
crate::handlers::routes::SEARCH
));
assert!(path_is_registered_route(
&Method::POST,
crate::handlers::routes::FORGET
));
assert!(path_is_registered_route(
&Method::POST,
crate::handlers::routes::CONSOLIDATE
));
assert!(path_is_registered_route(
&Method::POST,
crate::handlers::routes::SHARE
));
assert!(path_is_registered_route(
&Method::POST,
crate::handlers::routes::MEMORY_SMART_LOAD
));
assert!(path_is_registered_route(
&Method::POST,
crate::handlers::routes::MEMORY_REFLECT
));
assert!(path_is_registered_route(
&Method::POST,
crate::handlers::routes::MEMORY_ATOMISE
));
assert!(path_is_registered_route(
&Method::GET,
crate::handlers::routes::KG_TIMELINE
));
assert!(path_is_registered_route(
&Method::POST,
crate::handlers::routes::KG_QUERY
));
assert!(path_is_registered_route(
&Method::POST,
crate::handlers::routes::KG_FIND_PATHS
));
assert!(path_is_registered_route(
&Method::POST,
crate::handlers::routes::FIND_PATHS
));
assert!(path_is_registered_route(
&Method::GET,
crate::handlers::routes::NAMESPACES
));
assert!(path_is_registered_route(
&Method::POST,
crate::handlers::routes::NAMESPACES
));
assert!(path_is_registered_route(
&Method::DELETE,
crate::handlers::routes::NAMESPACES
));
assert!(path_is_registered_route(
&Method::GET,
"/api/v1/memories/abc-123"
));
assert!(path_is_registered_route(
&Method::PUT,
"/api/v1/memories/abc-123"
));
assert!(path_is_registered_route(
&Method::DELETE,
"/api/v1/memories/abc-123"
));
assert!(path_is_registered_route(
&Method::POST,
"/api/v1/memories/abc-123/promote"
));
assert!(path_is_registered_route(
&Method::GET,
"/api/v1/links/link-1"
));
assert!(path_is_registered_route(
&Method::POST,
"/api/v1/namespaces/proj/standard"
));
assert!(path_is_registered_route(
&Method::GET,
"/api/v1/namespaces/proj/standard"
));
assert!(path_is_registered_route(
&Method::DELETE,
"/api/v1/namespaces/proj/standard"
));
assert!(path_is_registered_route(
&Method::POST,
"/api/v1/archive/abc/restore"
));
assert!(path_is_registered_route(
&Method::POST,
"/api/v1/pending/p1/approve"
));
assert!(path_is_registered_route(
&Method::POST,
"/api/v1/pending/p1/reject"
));
assert!(path_is_registered_route(
&Method::POST,
"/api/v1/approvals/pa-1"
));
assert!(path_is_registered_route(
&Method::GET,
"/api/v1/skill/skill-1"
));
assert!(path_is_registered_route(
&Method::GET,
"/api/v1/skill/skill-1/resource"
));
assert!(path_is_registered_route(
&Method::POST,
"/api/v1/skill/skill-1/export"
));
assert!(path_is_registered_route(
&Method::POST,
"/api/v1/skill/skill-1/promote"
));
assert!(path_is_registered_route(
&Method::POST,
"/api/v1/skill/skill-1/compose"
));
assert!(path_is_registered_route(
&Method::GET,
crate::handlers::routes::SKILL_LIST
));
assert!(!path_is_registered_route(
&Method::POST,
"/api/v1/skill/skill-1/export/extra"
));
assert!(!path_is_registered_route(
&Method::POST,
"/api/v1/archive/abc/restore/extra"
));
}
#[test]
fn registered_route_not_in_postgres_allowlist_1410() {
assert!(path_is_registered_route(
&Method::POST,
crate::handlers::routes::SHARE
));
assert!(!postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::SHARE
));
assert!(path_is_registered_route(
&Method::POST,
crate::handlers::routes::MEMORY_SMART_LOAD
));
assert!(!postgres_endpoint_supported(
&Method::POST,
crate::handlers::routes::MEMORY_SMART_LOAD
));
assert!(path_is_registered_route(
&Method::GET,
crate::handlers::routes::SKILL_LIST
));
assert!(!postgres_endpoint_supported(
&Method::GET,
crate::handlers::routes::SKILL_LIST
));
}
#[test]
fn postgres_not_implemented_carries_endpoint_and_remediation() {
let resp = postgres_not_implemented("/api/v1/test");
assert_eq!(resp.status(), axum::http::StatusCode::NOT_IMPLEMENTED);
}
#[test]
fn store_err_to_response_maps_every_variant_to_status() {
use crate::store::StoreError;
let r = store_err_to_response(StoreError::NotFound {
id: "x".to_string(),
});
assert_eq!(r.status(), axum::http::StatusCode::NOT_FOUND);
let r = store_err_to_response(StoreError::Conflict {
id: "x".to_string(),
});
assert_eq!(r.status(), axum::http::StatusCode::CONFLICT);
let r = store_err_to_response(StoreError::PermissionDenied {
action: "r".to_string(),
target: "t".to_string(),
reason: "x".to_string(),
});
assert_eq!(r.status(), axum::http::StatusCode::FORBIDDEN);
let r = store_err_to_response(StoreError::InvalidInput {
detail: "bad".to_string(),
});
assert_eq!(r.status(), axum::http::StatusCode::BAD_REQUEST);
let r = store_err_to_response(StoreError::LinkRefused {
detail: "link refused: reflection cycle: a --reflects_on--> b would close a cycle"
.to_string(),
});
assert_eq!(r.status(), axum::http::StatusCode::CONFLICT);
let r = store_err_to_response(StoreError::UnsupportedCapability {
capability: "X".to_string(),
});
assert_eq!(r.status(), axum::http::StatusCode::NOT_IMPLEMENTED);
let r = store_err_to_response(StoreError::IntegrityFailed {
detail: "d".to_string(),
});
assert_eq!(r.status(), axum::http::StatusCode::SERVICE_UNAVAILABLE);
let r = store_err_to_response(StoreError::BackendUnavailable {
backend: "p".to_string(),
detail: "d".to_string(),
});
assert_eq!(r.status(), axum::http::StatusCode::SERVICE_UNAVAILABLE);
let r = store_err_to_response(StoreError::Backend(crate::store::BoxBackendError::new(
"raw",
)));
assert_eq!(r.status(), axum::http::StatusCode::INTERNAL_SERVER_ERROR);
}
}