feat(cli): the operator config surface — identity and output defaults (RFC-007 PR 1)

~/.omnigraph/config.yaml joins the resolution chains as the operator
surface: operator.actor becomes the last hop of THE actor chain (--as >
legacy cli.actor during the RFC-008 window > operator.actor > none, one
implementation for direct-engine and cluster commands alike) and
defaults.output joins the read-format cascade below every more-specific
source. Discovery honors $OMNIGRAPH_HOME (tilde-expanded, #139 finding 9);
an absent file is an empty layer; unknown keys WARN and load (a file
written for later slices must not break this CLI); malformed YAML is a
loud error. The module is CLI-only — the server never reads operator
config (invariant 11 by construction).

$OMNIGRAPH_CONFIG becomes a first-class stand-in for --config in
load_config (flag > env > ./omnigraph.yaml), one meaning in both binaries.

The test harness pins hermeticity: spawned binaries get a nonexistent
OMNIGRAPH_HOME by default so no test ever reads the developer's real
operator config. New coverage: loader unit tests, the env-precedence
matrix on load_config_in, and spawned-binary e2es for the actor chain
(operator wins with no flag/legacy key; legacy outranks it; --as wins) and
the format cascade.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
aaltshuler 2026-06-11 20:29:02 +03:00
parent 08ce8dc34d
commit be4bd46212
7 changed files with 445 additions and 36 deletions

View file

@ -726,6 +726,73 @@ fn cluster_apply_uses_cli_actor_from_local_config() {
assert_eq!(apply(&["--as", "andrew"]), "andrew", "--as overrides cli.actor");
}
/// RFC-007 PR 1: the operator layer joins the actor chain —
/// `--as` > legacy `cli.actor` (RFC-008 window) > `operator.actor` > none.
#[test]
fn cluster_apply_uses_operator_actor_from_omnigraph_home() {
let temp = tempdir().unwrap();
write_cluster_config_fixture(temp.path());
let operator_home = tempdir().unwrap();
fs::write(
operator_home.path().join("config.yaml"),
"operator:\n actor: act-operator\n",
)
.unwrap();
let output = cli()
.current_dir(temp.path())
.env("OMNIGRAPH_HOME", operator_home.path())
.arg("cluster")
.arg("import")
.arg("--config")
.arg(temp.path())
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
let apply = |extra: &[&str]| {
let mut command = cli();
command
.current_dir(temp.path())
.env("OMNIGRAPH_HOME", operator_home.path());
for arg in extra {
command.arg(arg);
}
let output = command
.arg("cluster")
.arg("apply")
.arg("--config")
.arg(temp.path())
.arg("--json")
.output()
.unwrap();
let json: serde_json::Value =
serde_json::from_str(String::from_utf8_lossy(&output.stdout).trim()).unwrap();
json["actor"].clone()
};
// No --as, no omnigraph.yaml: the operator identity applies.
assert_eq!(
apply(&[]),
"act-operator",
"operator.actor is the no-flag, no-legacy-config default"
);
// --as still wins over everything.
assert_eq!(apply(&["--as", "andrew"]), "andrew");
// A legacy cli.actor (RFC-008 window) outranks the operator layer.
fs::write(
temp.path().join("omnigraph.yaml"),
"cli:\n actor: act-legacy\n",
)
.unwrap();
assert_eq!(
apply(&[]),
"act-legacy",
"legacy cli.actor wins over operator.actor during the deprecation window"
);
}
#[test]
fn cluster_approve_uses_cli_actor_fallback() {
let temp = tempdir().unwrap();