diff --git a/crates/ghostlight/src/controllers.rs b/crates/ghostlight/src/controllers.rs index 0f12bd0..0f551c5 100644 --- a/crates/ghostlight/src/controllers.rs +++ b/crates/ghostlight/src/controllers.rs @@ -2924,9 +2924,6 @@ pub enum ConstituentRefusalReason { CallUnattributed, } -/// The longest tool name a run keeps of a call that named no handle. -pub const UNATTRIBUTED_TOOL_CAP: usize = 64; - /// One coarse cell's outcome: one inference, N ordinary one-opportunity /// submissions, and whatever the decode could not attribute. #[derive(Debug)] @@ -2936,8 +2933,8 @@ pub struct GroupedRun { pub submissions: Vec, /// Decode problems the grouped decode could not attribute to a constituent. pub needs: Vec, - /// The tool names of the calls that named no handle, each capped at - /// [`UNATTRIBUTED_TOOL_CAP`] characters; never their arguments. + /// The tool names of the calls that named no handle, already valid under + /// the provider contract; never their arguments. pub unattributed: Vec, /// Constituents of this cell that did not commit, one entry each, with the /// reason. A constituent is in `submissions` or here, never lost. @@ -6005,8 +6002,7 @@ pub(super) fn grouped_tool_result( // A model that writes `c99__carry` has not proposed anything. It has // produced a gap. None => { - fold.unattributed - .push(name.chars().take(UNATTRIBUTED_TOOL_CAP).collect()); + fold.unattributed.push(name.to_string()); fold.needs.push(tool_decode_need( name, arguments, @@ -15269,14 +15265,13 @@ mod tests { } } - /// The name a run keeps of a call that named no handle is bounded; the - /// call's arguments are not kept at all. + /// The call's arguments are not kept at all: only the name is. #[test] - fn the_name_kept_of_a_call_naming_no_handle_is_capped() { + fn the_name_of_a_call_naming_no_handle_is_kept_and_its_arguments_are_not() { let mut fold = GroupedFold::new(); - let long = "x".repeat(UNATTRIBUTED_TOOL_CAP + 36); - grouped_tool_result(&[], &mut fold, &long, r#"{"text":"SENTINEL-ARGUMENT"}"#).unwrap(); - assert_eq!(fold.unattributed, vec!["x".repeat(UNATTRIBUTED_TOOL_CAP)]); + grouped_tool_result(&[], &mut fold, "c9__speak", r#"{"text":"SENTINEL-ARGUMENT"}"#) + .unwrap(); + assert_eq!(fold.unattributed, vec!["c9__speak".to_string()]); } /// Verification 20b. No spelling of a handle outside the cell decodes, and diff --git a/crates/ghostlight/src/verse/run/tests.rs b/crates/ghostlight/src/verse/run/tests.rs index ea4c811..fcac9a1 100644 --- a/crates/ghostlight/src/verse/run/tests.rs +++ b/crates/ghostlight/src/verse/run/tests.rs @@ -2595,6 +2595,116 @@ async fn a_grouped_cell_whose_lists_do_not_match_its_members_ends_the_run() { assert!(committed.is_empty() && refusals.is_empty()); } +/// A cell of three members at a later position, so that the guard is held to +/// its length, to every member, and to the cell it is asked about rather than +/// to the two-member first cell the fixture covers. +async fn three_member_group(verse: &Harness) -> Cell { + let cover = cover_of(verse, 1).await; + let Cell::Group { id, tick, members } = &cover.cells[0] else { + panic!("a group") + }; + let mut members = members.clone(); + let mut third = members[0].clone(); + third.subject = SubjectId::issue(); + members.push(third); + Cell::Group { + id: *id, + tick: *tick, + members, + } +} + +#[tokio::test] +async fn a_finished_cell_of_three_must_list_each_member_exactly_once_wherever_it_sits() { + let (_dir, verse) = Harness::fresh().await; + let group = three_member_group(&verse).await; + let Cell::Group { id, members, .. } = &group else { + panic!("a group") + }; + let left = ConstituentRefusalReason::LeftBeforeSelection; + let run = |listed: Vec| { + Ok(CellRun::Grouped(crate::controllers::GroupedRun { + cell: *id, + resolution: crate::cover::Resolution::Coarse { + constituents: members.len(), + }, + submissions: Vec::new(), + needs: Vec::new(), + unattributed: Vec::new(), + refused: listed + .into_iter() + .map(|subject| crate::controllers::ConstituentRefusal { subject, reason: left }) + .collect(), + pending: None, + })) + }; + let [a, b, c] = [members[0].subject, members[1].subject, members[2].subject]; + let broken = Some(failed("cell_membership")); + for position in [0, 1, 2] { + let (status, _, _) = fold(run(vec![a, b, c]), &group, position); + assert_eq!(status, None, "every member listed once, at cell {position}"); + // Every member listed, and one subject the cell does not hold. + let (status, _, _) = fold(run(vec![a, b, c, SubjectId::issue()]), &group, position); + assert_eq!(status, broken, "a foreign extra at cell {position}"); + // Every member listed, and one of them twice. + let (status, _, _) = fold(run(vec![a, b, c, b]), &group, position); + assert_eq!(status, broken, "a duplicate at cell {position}"); + // The right number of entries, and the last member missing: only the + // member at the end of the cell can be the one that is not there. + let (status, _, _) = fold(run(vec![a, b, SubjectId::issue()]), &group, position); + assert_eq!(status, broken, "a missing last member at cell {position}"); + let (status, _, _) = fold(run(vec![a, c, a]), &group, position); + assert_eq!(status, broken, "a missing middle member at cell {position}"); + let (status, _, _) = fold(run(vec![a, b]), &group, position); + assert_eq!(status, broken, "a short list at cell {position}"); + } +} + +#[tokio::test] +async fn a_call_naming_no_handle_is_recorded_at_the_cell_it_was_made_in() { + let (_dir, verse) = Harness::fresh().await; + let group = three_member_group(&verse).await; + let Cell::Group { id, members, .. } = &group else { + panic!("a group") + }; + for position in [0, 1, 4] { + let run = CellRun::Grouped(crate::controllers::GroupedRun { + cell: *id, + resolution: crate::cover::Resolution::Coarse { + constituents: members.len(), + }, + submissions: Vec::new(), + needs: Vec::new(), + unattributed: vec!["c9__speak".to_string()], + refused: members + .iter() + .map(|member| crate::controllers::ConstituentRefusal { + subject: member.subject, + reason: ConstituentRefusalReason::CallUnattributed, + }) + .collect(), + pending: None, + }); + let mut unattributed = Vec::new(); + let status = absorb( + Ok(run), + &group, + position, + &mut Vec::new(), + &mut Vec::new(), + &mut unattributed, + ); + assert_eq!(status, None); + assert_eq!( + unattributed, + vec![UnattributedCall { + cell: position, + tool: "c9__speak".into() + }] + ); + } +} + #[tokio::test] async fn a_call_naming_no_handle_is_printed_and_its_quiet_member_is_refused_not_declined() { let (_dir, verse) = Harness::fresh().await;