14 KiB
Test Plan
This document captures the testing strategy for Data Formulator. It is meant to be a living document — update it as coverage grows and priorities shift.
Current State
Directory layout
All tests live under the repo-level tests/ directory (previously some lived
in py-src/tests/ — those have been consolidated here).
tests/
conftest.py # adds py-src to sys.path
test_plan.md # ← this file
run_test_dbs.sh # unified helper: start/stop/test all DBs
backend/ # included in default pytest (no Docker needed)
unit/ # pure functions, no Flask/network
security/ # security-focused tests
test_sandbox_security.py # sandbox confinement (file write, process exec)
test_code_signing.py # HMAC sign/verify for transformation code
test_sanitize.py # error message redaction
test_auth.py # identity extraction & namespace isolation
test_global_model_security.py # credential isolation, error sanitization
test_url_allowlist.py # SSRF protection for user-provided api_base
integration/ # Flask routes, workspace, sandbox
test_sandbox.py # sandbox functional tests (transforms, errors)
...
contract/ # API boundary guarantees
benchmarks/ # performance benchmarks (not in CI)
plugin/ # data loader tests (requires Docker, run separately)
mysql/ # MySQL loader (Dockerfile + init.sql)
mongodb/ # MongoDB loader (Dockerfile + init_data.js)
postgres/ # PostgreSQL loader (Dockerfile + init.sql)
bigquery/ # BigQuery emulator (Dockerfile + init_data.yaml)
cosmosdb/ # Cosmos DB emulator (Dockerfile + seed_data.py)
run_test_dbs.sh # unified script to start/stop/test databases
frontend/
setup.ts # jest-dom matchers
unit/ # vitest tests for src/
Running tests
# Default: backend + frontend (no Docker needed, fast)
pytest
# Plugin tests: data loader integrations (requires Docker)
./tests/database-dockers/run_test_dbs.sh start # start all test databases
./tests/database-dockers/run_test_dbs.sh test # run all loader tests
./tests/database-dockers/run_test_dbs.sh stop # tear down
# Or one-shot per service
./tests/database-dockers/run_test_dbs.sh test mysql
What exists today
| Layer | Location | Runner | Count |
|---|---|---|---|
| Backend unit | tests/backend/unit/ |
pytest | 20 files |
| Backend security | tests/backend/security/ |
pytest | 9 files |
| Backend integration | tests/backend/integration/ |
pytest | 8 files (7 route tests + sandbox) |
| Backend contract | tests/backend/contract/ |
pytest | 2 files |
| Backend benchmarks | tests/backend/benchmarks/ |
manual | 2 files |
| Plugin (data loaders) | tests/plugin/ |
pytest (manual) | 7 suites (requires Docker) |
| Frontend unit | tests/frontend/unit/ |
vitest | 4 files |
tests/backend/ runs by default with pytest — no Docker required.
tests/plugin/ is excluded from default runs and requires
./tests/run_test_dbs.sh to start test databases first.
Key gaps
- Session routes — save / load / export / import untested
- Agent pipelines — no isolated tests for any agent class
- Data loaders — S3, Azure Blob, Kusto, Athena, MSSQL not covered
- Workspace factory — backend selection logic untested
- Frontend components — almost no component or hook tests
- Frontend state — only 1 Redux selector test
- Vega-Lite assembly —
create_vl_plotsuntested - Semantic types — type resolution and classification untested
Proposed Test Categories
P0 — Security & correctness (add first)
These protect against regressions that could leak secrets, execute tampered code, or corrupt user data.
1. Code signing (code_signing.py) ✅ covered
Tests in tests/backend/security/test_code_signing.py:
sign/verify round-trip, tampered code/signature rejection, empty inputs,
whitespace sensitivity, Unicode, sign_result() helper.
2. Auth & identity (auth.py) ✅ covered
Tests in tests/backend/security/test_auth.py:
Azure principal → user: prefix, browser → browser: prefix, client
cannot spoof user: namespace, Azure takes priority, missing headers,
malformed values rejected, validation edge cases.
3. Error sanitization (sanitize.py) ✅ covered
Tests in tests/backend/security/test_sanitize.py:
API key redaction, path stripping (Unix/Windows/tmp), stack trace removal,
HTML escaping, truncation, edge cases (empty, Unicode).
4. Sandbox confinement (sandbox/) ✅ covered
Tests in tests/backend/security/test_sandbox_security.py:
file write blocked (open, csv), process exec blocked (os.system, popen,
execvp, spawnlp, kill, sys.modules bypass, putenv), Docker workspace
read-only mount.
4b. URL allowlist (url_allowlist.py) ✅ covered
Tests in tests/backend/security/test_url_allowlist.py:
open mode (env unset, all URLs allowed), enforce mode (matching patterns
pass, unlisted/private IPs rejected), empty api_base always allowed,
case insensitivity, glob edge cases, pattern loading.
4c. scratch_serve path safety ✅ covered
Tests in tests/backend/security/test_scratch_serve.py:
path traversal and missing files return HTTP 200 with status: "error" per the
unified error protocol, normal file → 200, and send_file usage is verified
instead of send_from_directory (FINDING-1 from issue-002).
4d. Agent tool path confinement ✅ covered
Tests in tests/backend/agents/test_tool_path_safety.py:
_tool_read_file / _tool_list_directory / _preview_scratch_files reject
traversal paths (../../etc/passwd), normal workspace paths work (FINDING-2 from issue-002).
4e. local_folder deployment restriction ✅ covered
Tests in tests/backend/security/test_local_folder_deployment.py:
multi-user mode disables local_folder connector, local mode keeps it,
create_connector rejects disabled types (FINDING-3 from issue-002).
4f. Workspace path safety ✅ covered
Tests in tests/backend/data/test_workspace_path_safety.py:
Workspace.__init__ sanitizes traversal-shaped identities, rejects root-equivalent
workspace paths, and delegates legacy root checks to ConfinedDir instead of
using str.startswith (FINDING-4 from issue-002).
4g. Startup safety checks ✅ covered
Tests in tests/backend/security/test_startup_safety.py:
multi-user + no sandbox → logger.critical warning, safe configs → no warning
(FINDING-5 from issue-002).
5. Session sensitive-field stripping (session_routes.py)
_strip_sensitive()removes model credentials, identity, API keys- round-trip: save → load preserves data but strips secrets
P1 — Core data pipeline (add next)
6. Workspace operations (datalake/workspace.py)
save_table()→load_table()round-trip preserves datalist_tables()reflects creates and deletesget_table_metadata()returns correct schema- concurrent metadata updates use file locking (atomic writes)
- temp file cleanup on workspace close
- workspace isolation between users
7. File manager (datalake/file_manager.py)
- encoding detection for UTF-8, UTF-16, Shift-JIS, GB2312
- BOM handling
- large file upload (near MAX_CONTENT_LENGTH boundary)
- file type validation (reject unsupported formats)
- (some coverage already exists — extend, don't duplicate)
8. Metadata persistence (datalake/metadata.py)
- YAML save / load round-trip
- cross-platform file locking
- concurrent writers don't corrupt metadata
- schema migration: old metadata format still loads
9. Table routes — end-to-end (tables_routes.py)
/create-tablewith JSON, CSV, Parquet payloads/parse-filewith Excel, CSV, TSV files/delete-tablecleans up Parquet + metadata/sample-tablereturns correct row count and schema- DuckDB sampling for large files
/open-workspaceinitializes workspace correctly- (some coverage exists — check and extend)
10. Model registry (model_registry.py)
- env-var scanning for each provider (OpenAI, Azure, Anthropic, Gemini, Ollama)
list_public()never exposes API keys- custom model endpoint configuration
- missing / malformed env vars → graceful degradation
- (some coverage exists — check and extend)
P2 — Agent layer (mock LLM calls)
11. Agent utilities
agent_utils.py: JSON extraction from LLM responses, data summary generationagent_utils_sql.py: DuckDB view creation, quoted identifiers for Unicodeagent_language.py: English / Chinese prompt instruction buildingagent_diagnostics.py: diagnostics payload builder captures correct fields- (some coverage exists for diagnostics and SQL utils)
12. Individual agents (mock Client / LiteLLM)
DataRecAgent— given a mock LLM response, produces correct Vega-Lite spec + codeDataTransformationAgent— generates valid Python transformation codeDataLoadAgent— infers semantic types and suggests table names from raw dataCodeExplanationAgent— produces explanation from code inputChartInsightAgent— generates insight from chart specDataAgent— observe→think→act loop terminates correctly
Focus on: prompt assembly, response parsing, error recovery, repair loops.
13. Semantic types (agents/semantic_types.py)
- type classification: temporal, categorical, measure sets are disjoint
- type resolution from sample data (dates, currencies, percentages)
- consistency with frontend
type-registry.tsconstants
P3 — Visualization & workflows
14. Vega-Lite chart assembly (workflows/create_vl_plots.py)
- field type coercion (quantitative, nominal, temporal, ordinal)
- encoding shelf assembly from semantic fields
- bar, line, scatter, area chart generation
- multi-layer charts
- invalid encoding combinations → graceful error
15. Chart semantics (workflows/chart_semantics.py)
- encoding channel validation
- specification completeness checks
P4 — Data loader integrations
16. Data loader framework ✅ partially covered
Integration tests with Dockerized services exist for MySQL, MongoDB,
PostgreSQL, and BigQuery (in tests/backend/integration/test_*/).
A standalone MySQL-to-datalake round-trip test also exists
(test_mysql_datalake.py). Still needed:
BaseDataLoaderinterface contract test (shared across all loaders)- Remaining loaders (require mocked connections or emulators):
- MSSQL: SQL query → DataFrame
- S3: file listing, download, Athena integration
- Azure Blob: container listing, file download
- Kusto: KQL query → DataFrame
- Athena: query execution, result fetching
- Connection error handling for each loader
- Credential validation
P5 — Frontend unit tests
17. Data utilities (src/data/)
- type coercion functions (extend existing
coerceDate.test.ts) - Excel cell resolution (extend existing)
- data transformation helpers
18. Redux state (src/app/)
- all exported selectors (extend existing
dfSelectors.test.ts) - reducer logic for table CRUD, model selection, session state
- state persistence (redux-persist integration)
19. View utilities & components
ViewUtils.tsxhelper functionsChartRenderService.tsx— Vega-Embed renderingModelSelectionDialog— model filtering and selectionDataView— table rendering with filtering/sortingEncodingBox— encoding shelf interactionsChatDialog— message handling
20. Internationalization (src/i18n/)
- all i18n keys have translations in both English and Chinese
- no missing keys in either locale
P6 — End-to-end workflows (optional / future)
21. Full pipeline tests
- upload CSV → create table → derive data → generate chart → export
- session save → reload → resume workflow
- multi-table join → visualization
These would likely use Playwright or Cypress and are out of scope initially.
Test Infrastructure
Fixtures to build
app_client— Flask test client with in-memory workspace (exists in some integration tests, should be standardized)tmp_workspace— temporary workspace directory, cleaned up after testmock_llm_client— patchedClientthat returns canned LLM responsessample_dataframes— standard DataFrames for table operations (small, wide, Unicode columns, empty)sample_files— CSV/Excel/Parquet fixture files (some exist intests/backend/fixtures/)
Markers
| Marker | Meaning |
|---|---|
@pytest.mark.backend |
Backend test (already configured) |
@pytest.mark.contract |
API contract test (already configured) |
@pytest.mark.slow |
Tests that take > 5s (DB, Docker sandbox) |
@pytest.mark.requires_docker |
Needs Docker daemon running |
@pytest.mark.requires_llm |
Needs real LLM API key (skip in CI by default) |
CI considerations
- P0–P2 tests should run on every PR (< 2 min target)
- P4 data loader tests need mock services or are skipped in CI
- P6 e2e tests run on merge to main only
- Frontend tests run via
npm testin CI
How to prioritize
- Start with P0 — these guard against security issues and data corruption
- Move to P1 — these catch breakages in daily development workflows
- P2 covers the AI agent layer — use mocks, not real LLM calls
- P3–P5 fill remaining coverage gaps
- P6 is aspirational — add when the team has bandwidth
Open questions
- Should sandbox tests use Docker in CI, or only test
LocalSandbox? - Do we want snapshot tests for Vega-Lite spec output?
- Should agent tests pin specific mock LLM responses, or use property-based testing?
- Is there a need for load/stress testing on the streaming endpoints?
- Should frontend component tests use shallow rendering or full mount?
- Do we need contract tests for the frontend ↔ backend API boundary (OpenAPI schema)?