Widen the shared-decoder test to every tool and route two more copies through it (PA.f59)
`the_elaborator_and_the_table_share_one_decoder` proved agreement between `apply_tool_call` and `decode_authoring_call` for exactly one tool, `open_route`. Renamed to `the_elaborator_and_the_table_agree_over_every_tool` and widened to every non-session `PATCH_TOOLS` entry's own generated example, asserting the whole `WorldPatch` — declarations, operations, and evidence — agrees, not only operations. patch.rs's `decodes` carried its own copy of the tag-injection-then- deserialize logic `decode_authoring_call` already owns; it now builds the same arguments string and calls `decode_authoring_call` directly (`Session` still short-circuits to `Ok(())`, matching `decode_authoring_call`'s own refusal of that shape). `every_declaration` and `every_component_op` similarly deserialized `arguments_for`'s output straight into `Declaration`/ `ComponentOp`, bypassing the table's decoder entirely; both now decode through `decode_authoring_call` and pull the single declaration or operation out of its `WorldPatch`. Corrected `authoring_tools_admits_every_known_name`'s doc comment, which described the unknown-name-refusal test above it rather than its own assertion. Added `describe_refusal_never_swaps_declaration_and_operation_site_wording` (mutation X6): a declaration site (`EmptyHandle`'s `position`) and an operation site (`SubjectNotAtOrigin`'s `operation`) render "declaration tool call #N" and "operation tool call #N" respectively, and must never trade words. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
1847974b62
commit
2035fe8722
2 changed files with 102 additions and 49 deletions
|
|
@ -7279,8 +7279,11 @@ mod catalog_tests {
|
|||
.iter()
|
||||
.filter(|entry| matches!(entry.shape, PatchToolShape::Declare { .. }))
|
||||
.map(|entry| {
|
||||
let declaration: Declaration =
|
||||
serde_json::from_value(arguments_for(entry)).expect("the entry decodes");
|
||||
let arguments = arguments_for(entry).to_string();
|
||||
let mut patch = crate::table::decode_authoring_call(entry.name, &arguments)
|
||||
.expect("the entry decodes");
|
||||
assert_eq!(patch.declarations.len(), 1, "{} decoded other than one item", entry.name);
|
||||
let declaration = patch.declarations.remove(0);
|
||||
assert!(
|
||||
tool_names(&declaration).contains(&entry.name),
|
||||
"{} decoded a declaration its own exhaustive match does not name",
|
||||
|
|
@ -7337,8 +7340,11 @@ mod catalog_tests {
|
|||
.iter()
|
||||
.filter(|entry| matches!(entry.shape, PatchToolShape::Operate { .. }))
|
||||
.map(|entry| {
|
||||
let operation: ComponentOp =
|
||||
serde_json::from_value(arguments_for(entry)).expect("the entry decodes");
|
||||
let arguments = arguments_for(entry).to_string();
|
||||
let mut patch = crate::table::decode_authoring_call(entry.name, &arguments)
|
||||
.expect("the entry decodes");
|
||||
assert_eq!(patch.operations.len(), 1, "{} decoded other than one item", entry.name);
|
||||
let operation = patch.operations.remove(0);
|
||||
assert_eq!(tool_name(&operation), entry.name);
|
||||
operation
|
||||
})
|
||||
|
|
@ -7607,28 +7613,17 @@ mod catalog_tests {
|
|||
serde_json::from_str(&definition.parameters_json).expect("an emitted schema is JSON")
|
||||
}
|
||||
|
||||
fn decodes(
|
||||
entry: &PatchTool,
|
||||
mut arguments: serde_json::Map<String, Value>,
|
||||
) -> Result<(), String> {
|
||||
match entry.shape {
|
||||
PatchToolShape::Declare { variant, fixed } => {
|
||||
arguments.insert("type".into(), Value::String(variant.into()));
|
||||
for (key, value) in fixed {
|
||||
arguments.insert((*key).into(), Value::String((*value).into()));
|
||||
}
|
||||
serde_json::from_value::<Declaration>(Value::Object(arguments))
|
||||
.map(|_| ())
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
PatchToolShape::Operate { variant } => {
|
||||
arguments.insert("op".into(), Value::String(variant.into()));
|
||||
serde_json::from_value::<ComponentOp>(Value::Object(arguments))
|
||||
.map(|_| ())
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
PatchToolShape::Session => Ok(()),
|
||||
/// Routed through the one decoder (PA.f59): this used to carry its own
|
||||
/// copy of the tag-injection-then-deserialize logic `decode_authoring_call`
|
||||
/// already owns. `Session` still decodes no patch item and is left to its
|
||||
/// own trivial `Ok`, exactly as `decode_authoring_call` itself refuses it
|
||||
/// rather than accepting it as a no-op.
|
||||
fn decodes(entry: &PatchTool, arguments: serde_json::Map<String, Value>) -> Result<(), String> {
|
||||
if matches!(entry.shape, PatchToolShape::Session) {
|
||||
return Ok(());
|
||||
}
|
||||
let arguments = Value::Object(arguments).to_string();
|
||||
crate::table::decode_authoring_call(entry.name, &arguments).map(|_| ())
|
||||
}
|
||||
|
||||
/// The direction a model actually travels: it reads the emitted schema and
|
||||
|
|
|
|||
|
|
@ -1585,8 +1585,11 @@ mod tests {
|
|||
assert_eq!(error, TableError::UnknownAuthoringTool("not_a_tool".to_owned()));
|
||||
}
|
||||
|
||||
/// Mutation M7.2: if `authoring_tools` stopped refusing an unknown name,
|
||||
/// this assertion is the one that would no longer hold.
|
||||
/// Mutation M7.2 (PA.f59: the doc here previously described the
|
||||
/// unknown-name refusal above, not this test): if `authoring_tools`
|
||||
/// silently dropped a known `PATCH_TOOLS` name instead of admitting it,
|
||||
/// `tools.len()` would fall short of `names.len()` and this assertion is
|
||||
/// the one that would catch it.
|
||||
#[test]
|
||||
fn authoring_tools_admits_every_known_name() {
|
||||
let names: Vec<&str> = PATCH_TOOLS.iter().map(|entry| entry.name).collect();
|
||||
|
|
@ -1683,30 +1686,53 @@ mod tests {
|
|||
assert!(error.contains("declare_place"), "{error}");
|
||||
}
|
||||
|
||||
/// Rule: the elaborator's own decode and the table's decode agree, because
|
||||
/// the elaborator's arm now calls the table's function rather than
|
||||
/// Rule (PA.f59): the elaborator's own decode and the table's decode
|
||||
/// agree over the *whole* catalog, not just `open_route` — for every
|
||||
/// non-session `PATCH_TOOLS` entry's own generated example, the
|
||||
/// elaboration path (`apply_tool_call`) and the table path
|
||||
/// (`decode_authoring_call`) produce the whole same `WorldPatch`,
|
||||
/// because the elaborator's arm calls the table's function rather than
|
||||
/// carrying a second copy.
|
||||
#[test]
|
||||
fn the_elaborator_and_the_table_share_one_decoder() {
|
||||
let arguments = serde_json::json!({
|
||||
"route": {"ref": "draft", "value": "route"}
|
||||
})
|
||||
.to_string();
|
||||
let mut draft = WorldPatch::default();
|
||||
let mut gaps = Vec::new();
|
||||
let mut submitted = false;
|
||||
crate::elaboration::apply_tool_call(
|
||||
"open_route",
|
||||
&arguments,
|
||||
&mut draft,
|
||||
&mut gaps,
|
||||
&mut submitted,
|
||||
);
|
||||
let table_patch = decode_authoring_call("open_route", &arguments)
|
||||
.expect("open_route decodes through the table too");
|
||||
assert!(gaps.is_empty());
|
||||
assert_eq!(draft.operations, table_patch.operations);
|
||||
assert!(draft.declarations.is_empty() && table_patch.declarations.is_empty());
|
||||
fn the_elaborator_and_the_table_agree_over_every_tool() {
|
||||
for entry in PATCH_TOOLS {
|
||||
if matches!(entry.shape, PatchToolShape::Session) {
|
||||
continue;
|
||||
}
|
||||
let mut object = serde_json::Map::new();
|
||||
for field in entry.fields {
|
||||
object.insert(field.name.to_owned(), patch::field_example(field.kind));
|
||||
}
|
||||
let arguments = Value::Object(object).to_string();
|
||||
let mut draft = WorldPatch::default();
|
||||
let mut gaps = Vec::new();
|
||||
let mut submitted = false;
|
||||
crate::elaboration::apply_tool_call(
|
||||
entry.name,
|
||||
&arguments,
|
||||
&mut draft,
|
||||
&mut gaps,
|
||||
&mut submitted,
|
||||
);
|
||||
let table_patch = decode_authoring_call(entry.name, &arguments)
|
||||
.unwrap_or_else(|error| panic!("{} did not decode through the table: {error}", entry.name));
|
||||
assert!(gaps.is_empty(), "{}: the elaborator recorded a gap: {gaps:?}", entry.name);
|
||||
assert_eq!(
|
||||
draft.declarations, table_patch.declarations,
|
||||
"{}: the elaborator's and the table's declarations disagree",
|
||||
entry.name
|
||||
);
|
||||
assert_eq!(
|
||||
draft.operations, table_patch.operations,
|
||||
"{}: the elaborator's and the table's operations disagree",
|
||||
entry.name
|
||||
);
|
||||
assert_eq!(
|
||||
draft.evidence, table_patch.evidence,
|
||||
"{}: the elaborator's and the table's evidence disagree",
|
||||
entry.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Rule: an actor tool call decodes against the subject's own currently
|
||||
|
|
@ -1981,6 +2007,38 @@ mod tests {
|
|||
assert!(!text.contains("tool call"), "{text}");
|
||||
}
|
||||
|
||||
/// Rule (PA.f59, mutation X6): a declaration site and an operation site
|
||||
/// never trade wording. `EmptyHandle`'s `position` names a declaration;
|
||||
/// `SubjectNotAtOrigin`'s `operation` names an operation; with no batch
|
||||
/// map in hand, each must keep its own word ("declaration"/"operation"),
|
||||
/// never the other's.
|
||||
#[test]
|
||||
fn describe_refusal_never_swaps_declaration_and_operation_site_wording() {
|
||||
let fixture = play_fixture();
|
||||
let declaration_text = describe_refusal(
|
||||
&fixture.snapshot,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&KernelError::PatchRejected(vec![Mismatch::EmptyHandle { position: 3 }]),
|
||||
);
|
||||
assert!(
|
||||
declaration_text.starts_with("declaration tool call #3:"),
|
||||
"{declaration_text}"
|
||||
);
|
||||
let operation_text = describe_refusal(
|
||||
&fixture.snapshot,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&KernelError::PatchRejected(vec![Mismatch::SubjectNotAtOrigin { operation: 3 }]),
|
||||
);
|
||||
assert!(
|
||||
operation_text.starts_with("operation tool call #3:"),
|
||||
"{operation_text}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Rule (PA.f56): `ActionRejected` renders the failed precondition's own
|
||||
/// role and what the actor's invocation actually bound it to, when
|
||||
/// `entry` and `invocation` are given.
|
||||
|
|
|
|||
Loading…
Reference in a new issue