feat: publish Dever core with project scaffolding and versioned AI skills
This commit is contained in:
1 parent
70fa73cdc2
commit
ac458ffa7f
960 files changed
+221319
-1163
No files matched your search
@@ -0,0 +1,326 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use dever_runtime::component::{self, Definition, Reply};
|
||||
use dever_runtime::wire::{Encoded, Encoder};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
#[path = "support/sandbox.rs"]
|
||||
mod sandbox;
|
||||
#[path = "support/temp.rs"]
|
||||
mod temp;
|
||||
|
||||
const OPERATIONS: &[&str] = &[
|
||||
"echo",
|
||||
"fail",
|
||||
"wait",
|
||||
"timeout",
|
||||
"crash",
|
||||
"duplicate",
|
||||
"out_of_order",
|
||||
"unknown",
|
||||
"oversize",
|
||||
"truncated",
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn component_protocol_and_lifecycle_are_bounded() {
|
||||
let fixture = install_fixture();
|
||||
let result = dever_runtime::task::run_entry(async {
|
||||
fixture.prepare()?;
|
||||
// One invocation keeps the same deployment grants even if settings are
|
||||
// replaced between Worker startup, cancellation and restart.
|
||||
fs::write(
|
||||
fixture.directory.path().join("config/setting.json"),
|
||||
"invalid",
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
normal_lifecycle(fixture.entry)
|
||||
.await
|
||||
.map_err(|error| format!("normal lifecycle: {error}"))?;
|
||||
shutdown_with_queued_calls_is_bounded(fixture.entry)
|
||||
.await
|
||||
.map_err(|error| format!("queued shutdown: {error}"))?;
|
||||
rejected_handshakes(fixture.entry)
|
||||
.await
|
||||
.map_err(|error| format!("handshake rejection: {error}"))?;
|
||||
malformed_responses(fixture.entry)
|
||||
.await
|
||||
.map_err(|error| format!("malformed response: {error}"))?;
|
||||
cancelled_lifecycle(&fixture)
|
||||
.await
|
||||
.map_err(|error| format!("cancelled lifecycle: {error}"))
|
||||
});
|
||||
result.expect("external component protocol fixture");
|
||||
assert!(fixture.path.with_extension("shutdown").is_file());
|
||||
assert!(dever_runtime::task::run_entry(async { fixture.prepare() }).is_err());
|
||||
}
|
||||
|
||||
async fn cancelled_lifecycle(fixture: &InstalledFixture) -> Result<(), String> {
|
||||
let entry = fixture.entry;
|
||||
let start = dever_runtime::task::run(async move {
|
||||
component::start(definition("cancel_start", entry, "delayed_start", 2_000)).await
|
||||
})
|
||||
.await?;
|
||||
wait_for_marker(&fixture.path.with_extension("starting")).await?;
|
||||
dever_runtime::task::stop(start).await?;
|
||||
component::shutdown().await?;
|
||||
|
||||
component::start(definition(
|
||||
"cancel_shutdown",
|
||||
entry,
|
||||
"delayed_shutdown",
|
||||
2_000,
|
||||
))
|
||||
.await?;
|
||||
let stopping = dever_runtime::task::run(component::shutdown()).await?;
|
||||
wait_for_marker(&fixture.path.with_extension("stopping")).await?;
|
||||
dever_runtime::task::stop(stopping).await?;
|
||||
// The cancelled caller must leave ownership available for a complete second drain.
|
||||
component::shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_marker(path: &std::path::Path) -> Result<(), String> {
|
||||
tokio::time::timeout(std::time::Duration::from_secs(2), async {
|
||||
while !path.is_file() {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(2)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| "fixture did not reach its lifecycle checkpoint".to_owned())
|
||||
}
|
||||
|
||||
async fn shutdown_with_queued_calls_is_bounded(entry: &'static str) -> Result<(), String> {
|
||||
component::start(definition("queued_shutdown", entry, "normal", 5_000)).await?;
|
||||
let mut calls = Vec::new();
|
||||
for _ in 0..80 {
|
||||
calls.push(
|
||||
dever_runtime::task::run(async {
|
||||
component::call("queued_shutdown", "wait", &payload(0))
|
||||
.await
|
||||
.map(|_| ())
|
||||
})
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
tokio::time::timeout(std::time::Duration::from_secs(1), component::shutdown())
|
||||
.await
|
||||
.map_err(|_| "external Adapter shutdown did not stop queued calls".to_owned())??;
|
||||
for call in calls {
|
||||
let error = dever_runtime::task::wait(call)
|
||||
.await
|
||||
.expect_err("shutdown must stop every pending call");
|
||||
assert!(
|
||||
error.contains("stopped"),
|
||||
"unexpected shutdown error: {error}"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn normal_lifecycle(entry: &'static str) -> Result<(), String> {
|
||||
// A short-lived caller starts the shared Worker; ending its Scope must not stop it.
|
||||
let started = dever_runtime::task::run(async move {
|
||||
component::start(definition("normal", entry, "normal", 50)).await
|
||||
})
|
||||
.await?;
|
||||
dever_runtime::task::wait(started)
|
||||
.await
|
||||
.map_err(|error| format!("start: {error}"))?;
|
||||
|
||||
for value in [1_i64, 2] {
|
||||
match component::call("normal", "echo", &payload(value))
|
||||
.await
|
||||
.map_err(|error| format!("echo {value}: {error}"))?
|
||||
{
|
||||
Reply::Result(body) => assert_eq!(body, format!(r#"{{"value":{value}}}"#)),
|
||||
Reply::Error { .. } => return Err("echo returned a business error".into()),
|
||||
}
|
||||
}
|
||||
match component::call("normal", "fail", &payload(0))
|
||||
.await
|
||||
.map_err(|error| format!("business error: {error}"))?
|
||||
{
|
||||
Reply::Error { identity, payload } => {
|
||||
assert_eq!(identity, "notification.delivery.Rejected");
|
||||
assert_eq!(payload, r#"{"reason":"fixture"}"#);
|
||||
}
|
||||
Reply::Result(_) => return Err("fail returned a result".into()),
|
||||
}
|
||||
|
||||
let wait = dever_runtime::task::run(async {
|
||||
component::call("normal", "wait", &payload(0))
|
||||
.await
|
||||
.map(|_| ())
|
||||
})
|
||||
.await?;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
|
||||
dever_runtime::task::stop(wait)
|
||||
.await
|
||||
.map_err(|error| format!("cancel wait: {error}"))?;
|
||||
|
||||
let timeout = component::call("normal", "timeout", &payload(0))
|
||||
.await
|
||||
.expect_err("timeout operation must reach the deadline");
|
||||
assert_eq!(timeout, "external Adapter call timed out");
|
||||
assert!(matches!(
|
||||
component::call("normal", "echo", &payload(3))
|
||||
.await
|
||||
.map_err(|error| format!("echo after timeout: {error}"))?,
|
||||
Reply::Result(_)
|
||||
));
|
||||
|
||||
let crash = component::call("normal", "crash", &payload(0))
|
||||
.await
|
||||
.expect_err("crashed worker must fail its pending call");
|
||||
assert!(
|
||||
crash.contains("early eof"),
|
||||
"unexpected crash fault: {crash}"
|
||||
);
|
||||
assert!(matches!(
|
||||
component::call("normal", "echo", &payload(4))
|
||||
.await
|
||||
.map_err(|error| format!("echo after crash: {error}"))?,
|
||||
Reply::Result(_)
|
||||
));
|
||||
component::shutdown()
|
||||
.await
|
||||
.map_err(|error| format!("shutdown: {error}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rejected_handshakes(entry: &'static str) -> Result<(), String> {
|
||||
for (key, mode, expected) in [
|
||||
("duplicate_ready", "duplicate_ready", "duplicate JSON field"),
|
||||
(
|
||||
"capability_mismatch",
|
||||
"capability_mismatch",
|
||||
"handshake does not match",
|
||||
),
|
||||
] {
|
||||
let error = component::start(definition(key, entry, mode, 100))
|
||||
.await
|
||||
.expect_err("invalid handshake must be rejected");
|
||||
assert!(
|
||||
error.contains(expected),
|
||||
"unexpected handshake error: {error}"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn malformed_responses(entry: &'static str) -> Result<(), String> {
|
||||
for operation in [
|
||||
"duplicate",
|
||||
"out_of_order",
|
||||
"unknown",
|
||||
"oversize",
|
||||
"truncated",
|
||||
] {
|
||||
component::start(definition(operation, entry, "normal", 100)).await?;
|
||||
let error = component::call(operation, operation, &payload(0))
|
||||
.await
|
||||
.expect_err("invalid response must fail its pending call");
|
||||
let expected = match operation {
|
||||
"duplicate" => "duplicate JSON field",
|
||||
"out_of_order" => "out-of-order request id",
|
||||
"unknown" => "unknown message kind",
|
||||
"oversize" => "frame exceeds byte limit",
|
||||
"truncated" => "cannot read complete component protocol frame",
|
||||
_ => unreachable!(),
|
||||
};
|
||||
assert!(
|
||||
error.contains(expected),
|
||||
"unexpected {operation} call fault: {error}"
|
||||
);
|
||||
let cleanup = component::shutdown()
|
||||
.await
|
||||
.expect_err("protocol fault must survive cleanup");
|
||||
assert!(
|
||||
cleanup.contains(expected),
|
||||
"unexpected {operation} fault: {cleanup}"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn definition(
|
||||
key: &'static str,
|
||||
entry: &'static str,
|
||||
mode: &'static str,
|
||||
timeout_ms: u64,
|
||||
) -> Definition {
|
||||
Definition {
|
||||
ecosystem: "exec".into(),
|
||||
key: key.into(),
|
||||
entry: entry.into(),
|
||||
port: "notification.delivery".into(),
|
||||
adapter: "notification.delivery.fixture".into(),
|
||||
schema: "a19ca3e615ce5e71".into(),
|
||||
capabilities: vec!["network".into(), "file".into()],
|
||||
operations: OPERATIONS.iter().map(|name| (*name).into()).collect(),
|
||||
setting: Some(format!(
|
||||
r#"{{"mode":"{mode}","marker":"/data/lifecycle/worker"}}"#
|
||||
)),
|
||||
timeout_ms,
|
||||
}
|
||||
}
|
||||
|
||||
fn payload(value: i64) -> Encoded {
|
||||
let mut writer = Encoder::default();
|
||||
writer.begin_object().expect("payload object");
|
||||
writer.key("value").expect("payload field");
|
||||
writer.int(value).expect("payload value");
|
||||
writer.end().expect("payload end");
|
||||
writer.finish().expect("payload JSON")
|
||||
}
|
||||
|
||||
struct InstalledFixture {
|
||||
directory: temp::TemporaryDirectory,
|
||||
path: PathBuf,
|
||||
entry: &'static str,
|
||||
resources: Vec<dever_runtime::external::Resource<'static>>,
|
||||
}
|
||||
|
||||
impl InstalledFixture {
|
||||
fn prepare(&self) -> Result<(), String> {
|
||||
let digest = dever_runtime::external::bundle_digest(&self.resources);
|
||||
dever_runtime::external::prepare(self.directory.path(), &digest, &self.resources)
|
||||
}
|
||||
}
|
||||
|
||||
fn install_fixture() -> InstalledFixture {
|
||||
let directory = temp::TemporaryDirectory::new();
|
||||
fs::create_dir(directory.path().join("lifecycle")).unwrap();
|
||||
fs::create_dir(directory.path().join("config")).unwrap();
|
||||
fs::write(directory.path().join("config/setting.json"), r#"{"adapter":{"notification.delivery":{"files":{"lifecycle":{"path":"lifecycle","write":true}}}}}"#).unwrap();
|
||||
let workspace = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let mut assets = sandbox::assets(&workspace);
|
||||
assets.push(sandbox::Asset {
|
||||
path: "workers/fixture/worker".into(),
|
||||
bytes: fs::read(env!("CARGO_BIN_EXE_component-fixture")).unwrap(),
|
||||
executable: true,
|
||||
});
|
||||
assets.push(sandbox::Asset {
|
||||
path: "worker.dever-worker.json".into(),
|
||||
bytes: br#"{"format":"dever-worker-launch-v1","ecosystem":"exec","entry":"worker","executable":"workers/fixture/worker","arguments":[],"working_directory":"workers/fixture"}"#.to_vec(), executable: false,
|
||||
});
|
||||
let resources = assets
|
||||
.into_iter()
|
||||
.map(|asset| dever_runtime::external::Resource {
|
||||
path: Box::leak(asset.path.into_boxed_str()),
|
||||
sha256: Box::leak(format!("{:x}", Sha256::digest(&asset.bytes)).into_boxed_str()),
|
||||
bytes: Box::leak(asset.bytes.into_boxed_slice()),
|
||||
executable: asset.executable,
|
||||
})
|
||||
.collect();
|
||||
let path = directory.path().join("lifecycle/worker");
|
||||
InstalledFixture {
|
||||
directory,
|
||||
path,
|
||||
entry: "worker",
|
||||
resources,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user