mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-29 05:37:14 +00:00
fix(agent): harden runner delivery validation
This commit is contained in:
@@ -60,6 +60,7 @@ import {
|
||||
} from "../scripts/e2e/lib/debug-chat.mjs";
|
||||
import {
|
||||
beginBackendLogCapture,
|
||||
clickFirstVisible,
|
||||
ensureAuthenticatedBrowser,
|
||||
finishBackendLogCapture,
|
||||
resolveLangBotRepo,
|
||||
@@ -68,6 +69,32 @@ import {
|
||||
|
||||
const root = process.cwd();
|
||||
|
||||
test("clickFirstVisible waits for a later visible DOM match", async () => {
|
||||
let pollCount = 0;
|
||||
let clickedIndex = -1;
|
||||
const emptyLocator = {
|
||||
count: async () => 0,
|
||||
nth: () => { throw new Error("empty locator has no children"); },
|
||||
};
|
||||
const textLocator = {
|
||||
count: async () => 2,
|
||||
nth: (index: number) => ({
|
||||
isVisible: async () => index === 1 && pollCount >= 1,
|
||||
click: async () => { clickedIndex = index; },
|
||||
}),
|
||||
};
|
||||
const page = {
|
||||
getByRole: () => emptyLocator,
|
||||
getByText: () => textLocator,
|
||||
waitForTimeout: async () => { pollCount += 1; },
|
||||
};
|
||||
|
||||
const clicked = await clickFirstVisible(page, ["Debug Chat"], 1_000);
|
||||
|
||||
assert.equal(clicked, "Debug Chat");
|
||||
assert.equal(clickedIndex, 1);
|
||||
});
|
||||
|
||||
test("repo root detects the skills tree before generated bin exists", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "lbs-root-no-bin-"));
|
||||
try {
|
||||
@@ -2064,6 +2091,25 @@ test("debug chat classifier passes when expected text appears in a new assistant
|
||||
assert.equal(result.new_assistant_message_count, 1);
|
||||
});
|
||||
|
||||
test("debug chat classifier rejects a matching assistant message that is not final", () => {
|
||||
const result = classifyDebugChatResult({
|
||||
beforeText: "",
|
||||
afterText: "Bot: E2E_OK:skill",
|
||||
expectedText: "E2E_OK:skill",
|
||||
prompt: "Run the task",
|
||||
latestExpectedLeaf: "E2E_OK:skill",
|
||||
latestFailureLeaf: "",
|
||||
beforeMessages: [],
|
||||
afterMessages: [{ role: "assistant", text: "E2E_OK:skill" }],
|
||||
latestAssistantText: "E2E_OK:skill",
|
||||
latestAssistantIsFinal: false,
|
||||
});
|
||||
|
||||
assert.equal(result.status, "fail");
|
||||
assert.match(result.reason, /was not final/);
|
||||
assert.equal(result.latest_assistant_is_final, false);
|
||||
});
|
||||
|
||||
test("debug chat classifier accepts formatted responses containing every required fragment", () => {
|
||||
const expectedTexts = ["MULTITOOL_COMBO_FINAL", "passcode-6718", "rag-7421", "tool-a", "tool-b"];
|
||||
const result = classifyDebugChatResult({
|
||||
|
||||
@@ -9,6 +9,8 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts" / "e2e"))
|
||||
|
||||
from agent_run_ledger_policy import ( # noqa: E402
|
||||
classify_invalid_tool_argument_errors,
|
||||
classify_tool_authorization,
|
||||
invalid_tool_argument_error_signal,
|
||||
load_ledger_json,
|
||||
)
|
||||
|
||||
@@ -64,6 +66,34 @@ class AgentRunLedgerPolicyTests(unittest.TestCase):
|
||||
self.assertEqual(failures[0]["kind"], "invalid_json")
|
||||
self.assertEqual(failures[0]["field"], "agent_run_event[10].data_json")
|
||||
|
||||
def test_detects_pydantic_tool_argument_validation_error(self) -> None:
|
||||
signal = invalid_tool_argument_error_signal(
|
||||
"1 validation error for RetrieveKnowledgeArgs\n"
|
||||
"kb_id\n Field required [type=missing, input_value={'top_k': 3}, input_type=dict]"
|
||||
)
|
||||
|
||||
self.assertEqual(signal, "1 validation error for RetrieveKnowledgeArgs")
|
||||
|
||||
def test_ignores_unrelated_tool_execution_error(self) -> None:
|
||||
self.assertEqual(invalid_tool_argument_error_signal("Exit code 1"), "")
|
||||
|
||||
def test_strict_mode_rejects_tool_absent_from_host_authorization(self) -> None:
|
||||
call = {"tool_call_id": "call-1", "tool_name": "Read"}
|
||||
|
||||
failures, warnings = classify_tool_authorization([call], authorization_mode="strict")
|
||||
|
||||
self.assertEqual(failures, [{"kind": "unauthorized_tool_calls", "calls": [call]}])
|
||||
self.assertEqual(warnings, [])
|
||||
|
||||
def test_runner_native_mode_records_non_host_tool_as_warning(self) -> None:
|
||||
call = {"tool_call_id": "call-1", "tool_name": "Read"}
|
||||
|
||||
failures, warnings = classify_tool_authorization([call], authorization_mode="runner-native")
|
||||
|
||||
self.assertEqual(failures, [])
|
||||
self.assertEqual(warnings[0]["kind"], "runner_native_tool_calls")
|
||||
self.assertEqual(warnings[0]["calls"], [call])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user