Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions crates/runvil-internal/lua/src/coro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,18 @@ mod tests {
.contains("set_selection takes a model handle or nil, got number"),
"the refusal names the Lua type: {error}"
);
let stranger = lua
.create_any_userdata(())
.expect("a userdata that is no handle builds");
let error = set
.call::<()>(stranger)
.expect_err("a userdata that is no handle is refused too");
assert!(
error
.to_string()
.contains("set_selection takes a model handle or nil, got userdata"),
"the refusal names the Lua type: {error}"
);
assert_eq!(label(), None, "a refused write leaves the selection alone");
}
}
5 changes: 5 additions & 0 deletions crates/runvil-internal/lua/src/error-tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ fn the_failure_text_drops_the_positions_in_front_and_the_traceback() {
"[string \"line 3\"]: no line",
"a marker with no line is no position"
);
assert_eq!(
failure_text("[string \"line 3\"]:: empty line"),
"[string \"line 3\"]:: empty line",
"nor is a marker whose line has no digits"
);
assert_eq!(failure_text("a refusal\n"), "a refusal");
}

Expand Down
2 changes: 2 additions & 0 deletions crates/runvil-internal/lua/src/messages-tests-list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ fn replace_out_of_bounds_is_refused_naming_the_bounds() {
("messages:replace(2, 5)", "replace(2, 5)"),
("messages:replace(6, 5)", "replace(6, 5)"),
("messages:replace(-1, 2)", "replace(-1, 2)"),
// The bounds are checked before any record is read.
("messages:replace(2, 5, 'not a record')", "replace(2, 5)"),
] {
assert_eq!(
refusal(&vm, call),
Expand Down
9 changes: 9 additions & 0 deletions crates/runvil-internal/lua/src/models-tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ fn models_use_options_reach_the_returned_handle() {
assert!((temperature - 0.3).abs() < f64::EPSILON);
assert_eq!(max_tokens, 256);
assert_eq!(current_label(&vm).as_deref(), Some("default"));
let whole: u32 = vm.eval("return models.use('default', { max_tokens = 512.0 }).max_tokens");
assert_eq!(whole, 512, "a whole-number float counts as its integer");
}

#[test]
Expand Down Expand Up @@ -423,6 +425,13 @@ fn models_use_rejects_an_argument_that_is_neither_a_role_nor_a_handle() {
.contains("models.use takes a model role name or a model handle, got number"),
"the rejection names the argument's Lua type: {error}"
);
let error = vm.eval_err("models.use(messages)");
assert!(
error
.to_string()
.contains("models.use takes a model role name or a model handle, got userdata"),
"a userdata that is no handle is rejected the same way: {error}"
);
assert_eq!(
current_label(&vm).as_deref(),
Some("default"),
Expand Down
7 changes: 7 additions & 0 deletions crates/runvil-internal/lua/src/models_loop-tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,13 @@ fn an_arrivals_answer_reads_as_its_messages_in_a_round_and_in_the_final_check()
};
assert_eq!(read, texts);
}
let (failed, _) = answer_envelope(&lua, Err(Error::canceled())).unwrap();
let refusal = read_input(&lua, &Phase::Checking, failed, &hook)
.expect_err("a failed answer to the final check holds no messages");
assert!(
refusal.to_string().contains("the loop's inbox read failed"),
"the final check takes only its messages: {refusal}"
);
}

/// `act`'s tag and values for `then`.
Expand Down
9 changes: 9 additions & 0 deletions crates/runvil-internal/lua/src/models_loop/machine-tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,15 @@ fn an_input_its_phase_does_not_await_raises_internal() {
"a batch answer holds no more texts than the calls it was sent"
);
let mut machine = chatting(2, &MessageList::default());
machine.step(batch(&["c1", "c2", "c3"]));
machine.step(batched(&["one"]));
assert_eq!(show(&machine.step(dispatched("two"))), "tool_calls c3@7");
assert_eq!(
show(&machine.step(batched(&["three", "four"]))),
unexpected,
"nor more than the calls still unanswered once some have their results"
);
let mut machine = chatting(2, &MessageList::default());
assert_eq!(show(&machine.step(reply("done"))), "arrivals");
assert_eq!(
show(&machine.step(reply("again"))),
Expand Down
9 changes: 9 additions & 0 deletions crates/runvil-internal/lua/src/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,15 @@ mod tests {
mapped, fault,
"it is not recoded at the line that raised it"
);
let mut unlocated = fault;
unlocated.at = None;
let mapped =
program.map_runtime_error_among(&raised_through(&program, &unlocated), &[], FILE);
assert_eq!(
(mapped.code, mapped.at),
(ErrorCode::LuaRuntime, Some(SourceLoc::line(FILE, line(6)))),
"one with no location has none to keep, so it maps as any other failure does"
);
}

#[test]
Expand Down
12 changes: 7 additions & 5 deletions crates/runvil-internal/lua/src/protocol/tests/parse_tasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,12 +382,14 @@ fn concurrency_converts_a_whole_number_float_and_refuses_an_out_of_range_one() {
// it. A whole-number float past `u64` (an author's `1e20`) is the
// call's error, raised at the call site like the shim's own refusal.
let lua = Lua::new();
let table = lua_table(&lua, "{ op = 'concurrency', limit = 8.0 }");
match expect_request(Request::from_yield(&lua, &Value::Table(table))) {
Request::Concurrency { limit } => {
assert_eq!(limit.map(std::num::NonZeroU64::get), Some(8));
for (limit, expected) in [("8.0", 8), ("1.0", 1)] {
let table = lua_table(&lua, &format!("{{ op = 'concurrency', limit = {limit} }}"));
match expect_request(Request::from_yield(&lua, &Value::Table(table))) {
Request::Concurrency { limit } => {
assert_eq!(limit.map(std::num::NonZeroU64::get), Some(expected));
}
other => panic!("expected a concurrency request, got {other:?}"),
}
other => panic!("expected a concurrency request, got {other:?}"),
}
let table = lua_table(&lua, "{ op = 'concurrency', limit = 1e20 }");
match Request::from_yield(&lua, &Value::Table(table)) {
Expand Down
22 changes: 22 additions & 0 deletions crates/runvil-internal/lua/src/tests/cancellation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,28 @@ fn a_canceled_run_unwinds_through_an_author_pcall_loop() {
outcome.as_ref().err() == Some(&Error::canceled()),
"an author pcall must not swallow cancellation, got {outcome:?}"
);

// A loop suspended inside its pcall when the run is canceled: the
// failed answer the pcall raises again is the answer's own error table,
// and the block still ends canceled.
let handle = runvil_types::cancel::CancelHandle::new();
let vm = TestVm::new().shims().build();
vm.set_cancel(handle.clone());
let waiting = program("while true do pcall(models.infer, 'hi') end");
let CoroStep::Yielded(thread, _) = vm
.start_block_coro(&waiting)
.expect("the block suspends on its infer")
else {
panic!("the infer shim must suspend the block");
};
handle.cancel();
let timeout =
runvil_types::error::make_failure_error(ErrorCode::Timeout, "the model timed out");
let outcome = vm.resume_block_coro_answer(&waiting, &thread, Err(timeout));
assert!(
outcome.as_ref().err() == Some(&Error::canceled()),
"a failed answer must not replace cancellation, got {outcome:?}"
);
}

#[test]
Expand Down
15 changes: 15 additions & 0 deletions crates/runvil-internal/lua/src/tests/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,21 @@ fn map_chunk_line_to_absolute_no_match_passthrough() {
)],
);
assert_eq!(result, (msg.to_owned(), None));
// An owner that lacks a chunk name or a location owns no marker.
let ten = NonZeroU32::new(10).expect("10 is non-zero");
for (msg, owner) in [
(
r#"[string ""]:2: boom"#,
("", "section `Main` prologue", ten),
),
(r#"[string "line 10"]:2: boom"#, ("line 10", "", ten)),
] {
assert_eq!(
map_chunk_line_to_absolute(msg, &[owner]),
(msg.to_owned(), None),
"{owner:?}"
);
}
}

#[test]
Expand Down
10 changes: 10 additions & 0 deletions crates/runvil-internal/lua/src/tests/json/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ fn cycles_and_tables_past_the_depth_limit_become_placeholders() {
debug_text(&vm, "deep"),
format!("{}\"[too deep]\"{}", "[".repeat(127), "]".repeat(127))
);
vm.exec("broken = 1\nfor _ = 1, 128 do broken = { [true] = broken } end");
assert_eq!(
debug_text(&vm, "broken"),
format!(
"{}\"[too deep]\"{}",
"{\"true\":".repeat(127),
"}".repeat(127)
),
"a table that breaks the key rules counts toward the depth too"
);
}

#[test]
Expand Down
9 changes: 9 additions & 0 deletions crates/runvil-internal/lua/src/tests/json/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,13 @@ fn tables_127_deep_encode_and_128_deep_raise_the_depth_message() {
nested_path(127)
))
);
vm.exec("keyed = 1\nfor _ = 1, 128 do keyed = { k = keyed } end");
assert_eq!(
caught(&vm, "json.encode(keyed)"),
lua_error(&format!(
"json.encode: value{} is nested more than 127 levels deep",
".k".repeat(127)
)),
"an object counts toward the depth as a list does"
);
}
10 changes: 8 additions & 2 deletions crates/runvil-internal/lua/src/tests/json/limits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,22 @@ fn sibling_objects_each_under_the_held_key_limit_encode_whole() {
#[test]
fn keys_held_by_tables_on_one_path_add_up_to_the_limit() {
// A parent with 60,001 keys over a child with 60,000 holds 120,001
// keys on the path to the child's values.
// keys on the path to the child's values. A parent with 40,000 holds
// 100,000 there, which is the limit itself.
let vm = json_vm();
vm.exec(
"local function keyed(n) local o = {} for i = 1, n do o['k' .. i] = true end return o end\n\
t = keyed(60000) t.child = keyed(60000)",
t = keyed(60000) t.child = keyed(60000)\n\
full = keyed(39999) full.child = keyed(60000)",
);
assert_eq!(
caught(&vm, "json.encode(t)"),
lua_error("json.encode: value is too large to encode")
);
assert!(
encoded(&vm, "json.encode(full)").starts_with("{\"child\":{\"k1\":true,"),
"a path that holds exactly the limit encodes whole"
);
}

#[test]
Expand Down
6 changes: 6 additions & 0 deletions crates/runvil-internal/lua/src/tests/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,12 @@ fn store_read_numbered_pads_across_the_hundred_boundary() {
.expect("the memory store can seed a file");
let out = run_with("return store.read_numbered('a.txt', 99, 100)", &access).unwrap();
assert_eq!(out.returned.as_deref(), Some(" 99| line99\n100| line100"));
let out = run_with("return store.read_numbered('a.txt', 98, 99)", &access).unwrap();
assert_eq!(
out.returned.as_deref(),
Some("98| line98\n99| line99"),
"a slice that ends below the boundary is not padded"
);
}

#[test]
Expand Down
9 changes: 9 additions & 0 deletions crates/runvil-internal/lua/src/tests/store_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,15 @@ fn store_error_values_carry_reason_fields_and_message_for_each_reason() {
fields: path_field("d"),
message: "unsupported store operation on d: no rename",
},
Case {
error: VfsError::Conflict {
path: "d".to_owned(),
detail: "two unordered tasks wrote it".to_owned(),
},
reason: "conflict",
fields: path_field("d"),
message: "store conflict on d: two unordered tasks wrote it",
},
Case {
error: VfsError::Backend {
message: "disk gone".to_owned(),
Expand Down
15 changes: 9 additions & 6 deletions crates/runvil-internal/lua/src/tests/sys_and_var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,12 +216,15 @@ fn json_null_in_var_reads_back_as_null_and_equals_json_null_in_lua() {
#[test]
fn reassigning_the_var_global_fails_read_back() {
// `var = 5` drops the guarded proxy from reach; read-back must reject it
// rather than silently roll the pre-reassignment data forward.
let error = run("var = 5", "").expect_err("reassigning the var global must fail at read-back");
assert!(
error.to_string().contains("`var` global was reassigned"),
"the error must name the reassignment: {error}"
);
// rather than silently roll the pre-reassignment data forward. Another
// table in its place is not the proxy either.
for source in ["var = 5", "var = {}"] {
let error = run(source, "").expect_err("reassigning the var global must fail at read-back");
assert!(
error.to_string().contains("`var` global was reassigned"),
"the error must name the reassignment after {source}: {error}"
);
}
}

/// A VM whose Lua heap is 4 MiB, so a write may read at most 524,288 values
Expand Down
6 changes: 6 additions & 0 deletions crates/runvil-internal/lua/src/tools/tests-offering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,12 @@ fn an_array_is_read_through_its_metamethods() {
}))",
);
assert_eq!(offers(&vm), ["static:gh", "static:web/fetch"]);
vm.exec("tools.offer(setmetatable({}, { __len = function() return -1 end }))");
assert_eq!(
offers(&vm),
["static:gh", "static:web/fetch"],
"a negative length reads as no entries"
);
}

#[test]
Expand Down
8 changes: 7 additions & 1 deletion crates/runvil-internal/lua/src/tools/tests-stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ fn an_unknown_key_raises_naming_it_and_the_keys_held_never_a_discovery_or_model_
#[test]
fn tool_call_counts_seed_by_id_and_explain_an_unseeded_key() {
let vm = lua_over(catalog_set([echo_tool(), tool_at("tools/other")]));
vm.install_tool_call_counts(&[echo_tool().id])
let counts = vm
.install_tool_call_counts(&[echo_tool().id])
.expect("the counts install");
vm.exec("tools.adhoc({ name = 'grab', description = 'Grab a value' }, function() end)");
let seeded: i64 = vm.eval("return tools.stats.tools['tools/echo']");
Expand Down Expand Up @@ -212,6 +213,11 @@ fn tool_call_counts_seed_by_id_and_explain_an_unseeded_key() {
"an unknown key, a wire name included, names itself and the remedy: {error}"
);
}
counts
.ensure("tools/other")
.expect("an id seeds after the install");
let ensured: i64 = vm.eval("return tools.stats.tools['tools/other']");
assert_eq!(ensured, 0, "an id seeded later reads zero before any call");
}

#[test]
Expand Down
5 changes: 5 additions & 0 deletions crates/runvil-internal/model-client/src/client-tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ fn tool_arguments_view_exposes_no_raw_value() {
let mut names: Vec<_> = iter.collect();
names.sort_unstable();
assert_eq!(names, ["limit", "query"]);
assert_eq!(
args.names().size_hint(),
(2, Some(2)),
"the key iterator's hint matches the map it reads"
);
let json = args.to_json_string();
assert!(json.contains("\"query\":\"rust\""), "got {json}");
}
Expand Down
3 changes: 3 additions & 0 deletions crates/runvil-internal/prompt/src/body_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ mod tests {
assert_ne!(first, third);
assert_eq!(first, BodyText::from_owned("alpha".to_owned()));
assert_eq!(first, "alpha");
assert_ne!(first, "beta");
assert_eq!(&first, "alpha");
assert_ne!(&first, "beta");
assert_eq!(
format!("{second:?}"),
format!("{:?}", "alpha"),
Expand Down
31 changes: 18 additions & 13 deletions crates/runvil-internal/prompt/src/contract/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ fn an_arg_default_must_match_the_declared_type() {
("boolean", "'true'", "string"),
("integer", "1.5", "number"),
("string", "42", "integer"),
("string", "-3", "integer"),
("number", "[1]", "sequence"),
] {
let front = format!("args: {{ flag: {{ type: {kind}, default: {default} }} }}");
Expand All @@ -139,19 +140,19 @@ fn an_arg_default_must_match_the_declared_type() {
),
);
}
// A matching default passes.
let prompt = parse_ok(&prompt(
"args: { limit: { type: integer, default: 3 } }",
&[],
));
assert_eq!(
prompt
.frontmatter()
.args()
.get("limit")
.and_then(|a| a.default().cloned()),
Some(serde_yaml_ng::Value::Number(3.into()))
);
// A matching default passes, a negative integer included.
for default in [3_i64, -3] {
let front = format!("args: {{ limit: {{ type: integer, default: {default} }} }}");
let prompt = parse_ok(&prompt(&front, &[]));
assert_eq!(
prompt
.frontmatter()
.args()
.get("limit")
.and_then(|a| a.default().cloned()),
Some(serde_yaml_ng::Value::Number(default.into()))
);
}
}

#[test]
Expand Down Expand Up @@ -275,6 +276,10 @@ fn a_duplicate_role_key_is_rejected() {
&[],
));
assert_eq!(error.code, ErrorCode::FrontmatterInvalid, "{error}");
assert!(
error.message.contains("duplicate model role `strong`"),
"the refusal names the role: {error:?}"
);
}

#[test]
Expand Down
Loading
Loading