Le first-run wizard restait figé : `CliAgentRuntime::detect` construisait un runtime tokio courant-thread via `futures_block_on` pour piloter un `ProcessSpawner` async. Appelé depuis le runtime async de Tauri, ce `block_on` imbriqué panique ; la commande IPC ne répond alors jamais, la promesse `detectProfiles` reste pendante et `busy` ne redescend plus. Ce commit s'attaque à la cause côté backend : - `AgentRuntime::detect` devient `async fn` (`#[async_trait]`) : le port cesse de mentir sur sa nature. Il pilote un spawner async, il est async. Le `futures_block_on` disparaît, et avec lui le runtime imbriqué. - La sonde CLI est bornée par `tokio::time::timeout(DETECTION_TIMEOUT)` : un binaire qui ne rend jamais la main dégrade la détection en `Err`, il ne gèle plus l'appelant. - Les profils `StructuredAdapter::OpenAiCompatible` n'ont pas de CLI à spawner : les sonder revenait à tester un binaire inexistant. Ils sont désormais sondés par un GET HTTP sur l'endpoint `/models` dérivé de `chat_http.endpoint`, borné par le même timeout. `DetectProfiles` séquence les `await` sur les candidats. Les fakes `AgentRuntime` des crates application/infrastructure/app-tauri suivent la nouvelle signature. Tests: cargo test -p domain (241), -p application (81), -p infrastructure (269), -p app-tauri (63+15) — verts. `rg futures_block_on crates` sans match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
585 lines
18 KiB
Rust
585 lines
18 KiB
Rust
//! L5 tests for [`CliAgentRuntime`].
|
|
//!
|
|
//! Covers:
|
|
//! - `prepare_invocation` (the **pure** core): for every [`ContextInjection`]
|
|
//! strategy, the produced [`SpawnSpec`] (command, args order, resolved cwd,
|
|
//! `context_plan`) is asserted.
|
|
//! - `detection_spec` (pure): custom `detect` tokenisation vs `--version`
|
|
//! fallback.
|
|
//! - `detect` driven by a **mocked** [`ProcessSpawner`]: exit 0 ⇒ `true`,
|
|
//! non-zero ⇒ `false`, spawner error ⇒ propagated as `RuntimeError`.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use async_trait::async_trait;
|
|
|
|
use domain::ids::ProfileId;
|
|
use domain::ports::{
|
|
AgentRuntime, ContextInjectionPlan, ExitStatus, Output, PreparedContext, ProcessError,
|
|
ProcessSpawner, RuntimeError, SessionPlan, SpawnSpec,
|
|
};
|
|
use domain::profile::{
|
|
AgentProfile, ContextInjection, HttpChatConfig, SessionStrategy, StructuredAdapter,
|
|
};
|
|
use domain::project::ProjectPath;
|
|
use domain::MarkdownDoc;
|
|
use infrastructure::CliAgentRuntime;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn profile(injection: ContextInjection, cwd_template: &str) -> AgentProfile {
|
|
AgentProfile::new(
|
|
ProfileId::from_uuid(uuid::Uuid::from_u128(1)),
|
|
"Test",
|
|
"mycli",
|
|
vec!["--static".to_owned(), "arg".to_owned()],
|
|
injection,
|
|
Some("mycli probe --json".to_owned()),
|
|
cwd_template,
|
|
None,
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
fn ctx() -> PreparedContext {
|
|
PreparedContext {
|
|
content: MarkdownDoc::new("# hi"),
|
|
relative_path: ".ideai/agent.md".to_owned(),
|
|
project_root: "/repo".to_owned(),
|
|
}
|
|
}
|
|
|
|
/// A [`ProcessSpawner`] that returns a fixed outcome regardless of the spec.
|
|
struct FixedSpawner(Result<Output, ProcessError>);
|
|
|
|
#[async_trait]
|
|
impl ProcessSpawner for FixedSpawner {
|
|
async fn run(&self, _spec: SpawnSpec) -> Result<Output, ProcessError> {
|
|
self.0.clone()
|
|
}
|
|
}
|
|
|
|
fn runtime_with(outcome: Result<Output, ProcessError>) -> CliAgentRuntime {
|
|
CliAgentRuntime::new(Arc::new(FixedSpawner(outcome)))
|
|
}
|
|
|
|
/// A spawner that just records the spec it was handed (for detect-spec assertions).
|
|
struct RecordingSpawner(std::sync::Mutex<Option<SpawnSpec>>);
|
|
|
|
#[async_trait]
|
|
impl ProcessSpawner for RecordingSpawner {
|
|
async fn run(&self, spec: SpawnSpec) -> Result<Output, ProcessError> {
|
|
*self.0.lock().unwrap() = Some(spec);
|
|
Ok(Output {
|
|
status: ExitStatus { code: Some(0) },
|
|
stdout: Vec::new(),
|
|
stderr: Vec::new(),
|
|
})
|
|
}
|
|
}
|
|
|
|
fn pure_runtime() -> CliAgentRuntime {
|
|
runtime_with(Ok(Output {
|
|
status: ExitStatus { code: Some(0) },
|
|
stdout: Vec::new(),
|
|
stderr: Vec::new(),
|
|
}))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// prepare_invocation — ConventionFile
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn prepare_convention_file_keeps_args_and_plans_file() {
|
|
let rt = pure_runtime();
|
|
let p = profile(
|
|
ContextInjection::convention_file("CLAUDE.md").unwrap(),
|
|
"{projectRoot}",
|
|
);
|
|
let root = ProjectPath::new("/home/me/proj").unwrap();
|
|
|
|
let spec = rt
|
|
.prepare_invocation(&p, &ctx(), &root, &SessionPlan::None)
|
|
.unwrap();
|
|
|
|
assert_eq!(spec.command, "mycli");
|
|
assert_eq!(spec.args, vec!["--static", "arg"], "args unchanged");
|
|
assert_eq!(spec.cwd.as_str(), "/home/me/proj");
|
|
assert_eq!(
|
|
spec.context_plan,
|
|
Some(ContextInjectionPlan::File {
|
|
target: "CLAUDE.md".to_owned()
|
|
})
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// prepare_invocation — Flag with {path}
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn prepare_flag_with_path_substitutes_and_splits() {
|
|
let rt = pure_runtime();
|
|
let p = profile(
|
|
ContextInjection::flag("--context-file {path}").unwrap(),
|
|
"{projectRoot}",
|
|
);
|
|
let root = ProjectPath::new("/p").unwrap();
|
|
|
|
let spec = rt
|
|
.prepare_invocation(&p, &ctx(), &root, &SessionPlan::None)
|
|
.unwrap();
|
|
|
|
// static args first, then the substituted+split flag args.
|
|
assert_eq!(
|
|
spec.args,
|
|
vec!["--static", "arg", "--context-file", ".ideai/agent.md"]
|
|
);
|
|
assert_eq!(
|
|
spec.context_plan,
|
|
Some(ContextInjectionPlan::Args {
|
|
args: vec!["--context-file".to_owned(), ".ideai/agent.md".to_owned()]
|
|
})
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// prepare_invocation — Flag without {path} (switch + path)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn prepare_flag_without_path_is_switch_then_path() {
|
|
let rt = pure_runtime();
|
|
let p = profile(ContextInjection::flag("-f").unwrap(), "{projectRoot}");
|
|
let root = ProjectPath::new("/p").unwrap();
|
|
|
|
let spec = rt
|
|
.prepare_invocation(&p, &ctx(), &root, &SessionPlan::None)
|
|
.unwrap();
|
|
|
|
assert_eq!(spec.args, vec!["--static", "arg", "-f", ".ideai/agent.md"]);
|
|
assert_eq!(
|
|
spec.context_plan,
|
|
Some(ContextInjectionPlan::Args {
|
|
args: vec!["-f".to_owned(), ".ideai/agent.md".to_owned()]
|
|
})
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// prepare_invocation — Stdin
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn prepare_stdin_keeps_args_and_plans_stdin() {
|
|
let rt = pure_runtime();
|
|
let p = profile(ContextInjection::stdin(), "{projectRoot}");
|
|
let root = ProjectPath::new("/p").unwrap();
|
|
|
|
let spec = rt
|
|
.prepare_invocation(&p, &ctx(), &root, &SessionPlan::None)
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
spec.args,
|
|
vec!["--static", "arg"],
|
|
"args unchanged for stdin"
|
|
);
|
|
assert_eq!(spec.context_plan, Some(ContextInjectionPlan::Stdin));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// prepare_invocation — Env
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn prepare_env_keeps_args_and_plans_env() {
|
|
let rt = pure_runtime();
|
|
let p = profile(
|
|
ContextInjection::env("AGENT_CONTEXT").unwrap(),
|
|
"{projectRoot}",
|
|
);
|
|
let root = ProjectPath::new("/p").unwrap();
|
|
|
|
let spec = rt
|
|
.prepare_invocation(&p, &ctx(), &root, &SessionPlan::None)
|
|
.unwrap();
|
|
|
|
assert_eq!(spec.args, vec!["--static", "arg"], "args unchanged for env");
|
|
assert_eq!(
|
|
spec.context_plan,
|
|
Some(ContextInjectionPlan::Env {
|
|
var: "AGENT_CONTEXT".to_owned()
|
|
})
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// prepare_invocation — cwd template substitution
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn prepare_substitutes_project_root_in_cwd_template() {
|
|
let rt = pure_runtime();
|
|
let p = profile(ContextInjection::stdin(), "{projectRoot}/subdir");
|
|
let root = ProjectPath::new("/home/me/proj").unwrap();
|
|
|
|
let spec = rt
|
|
.prepare_invocation(&p, &ctx(), &root, &SessionPlan::None)
|
|
.unwrap();
|
|
assert_eq!(spec.cwd.as_str(), "/home/me/proj/subdir");
|
|
}
|
|
|
|
#[test]
|
|
fn prepare_empty_cwd_template_defaults_to_base() {
|
|
let rt = pure_runtime();
|
|
let p = profile(ContextInjection::stdin(), "");
|
|
let base = ProjectPath::new("/home/me/proj").unwrap();
|
|
|
|
let spec = rt
|
|
.prepare_invocation(&p, &ctx(), &base, &SessionPlan::None)
|
|
.unwrap();
|
|
assert_eq!(spec.cwd.as_str(), "/home/me/proj");
|
|
}
|
|
|
|
#[test]
|
|
fn prepare_substitutes_agent_run_dir_in_cwd_template() {
|
|
// The canonical template (ARCHITECTURE §14.1): `{agentRunDir}` resolves to the
|
|
// base cwd the launcher passes — the agent's isolated run directory.
|
|
let rt = pure_runtime();
|
|
let p = profile(
|
|
ContextInjection::convention_file("CLAUDE.md").unwrap(),
|
|
"{agentRunDir}",
|
|
);
|
|
let run_dir = ProjectPath::new("/home/me/proj/.ideai/run/agent-1").unwrap();
|
|
|
|
let spec = rt
|
|
.prepare_invocation(&p, &ctx(), &run_dir, &SessionPlan::None)
|
|
.unwrap();
|
|
assert_eq!(spec.cwd.as_str(), "/home/me/proj/.ideai/run/agent-1");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// detection_spec (pure)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn detection_spec_uses_custom_detect_tokenised() {
|
|
let p = profile(ContextInjection::stdin(), "{projectRoot}");
|
|
let spec = CliAgentRuntime::detection_spec(&p).unwrap();
|
|
assert_eq!(spec.command, "mycli");
|
|
assert_eq!(spec.args, vec!["probe", "--json"]);
|
|
assert!(spec.context_plan.is_none());
|
|
assert!(spec.env.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn detection_spec_falls_back_to_command_version() {
|
|
let p = AgentProfile::new(
|
|
ProfileId::from_uuid(uuid::Uuid::from_u128(2)),
|
|
"NoDetect",
|
|
"somecli",
|
|
Vec::new(),
|
|
ContextInjection::stdin(),
|
|
None,
|
|
"{projectRoot}",
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
let spec = CliAgentRuntime::detection_spec(&p).unwrap();
|
|
assert_eq!(spec.command, "somecli");
|
|
assert_eq!(spec.args, vec!["--version"]);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// detect (mocked spawner)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[tokio::test]
|
|
async fn detect_true_on_exit_zero() {
|
|
let rt = runtime_with(Ok(Output {
|
|
status: ExitStatus { code: Some(0) },
|
|
stdout: Vec::new(),
|
|
stderr: Vec::new(),
|
|
}));
|
|
let p = profile(ContextInjection::stdin(), "{projectRoot}");
|
|
assert!(rt.detect(&p).await.unwrap());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn detect_false_on_nonzero_exit() {
|
|
let rt = runtime_with(Ok(Output {
|
|
status: ExitStatus { code: Some(127) },
|
|
stdout: Vec::new(),
|
|
stderr: Vec::new(),
|
|
}));
|
|
let p = profile(ContextInjection::stdin(), "{projectRoot}");
|
|
assert!(!rt.detect(&p).await.unwrap());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn detect_false_on_signal_terminated() {
|
|
let rt = runtime_with(Ok(Output {
|
|
status: ExitStatus { code: None },
|
|
stdout: Vec::new(),
|
|
stderr: Vec::new(),
|
|
}));
|
|
let p = profile(ContextInjection::stdin(), "{projectRoot}");
|
|
assert!(!rt.detect(&p).await.unwrap());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn detect_propagates_spawner_error() {
|
|
let rt = runtime_with(Err(ProcessError::Spawn("no such file".to_owned())));
|
|
let p = profile(ContextInjection::stdin(), "{projectRoot}");
|
|
let err = rt.detect(&p).await.expect_err("spawner error surfaces");
|
|
assert!(matches!(err, RuntimeError::Detection(_)), "got {err:?}");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn detect_runs_the_detection_spec_command() {
|
|
let recorder = Arc::new(RecordingSpawner(std::sync::Mutex::new(None)));
|
|
let rt = CliAgentRuntime::new(recorder.clone());
|
|
let p = profile(ContextInjection::stdin(), "{projectRoot}");
|
|
|
|
rt.detect(&p).await.unwrap();
|
|
|
|
let spec = recorder.0.lock().unwrap().clone().expect("spec recorded");
|
|
assert_eq!(spec.command, "mycli");
|
|
assert_eq!(spec.args, vec!["probe", "--json"]);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn detect_openai_compatible_uses_http_endpoint_not_cli_spawner() {
|
|
let recorder = Arc::new(RecordingSpawner(std::sync::Mutex::new(None)));
|
|
let rt = CliAgentRuntime::new(recorder.clone());
|
|
let p = profile(ContextInjection::stdin(), "{projectRoot}")
|
|
.with_structured_adapter(StructuredAdapter::OpenAiCompatible)
|
|
.with_chat_http(
|
|
HttpChatConfig::new(
|
|
"http://127.0.0.1:1/v1",
|
|
"model",
|
|
None,
|
|
Some(1000),
|
|
Some(1000),
|
|
None,
|
|
)
|
|
.unwrap(),
|
|
);
|
|
|
|
assert!(!rt.detect(&p).await.unwrap());
|
|
assert!(
|
|
recorder.0.lock().unwrap().is_none(),
|
|
"OpenAI-compatible detection must not spawn the profile command"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// prepare_invocation — session args (T3 truth table)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Like [`profile`] but carries a [`SessionStrategy`]. `Stdin` injection keeps
|
|
/// the context out of the args so session args are the *only* trailing tokens.
|
|
fn profile_with_session(session: Option<SessionStrategy>) -> AgentProfile {
|
|
AgentProfile::new(
|
|
ProfileId::from_uuid(uuid::Uuid::from_u128(7)),
|
|
"Test",
|
|
"mycli",
|
|
vec!["--static".to_owned(), "arg".to_owned()],
|
|
ContextInjection::stdin(),
|
|
Some("mycli probe --json".to_owned()),
|
|
"{agentRunDir}",
|
|
session,
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
fn root() -> ProjectPath {
|
|
ProjectPath::new("/p").unwrap()
|
|
}
|
|
|
|
// Row 1: profile.session == None ⇒ no args added, whatever the plan.
|
|
#[test]
|
|
fn session_none_profile_adds_nothing_for_any_plan() {
|
|
let rt = pure_runtime();
|
|
let p = profile_with_session(None);
|
|
|
|
for plan in [
|
|
SessionPlan::None,
|
|
SessionPlan::Assign {
|
|
conversation_id: "id-1".to_owned(),
|
|
},
|
|
SessionPlan::Resume {
|
|
conversation_id: "id-1".to_owned(),
|
|
},
|
|
] {
|
|
let spec = rt.prepare_invocation(&p, &ctx(), &root(), &plan).unwrap();
|
|
assert_eq!(spec.args, vec!["--static", "arg"], "plan = {plan:?}");
|
|
}
|
|
}
|
|
|
|
// Row 2: Some{assign_flag: Some(f)} + Assign{id} ⇒ [f, id].
|
|
#[test]
|
|
fn session_assign_with_flag_emits_flag_and_id() {
|
|
let rt = pure_runtime();
|
|
let p = profile_with_session(Some(
|
|
SessionStrategy::new(Some("--session-id".to_owned()), "--resume").unwrap(),
|
|
));
|
|
|
|
let spec = rt
|
|
.prepare_invocation(
|
|
&p,
|
|
&ctx(),
|
|
&root(),
|
|
&SessionPlan::Assign {
|
|
conversation_id: "abc".to_owned(),
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(spec.args, vec!["--static", "arg", "--session-id", "abc"]);
|
|
}
|
|
|
|
// Row 3: Some{resume_flag: r, assign_flag: Some} + Resume{id} ⇒ [r, id].
|
|
#[test]
|
|
fn session_resume_with_flag_emits_resume_and_id() {
|
|
let rt = pure_runtime();
|
|
let p = profile_with_session(Some(
|
|
SessionStrategy::new(Some("--session-id".to_owned()), "--resume").unwrap(),
|
|
));
|
|
|
|
let spec = rt
|
|
.prepare_invocation(
|
|
&p,
|
|
&ctx(),
|
|
&root(),
|
|
&SessionPlan::Resume {
|
|
conversation_id: "abc".to_owned(),
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(spec.args, vec!["--static", "arg", "--resume", "abc"]);
|
|
}
|
|
|
|
// Row 4: Some{assign_flag: None, resume_flag: r} + Resume{id} ⇒ [r] (degraded).
|
|
#[test]
|
|
fn session_resume_without_assign_flag_emits_resume_only() {
|
|
let rt = pure_runtime();
|
|
let p = profile_with_session(Some(SessionStrategy::new(None, "--continue").unwrap()));
|
|
|
|
let spec = rt
|
|
.prepare_invocation(
|
|
&p,
|
|
&ctx(),
|
|
&root(),
|
|
&SessionPlan::Resume {
|
|
conversation_id: "abc".to_owned(),
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(spec.args, vec!["--static", "arg", "--continue"]);
|
|
}
|
|
|
|
// Row 5: Some{..} + SessionPlan::None ⇒ nothing added.
|
|
#[test]
|
|
fn session_plan_none_with_strategy_adds_nothing() {
|
|
let rt = pure_runtime();
|
|
let p = profile_with_session(Some(
|
|
SessionStrategy::new(Some("--session-id".to_owned()), "--resume").unwrap(),
|
|
));
|
|
|
|
let spec = rt
|
|
.prepare_invocation(&p, &ctx(), &root(), &SessionPlan::None)
|
|
.unwrap();
|
|
|
|
assert_eq!(spec.args, vec!["--static", "arg"]);
|
|
}
|
|
|
|
// Row 6: Some{assign_flag: None} + Assign{id} ⇒ nothing (no assign possible).
|
|
#[test]
|
|
fn session_assign_without_flag_adds_nothing() {
|
|
let rt = pure_runtime();
|
|
let p = profile_with_session(Some(SessionStrategy::new(None, "--continue").unwrap()));
|
|
|
|
let spec = rt
|
|
.prepare_invocation(
|
|
&p,
|
|
&ctx(),
|
|
&root(),
|
|
&SessionPlan::Assign {
|
|
conversation_id: "abc".to_owned(),
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(spec.args, vec!["--static", "arg"]);
|
|
}
|
|
|
|
// Non-regression: a profile WITHOUT session + SessionPlan::None yields the exact
|
|
// same args as before T3 (the existing strategy tests already assert these; here
|
|
// we pin it against an Assign/Resume plan too — still nothing added).
|
|
#[test]
|
|
fn no_session_profile_is_unaffected_by_any_plan() {
|
|
let rt = pure_runtime();
|
|
let p = profile(ContextInjection::stdin(), "{agentRunDir}");
|
|
|
|
for plan in [
|
|
SessionPlan::None,
|
|
SessionPlan::Assign {
|
|
conversation_id: "x".to_owned(),
|
|
},
|
|
SessionPlan::Resume {
|
|
conversation_id: "x".to_owned(),
|
|
},
|
|
] {
|
|
let spec = rt.prepare_invocation(&p, &ctx(), &root(), &plan).unwrap();
|
|
assert_eq!(spec.args, vec!["--static", "arg"], "plan = {plan:?}");
|
|
}
|
|
}
|
|
|
|
// Ordering: session args land *after* the context-injection (Flag) args.
|
|
#[test]
|
|
fn session_args_come_after_context_injection_args() {
|
|
let rt = pure_runtime();
|
|
let p = AgentProfile::new(
|
|
ProfileId::from_uuid(uuid::Uuid::from_u128(8)),
|
|
"Test",
|
|
"mycli",
|
|
vec!["--static".to_owned()],
|
|
ContextInjection::flag("--context-file {path}").unwrap(),
|
|
Some("mycli probe".to_owned()),
|
|
"{agentRunDir}",
|
|
Some(SessionStrategy::new(Some("--session-id".to_owned()), "--resume").unwrap()),
|
|
)
|
|
.unwrap();
|
|
|
|
let spec = rt
|
|
.prepare_invocation(
|
|
&p,
|
|
&ctx(),
|
|
&root(),
|
|
&SessionPlan::Assign {
|
|
conversation_id: "abc".to_owned(),
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
// static arg, then context-injection args, then session args — in that order.
|
|
assert_eq!(
|
|
spec.args,
|
|
vec![
|
|
"--static",
|
|
"--context-file",
|
|
".ideai/agent.md",
|
|
"--session-id",
|
|
"abc"
|
|
]
|
|
);
|
|
}
|