use super::*;
use crate::{
providers::codex_responses_body,
skills::{SkillDiscovery, load_skill_descriptor},
};
use std::{collections::BTreeMap, path::PathBuf};
const RULE: &str = "RULE_NEAR_END: validate the observable workflow, preserve durable raw output, and never substitute stale skill instructions at a continuation boundary.";
const MARKER: &str = "[Unchanged skill body:";
fn skill_fixture(temp: &tempfile::TempDir, body: &str) -> (PathBuf, SkillDiscovery) {
let dir = temp.path().join("skills/demo");
std::fs::create_dir_all(dir.join("refs")).unwrap();
let path = dir.join("SKILL.md");
std::fs::write(&path, body).unwrap();
let skill = load_skill_descriptor(&path).unwrap();
(
path,
SkillDiscovery {
skills: BTreeMap::from([("demo".to_string(), skill)]),
diagnostics: Vec::new(),
},
)
}
fn skill_runtime(temp: &tempfile::TempDir, discovery: &SkillDiscovery) -> ToolRuntime {
ToolRuntime::new(temp.path())
.unwrap()
.with_skills(discovery)
}
fn read_skill(id: &str, target: &str) -> ProviderEvent {
tool_call(id, "read", json!({"paths":[target]}))
}
fn codex_input(request: &ProviderRequest) -> String {
serde_json::to_string(&codex_responses_body("model", request)["input"]).unwrap()
}
fn raw_tool_outputs(session: &crate::sessions::Session) -> Vec<(String, String)> {
session
.read_events()
.unwrap()
.into_iter()
.filter(|event| event.event_type == "tool_result")
.map(|event| {
(
event.payload["call_id"].as_str().unwrap().to_string(),
event.payload["result"]["content"]
.as_str()
.unwrap()
.to_string(),
)
})
.collect()
}
fn request_with_raw_skill_outputs(
request: &ProviderRequest,
raw_by_id: &BTreeMap<String, String>,
) -> ProviderRequest {
let mut items = request.conversation_items().into_owned();
for item in &mut items {
if let ProviderConversationItem::ToolResult(result) = item {
if let Some(raw) = raw_by_id.get(&result.call_id) {
result.output.clone_from(raw);
}
result.skill_reads.clear();
}
}
let rebuilt = ProviderRequest::from_conversation("model", items);
match codex_responses_body("model", request)["prompt_cache_key"].as_str() {
Some(key) => rebuilt.with_prompt_cache_key(key),
None => rebuilt,
}
}
fn assert_same_codex_request_configuration(left: &ProviderRequest, right: &ProviderRequest) {
let mut left = codex_responses_body("model", left);
let mut right = codex_responses_body("model", right);
left.as_object_mut().unwrap().remove("input");
right.as_object_mut().unwrap().remove("input");
assert_eq!(left, right);
}
#[test]
fn repeated_skill_read_is_projected_once_but_remains_full_in_sink_and_session() {
let temp = tempfile::TempDir::new().unwrap();
let skill_text = format!(
"---\nname: demo\ndescription: regression fixture\n---\n# Demo\n{}\n{RULE}\n",
"workflow evidence ".repeat(30)
);
let (_, discovery) = skill_fixture(&temp, &skill_text);
std::fs::write(temp.path().join("ordinary.txt"), "ordinary work").unwrap();
let tools = skill_runtime(&temp, &discovery);
let session = crate::sessions::SessionManager::new(temp.path().join("sessions"))
.create()
.unwrap();
let provider = ScriptedProvider::new(vec![
vec![read_skill("skill_first", "skill://demo"), done()],
vec![read_skill("ordinary", "ordinary.txt:raw"), done()],
vec![read_skill("skill_again", "skill://demo"), done()],
text_done("done"),
]);
let agent = AgentSession::new("model", &[], &discovery);
let mut sink = CapturingOutputSink::default();
let output = agent
.run_print_with_tools_streaming_output(
&provider,
"read, do ordinary work, then reread",
Some(&tools),
Some(&session),
temp.path(),
Some(&mut sink),
)
.unwrap();
assert!(output.usage.is_none(), "scripted provider reports no usage");
let requests = provider.requests();
let last = requests.last().unwrap();
let projected = codex_input(last);
assert_eq!(projected.matches(RULE).count(), 1);
assert!(projected.contains(MARKER));
assert!(projected.contains("RULE_NEAR_END"));
let sink_skill_reads = sink
.outputs
.iter()
.filter(|event| matches!(event, OutputEvent::ToolResult { result, .. } if result.content.contains(RULE)))
.count();
assert_eq!(sink_skill_reads, 2);
let durable = raw_tool_outputs(&session);
assert_eq!(
durable.len(),
3,
"safe replay input is the durable tool-result count"
);
assert_eq!(
durable
.iter()
.filter(|(_, output)| output.contains(RULE))
.count(),
2
);
let raw_by_id = durable.into_iter().collect::<BTreeMap<_, _>>();
let baseline = request_with_raw_skill_outputs(last, &raw_by_id);
assert_same_codex_request_configuration(&baseline, last);
let before_body = serde_json::to_vec(&codex_responses_body("model", &baseline)).unwrap();
let after_body = serde_json::to_vec(&codex_responses_body("model", last)).unwrap();
let before_tokens =
project_provider_request_input_tokens(crate::providers::OPENAI_CODEX_PROVIDER, &baseline);
let after_tokens =
project_provider_request_input_tokens(crate::providers::OPENAI_CODEX_PROVIDER, last);
let provenance_bytes: usize = last
.tool_results()
.iter()
.map(|result| serde_json::to_vec(&result.skill_reads).unwrap().len())
.sum();
let safe_skill_read_count = sink_skill_reads;
eprintln!(
"skill dedup measurement: serialized_codex_bytes before={} after={}; local_projected_tokens before={} after={}; safe_skill_read_count={}; durable_tool_results={}; provenance_encoded_bytes={}; recorded_provider_usage=not_reported",
before_body.len(),
after_body.len(),
before_tokens.tokens,
after_tokens.tokens,
safe_skill_read_count,
raw_by_id.len(),
provenance_bytes
);
assert_eq!(safe_skill_read_count, 2);
assert!(after_body.len() < before_body.len());
assert!(after_tokens.tokens < before_tokens.tokens);
assert!(provenance_bytes < 8 * 1024);
let emitted = sink
.outputs
.iter()
.rev()
.find_map(|event| match event {
OutputEvent::ContextUsage {
current_tokens,
source: ContextUsageSource::FallbackEstimate | ContextUsageSource::TokenizerEstimate,
..
} => Some(*current_tokens),
_ => None,
})
.unwrap();
assert_eq!(emitted, after_tokens.tokens);
}
#[test]
fn repeated_skill_reads_use_projected_request_for_soft_compaction_threshold() {
let temp = tempfile::TempDir::new().unwrap();
let skill_text = format!(
"---\nname: demo\ndescription: soft compaction fixture\n---\n{}\n{RULE}\n",
"large stable workflow instruction ".repeat(2_000)
);
let (_, discovery) = skill_fixture(&temp, &skill_text);
let tools = skill_runtime(&temp, &discovery);
let steps = || {
vec![
vec![
read_skill("soft_first", "skill://demo"),
read_skill("soft_again", "skill://demo"),
done(),
],
text_done("done"),
]
};
let baseline_provider = ScriptedProvider::new(steps());
let mut baseline_sink = CapturingOutputSink::default();
AgentSession::new("model", &[], &discovery)
.run_print_with_tools_streaming_output(
&baseline_provider,
"read the same large skill twice",
Some(&tools),
None,
temp.path(),
Some(&mut baseline_sink),
)
.unwrap();
let baseline_requests = baseline_provider.requests();
let projected_request = baseline_requests.last().unwrap();
let projected_tokens = project_provider_request_input_tokens(
crate::providers::OPENAI_CODEX_PROVIDER,
projected_request,
)
.tokens;
let raw_by_id = baseline_sink
.outputs
.iter()
.filter_map(|event| match event {
OutputEvent::ToolResult { call, result, .. }
if matches!(call.id.as_str(), "soft_first" | "soft_again") =>
{
Some((call.id.clone(), result.content.clone()))
}
_ => None,
})
.collect::<BTreeMap<_, _>>();
assert_eq!(raw_by_id.len(), 2);
let raw_request = request_with_raw_skill_outputs(projected_request, &raw_by_id);
assert_same_codex_request_configuration(&raw_request, projected_request);
let raw_tokens = project_provider_request_input_tokens(
crate::providers::OPENAI_CODEX_PROVIDER,
&raw_request,
)
.tokens;
assert!(projected_tokens < raw_tokens);
let soft_threshold = projected_tokens + (raw_tokens - projected_tokens) / 2;
assert!(projected_tokens < soft_threshold && soft_threshold < raw_tokens);
let provider = ScriptedProvider::new(steps());
let mut sink = CapturingOutputSink::default();
let output = AgentSession::new("model", &[], &discovery)
.run_print_with_tools_streaming_output_cancellable(
&provider,
AgentRunRequest {
tools: Some(&tools),
output_sink: Some(&mut sink),
continuation_auto_compaction_policy: Some((
soft_threshold,
format!("{soft_threshold} tokens"),
)),
..run_request("read the same large skill twice", temp.path())
},
)
.unwrap();
assert_eq!(output.text, "done");
let requests = provider.requests();
assert_eq!(requests.len(), 2);
let actual_request = &requests[1];
let actual_tokens = project_provider_request_input_tokens(
crate::providers::OPENAI_CODEX_PROVIDER,
actual_request,
)
.tokens;
assert_eq!(actual_tokens, projected_tokens);
let continuation_usage = sink
.outputs
.iter()
.find_map(|event| match event {
OutputEvent::ContextUsage {
current_tokens,
request_sequence: 1,
source:
ContextUsageSource::FallbackEstimate
| ContextUsageSource::TokenizerEstimate
| ContextUsageSource::TokenizerProjection,
..
} => Some(*current_tokens),
_ => None,
})
.unwrap();
assert_eq!(continuation_usage, actual_tokens);
}
#[test]
fn duplicate_calls_and_duplicate_multi_targets_keep_order_and_one_full_copy() {
let temp = tempfile::TempDir::new().unwrap();
let skill_text = format!(
"---\nname: demo\ndescription: batch fixture\n---\n{}\n{RULE}\n",
"batch rule ".repeat(45)
);
let (_, discovery) = skill_fixture(&temp, &skill_text);
let tools = skill_runtime(&temp, &discovery);
let provider = ScriptedProvider::new(vec![
vec![
tool_call(
"multi",
"read",
json!({"paths":["skill://demo", "skill://demo"]}),
),
read_skill("one", "skill://demo"),
read_skill("two", "skill://demo"),
done(),
],
text_done("done"),
]);
AgentSession::new("model", &[], &discovery)
.run_print_with_tools(&provider, "batch", Some(&tools), None, temp.path())
.unwrap();
let input = codex_responses_body("model", provider.requests().last().unwrap())["input"]
.as_array()
.unwrap()
.clone();
let outputs = input
.iter()
.filter(|item| item["type"] == "function_call_output")
.collect::<Vec<_>>();
assert_eq!(
outputs
.iter()
.map(|item| item["call_id"].as_str().unwrap())
.collect::<Vec<_>>(),
vec!["multi", "one", "two"]
);
let serialized = serde_json::to_string(&outputs).unwrap();
assert_eq!(serialized.matches(RULE).count(), 1);
assert!(serialized.matches(MARKER).count() >= 3);
}
struct RewriteSkillSink {
path: PathBuf,
replacement: String,
rewritten: bool,
outputs: Vec<OutputEvent>,
}
impl AgentOutputSink for RewriteSkillSink {
fn assistant_delta(&mut self, _text: &str) -> anyhow::Result<()> {
Ok(())
}
fn tool_block(&mut self, _block: &str) -> anyhow::Result<()> {
Ok(())
}
fn output_event(&mut self, event: OutputEvent) -> anyhow::Result<()> {
if !self.rewritten
&& matches!(&event, OutputEvent::ToolResult { result, .. } if result.content.contains("OLD_SNAPSHOT_RULE"))
{
std::fs::write(&self.path, &self.replacement)?;
self.rewritten = true;
}
self.outputs.push(event);
Ok(())
}
}
#[test]
fn completed_read_snapshot_survives_same_size_edit_and_changed_reread_is_not_suppressed() {
let temp = tempfile::TempDir::new().unwrap();
let old = format!(
"---\nname: demo\ndescription: snapshot fixture\n---\n{}\nOLD_SNAPSHOT_RULE\n",
"old snapshot instruction ".repeat(24)
);
let new = format!(
"---\nname: demo\ndescription: snapshot fixture\n---\n{}\nNEW_SNAPSHOT_RULE\n",
"new snapshot instruction ".repeat(24)
);
assert_eq!(old.len(), new.len());
assert!(old.len() > 500);
let (path, discovery) = skill_fixture(&temp, &old);
let tools = skill_runtime(&temp, &discovery);
let provider = ScriptedProvider::new(vec![
vec![read_skill("old", "skill://demo"), done()],
vec![read_skill("new", "skill://demo"), done()],
text_done("done"),
]);
let mut sink = RewriteSkillSink {
path,
replacement: new,
rewritten: false,
outputs: Vec::new(),
};
AgentSession::new("model", &[], &discovery)
.run_print_with_tools_streaming_output(
&provider,
"snapshot",
Some(&tools),
None,
temp.path(),
Some(&mut sink),
)
.unwrap();
let requests = provider.requests();
let after_first_read = codex_input(&requests[1]);
assert!(after_first_read.contains("OLD_SNAPSHOT_RULE"));
assert!(!after_first_read.contains("NEW_SNAPSHOT_RULE"));
let after_reread = codex_input(&requests[2]);
assert!(after_reread.contains("OLD_SNAPSHOT_RULE"));
assert!(after_reread.contains("NEW_SNAPSHOT_RULE"));
assert!(!after_reread.contains(MARKER));
}
#[test]
fn slice_whole_reference_and_reloaded_or_fresh_sessions_do_not_create_false_coverage() {
let temp = tempfile::TempDir::new().unwrap();
let body = format!(
"---\nname: demo\ndescription: selector fixture\n---\nSLICE_ONLY_RULE\n{}\nWHOLE_ONLY_RULE\n",
"whole selector instruction ".repeat(24)
);
assert!(body.len() > 500);
let (_, discovery) = skill_fixture(&temp, &body);
let reference = format!(
"{}\nREFERENCE_ONLY_RULE\n",
"distinct reference instruction ".repeat(24)
);
assert!(reference.len() > 500);
std::fs::write(temp.path().join("skills/demo/refs/other.md"), reference).unwrap();
let tools = skill_runtime(&temp, &discovery);
let manager = crate::sessions::SessionManager::new(temp.path().join("sessions"));
let session = manager.create().unwrap();
let first = ScriptedProvider::new(vec![
vec![read_skill("slice", "skill://demo:5"), done()],
vec![read_skill("whole", "skill://demo"), done()],
vec![
read_skill("reference", "skill://demo/refs/other.md"),
done(),
],
text_done("done"),
]);
AgentSession::new("model", &[], &discovery)
.run_print_with_tools(
&first,
"selectors",
Some(&tools),
Some(&session),
temp.path(),
)
.unwrap();
let selected = codex_input(first.requests().last().unwrap());
assert!(selected.contains("SLICE_ONLY_RULE"));
assert!(selected.contains("WHOLE_ONLY_RULE"));
assert!(selected.contains("REFERENCE_ONLY_RULE"));
assert!(!selected.contains(MARKER));
let reloaded = ScriptedProvider::new(vec![
vec![read_skill("reload", "skill://demo"), done()],
text_done("done"),
]);
AgentSession::new("model", &[], &discovery)
.run_print_with_tools(
&reloaded,
"reload",
Some(&tools),
Some(&session),
temp.path(),
)
.unwrap();
let reload_input = codex_input(reloaded.requests().last().unwrap());
assert_eq!(reload_input.matches("WHOLE_ONLY_RULE").count(), 1);
assert!(
reload_input.contains(MARKER),
"replay must deduplicate the unchanged whole read"
);
let fresh_session = manager.create().unwrap();
let fresh = ScriptedProvider::new(vec![
vec![read_skill("fresh", "skill://demo"), done()],
text_done("done"),
]);
AgentSession::new("model", &[], &discovery)
.run_print_with_tools(
&fresh,
"fresh",
Some(&tools),
Some(&fresh_session),
temp.path(),
)
.unwrap();
let fresh_input = codex_input(fresh.requests().last().unwrap());
assert!(fresh_input.contains("WHOLE_ONLY_RULE"));
assert!(!fresh_input.contains(MARKER));
}
#[test]
fn truncated_historical_skill_output_cannot_witness_a_new_full_read() {
let temp = tempfile::TempDir::new().unwrap();
let body = format!(
"---\nname: demo\ndescription: large replay fixture\n---\n{}BIG_SKILL_END_RULE\n",
"large historical instruction ".repeat(1_100)
);
assert!(body.len() > 24_000);
let (_, discovery) = skill_fixture(&temp, &body);
let tools = skill_runtime(&temp, &discovery);
let session = crate::sessions::SessionManager::new(temp.path().join("sessions"))
.create()
.unwrap();
let historical = ScriptedProvider::new(vec![
vec![read_skill("historical", "skill://demo"), done()],
text_done("done"),
]);
AgentSession::new("model", &[], &discovery)
.run_print_with_tools(
&historical,
"read large skill",
Some(&tools),
Some(&session),
temp.path(),
)
.unwrap();
assert!(
raw_tool_outputs(&session)[0]
.1
.contains("BIG_SKILL_END_RULE")
);
let reread = ScriptedProvider::new(vec![
vec![read_skill("reread", "skill://demo"), done()],
text_done("done"),
]);
AgentSession::new("model", &[], &discovery)
.run_print_with_tools(
&reread,
"read it again after reload",
Some(&tools),
Some(&session),
temp.path(),
)
.unwrap();
let input = codex_input(reread.requests().last().unwrap());
assert!(input.contains("BIG_SKILL_END_RULE"));
assert!(!input.contains(MARKER));
}
struct CancelAfterSkillReadSink {
cancel: Arc<AtomicBool>,
saw_full_read: bool,
}
impl AgentOutputSink for CancelAfterSkillReadSink {
fn assistant_delta(&mut self, _text: &str) -> anyhow::Result<()> {
Ok(())
}
fn tool_block(&mut self, _block: &str) -> anyhow::Result<()> {
Ok(())
}
fn output_event(&mut self, event: OutputEvent) -> anyhow::Result<()> {
if matches!(&event, OutputEvent::ToolResult { result, .. } if result.content.contains(RULE))
{
self.saw_full_read = true;
self.cancel.store(true, Ordering::SeqCst);
}
Ok(())
}
}
#[test]
fn canceled_after_completed_read_retains_real_coverage_for_retry() {
let temp = tempfile::TempDir::new().unwrap();
let body = format!(
"---\nname: demo\ndescription: cancellation fixture\n---\n{}\n{RULE}\n",
"cancellation instruction ".repeat(24)
);
let (_, discovery) = skill_fixture(&temp, &body);
let tools = skill_runtime(&temp, &discovery);
let session = crate::sessions::SessionManager::new(temp.path().join("sessions"))
.create()
.unwrap();
let cancel = Arc::new(AtomicBool::new(false));
let provider = ScriptedProvider::new(vec![
vec![read_skill("cancelled_read", "skill://demo"), done()],
text_done("must not be requested"),
]);
let mut sink = CancelAfterSkillReadSink {
cancel: Arc::clone(&cancel),
saw_full_read: false,
};
let mut request = run_request("read then cancel", temp.path());
request.tools = Some(&tools);
request.session = Some(&session);
request.output_sink = Some(&mut sink);
request.cancellation = AgentCancellation::new(cancel);
let error = AgentSession::new("model", &[], &discovery)
.run_print_with_tools_streaming_output_cancellable(&provider, request)
.unwrap_err();
assert!(is_run_canceled(&error));
assert!(sink.saw_full_read);
assert_eq!(provider.requests().len(), 1);
let retry = ScriptedProvider::new(vec![
vec![read_skill("retry_read", "skill://demo"), done()],
text_done("done"),
]);
AgentSession::new("model", &[], &discovery)
.run_print_with_tools(
&retry,
"retry the read",
Some(&tools),
Some(&session),
temp.path(),
)
.unwrap();
let retry_result = retry
.requests()
.last()
.unwrap()
.tool_results()
.into_iter()
.find(|result| result.call_id == "retry_read")
.unwrap();
assert!(retry_result.output.contains(MARKER));
let retry_input = codex_input(retry.requests().last().unwrap());
assert_eq!(retry_input.matches(RULE).count(), 1);
assert!(
raw_tool_outputs(&session)
.iter()
.any(|(call_id, output)| call_id == "cancelled_read" && output.contains(RULE))
);
}
#[test]
fn compaction_removes_old_skill_body_and_explicit_reread_is_full() {
let temp = tempfile::TempDir::new().unwrap();
let body = format!(
"---\nname: demo\ndescription: compaction fixture\n---\n{}\n{RULE}\n",
"compacted instruction ".repeat(24)
);
let (_, discovery) = skill_fixture(&temp, &body);
let tools = skill_runtime(&temp, &discovery);
let session = crate::sessions::SessionManager::new(temp.path().join("sessions"))
.create()
.unwrap();
let first = ScriptedProvider::new(vec![
vec![read_skill("before_compaction", "skill://demo"), done()],
text_done("done"),
]);
AgentSession::new("model", &[], &discovery)
.run_print_with_tools(
&first,
"read before compaction",
Some(&tools),
Some(&session),
temp.path(),
)
.unwrap();
let cutoff = session.read_events().unwrap().len();
crate::sessions::record_session_compaction(
&session,
temp.path(),
"COMPACTED_SKILL_SUMMARY without copied instructions",
crate::providers::OPENAI_CODEX_PROVIDER,
"model",
cutoff,
)
.unwrap();
let reread = ScriptedProvider::new(vec![
vec![read_skill("after_compaction", "skill://demo"), done()],
text_done("done"),
]);
AgentSession::new("model", &[], &discovery)
.run_print_with_tools(
&reread,
"read after compaction",
Some(&tools),
Some(&session),
temp.path(),
)
.unwrap();
let requests = reread.requests();
let before_read = codex_input(&requests[0]);
assert!(before_read.contains("COMPACTED_SKILL_SUMMARY"));
assert!(!before_read.contains(RULE));
let result = requests[1]
.tool_results()
.into_iter()
.find(|result| result.call_id == "after_compaction")
.unwrap();
assert!(result.output.contains(RULE));
assert!(!result.output.contains(MARKER));
}
#[cfg(unix)]
#[test]
fn changed_discovery_path_and_unavailable_targets_do_not_reuse_past_coverage() {
use std::os::unix::fs::symlink;
let temp = tempfile::TempDir::new().unwrap();
let body = format!(
"---\nname: demo\ndescription: resolution fixture\n---\n{}\n{RULE}\n",
"resolution instruction ".repeat(24)
);
let (_, original_discovery) = skill_fixture(&temp, &body);
let original_tools = skill_runtime(&temp, &original_discovery);
let session = crate::sessions::SessionManager::new(temp.path().join("sessions"))
.create()
.unwrap();
let original = ScriptedProvider::new(vec![
vec![read_skill("original", "skill://demo"), done()],
text_done("done"),
]);
AgentSession::new("model", &[], &original_discovery)
.run_print_with_tools(
&original,
"read original",
Some(&original_tools),
Some(&session),
temp.path(),
)
.unwrap();
let selected_path = temp.path().join("alternate/demo/SKILL.md");
std::fs::create_dir_all(selected_path.parent().unwrap()).unwrap();
std::fs::write(&selected_path, &body).unwrap();
let selected_discovery = SkillDiscovery {
skills: BTreeMap::from([(
"demo".to_string(),
load_skill_descriptor(&selected_path).unwrap(),
)]),
diagnostics: Vec::new(),
};
let selected_tools = skill_runtime(&temp, &selected_discovery);
let changed = ScriptedProvider::new(vec![
vec![read_skill("changed_path", "skill://demo"), done()],
text_done("done"),
]);
AgentSession::new("model", &[], &selected_discovery)
.run_print_with_tools(
&changed,
"read changed selection",
Some(&selected_tools),
Some(&session),
temp.path(),
)
.unwrap();
let changed_output = changed
.requests()
.last()
.unwrap()
.tool_results()
.into_iter()
.find(|result| result.call_id == "changed_path")
.unwrap()
.output;
assert!(changed_output.contains(RULE));
assert!(!changed_output.contains(MARKER));
let disabled_tools = ToolRuntime::new(temp.path()).unwrap();
let disabled = ScriptedProvider::new(vec![
vec![read_skill("disabled", "skill://demo"), done()],
text_done("done"),
]);
AgentSession::new("model", &[], &SkillDiscovery::default())
.run_print_with_tools(
&disabled,
"read disabled selection",
Some(&disabled_tools),
Some(&session),
temp.path(),
)
.unwrap();
let disabled_result = disabled
.requests()
.last()
.unwrap()
.tool_results()
.into_iter()
.find(|result| result.call_id == "disabled")
.unwrap();
assert!(!disabled_result.success);
assert!(disabled_result.output.contains("unknown skill 'demo'"));
std::fs::remove_file(&selected_path).unwrap();
let removed = ScriptedProvider::new(vec![
vec![read_skill("removed", "skill://demo"), done()],
text_done("done"),
]);
AgentSession::new("model", &[], &selected_discovery)
.run_print_with_tools(
&removed,
"read removed target",
Some(&selected_tools),
Some(&session),
temp.path(),
)
.unwrap();
let removed_result = removed
.requests()
.last()
.unwrap()
.tool_results()
.into_iter()
.find(|result| result.call_id == "removed")
.unwrap();
assert!(!removed_result.success);
assert!(removed_result.output.contains("could not be opened"));
let real_target = temp.path().join("real-skill.md");
std::fs::write(&real_target, &body).unwrap();
symlink(&real_target, &selected_path).unwrap();
let symlinked = ScriptedProvider::new(vec![
vec![read_skill("symlinked", "skill://demo"), done()],
text_done("done"),
]);
AgentSession::new("model", &[], &selected_discovery)
.run_print_with_tools(
&symlinked,
"read symlinked target",
Some(&selected_tools),
Some(&session),
temp.path(),
)
.unwrap();
let symlinked_result = symlinked
.requests()
.last()
.unwrap()
.tool_results()
.into_iter()
.find(|result| result.call_id == "symlinked")
.unwrap();
assert!(!symlinked_result.success);
assert!(
symlinked_result
.output
.contains("without following symlinks")
);
}