use super::*;
use crate::{generated::types::DocOpenParams, Client, HostOptions};
use serde_json::json;
use std::{fs, path::PathBuf, time::Duration};
fn vectors() -> Value {
serde_json::from_str(include_str!("../tests/fixtures/action-policy-vectors.json")).unwrap()
}
#[test]
fn core_action_policy_matches_shared_node_vectors_and_csv_precedence() {
let vectors = vectors();
let authority = &action_policy().unwrap().preflight;
assert_eq!(vectors["rows"].as_array().unwrap().len(), 10);
for row in vectors["rows"].as_array().unwrap() {
let mut operations = vectors["capabilities"]["operations"].clone();
for missing in row["missing"].as_array().into_iter().flatten() {
operations
.as_object_mut()
.unwrap()
.remove(missing.as_str().unwrap());
}
for (id, capability) in row["overrides"].as_object().into_iter().flatten() {
operations[id] = capability.clone();
}
let unavailable = unavailable_from_operations(&operations).unwrap();
assert_eq!(
json!(unavailable),
row["expectedUnavailable"],
"{}",
row["id"]
);
let configured = row["configuredCsv"]
.as_str()
.map_or(Presence::Missing, |s| Presence::Present(s.into()));
let actual = match merge_exclusions(&configured, &unavailable, authority) {
Presence::Missing => Value::Null,
Presence::Present(value) => json!(value),
};
assert_eq!(actual, row["expectedCsv"], "{}", row["id"]);
}
assert_eq!(
merge_exclusions(&Presence::Present(String::new()), &[], authority),
Presence::Present(String::new())
);
for (configured, merged) in [
(
"\u{feff}insert_paragraphs\u{feff},insert_paragraphs",
"insert_paragraphs,insert_table_row",
),
(
"\u{85}insert_paragraphs\u{85}",
"\u{85}insert_paragraphs\u{85},insert_table_row",
),
] {
let configured = Presence::Present(configured.into());
assert_eq!(merge_exclusions(&configured, &[], authority), configured);
assert_eq!(
merge_exclusions(&configured, &["insert_table_row".into()], authority),
Presence::Present(merged.into())
);
}
let typed: DocCapabilitiesGetResult =
serde_json::from_value(vectors["capabilities"].clone()).unwrap();
assert!(unavailable_tracked_actions(&typed).unwrap().is_empty());
}
struct Fixture {
directory: tempfile::TempDir,
options: HostOptions,
}
impl Fixture {
fn new(capability_error: bool) -> Self {
let directory = tempfile::tempdir().unwrap();
let mut capabilities = vectors()["capabilities"].clone();
capabilities["operations"]["tables.insertRow"] =
json!({"available":true,"tracked":false,"trackedSupport":"never","dryRun":false});
let capabilities_file = directory.path().join("capabilities.json");
fs::write(
&capabilities_file,
serde_json::to_vec(&capabilities).unwrap(),
)
.unwrap();
let mut options = HostOptions::new(
std::env::var_os("SUPERDOC_SDK_TEST_NODE").unwrap_or_else(|| "node".into()),
);
options.prefix_args = vec![PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/agent-host.mjs")
.to_string_lossy()
.into_owned()];
options.state_directory = Some(directory.path().join("state"));
options.env.insert(
"SUPERDOC_AGENT_REQUESTS".into(),
directory
.path()
.join("requests.jsonl")
.to_string_lossy()
.into_owned(),
);
options.env.insert(
"SUPERDOC_AGENT_CAPABILITIES".into(),
capabilities_file.to_string_lossy().into_owned(),
);
if capability_error {
options
.env
.insert("SUPERDOC_AGENT_CAPABILITIES_ERROR".into(), "1".into());
}
Self { directory, options }
}
fn requests(&self) -> Vec<Value> {
fs::read_to_string(self.directory.path().join("requests.jsonl"))
.unwrap()
.lines()
.map(|line| serde_json::from_str(line).unwrap())
.collect()
}
}
fn argv(request: &Value) -> Vec<String> {
request["params"]["argv"]
.as_array()
.unwrap()
.iter()
.map(|value| value.as_str().unwrap().to_owned())
.collect()
}
fn flag(args: &[String], name: &str) -> Option<String> {
args.iter()
.position(|arg| arg == &format!("--{name}"))
.map(|at| args[at + 1].clone())
}
#[tokio::test]
async fn public_core_toolkit_snapshots_one_capability_read_and_lossless_shared_request_policy() {
let fixture = Fixture::new(false);
let client = Client::connect(fixture.options.clone()).await.unwrap();
let document = client
.open(
&DocOpenParams::new()
.with_doc("unused.docx".into())
.with_session_id("toolkit".into()),
)
.await
.unwrap();
let mut input: DocPresetGetToolsParams = serde_json::from_value(json!({"provider":"anthropic","cache":true,"excludeActions":" insert_heading,insert_heading,, ","enforceChangeMode":"tracked","allowDirectActions":" insert_table_row,undo_changes "})).unwrap();
let toolkit = document.core_toolkit(&input).await.unwrap();
assert!(toolkit.capabilities_snapshot().is_some());
assert_eq!(
toolkit.tools().tools,
vec![json!({"requestCaptureFixture":true})]
);
assert_eq!(toolkit.tools().cache_strategy, "fixture-cache");
assert_eq!(
toolkit.tools().additional()["fixtureExtension"],
json!({"untouched":true})
);
assert_eq!(
toolkit.prompt(),
"Unmodified request-capture fixture prompt"
);
assert_eq!(
toolkit.system_prompt().additional()["fixtureExtension"],
json!({"untouched":true})
);
input.exclude_actions = Presence::Present("delete_text".into());
input.allow_direct_actions = Presence::Present("*".into());
let params = DocPresetDispatchBoundParams::new("superdoc_perform_action".into())
.with_args(json!({"action":"insert_table_row"}))
.with_exclude_actions("".into())
.with_allow_direct_actions("*".into())
.with_preset("legacy".into());
let excluded = toolkit.dispatch(¶ms).await.unwrap_err();
assert_eq!(excluded.code, "INVALID_ARGUMENT");
assert_eq!(excluded.details.as_ref().unwrap()["reason"], "excluded");
assert_eq!(
excluded.details.as_ref().unwrap()["action"],
"insert_table_row"
);
let allowed = params
.clone()
.with_args(json!({"action":"insert_paragraphs","text":"CONTROL"}));
let error = toolkit
.dispatch_with_options(
&allowed,
InvokeOptions {
timeout: Some(Duration::from_millis(1234)),
..Default::default()
},
)
.await
.unwrap_err();
assert_eq!(error.code, "RECORDED_DISPATCH");
assert!(!error.outcome_unknown);
let requests = fixture.requests();
let calls: Vec<_> = requests
.iter()
.filter(|row| row["method"] == "cli.invoke")
.map(argv)
.collect();
assert_eq!(
calls
.iter()
.filter(|args| args[0] == "capabilities")
.count(),
1
);
let getters: Vec<_> = calls.iter().filter(|args| args[0] == "preset").collect();
assert_eq!(getters.len(), 3);
for args in getters {
assert_eq!(flag(args, "preset").as_deref(), Some("core"));
assert_eq!(
flag(args, "excludeActions").as_deref(),
Some("insert_heading,insert_table_row")
);
assert_eq!(flag(args, "enforceChangeMode").as_deref(), Some("tracked"));
assert_eq!(
flag(args, "allowDirectActions").as_deref(),
Some(" insert_table_row,undo_changes ")
);
if args[1] != "dispatch" {
assert_eq!(flag(args, "session"), None);
}
}
let tools = calls
.iter()
.find(|args| args.get(1).map(String::as_str) == Some("get-tools"))
.unwrap();
assert_eq!(flag(tools, "provider").as_deref(), Some("anthropic"));
assert!(tools.contains(&"--cache".into()));
let dispatch = requests
.iter()
.find(|row| row["params"]["argv"][1] == "dispatch")
.unwrap();
assert_eq!(dispatch["requestTimeoutMs"], 1234);
assert_eq!(flag(&argv(dispatch), "session").as_deref(), Some("toolkit"));
assert_eq!(params.preset, Presence::Present("legacy".into()));
document.close(&Default::default()).await.unwrap();
client.shutdown().await.unwrap();
}
#[tokio::test]
async fn public_core_toolkit_skips_capabilities_without_exact_tracked_enforcement() {
let fixture = Fixture::new(false);
let client = Client::connect(fixture.options.clone()).await.unwrap();
let document = client
.open(&DocOpenParams::new().with_doc("unused.docx".into()))
.await
.unwrap();
for mode in [None, Some("direct"), Some("TRACKED")] {
let mut input: DocPresetGetToolsParams = serde_json::from_value(
json!({"provider":"generic","excludeActions":" insert_heading,insert_heading,, "}),
)
.unwrap();
input.enforce_change_mode =
mode.map_or(Presence::Missing, |value| Presence::Present(value.into()));
let result = document.core_toolkit(&input).await;
if mode.is_some() {
let error = result.err().unwrap();
assert_eq!(error.code, "INVALID_ARGUMENT");
assert!(!error.outcome_unknown);
} else {
let toolkit = result.unwrap();
assert!(toolkit.capabilities_snapshot().is_none());
assert_eq!(toolkit.policy().exclude_actions, input.exclude_actions);
assert_eq!(toolkit.policy().preset, Presence::Present("core".into()));
}
}
assert!(!fixture
.requests()
.iter()
.filter(|row| row["method"] == "cli.invoke")
.map(argv)
.any(|args| args[0] == "capabilities"));
document.close(&Default::default()).await.unwrap();
client.shutdown().await.unwrap();
}
#[tokio::test]
async fn public_core_toolkit_rejects_noncore_before_reads_and_propagates_capability_errors() {
let fixture = Fixture::new(true);
let client = Client::connect(fixture.options.clone()).await.unwrap();
let document = client
.open(&DocOpenParams::new().with_doc("unused.docx".into()))
.await
.unwrap();
let before = fixture.requests().len();
let input: DocPresetGetToolsParams = serde_json::from_value(
json!({"provider":"generic","preset":"legacy","enforceChangeMode":"tracked"}),
)
.unwrap();
let error = document.core_toolkit(&input).await.err().unwrap();
assert_eq!(error.code, "INVALID_ARGUMENT");
assert!(!error.outcome_unknown);
assert_eq!(fixture.requests().len(), before);
let input = input.with_preset("core".into());
let error = document.core_toolkit(&input).await.err().unwrap();
assert_eq!(error.code, "CAPABILITY_READ_REFUSED");
assert!(!error.outcome_unknown);
let calls: Vec<_> = fixture
.requests()
.iter()
.filter(|row| row["method"] == "cli.invoke")
.map(argv)
.collect();
assert_eq!(
calls
.iter()
.filter(|args| args[0] == "capabilities")
.count(),
1
);
assert!(!calls.iter().any(|args| args[0] == "preset"));
document.close(&Default::default()).await.unwrap();
client.shutdown().await.unwrap();
}
#[tokio::test]
async fn public_core_toolkit_preserves_tracked_csv_literal_when_all_operations_remain_available() {
let fixture = Fixture::new(false);
let capabilities_file = fixture.directory.path().join("capabilities.json");
fs::write(
&capabilities_file,
serde_json::to_vec(&vectors()["capabilities"]).unwrap(),
)
.unwrap();
let client = Client::connect(fixture.options.clone()).await.unwrap();
let document = client
.open(&DocOpenParams::new().with_doc("unused.docx".into()))
.await
.unwrap();
let direct = document
.api()
.capabilities()
.get(&DocCapabilitiesGetBoundParams::new())
.await
.unwrap();
assert!(unavailable_tracked_actions(&direct).unwrap().is_empty());
drop(direct);
let direct_reads = fixture
.requests()
.iter()
.filter(|row| row["method"] == "cli.invoke")
.map(argv)
.filter(|args| args[0] == "capabilities")
.count();
assert_eq!(direct_reads, 1);
let input: DocPresetGetToolsParams = serde_json::from_value(json!({"provider":"openai","enforceChangeMode":"tracked","excludeActions":" insert_heading,insert_heading,, "})).unwrap();
let toolkit = document.core_toolkit(&input).await.unwrap();
assert!(toolkit.capabilities_snapshot().is_some());
assert_eq!(toolkit.policy().exclude_actions, input.exclude_actions);
let calls: Vec<_> = fixture
.requests()
.iter()
.filter(|row| row["method"] == "cli.invoke")
.map(argv)
.collect();
assert_eq!(
calls
.iter()
.filter(|args| args[0] == "capabilities")
.count(),
direct_reads + 1
);
for args in calls.iter().filter(|args| args[0] == "preset") {
assert_eq!(
flag(args, "excludeActions").as_deref(),
Some(" insert_heading,insert_heading,, ")
);
}
document.close(&Default::default()).await.unwrap();
client.shutdown().await.unwrap();
}
#[tokio::test]
async fn public_full_toolkit_preserves_shared_full_policy_and_helpers_without_core_filtering() {
let fixture = Fixture::new(true);
let client = Client::connect(fixture.options.clone()).await.unwrap();
let document = client
.open(
&DocOpenParams::new()
.with_doc("unused.docx".into())
.with_session_id("full-toolkit".into()),
)
.await
.unwrap();
let mut input: DocPresetGetToolsParams = serde_json::from_value(json!({
"provider":"anthropic", "cache":true, "excludeActions":" insert_heading,insert_heading,, ",
"enforceChangeMode":"tracked", "allowDirectActions":" compare_with_file "
}))
.unwrap();
let original_policy = input.clone();
let toolkit = document.full_toolkit(&input).await.unwrap();
assert_eq!(
toolkit.tools().tools,
vec![json!({"requestCaptureFixture":true})]
);
assert_eq!(
toolkit.tools().additional()["fixtureExtension"],
json!({"untouched":true})
);
assert_eq!(
toolkit.prompt(),
"Unmodified request-capture fixture prompt"
);
assert_eq!(
toolkit.system_prompt().additional()["fixtureExtension"],
json!({"untouched":true})
);
input.exclude_actions = Presence::Present("shade_cells".into());
for (name, args) in [
("superdoc_api_search", json!({"query":"tables", "limit":0})),
(
"superdoc_api_call",
json!({"operationId":"doc.create.paragraph", "args":{"text":"synthetic"}, "dryRun":false}),
),
(
"superdoc_perform_action",
json!({"action":"insert_image", "src":"synthetic-image", "placement":{"position":"end"}}),
),
] {
let params = DocPresetDispatchBoundParams::new(name.into())
.with_args(args.clone())
.with_preset("legacy".into())
.with_exclude_actions("".into());
let error = toolkit
.dispatch_with_options(
¶ms,
InvokeOptions {
timeout: Some(Duration::from_millis(1234)),
change_mode: Some("tracked".into()),
..Default::default()
},
)
.await
.unwrap_err();
assert_eq!(error.code, "RECORDED_DISPATCH");
assert!(!error.outcome_unknown);
assert_eq!(params.preset, Presence::Present("legacy".into()));
assert_eq!(params.args, Presence::Present(args.clone()));
let requests = fixture.requests();
let request = requests
.iter()
.rfind(|row| row["params"]["argv"][1] == "dispatch")
.unwrap();
let command = argv(request);
assert_eq!(flag(&command, "preset").as_deref(), Some("full"));
assert_eq!(flag(&command, "tool-name").as_deref(), Some(name));
assert_eq!(
serde_json::from_str::<Value>(&flag(&command, "args-json").unwrap()).unwrap(),
args
);
assert_eq!(
flag(&command, "excludeActions"),
match &original_policy.exclude_actions {
Presence::Present(value) => Some(value.clone()),
Presence::Missing => None,
}
);
assert_eq!(
flag(&command, "allowDirectActions").as_deref(),
Some(" compare_with_file ")
);
assert_eq!(flag(&command, "session").as_deref(), Some("full-toolkit"));
assert_eq!(request["requestTimeoutMs"], 1234);
}
assert_eq!(
toolkit.policy().exclude_actions,
original_policy.exclude_actions
);
assert_eq!(toolkit.policy().preset, Presence::Present("full".into()));
let requests = fixture.requests();
let commands: Vec<_> = requests
.iter()
.filter(|row| row["method"] == "cli.invoke")
.map(argv)
.collect();
assert!(!commands.iter().any(|command| command[0] == "capabilities"));
assert_eq!(
commands
.iter()
.filter(|command| command[0] == "preset" && command[1] == "get-tools")
.count(),
1
);
assert_eq!(
commands
.iter()
.filter(|command| command[0] == "preset" && command[1] == "get-system-prompt")
.count(),
1
);
document.close(&Default::default()).await.unwrap();
client.shutdown().await.unwrap();
}
#[tokio::test]
async fn public_full_toolkit_rejects_another_preset_before_getters() {
let fixture = Fixture::new(true);
let client = Client::connect(fixture.options.clone()).await.unwrap();
let document = client
.open(&DocOpenParams::new().with_doc("unused.docx".into()))
.await
.unwrap();
let before = fixture.requests().len();
let input: DocPresetGetToolsParams =
serde_json::from_value(json!({"provider":"generic","preset":"core"})).unwrap();
let error = document.full_toolkit(&input).await.err().unwrap();
assert_eq!(error.code, "INVALID_ARGUMENT");
assert!(!error.outcome_unknown);
assert_eq!(fixture.requests().len(), before);
document.close(&Default::default()).await.unwrap();
client.shutdown().await.unwrap();
}