Security hotfix — v0.15.1 Wave 1 fixes from 4-parallel audit. RED-1 (CVE): KEI_DISABLED_HOOKS tokenized match — was `*all*` substring-glob (trivially bypassable via "install", "wall-clock", etc.), now exact-token split on comma/space. Patched in all 9 hooks: no-hand-edit-agents, assemble-agents, assemble-validate, tomd-preread, agent-fork-logger, site-wysiwyd-check, error-spike-detector, milestone-commit-hook, session-end-dump. RED-2 (observability): minimal profile whitelist now includes agent-fork-logger and session-end-dump (ledger + trace paths) so observability is not silently lost on minimal installs. HIGH: review.json schema minItems:1 on findings — rejects empty reviews; new Rust test review_schema_rejects_empty_findings. HIGH: typed-handoff wire-up — produces_artifact declared at top level on 5 manifests (kei-security-auditor, kei-validator, kei-architect, kei-code-implementer, kei-critic); duplicate per-handoff declarations removed. MED: kei-artifact validate.rs gains warn_unsupported_keywords — non-fatal stderr warning when schema uses keywords outside the hand-rolled 2020-12 subset. LOW: CI Node matrix dropped 18, now ['20','22']. Doc drift: skills/hooks-control/SKILL.md reflects tokenized-match semantics and updated minimal-profile hook list. Tests: 191 Rust workspace + 30 assembler (both pass). RED-1 reproducer 10/10 (4 former-CVE vectors blocked, 5 legit vectors accepted, empty passes). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
158 lines
4.9 KiB
Rust
158 lines
4.9 KiB
Rust
//! Integration tests — schema validation edge cases.
|
|
//!
|
|
//! Constructor Pattern: one scenario per test, one assertion focus.
|
|
|
|
use kei_artifact::artifact::{emit, get, register_schema, validate_by_id};
|
|
use kei_artifact::schemas::register_builtins;
|
|
use kei_artifact::Store;
|
|
use serde_json::json;
|
|
|
|
fn seed() -> Store {
|
|
let s = Store::open_memory().unwrap();
|
|
register_builtins(&s).unwrap();
|
|
s
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_content_rejected_at_emit_time() {
|
|
let s = seed();
|
|
// spec requires goal, constraints, invariants — give only goal.
|
|
let bad = serde_json::to_vec(&json!({"goal": "x"})).unwrap();
|
|
let err = emit(&s, "spec", "kei-architect", &bad, None, None).unwrap_err();
|
|
let msg = format!("{err:#}");
|
|
assert!(
|
|
msg.contains("constraints") || msg.contains("invariants"),
|
|
"unexpected error: {msg}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn validate_by_id_passes_for_conforming_content() {
|
|
let s = seed();
|
|
let content = serde_json::to_vec(&json!({
|
|
"summary": "ok",
|
|
"changes": [{"path": "x", "op": "mod", "summary": "tiny"}]
|
|
}))
|
|
.unwrap();
|
|
let id = emit(&s, "patch", "kei-code-implementer", &content, None, None).unwrap();
|
|
assert!(validate_by_id(&s, &id).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn validate_by_id_detects_drift_after_schema_override() {
|
|
let s = seed();
|
|
let content = serde_json::to_vec(&json!({
|
|
"summary": "ok",
|
|
"changes": [{"path": "x", "op": "mod", "summary": "t"}]
|
|
}))
|
|
.unwrap();
|
|
let id = emit(&s, "patch", "kei-code-implementer", &content, None, None).unwrap();
|
|
let stricter = r#"{
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"required": ["summary", "changes", "must_have"],
|
|
"properties": {
|
|
"summary": {"type": "string"},
|
|
"changes": {"type": "array"},
|
|
"must_have": {"type": "string"}
|
|
}
|
|
}"#;
|
|
register_schema(&s, "patch", stricter).unwrap();
|
|
let err = validate_by_id(&s, &id).unwrap_err();
|
|
let msg = format!("{err:#}");
|
|
assert!(msg.contains("must_have"), "unexpected: {msg}");
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_schema_name_rejected_at_emit() {
|
|
let s = seed();
|
|
let err = emit(&s, "not-a-real-schema", "x", b"{}", None, None).unwrap_err();
|
|
let msg = format!("{err:#}");
|
|
assert!(msg.contains("unknown schema"), "unexpected: {msg}");
|
|
}
|
|
|
|
#[test]
|
|
fn review_schema_accepts_canonical_critic_output() {
|
|
let s = seed();
|
|
let content = serde_json::to_vec(&json!({
|
|
"reviewer": "kei-critic",
|
|
"findings": [
|
|
{
|
|
"severity": "high",
|
|
"category": "bug",
|
|
"title": "off-by-one",
|
|
"file": "src/x.rs",
|
|
"line": 42
|
|
}
|
|
],
|
|
"verdict": "request_changes"
|
|
}))
|
|
.unwrap();
|
|
let id = emit(&s, "review", "kei-critic", &content, None, None).unwrap();
|
|
let back = get(&s, &id).unwrap().unwrap();
|
|
assert_eq!(back.schema_name, "review");
|
|
}
|
|
|
|
#[test]
|
|
fn research_schema_accepts_claims_with_evidence_grade() {
|
|
let s = seed();
|
|
let content = serde_json::to_vec(&json!({
|
|
"question": "Does Rust's cargo support offline builds?",
|
|
"claims": [
|
|
{
|
|
"claim": "cargo --offline works with pre-cached deps",
|
|
"evidence_grade": "E1",
|
|
"confidence": "100",
|
|
"sources": [{"url": "https://doc.rust-lang.org/cargo/", "verified": true}]
|
|
}
|
|
]
|
|
}))
|
|
.unwrap();
|
|
let id = emit(&s, "research", "kei-researcher", &content, None, None).unwrap();
|
|
assert!(validate_by_id(&s, &id).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn plan_schema_rejects_empty_steps() {
|
|
let s = seed();
|
|
let bad = serde_json::to_vec(&json!({
|
|
"goal": "g",
|
|
"steps": []
|
|
}))
|
|
.unwrap();
|
|
let err = emit(&s, "plan", "kei-architect", &bad, None, None).unwrap_err();
|
|
let msg = format!("{err:#}");
|
|
assert!(msg.contains("array"), "unexpected: {msg}");
|
|
}
|
|
|
|
#[test]
|
|
fn patch_schema_rejects_invalid_op_enum() {
|
|
let s = seed();
|
|
let bad = serde_json::to_vec(&json!({
|
|
"summary": "ok",
|
|
"changes": [{"path": "x", "op": "RENAME", "summary": "t"}]
|
|
}))
|
|
.unwrap();
|
|
let err = emit(&s, "patch", "kei-code-implementer", &bad, None, None).unwrap_err();
|
|
let msg = format!("{err:#}");
|
|
assert!(msg.contains("enum"), "unexpected: {msg}");
|
|
}
|
|
|
|
#[test]
|
|
fn review_schema_rejects_empty_findings() {
|
|
// v0.15.1 HIGH fix: review artifacts must list ≥ 1 finding so a `reject`
|
|
// or `request_changes` verdict cannot ship with nothing to point at.
|
|
let s = seed();
|
|
let bad = serde_json::to_vec(&json!({
|
|
"reviewer": "kei-critic",
|
|
"findings": [],
|
|
"verdict": "reject"
|
|
}))
|
|
.unwrap();
|
|
let err = emit(&s, "review", "kei-critic", &bad, None, None).unwrap_err();
|
|
let msg = format!("{err:#}");
|
|
assert!(
|
|
msg.contains("array") || msg.contains("min"),
|
|
"unexpected: {msg}"
|
|
);
|
|
}
|