import { defineConfig } from "windupjs";
export default defineConfig({
baseUrl: "http://localhost:3000",
llm: {
provider: "google",
model: "gemini-3.1-flash-lite",
// Several providers at once — pick per run with --llm (see "LLM providers"):
providers: { openai: { model: "gpt-5-mini" } },
},
scenarios: "e2e/scenarios",
framework: "react-router", // detected by init; used by scan
// browser: "chromium", // or "firefox" / "webkit" (need: npx playwright install <name>)
scan: {
llmAssist: { enabled: true, maxCalls: 20 }, // hard cost cap per scan
},
// Project manifest: team-provided knowledge injected into the planner prompt.
context: {
conventions: ["every interactive element has a data-testid"],
credentials: {
qa: { user: "ENV:QA_USER", password: "ENV:QA_PASSWORD" },
},
vocabulary: { "order": "the Order entity, screen /orders" },
},
});
context.credentials maps account names to ENV references. When a task mentions the account, the plan uses value_ref — manifest credentials take precedence even if the page displays values, and the planner is forbidden from inventing ENV names.
LLM-assist (scan layer 3) reads files the static layers couldn’t resolve (dynamically built routes, indirect components), capped by maxCalls. Results are remembered per file hash — unchanged files never cost again. Costs are recorded in the ledger and shown by windup costs.
What lives where
Path
Contents
Commit?
windup.config.ts
Configuration
✅
e2e/scenarios/*.json
Your tests, in natural language
✅
e2e/fragments/*.json
Curated reusable blocks
✅
windup.credentials.json
Account → ENV-name mapping (no values)
✅
.env.local
Credential values
❌ (auto-gitignored; CI uses secrets with the same names)
.windup/
Derived state: plan cache, run ledger, site map, reports