Replace NotImplemented stub with real atom execution per schema
§Runtime invocation contract.
Convention: JSON-in/JSON-out over subprocess. Every refactored crate
exposes `<crate> run-atom <verb>` that reads JSON from stdin (or
--input), dispatches to atoms::<verb>::run, emits Output JSON on
stdout, exits per atom-error class.
Runtime side (kei-runtime):
- InvokeError: +AtomFailed{atom,code,stderr} +SubprocessError
+OutputParse +BinaryNotFound{crate_name}. NotImplemented kept as
legacy escape for atoms opting out of run-atom protocol.
- Output: now {atom: String, result: Value} — carries atom's actual
return value.
- invoke_exit_code: AtomFailed passes through child exit (0..=255),
Subprocess/OutputParse → 1, BinaryNotFound → 127, NotImplemented → 64.
- Binary resolution: KEI_RUNTIME_BIN_DIR env → PATH fallback.
kei-task side:
- New `pub mod run_atom` in lib.rs
- atoms/mod.rs: VERBS const + DispatchError enum wrapping per-atom errors
- src/run_atom.rs: read_input (stdin/@path/literal), dispatch, exit mapping
- main.rs: Cmd::RunAtom{verb, input} subcommand; collapsed three
classify_*_error helpers into single classify_dispatch. Legacy
create/search/add-dependency CLIs preserved.
Tests: 5/5 runtime (+1 invoke_real_atom integration), 9/9 kei-task
(+1 atoms::tests::verbs_list_matches_submodules).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
81 lines
3 KiB
Rust
81 lines
3 KiB
Rust
//! Integration test — `kei-runtime invoke` actually executes `kei-task::create`.
|
|
//!
|
|
//! Wire-up:
|
|
//! 1. Pre-build `kei-task` in the workspace target dir.
|
|
//! 2. Point the `--root` at the workspace's `_primitives/_rust/` so the
|
|
//! runtime discovers the real atom metadata (`kei-task/atoms/create.md`).
|
|
//! 3. Point `KEI_RUNTIME_BIN_DIR` at the target dir so the runtime resolves
|
|
//! the `kei-task` binary without polluting $PATH.
|
|
//! 4. Invoke → expect exit 0 and a JSON result containing `id` as integer.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
|
|
const BIN: &str = env!("CARGO_BIN_EXE_kei-runtime");
|
|
|
|
/// Absolute path to `_primitives/_rust/` (the atom-discovery root).
|
|
fn rust_root() -> PathBuf {
|
|
// This file lives in `_primitives/_rust/kei-runtime/tests/`.
|
|
let here = Path::new(env!("CARGO_MANIFEST_DIR"));
|
|
here.parent().expect("_primitives/_rust").to_path_buf()
|
|
}
|
|
|
|
/// Build `kei-task` so the runtime can spawn it. Uses the current profile's
|
|
/// target dir, then hands that dir to the invoke via KEI_RUNTIME_BIN_DIR.
|
|
fn build_kei_task_and_target_dir() -> PathBuf {
|
|
let rust_root = rust_root();
|
|
let status = Command::new(env!("CARGO"))
|
|
.arg("build")
|
|
.arg("-p")
|
|
.arg("kei-task")
|
|
.arg("--quiet")
|
|
.current_dir(&rust_root)
|
|
.status()
|
|
.expect("cargo build kei-task");
|
|
assert!(status.success(), "cargo build kei-task failed");
|
|
// `target` dir — try explicit override first, then fallback to `target/debug`.
|
|
if let Ok(t) = std::env::var("CARGO_TARGET_DIR") {
|
|
return PathBuf::from(t).join("debug");
|
|
}
|
|
rust_root.join("target").join("debug")
|
|
}
|
|
|
|
#[test]
|
|
fn invoke_kei_task_create_returns_id() {
|
|
let bin_dir = build_kei_task_and_target_dir();
|
|
assert!(
|
|
bin_dir.join("kei-task").is_file(),
|
|
"kei-task binary not at {}/kei-task",
|
|
bin_dir.display()
|
|
);
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let db = tmp.path().join("task.sqlite");
|
|
let out = Command::new(BIN)
|
|
.env("KEI_RUNTIME_BIN_DIR", &bin_dir)
|
|
.env("KEI_TASK_DB", &db)
|
|
.arg("invoke")
|
|
.arg("kei-task::create")
|
|
.arg("--input")
|
|
.arg(r#"{"title":"integration"}"#)
|
|
.arg("--root")
|
|
.arg(rust_root())
|
|
.output()
|
|
.expect("spawn kei-runtime");
|
|
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
|
|
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
|
assert_eq!(
|
|
out.status.code(),
|
|
Some(0),
|
|
"expected exit 0; stdout: {stdout}; stderr: {stderr}"
|
|
);
|
|
let parsed: serde_json::Value =
|
|
serde_json::from_str(stdout.trim()).expect("stdout is JSON");
|
|
// Output shape: { "atom": "kei-task::create", "result": { "id": N, "created_at": ... } }
|
|
let result = parsed.get("result").expect("result field");
|
|
let id = result
|
|
.get("id")
|
|
.expect("id field on result")
|
|
.as_i64()
|
|
.expect("id is integer");
|
|
assert!(id >= 1, "id must be a positive integer, got {id}");
|
|
}
|