fix(enabled): close disabled-script execution on the async paths (review)

Holistic Phase-1 review found the "a disabled script can't execute via
any path" guarantee (§4.3) held only for the sync user-route, the
execute-by-id bypass, and the trigger outbox arm — three paths still ran
disabled scripts:

- Queue arm: `list_active_queue_consumers` filtered the trigger's
  `enabled` but not the bound script's. Add `JOIN scripts … AND
  s.enabled = TRUE` so a disabled script's queue trigger stops consuming.
- Async-HTTP (202) + queued invoke() arms: `build_http_request` /
  `build_invoke_request` hardcoded `active: true`. Set it to
  `script.enabled`, and MOVE the dispatcher's fire-time `active` drop to
  after the source-kind match so it covers all three arms uniformly
  (previously trigger-only).
- `invoke()` (script-to-script): `resolve_id`/`resolve_name` checked
  cross-app isolation but not `enabled`. A disabled target now resolves
  to NotFound (indistinguishable from absent), matching the data plane.

Also (review LOW/§4.7):
- The route-on/script-off 404 returned `NotFound(script_id)`, leaking the
  internal id and distinguishable from absent. Return the same flat "no
  route matches" 404 as the unmatched case.
- Add the §4.7 "enabled endpoint with no route and no trigger" reachability
  warning (was unimplemented; only disabled-target shipped).

Tested: manager-core lib 368 + orchestrator 75 + 22 project-tool journeys
(incl. a new enabled-route→disabled-script flat-404 e2e) green; clippy
-D warnings clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-23 21:59:50 +02:00
parent 5e62f4acfe
commit b3f05dfe2a
6 changed files with 155 additions and 24 deletions

View File

@@ -125,6 +125,55 @@ fn disabling_a_route_makes_it_404_then_reenable() {
);
}
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn enabled_route_to_disabled_script_404s_flatly() {
let Some(fx) = common::fixture_or_skip() else {
return;
};
let env = common::admin_env(fx);
let slug = common::unique_slug("enbl-os");
common::pic_as(&env)
.args(["apps", "create", &slug])
.assert()
.success();
let _guard = AppGuard::new(&env.url, &env.token, &slug);
let host = format!("{slug}.test");
common::pic_as(&env)
.args(["apps", "domains", "add", &slug, &host])
.assert()
.success();
let dir = TempDir::new().unwrap();
fs::create_dir_all(dir.path().join("scripts")).unwrap();
fs::write(dir.path().join("scripts/hello.rhai"), "\"hi\"").unwrap();
let manifest_path = dir.path().join("picloud.toml");
// Route stays enabled; the SCRIPT it binds is disabled (route-on/script-off).
fs::write(
&manifest_path,
format!(
"[app]\nslug = \"{slug}\"\nname = \"OS\"\n\n\
[[scripts]]\nname = \"hello\"\nfile = \"scripts/hello.rhai\"\nenabled = false\n\n\
[[routes]]\nscript = \"hello\"\nmethod = \"GET\"\n\
host_kind = \"any\"\npath_kind = \"exact\"\npath = \"/hello\"\n"
),
)
.unwrap();
apply(&env, &manifest_path);
// 404, and the body must be the flat "no route matches" form — never the
// internal script id (no info leak; indistinguishable from absent).
let client = reqwest::blocking::Client::new();
let resp = client
.get(format!("{}/hello", env.url))
.header(reqwest::header::HOST, &host)
.send()
.unwrap();
assert_eq!(resp.status().as_u16(), 404);
let body = resp.text().unwrap();
assert!(body.contains("no route matches"), "flat 404 body: {body}");
}
fn apply(env: &common::TestEnv, manifest_path: &Path) {
common::pic_as(env)
.args(["apply", "--file"])