mirror of
https://github.com/we-promise/sure.git
synced 2026-09-07 23:54:25 +00:00
* feat(bills): assistant and MCP tools for bills Last of three chunks carved out of #3083, stacked on the UI bundle. Exposes bills to the builtin assistant and to MCP clients. Everything here is gated behind preview features, so the tools are absent from tools/list until a user opts in. Seven tools: - get_bills, get_bill_details and get_paycheck_plan for reads - get_bill_audit, a deterministic review that surfaces likely duplicates, price changes, trials about to convert, upcoming renewals and long-overdue bills - create_bill, update_bill and record_bill_payment for writes Shared argument parsing, permission checks and error shapes live in BillsSupport, so every tool answers with the same {error, hint} contract the existing tools use, and a bad argument never aborts the turn. The write tools mutate financial records on a model's say-so, so they refuse rather than guess: a payment cannot exceed what its cycle still owes, a repeated settle will not quietly close next month, an unrecognized frequency is an error instead of a silent monthly default, and non-finite or negative amounts are rejected before they reach the database. The read tools say what they filtered. An empty result names the statuses that do hold matches, the paycheck plan discloses the unconfirmed series it excluded from spending headroom, and history and price-change windows report their real totals rather than letting a caller sum a truncated list. A not-found no longer returns the scoped relation's SQL, which handed any MCP client the access-control schema for the cost of a guessed id. The in-page AI helpers are not here. Smart fill and smart configuration are buttons on the bills pages, so they ship with the UI bundle along with the provider-side suggester they call. Suite 7,854 runs, 0 failures. Rubocop clean, eager loading verified. * Address the ready-review round * Reject an out-of-range audit lookback out loud * Speak the cycle remainder guard through the allocator locale
99 lines
3.3 KiB
Ruby
99 lines
3.3 KiB
Ruby
require "test_helper"
|
|
|
|
class AssistantConfigurableTest < ActiveSupport::TestCase
|
|
test "returns dashboard configuration by default" do
|
|
chat = chats(:one)
|
|
|
|
config = Assistant.config_for(chat)
|
|
|
|
assert_not_empty config[:functions]
|
|
assert_includes config[:instructions], "You help users understand their financial data"
|
|
end
|
|
|
|
test "returns intro configuration without functions" do
|
|
chat = chats(:intro)
|
|
|
|
config = Assistant.config_for(chat)
|
|
|
|
assert_equal [], config[:functions]
|
|
assert_includes config[:instructions], "stage of life"
|
|
end
|
|
|
|
test "instructions start with the byte-stable static block and end with session context" do
|
|
chat = chats(:one)
|
|
|
|
instructions = Assistant.config_for(chat)[:instructions]
|
|
|
|
assert instructions.start_with?(Assistant::Configurable::STATIC_INSTRUCTIONS)
|
|
assert_operator instructions.index("## Session context"), :>, instructions.index("### Rules about financial advice")
|
|
assert_includes instructions, "Today's date: #{Date.current}"
|
|
end
|
|
|
|
test "session context lists accounts and categories for a typical family" do
|
|
chat = chats(:one)
|
|
family = chat.user.family
|
|
|
|
# The roster only itemizes when the model's context window is comfortable
|
|
Setting.stubs(:llm_context_window).returns(128_000)
|
|
|
|
instructions = Assistant.config_for(chat)[:instructions]
|
|
|
|
visible_account = chat.user.accessible_accounts.visible.first
|
|
|
|
assert_includes instructions, "### Accounts"
|
|
assert_includes instructions, visible_account.name
|
|
assert_includes instructions, "### Categories"
|
|
assert_includes instructions, family.categories.first.name
|
|
assert_includes instructions, "Uncategorized"
|
|
end
|
|
|
|
test "session context collapses to counts for large account rosters" do
|
|
chat = chats(:one)
|
|
user = chat.user
|
|
|
|
Setting.stubs(:llm_context_window).returns(128_000)
|
|
|
|
26.times do |i|
|
|
user.family.accounts.create!(
|
|
name: "Roster Account #{i}",
|
|
balance: 100,
|
|
currency: "USD",
|
|
accountable: Depository.new
|
|
)
|
|
end
|
|
|
|
instructions = Assistant.config_for(chat)[:instructions]
|
|
|
|
assert_match(/\d+ accounts:/, instructions)
|
|
assert_not_includes instructions, "Roster Account 1:"
|
|
end
|
|
|
|
test "session context collapses to counts on small context windows" do
|
|
chat = chats(:one)
|
|
|
|
Setting.stubs(:llm_context_window).returns(2048)
|
|
|
|
instructions = Assistant.config_for(chat)[:instructions]
|
|
|
|
assert_match(/\d+ accounts:/, instructions)
|
|
assert_match(/\d+ categories\./, instructions)
|
|
end
|
|
# The tool caller returns {error:, hint:} instead of raising; without this
|
|
# rule the model sees those results as opaque data and never self-corrects.
|
|
test "instructions teach the model to follow tool error hints" do
|
|
config = Assistant.config_for(chats(:one))
|
|
|
|
assert_includes config[:instructions],
|
|
%(If a tool result contains an "error" and a "hint", follow the hint and retry once)
|
|
end
|
|
|
|
# Function names are plumbing; a reply that says "I ran get_bill_audit"
|
|
# reads like a stack trace, not an assistant.
|
|
test "instructions forbid naming internal tools in responses" do
|
|
config = Assistant.config_for(chats(:one))
|
|
|
|
assert_includes config[:instructions],
|
|
"Never mention internal tool or function names in your responses"
|
|
end
|
|
end
|