test(agent): add complex local agent regression coverage

This commit is contained in:
huanghuoguoguo
2026-07-19 07:12:19 +08:00
parent cfa7c7b336
commit 7706634c34
17 changed files with 563 additions and 5 deletions
@@ -441,10 +441,10 @@ async function ensureLocalAgentPipeline({
pipelineName,
runnerId,
}) {
const fakeProviderBaseUrl =
env.LANGBOT_E2E_FAKE_PROVIDER_BASE_URL ||
env.LANGBOT_FAKE_PROVIDER_BASE_URL ||
"";
// Fake-provider use must be explicit for this pipeline. The generic fake
// provider variables persist after load tests and must not silently replace
// a real LocalAgent model in later QA runs.
const fakeProviderBaseUrl = env.LANGBOT_E2E_FAKE_PROVIDER_BASE_URL || "";
let fakeModel = null;
if (fakeProviderBaseUrl) {
fakeModel = await ensureFakeProviderModel({
@@ -0,0 +1,62 @@
#!/usr/bin/env node
import { cp, mkdir, rm } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { env } from "node:process";
import { spawnSync } from "node:child_process";
import {
ensureEvidence,
evidencePaths,
loadEnvFiles,
writeResult,
} from "./lib/langbot-e2e.mjs";
await loadEnvFiles();
const paths = evidencePaths("reset-complex-agent-task");
await ensureEvidence(paths);
const scriptDir = dirname(fileURLToPath(import.meta.url));
const source = resolve(
scriptDir,
"../../skills/langbot-testing/fixtures/complex-agent-task/workspace",
);
const repo = env.LANGBOT_REPO || "";
const target = repo ? resolve(repo, "data/box/default/order-orchestrator") : "";
const result = {
source: "setup_automation",
case_id: "reset-complex-agent-task",
run_id: paths.runId,
status: "fail",
reason: "",
target,
initial_test_exit_code: null,
evidence_collected: ["filesystem"],
};
try {
if (!repo) throw new Error("LANGBOT_REPO is required.");
await rm(target, { recursive: true, force: true });
await mkdir(dirname(target), { recursive: true });
await cp(source, target, { recursive: true });
const baseline = spawnSync(
"python3",
["-m", "unittest", "discover", "-s", "tests", "-v"],
{ cwd: target, encoding: "utf8", timeout: 60_000 },
);
result.initial_test_exit_code = baseline.status;
result.initial_failure_preview = `${baseline.stdout || ""}\n${baseline.stderr || ""}`.slice(0, 4000);
if (baseline.error) throw baseline.error;
if (baseline.status === 0) throw new Error("Complex task baseline unexpectedly passes; the fixture must start failing.");
result.status = "pass";
result.reason = "Complex task workspace reset and failing baseline confirmed.";
} catch (error) {
result.status = /required|ENOENT/.test(error.message) ? "env_issue" : "fail";
result.reason = error.message;
}
await writeResult(paths, result);
console.log(JSON.stringify(result, null, 2));
process.exit(result.status === "pass" ? 0 : result.status === "env_issue" ? 2 : 1);
+38
View File
@@ -169,6 +169,7 @@
"langrag-sentinel-kb-discover",
"local-agent-basic-debug-chat",
"local-agent-combo-rag-compaction-tool-debug-chat",
"local-agent-complex-coding-task-debug-chat",
"local-agent-context-compaction-debug-chat",
"local-agent-effective-prompt-debug-chat",
"local-agent-multimodal-debug-chat",
@@ -1008,6 +1009,43 @@
"api_diagnostic"
]
},
{
"id": "local-agent-complex-coding-task-debug-chat",
"title": "Local Agent completes a multi-file coding task with iterative verification",
"mode": "agent-browser",
"area": "pipeline",
"type": "regression",
"priority": "p1",
"risk": "high",
"ci_eligible": false,
"tags": [
"local-agent",
"agentic",
"coding",
"sandbox",
"tools",
"long-running"
],
"automation": "scripts/e2e/pipeline-debug-chat.mjs",
"setup_automation": [
"node:scripts/e2e/ensure-local-agent-pipeline.mjs --write-env",
"node:scripts/e2e/reset-complex-agent-task.mjs"
],
"setup_provides_env": [
"LANGBOT_LOCAL_AGENT_PIPELINE_URL",
"LANGBOT_LOCAL_AGENT_PIPELINE_NAME"
],
"evidence_required": [
"ui",
"screenshot",
"console",
"network",
"backend_log",
"api_diagnostic",
"filesystem",
"metrics"
]
},
{
"id": "local-agent-context-compaction-debug-chat",
"title": "Local Agent compacts long Debug Chat history and preserves older facts",
@@ -0,0 +1,95 @@
id: local-agent-complex-coding-task-debug-chat
title: "Local Agent completes a multi-file coding task with iterative verification"
mode: agent-browser
area: pipeline
type: regression
priority: p1
risk: high
ci_eligible: false
tags:
- local-agent
- agentic
- coding
- sandbox
- tools
- long-running
skills:
- langbot-env-setup
- langbot-testing
env:
- LANGBOT_REPO
- LANGBOT_FRONTEND_URL
- LANGBOT_BACKEND_URL
- LANGBOT_LOCAL_AGENT_PIPELINE_URL
- LANGBOT_LOCAL_AGENT_PIPELINE_NAME
automation: scripts/e2e/pipeline-debug-chat.mjs
automation_env:
- LANGBOT_REPO
- LANGBOT_FRONTEND_URL
- LANGBOT_BACKEND_URL
- LANGBOT_BROWSER_PROFILE
- LANGBOT_CHROMIUM_EXECUTABLE
- LANGBOT_LOCAL_AGENT_PIPELINE_URL
- LANGBOT_LOCAL_AGENT_PIPELINE_NAME
automation_pipeline_url_env: LANGBOT_LOCAL_AGENT_PIPELINE_URL
automation_pipeline_name_env: LANGBOT_LOCAL_AGENT_PIPELINE_NAME
automation_expected_runner_id: "plugin:langbot-team/LocalAgent/default"
automation_runner_config_patch_json: '{"knowledge-bases":[],"context-window-tokens":200000,"context-keep-recent-tokens":120000,"context-reserve-tokens":20000,"context-summary-tokens":12000,"max-tool-iterations":40,"tool-execution-mode":"serial","timeout":900}'
automation_extensions_patch_json: '{"enable_all_plugins":false,"bound_plugins":[{"author":"langbot-team","name":"LocalAgent"}],"enable_all_mcp_servers":false,"bound_mcp_servers":[],"enable_all_skills":false,"bound_skills":[]}'
automation_restore_runner_config: "1"
automation_restore_extensions: "1"
automation_reset_debug_chat: "1"
automation_debug_chat_session_type: "person"
automation_prompt: "Work autonomously on the project at /workspace/order-orchestrator. Read TASK.md completely, inspect the code and tests, then follow the required workflow. Run the full baseline test suite before editing and record the failing test names. Fix only production code under order_engine; do not alter TASK.md, tests, or acceptance_check.py. This is an end-to-end task: continue through failures, diagnose them, make multi-file fixes, rerun all 12 tests until green, run acceptance_check.py, and create both result.json and AGENT_REPORT.md exactly as required. Do not stop after analysis or ask me for confirmation. When and only when all verification passes, return exactly COMPLEX_AGENT_TASK_OK tests=12 acceptance=PASS."
automation_expected_text: "COMPLEX_AGENT_TASK_OK tests=12 acceptance=PASS"
automation_response_timeout_ms: "900000"
automation_stream_output: "1"
automation_filesystem_checks_json: '[{"argv":["python3","skills/skills/langbot-testing/fixtures/complex-agent-task/verify.py","--workspace","${LANGBOT_REPO}/data/box/default/order-orchestrator"],"cwd":"${LANGBOT_REPO}","stdout_contains":"HOST_VERIFY_PASS tests=12 acceptance=PASS protected=PASS","exit_code":0,"timeout_ms":120000}]'
setup_automation:
- "node:scripts/e2e/ensure-local-agent-pipeline.mjs --write-env"
- "node:scripts/e2e/reset-complex-agent-task.mjs"
setup_provides_env:
- LANGBOT_LOCAL_AGENT_PIPELINE_URL
- LANGBOT_LOCAL_AGENT_PIPELINE_NAME
preconditions:
- "The local Box workspace is mounted at LANGBOT_REPO/data/box/default and exposed to native tools as /workspace."
- "The selected real model supports sustained function calling across file inspection, edits, and test reruns."
steps:
- "Reset the isolated order-orchestrator workspace and confirm its baseline test suite fails."
- "Open the Local Agent pipeline in Debug Chat and send one complete autonomous coding task."
- "Observe the Agent inspect requirements and code, run the baseline tests, edit multiple production files, and rerun verification after failures."
- "Require the visible final sentinel only after all 12 tests and the acceptance check pass."
- "Run the host-side verifier to confirm protected files were not changed and required artifacts are valid."
checks:
- "UI: A new Bot message contains exactly COMPLEX_AGENT_TASK_OK tests=12 acceptance=PASS."
- "Run ledger: The real model performs a sustained sequence of native read, exec, and edit/write tool calls rather than a short answer."
- "Filesystem: Host verification reruns all 12 tests and acceptance_check.py successfully."
- "Filesystem: TASK.md, tests/test_order_engine.py, and acceptance_check.py match the immutable fixture copies."
- "Artifacts: result.json and AGENT_REPORT.md contain the required completion evidence."
- "API diagnostic: The temporary runner config uses a 200000-token context window and restores the prior config after the run."
evidence_required:
- ui
- screenshot
- console
- network
- backend_log
- api_diagnostic
- filesystem
- metrics
diagnostics:
- "A generic explanation or early plan without filesystem artifacts is a product-path failure, even if the model response sounds plausible."
- "If the Agent edits protected tests, the host verifier fails even when the visible test command passes."
- "If the run times out, inspect agent_run_event to distinguish model latency from a stuck tool or runtime action."
success_patterns:
- "COMPLEX_AGENT_TASK_OK tests=12 acceptance=PASS"
- "HOST_VERIFY_PASS tests=12 acceptance=PASS protected=PASS"
failure_patterns:
- "Agent runner temporarily unavailable"
- "runner.timeout"
- "Action invoke_llm_stream call timed out"
- "Tool call iteration limit reached"
troubleshooting:
- local-agent-model-route-unavailable
- sandbox-native-tools-unavailable
- plugin-runtime-timeout
- debug-chat-history-contaminates-automation
@@ -30,6 +30,11 @@ automation_env:
- LANGBOT_LOCAL_AGENT_PIPELINE_NAME
automation_pipeline_url_env: LANGBOT_LOCAL_AGENT_PIPELINE_URL
automation_pipeline_name_env: LANGBOT_LOCAL_AGENT_PIPELINE_NAME
automation_expected_runner_id: "plugin:langbot-team/LocalAgent/default"
automation_extensions_patch_json: '{"enable_all_plugins":false,"bound_plugins":[{"author":"langbot-team","name":"LocalAgent"},{"author":"qa","name":"plugin-smoke"}],"enable_all_mcp_servers":false,"bound_mcp_servers":[],"enable_all_skills":false,"bound_skills":[]}'
automation_restore_extensions: "1"
automation_reset_debug_chat: "1"
automation_debug_chat_session_type: "person"
automation_prompt: "Call the qa_plugin_echo tool with exactly this text: plugin-tool-ok-local-agent. Return only the tool result."
automation_expected_text: "qa-plugin-smoke:plugin-tool-ok-local-agent"
automation_response_timeout_ms: "180000"
@@ -44,7 +49,7 @@ preconditions:
steps:
- "Install or enable the bundled qa-plugin-smoke fixture plugin."
- "Confirm /api/v1/tools or the plugin detail shows qa_plugin_echo."
- "Confirm the fixture plugin is bound to the target pipeline through Extensions, or that all plugins are enabled."
- "Temporarily bind only langbot-team/LocalAgent and qa/plugin-smoke; disable MCP servers and skills to isolate the tool surface."
- "Open the target local-agent pipeline."
- "Use runner Default or the pluginized langbot-team/LocalAgent runner."
- "Select a model with function-calling ability that is known to work with tools in the current environment."
@@ -52,6 +57,7 @@ steps:
- "Send: Call the qa_plugin_echo tool with exactly this text: plugin-tool-ok-local-agent. Return only the tool result."
checks:
- "UI: Debug Chat bot response contains qa-plugin-smoke:plugin-tool-ok-local-agent."
- "API diagnostic: pipeline-extensions-diagnostic.json shows only langbot-team/LocalAgent and qa/plugin-smoke were bound."
- "Logs: Backend logs show the plugin tool call was executed, not only listed."
- "Console: No unexpected frontend errors appear during Debug Chat."
evidence_required:
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
import argparse
import json
import subprocess
import sys
from pathlib import Path
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--workspace", required=True)
args = parser.parse_args()
workspace = Path(args.workspace).resolve()
baseline = Path(__file__).resolve().parent / "workspace"
for relative in ("TASK.md", "tests/test_order_engine.py", "acceptance_check.py"):
if (workspace / relative).read_bytes() != (baseline / relative).read_bytes():
raise AssertionError(f"protected fixture changed: {relative}")
tests = subprocess.run(
[sys.executable, "-m", "unittest", "discover", "-s", "tests", "-v"],
cwd=workspace,
text=True,
capture_output=True,
timeout=60,
)
if tests.returncode != 0:
raise AssertionError(f"unit tests failed:\n{tests.stdout}\n{tests.stderr}")
acceptance = subprocess.run(
[sys.executable, "acceptance_check.py"],
cwd=workspace,
text=True,
capture_output=True,
timeout=30,
)
if acceptance.returncode != 0 or "ACCEPTANCE_PASS" not in acceptance.stdout:
raise AssertionError(f"acceptance failed:\n{acceptance.stdout}\n{acceptance.stderr}")
result = json.loads((workspace / "result.json").read_text(encoding="utf-8"))
assert result["status"] == "pass"
assert result["tests_run"] == 12
assert result["acceptance"] == "PASS"
report = (workspace / "AGENT_REPORT.md").read_text(encoding="utf-8")
folded_report = report.casefold()
assert "initial" in folded_report and "fail" in folded_report, "report missing initial failure section"
for heading in ("root causes", "changed files", "verification"):
assert heading in folded_report, f"report missing section: {heading}"
assert report.rstrip().endswith("COMPLEX_AGENT_TASK_OK tests=12 acceptance=PASS")
print("HOST_VERIFY_PASS tests=12 acceptance=PASS protected=PASS")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,52 @@
# Order Orchestrator Repair Task
Repair the production code in `order_engine/` so the project satisfies all requirements below.
## Required workflow
1. Inspect the project and run the test suite before editing.
2. Record the initial failing test names in `AGENT_REPORT.md`.
3. Modify production code only. Do not edit `TASK.md`, `tests/`, or `acceptance_check.py`.
4. Re-run the full test suite after each logical fix until all 12 tests pass.
5. Run `python3 acceptance_check.py` and require `ACCEPTANCE_PASS`.
6. Write `result.json` and finish `AGENT_REPORT.md` with root causes, changed files, and verification commands.
Use these commands from `/workspace/order-orchestrator`:
```bash
python3 -m unittest discover -s tests -v
python3 acceptance_check.py
```
## Business rules
- Monetary input must be converted through `Decimal(str(value))`.
- Every public monetary result is rounded to two decimals with `ROUND_HALF_UP`.
- Supported customer tiers are `standard` (0%), `vip` (10%), and `premium` (15%). Unknown tiers raise `ValueError`.
- Discount is calculated from the rounded subtotal. Tax is calculated from the subtotal after discount. Total is discounted subtotal plus tax.
- Every line quantity must be a positive integer.
- Duplicate SKUs are allowed, but inventory checks and decrements must use their aggregate quantity.
- Inventory reservation is atomic: no stock changes when any requested SKU is missing or insufficient.
- Processing the same successful `order_id` again is idempotent and returns the original result without decrementing stock again.
- Failed orders are not cached. A corrected retry with the same `order_id` must be processed normally.
- Batch processing continues after rejected orders and returns one result per input order.
- Reports sort orders by `order_id`, count accepted and rejected orders, and sum revenue from accepted orders only.
- Report money values are strings with exactly two decimal places.
## Required artifacts
`result.json` must contain at least:
```json
{
"status": "pass",
"tests_run": 12,
"acceptance": "PASS"
}
```
The final line of `AGENT_REPORT.md` must be:
```text
COMPLEX_AGENT_TASK_OK tests=12 acceptance=PASS
```
@@ -0,0 +1,26 @@
from order_engine import Inventory, OrderLine, OrderProcessor, OrderRequest
def request(order_id, tier, tax, *lines):
return OrderRequest(order_id, tier, tax, tuple(lines))
inventory = Inventory({"BOOK": 6, "PEN": 3})
processor = OrderProcessor(inventory)
orders = [
request("c-3", "vip", "0.08", OrderLine("BOOK", 2, "12.50")),
request("c-2", "standard", "0", OrderLine("PEN", 2, "1.005"), OrderLine("PEN", 2, "1.005")),
request("c-1", "premium", "0.05", OrderLine("BOOK", 1, "20.00")),
]
results, report = processor.process_batch(orders)
assert [result.status for result in results] == ["accepted", "rejected", "accepted"]
assert report == {
"accepted": 2,
"rejected": 1,
"revenue": "42.15",
"orders": ["c-1", "c-2", "c-3"],
}
assert inventory.snapshot() == {"BOOK": 3, "PEN": 3}
assert processor.process(orders[0]) == results[0]
assert inventory.snapshot() == {"BOOK": 3, "PEN": 3}
print("ACCEPTANCE_PASS")
@@ -0,0 +1,5 @@
from .inventory import Inventory
from .models import OrderLine, OrderRequest, OrderResult
from .service import OrderProcessor
__all__ = ["Inventory", "OrderLine", "OrderProcessor", "OrderRequest", "OrderResult"]
@@ -0,0 +1,19 @@
from collections.abc import Mapping
from .models import OrderLine
class Inventory:
def __init__(self, stock: Mapping[str, int]):
self._stock = dict(stock)
def snapshot(self) -> dict[str, int]:
return dict(self._stock)
def reserve(self, lines: tuple[OrderLine, ...]) -> None:
for line in lines:
if line.sku not in self._stock:
raise ValueError(f"unknown sku: {line.sku}")
if self._stock[line.sku] < line.quantity:
raise ValueError(f"insufficient stock: {line.sku}")
self._stock[line.sku] -= line.quantity
@@ -0,0 +1,28 @@
from dataclasses import dataclass
from decimal import Decimal
@dataclass(frozen=True)
class OrderLine:
sku: str
quantity: int
unit_price: Decimal | str | int | float
@dataclass(frozen=True)
class OrderRequest:
order_id: str
customer_tier: str
tax_rate: Decimal | str | int | float
lines: tuple[OrderLine, ...]
@dataclass(frozen=True)
class OrderResult:
order_id: str
status: str
subtotal: str = "0.00"
discount: str = "0.00"
tax: str = "0.00"
total: str = "0.00"
error: str = ""
@@ -0,0 +1,26 @@
from decimal import Decimal
from .models import OrderLine
CENT = Decimal("0.01")
DISCOUNT_RATES = {
"standard": Decimal("0"),
"vip": Decimal("0.10"),
"premium": Decimal("0.15"),
}
def money(value: Decimal) -> Decimal:
return value.quantize(CENT)
def calculate_price(
lines: tuple[OrderLine, ...], customer_tier: str, tax_rate: Decimal | str | int | float
) -> tuple[Decimal, Decimal, Decimal, Decimal]:
discount_rate = DISCOUNT_RATES.get(customer_tier, Decimal("0"))
subtotal = money(sum((Decimal(line.unit_price) * line.quantity for line in lines), Decimal("0")))
tax = money(subtotal * Decimal(tax_rate))
discount = money((subtotal + tax) * discount_rate)
total = money(subtotal + tax - discount)
return subtotal, discount, tax, total
@@ -0,0 +1,15 @@
from decimal import Decimal
from .models import OrderResult
def build_report(results: list[OrderResult]) -> dict[str, object]:
accepted = [result for result in results if result.status == "accepted"]
rejected = [result for result in results if result.status == "rejected"]
revenue = sum((Decimal(result.total) for result in results), Decimal("0"))
return {
"accepted": len(accepted),
"rejected": len(rejected),
"revenue": str(revenue),
"orders": [result.order_id for result in results],
}
@@ -0,0 +1,25 @@
from .inventory import Inventory
from .models import OrderRequest, OrderResult
from .pricing import calculate_price
from .report import build_report
class OrderProcessor:
def __init__(self, inventory: Inventory):
self.inventory = inventory
def process(self, order: OrderRequest) -> OrderResult:
self.inventory.reserve(order.lines)
subtotal, discount, tax, total = calculate_price(order.lines, order.customer_tier, order.tax_rate)
return OrderResult(
order_id=order.order_id,
status="accepted",
subtotal=str(subtotal),
discount=str(discount),
tax=str(tax),
total=str(total),
)
def process_batch(self, orders: list[OrderRequest]) -> tuple[list[OrderResult], dict[str, object]]:
results = [self.process(order) for order in orders]
return results, build_report(results)
@@ -0,0 +1,103 @@
import unittest
from decimal import Decimal
from order_engine import Inventory, OrderLine, OrderProcessor, OrderRequest
from order_engine.pricing import calculate_price
from order_engine.report import build_report
from order_engine.models import OrderResult
def order(order_id, lines, tier="standard", tax="0"):
return OrderRequest(order_id, tier, tax, tuple(lines))
class PricingTests(unittest.TestCase):
def test_standard_total(self):
values = calculate_price((OrderLine("A", 2, "10.00"),), "standard", "0.08")
self.assertEqual(tuple(map(str, values)), ("20.00", "0.00", "1.60", "21.60"))
def test_vip_discount_precedes_tax(self):
values = calculate_price((OrderLine("A", 2, "25.00"),), "vip", "0.10")
self.assertEqual(tuple(map(str, values)), ("50.00", "5.00", "4.50", "49.50"))
def test_round_half_up_and_string_conversion(self):
values = calculate_price((OrderLine("A", 1, 1.005),), "standard", "0")
self.assertEqual(str(values[0]), "1.01")
def test_unknown_tier_is_rejected(self):
with self.assertRaisesRegex(ValueError, "unknown customer tier"):
calculate_price((OrderLine("A", 1, "1"),), "wholesale", "0")
class InventoryTests(unittest.TestCase):
def test_reservation_is_atomic(self):
inventory = Inventory({"A": 3, "B": 1})
with self.assertRaisesRegex(ValueError, "insufficient stock: B"):
inventory.reserve((OrderLine("A", 2, "1"), OrderLine("B", 2, "1")))
self.assertEqual(inventory.snapshot(), {"A": 3, "B": 1})
def test_duplicate_skus_are_aggregated(self):
inventory = Inventory({"A": 3})
with self.assertRaisesRegex(ValueError, "insufficient stock: A"):
inventory.reserve((OrderLine("A", 2, "1"), OrderLine("A", 2, "1")))
self.assertEqual(inventory.snapshot(), {"A": 3})
def test_successful_reservation_decrements_aggregate(self):
inventory = Inventory({"A": 5})
inventory.reserve((OrderLine("A", 2, "1"), OrderLine("A", 1, "1")))
self.assertEqual(inventory.snapshot(), {"A": 2})
def test_quantity_must_be_positive_integer(self):
for bad_quantity in (0, -1, 1.5, True):
with self.subTest(quantity=bad_quantity):
inventory = Inventory({"A": 5})
with self.assertRaisesRegex(ValueError, "positive integer"):
inventory.reserve((OrderLine("A", bad_quantity, "1"),))
self.assertEqual(inventory.snapshot(), {"A": 5})
class ServiceTests(unittest.TestCase):
def test_success_is_idempotent(self):
inventory = Inventory({"A": 5})
processor = OrderProcessor(inventory)
request = order("same", [OrderLine("A", 2, "4.00")])
self.assertEqual(processor.process(request), processor.process(request))
self.assertEqual(inventory.snapshot(), {"A": 3})
def test_failed_order_is_not_cached(self):
inventory = Inventory({"A": 1})
processor = OrderProcessor(inventory)
with self.assertRaises(ValueError):
processor.process(order("retry", [OrderLine("A", 2, "1")]))
accepted = processor.process(order("retry", [OrderLine("A", 1, "1")]))
self.assertEqual(accepted.status, "accepted")
self.assertEqual(inventory.snapshot(), {"A": 0})
def test_batch_continues_after_rejection(self):
processor = OrderProcessor(Inventory({"A": 2}))
results, report = processor.process_batch([
order("ok-2", [OrderLine("A", 1, "3")]),
order("bad", [OrderLine("MISSING", 1, "5")]),
order("ok-1", [OrderLine("A", 1, "7")]),
])
self.assertEqual([item.status for item in results], ["accepted", "rejected", "accepted"])
self.assertIn("unknown sku", results[1].error)
self.assertEqual(report["accepted"], 2)
self.assertEqual(report["rejected"], 1)
def test_report_is_sorted_and_accepted_revenue_only(self):
report = build_report([
OrderResult("z-order", "accepted", total="2.50"),
OrderResult("a-rejected", "rejected", total="999.00", error="no"),
OrderResult("m-order", "accepted", total="1.25"),
])
self.assertEqual(report, {
"accepted": 2,
"rejected": 1,
"revenue": "3.75",
"orders": ["a-rejected", "m-order", "z-order"],
})
if __name__ == "__main__":
unittest.main()
@@ -38,6 +38,7 @@ These browser cases are the minimum gate for a local-agent migration check:
| `local-agent-multimodal-debug-chat` | Image upload, structured input contents, and multimodal runner consumption | UI shows uploaded image and bot returns `IMAGE_OK`; backend receives an image input. |
| `local-agent-rag-multimodal-debug-chat` | RAG insertion while structured image input is present | UI shows uploaded image, bot returns the KB sentinel, and backend logs the same request with `[Image]`. |
| `local-agent-nonstreaming-debug-chat` | Debug Chat non-streaming UI delivery path | Bot returns `NONSTREAM_OK` with the Debug Chat stream switch disabled; runner-internal `invoke_llm` mode is covered by local-agent component tests using `runtime_metadata.streaming_supported=false`. |
| `local-agent-complex-coding-task-debug-chat` | Sustained native-tool planning, multi-file edits, iterative test recovery, and artifact production | Bot returns the completion sentinel only after host verification reruns 12 tests, acceptance, and protected-file checks. |
## Full Coverage Matrix
@@ -36,6 +36,8 @@ If the direct MCP fixture passes but `/api/v1/tools` still shows the old MCP nam
For a multimodal check, upload a small image and ask for a deterministic acknowledgement. Prefer the bundled 64x64 red-square fixture over a 1x1 image because some model providers reject tiny images before the runner path is exercised.
For sustained agentic behavior, run `local-agent-complex-coding-task-debug-chat`. It gives the runner one complete task in an isolated Box workspace, requires it to inspect a failing multi-file project, iterate on production fixes, rerun tests, and produce host-verifiable artifacts. Keep the normal context budget for this case; it is not a context-compaction or provider-concurrency probe.
For a Debug Chat non-streaming delivery check, disable the Debug Chat stream switch before sending the prompt. This validates the UI/adapter delivery path. Runner-internal non-streaming model invocation is covered by component tests that set `runtime_metadata.streaming_supported=false`.
## Timeout And Tool Regression Checks