From 9e73cde0a9367bab8fa89c219e917dbbece08e67 Mon Sep 17 00:00:00 2001 From: fylorn <249551762+fylorn@users.noreply.github.com> Date: Fri, 2 Oct 2026 20:15:46 +0800 Subject: [PATCH] Test the plugin control plane against the real sandbox The control-plane tests use the fake engine, which reads a one-line JSON manifest and runs nothing. One test now takes a real JavaScript plugin through the tw-plugin sandbox: inspect (manifest, scope) and a syntax error with its line, install (status ok, ready to run), the file edited on disk (status changed), and approval of exactly the reviewed bytes (status ok again, the new default shown). Co-Authored-By: Claude Opus 5.5 --- crates/tw-control/tests/plugins.rs | 75 +++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/crates/tw-control/tests/plugins.rs b/crates/tw-control/tests/plugins.rs index df7575d..aa61ece 100644 --- a/crates/tw-control/tests/plugins.rs +++ b/crates/tw-control/tests/plugins.rs @@ -82,8 +82,12 @@ impl Bed { } } -/// 一张床:配置文件在 `dir`(相对临时目录的一段路径)里 +/// 一张床:配置文件在 `dir`(相对临时目录的一段路径)里,引擎是假的 fn bed_in(sub: &str) -> Bed { + bed_with(sub, true) +} + +fn bed_with(sub: &str, fake: bool) -> Bed { let tmp = tempfile::tempdir().unwrap(); let dir = tmp.path().join(sub); std::fs::create_dir_all(&dir).unwrap(); @@ -97,7 +101,9 @@ fn bed_in(sub: &str) -> Bed { ); let store = Arc::new(tokio::sync::Mutex::new(rec)); let gw = tw_gateway::AppState::new(tw_config::try_parse(BASE).unwrap()).unwrap(); - gw.set_plugin_engine(Arc::new(FakeEngine)); + if fake { + gw.set_plugin_engine(Arc::new(FakeEngine)); + } let bus = gw.bus.clone(); let state = ControlState { shutdown: Default::default(), @@ -1079,3 +1085,68 @@ async fn a_plugin_changed_while_core_was_down_starts_out_changed() { let p = gw.runtime().plugins.get(&id).unwrap().clone(); assert_eq!(p.broken(), Some(&tw_gateway::plugin::Broken::Changed)); } + +/// 真的沙箱:从源码到装上、文件被改、批准,整条路走一遍(不跑钩子,那是数据面的事) +#[tokio::test] +async fn a_real_plugin_goes_through_the_sandbox_from_source_to_approval() { + let b = bed_with("real", false); + let src = r#"export const manifest = { + name: "Add date", + api: 1, + permissions: ["system"], + match: { models: ["claude-*"] }, + settings: { note: { type: "string", label: "Note", default: "today" } }, +}; +export function onRequest(req, ctx) { + return { ...req, system: `${req.system} ${ctx.settings.note}` }; +} +"#; + let (st, v) = call( + &b.app, + "POST", + "/plugins/inspect", + Some(json!({"source": src})), + ) + .await; + assert_eq!(st, StatusCode::OK, "{v}"); + assert!(v["error"].is_null(), "{v}"); + assert_eq!(v["manifest"]["name"], "Add date"); + assert_eq!(v["manifest"]["scope"]["models"], json!(["claude-*"])); + + let broken = src.replace("return {", "return {{"); + let (_, v) = call( + &b.app, + "POST", + "/plugins/inspect", + Some(json!({"source": broken})), + ) + .await; + assert!(v["manifest"].is_null(), "{v}"); + assert!(v["error"]["line"].is_number(), "{v}"); + + let id = b + .install( + src, + json!({"scope": {"clients": [], "models": ["claude-*"], "upstreams": []}}), + ) + .await; + assert_eq!(id, "add-date"); + assert_eq!(b.plugin(&id).await["status"], json!({"kind": "ok"})); + assert!(b.gw.runtime().plugins.get(&id).unwrap().ready().is_some()); + + let edited = src.replace("today", "tomorrow"); + std::fs::write(b.file(&id), &edited).unwrap(); + b.gw.reload_plugins(); + assert_eq!(b.plugin(&id).await["status"], json!({"kind": "changed"})); + let (st, v) = call( + &b.app, + "POST", + &format!("/plugins/{id}/approve"), + Some(json!({"sha256": sha(&edited)})), + ) + .await; + assert_eq!(st, StatusCode::OK, "{v}"); + let v = b.plugin(&id).await; + assert_eq!(v["status"], json!({"kind": "ok"})); + assert_eq!(v["settings_schema"][0]["default"], "tomorrow"); +}