Mock LLM for regression tests
Run the deterministic OpenAI Chat Completions endpoint with Python 3.11+, without API credentials or third-party dependencies:
python3 tools/mock_llm.py --port 18080 --record /tmp/langbot-mock-requests.jsonl
In LangBot, create an openai-chat-completions Provider whose base URL is
http://127.0.0.1:18080/v1 and whose API key is any test-only value. Use a model
from GET /v1/models. The loopback address works when Core and this tool run on
the same host; containers must use an address reachable from their own network.
| Model | Behavior |
|---|---|
mock-chat |
Returns LANGBOT_CHAT_OK, including SSE streaming |
mock-rag |
Requires the golden document sentinel in the model input, then returns LANGBOT_RAG_OK |
mock-embedding |
Returns deterministic normalized token-hash vectors from /v1/embeddings |
mock-box |
Calls exec twice to write/read /workspace/mock-llm.txt, then returns LANGBOT_BOX_OK |
mock-401, mock-429, mock-503 |
Returns the selected HTTP failure |
mock-slow |
Delays the response by two seconds |
mock-slow-stream |
Delays each SSE chunk by 0.2 seconds |
mock-rag requires aurora-parser-rag-9137 in the submitted messages. The
golden HTML fixture
contains this marker; retrieve it into the model context without including it
in the user's question.
For mock-box, enable Box in the Runner and expose the native exec tool. Each
tool result must contain LANGBOT_BOX_SENTINEL before the next step is returned.
Tool-call IDs and fragmented streaming arguments use the normal OpenAI protocol.
Scenario progress comes from each request's current conversation turn, so
concurrent sessions and retries do not consume a shared script counter.
Add fixed response/tool sequences with --scenarios path/to/scenarios.json, using
mock_llm_scenarios.json as the example. expect_tool_output checks a substring
in the previous tool results; expect_prompt checks the submitted messages.
The server returns tool calls; it never executes
their commands. Request recordings contain prompts, tool results, responses,
status, and disconnects, but no HTTP headers. Use test data only.
Run the focused HTTP checks on the Linux test host:
python3 -m unittest discover -s tools -p test_mock_llm.py -v
These checks validate the mock server. A LangBot regression must additionally verify real tool outputs and retrieved document IDs/content. Token usage is synthetic, and this endpoint does not evaluate answer quality or embedding models.
mock-embedding accepts string/batch inputs, float/base64 encoding, and optional
dimensions (default 384). It tests the embedding protocol and RAG plumbing;
token hashes do not establish semantic retrieval quality.
Normal traffic and resource collection
regression_traffic.py executes a JSON list of normal lbctl cases at a selected
rate. Each case has a name, CLI arguments, and an expected response substring:
[{"name":"chat","args":["pipeline","run","PIPELINE_UUID","--message","Hello"],"expect":"LANGBOT_CHAT_OK"}]
Prepare a Workspace-bound CLI context and its API key environment variable, then run on the Linux test host:
python3 tools/regression_traffic.py \
--lbctl /path/to/lbctl --config /path/to/lbctl.yaml \
--cases /path/to/cases.json --output /path/to/new-results-directory \
--duration 600 --rps 2 --workers 4 --cooldown 30 \
--pid core=CORE_PID --pid mock=MOCK_PID \
--health core=http://127.0.0.1:5300/healthz \
--health mock=http://127.0.0.1:18080/healthz
Add --container CONTAINER_ID for Docker CPU/memory/PID statistics. The executing
account must be able to inspect the selected process trees and Docker daemon.
Resource collection reuses scripts/cloud_runtime_soak.py; it does not require
cgroup v2. PID resource totals include descendants, so select disjoint trees.
The result directory contains requests.jsonl, resources.jsonl, progress.json,
and summary.json. The requested rate is a target: four workers bound concurrency,
and slow requests lower the achieved rate. Requests with unknown outcomes are
recorded without retry. A command fails its business check if it returns an error
or omits the expected response. Health/resource collection failures also make the
run fail. API key values are redacted from recorded CLI output.
Latency percentiles include the entire CLI invocation and use successful
requests only. Achieved request rate is requests / load_elapsed_seconds.
health_samples counts sampling rounds; health_failures counts rounds with
any failed health probe or process/container resource read.
Configured regression suites
regression_suite.py turns a JSON profile into a repeatable
resource preparation, traffic, observation, verdict, and cleanup sequence. Start
with regression_profiles/normal.json, which
defines the chat/Box/RAG mix from the ten-minute trial.
Use a dedicated QA Workspace. First provision the instance with lbctl install
or your source deployment, enable Docker Box, start the mock and embedding
backends, and prepare a Workspace-bound lbctl config. Set the profile's CLI
config path, backend URLs, and three plugin archive paths. The sample embedding
endpoint must serve all-MiniLM-L6-v2; the mock serves generation only. Relative
paths resolve against the profile directory, so adjust them if you move it.
Run on the Linux test host:
python3 tools/regression_suite.py validate --profile tools/regression_profiles/normal.json
python3 tools/regression_suite.py run \
--profile tools/regression_profiles/normal.json --output .qa/regression/run-001
validate checks profile syntax and references without contacting the instance.
The output directory for run must be new. CLI options --duration, --rps, and
--workers override those values in every phase. No additional Python dependency
is needed.
The profile controls:
plugins: local archives or GitHub/marketplace request files, installed withlbctl plugin install ... --wait.resources: Providers, models, knowledge bases, document ingestion, and Pipelines, created in dependency order.${resource_id}refers to a UUID returned by an earlier creation;${workspace_uuid}is the current Workspace. Pipeline config patches merge with actual server defaults. Existing resources can instead be supplied throughvars, leavingresourcesempty.cases: CLI argument arrays and expected business text. Integerweightvalues set the mix;2/1/1/1means40/20/20/20. An optionalbodyobject becomes the case's request file. All workers share the weighted sequence.phases: each phase has a name, duration, target RPS, worker limit, and optional cooldown. Multiple phases provide a step load. In-flight requests drain at the end; the concurrency limit can reduce achieved RPS.observe: health URLs, host PIDs, PID files, and Docker container names/IDs. Set PIDs for disjoint process trees to avoid counting descendants twice.thresholds: minimum achieved/target RPS ratio and maximum P95 seconds per case, in addition to the existing business and health/resource checks. The sample limits are starting acceptance settings, not established product SLOs.cleanup:on-success(default),always, ornever. Only recorded resources created by this run are deleted, in reverse order. Plugins and the existing instance remain installed. Failed/unknown creations are not automatically retried; inspect the instance if a creation outcome is unknown.
Failed runs retain resources by default. After inspection, clean them with the same profile and credentials:
python3 tools/regression_suite.py cleanup \
--profile tools/regression_profiles/normal.json --output .qa/regression/run-001
Cleanup checks the Workspace against owned.json and records remaining items
after partial failures. Keep owned.json until cleanup completes. Each run also
contains profile.json, resolved resources.json, and one directory per phase
with the original request/resource records. The root summary.json includes
target/achieved RPS, phase results, threshold failures, and the final verdict.
The regression_suite.py entry reuses an existing instance and backend processes.
Use the complete lifecycle Job below to own deployment, credentials and backends.
Cases still use the existing text oracle and new-session CLI entry; continuous
conversations, IM adapter acceptance, and resource-leak verdicts need separate
scenarios. Test the suite plumbing on Linux with:
python3 -m unittest discover -s tools -p test_regression_suite.py -v
Complete lifecycle Job
regression_job.py owns the deployment as well as the suite.
Use regression_profiles/job.json to pin the Core
Release and select the suite and plugin input directory. The job runs this chain:
lbctl install --profile allcreates a fresh deployment inside the run directory.- A Compose override starts our mock backend on the deployment's private network and injects a generated test-only API key. The Job binds its own CLI context.
- The suite installs plugins, creates resources and ingests documents. Each case runs once as warmup before measured traffic starts.
- Configured load phases run with health, process-tree and container sampling.
- Service-log tails and raw request/resource records are retained. Successful
Jobs stop the deployment, remove only sandboxes mounted under their own data
root, and use
lbctl uninstall --purge-datato reclaim the deployment.
Both generation and embedding use our mock backend, with no external model
service or real model credentials. Plugin archives, the pinned Core image,
lbctl, Docker and Compose v2 are Job inputs/prerequisites. This wrapper uses
PyYAML, already included in LangBot's dependencies, to verify the CLI's deployment
record and recover ownership after an interrupted install.
Run on a Linux host with Docker access and permission to inspect the selected
container processes. Use an idle dedicated Docker daemon: current Box runtime
startup cleanup can remove sandboxes from another runtime. The Job refuses to
start when existing Box sandboxes are present. The current lbctl installer
also reserves host port 5401 and 2280–2285, independently of its HTTP port.
Set the plugin directory and the lbctl executable in job.json, then run:
uv run --no-sync python tools/regression_job.py validate --profile tools/regression_profiles/job.json
uv run --no-sync python tools/regression_job.py run \
--profile tools/regression_profiles/job.json --output .qa/regression/job-001
--duration, --rps and --workers override every load phase. Each output
directory must be new. Job cleanup policy is on-success, always, or never;
it governs the whole deployment, whereas suite cleanup governs API resources
when a suite runs on an existing deployment. purge_data: false preserves the
deployment data after removing containers. Failed Jobs retain their environment
by default; after inspection use:
uv run --no-sync python tools/regression_job.py cleanup \
--output .qa/regression/job-001
The root summary.json is the final Job verdict; suite/ contains the measured
results, mock/requests.jsonl contains backend recordings, and services.log
contains up to 10,000 log lines per service. Generated credentials and deployment
inputs stay in .private/; share the result files, not that directory. A Job has
its own CLI locator/config and records its deployment identity before cleanup.
Changing that identity makes cleanup fail closed. Cleanup uses the saved Job
state and does not require the original profile or plugin input files.
The Job replaces the real embedding model with mock-embedding. This measures
LangBot/plugin/Box/RAG overhead, not real-model inference cost or retrieval
quality. Run the Job plumbing checks on Linux with:
uv run --no-sync python -m unittest discover -s tools -p 'test_*.py' -v