mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-16 08:02:28 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e915dc4265 | ||
|
|
de13ee9426 | ||
|
|
3508c86dcb | ||
|
|
98878ffc90 |
@@ -55,55 +55,18 @@ The body MUST be explicit about what changed. Structure:
|
||||
|
||||
The harness/tooling that invoked the skill may add its own attribution trailer; the skill itself does not prescribe one.
|
||||
|
||||
## Screenshots (required for frontend changes)
|
||||
|
||||
If `git diff main...HEAD --name-only` matches `^frontend/`, the PR body **must** include
|
||||
screenshots of the affected UI. Skip only when there is no visible UI effect (types,
|
||||
tests, build config) — and say so in the body.
|
||||
|
||||
1. Verify the change in the browser (AGENTS.md → "Verifying Frontend Changes").
|
||||
2. Screenshot each affected page with `mcp__playwright__browser_take_screenshot` (save to a file).
|
||||
3. Host each image and get its Markdown embed by pushing to the public
|
||||
`windmill-labs/agent-screenshots-internal` repo. **Pipe base64 through stdin** —
|
||||
passing it as `-f content=…` fails with `argument list too long` on real images:
|
||||
|
||||
```bash
|
||||
REPO=windmill-labs/agent-screenshots-internal
|
||||
IMG=screenshot.png # repeat per page
|
||||
DEST="shots/$(git branch --show-current)/$(date +%s)-$(basename "$IMG")"
|
||||
base64 -w0 "$IMG" | jq -Rs --arg m "add $DEST" '{message:$m, content:.}' \
|
||||
| gh api -X PUT "repos/$REPO/contents/$DEST" --input - >/dev/null
|
||||
echo ""
|
||||
```
|
||||
Derive `$DEST` from the file name (as above) so distinct pages never collide — a
|
||||
fixed name would make same-second uploads reuse one path, and the second `PUT`
|
||||
then 422s (the Contents API needs the existing file's `sha` to overwrite).
|
||||
4. Put the printed `` lines under a `## Screenshots` heading in the PR body.
|
||||
|
||||
Requires `gh` (`repo` scope), `jq`, `base64` — all in the devShell. The host repo is
|
||||
public (so the raw URLs render for reviewers without a token) and its history is
|
||||
permanent — **never screenshot pages that show secrets or sensitive values** (workspace
|
||||
variables, resource values, instance settings, OAuth/SMTP config); deleting the file
|
||||
can't undo an accidental capture. (GitHub's drag-and-drop uploader needs a browser
|
||||
session and can't be driven from a token.)
|
||||
|
||||
If `gh` can't push to the host repo (e.g. a CI token scoped only to `windmill`), do
|
||||
**not** fail the PR or skip silently — hand the upload to the user, who has push access,
|
||||
and continue once they confirm it's done.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
1. Run `git status` to check for uncommitted changes
|
||||
2. Run `git log main..HEAD --oneline` to see all commits in this branch
|
||||
3. Run `git diff main...HEAD` to see the full diff against main
|
||||
4. **Invoke the `local-review` skill** before creating the PR (`/local-review` in Claude Code, `$local-review` in Codex, `pi --skill local-review` / `/skill:local-review` in Pi). If issues are found, fix them and commit before proceeding. Do not skip this step.
|
||||
5. **Screenshots for frontend changes**: if `git diff main...HEAD --name-only` matches `^frontend/`, capture and embed screenshots of the affected UI per "Screenshots" above before writing the PR body (skip only if there is no visible UI effect).
|
||||
6. Check if remote branch exists and is up to date:
|
||||
5. Check if remote branch exists and is up to date:
|
||||
```bash
|
||||
git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || echo "no upstream"
|
||||
```
|
||||
7. Push to remote if needed: `git push -u origin HEAD`
|
||||
8. Create draft PR using gh CLI:
|
||||
6. Push to remote if needed: `git push -u origin HEAD`
|
||||
7. Create draft PR using gh CLI:
|
||||
```bash
|
||||
gh pr create --draft --title "<type>: <description>" --body "$(cat <<'EOF'
|
||||
## Summary
|
||||
@@ -119,7 +82,7 @@ and continue once they confirm it's done.
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
9. Return the PR URL to the user
|
||||
8. Return the PR URL to the user
|
||||
|
||||
## EE Companion PR (when `*_ee.rs` files were modified)
|
||||
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
name: AI Agent Integration Tests
|
||||
|
||||
# Exercises the AI agent flow path (preview_flow with `aiagent` modules) against
|
||||
# real LLM providers. Runs only when AI-agent backend code or the tests change,
|
||||
# because each run makes real (paid) LLM calls. To avoid spending on every commit,
|
||||
# the PR side triggers only when a PR is marked ready for review (out of draft) —
|
||||
# not on `synchronize` — plus push to main and manual dispatch.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "integration_tests/ai_agent_tests/**"
|
||||
- "backend/windmill-ai/**"
|
||||
- "backend/windmill-api/src/ai.rs"
|
||||
- "backend/windmill-worker/src/ai_executor.rs"
|
||||
- "backend/windmill-worker/src/ai/**"
|
||||
- "backend/windmill-worker/src/memory_common.rs"
|
||||
- "backend/windmill-common/src/flow_conversations.rs"
|
||||
- ".github/workflows/ai-agent-tests.yml"
|
||||
pull_request:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
paths:
|
||||
- "integration_tests/ai_agent_tests/**"
|
||||
- "backend/windmill-ai/**"
|
||||
- "backend/windmill-api/src/ai.rs"
|
||||
- "backend/windmill-worker/src/ai_executor.rs"
|
||||
- "backend/windmill-worker/src/ai/**"
|
||||
- "backend/windmill-worker/src/memory_common.rs"
|
||||
- "backend/windmill-common/src/flow_conversations.rs"
|
||||
- ".github/workflows/ai-agent-tests.yml"
|
||||
|
||||
concurrency:
|
||||
group: ai-agent-tests-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
ai_agent_e2e:
|
||||
# Skip draft PRs; the `opened`/`reopened` types would otherwise fire while
|
||||
# still a draft. `ready_for_review` always arrives non-draft.
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
|
||||
runs-on: ubicloud-standard-16
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
ports:
|
||||
- 5432:5432
|
||||
env:
|
||||
POSTGRES_DB: windmill
|
||||
POSTGRES_PASSWORD: changeme
|
||||
options: >-
|
||||
--health-cmd pg_isready --health-interval 10s --health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.93.0
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.10
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
# CE build (no enterprise/license needed for AI agents). `quickjs` powers
|
||||
# flow input-transform JS eval; `mcp` is required by the deepwiki MCP tool
|
||||
# test. Bun tool scripts run via the always-on worker (BUN_PATH).
|
||||
- name: Build Windmill
|
||||
working-directory: ./backend
|
||||
env:
|
||||
SQLX_OFFLINE: true
|
||||
CARGO_BUILD_JOBS: 12
|
||||
RUSTFLAGS: ""
|
||||
run: cargo build --features quickjs,mcp
|
||||
|
||||
- name: Start Windmill
|
||||
working-directory: ./backend
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:changeme@localhost:5432/windmill
|
||||
BUN_PATH: bun
|
||||
NODE_BIN_PATH: node
|
||||
RUST_LOG: info
|
||||
run: |
|
||||
mkdir -p ../integration_tests/logs
|
||||
./target/debug/windmill > ../integration_tests/logs/windmill.log 2>&1 &
|
||||
echo "Waiting for Windmill to be ready..."
|
||||
for i in $(seq 1 60); do
|
||||
if curl -sf http://localhost:8000/api/version > /dev/null 2>&1; then
|
||||
echo "Windmill is ready"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
curl -sf http://localhost:8000/api/version > /dev/null || { echo "Windmill failed to start"; tail -50 ../integration_tests/logs/windmill.log; exit 1; }
|
||||
|
||||
- name: Run AI agent integration tests
|
||||
timeout-minutes: 20
|
||||
working-directory: ./integration_tests/ai_agent_tests
|
||||
env:
|
||||
WINDMILL_URL: http://localhost:8000
|
||||
# Only the providers we have org secrets for. Other providers
|
||||
# (Azure, Bedrock, OpenRouter) are skipped by conftest when their
|
||||
# keys are absent — see skip_provider_without_credentials.
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
run: |
|
||||
python -m venv .venv
|
||||
.venv/bin/pip install -r requirements.txt
|
||||
# The S3/vision-attachment tests need MinIO large-file storage and
|
||||
# image-capable provider setup; out of scope for this cost-controlled
|
||||
# smoke. Add MinIO secrets + a storage service to enable them.
|
||||
.venv/bin/python -m pytest -v \
|
||||
--ignore=test_user_attachments.py \
|
||||
--ignore=test_user_images.py \
|
||||
--ignore=test_image_output.py
|
||||
|
||||
- name: Archive Windmill logs
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: ai-agent-tests-windmill-logs
|
||||
path: integration_tests/logs
|
||||
@@ -1,166 +0,0 @@
|
||||
name: AI Evals (global mode)
|
||||
|
||||
# Smoke-tests the production global AI chat proxy/frontend execution path via
|
||||
# the ai_evals harness, one case across one cheap model per provider. Runs only
|
||||
# when the eval harness or the global chat code change, since each run makes real
|
||||
# (paid) LLM calls. The backend is built from source purely as the AI proxy the
|
||||
# harness routes model calls through; the global tools/drafts run in-process in
|
||||
# the Vitest bridge against production frontend code. To avoid spending on every
|
||||
# commit, the PR side triggers only when a PR is marked ready for review (out of
|
||||
# draft) — not on `synchronize` — plus push to main and manual dispatch.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "ai_evals/**"
|
||||
- "backend/windmill-api/src/ai.rs"
|
||||
- "backend/windmill-ai/**"
|
||||
- "frontend/src/lib/components/copilot/**"
|
||||
# The eval harness runs production frontend code in-process; these are the
|
||||
# AI/draft-specific deps outside copilot/ that the global smoke exercises.
|
||||
- "frontend/src/lib/userDraft.svelte.ts"
|
||||
- "frontend/src/lib/userDraftDbSyncer.svelte.ts"
|
||||
- "frontend/src/lib/infer.ts"
|
||||
- ".github/workflows/ai-evals-test.yml"
|
||||
pull_request:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
paths:
|
||||
- "ai_evals/**"
|
||||
- "backend/windmill-api/src/ai.rs"
|
||||
- "backend/windmill-ai/**"
|
||||
- "frontend/src/lib/components/copilot/**"
|
||||
# The eval harness runs production frontend code in-process; these are the
|
||||
# AI/draft-specific deps outside copilot/ that the global smoke exercises.
|
||||
- "frontend/src/lib/userDraft.svelte.ts"
|
||||
- "frontend/src/lib/userDraftDbSyncer.svelte.ts"
|
||||
- "frontend/src/lib/infer.ts"
|
||||
- ".github/workflows/ai-evals-test.yml"
|
||||
|
||||
concurrency:
|
||||
group: ai-evals-test-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
ai_evals_global:
|
||||
# Provider secrets are unavailable to forked and Dependabot PRs.
|
||||
if: >-
|
||||
github.event_name != 'pull_request' ||
|
||||
(
|
||||
github.event.pull_request.draft == false &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository &&
|
||||
github.event.pull_request.user.login != 'dependabot[bot]'
|
||||
)
|
||||
runs-on: ubicloud-standard-16
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
ports:
|
||||
- 5432:5432
|
||||
env:
|
||||
POSTGRES_DB: windmill
|
||||
POSTGRES_PASSWORD: changeme
|
||||
options: >-
|
||||
--health-cmd pg_isready --health-interval 10s --health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
cache-workspaces: backend
|
||||
toolchain: 1.93.0
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.10
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
# Node 22.19+ is required by the frontend's undici 8.x, which the
|
||||
# Vitest bridge loads; Node 20 fails with markAsUncloneable.
|
||||
node-version: "22"
|
||||
|
||||
# CE build used only as the AI proxy (login, workspace, provider resource,
|
||||
# /ai/proxy). No worker execution or MCP needed — global tools/drafts run
|
||||
# in the Vitest bridge. quickjs matches the standard CE feature set.
|
||||
- name: Build Windmill (AI proxy)
|
||||
working-directory: ./backend
|
||||
env:
|
||||
SQLX_OFFLINE: true
|
||||
CARGO_BUILD_JOBS: 12
|
||||
RUSTFLAGS: ""
|
||||
run: cargo build --features quickjs
|
||||
|
||||
- name: Start Windmill
|
||||
working-directory: ./backend
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:changeme@localhost:5432/windmill
|
||||
RUST_LOG: info
|
||||
run: |
|
||||
mkdir -p ../ai_evals/logs
|
||||
./target/debug/windmill > ../ai_evals/logs/windmill.log 2>&1 &
|
||||
echo "Waiting for Windmill to be ready..."
|
||||
for i in $(seq 1 60); do
|
||||
if curl -sf http://localhost:8000/api/version > /dev/null 2>&1; then
|
||||
echo "Windmill is ready"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
curl -sf http://localhost:8000/api/version > /dev/null || { echo "Windmill failed to start"; tail -50 ../ai_evals/logs/windmill.log; exit 1; }
|
||||
|
||||
- name: Install frontend deps + generate client
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
npm ci
|
||||
npm run generate-backend-client
|
||||
|
||||
- name: Run global AI evals
|
||||
timeout-minutes: 20
|
||||
working-directory: ./ai_evals
|
||||
env:
|
||||
WMILL_AI_EVAL_BACKEND_URL: http://localhost:8000
|
||||
WMILL_AI_EVAL_BACKEND_WORKSPACE: integration-tests
|
||||
# Anthropic backs the haiku model. Google AI uses GEMINI_API_KEY.
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
||||
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
|
||||
run: |
|
||||
bun install
|
||||
mkdir -p results
|
||||
# One cheap model per provider (anthropic/openai/googleai/deepseek).
|
||||
fail=0
|
||||
for m in haiku 4o gemini-3-flash-preview deepseek-v4-flash; do
|
||||
echo "::group::global-test1-script-create ($m)"
|
||||
if ! bun run cli -- run global global-test1-script-create \
|
||||
--model "$m" --execution-only --output "$PWD/results/ci-$m.json"; then
|
||||
echo "$m: harness/proxy errored"
|
||||
fail=1
|
||||
echo "::endgroup::"
|
||||
continue
|
||||
fi
|
||||
# The CLI exits 0 when the harness records failed attempts, so gate
|
||||
# on execution-only pass counts while ignoring model output quality.
|
||||
if jq -e \
|
||||
'.attemptCount > 0 and .passedAttempts == .attemptCount' \
|
||||
"results/ci-$m.json" > /dev/null; then
|
||||
echo "$m: OK — proxy/frontend execution completed"
|
||||
else
|
||||
echo "$m: FAILED proxy/frontend execution"
|
||||
jq -c '.cases[0].attempts[0].checks' "results/ci-$m.json" || true
|
||||
fail=1
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
done
|
||||
[ "$fail" = 0 ] || { echo "ai_evals global smoke failed"; exit 1; }
|
||||
|
||||
- name: Archive logs and results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: ai-evals-global-logs
|
||||
path: |
|
||||
ai_evals/logs
|
||||
ai_evals/results
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
allowed_bots: 'windmill-internal-app[bot]'
|
||||
trigger_phrase: '/plan'
|
||||
claude_args: |
|
||||
--model claude-opus-4-8
|
||||
--model opus
|
||||
--system-prompt "# Claude Planning Mode
|
||||
|
||||
You are operating in PLANNING MODE ONLY. Your role is to create detailed, structured plans without making any code changes.
|
||||
|
||||
@@ -51,4 +51,4 @@ jobs:
|
||||
}
|
||||
claude_args: |
|
||||
--allowedTools "Bash,WebFetch,WebSearch"
|
||||
--model claude-opus-4-8
|
||||
--model opus
|
||||
|
||||
@@ -160,4 +160,4 @@ jobs:
|
||||
${{ env.REVIEW_PROMPT }}
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
|
||||
--model claude-opus-4-8
|
||||
--model opus
|
||||
|
||||
+1
-2
@@ -33,5 +33,4 @@ backend/chrome_profiler.json
|
||||
.fast-check/
|
||||
__pycache__/
|
||||
.playwright-mcp/
|
||||
.codex
|
||||
.claude/scheduled_tasks.lock
|
||||
.codex
|
||||
@@ -15,7 +15,6 @@ Open-source platform for internal tools, workflows, API integrations, background
|
||||
- **Enterprise**: `docs/enterprise.md` — EE file conventions and PR workflow
|
||||
- **Backend patterns**: use the `rust-backend` skill when writing Rust code
|
||||
- **Frontend patterns**: use the `svelte-frontend` skill when writing Svelte code. Do NOT edit svelte files unless you have read that skill.
|
||||
- **Frontend UUIDs**: do not call `crypto.randomUUID()` in frontend code. Import `randomUUID` from `$lib/utils/uuid` instead.
|
||||
- **Code review**: review the current PR or branch against the shared review policy in `REVIEW.md` (severity triage, public-surface checklist, AGENTS.md compliance, test-coverage assessment). The skill at `.agents/skills/local-review/SKILL.md` orchestrates it. All three CLIs auto-discover the same SKILL — Claude reads `.claude/skills/` (symlinked to the canonical `.agents/skills/` file), Codex and Pi read `.agents/skills/` directly. Invoke with `/local-review` in Claude Code, `$local-review` (or `/skills` selector) in Codex, or `pi --skill local-review` / `/skill:local-review` in Pi.
|
||||
- **Domain guides**: `.claude/skills/native-trigger/` and `frontend/tutorial-system-guide.mdc`
|
||||
- **Brand/UI guidelines**: `frontend/brand-guidelines.md`
|
||||
@@ -48,8 +47,6 @@ Typical flow:
|
||||
5. `mcp__playwright__browser_take_screenshot` for visual confirmation
|
||||
6. `mcp__playwright__browser_console_messages` / `browser_network_requests` to surface errors
|
||||
|
||||
**Attach the screenshots to the PR.** For any change under `frontend/`, embed screenshots of the affected UI in the PR body — the `pr` skill requires this and carries the upload recipe.
|
||||
|
||||
If you cannot exercise a UI change (no dev server, etc.), say so explicitly rather than claiming success.
|
||||
|
||||
## Banned Patterns
|
||||
@@ -109,5 +106,3 @@ $NAV --root backend callees "X" # what does X call?
|
||||
- Search for existing code to reuse before writing new code
|
||||
- Follow established patterns in the codebase
|
||||
- Keep changes focused — don't refactor beyond what's asked
|
||||
- **Comments record constraints, not narration.** Write a comment only for what the code can't show: why a non-obvious approach is required, what breaks if it's "simplified" away. State each invariant once, at the place where someone would break it, in ≤4 lines. Don't describe what the next line does, don't repeat the same rationale at multiple sites, and don't address the PR reviewer (justifying a change belongs in the PR description, not the code). Describe the code as it is, never its drafting history: "we no longer do X", "unchanged behavior", "instead of the previous approach" are meaningless to a reader who never saw the earlier iteration — before finishing, reread your comments as if the current state is the only state that ever existed.
|
||||
- **Never attribute work to a specific customer, account, or "requested by a customer" in repo-tracked content** (PR descriptions, commit messages, code comments, docs). Describe changes by their technical motivation instead.
|
||||
|
||||
-374
@@ -1,379 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [1.737.0](https://github.com/windmill-labs/windmill/compare/v1.736.0...v1.737.0) (2026-06-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **apps:** opt-in sandbox isolation for published & raw apps (alpha) ([#9420](https://github.com/windmill-labs/windmill/issues/9420)) ([2879cbb](https://github.com/windmill-labs/windmill/commit/2879cbb65a4122c86b4a472206d74d9009b07904))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow SQL args in managed // materialize scripts ([#9733](https://github.com/windmill-labs/windmill/issues/9733)) ([fa35968](https://github.com/windmill-labs/windmill/commit/fa3596885bf2d7ee8859f295e820ee362c756911))
|
||||
* bound orphan-cleanup drain rate with capped multi-batch loop ([#9730](https://github.com/windmill-labs/windmill/issues/9730)) ([31d9215](https://github.com/windmill-labs/windmill/commit/31d9215e5a61f19662cc87be8147007e1d47ebb6))
|
||||
* **ext-jwt:** reject external JWT auth for non-existent workspaces ([#9723](https://github.com/windmill-labs/windmill/issues/9723)) ([c644311](https://github.com/windmill-labs/windmill/commit/c644311eca4bcaf4b68058cf1d5d79d4078aee1a))
|
||||
* optimize cleanup_job_perms_orphaned and job_result_stream cleanup queries ([#9727](https://github.com/windmill-labs/windmill/issues/9727)) ([6d94865](https://github.com/windmill-labs/windmill/commit/6d9486510933af9109f52011d93b13847dbdbb39))
|
||||
* prevent silent audit-partition outage via monitor watchdog + alert ([#9729](https://github.com/windmill-labs/windmill/issues/9729)) ([8dea383](https://github.com/windmill-labs/windmill/commit/8dea38383f884f59b2956c39f1424005a21265bd))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **monitor:** hash active-root exclusion in retention delete (WIN-2088) ([#9732](https://github.com/windmill-labs/windmill/issues/9732)) ([75bafab](https://github.com/windmill-labs/windmill/commit/75bafabeeec76cf6da33eef41f588e37071df011))
|
||||
|
||||
## [1.736.0](https://github.com/windmill-labs/windmill/compare/v1.735.0...v1.736.0) (2026-06-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ai-chat:** workspace AI chat skills (SKILL.md upload + read_skill tool) ([#9648](https://github.com/windmill-labs/windmill/issues/9648)) ([6f4017d](https://github.com/windmill-labs/windmill/commit/6f4017d694494a158ebd0579c93336c378cba0fd))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **drafts:** stop mis-filing workspace-blind legacy drafts on migration ([#9725](https://github.com/windmill-labs/windmill/issues/9725)) ([3bf5b72](https://github.com/windmill-labs/windmill/commit/3bf5b72afab3241ea41a261a40c2434764bdaf72))
|
||||
* **frontend:** destroy old WebsocketProvider on workspace switch in MultiplayerMenu ([#9719](https://github.com/windmill-labs/windmill/issues/9719)) ([6e96f90](https://github.com/windmill-labs/windmill/commit/6e96f90065dfe2f6ccc5eb4f85f4facd3515c70c))
|
||||
* **frontend:** ensure type:object in test_run_flow tool schema for Anthropic ([#9721](https://github.com/windmill-labs/windmill/issues/9721)) ([d5cb944](https://github.com/windmill-labs/windmill/commit/d5cb944cf92f074b2ee42c876595eacdfa2f4d76))
|
||||
* **health:** detect read-only replica via pg_is_in_recovery() ([#9722](https://github.com/windmill-labs/windmill/issues/9722)) ([e16061d](https://github.com/windmill-labs/windmill/commit/e16061df06babeae935a9396de5bdcd46e8119a9))
|
||||
* re-enforce scoped API token boundaries across handlers ([#9712](https://github.com/windmill-labs/windmill/issues/9712)) ([e19594d](https://github.com/windmill-labs/windmill/commit/e19594df2ad015a0336ade95e04562f5562ec3f6))
|
||||
|
||||
## [1.735.0](https://github.com/windmill-labs/windmill/compare/v1.734.0...v1.735.0) (2026-06-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* clarify session draft bar tracks all workspace draft changes ([#9714](https://github.com/windmill-labs/windmill/issues/9714)) ([ed016a5](https://github.com/windmill-labs/windmill/commit/ed016a5edb4527877bf7e6bf92feafc2710669c2))
|
||||
* **copilot:** improve global-mode path selection + add path-selection evals ([#9698](https://github.com/windmill-labs/windmill/issues/9698)) ([74a2329](https://github.com/windmill-labs/windmill/commit/74a2329d2e8395141807c43acef07ef132490039))
|
||||
* link files & folders to the global AI chat ([#9520](https://github.com/windmill-labs/windmill/issues/9520)) ([84cc043](https://github.com/windmill-labs/windmill/commit/84cc043406d63a1e1472165cf24ce8c09905fc5f))
|
||||
* scope default instance db name to workspace (dt_/dl_) ([#9699](https://github.com/windmill-labs/windmill/issues/9699)) ([4a8a724](https://github.com/windmill-labs/windmill/commit/4a8a724895dcecb835e1eb1e4fd7d1bbc8b3e0fb))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enforce job_dir containment when writing module files ([#9703](https://github.com/windmill-labs/windmill/issues/9703)) ([e403f92](https://github.com/windmill-labs/windmill/commit/e403f92d7e84cebc78709dce1a0928048ba2506d))
|
||||
* **frontend:** deploy full script/flow draft from AI chat via shared module ([#9642](https://github.com/windmill-labs/windmill/issues/9642)) ([23bf6bf](https://github.com/windmill-labs/windmill/commit/23bf6bf3da01d552d2dfab6dbcfd30f758ed34d2))
|
||||
* **frontend:** strip raw-app post-deploy diff noise (raw_app/lock/data) ([#9706](https://github.com/windmill-labs/windmill/issues/9706)) ([e20a277](https://github.com/windmill-labs/windmill/commit/e20a27745a08d552e6d2c5a8bbaf08ccfe89c68f))
|
||||
* ignore NotFound errors when deleting log files from object store ([#9707](https://github.com/windmill-labs/windmill/issues/9707)) ([8a0b0ab](https://github.com/windmill-labs/windmill/commit/8a0b0abead71320c4f69eb3007739a19f76d4126))
|
||||
* **oauth:** restore bring-your-own CC token URL override ([#9711](https://github.com/windmill-labs/windmill/issues/9711)) ([ef4962e](https://github.com/windmill-labs/windmill/commit/ef4962e52aba0bc79bf72523de9101853c660654))
|
||||
* sanitize git credentials from ansible executor errors and logs ([#9697](https://github.com/windmill-labs/windmill/issues/9697)) ([ace7b68](https://github.com/windmill-labs/windmill/commit/ace7b68a28b00d715298ffcb6ae907c1974a74b8))
|
||||
|
||||
## [1.734.0](https://github.com/windmill-labs/windmill/compare/v1.733.1...v1.734.0) (2026-06-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* ducklake materialization for data pipelines ([#9689](https://github.com/windmill-labs/windmill/issues/9689)) ([3ebf243](https://github.com/windmill-labs/windmill/commit/3ebf24359d66048d6361ce65cd879cdc04b737ed))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** clear branch step state when switching outer loop iterations ([#9650](https://github.com/windmill-labs/windmill/issues/9650)) ([09a8004](https://github.com/windmill-labs/windmill/commit/09a80040ca268a4379d5302e3401435ba93247e0))
|
||||
|
||||
## [1.733.1](https://github.com/windmill-labs/windmill/compare/v1.733.0...v1.733.1) (2026-06-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backend:** validate ansible vault_id entries before config generation ([#9681](https://github.com/windmill-labs/windmill/issues/9681)) ([c1f31c0](https://github.com/windmill-labs/windmill/commit/c1f31c0e4777bf0cfed0dd7f03249e9a61cd8cb9))
|
||||
* **frontend:** group live pipeline runs in the activity panel ([#9684](https://github.com/windmill-labs/windmill/issues/9684)) ([1be4df9](https://github.com/windmill-labs/windmill/commit/1be4df9acb935250d4cc12e83cf67e366d870d5a))
|
||||
* require super admin for object storage config test endpoint ([#9683](https://github.com/windmill-labs/windmill/issues/9683)) ([fb44fe7](https://github.com/windmill-labs/windmill/commit/fb44fe7af2b8ebe8ef64ffb0e5acce8580bf4200))
|
||||
* validate websocket trigger urls and gate trigger test route ([#9682](https://github.com/windmill-labs/windmill/issues/9682)) ([c39ee07](https://github.com/windmill-labs/windmill/commit/c39ee07c0bcd2249dd19ffa5cd988125eefc6c9f))
|
||||
|
||||
## [1.733.0](https://github.com/windmill-labs/windmill/compare/v1.732.0...v1.733.0) (2026-06-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ai-chat:** cap read_app_file + search_app grep tool to bound context in large raw apps ([#9653](https://github.com/windmill-labs/windmill/issues/9653)) ([4296a6a](https://github.com/windmill-labs/windmill/commit/4296a6ae1f73564de4df54fe1df0a03c1df05dfd))
|
||||
* **python, windows:** enable S3 to cache wheels ([#5199](https://github.com/windmill-labs/windmill/issues/5199)) ([ab3bc97](https://github.com/windmill-labs/windmill/commit/ab3bc97cd92b6480327029bcf018280442462af7))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow users to always discard their own drafts without write permission ([#9659](https://github.com/windmill-labs/windmill/issues/9659)) ([6833a55](https://github.com/windmill-labs/windmill/commit/6833a554aeddb3e63173d3c3140b490c0bf2822b))
|
||||
* **backend:** clean up unique_ext_jwt_token on workspace deletion ([#9676](https://github.com/windmill-labs/windmill/issues/9676)) ([9add719](https://github.com/windmill-labs/windmill/commit/9add719d936cdcfb2c4062629e3e1f792694dafe))
|
||||
* **backend:** strip NUL bytes from draft values on write ([#9673](https://github.com/windmill-labs/windmill/issues/9673)) ([924f9c7](https://github.com/windmill-labs/windmill/commit/924f9c7e8d8863d9af40aee246a519b4be0e1ea2))
|
||||
* **python:** split PIP_TRUSTED_HOST by whitespace to support multiple hosts ([#9675](https://github.com/windmill-labs/windmill/issues/9675)) ([cafb473](https://github.com/windmill-labs/windmill/commit/cafb473494d9cff3a8b2aeaf9f18b015f966e7b3))
|
||||
|
||||
## [1.732.0](https://github.com/windmill-labs/windmill/compare/v1.731.0...v1.732.0) (2026-06-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ansible:** add AI chat and editor bar buttons for ansible ([#9671](https://github.com/windmill-labs/windmill/issues/9671)) ([017c3d3](https://github.com/windmill-labs/windmill/commit/017c3d3343c2577501103be4b2dd8dac9727d80d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ai:** emit token usage in gemini proxy streaming translation ([#9669](https://github.com/windmill-labs/windmill/issues/9669)) ([0cc2257](https://github.com/windmill-labs/windmill/commit/0cc2257596a3965cf6db21a0090edcce6e1b8419))
|
||||
* **backend:** grant script_trigger access to windmill roles ([#9674](https://github.com/windmill-labs/windmill/issues/9674)) ([3361736](https://github.com/windmill-labs/windmill/commit/33617367d09537667d2ab3f91135c736194b9e7e))
|
||||
* **frontend:** ignore hash/assets in script diffs and drafts (WIN-2071) ([#9664](https://github.com/windmill-labs/windmill/issues/9664)) ([3371265](https://github.com/windmill-labs/windmill/commit/33712653821e83f2562dd5f271dbec0188d5d2f8))
|
||||
|
||||
## [1.731.0](https://github.com/windmill-labs/windmill/compare/v1.730.0...v1.731.0) (2026-06-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **backend:** auto-reconnect postgres trigger listener with backoff (WIN-2073) ([#9666](https://github.com/windmill-labs/windmill/issues/9666)) ([a425431](https://github.com/windmill-labs/windmill/commit/a425431e9067bcf85474fdc7b7ef7f73e41b9071))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backend:** grant notify_event access to windmill roles ([#9665](https://github.com/windmill-labs/windmill/issues/9665)) ([a682d02](https://github.com/windmill-labs/windmill/commit/a682d02311a2110bfc0d5e0a5b52e96147fe0dd7))
|
||||
* **mcp:** repair invalid type keywords in tool JSON schemas ([#9667](https://github.com/windmill-labs/windmill/issues/9667)) ([c30bdec](https://github.com/windmill-labs/windmill/commit/c30bdecea77ff9b4d74d52961f3101201099b683))
|
||||
* trigger flow error handler on unrecoverable (OOM/zombie) step failures ([#9662](https://github.com/windmill-labs/windmill/issues/9662)) ([7e4df02](https://github.com/windmill-labs/windmill/commit/7e4df02bd60c4d6ee8c92d3dfd19f4e587ff9632))
|
||||
|
||||
## [1.730.0](https://github.com/windmill-labs/windmill/compare/v1.729.0...v1.730.0) (2026-06-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ai-chat:** summary-based conversation compaction ([#9645](https://github.com/windmill-labs/windmill/issues/9645)) ([5d553b8](https://github.com/windmill-labs/windmill/commit/5d553b81c06664aab61131a93b198575c088d12d))
|
||||
* Data Pipelines alpha ([#9193](https://github.com/windmill-labs/windmill/issues/9193)) ([7155a0b](https://github.com/windmill-labs/windmill/commit/7155a0bb96cf30bd878272a0f4c3c3b02341b261))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ai-chat:** stop echoing app draft value in global chat write tool results ([#9658](https://github.com/windmill-labs/windmill/issues/9658)) ([2fed808](https://github.com/windmill-labs/windmill/commit/2fed808b9e716d9a44b34c7a073ec0d37374be05))
|
||||
* **backend:** include raw_app drafts in list_apps draft_users ([#9647](https://github.com/windmill-labs/windmill/issues/9647)) ([19bc005](https://github.com/windmill-labs/windmill/commit/19bc0052f1069d732231950a0ec958f675d57417))
|
||||
* **frontend:** keep ?new_draft flag until first save is confirmed ([#9656](https://github.com/windmill-labs/windmill/issues/9656)) ([9b6b7c3](https://github.com/windmill-labs/windmill/commit/9b6b7c3862d9988e5e91eaab2b967a23f41cdc0d))
|
||||
* **frontend:** re-key raw-app autosave on post-deploy navigation ([#9646](https://github.com/windmill-labs/windmill/issues/9646)) ([1058bde](https://github.com/windmill-labs/windmill/commit/1058bdeccdc4c403ef4599db0ee74a65a66c715f))
|
||||
* gate agent-worker global setting reads with a blocklist ([#9623](https://github.com/windmill-labs/windmill/issues/9623)) ([fdd82f0](https://github.com/windmill-labs/windmill/commit/fdd82f0c48f29805cd9e219649f27fba45c7fd92))
|
||||
* **workspaces:** add instance setting to disable workspace invite/add emails ([#9643](https://github.com/windmill-labs/windmill/issues/9643)) ([796230d](https://github.com/windmill-labs/windmill/commit/796230d90a7e6d1debc15e139ab708881e527862))
|
||||
|
||||
## [1.729.0](https://github.com/windmill-labs/windmill/compare/v1.728.1...v1.729.0) (2026-06-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add ducklake schema support to the database manager ([#9633](https://github.com/windmill-labs/windmill/issues/9633)) ([3eeccaf](https://github.com/windmill-labs/windmill/commit/3eeccaf9682b7803fdf5be8dcbc4d243e0ba2e49))
|
||||
* **ai-chat:** self-hosted docs tools via windmill.dev llms.txt + ask benchmark ([#9578](https://github.com/windmill-labs/windmill/issues/9578)) ([f4425fc](https://github.com/windmill-labs/windmill/commit/f4425fca9fb0d02b845bd72888ade54905c5a30b))
|
||||
* **frontend:** View Diff and in-place Load for other users' drafts ([#9621](https://github.com/windmill-labs/windmill/issues/9621)) ([5508f1d](https://github.com/windmill-labs/windmill/commit/5508f1da9cd04c2583eb3f7ee6bce19d067f2227))
|
||||
* per-user draft review & deploy page (gating, badges, rename, raw-app deploy fixes) ([#9625](https://github.com/windmill-labs/windmill/issues/9625)) ([e09cd58](https://github.com/windmill-labs/windmill/commit/e09cd5862cb636e143027fe8d9a5be9c7097b031))
|
||||
* queue messages typed while ai chat is streaming ([#9525](https://github.com/windmill-labs/windmill/issues/9525)) ([51bd869](https://github.com/windmill-labs/windmill/commit/51bd8692a482850f7ac8b04dd16db5876336b5b9))
|
||||
* zero-setup oauth client credentials for registry providers ([#9559](https://github.com/windmill-labs/windmill/issues/9559)) ([e26a923](https://github.com/windmill-labs/windmill/commit/e26a9239a62a25abf90ef06ade4dde7f36e791bb))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ai_evals:** adapt global eval harness to DB-backed user drafts ([#9641](https://github.com/windmill-labs/windmill/issues/9641)) ([e87ff79](https://github.com/windmill-labs/windmill/commit/e87ff79ecf6a6e0958916ed1b3756fb3addf719f))
|
||||
* **drafts:** preserve original timestamp when migrating localStorage drafts ([#9638](https://github.com/windmill-labs/windmill/issues/9638)) ([8021775](https://github.com/windmill-labs/windmill/commit/8021775f5f961ef6fd01b022639b85855326a1da))
|
||||
* **frontend:** don't save drafts on leave when auto-save is off, warn instead ([#9630](https://github.com/windmill-labs/windmill/issues/9630)) ([2523465](https://github.com/windmill-labs/windmill/commit/252346500945a9571af744c839ac0c7d6870504f))
|
||||
* **frontend:** render Modal2 dialogs above the AI chat panel ([#9636](https://github.com/windmill-labs/windmill/issues/9636)) ([b67c8cf](https://github.com/windmill-labs/windmill/commit/b67c8cf42b477575fc1bc448058ec0d3b7e54fee))
|
||||
* **frontend:** show AI sessions when AI unconfigured, with disabled chat ([#9644](https://github.com/windmill-labs/windmill/issues/9644)) ([ba69d81](https://github.com/windmill-labs/windmill/commit/ba69d8147b615e160cf3d2885fc65a0777b78b71))
|
||||
* **git-sync:** bump default sync script to hub/28719 (windmill-cli 1.728.1) for WAC modules ([#9649](https://github.com/windmill-labs/windmill/issues/9649)) ([3c0e38b](https://github.com/windmill-labs/windmill/commit/3c0e38b5890d77983cb5cf5f422a62a73e7a4f22))
|
||||
|
||||
## [1.728.1](https://github.com/windmill-labs/windmill/compare/v1.728.0...v1.728.1) (2026-06-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **backend:** purge workspace_diff cache on workspace delete ([#9627](https://github.com/windmill-labs/windmill/issues/9627)) ([8a3f69d](https://github.com/windmill-labs/windmill/commit/8a3f69dda8f2088fb859ed8ed6e54458940423d0))
|
||||
* **cli:** fall back to esbuild-wasm on native host/binary mismatch ([#9629](https://github.com/windmill-labs/windmill/issues/9629)) ([86d1d16](https://github.com/windmill-labs/windmill/commit/86d1d160f0d3bd9faabdafada07e2956dd98445d))
|
||||
* **frontend:** persist session-editor draft path/summary edits + per-line diff tooltips ([#9622](https://github.com/windmill-labs/windmill/issues/9622)) ([e4bfeb2](https://github.com/windmill-labs/windmill/commit/e4bfeb29bc4e89669863b5f6396904a331167658))
|
||||
|
||||
## [1.728.0](https://github.com/windmill-labs/windmill/compare/v1.727.0...v1.728.0) (2026-06-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **frontend:** adapt AI-chat/sessions drafts to DB-backed model ([#9601](https://github.com/windmill-labs/windmill/issues/9601)) ([611c70a](https://github.com/windmill-labs/windmill/commit/611c70acd211cf4b8f8308da4a264c670a2f5f43))
|
||||
* **frontend:** consolidate draft-migration errors into a single toast + modal ([#9612](https://github.com/windmill-labs/windmill/issues/9612)) ([bc0d5bf](https://github.com/windmill-labs/windmill/commit/bc0d5bf241df3633921bd9d43d171e91034fbfcf))
|
||||
* **frontend:** dedup user drafts against the deployed baseline ([#9618](https://github.com/windmill-labs/windmill/issues/9618)) ([a2ce446](https://github.com/windmill-labs/windmill/commit/a2ce44645fdbfa98bf250fac2d15d2b5b26c4b47))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** reset deleteWorkspaceForkModal on confirm in SidebarContent ([#9619](https://github.com/windmill-labs/windmill/issues/9619)) ([7cb5c6e](https://github.com/windmill-labs/windmill/commit/7cb5c6e749b2020dee5ee1499f0dc69c5109a6d8))
|
||||
* **frontend:** session Drafts drawer uses raw_app kind for the raw-app diff ([#9617](https://github.com/windmill-labs/windmill/issues/9617)) ([46288b6](https://github.com/windmill-labs/windmill/commit/46288b6143efae4dfdf6fe068b97a1e8831fce6a))
|
||||
* **nativets:** respect custom CA certs in in-process fetch runtime ([#9615](https://github.com/windmill-labs/windmill/issues/9615)) ([41562c7](https://github.com/windmill-labs/windmill/commit/41562c7d7c708d7d056d9b3d0c39b994a6f4a016))
|
||||
* **ResourceForm:** initialize JSON editor when resource type schema is unavailable ([#9611](https://github.com/windmill-labs/windmill/issues/9611)) ([5a24057](https://github.com/windmill-labs/windmill/commit/5a2405743b4622fc1021109114d007057abd5dfd))
|
||||
* show folder labels in the folder list table ([#9620](https://github.com/windmill-labs/windmill/issues/9620)) ([651fa13](https://github.com/windmill-labs/windmill/commit/651fa13ee80ff76e5a53ef1ed545b03ce6792294))
|
||||
* show last updated date per user in other-users-drafts modal ([#9614](https://github.com/windmill-labs/windmill/issues/9614)) ([f6104ce](https://github.com/windmill-labs/windmill/commit/f6104ce05c4005ffb9fe8112782d1ef6d3065300))
|
||||
|
||||
## [1.727.0](https://github.com/windmill-labs/windmill/compare/v1.726.1...v1.727.0) (2026-06-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* support temp_script_refs in wmill dev for local relative imports ([#9554](https://github.com/windmill-labs/windmill/issues/9554)) ([33ac287](https://github.com/windmill-labs/windmill/commit/33ac287065742df53f363a5fe09f54f5584a85a6))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cli:** harden legacy flow lock migration ordering and collision guard ([#9557](https://github.com/windmill-labs/windmill/issues/9557)) ([cd09870](https://github.com/windmill-labs/windmill/commit/cd098700c2cd8d7e9150f760938f4eaf34d188ec))
|
||||
* **cli:** include __mod/ folder in gitSyncIncludePattern for scripts ([#9606](https://github.com/windmill-labs/windmill/issues/9606)) ([252c1b3](https://github.com/windmill-labs/windmill/commit/252c1b35fc716c3486109d89615127c588bbe90a))
|
||||
* **frontend:** allow same-origin redirects in isValidLogoutRedirect ([#9568](https://github.com/windmill-labs/windmill/issues/9568)) ([8500435](https://github.com/windmill-labs/windmill/commit/8500435e82231e13a1b8a874fd0545f0a0a73fee))
|
||||
* **frontend:** make UserDraft read-after-write work without live entry ([#9609](https://github.com/windmill-labs/windmill/issues/9609)) ([51e82d7](https://github.com/windmill-labs/windmill/commit/51e82d7c6d30c66c84236feb743c09929934e564))
|
||||
* **frontend:** seed detached user-draft handles so new-item drawers render ([#9608](https://github.com/windmill-labs/windmill/issues/9608)) ([9e3c0de](https://github.com/windmill-labs/windmill/commit/9e3c0decf95378c66055d82215c15cd3bf4a69cb))
|
||||
* **frontend:** strip server-managed fields from value diffs ([#9599](https://github.com/windmill-labs/windmill/issues/9599)) ([c213801](https://github.com/windmill-labs/windmill/commit/c213801b5aee54d801c14b9eb31422f2a312ef7e))
|
||||
|
||||
## [1.726.1](https://github.com/windmill-labs/windmill/compare/v1.726.0...v1.726.1) (2026-06-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **apps:** prevent decision tree graph editor crash on missing graph context ([#9602](https://github.com/windmill-labs/windmill/issues/9602)) ([24f3259](https://github.com/windmill-labs/windmill/commit/24f32596e9ca39c963c19af4a8b14fbcd04e3a78))
|
||||
* db-backed draft fixes — review-page UX, legacy drafts, session restore ([#9600](https://github.com/windmill-labs/windmill/issues/9600)) ([4e4b224](https://github.com/windmill-labs/windmill/commit/4e4b2247ef471dada1b8c894974fe921d14b3947))
|
||||
|
||||
## [1.726.0](https://github.com/windmill-labs/windmill/compare/v1.725.1...v1.726.0) (2026-06-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **audit:** record workspace archive/unarchive/delete in instance audit log ([#9596](https://github.com/windmill-labs/windmill/issues/9596)) ([9de5708](https://github.com/windmill-labs/windmill/commit/9de57086086bb5626d175c7f926915d1d6ac67ca))
|
||||
* **frontend:** add user-level toggle to disable Windmill AI ([#9585](https://github.com/windmill-labs/windmill/issues/9585)) ([5709a56](https://github.com/windmill-labs/windmill/commit/5709a564fbafd9aa91943572ecd8c3e0c45c20b1))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **embeddings:** retry HuggingFace model downloads with backoff ([#9597](https://github.com/windmill-labs/windmill/issues/9597)) ([6a62959](https://github.com/windmill-labs/windmill/commit/6a6295921d681359155d814507908792be405679))
|
||||
* resolve release CI failures (pypi bundle, flow serde test, cli windows) ([#9595](https://github.com/windmill-labs/windmill/issues/9595)) ([5ccaae8](https://github.com/windmill-labs/windmill/commit/5ccaae8ab36f2be18b67863ea069763455908029))
|
||||
|
||||
## [1.725.1](https://github.com/windmill-labs/windmill/compare/v1.725.0...v1.725.1) (2026-06-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **apps:** apply scope-path predicate to app list/search endpoints ([#9581](https://github.com/windmill-labs/windmill/issues/9581)) ([3bf6e10](https://github.com/windmill-labs/windmill/commit/3bf6e102afbdad41e558617bc812012eaaaecd9b))
|
||||
* **auth:** add scope checks to scripts/flows list_tokens endpoints ([#9582](https://github.com/windmill-labs/windmill/issues/9582)) ([36c9f86](https://github.com/windmill-labs/windmill/commit/36c9f8612b5778aa2c981454729590b71671ce8d))
|
||||
* **cli:** preserve committed script.lock on transient NULL lock during git-sync deploy ([#9593](https://github.com/windmill-labs/windmill/issues/9593)) ([6b916ac](https://github.com/windmill-labs/windmill/commit/6b916ac688e0305284e6cf819bf28803bcca0118))
|
||||
* expose parent_hash in MCP createScript tool for updates ([#9586](https://github.com/windmill-labs/windmill/issues/9586)) ([a69505d](https://github.com/windmill-labs/windmill/commit/a69505df9bf25d7c4f11d0528a7450c08dbb422c))
|
||||
* **flows:** stop serializing default retry/stop_after_if fields ([#9583](https://github.com/windmill-labs/windmill/issues/9583)) ([e1e2a24](https://github.com/windmill-labs/windmill/commit/e1e2a24b6a6752b3ac779cb0db38061cbc54425e))
|
||||
* **security:** sanitize dependency names & connection strings against command/SQL injection ([#9590](https://github.com/windmill-labs/windmill/issues/9590)) ([aff0a4e](https://github.com/windmill-labs/windmill/commit/aff0a4ec189cd8e315282e878bb858ef00635b90))
|
||||
|
||||
## [1.725.0](https://github.com/windmill-labs/windmill/compare/v1.724.0...v1.725.0) (2026-06-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Db-backed user drafts ([#9351](https://github.com/windmill-labs/windmill/issues/9351)) ([1fc3557](https://github.com/windmill-labs/windmill/commit/1fc355709c025fd256c5a4035356e15a5a05b23d))
|
||||
* scope AI session storage per user, session list in IndexedDB ([#9518](https://github.com/windmill-labs/windmill/issues/9518)) ([aa26c4d](https://github.com/windmill-labs/windmill/commit/aa26c4d9b22b3a353a6c0605eb9a4193e34aa18c))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **powershell:** sanitize module names to prevent command injection (CWE-78) ([#9587](https://github.com/windmill-labs/windmill/issues/9587)) ([6acce7a](https://github.com/windmill-labs/windmill/commit/6acce7a88733683153db534cb18752b31d93af82))
|
||||
|
||||
## [1.724.0](https://github.com/windmill-labs/windmill/compare/v1.723.0...v1.724.0) (2026-06-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cli:** add --yes, --secret/--no-secret and --description to variable add ([#9548](https://github.com/windmill-labs/windmill/issues/9548)) ([4e9e0c0](https://github.com/windmill-labs/windmill/commit/4e9e0c024b4b95f9676b1646591d8f0c662e84ab))
|
||||
* **frontend:** improve AI chat cancel and interrupted-turn handling ([#9539](https://github.com/windmill-labs/windmill/issues/9539)) ([114c412](https://github.com/windmill-labs/windmill/commit/114c41251a8c738b58a1a3dd9434d09d33feb6f1))
|
||||
* **frontend:** precise AI chat context usage tracking + indicator ([#9551](https://github.com/windmill-labs/windmill/issues/9551)) ([2b47180](https://github.com/windmill-labs/windmill/commit/2b471805bf1c92bb210cfacda217a4341e1f989c))
|
||||
* wire chat reasoning effort through gemini and bedrock proxies ([#9545](https://github.com/windmill-labs/windmill/issues/9545)) ([aaf0563](https://github.com/windmill-labs/windmill/commit/aaf05635cedadc73455dc522474b673719f9fd5c))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* actually isolate windows job children from CTRL_BREAK_EVENT + reap on worker death ([#9563](https://github.com/windmill-labs/windmill/issues/9563)) ([61f3291](https://github.com/windmill-labs/windmill/commit/61f3291b240bdb5c26bee8947351a9590bc3bd45))
|
||||
* **ai:** enforce resource authz when loading MCP tools in agent worker ([#9571](https://github.com/windmill-labs/windmill/issues/9571)) ([317a862](https://github.com/windmill-labs/windmill/commit/317a8629d1c8436d2a6f3443bd25b81d606ce283))
|
||||
* append system CA bundle to tracing proxy cert file ([#9549](https://github.com/windmill-labs/windmill/issues/9549)) ([3cf4083](https://github.com/windmill-labs/windmill/commit/3cf40839602e5c3d1df51f0a29b01736bade09da))
|
||||
* **cli:** consistent flow inline lock filenames for compound extensions ([#9555](https://github.com/windmill-labs/windmill/issues/9555)) ([f0659a7](https://github.com/windmill-labs/windmill/commit/f0659a755a161420833e3bfdbe04befc6ebeb977))
|
||||
* **flows:** skip_if evaluates wrong previous_result during retry ([#9547](https://github.com/windmill-labs/windmill/issues/9547)) ([2aab352](https://github.com/windmill-labs/windmill/commit/2aab35245c362c2f911c60ea435f29bfb1369ebf))
|
||||
* **folders:** allow hyphens in folder names ([#9566](https://github.com/windmill-labs/windmill/issues/9566)) ([84df111](https://github.com/windmill-labs/windmill/commit/84df11177f2009bff007e9b722b55a9a5a63c06a)), closes [#8474](https://github.com/windmill-labs/windmill/issues/8474)
|
||||
* **frontend:** load resource value in JSON editor when resource type is missing ([#9574](https://github.com/windmill-labs/windmill/issues/9574)) ([251266c](https://github.com/windmill-labs/windmill/commit/251266cd8119dbef314314daed43aa43ab92f1c9))
|
||||
* isolate windows job children from worker CTRL_BREAK_EVENT ([#9562](https://github.com/windmill-labs/windmill/issues/9562)) ([1d6191e](https://github.com/windmill-labs/windmill/commit/1d6191ebb75843917eec6c76a4be347f9ac4cb72))
|
||||
* stop sending temperature for AI chat across all providers ([#9553](https://github.com/windmill-labs/windmill/issues/9553)) ([3585716](https://github.com/windmill-labs/windmill/commit/358571687296fbe5c378533b3c1662707955c64a))
|
||||
|
||||
## [1.723.0](https://github.com/windmill-labs/windmill/compare/v1.722.0...v1.723.0) (2026-06-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add get_app_runtime_logs tool to global chat ([#9502](https://github.com/windmill-labs/windmill/issues/9502)) ([f86d0d7](https://github.com/windmill-labs/windmill/commit/f86d0d79fc6aa23119fd59761330ab79592d0a2a))
|
||||
* **cli:** improve agent prompts/skills and workspace fork workflow ([#9531](https://github.com/windmill-labs/windmill/issues/9531)) ([5bdc4f8](https://github.com/windmill-labs/windmill/commit/5bdc4f83ce37302a2c375d0ff73763acfee2aadb))
|
||||
* enable native web search in copilot ([#9522](https://github.com/windmill-labs/windmill/issues/9522)) ([d3f5fe1](https://github.com/windmill-labs/windmill/commit/d3f5fe1c8c39ff07d05f0922b8f40aa95756a707))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **frontend:** stop live activity flickering when user has multiple tabs ([#9543](https://github.com/windmill-labs/windmill/issues/9543)) ([57e627e](https://github.com/windmill-labs/windmill/commit/57e627eabf7c4144ce1c07214ad44d026b82f0b4))
|
||||
* omit temperature for claude fable 5 ([#9540](https://github.com/windmill-labs/windmill/issues/9540)) ([bd00bee](https://github.com/windmill-labs/windmill/commit/bd00beeac54dbcfa9ab86fd336fca1a8fa289341))
|
||||
* refetch license key from settings when in-memory key is invalid ([#9534](https://github.com/windmill-labs/windmill/issues/9534)) ([38c0ccd](https://github.com/windmill-labs/windmill/commit/38c0ccdf563d3655a4cba390ce9372bc9f9c4a9b))
|
||||
|
||||
## [1.722.0](https://github.com/windmill-labs/windmill/compare/v1.721.0...v1.722.0) (2026-06-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add reasoning effort control and thinking display to AI chat ([#9511](https://github.com/windmill-labs/windmill/issues/9511)) ([7f987e8](https://github.com/windmill-labs/windmill/commit/7f987e8c9807b72d9cc3901b6e4d02a24c423f50))
|
||||
* **ai-chat:** collapse big pastes, cap input height, escape HTML ([#9487](https://github.com/windmill-labs/windmill/issues/9487)) ([365e204](https://github.com/windmill-labs/windmill/commit/365e20410ed528d5b4e967b64fb282bcc1e03ccd))
|
||||
* **ai-chat:** quick access to AI prompt settings from chat ([#9508](https://github.com/windmill-labs/windmill/issues/9508)) ([b894f78](https://github.com/windmill-labs/windmill/commit/b894f783f183ab795eab6f57da8654275d9ad82d))
|
||||
* **ai:** add list_runs and get_job_logs tools to global chat mode ([#9488](https://github.com/windmill-labs/windmill/issues/9488)) ([cfe5119](https://github.com/windmill-labs/windmill/commit/cfe51190356a9e922f6398dd875abc368accbd15))
|
||||
* clear conflict error + force delete when reusing a fork workspace id ([#9499](https://github.com/windmill-labs/windmill/issues/9499)) ([fddabe9](https://github.com/windmill-labs/windmill/commit/fddabe9c5c6f178b4b09854dc11e50adafd44c87))
|
||||
* **flow:** support worker tag override on AI agent steps ([#9513](https://github.com/windmill-labs/windmill/issues/9513)) ([a6a5600](https://github.com/windmill-labs/windmill/commit/a6a5600833063e36b35aac7f90dcbdcd3db7c627))
|
||||
* folder-level label inheritance for scripts, flows and jobs ([#9524](https://github.com/windmill-labs/windmill/issues/9524)) ([765f50c](https://github.com/windmill-labs/windmill/commit/765f50c474f8abf76550664ed15418ddb3c0b221))
|
||||
* **frontend:** show AI sessions in narrow-screen burger menu ([#9523](https://github.com/windmill-labs/windmill/issues/9523)) ([a8f1062](https://github.com/windmill-labs/windmill/commit/a8f1062f37228e7a8257aac1cd3295bd5084ff90))
|
||||
* prefer idle worker pods on k8s autoscaling scale-in via pod-deletion-cost ([#9515](https://github.com/windmill-labs/windmill/issues/9515)) ([3c3f157](https://github.com/windmill-labs/windmill/commit/3c3f15722fd22713cc8baa744d9a81207943a33c))
|
||||
* prompt browser confirmation on page exit with unsaved changes ([#9503](https://github.com/windmill-labs/windmill/issues/9503)) ([3119e16](https://github.com/windmill-labs/windmill/commit/3119e16ed8df0daec0e019ae2efb2176d7e9e953))
|
||||
* **worker:** #ssh directive to run a bash script on a remote SSH host ([#9479](https://github.com/windmill-labs/windmill/issues/9479)) ([afddfe8](https://github.com/windmill-labs/windmill/commit/afddfe84452357b06f4fb815566d4c8269ceafbf))
|
||||
* workspace protection rule to restrict anonymous app deployment ([#9509](https://github.com/windmill-labs/windmill/issues/9509)) ([cf9ad54](https://github.com/windmill-labs/windmill/commit/cf9ad54181d38c5d3c3aa05208e17d8d97eae4ef))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cli:** include lock-relevant script content in lock cache key ([#9528](https://github.com/windmill-labs/windmill/issues/9528)) ([dc60e1a](https://github.com/windmill-labs/windmill/commit/dc60e1aa174f2a7616d2d4a37117a463ef25a9d6))
|
||||
* **frontend:** allow copy/paste shortcuts inside ConfirmationModal ([#9505](https://github.com/windmill-labs/windmill/issues/9505)) ([7fc5340](https://github.com/windmill-labs/windmill/commit/7fc5340da39c049d059aa0ae15ceb6a98ca58053))
|
||||
* **frontend:** clarify trigger filters match the message parsed as JSON ([#9516](https://github.com/windmill-labs/windmill/issues/9516)) ([0b17843](https://github.com/windmill-labs/windmill/commit/0b178437ce8cd8295be21c0c6077c39e957a3337))
|
||||
* **frontend:** enable Apply button when env vars change in worker group config ([#9501](https://github.com/windmill-labs/windmill/issues/9501)) ([c80c6d8](https://github.com/windmill-labs/windmill/commit/c80c6d8fcdfc1de8fe841e9699452f67a25cad23))
|
||||
* **frontend:** improve AI chat markdown and typing dots in dark mode ([#9497](https://github.com/windmill-labs/windmill/issues/9497)) ([4e86806](https://github.com/windmill-labs/windmill/commit/4e868062d4c22c5574149d35235b7ad1abe805f3))
|
||||
* **frontend:** stop echoing draft values in global chat write tool results ([#9530](https://github.com/windmill-labs/windmill/issues/9530)) ([ce6e2f7](https://github.com/windmill-labs/windmill/commit/ce6e2f7ade25ca91c375f0de5f1be3f1c82ccb47))
|
||||
* inherit container NO_PROXY into MITM tracing proxy job exclusions ([#9492](https://github.com/windmill-labs/windmill/issues/9492)) ([4c22e3b](https://github.com/windmill-labs/windmill/commit/4c22e3b712a74828cf654ea7d89aeab5b50cfbd7))
|
||||
* make default chat model optional in AI settings ([#9514](https://github.com/windmill-labs/windmill/issues/9514)) ([1d43288](https://github.com/windmill-labs/windmill/commit/1d4328877fcc87352fb56de63f0570739d5a9dc4))
|
||||
* **nsjail:** make ansible collections mount non-mandatory ([#9510](https://github.com/windmill-labs/windmill/issues/9510)) ([08da7a1](https://github.com/windmill-labs/windmill/commit/08da7a121b4b835500dfc2bd943c4bdce912c63e))
|
||||
* **nsjail:** make ansible uv tools mount non-mandatory ([#9507](https://github.com/windmill-labs/windmill/issues/9507)) ([dc368a9](https://github.com/windmill-labs/windmill/commit/dc368a9669e812c593ad848266f645b6223501e6))
|
||||
|
||||
## [1.721.0](https://github.com/windmill-labs/windmill/compare/v1.720.0...v1.721.0) (2026-06-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* deployed↔draft compare + AI-session draft bar ([#9435](https://github.com/windmill-labs/windmill/issues/9435)) ([b0b330c](https://github.com/windmill-labs/windmill/commit/b0b330c7864d0159af4b0f17dbb3c09bd015145b))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cli:** reconcile case-only path drift during sync on case-insensitive filesystems (WIN-2020) ([#9485](https://github.com/windmill-labs/windmill/issues/9485)) ([c258928](https://github.com/windmill-labs/windmill/commit/c258928ab62adc1327913c21556520dfd1e5c24c))
|
||||
* drop archived items from fork compare (spurious 'not visible' warning) ([#9481](https://github.com/windmill-labs/windmill/issues/9481)) ([92c21bb](https://github.com/windmill-labs/windmill/commit/92c21bbe6586f3c285796a98692e976515a629d5))
|
||||
* require auth to view approval details when user_auth_required ([#9482](https://github.com/windmill-labs/windmill/issues/9482)) ([5f41ddd](https://github.com/windmill-labs/windmill/commit/5f41ddd3a592bcd504f94fc99060ca5d79c36190))
|
||||
|
||||
## [1.720.0](https://github.com/windmill-labs/windmill/compare/v1.719.0...v1.720.0) (2026-06-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* allow private MCP server URLs ([#9470](https://github.com/windmill-labs/windmill/issues/9470)) ([3bc5800](https://github.com/windmill-labs/windmill/commit/3bc5800197db383ae6f708415701a4bdbe2e3345))
|
||||
* **api:** add endpoint to update token label ([#9474](https://github.com/windmill-labs/windmill/issues/9474)) ([e8e0701](https://github.com/windmill-labs/windmill/commit/e8e0701a360d0614c4c5a74f6410ba6ac0638caa))
|
||||
* **frontend:** use unified drill picker for AI chat @-mention dropdown ([#9159](https://github.com/windmill-labs/windmill/issues/9159)) ([64b089c](https://github.com/windmill-labs/windmill/commit/64b089cd23cca4601abb09f092a32becb80d9394))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* center auth0/okta icons and respect currentColor ([#9457](https://github.com/windmill-labs/windmill/issues/9457)) ([5d0ef7d](https://github.com/windmill-labs/windmill/commit/5d0ef7dfd91b3021d125a1b34f81f0788f173786))
|
||||
* **forks:** keep trigger/schedule operational state owned by the parent - WIN-2019 ([#9476](https://github.com/windmill-labs/windmill/issues/9476)) ([192574a](https://github.com/windmill-labs/windmill/commit/192574ab8f98d9521a232fc8a4935d407b00cb3a))
|
||||
* **frontend:** respect forced column order for numeric column names ([#9463](https://github.com/windmill-labs/windmill/issues/9463)) ([44f5dd6](https://github.com/windmill-labs/windmill/commit/44f5dd6636d4b23aa55383b8b8abe4c2f73bc88d))
|
||||
* **frontend:** use ban icon for canceled jobs instead of hourglass ([#9478](https://github.com/windmill-labs/windmill/issues/9478)) ([fa86c62](https://github.com/windmill-labs/windmill/commit/fa86c62b6600e7d47dadf4706d7002706333d919))
|
||||
* gate native integration pickers behind non-operator check ([#9465](https://github.com/windmill-labs/windmill/issues/9465)) ([6156e23](https://github.com/windmill-labs/windmill/commit/6156e2372a785ccd0c6f29cb74e90bee69e76483))
|
||||
* **oauth:** persist refreshed token through configured secret backend ([#9471](https://github.com/windmill-labs/windmill/issues/9471)) ([76c0d97](https://github.com/windmill-labs/windmill/commit/76c0d970a18bf28ddc48dd746570e73486542606))
|
||||
* refresh session editor preview on breadcrumb target switch ([#9475](https://github.com/windmill-labs/windmill/issues/9475)) ([6d522b3](https://github.com/windmill-labs/windmill/commit/6d522b3989ace1f214bd049d910bc0d2a2a6893e))
|
||||
|
||||
## [1.719.0](https://github.com/windmill-labs/windmill/compare/v1.718.0...v1.719.0) (2026-06-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **otel:** connect jobs to the inbound distributed trace ([#9456](https://github.com/windmill-labs/windmill/issues/9456)) ([fad1a54](https://github.com/windmill-labs/windmill/commit/fad1a549d95c00d0746a48163c4f95fc69733e1a))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* authenticate slack callback payload with per-workspace hmac ([#9461](https://github.com/windmill-labs/windmill/issues/9461)) ([fbdf81b](https://github.com/windmill-labs/windmill/commit/fbdf81ba5f77d282c025360ecee14138dd4cb4a2))
|
||||
* prevent token label collision bypassing job read access control ([#9462](https://github.com/windmill-labs/windmill/issues/9462)) ([e1e7af6](https://github.com/windmill-labs/windmill/commit/e1e7af6a25a44eb06b67332ce1efeae2a21e0c6d))
|
||||
* **python:** escape reserved-keyword step ids in wrapper codegen ([#9460](https://github.com/windmill-labs/windmill/issues/9460)) ([6a15a9b](https://github.com/windmill-labs/windmill/commit/6a15a9b152ad20be4b5c3de6000516da231e41e0)), closes [#8893](https://github.com/windmill-labs/windmill/issues/8893)
|
||||
|
||||
## [1.718.0](https://github.com/windmill-labs/windmill/compare/v1.717.1...v1.718.0) (2026-06-05)
|
||||
|
||||
|
||||
|
||||
+1
-3
@@ -75,8 +75,6 @@ Public CLI surface:
|
||||
- `--model <alias>`: choose the model under test
|
||||
- `--models <a,b,c>`: run the same cases sequentially against several model aliases
|
||||
- `--verbose`: stream assistant output for frontend runs
|
||||
- `--skip-judge`: skip LLM judge scoring for the run
|
||||
- `--execution-only`: only require the model/proxy/frontend loop to complete; skip validators, tool expectations, backend artifact validation, and judge scoring
|
||||
- `--record`: append a compact tracked summary line to `ai_evals/history/<mode>.jsonl` for full-suite runs only
|
||||
- `--backend-validation <mode>`: optional backend smoke validation (`off` or `preview`) for `script` and `flow` evals
|
||||
|
||||
@@ -101,7 +99,7 @@ Notes:
|
||||
- the command also prints accepted alias spellings such as `gpt-4o`, `gpt-55`, `claude-opus-4.6`, and `claude-haiku-4.5`
|
||||
- frontend modes (`flow`, `script`, `app`, `global`) can use Anthropic, OpenAI, Gemini, and DeepSeek-backed aliases
|
||||
- `cli` mode always uses the Anthropic agent SDK, so only Anthropic aliases are valid there
|
||||
- the judge model is separate and currently defaults to `claude-sonnet-4-6`; use `--skip-judge` for deterministic-only runs
|
||||
- the judge model is separate and currently defaults to `claude-sonnet-4-6`
|
||||
|
||||
## Case Format
|
||||
|
||||
|
||||
@@ -16,9 +16,6 @@ export interface PromptRunResult {
|
||||
output: string;
|
||||
durationMs: number;
|
||||
tokenUsage: BenchmarkTokenUsage | null;
|
||||
// Input tokens on the last assistant turn. The SDK `result` message reports
|
||||
// usage cumulatively, so the final context size comes from per-turn usage.
|
||||
finalContextTokens: number | null;
|
||||
trace: CliTrace;
|
||||
}
|
||||
|
||||
@@ -147,7 +144,6 @@ export async function runPromptAndCapture(
|
||||
let output = "";
|
||||
let assistantMessageCount = 0;
|
||||
let tokenUsage: BenchmarkTokenUsage | null = null;
|
||||
let finalContextTokens: number | null = null;
|
||||
const startedAt = Date.now();
|
||||
const stubBinDir = join(cwd, WMILL_STUB_DIR_NAME);
|
||||
const wmillLogPath = join(cwd, WMILL_LOG_FILE_NAME);
|
||||
@@ -170,12 +166,6 @@ export async function runPromptAndCapture(
|
||||
for await (const message of query({ prompt, options })) {
|
||||
if (message.type === "assistant") {
|
||||
assistantMessageCount += 1;
|
||||
const turnContext = anthropicUsageToBenchmarkTokenUsage(
|
||||
message.message?.usage
|
||||
)?.prompt;
|
||||
if (turnContext && turnContext > 0) {
|
||||
finalContextTokens = turnContext;
|
||||
}
|
||||
const content = message.message?.content;
|
||||
if (Array.isArray(content)) {
|
||||
for (const block of content) {
|
||||
@@ -220,7 +210,6 @@ export async function runPromptAndCapture(
|
||||
output,
|
||||
durationMs: Date.now() - startedAt,
|
||||
tokenUsage,
|
||||
finalContextTokens,
|
||||
trace: {
|
||||
toolsUsed,
|
||||
skillsInvoked,
|
||||
|
||||
@@ -25,12 +25,6 @@ export async function runFrontendBenchmarkFromEnv(): Promise<BenchmarkRunResult>
|
||||
);
|
||||
const emitProgress = process.env.WMILL_FRONTEND_AI_EVAL_PROGRESS === "1";
|
||||
const verbose = process.env.WMILL_FRONTEND_AI_EVAL_VERBOSE === "1";
|
||||
const executionOnly =
|
||||
process.env.WMILL_FRONTEND_AI_EVAL_EXECUTION_ONLY === "1";
|
||||
const judgeModel =
|
||||
process.env.WMILL_FRONTEND_AI_EVAL_SKIP_JUDGE === "1" || executionOnly
|
||||
? null
|
||||
: DEFAULT_JUDGE_MODEL;
|
||||
const model = resolveEvalModel(
|
||||
mode,
|
||||
process.env.WMILL_FRONTEND_AI_EVAL_MODEL,
|
||||
@@ -54,8 +48,7 @@ export async function runFrontendBenchmarkFromEnv(): Promise<BenchmarkRunResult>
|
||||
cases: selectedCases,
|
||||
runs,
|
||||
runModel,
|
||||
judgeModel,
|
||||
executionOnly,
|
||||
judgeModel: DEFAULT_JUDGE_MODEL,
|
||||
concurrency: verbose ? 1 : undefined,
|
||||
verbose,
|
||||
onProgress: emitProgress
|
||||
@@ -67,7 +60,7 @@ export async function runFrontendBenchmarkFromEnv(): Promise<BenchmarkRunResult>
|
||||
mode,
|
||||
runs,
|
||||
runModel,
|
||||
judgeModel,
|
||||
judgeModel: DEFAULT_JUDGE_MODEL,
|
||||
caseResults,
|
||||
});
|
||||
}
|
||||
@@ -103,12 +96,7 @@ async function getModeRunner(
|
||||
}
|
||||
|
||||
function parseMode(value: string | undefined): FrontendBenchmarkMode {
|
||||
if (
|
||||
value === "flow" ||
|
||||
value === "app" ||
|
||||
value === "script" ||
|
||||
value === "global"
|
||||
) {
|
||||
if (value === "flow" || value === "app" || value === "script" || value === "global") {
|
||||
return value;
|
||||
}
|
||||
throw new Error(`Unsupported frontend benchmark mode: ${String(value)}`);
|
||||
|
||||
@@ -38,7 +38,6 @@ export interface AppEvalResult {
|
||||
toolCallCount: number;
|
||||
toolsUsed: string[];
|
||||
tokenUsage: TokenUsage;
|
||||
finalContextTokens: number | null;
|
||||
}
|
||||
|
||||
export interface AppEvalOptions {
|
||||
@@ -114,7 +113,6 @@ export async function runAppEval(
|
||||
toolCallCount: rawResult.toolCallsCount,
|
||||
toolsUsed: rawResult.toolsCalled,
|
||||
tokenUsage: rawResult.tokenUsage,
|
||||
finalContextTokens: rawResult.finalContextTokens,
|
||||
};
|
||||
} finally {
|
||||
await cleanup();
|
||||
|
||||
@@ -39,7 +39,6 @@ export interface FlowEvalResult {
|
||||
toolsUsed: string[];
|
||||
toolCallDetails: ToolCallDetail[];
|
||||
tokenUsage: TokenUsage;
|
||||
finalContextTokens: number | null;
|
||||
}
|
||||
|
||||
export interface FlowEvalOptions {
|
||||
@@ -114,7 +113,6 @@ export async function runFlowEval(
|
||||
toolsUsed: rawResult.toolsCalled,
|
||||
toolCallDetails: rawResult.toolCallDetails,
|
||||
tokenUsage: rawResult.tokenUsage,
|
||||
finalContextTokens: rawResult.finalContextTokens,
|
||||
};
|
||||
} finally {
|
||||
await cleanup();
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from "../../../../../frontend/src/lib/components/copilot/chat/global/core";
|
||||
import {
|
||||
clearGlobalDrafts,
|
||||
getGlobalDraft,
|
||||
listGlobalDrafts,
|
||||
} from "../../../../../frontend/src/lib/components/copilot/chat/global/userDraftAdapter";
|
||||
import type { Tool as ProductionTool } from "../../../../../frontend/src/lib/components/copilot/chat/shared";
|
||||
@@ -19,7 +18,6 @@ import type { GlobalDraftState } from "../../../../core/validators";
|
||||
import type { WindmillBackendSettings } from "../../../../core/windmillBackendSettings";
|
||||
import {
|
||||
registerBenchmarkWorkspaceRunnables,
|
||||
seedBenchmarkDraft,
|
||||
unregisterBenchmarkWorkspaceRunnables,
|
||||
type BenchmarkWorkspaceRunnables,
|
||||
} from "../../mockBackend";
|
||||
@@ -32,10 +30,6 @@ const MUTATING_GLOBAL_TOOLS = new Set([
|
||||
]);
|
||||
const DISABLE_ACTIVE_EDITOR_CONTEXT_ENV =
|
||||
"WMILL_AI_EVAL_DISABLE_ACTIVE_EDITOR_CONTEXT";
|
||||
// A/B gate for the search_app read tool: set to "1" to run the baseline arm
|
||||
// (toolset without search_app) so its token cost can be compared against the arm
|
||||
// that offers it.
|
||||
const DISABLE_SEARCH_APP_ENV = "WMILL_AI_EVAL_DISABLE_SEARCH_APP";
|
||||
|
||||
const LIVE_EDITOR_ITEM_KINDS = {
|
||||
script: "script",
|
||||
@@ -50,20 +44,6 @@ export interface GlobalLiveEditorDraftFixture {
|
||||
value?: unknown;
|
||||
}
|
||||
|
||||
// Identity the global system prompt builds paths from. Production reads
|
||||
// `userStore` (whoami) to fill `u/{username}/...`; the eval harness never logs
|
||||
// in, so without this the prompt sees an empty username (`u//...`) and no
|
||||
// path-selection case is meaningful. Seeded per-case via the initial fixture and
|
||||
// passed straight to `prepareGlobalSystemMessage` (no global-store mutation).
|
||||
export interface GlobalUserFixture {
|
||||
username: string;
|
||||
is_admin?: boolean;
|
||||
/** Folders the user can write to (the writable set whoami returns). */
|
||||
folders?: string[];
|
||||
/** Folders the user can read; read-only folders = folders_read \ folders. */
|
||||
folders_read?: string[];
|
||||
}
|
||||
|
||||
export interface GlobalEvalResult {
|
||||
success: boolean;
|
||||
state: GlobalDraftState;
|
||||
@@ -73,13 +53,11 @@ export interface GlobalEvalResult {
|
||||
toolsUsed: string[];
|
||||
toolCallDetails: ToolCallDetail[];
|
||||
tokenUsage: TokenUsage;
|
||||
finalContextTokens: number | null;
|
||||
}
|
||||
|
||||
export interface GlobalEvalOptions {
|
||||
workspaceFixtures?: BenchmarkWorkspaceRunnables;
|
||||
liveEditorDrafts?: GlobalLiveEditorDraftFixture[];
|
||||
user?: GlobalUserFixture;
|
||||
model?: string;
|
||||
maxIterations?: number;
|
||||
provider?: AIProvider;
|
||||
@@ -105,11 +83,9 @@ export async function runGlobalEval(
|
||||
const model = options.model ?? "claude-haiku-4-5-20251001";
|
||||
const injectActiveEditorContext =
|
||||
process.env[DISABLE_ACTIVE_EDITOR_CONTEXT_ENV] !== "1";
|
||||
// Pass the seeded identity straight to the prompt builder rather than mutating
|
||||
// the process-global `userStore`, so concurrent cases never race on it.
|
||||
const rawResult = await runEval({
|
||||
userPrompt,
|
||||
systemMessage: prepareGlobalSystemMessage(undefined, { user: options.user }),
|
||||
systemMessage: prepareGlobalSystemMessage(),
|
||||
userMessage: prepareGlobalUserMessage(
|
||||
userPrompt,
|
||||
[],
|
||||
@@ -118,7 +94,7 @@ export async function runGlobalEval(
|
||||
tools: getGlobalEvalTools(),
|
||||
helpers: {},
|
||||
apiKey,
|
||||
getOutput: () => collectGlobalDraftState(workspaceRoot),
|
||||
getOutput: () => ({ drafts: listGlobalDrafts(workspaceRoot) }),
|
||||
onAssistantMessageStart: options.runContext?.onAssistantMessageStart,
|
||||
onAssistantToken: options.runContext?.onAssistantChunk,
|
||||
onAssistantMessageEnd: options.runContext?.onAssistantMessageEnd,
|
||||
@@ -143,7 +119,6 @@ export async function runGlobalEval(
|
||||
toolsUsed: rawResult.toolsCalled,
|
||||
toolCallDetails: rawResult.toolCallDetails,
|
||||
tokenUsage: rawResult.tokenUsage,
|
||||
finalContextTokens: rawResult.finalContextTokens,
|
||||
};
|
||||
} finally {
|
||||
clearGlobalDrafts(workspaceRoot);
|
||||
@@ -155,32 +130,6 @@ export async function runGlobalEval(
|
||||
}
|
||||
}
|
||||
|
||||
// Build the harness output from the DB-backed drafts. `listGlobalDrafts` returns
|
||||
// metadata-only rows for backend drafts (the model's `write_script` etc. persist
|
||||
// straight to the backend with no in-tab editor cell), so re-read each such row
|
||||
// with `getGlobalDraft` to attach the full value the validators assert on. A row
|
||||
// that already carries a value (the production in-tab cell overlay) is kept as-is.
|
||||
async function collectGlobalDraftState(
|
||||
workspace: string,
|
||||
): Promise<GlobalDraftState> {
|
||||
const items = await listGlobalDrafts(workspace);
|
||||
const drafts = await Promise.all(
|
||||
items.map(async (item) => {
|
||||
if (item.value !== undefined) {
|
||||
return item;
|
||||
}
|
||||
const full = await getGlobalDraft(
|
||||
workspace,
|
||||
item.type,
|
||||
item.path,
|
||||
item.triggerKind,
|
||||
);
|
||||
return full ?? item;
|
||||
}),
|
||||
);
|
||||
return { drafts: drafts as GlobalDraftState["drafts"] };
|
||||
}
|
||||
|
||||
function seedLiveEditorDrafts(
|
||||
workspace: string,
|
||||
fixtures: GlobalLiveEditorDraftFixture[],
|
||||
@@ -189,9 +138,7 @@ function seedLiveEditorDrafts(
|
||||
const itemKind = LIVE_EDITOR_ITEM_KINDS[fixture.type];
|
||||
const storagePath = fixture.storagePath ?? fixture.effectivePath ?? "";
|
||||
if (fixture.value !== undefined) {
|
||||
// Seed as a backend draft row, not an in-tab cell: a cell would shadow the
|
||||
// model's DB-backed edit when the output is read back via listGlobalDrafts.
|
||||
seedBenchmarkDraft(workspace, itemKind, storagePath, fixture.value);
|
||||
UserDraft.save(itemKind, storagePath, fixture.value, { workspace });
|
||||
}
|
||||
UserDraft.setLiveEditorDraft({
|
||||
workspace,
|
||||
@@ -214,28 +161,25 @@ function clearLiveEditorDrafts(
|
||||
}
|
||||
|
||||
function getGlobalEvalTools(): ProductionTool<{}>[] {
|
||||
const disableSearchApp = process.env[DISABLE_SEARCH_APP_ENV] === "1";
|
||||
return (globalTools as ProductionTool<{}>[])
|
||||
.filter((tool) => !(disableSearchApp && tool.def.function.name === "search_app"))
|
||||
.map((tool) => {
|
||||
if (!MUTATING_GLOBAL_TOOLS.has(tool.def.function.name)) {
|
||||
return tool;
|
||||
}
|
||||
return (globalTools as ProductionTool<{}>[]).map((tool) => {
|
||||
if (!MUTATING_GLOBAL_TOOLS.has(tool.def.function.name)) {
|
||||
return tool;
|
||||
}
|
||||
|
||||
return {
|
||||
...tool,
|
||||
requiresConfirmation: false,
|
||||
validateBeforeConfirmation: undefined,
|
||||
fn: async () =>
|
||||
JSON.stringify(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
"This mutating workspace tool is disabled during ai_evals global mode.",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
};
|
||||
});
|
||||
return {
|
||||
...tool,
|
||||
requiresConfirmation: false,
|
||||
validateBeforeConfirmation: undefined,
|
||||
fn: async () =>
|
||||
JSON.stringify(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
"This mutating workspace tool is disabled during ai_evals global mode.",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ export interface ScriptEvalResult {
|
||||
toolsUsed: string[];
|
||||
toolCallDetails: ToolCallDetail[];
|
||||
tokenUsage: TokenUsage;
|
||||
finalContextTokens: number | null;
|
||||
}
|
||||
|
||||
export interface ScriptEvalOptions {
|
||||
@@ -112,7 +111,6 @@ export async function runScriptEval(
|
||||
toolsUsed: rawResult.toolsCalled,
|
||||
toolCallDetails: rawResult.toolCallDetails,
|
||||
tokenUsage: rawResult.tokenUsage,
|
||||
finalContextTokens: rawResult.finalContextTokens,
|
||||
};
|
||||
} finally {
|
||||
await cleanup();
|
||||
|
||||
@@ -38,9 +38,8 @@ export interface RunEvalParams<THelpers, TOutput> {
|
||||
helpers: THelpers;
|
||||
/** API key for the provider */
|
||||
apiKey: string;
|
||||
/** Function to get the current output state. May be async — global mode reads
|
||||
* DB-backed drafts back through the (mocked) backend to build its output. */
|
||||
getOutput: () => TOutput | Promise<TOutput>;
|
||||
/** Function to get the current output state */
|
||||
getOutput: () => TOutput;
|
||||
/** Model and Windmill backend configuration */
|
||||
options: EvalRunnerOptions;
|
||||
onAssistantMessageStart?: () => void;
|
||||
@@ -155,10 +154,9 @@ export async function runEval<THelpers, TOutput>(
|
||||
if (result.hitMaxIterations) {
|
||||
return {
|
||||
success: false,
|
||||
output: (await getOutput()) as TOutput,
|
||||
output: getOutput(),
|
||||
error: `Reached max turns (${maxIterations})`,
|
||||
tokenUsage: result.tokenUsage,
|
||||
finalContextTokens: result.lastIterationUsage?.prompt ?? null,
|
||||
toolCallsCount,
|
||||
toolsCalled,
|
||||
toolCallDetails,
|
||||
@@ -172,9 +170,8 @@ export async function runEval<THelpers, TOutput>(
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: (await getOutput()) as TOutput,
|
||||
output: getOutput(),
|
||||
tokenUsage: result.tokenUsage,
|
||||
finalContextTokens: result.lastIterationUsage?.prompt ?? null,
|
||||
toolCallsCount,
|
||||
toolsCalled,
|
||||
toolCallDetails,
|
||||
@@ -194,10 +191,9 @@ export async function runEval<THelpers, TOutput>(
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: (await getOutput()) as TOutput,
|
||||
output: getOutput(),
|
||||
error: errorMessage,
|
||||
tokenUsage: { prompt: 0, completion: 0, total: 0 },
|
||||
finalContextTokens: null,
|
||||
toolCallsCount,
|
||||
toolsCalled,
|
||||
toolCallDetails,
|
||||
|
||||
@@ -28,8 +28,6 @@ export interface RawEvalResult<TOutput> {
|
||||
output: TOutput;
|
||||
error?: string;
|
||||
tokenUsage: TokenUsage;
|
||||
/** Input tokens on the last model request of the loop (see BenchmarkAttemptResult.finalContextTokens). */
|
||||
finalContextTokens: number | null;
|
||||
toolCallsCount: number;
|
||||
toolsCalled: string[];
|
||||
toolCallDetails: ToolCallDetail[];
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type {
|
||||
AppWithLastVersion,
|
||||
CompletedJob,
|
||||
Flow,
|
||||
Job,
|
||||
ListableApp,
|
||||
Script
|
||||
} from '../../../frontend/src/lib/gen'
|
||||
import type { CompletedJob, Flow, Script } from '../../../frontend/src/lib/gen'
|
||||
import type {
|
||||
DataTableTables,
|
||||
DataTableTableSchema,
|
||||
GetDraftForUserResponse,
|
||||
ListDraftsResponse,
|
||||
ScriptLang,
|
||||
UpdateDraftResponse,
|
||||
UserDraftItemKind
|
||||
ScriptLang
|
||||
} from '../../../frontend/src/lib/gen/types.gen'
|
||||
import { buildScriptLintResult } from './core/script/preview'
|
||||
import { applyDatatableSql, type BenchmarkDatatableSeed } from './datatableSqlEngine'
|
||||
@@ -40,54 +29,22 @@ export interface BenchmarkWorkspaceFlow {
|
||||
value: Flow['value']
|
||||
}
|
||||
|
||||
export interface BenchmarkWorkspaceApp {
|
||||
path: string
|
||||
summary: string
|
||||
value: {
|
||||
files: Record<string, string>
|
||||
runnables: Record<string, unknown>
|
||||
data?: unknown
|
||||
policy?: unknown
|
||||
custom_path?: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export interface BenchmarkWorkspaceJob {
|
||||
/** Stable id so a case prompt can reference a specific run (e.g. for get_job_logs). */
|
||||
id?: string
|
||||
jobKind?: CompletedJob['job_kind']
|
||||
scriptPath?: string
|
||||
createdBy?: string
|
||||
label?: string
|
||||
success?: boolean
|
||||
logs?: string
|
||||
}
|
||||
|
||||
export interface BenchmarkWorkspaceRunnables {
|
||||
scripts?: BenchmarkWorkspaceScript[]
|
||||
flows?: BenchmarkWorkspaceFlow[]
|
||||
apps?: BenchmarkWorkspaceApp[]
|
||||
datatables?: BenchmarkDatatableSeed[]
|
||||
jobs?: BenchmarkWorkspaceJob[]
|
||||
}
|
||||
|
||||
type BenchmarkCompletedJob = CompletedJob & { type: 'CompletedJob' }
|
||||
|
||||
const benchmarkWorkspaces = new Set<string>()
|
||||
const benchmarkWorkspaceRunnables = new Map<string, BenchmarkWorkspaceRunnables>()
|
||||
// Keyed by `${workspace}::${jobId}` so concurrent attempts (or distinct cases)
|
||||
// can seed the same fixed job id without clobbering each other's entry.
|
||||
const benchmarkJobs = new Map<string, { workspace: string; job: BenchmarkCompletedJob }>()
|
||||
|
||||
function benchmarkJobKey(workspace: string, jobId: string): string {
|
||||
return `${workspace}::${jobId}`
|
||||
}
|
||||
|
||||
export function resetBenchmarkMockBackend(): void {
|
||||
benchmarkWorkspaces.clear()
|
||||
benchmarkWorkspaceRunnables.clear()
|
||||
benchmarkJobs.clear()
|
||||
benchmarkDrafts.clear()
|
||||
}
|
||||
|
||||
export function registerBenchmarkWorkspace(workspace: string): void {
|
||||
@@ -99,33 +56,17 @@ export function registerBenchmarkWorkspaceRunnables(
|
||||
runnables: BenchmarkWorkspaceRunnables
|
||||
): void {
|
||||
benchmarkWorkspaces.add(workspace)
|
||||
// Fresh case: drop any drafts left from a prior run on this workspace id.
|
||||
clearBenchmarkDrafts(workspace)
|
||||
// Datatables are mutated in place by exec_datatable_sql (a write must be visible
|
||||
// to later reads), so store an isolated deep copy — never mutate the caller's seed.
|
||||
benchmarkWorkspaceRunnables.set(workspace, {
|
||||
...runnables,
|
||||
datatables: runnables.datatables ? structuredClone(runnables.datatables) : undefined
|
||||
})
|
||||
// Seed any fixture jobs so list_runs / get_job_logs have data to return.
|
||||
for (const seed of runnables.jobs ?? []) {
|
||||
createBenchmarkCompletedJob({
|
||||
workspace,
|
||||
id: seed.id,
|
||||
jobKind: seed.jobKind ?? 'script',
|
||||
success: seed.success,
|
||||
scriptPath: seed.scriptPath,
|
||||
createdBy: seed.createdBy,
|
||||
label: seed.label,
|
||||
logs: seed.logs
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function unregisterBenchmarkWorkspace(workspace: string): void {
|
||||
benchmarkWorkspaces.delete(workspace)
|
||||
benchmarkWorkspaceRunnables.delete(workspace)
|
||||
clearBenchmarkDrafts(workspace)
|
||||
for (const [jobId, entry] of benchmarkJobs.entries()) {
|
||||
if (entry.workspace === workspace) {
|
||||
benchmarkJobs.delete(jobId)
|
||||
@@ -181,22 +122,6 @@ export function getBenchmarkFlowByPath(workspace: string, path: string): Flow |
|
||||
return flow ? buildBenchmarkFlow(flow) : null
|
||||
}
|
||||
|
||||
export function listBenchmarkApps(workspace: string): ListableApp[] | null {
|
||||
const runnables = benchmarkWorkspaceRunnables.get(workspace)
|
||||
if (!runnables) {
|
||||
return null
|
||||
}
|
||||
return (runnables.apps ?? []).map(buildBenchmarkListableApp)
|
||||
}
|
||||
|
||||
export function getBenchmarkAppByPath(workspace: string, path: string): AppWithLastVersion | null {
|
||||
const app = benchmarkWorkspaceRunnables
|
||||
.get(workspace)
|
||||
?.apps?.find((entry) => entry.path === path)
|
||||
|
||||
return app ? buildBenchmarkApp(app) : null
|
||||
}
|
||||
|
||||
export function createBenchmarkCompletedJob(input: {
|
||||
workspace: string
|
||||
jobKind: CompletedJob['job_kind']
|
||||
@@ -206,17 +131,14 @@ export function createBenchmarkCompletedJob(input: {
|
||||
scriptPath?: string
|
||||
scriptHash?: string
|
||||
args?: Record<string, unknown>
|
||||
id?: string
|
||||
createdBy?: string
|
||||
label?: string
|
||||
}): string {
|
||||
const jobId = input.id ?? `benchmark-job-${randomUUID()}`
|
||||
const jobId = `benchmark-job-${randomUUID()}`
|
||||
const now = new Date().toISOString()
|
||||
const job: BenchmarkCompletedJob = {
|
||||
type: 'CompletedJob',
|
||||
id: jobId,
|
||||
workspace_id: input.workspace,
|
||||
created_by: input.createdBy ?? 'ai-evals',
|
||||
created_by: 'ai-evals',
|
||||
created_at: now,
|
||||
started_at: now,
|
||||
completed_at: now,
|
||||
@@ -234,11 +156,10 @@ export function createBenchmarkCompletedJob(input: {
|
||||
is_skipped: false,
|
||||
email: 'ai-evals@local',
|
||||
visible_to_owner: true,
|
||||
tag: 'benchmark',
|
||||
labels: input.label ? [input.label] : undefined
|
||||
tag: 'benchmark'
|
||||
}
|
||||
|
||||
benchmarkJobs.set(benchmarkJobKey(input.workspace, jobId), { workspace: input.workspace, job })
|
||||
benchmarkJobs.set(jobId, { workspace: input.workspace, job })
|
||||
return jobId
|
||||
}
|
||||
|
||||
@@ -246,146 +167,13 @@ export function getBenchmarkCompletedJob(
|
||||
workspace: string,
|
||||
jobId: string
|
||||
): BenchmarkCompletedJob | null {
|
||||
const entry = benchmarkJobs.get(benchmarkJobKey(workspace, jobId))
|
||||
if (!entry) {
|
||||
const entry = benchmarkJobs.get(jobId)
|
||||
if (!entry || entry.workspace !== workspace) {
|
||||
return null
|
||||
}
|
||||
return structuredClone(entry.job)
|
||||
}
|
||||
|
||||
/**
|
||||
* List seeded/recorded jobs for a benchmark workspace, most recent first —
|
||||
* the shape `JobService.listJobs` returns. Returns `null` for a non-benchmark
|
||||
* workspace so the caller can fall through to the real backend. Server-side
|
||||
* filters (path/creator/status/limit) are intentionally not applied: global
|
||||
* eval cases assert on the recorded `list_runs` tool call, not on filtering.
|
||||
*/
|
||||
export function listBenchmarkJobs(workspace: string): Job[] | null {
|
||||
if (!hasBenchmarkWorkspace(workspace)) {
|
||||
return null
|
||||
}
|
||||
return [...benchmarkJobs.values()]
|
||||
.filter((entry) => entry.workspace === workspace)
|
||||
.map((entry) => structuredClone(entry.job) as Job)
|
||||
.sort((a, b) => (b.created_at ?? '').localeCompare(a.created_at ?? ''))
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirror `JobService.getJobLogs` (response is the raw log string). Throws a
|
||||
* "not found" error for an unknown id, matching the backend 404.
|
||||
*/
|
||||
export function getBenchmarkJobLogs(workspace: string, jobId: string): string {
|
||||
const job = getBenchmarkCompletedJob(workspace, jobId)
|
||||
if (!job) {
|
||||
throw new Error(`Job Logs not found for "${jobId}"`)
|
||||
}
|
||||
return job.logs ?? ''
|
||||
}
|
||||
|
||||
// ============= Drafts (per-user, DB-backed in production) =============
|
||||
|
||||
/**
|
||||
* In-memory stand-in for the per-user draft backend (`DraftService`). The global
|
||||
* AI chat now persists and reads drafts through the backend DB instead of an
|
||||
* in-tab `UserDraft` cell, so the eval mocks the three draft endpoints it
|
||||
* exercises (`updateDraft` / `getDraftForUser` / `listDrafts`) and keeps the
|
||||
* saved values here, keyed by workspace + draft kind + storage path. Mirrors the
|
||||
* semantics of the production unit test's mock in
|
||||
* `frontend/src/lib/components/copilot/chat/global/core.test.ts`.
|
||||
*/
|
||||
const benchmarkDrafts = new Map<
|
||||
string,
|
||||
{ workspace: string; kind: UserDraftItemKind; path: string; value: unknown }
|
||||
>()
|
||||
|
||||
// Fixed timestamp so artifacts stay deterministic. No eval simulates a
|
||||
// concurrent writer, so every save is accepted and the conflict branch is
|
||||
// never taken — the syncer just records this as its `last_sync` baseline.
|
||||
const BENCHMARK_DRAFT_TIMESTAMP = '1970-01-01T00:00:00.000Z'
|
||||
|
||||
function benchmarkDraftKey(workspace: string, kind: string, path: string): string {
|
||||
return `${workspace}::${kind}::${path}`
|
||||
}
|
||||
|
||||
export function clearBenchmarkDrafts(workspace: string): void {
|
||||
for (const [key, entry] of benchmarkDrafts.entries()) {
|
||||
if (entry.workspace === workspace) {
|
||||
benchmarkDrafts.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed a draft straight into the store — used by the eval's live-editor draft
|
||||
* fixtures, which model "the user already has this draft open/saved". Writing it
|
||||
* here (instead of through `UserDraft.save`) keeps it a backend draft row with no
|
||||
* shadowing in-tab cell, so a model edit that persists to the backend is what the
|
||||
* output read-back captures — not the stale seed.
|
||||
*/
|
||||
export function seedBenchmarkDraft(
|
||||
workspace: string,
|
||||
kind: UserDraftItemKind,
|
||||
path: string,
|
||||
value: unknown
|
||||
): void {
|
||||
benchmarkDrafts.set(benchmarkDraftKey(workspace, kind, path), {
|
||||
workspace,
|
||||
kind,
|
||||
path,
|
||||
value
|
||||
})
|
||||
}
|
||||
|
||||
/** Mirror `DraftService.updateDraft`: a `null`/omitted value deletes the row. */
|
||||
export function updateBenchmarkDraft(input: {
|
||||
workspace: string
|
||||
kind: UserDraftItemKind
|
||||
path: string
|
||||
requestBody?: { value?: unknown }
|
||||
}): UpdateDraftResponse {
|
||||
const key = benchmarkDraftKey(input.workspace, input.kind, input.path)
|
||||
const value = input.requestBody?.value
|
||||
if (value == null) {
|
||||
benchmarkDrafts.delete(key)
|
||||
} else {
|
||||
benchmarkDrafts.set(key, {
|
||||
workspace: input.workspace,
|
||||
kind: input.kind,
|
||||
path: input.path,
|
||||
value
|
||||
})
|
||||
}
|
||||
return { status: 'saved', current_timestamp: BENCHMARK_DRAFT_TIMESTAMP }
|
||||
}
|
||||
|
||||
/** Mirror `DraftService.getDraftForUser`: 404-shaped throw when absent so the
|
||||
* adapter's narrowed catch treats it as "no draft" instead of re-throwing. */
|
||||
export function getBenchmarkDraftForUser(input: {
|
||||
workspace: string
|
||||
kind: UserDraftItemKind
|
||||
path: string
|
||||
}): GetDraftForUserResponse {
|
||||
const entry = benchmarkDrafts.get(benchmarkDraftKey(input.workspace, input.kind, input.path))
|
||||
if (!entry) {
|
||||
throw Object.assign(new Error(`no draft for "${input.path}"`), { status: 404 })
|
||||
}
|
||||
return { value: entry.value, created_at: BENCHMARK_DRAFT_TIMESTAMP }
|
||||
}
|
||||
|
||||
/** Mirror `DraftService.listDrafts`: metadata rows (no value) for a workspace. */
|
||||
export function listBenchmarkDrafts(workspace: string): ListDraftsResponse {
|
||||
return [...benchmarkDrafts.values()]
|
||||
.filter((entry) => entry.workspace === workspace)
|
||||
.map((entry) => ({
|
||||
kind: entry.kind,
|
||||
path: entry.path,
|
||||
summary: (entry.value as { summary?: string } | null)?.summary,
|
||||
draft_only: true,
|
||||
legacy_draft: false,
|
||||
created_at: BENCHMARK_DRAFT_TIMESTAMP
|
||||
}))
|
||||
}
|
||||
|
||||
// ============= Datatables (best-effort in-memory SQL) =============
|
||||
|
||||
/**
|
||||
@@ -640,35 +428,3 @@ function buildBenchmarkFlow(flow: BenchmarkWorkspaceFlow): Flow {
|
||||
extra_perms: {}
|
||||
} as Flow
|
||||
}
|
||||
|
||||
function buildBenchmarkListableApp(app: BenchmarkWorkspaceApp): ListableApp {
|
||||
return {
|
||||
id: 0,
|
||||
workspace_id: 'benchmark',
|
||||
path: app.path,
|
||||
summary: app.summary,
|
||||
version: 1,
|
||||
extra_perms: {},
|
||||
edited_at: BENCHMARK_TIMESTAMP,
|
||||
execution_mode: 'viewer',
|
||||
raw_app: true
|
||||
}
|
||||
}
|
||||
|
||||
function buildBenchmarkApp(app: BenchmarkWorkspaceApp): AppWithLastVersion {
|
||||
return {
|
||||
id: 0,
|
||||
workspace_id: 'benchmark',
|
||||
path: app.path,
|
||||
summary: app.summary,
|
||||
versions: [1],
|
||||
created_by: 'benchmark',
|
||||
created_at: BENCHMARK_TIMESTAMP,
|
||||
value: app.value,
|
||||
policy: (app.value.policy ?? {}) as AppWithLastVersion['policy'],
|
||||
execution_mode: 'viewer',
|
||||
extra_perms: {},
|
||||
custom_path: app.value.custom_path as string | undefined,
|
||||
raw_app: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
|
||||
import {
|
||||
clearBenchmarkDrafts,
|
||||
getBenchmarkDraftForUser,
|
||||
listBenchmarkDrafts,
|
||||
resetBenchmarkMockBackend,
|
||||
seedBenchmarkDraft,
|
||||
updateBenchmarkDraft
|
||||
} from './mockBackend'
|
||||
|
||||
const WORKSPACE = 'benchmark-drafts-ws'
|
||||
|
||||
// Drives the in-memory stand-in for the per-user draft backend (`DraftService`)
|
||||
// that the global AI-chat eval round-trips its drafts through. Mirrors the
|
||||
// production-unit-test mock in
|
||||
// `frontend/src/lib/components/copilot/chat/global/core.test.ts`.
|
||||
describe('mockBackend drafts', () => {
|
||||
beforeEach(() => resetBenchmarkMockBackend())
|
||||
afterEach(() => resetBenchmarkMockBackend())
|
||||
|
||||
it('round-trips a saved draft through update / get / list', () => {
|
||||
const value = { summary: 'Greet a user', content: 'export async function main() {}' }
|
||||
const res = updateBenchmarkDraft({
|
||||
workspace: WORKSPACE,
|
||||
kind: 'script',
|
||||
path: 'f/evals/greet',
|
||||
requestBody: { value }
|
||||
})
|
||||
expect(res.status).toBe('saved')
|
||||
|
||||
expect(getBenchmarkDraftForUser({ workspace: WORKSPACE, kind: 'script', path: 'f/evals/greet' }).value).toEqual(
|
||||
value
|
||||
)
|
||||
|
||||
const rows = listBenchmarkDrafts(WORKSPACE)
|
||||
expect(rows).toHaveLength(1)
|
||||
expect(rows[0]).toMatchObject({ kind: 'script', path: 'f/evals/greet', summary: 'Greet a user', draft_only: true })
|
||||
})
|
||||
|
||||
it('treats a null value as a delete', () => {
|
||||
updateBenchmarkDraft({
|
||||
workspace: WORKSPACE,
|
||||
kind: 'variable',
|
||||
path: 'f/evals/token',
|
||||
requestBody: { value: { summary: 'token' } }
|
||||
})
|
||||
updateBenchmarkDraft({
|
||||
workspace: WORKSPACE,
|
||||
kind: 'variable',
|
||||
path: 'f/evals/token',
|
||||
requestBody: { value: null }
|
||||
})
|
||||
|
||||
expect(listBenchmarkDrafts(WORKSPACE)).toHaveLength(0)
|
||||
expect(() => getBenchmarkDraftForUser({ workspace: WORKSPACE, kind: 'variable', path: 'f/evals/token' })).toThrow()
|
||||
})
|
||||
|
||||
it('throws a 404-shaped error when no draft exists', () => {
|
||||
try {
|
||||
getBenchmarkDraftForUser({ workspace: WORKSPACE, kind: 'script', path: 'f/evals/missing' })
|
||||
throw new Error('expected a throw')
|
||||
} catch (e) {
|
||||
expect((e as { status?: number }).status).toBe(404)
|
||||
}
|
||||
})
|
||||
|
||||
it('seeds a draft as a backend row that a later edit overwrites', () => {
|
||||
seedBenchmarkDraft(WORKSPACE, 'script', 'f/evals/current', { content: 'seed' })
|
||||
expect(getBenchmarkDraftForUser({ workspace: WORKSPACE, kind: 'script', path: 'f/evals/current' }).value).toEqual({
|
||||
content: 'seed'
|
||||
})
|
||||
|
||||
// A model edit persists the same path and must win over the seed.
|
||||
updateBenchmarkDraft({
|
||||
workspace: WORKSPACE,
|
||||
kind: 'script',
|
||||
path: 'f/evals/current',
|
||||
requestBody: { value: { content: 'edited' } }
|
||||
})
|
||||
expect(getBenchmarkDraftForUser({ workspace: WORKSPACE, kind: 'script', path: 'f/evals/current' }).value).toEqual({
|
||||
content: 'edited'
|
||||
})
|
||||
})
|
||||
|
||||
it('clears only the targeted workspace', () => {
|
||||
seedBenchmarkDraft(WORKSPACE, 'script', 'f/a', { content: 'a' })
|
||||
seedBenchmarkDraft('other-ws', 'script', 'f/b', { content: 'b' })
|
||||
|
||||
clearBenchmarkDrafts(WORKSPACE)
|
||||
|
||||
expect(listBenchmarkDrafts(WORKSPACE)).toHaveLength(0)
|
||||
expect(listBenchmarkDrafts('other-ws')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
@@ -24,8 +24,6 @@ export async function runFrontendBenchmarkAdapter(input: {
|
||||
runs: number;
|
||||
model?: string;
|
||||
verbose?: boolean;
|
||||
skipJudge?: boolean;
|
||||
executionOnly?: boolean;
|
||||
backendValidation?: string;
|
||||
}): Promise<BenchmarkRunResult> {
|
||||
const tempDir = await mkdtemp(
|
||||
@@ -42,9 +40,6 @@ export async function runFrontendBenchmarkAdapter(input: {
|
||||
WMILL_FRONTEND_AI_EVAL_MODEL: input.model ?? "",
|
||||
WMILL_FRONTEND_AI_EVAL_PROGRESS: "1",
|
||||
WMILL_FRONTEND_AI_EVAL_VERBOSE: input.verbose ? "1" : "0",
|
||||
WMILL_FRONTEND_AI_EVAL_SKIP_JUDGE:
|
||||
input.skipJudge || input.executionOnly ? "1" : "0",
|
||||
WMILL_FRONTEND_AI_EVAL_EXECUTION_ONLY: input.executionOnly ? "1" : "0",
|
||||
WMILL_FRONTEND_AI_EVAL_BACKEND_VALIDATION: input.backendValidation ?? "",
|
||||
};
|
||||
|
||||
|
||||
@@ -33,29 +33,22 @@ vi.mock('$lib/components/vscode', () => ({}))
|
||||
vi.mock('$lib/gen', async () => {
|
||||
const actual = await vi.importActual<any>('$lib/gen')
|
||||
const {
|
||||
getBenchmarkAppByPath,
|
||||
getBenchmarkCompletedJob,
|
||||
getBenchmarkCompletedJobResultMaybe,
|
||||
getBenchmarkDatatableSchema,
|
||||
getBenchmarkDraftForUser,
|
||||
getBenchmarkFlowByPath,
|
||||
getBenchmarkJobLogs,
|
||||
getBenchmarkScriptByHash,
|
||||
getBenchmarkScriptByPath,
|
||||
hasBenchmarkWorkspace,
|
||||
listBenchmarkApps,
|
||||
listBenchmarkDatatables,
|
||||
listBenchmarkDrafts,
|
||||
listBenchmarkFlows,
|
||||
listBenchmarkJobs,
|
||||
listBenchmarkScripts,
|
||||
createBenchmarkHttpTrigger,
|
||||
createBenchmarkSchedule,
|
||||
previewBenchmarkSchedule,
|
||||
runBenchmarkDatatableSql,
|
||||
runBenchmarkFlowByPath,
|
||||
runBenchmarkScriptPreview,
|
||||
updateBenchmarkDraft
|
||||
runBenchmarkScriptPreview
|
||||
} = await import('./mockBackend')
|
||||
|
||||
function wrapService<T extends object>(target: T, overrides: Record<string, unknown>): T {
|
||||
@@ -71,25 +64,6 @@ vi.mock('$lib/gen', async () => {
|
||||
|
||||
return {
|
||||
...actual,
|
||||
DraftService: wrapService(actual.DraftService, {
|
||||
updateDraft: async (data: {
|
||||
workspace: string
|
||||
kind: any
|
||||
path: string
|
||||
requestBody?: { value?: unknown }
|
||||
}) =>
|
||||
hasBenchmarkWorkspace(data.workspace)
|
||||
? updateBenchmarkDraft(data)
|
||||
: actual.DraftService.updateDraft(data),
|
||||
getDraftForUser: async (data: { workspace: string; kind: any; path: string }) =>
|
||||
hasBenchmarkWorkspace(data.workspace)
|
||||
? getBenchmarkDraftForUser(data)
|
||||
: actual.DraftService.getDraftForUser(data),
|
||||
listDrafts: async (data: { workspace: string }) =>
|
||||
hasBenchmarkWorkspace(data.workspace)
|
||||
? listBenchmarkDrafts(data.workspace)
|
||||
: actual.DraftService.listDrafts(data)
|
||||
}),
|
||||
ScriptService: wrapService(actual.ScriptService, {
|
||||
listScripts: async (data: { workspace: string }) =>
|
||||
hasBenchmarkWorkspace(data.workspace)
|
||||
@@ -225,15 +199,7 @@ vi.mock('$lib/gen', async () => {
|
||||
getCompletedJobResultMaybe: async (data: { workspace: string; id: string }) =>
|
||||
hasBenchmarkWorkspace(data.workspace)
|
||||
? getBenchmarkCompletedJobResultMaybe({ workspace: data.workspace, id: data.id })
|
||||
: actual.JobService.getCompletedJobResultMaybe(data),
|
||||
listJobs: async (data: { workspace: string }) =>
|
||||
hasBenchmarkWorkspace(data.workspace)
|
||||
? (listBenchmarkJobs(data.workspace) ?? [])
|
||||
: actual.JobService.listJobs(data),
|
||||
getJobLogs: async (data: { workspace: string; id: string }) =>
|
||||
hasBenchmarkWorkspace(data.workspace)
|
||||
? getBenchmarkJobLogs(data.workspace, data.id)
|
||||
: actual.JobService.getJobLogs(data)
|
||||
: actual.JobService.getCompletedJobResultMaybe(data)
|
||||
}),
|
||||
WorkspaceService: wrapService(actual.WorkspaceService, {
|
||||
listDataTableTables: async (data: { workspace: string }) =>
|
||||
@@ -301,20 +267,12 @@ vi.mock('$lib/gen', async () => {
|
||||
}),
|
||||
AppService: wrapService(actual.AppService, {
|
||||
existsApp: async (data: { workspace: string; path: string }) =>
|
||||
hasBenchmarkWorkspace(data.workspace)
|
||||
? Boolean(getBenchmarkAppByPath(data.workspace, data.path))
|
||||
: actual.AppService.existsApp(data),
|
||||
hasBenchmarkWorkspace(data.workspace) ? false : actual.AppService.existsApp(data),
|
||||
listApps: async (data: { workspace: string }) =>
|
||||
hasBenchmarkWorkspace(data.workspace)
|
||||
? (listBenchmarkApps(data.workspace) ?? [])
|
||||
: actual.AppService.listApps(data),
|
||||
hasBenchmarkWorkspace(data.workspace) ? [] : actual.AppService.listApps(data),
|
||||
getAppByPath: async (data: { workspace: string; path: string }) => {
|
||||
if (hasBenchmarkWorkspace(data.workspace)) {
|
||||
const app = getBenchmarkAppByPath(data.workspace, data.path)
|
||||
if (!app) {
|
||||
throw new Error(`App "${data.path}" not found in benchmark workspace`)
|
||||
}
|
||||
return app
|
||||
throw new Error(`App "${data.path}" not found in benchmark workspace`)
|
||||
}
|
||||
return actual.AppService.getAppByPath(data)
|
||||
}
|
||||
@@ -466,6 +424,5 @@ benchmarkIt(
|
||||
resetBenchmarkMockBackend()
|
||||
}
|
||||
},
|
||||
// Full-suite runs (30+ cases at concurrency 2-3) routinely exceed 10 minutes.
|
||||
7_200_000
|
||||
600_000
|
||||
)
|
||||
|
||||
+1
-401
@@ -4,7 +4,7 @@
|
||||
It should take a string `name` input and return `Hello, ${name}!`.
|
||||
Leave it as an AI draft only; do not deploy or save it.
|
||||
runtime:
|
||||
maxTurns: 10
|
||||
maxTurns: 8
|
||||
validate:
|
||||
draftCountExactly: 1
|
||||
requiredDrafts:
|
||||
@@ -821,403 +821,3 @@
|
||||
- uses the wmill.datatable() SDK to query the orders table and sum the order totals
|
||||
- returns the total revenue from the script
|
||||
- leaves the result as an AI draft and does not deploy or save it
|
||||
|
||||
- id: global-test27-list-recent-runs
|
||||
prompt: |-
|
||||
What are the most recent runs in this workspace? Give me a quick rundown of what ran and whether it succeeded.
|
||||
initial: ai_evals/fixtures/frontend/global/initial/jobs_seed.json
|
||||
runtime:
|
||||
maxTurns: 6
|
||||
validate:
|
||||
draftCountExactly: 0
|
||||
toolExpect:
|
||||
requiredToolsUsed:
|
||||
- list_runs
|
||||
forbiddenToolsUsed:
|
||||
- deploy_workspace_item
|
||||
- delete_workspace_item
|
||||
- write_script
|
||||
# Read-only job inspection produces no draft, so the global judge (which only
|
||||
# sees the drafts artifact) would score it empty — validate via tool use.
|
||||
skipJudge: true
|
||||
judgeChecklist:
|
||||
- lists the recent runs from the workspace rather than fabricating them
|
||||
- summarizes each run's path and success/failure from the returned data
|
||||
|
||||
- id: global-test28-fetch-failed-run-logs
|
||||
prompt: |-
|
||||
The run with id 01920000-0000-7000-8000-0000000000f1 failed. Pull its logs and tell me what went wrong.
|
||||
initial: ai_evals/fixtures/frontend/global/initial/jobs_seed.json
|
||||
runtime:
|
||||
maxTurns: 6
|
||||
validate:
|
||||
draftCountExactly: 0
|
||||
toolExpect:
|
||||
requiredToolsUsed:
|
||||
- get_job_logs
|
||||
forbiddenToolsUsed:
|
||||
- deploy_workspace_item
|
||||
- delete_workspace_item
|
||||
- write_script
|
||||
toolCallArgs:
|
||||
- tool: get_job_logs
|
||||
field: id
|
||||
stringIncludesAnyOf:
|
||||
- 01920000-0000-7000-8000-0000000000f1
|
||||
# Same as above — no draft is produced, so rely on the deterministic tool-use
|
||||
# and argument checks rather than the judge.
|
||||
skipJudge: true
|
||||
judgeChecklist:
|
||||
- fetches the logs for the requested job id
|
||||
- explains the failure from the returned logs (connection refused to the upstream API)
|
||||
|
||||
# --- Documentation search (search_docs) ---
|
||||
# Pure product-knowledge questions: the assistant should consult the docs via
|
||||
# search_docs and answer conversationally, not draft or mutate anything. No
|
||||
# draft is produced, so the global judge is skipped and we validate tool use.
|
||||
|
||||
- id: global-docs-ai-agent-step
|
||||
prompt: |-
|
||||
Does Windmill support a flow step where an LLM decides which of my scripts to call based on the input?
|
||||
runtime:
|
||||
maxTurns: 6
|
||||
validate:
|
||||
draftCountExactly: 0
|
||||
toolExpect:
|
||||
requiredToolsUsed:
|
||||
- search_docs
|
||||
forbiddenToolsUsed:
|
||||
- write_script
|
||||
- write_flow
|
||||
- deploy_workspace_item
|
||||
- delete_workspace_item
|
||||
skipJudge: true
|
||||
|
||||
- id: global-docs-retry-step
|
||||
prompt: |-
|
||||
How does automatic retry work for a flow step that calls a flaky API?
|
||||
runtime:
|
||||
maxTurns: 6
|
||||
validate:
|
||||
draftCountExactly: 0
|
||||
toolExpect:
|
||||
requiredToolsUsed:
|
||||
- search_docs
|
||||
forbiddenToolsUsed:
|
||||
- write_script
|
||||
- write_flow
|
||||
- deploy_workspace_item
|
||||
- delete_workspace_item
|
||||
skipJudge: true
|
||||
|
||||
- id: global-docs-key-value-store
|
||||
prompt: |-
|
||||
Can I use a Redis-style key-value store from my Windmill scripts, and how?
|
||||
runtime:
|
||||
maxTurns: 6
|
||||
validate:
|
||||
draftCountExactly: 0
|
||||
toolExpect:
|
||||
requiredToolsUsed:
|
||||
- search_docs
|
||||
forbiddenToolsUsed:
|
||||
- write_script
|
||||
- write_flow
|
||||
- deploy_workspace_item
|
||||
- delete_workspace_item
|
||||
skipJudge: true
|
||||
|
||||
- id: global-docs-cron-schedule-format
|
||||
prompt: |-
|
||||
How do Windmill's cron schedules work, and what format does the schedule expression use?
|
||||
runtime:
|
||||
maxTurns: 6
|
||||
validate:
|
||||
draftCountExactly: 0
|
||||
toolExpect:
|
||||
requiredToolsUsed:
|
||||
- search_docs
|
||||
forbiddenToolsUsed:
|
||||
- write_script
|
||||
- write_flow
|
||||
- deploy_workspace_item
|
||||
- delete_workspace_item
|
||||
skipJudge: true
|
||||
|
||||
# --- Raw app on a large project (context-usage benchmark) ---
|
||||
# These cases run against the deliberately large `analytics_dashboard` raw-app
|
||||
# fixture (~20 frontend files incl. a 5k-line data module, plus backend runnables).
|
||||
# They exist to measure how much context the global chat consumes when working in a
|
||||
# big raw app: test29 is a read-heavy debugging hunt, test30 is a small edit baseline.
|
||||
# tokenUsage is recorded per run, so the same cases re-run after a read-tool change
|
||||
# (the read_app_file cap + offset/limit paging) quantify the optimization. skipJudge:
|
||||
# the judge only sees the drafts artifact and cannot run the app, so we validate
|
||||
# deterministically.
|
||||
|
||||
- id: global-test29-raw-app-debug-large
|
||||
prompt: |-
|
||||
The analytics dashboard app at `f/evals/global/analytics_dashboard` has a bug:
|
||||
the Revenue Summary tile shows a total that is lower than the per-order line
|
||||
totals and the per-region breakdown. Track down what is computing revenue
|
||||
incorrectly and fix it. Keep the change as an AI draft only; do not deploy or
|
||||
save it.
|
||||
initial: ai_evals/fixtures/frontend/global/initial/analytics_dashboard
|
||||
runtime:
|
||||
maxTurns: 20
|
||||
validate:
|
||||
draftCountExactly: 1
|
||||
requiredDrafts:
|
||||
- type: app
|
||||
path: f/evals/global/analytics_dashboard
|
||||
valueIncludes:
|
||||
- "return order.unitPrice * order.quantity"
|
||||
toolExpect:
|
||||
requiredToolsAnyOf:
|
||||
# Inspecting the app's files is satisfied by either reading them directly
|
||||
# or grepping for the revenue calculation.
|
||||
- [read_app_file, search_app]
|
||||
forbiddenToolsUsed:
|
||||
- deploy_workspace_item
|
||||
- delete_workspace_item
|
||||
skipJudge: true
|
||||
judgeChecklist:
|
||||
- inspects the dashboard app's files to locate the revenue calculation
|
||||
- fixes the per-order revenue so it multiplies unit price by quantity
|
||||
- leaves the result as an AI draft and does not deploy or save it
|
||||
|
||||
- id: global-test30-raw-app-small-edit-large
|
||||
prompt: |-
|
||||
In the dashboard app at `f/evals/global/analytics_dashboard`, change the main
|
||||
page heading from "Operations Console" to "Revenue Overview". Leave everything
|
||||
else unchanged. Keep it as an AI draft only; do not deploy or save it.
|
||||
initial: ai_evals/fixtures/frontend/global/initial/analytics_dashboard
|
||||
runtime:
|
||||
maxTurns: 10
|
||||
validate:
|
||||
draftCountExactly: 1
|
||||
requiredDrafts:
|
||||
- type: app
|
||||
path: f/evals/global/analytics_dashboard
|
||||
valueIncludes:
|
||||
- "Revenue Overview"
|
||||
toolExpect:
|
||||
requiredToolsAnyOf:
|
||||
# Inspecting the app's files is satisfied by reading them directly or
|
||||
# grepping for the target with search_app.
|
||||
- [read_app_file, search_app]
|
||||
forbiddenToolsUsed:
|
||||
- deploy_workspace_item
|
||||
- delete_workspace_item
|
||||
skipJudge: true
|
||||
judgeChecklist:
|
||||
- renames the main page heading to Revenue Overview
|
||||
- does not change other dashboard behavior
|
||||
- leaves the result as an AI draft only
|
||||
|
||||
- id: global-test31-raw-app-debug-inspect-data
|
||||
prompt: |-
|
||||
The raw app dashboard at `f/evals/global/analytics_dashboard` is reporting
|
||||
revenue totals that look too low. Inspect the app's files — both the sample
|
||||
order data module and the revenue calculation — to work out whether the bug is
|
||||
in the data or in the calculation, then fix the actual cause. Keep the change as
|
||||
an AI draft only; do not deploy or save it.
|
||||
initial: ai_evals/fixtures/frontend/global/initial/analytics_dashboard
|
||||
runtime:
|
||||
maxTurns: 22
|
||||
validate:
|
||||
draftCountExactly: 1
|
||||
requiredDrafts:
|
||||
- type: app
|
||||
path: f/evals/global/analytics_dashboard
|
||||
valueIncludes:
|
||||
- "return order.unitPrice * order.quantity"
|
||||
toolExpect:
|
||||
requiredToolsAnyOf:
|
||||
# Inspecting the app's files is satisfied by reading them directly or
|
||||
# grepping for the target with search_app.
|
||||
- [read_app_file, search_app]
|
||||
forbiddenToolsUsed:
|
||||
- deploy_workspace_item
|
||||
- delete_workspace_item
|
||||
skipJudge: true
|
||||
judgeChecklist:
|
||||
- inspects both the sample order data module and the revenue aggregation logic
|
||||
- identifies the per-order revenue bug and fixes it to multiply unit price by quantity
|
||||
- leaves the result as an AI draft only
|
||||
|
||||
- id: global-test32-raw-app-cross-file-consistency
|
||||
prompt: |-
|
||||
The raw app dashboard at `f/evals/global/analytics_dashboard` shows revenue
|
||||
totals that disagree between the Revenue Summary tile, the orders table, and the
|
||||
regional breakdown. Investigate how each of those computes revenue, work out
|
||||
which calculation is wrong, and fix it. Keep the change as an AI draft only; do
|
||||
not deploy or save it.
|
||||
# Cross-file investigation: forces the model through several overlapping files
|
||||
# (the summary's aggregation helper, the orders table, the regional breakdown) —
|
||||
# a realistic multi-file read load that exercises the read_app_file cap.
|
||||
initial: ai_evals/fixtures/frontend/global/initial/analytics_dashboard
|
||||
runtime:
|
||||
maxTurns: 24
|
||||
validate:
|
||||
draftCountExactly: 1
|
||||
requiredDrafts:
|
||||
- type: app
|
||||
path: f/evals/global/analytics_dashboard
|
||||
valueIncludes:
|
||||
- "return order.unitPrice * order.quantity"
|
||||
toolExpect:
|
||||
requiredToolsAnyOf:
|
||||
# Inspecting the app's files is satisfied by reading them directly or
|
||||
# grepping for the target with search_app.
|
||||
- [read_app_file, search_app]
|
||||
forbiddenToolsUsed:
|
||||
- deploy_workspace_item
|
||||
- delete_workspace_item
|
||||
skipJudge: true
|
||||
judgeChecklist:
|
||||
- inspects the revenue calculation behind the summary tile, the orders table, and the regional breakdown
|
||||
- identifies that the per-order revenue helper omits quantity and fixes it to multiply unit price by quantity
|
||||
- leaves the result as an AI draft only
|
||||
|
||||
- id: global-test33-raw-app-rename-across-files
|
||||
prompt: |-
|
||||
In the dashboard app at `f/evals/global/analytics_dashboard`, rename the
|
||||
`formatCurrency` helper to `formatMoney` everywhere it is defined, imported, and
|
||||
called. Leave the separate `formatCurrencyPrecise` helper exactly as it is. Keep
|
||||
the change as an AI draft only; do not deploy or save it.
|
||||
# Find-all-usages rename: formatCurrency is defined once and called in 6 places
|
||||
# spread across 4 component files (and imported in 4). Locating every usage is the
|
||||
# exact task search_app is meant to make cheap — one grep returns all file:line
|
||||
# rows instead of reading each component whole. valueExcludes "formatCurrency("
|
||||
# asserts the definition and all call sites were renamed while tolerating the
|
||||
# preserved formatCurrencyPrecise (which is never followed by "(").
|
||||
initial: ai_evals/fixtures/frontend/global/initial/analytics_dashboard
|
||||
runtime:
|
||||
maxTurns: 22
|
||||
validate:
|
||||
draftCountExactly: 1
|
||||
requiredDrafts:
|
||||
- type: app
|
||||
path: f/evals/global/analytics_dashboard
|
||||
valueIncludes:
|
||||
- "export function formatMoney"
|
||||
- "formatMoney("
|
||||
valueExcludes:
|
||||
- "formatCurrency("
|
||||
toolExpect:
|
||||
forbiddenToolsUsed:
|
||||
- deploy_workspace_item
|
||||
- delete_workspace_item
|
||||
skipJudge: true
|
||||
judgeChecklist:
|
||||
- renames the formatCurrency definition, imports, and all call sites to formatMoney
|
||||
- leaves the unrelated formatCurrencyPrecise helper unchanged
|
||||
- leaves the result as an AI draft only
|
||||
|
||||
# --- Path selection (u/<user> vs f/<folder>) ---
|
||||
# These cases assert how the assistant picks a workspace path when the user gives
|
||||
# none: a bare name defaults to the personal scope `u/<user>/`, an existing folder
|
||||
# whose purpose matches is used, a non-admin targets a writable folder and never a
|
||||
# read-only one, and shared intent with no matching folder asks rather than invents.
|
||||
# Each depends on the seeded `user` fixture (username / is_admin / folders /
|
||||
# folders_read) so the prompt's folder guidance and `u/{username}` are well-formed.
|
||||
|
||||
- id: global-path1-bare-name-defaults-to-personal
|
||||
prompt: |-
|
||||
Stage a quick draft helper that takes a string and returns it trimmed of
|
||||
leading and trailing whitespace. Just keep it as a draft.
|
||||
initial: ai_evals/fixtures/frontend/global/initial/user_admin_empty.json
|
||||
runtime:
|
||||
maxTurns: 8
|
||||
validate:
|
||||
draftCountExactly: 1
|
||||
requiredDrafts:
|
||||
- type: script
|
||||
pathStartsWith: u/admin/
|
||||
toolExpect:
|
||||
requiredToolsUsed:
|
||||
- write_script
|
||||
forbiddenToolsUsed:
|
||||
- deploy_workspace_item
|
||||
- delete_workspace_item
|
||||
skipJudge: true
|
||||
judgeChecklist:
|
||||
- stages a single script draft for a trim helper
|
||||
- defaults the path to the current user's personal scope (u/admin/...) since no path or folder was given
|
||||
- does not invent an f/<folder> path
|
||||
- leaves the result as a draft only
|
||||
|
||||
- id: global-path2-match-existing-folder
|
||||
prompt: |-
|
||||
Draft a flow for the marketing team's weekly campaign report.
|
||||
It should take a week number and return a short summary string.
|
||||
Keep it as a draft only.
|
||||
initial: ai_evals/fixtures/frontend/global/initial/user_admin_folders.json
|
||||
runtime:
|
||||
maxTurns: 10
|
||||
validate:
|
||||
draftCountExactly: 1
|
||||
requiredDrafts:
|
||||
- type: flow
|
||||
pathStartsWith: f/marketing/
|
||||
toolExpect:
|
||||
requiredToolsUsed:
|
||||
- write_flow
|
||||
forbiddenToolsUsed:
|
||||
- deploy_workspace_item
|
||||
- delete_workspace_item
|
||||
skipJudge: true
|
||||
judgeChecklist:
|
||||
- drafts a flow for the marketing campaign report
|
||||
- places it in the existing marketing folder (f/marketing/...) rather than the personal scope or an invented folder
|
||||
- leaves the result as a draft only
|
||||
|
||||
- id: global-path3-shared-intent-unknown-folder-asks
|
||||
prompt: |-
|
||||
Put together a draft onboarding checklist flow for the People Ops team to use
|
||||
when a new hire joins. Keep it as a draft.
|
||||
initial: ai_evals/fixtures/frontend/global/initial/user_admin_folders.json
|
||||
runtime:
|
||||
maxTurns: 6
|
||||
validate:
|
||||
draftCountExactly: 0
|
||||
toolExpect:
|
||||
requiredToolsUsed:
|
||||
- askUserQuestion
|
||||
forbiddenToolsUsed:
|
||||
- write_flow
|
||||
- write_script
|
||||
- deploy_workspace_item
|
||||
- delete_workspace_item
|
||||
skipJudge: true
|
||||
judgeChecklist:
|
||||
- recognizes the request implies shared/team work but names no existing folder (none of marketing/data_engineering/shared_utils fit People Ops)
|
||||
- asks which folder to use instead of guessing or inventing one
|
||||
- does not create a draft until the folder is known
|
||||
|
||||
- id: global-path4-nonadmin-avoids-readonly-folder
|
||||
prompt: |-
|
||||
Draft a small flow that returns today's date as an ISO string, and stage it in
|
||||
one of our shared team folders. Keep it as a draft.
|
||||
initial: ai_evals/fixtures/frontend/global/initial/user_bob_nonadmin_teams.json
|
||||
runtime:
|
||||
maxTurns: 10
|
||||
validate:
|
||||
draftCountExactly: 1
|
||||
requiredDrafts:
|
||||
- type: flow
|
||||
pathStartsWith: f/team_a/
|
||||
forbiddenDrafts:
|
||||
- type: flow
|
||||
pathStartsWith: f/team_b/
|
||||
toolExpect:
|
||||
requiredToolsUsed:
|
||||
- write_flow
|
||||
forbiddenToolsUsed:
|
||||
- deploy_workspace_item
|
||||
- delete_workspace_item
|
||||
skipJudge: true
|
||||
judgeChecklist:
|
||||
- drafts a flow that returns the current date as an ISO string
|
||||
- places it in team_a (writable by this non-admin user) and not team_b (read-only)
|
||||
- leaves the result as a draft only
|
||||
|
||||
+3
-25
@@ -25,9 +25,7 @@ import {
|
||||
import { runSuite } from "../core/runSuite";
|
||||
import { EVAL_MODES, type EvalMode } from "../core/types";
|
||||
import { DEFAULT_JUDGE_MODEL } from "../core/judge";
|
||||
// createCliModeRunner is imported lazily in runCliBenchmark so the non-cli modes
|
||||
// (global/flow/script/app) don't pull in the wmill CLI toolchain and its JSR deps
|
||||
// (e.g. @cliffy/*) just to load this entrypoint.
|
||||
import { createCliModeRunner } from "../modes/cli";
|
||||
import { runFrontendBenchmarkAdapter } from "../adapters/frontend/runtime";
|
||||
import { resolveWindmillBackendSettings } from "../core/windmillBackendSettings";
|
||||
import { assertWindmillBackendReachable } from "../adapters/frontend/windmillBackend";
|
||||
@@ -99,11 +97,6 @@ async function main() {
|
||||
"comma-separated model aliases to run sequentially",
|
||||
)
|
||||
.option("--verbose", "stream assistant output during frontend runs")
|
||||
.option("--skip-judge", "skip LLM judge scoring for this run")
|
||||
.option(
|
||||
"--execution-only",
|
||||
"only require the model/proxy/frontend loop to complete",
|
||||
)
|
||||
.option(
|
||||
"--record",
|
||||
"append a compact summary line to ai_evals/history/<mode>.jsonl",
|
||||
@@ -122,8 +115,6 @@ async function main() {
|
||||
model?: string;
|
||||
models?: string;
|
||||
verbose?: boolean;
|
||||
skipJudge?: boolean;
|
||||
executionOnly?: boolean;
|
||||
record?: boolean;
|
||||
backendValidation?: string;
|
||||
},
|
||||
@@ -136,8 +127,6 @@ async function main() {
|
||||
model: options.model,
|
||||
models: options.models,
|
||||
verbose: options.verbose ?? false,
|
||||
skipJudge: options.skipJudge ?? false,
|
||||
executionOnly: options.executionOnly ?? false,
|
||||
record: options.record ?? false,
|
||||
backendValidation: options.backendValidation,
|
||||
});
|
||||
@@ -186,8 +175,6 @@ async function handleRun(input: {
|
||||
model?: string;
|
||||
models?: string;
|
||||
verbose: boolean;
|
||||
skipJudge: boolean;
|
||||
executionOnly: boolean;
|
||||
record: boolean;
|
||||
backendValidation?: string;
|
||||
}) {
|
||||
@@ -243,8 +230,6 @@ async function handleRun(input: {
|
||||
input.runs,
|
||||
getCliEvalModel(model),
|
||||
runModel,
|
||||
input.skipJudge,
|
||||
input.executionOnly,
|
||||
)
|
||||
: await runFrontendBenchmarkAdapter({
|
||||
mode: input.mode,
|
||||
@@ -252,8 +237,6 @@ async function handleRun(input: {
|
||||
runs: input.runs,
|
||||
model: model.id,
|
||||
verbose: input.verbose,
|
||||
skipJudge: input.skipJudge,
|
||||
executionOnly: input.executionOnly,
|
||||
backendValidation,
|
||||
});
|
||||
|
||||
@@ -295,25 +278,20 @@ async function runCliBenchmark(
|
||||
runs: number,
|
||||
model: ReturnType<typeof getCliEvalModel>,
|
||||
runModel: string,
|
||||
skipJudge: boolean,
|
||||
executionOnly: boolean,
|
||||
) {
|
||||
const { createCliModeRunner } = await import("../modes/cli");
|
||||
const judgeModel = skipJudge || executionOnly ? null : DEFAULT_JUDGE_MODEL;
|
||||
const caseResults = await runSuite({
|
||||
modeRunner: createCliModeRunner(model),
|
||||
cases,
|
||||
runs,
|
||||
runModel,
|
||||
judgeModel,
|
||||
executionOnly,
|
||||
judgeModel: DEFAULT_JUDGE_MODEL,
|
||||
});
|
||||
|
||||
return buildRunResult({
|
||||
mode: "cli",
|
||||
runs,
|
||||
runModel,
|
||||
judgeModel,
|
||||
judgeModel: DEFAULT_JUDGE_MODEL,
|
||||
caseResults,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -246,21 +246,6 @@ describe("loadCases", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("loads global docs-search cases as tool-use checks", async () => {
|
||||
const globalCases = await loadCases("global");
|
||||
const docsCases = globalCases.filter((entry) =>
|
||||
entry.id.startsWith("global-docs-"),
|
||||
);
|
||||
expect(docsCases.length).toBeGreaterThanOrEqual(3);
|
||||
|
||||
// Each docs case verifies the assistant reaches for search_docs and does not
|
||||
// draft anything; with no draft, the global judge is skipped.
|
||||
for (const entry of docsCases) {
|
||||
expect(entry.skipJudge).toBe(true);
|
||||
expect(entry.toolExpect?.requiredToolsUsed).toContain("search_docs");
|
||||
}
|
||||
});
|
||||
|
||||
it("loads tool expectations for workspace mutation cases", async () => {
|
||||
const scriptCases = await loadCases("script");
|
||||
const caseEntry = scriptCases.find(
|
||||
|
||||
@@ -92,71 +92,6 @@ describe("benchmark results", () => {
|
||||
expect(summary).toContain("Average duration (all attempts): 550ms");
|
||||
});
|
||||
|
||||
it("aggregates final context size over passed attempts only", () => {
|
||||
const result = buildRunResult({
|
||||
mode: "global",
|
||||
runs: 1,
|
||||
runModel: "model-under-test",
|
||||
judgeModel: "judge-model",
|
||||
caseResults: [
|
||||
caseResult([
|
||||
{
|
||||
attempt: 1,
|
||||
passed: true,
|
||||
durationMs: 1000,
|
||||
assistantMessageCount: 1,
|
||||
toolCallCount: 1,
|
||||
toolsUsed: ["edit_script"],
|
||||
skillsInvoked: [],
|
||||
checks: [{ name: "edited", passed: true }],
|
||||
judgeScore: 100,
|
||||
judgeSummary: "ok",
|
||||
error: null,
|
||||
tokenUsage: { prompt: 12000, completion: 200, total: 12200 },
|
||||
finalContextTokens: 5000,
|
||||
},
|
||||
{
|
||||
attempt: 2,
|
||||
passed: true,
|
||||
durationMs: 1100,
|
||||
assistantMessageCount: 1,
|
||||
toolCallCount: 1,
|
||||
toolsUsed: ["edit_script"],
|
||||
skillsInvoked: [],
|
||||
checks: [{ name: "edited", passed: true }],
|
||||
judgeScore: 100,
|
||||
judgeSummary: "ok",
|
||||
error: null,
|
||||
tokenUsage: { prompt: 18000, completion: 300, total: 18300 },
|
||||
finalContextTokens: 7000,
|
||||
},
|
||||
{
|
||||
attempt: 3,
|
||||
passed: false,
|
||||
durationMs: 100,
|
||||
assistantMessageCount: 1,
|
||||
toolCallCount: 0,
|
||||
toolsUsed: [],
|
||||
skillsInvoked: [],
|
||||
checks: [{ name: "edited", passed: false }],
|
||||
judgeScore: 10,
|
||||
judgeSummary: "missed",
|
||||
error: "failed",
|
||||
tokenUsage: { prompt: 20000, completion: 100, total: 20100 },
|
||||
finalContextTokens: 9000,
|
||||
},
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
// Final context size stays below cumulative prompt and ignores the failed attempt.
|
||||
expect(result.averageFinalContextTokensPassed).toBe(6000);
|
||||
expect(result.maxFinalContextTokensPassed).toBe(7000);
|
||||
expect(formatRunSummary(result)).toContain(
|
||||
"Final context size (passed): 6000 tokens (max 7000)",
|
||||
);
|
||||
});
|
||||
|
||||
it("reports passed averages as unavailable when no attempt passes", () => {
|
||||
const result = buildRunResult({
|
||||
mode: "global",
|
||||
|
||||
@@ -16,9 +16,6 @@ type AttemptAggregate = {
|
||||
durationTotal: number;
|
||||
tokenUsageAttemptCount: number;
|
||||
tokenUsageTotal: BenchmarkTokenUsage | null;
|
||||
finalContextAttemptCount: number;
|
||||
finalContextTotal: number;
|
||||
finalContextMax: number | null;
|
||||
};
|
||||
|
||||
export async function writeRunResult(
|
||||
@@ -120,8 +117,6 @@ export function buildRunResult(input: {
|
||||
passedAttemptAggregate,
|
||||
passedAttempts,
|
||||
),
|
||||
averageFinalContextTokensPassed: averageFinalContext(passedAttemptAggregate),
|
||||
maxFinalContextTokensPassed: passedAttemptAggregate.finalContextMax,
|
||||
cases: input.caseResults,
|
||||
};
|
||||
}
|
||||
@@ -138,11 +133,6 @@ export function formatRunSummary(result: BenchmarkRunResult): string {
|
||||
`Average tokens (passed): ${formatTokenUsage(result.averageTokenUsagePerPassedAttempt)}`,
|
||||
);
|
||||
}
|
||||
if (result.averageFinalContextTokensPassed != null) {
|
||||
lines.push(
|
||||
`Final context size (passed): ${Math.round(result.averageFinalContextTokensPassed)} tokens (max ${Math.round(result.maxFinalContextTokensPassed ?? 0)})`,
|
||||
);
|
||||
}
|
||||
if (result.passedAttempts < result.attemptCount) {
|
||||
lines.push(
|
||||
`Average duration (all attempts): ${Math.round(result.averageDurationMs)}ms`,
|
||||
@@ -191,21 +181,10 @@ function aggregateAttempts(attempts: BenchmarkAttemptResult[]): AttemptAggregate
|
||||
durationTotal: 0,
|
||||
tokenUsageAttemptCount: 0,
|
||||
tokenUsageTotal: null,
|
||||
finalContextAttemptCount: 0,
|
||||
finalContextTotal: 0,
|
||||
finalContextMax: null,
|
||||
};
|
||||
|
||||
for (const attempt of attempts) {
|
||||
aggregate.durationTotal += attempt.durationMs;
|
||||
if (typeof attempt.finalContextTokens === "number") {
|
||||
aggregate.finalContextAttemptCount += 1;
|
||||
aggregate.finalContextTotal += attempt.finalContextTokens;
|
||||
aggregate.finalContextMax = Math.max(
|
||||
aggregate.finalContextMax ?? 0,
|
||||
attempt.finalContextTokens,
|
||||
);
|
||||
}
|
||||
if (!attempt.tokenUsage) {
|
||||
continue;
|
||||
}
|
||||
@@ -225,12 +204,6 @@ function averageDuration(aggregate: AttemptAggregate): number | null {
|
||||
: aggregate.durationTotal / aggregate.attemptCount;
|
||||
}
|
||||
|
||||
function averageFinalContext(aggregate: AttemptAggregate): number | null {
|
||||
return aggregate.finalContextAttemptCount === 0
|
||||
? null
|
||||
: aggregate.finalContextTotal / aggregate.finalContextAttemptCount;
|
||||
}
|
||||
|
||||
function averageTokenUsage(
|
||||
aggregate: AttemptAggregate,
|
||||
denominator: number,
|
||||
@@ -345,9 +318,6 @@ function toHistoryRecord(result: BenchmarkRunResult) {
|
||||
averageTokenUsagePerAttempt: result.averageTokenUsagePerAttempt ?? null,
|
||||
averageTokenUsagePerPassedAttempt:
|
||||
result.averageTokenUsagePerPassedAttempt ?? null,
|
||||
averageFinalContextTokensPassed:
|
||||
result.averageFinalContextTokensPassed ?? null,
|
||||
maxFinalContextTokensPassed: result.maxFinalContextTokensPassed ?? null,
|
||||
failedCaseIds: Array.from(
|
||||
new Set(
|
||||
result.cases
|
||||
@@ -391,10 +361,6 @@ function toHistoryRecord(result: BenchmarkRunResult) {
|
||||
passedAttemptAggregate,
|
||||
passedAttempts,
|
||||
),
|
||||
averageFinalContextTokensPassed: averageFinalContext(
|
||||
passedAttemptAggregate,
|
||||
),
|
||||
maxFinalContextTokensPassed: passedAttemptAggregate.finalContextMax,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { runSuite } from "./runSuite";
|
||||
import type { ModeRunner } from "./types";
|
||||
|
||||
const modeRunner: ModeRunner<undefined, undefined, { ok: boolean }> = {
|
||||
mode: "global",
|
||||
concurrency: 1,
|
||||
loadInitial: async () => undefined,
|
||||
loadExpected: async () => undefined,
|
||||
run: async () => ({
|
||||
success: true,
|
||||
actual: { ok: true },
|
||||
assistantMessageCount: 1,
|
||||
toolCallCount: 0,
|
||||
toolsUsed: [],
|
||||
skillsInvoked: [],
|
||||
tokenUsage: null,
|
||||
}),
|
||||
validate: () => [],
|
||||
};
|
||||
|
||||
describe("runSuite", () => {
|
||||
it("skips judge checks when the run disables judge scoring", async () => {
|
||||
const [caseResult] = await runSuite({
|
||||
modeRunner,
|
||||
cases: [
|
||||
{
|
||||
id: "case-1",
|
||||
prompt: "Create a draft script",
|
||||
judgeChecklist: ["the output satisfies the prompt"],
|
||||
},
|
||||
],
|
||||
runs: 1,
|
||||
runModel: "model-under-test",
|
||||
judgeModel: null,
|
||||
});
|
||||
|
||||
const [attempt] = caseResult.attempts;
|
||||
expect(attempt.passed).toBe(true);
|
||||
expect(attempt.judgeScore).toBeNull();
|
||||
expect(attempt.judgeSummary).toBeNull();
|
||||
expect(attempt.checks.map((check) => check.name)).toEqual([
|
||||
"run succeeded",
|
||||
]);
|
||||
});
|
||||
|
||||
it("only requires run success when execution-only is enabled", async () => {
|
||||
let loadExpectedCalls = 0;
|
||||
let validateCalls = 0;
|
||||
let backendValidateCalls = 0;
|
||||
|
||||
const executionOnlyRunner: ModeRunner<
|
||||
undefined,
|
||||
undefined,
|
||||
{ ok: boolean }
|
||||
> = {
|
||||
...modeRunner,
|
||||
loadExpected: async () => {
|
||||
loadExpectedCalls++;
|
||||
return undefined;
|
||||
},
|
||||
validate: () => {
|
||||
validateCalls++;
|
||||
return [{ name: "validator failed", passed: false }];
|
||||
},
|
||||
backendValidate: async () => {
|
||||
backendValidateCalls++;
|
||||
return {
|
||||
checks: [{ name: "backend validation failed", passed: false }],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const [caseResult] = await runSuite({
|
||||
modeRunner: executionOnlyRunner,
|
||||
cases: [
|
||||
{
|
||||
id: "case-1",
|
||||
prompt: "Create a draft script",
|
||||
expectedPath: "fixtures/expected.json",
|
||||
toolExpect: { requiredToolsUsed: ["write_script"] },
|
||||
judgeChecklist: ["the output satisfies the prompt"],
|
||||
},
|
||||
],
|
||||
runs: 1,
|
||||
runModel: "model-under-test",
|
||||
judgeModel: "judge-model",
|
||||
executionOnly: true,
|
||||
});
|
||||
|
||||
const [attempt] = caseResult.attempts;
|
||||
expect(attempt.passed).toBe(true);
|
||||
expect(attempt.judgeScore).toBeNull();
|
||||
expect(attempt.judgeSummary).toBeNull();
|
||||
expect(attempt.checks.map((check) => check.name)).toEqual([
|
||||
"run succeeded",
|
||||
]);
|
||||
expect(loadExpectedCalls).toBe(0);
|
||||
expect(validateCalls).toBe(0);
|
||||
expect(backendValidateCalls).toBe(0);
|
||||
});
|
||||
});
|
||||
+18
-41
@@ -15,13 +15,11 @@ export async function runSuite<TInitial, TExpected, TActual>(input: {
|
||||
runs: number;
|
||||
runModel: string | null;
|
||||
judgeModel?: string | null;
|
||||
executionOnly?: boolean;
|
||||
concurrency?: number;
|
||||
verbose?: boolean;
|
||||
onProgress?: (event: FrontendBenchmarkProgressEvent) => void;
|
||||
}): Promise<BenchmarkCaseResult[]> {
|
||||
const judgeModel =
|
||||
input.judgeModel === undefined ? DEFAULT_JUDGE_MODEL : input.judgeModel;
|
||||
const judgeModel = input.judgeModel ?? DEFAULT_JUDGE_MODEL;
|
||||
const concurrency = Math.max(1, input.concurrency ?? input.modeRunner.concurrency);
|
||||
const results = new Array<BenchmarkCaseResult>(input.cases.length);
|
||||
let cursor = 0;
|
||||
@@ -54,7 +52,6 @@ export async function runSuite<TInitial, TExpected, TActual>(input: {
|
||||
runs: input.runs,
|
||||
judgeModel,
|
||||
judgeThreshold: input.modeRunner.judgeThreshold ?? 80,
|
||||
executionOnly: input.executionOnly ?? false,
|
||||
modeRunner: input.modeRunner,
|
||||
totalCases: input.cases.length,
|
||||
verbose: input.verbose ?? false,
|
||||
@@ -75,9 +72,8 @@ async function runCaseAttempts<TInitial, TExpected, TActual>(input: {
|
||||
caseIndex: number;
|
||||
evalCase: EvalCase;
|
||||
runs: number;
|
||||
judgeModel: string | null;
|
||||
judgeModel: string;
|
||||
judgeThreshold: number;
|
||||
executionOnly: boolean;
|
||||
modeRunner: ModeRunner<TInitial, TExpected, TActual>;
|
||||
totalCases: number;
|
||||
verbose: boolean;
|
||||
@@ -103,9 +99,7 @@ async function runCaseAttempts<TInitial, TExpected, TActual>(input: {
|
||||
|
||||
try {
|
||||
const initial = await input.modeRunner.loadInitial(input.evalCase.initialPath);
|
||||
const expected = input.executionOnly
|
||||
? undefined
|
||||
: await input.modeRunner.loadExpected(input.evalCase.expectedPath);
|
||||
const expected = await input.modeRunner.loadExpected(input.evalCase.expectedPath);
|
||||
const run = await input.modeRunner.run(input.evalCase.prompt, initial, {
|
||||
evalCase: input.evalCase,
|
||||
caseId: input.evalCase.id,
|
||||
@@ -168,30 +162,22 @@ async function runCaseAttempts<TInitial, TExpected, TActual>(input: {
|
||||
});
|
||||
const checks: BenchmarkCheck[] = [
|
||||
buildCheck("run succeeded", run.success, run.error),
|
||||
...input.modeRunner.validate({
|
||||
evalCase: input.evalCase,
|
||||
prompt: input.evalCase.prompt,
|
||||
initial,
|
||||
expected,
|
||||
actual: run.actual,
|
||||
run,
|
||||
}),
|
||||
...validateToolExpectations({
|
||||
run,
|
||||
toolExpect: input.evalCase.toolExpect,
|
||||
}),
|
||||
];
|
||||
if (!input.executionOnly) {
|
||||
checks.push(
|
||||
...input.modeRunner.validate({
|
||||
evalCase: input.evalCase,
|
||||
prompt: input.evalCase.prompt,
|
||||
initial,
|
||||
expected,
|
||||
actual: run.actual,
|
||||
run,
|
||||
}),
|
||||
...validateToolExpectations({
|
||||
run,
|
||||
toolExpect: input.evalCase.toolExpect,
|
||||
})
|
||||
);
|
||||
}
|
||||
const artifactFiles = input.modeRunner.buildArtifacts?.(run.actual) ?? [];
|
||||
|
||||
if (
|
||||
run.success &&
|
||||
!input.executionOnly &&
|
||||
input.modeRunner.backendValidate
|
||||
) {
|
||||
if (run.success && input.modeRunner.backendValidate) {
|
||||
try {
|
||||
const backendValidation = await input.modeRunner.backendValidate({
|
||||
evalCase: input.evalCase,
|
||||
@@ -232,21 +218,14 @@ async function runCaseAttempts<TInitial, TExpected, TActual>(input: {
|
||||
let judgeScore: number | null = null;
|
||||
let judgeSummary: string | null = null;
|
||||
|
||||
if (
|
||||
run.success &&
|
||||
!input.executionOnly &&
|
||||
input.judgeModel !== null &&
|
||||
!input.evalCase.skipJudge
|
||||
) {
|
||||
if (run.success && !input.evalCase.skipJudge) {
|
||||
const judge = await judgeOutput({
|
||||
mode: input.modeRunner.mode,
|
||||
prompt: input.evalCase.prompt,
|
||||
checklist: input.evalCase.judgeChecklist,
|
||||
initial,
|
||||
expected: input.modeRunner.mode === "cli" ? undefined : expected,
|
||||
actual: input.modeRunner.prepareJudgeActual
|
||||
? input.modeRunner.prepareJudgeActual(run.actual)
|
||||
: run.actual,
|
||||
actual: run.actual,
|
||||
model: input.judgeModel,
|
||||
});
|
||||
|
||||
@@ -276,7 +255,6 @@ async function runCaseAttempts<TInitial, TExpected, TActual>(input: {
|
||||
judgeSummary,
|
||||
error: run.error ?? null,
|
||||
tokenUsage: run.tokenUsage ?? null,
|
||||
finalContextTokens: run.finalContextTokens ?? null,
|
||||
artifactsPath: null,
|
||||
artifactFiles,
|
||||
};
|
||||
@@ -313,7 +291,6 @@ async function runCaseAttempts<TInitial, TExpected, TActual>(input: {
|
||||
judgeSummary: null,
|
||||
error: message,
|
||||
tokenUsage: null,
|
||||
finalContextTokens: null,
|
||||
};
|
||||
if (surface) {
|
||||
input.onProgress?.({
|
||||
|
||||
+1
-26
@@ -168,21 +168,11 @@ export interface ToolCallArgumentRule {
|
||||
|
||||
export interface ToolValidationSpec {
|
||||
requiredToolsUsed?: string[];
|
||||
/**
|
||||
* Each inner array is an alternatives group: the check passes when at least
|
||||
* one tool in the group was used. Use when several tools satisfy the same
|
||||
* intent so a model that picks any valid path passes — e.g. inspecting an
|
||||
* app's files via either `read_app_file` or `search_app`.
|
||||
*/
|
||||
requiredToolsAnyOf?: string[][];
|
||||
forbiddenToolsUsed?: string[];
|
||||
toolCallArgs?: ToolCallArgumentRule[];
|
||||
}
|
||||
|
||||
export type EvalValidationSpec =
|
||||
| FlowValidationSpec
|
||||
| AppValidationSpec
|
||||
| GlobalValidationSpec;
|
||||
export type EvalValidationSpec = FlowValidationSpec | AppValidationSpec | GlobalValidationSpec;
|
||||
|
||||
export interface EvalCase {
|
||||
id: string;
|
||||
@@ -259,12 +249,6 @@ export interface ModeRunOutput<TActual> {
|
||||
toolCallDetails?: ToolCallDetail[];
|
||||
skillsInvoked: string[];
|
||||
tokenUsage?: BenchmarkTokenUsage | null;
|
||||
/**
|
||||
* Total input tokens occupying the context window on the LAST model request
|
||||
* of the agentic loop (input + cache-creation + cache-read). Complements the
|
||||
* cumulative `tokenUsage.prompt`, which sums every iteration's input.
|
||||
*/
|
||||
finalContextTokens?: number | null;
|
||||
}
|
||||
|
||||
export interface ModeRunContext {
|
||||
@@ -310,12 +294,6 @@ export interface ModeRunner<TInitial, TExpected, TActual> {
|
||||
context: ModeRunContext;
|
||||
}): Promise<BackendValidationResult | null>;
|
||||
buildArtifacts?(actual: TActual): BenchmarkArtifactFile[];
|
||||
/**
|
||||
* Optional transform applied to `actual` before it is handed to the LLM judge.
|
||||
* Use it to strip fields the judge must stay blind to (e.g. which docs-tool
|
||||
* arm produced an answer). When omitted, the judge receives `actual` as-is.
|
||||
*/
|
||||
prepareJudgeActual?(actual: TActual): unknown;
|
||||
}
|
||||
|
||||
export interface BenchmarkAttemptResult {
|
||||
@@ -332,7 +310,6 @@ export interface BenchmarkAttemptResult {
|
||||
judgeSummary: string | null;
|
||||
error: string | null;
|
||||
tokenUsage?: BenchmarkTokenUsage | null;
|
||||
finalContextTokens?: number | null;
|
||||
artifactsPath?: string | null;
|
||||
artifactFiles?: BenchmarkArtifactFile[];
|
||||
}
|
||||
@@ -363,8 +340,6 @@ export interface BenchmarkRunResult {
|
||||
totalPassedTokenUsage?: BenchmarkTokenUsage | null;
|
||||
averageTokenUsagePerAttempt?: BenchmarkTokenUsage | null;
|
||||
averageTokenUsagePerPassedAttempt?: BenchmarkTokenUsage | null;
|
||||
averageFinalContextTokensPassed?: number | null;
|
||||
maxFinalContextTokensPassed?: number | null;
|
||||
artifactsPath?: string | null;
|
||||
cases: BenchmarkCaseResult[];
|
||||
}
|
||||
|
||||
@@ -245,49 +245,6 @@ describe("validateToolExpectations", () => {
|
||||
'accepted substrings: insert into, update; values: "DROP TABLE orders"',
|
||||
});
|
||||
});
|
||||
|
||||
it("passes requiredToolsAnyOf when any alternative in the group is used", () => {
|
||||
const checks = validateToolExpectations({
|
||||
run: {
|
||||
success: true,
|
||||
actual: {},
|
||||
assistantMessageCount: 1,
|
||||
toolCallCount: 1,
|
||||
toolsUsed: ["search_app", "patch_app_file"],
|
||||
skillsInvoked: [],
|
||||
},
|
||||
toolExpect: {
|
||||
requiredToolsAnyOf: [["read_app_file", "search_app"]],
|
||||
},
|
||||
});
|
||||
|
||||
expect(checks).toContainEqual({
|
||||
name: "uses one of read_app_file, search_app",
|
||||
passed: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("fails requiredToolsAnyOf when no alternative in the group is used", () => {
|
||||
const checks = validateToolExpectations({
|
||||
run: {
|
||||
success: true,
|
||||
actual: {},
|
||||
assistantMessageCount: 1,
|
||||
toolCallCount: 1,
|
||||
toolsUsed: ["patch_app_file"],
|
||||
skillsInvoked: [],
|
||||
},
|
||||
toolExpect: {
|
||||
requiredToolsAnyOf: [["read_app_file", "search_app"]],
|
||||
},
|
||||
});
|
||||
|
||||
expect(checks).toContainEqual({
|
||||
name: "uses one of read_app_file, search_app",
|
||||
passed: false,
|
||||
details: "tools used: patch_app_file",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateGlobalState", () => {
|
||||
|
||||
@@ -169,16 +169,6 @@ export function validateToolExpectations(input: {
|
||||
);
|
||||
}
|
||||
|
||||
for (const group of expect.requiredToolsAnyOf ?? []) {
|
||||
checks.push(
|
||||
check(
|
||||
`uses one of ${group.join(", ")}`,
|
||||
group.some((toolName) => input.run.toolsUsed.includes(toolName)),
|
||||
`tools used: ${input.run.toolsUsed.join(", ") || "none"}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
for (const toolName of expect.forbiddenToolsUsed ?? []) {
|
||||
checks.push(
|
||||
check(
|
||||
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
type OrderStatus = 'paid' | 'shipped' | 'delivered' | 'pending' | 'refunded' | 'cancelled'
|
||||
|
||||
interface Order {
|
||||
id: string
|
||||
region: string
|
||||
quantity: number
|
||||
unitPrice: number
|
||||
status: OrderStatus
|
||||
placedAt: string
|
||||
}
|
||||
|
||||
// Server-side revenue rollup. Mirrors the client aggregation but is computed
|
||||
// from the authoritative mocked order book so it can be used to cross-check
|
||||
// the dashboard and to back the export.
|
||||
const orders: Order[] = [
|
||||
{ id: 'ORD-10001', region: 'North America', quantity: 3, unitPrice: 1195, status: 'delivered', placedAt: '2024-05-02' },
|
||||
{ id: 'ORD-10002', region: 'EMEA', quantity: 5, unitPrice: 880, status: 'shipped', placedAt: '2024-05-03' },
|
||||
{ id: 'ORD-10003', region: 'APAC', quantity: 2, unitPrice: 640, status: 'paid', placedAt: '2024-05-05' },
|
||||
{ id: 'ORD-10004', region: 'LATAM', quantity: 7, unitPrice: 315, status: 'delivered', placedAt: '2024-05-07' },
|
||||
{ id: 'ORD-10005', region: 'North America', quantity: 4, unitPrice: 150, status: 'refunded', placedAt: '2024-05-09' },
|
||||
{ id: 'ORD-10006', region: 'EMEA', quantity: 6, unitPrice: 220, status: 'shipped', placedAt: '2024-05-12' },
|
||||
{ id: 'ORD-10007', region: 'APAC', quantity: 1, unitPrice: 980, status: 'pending', placedAt: '2024-05-15' },
|
||||
{ id: 'ORD-10008', region: 'North America', quantity: 8, unitPrice: 1100, status: 'delivered', placedAt: '2024-05-18' },
|
||||
{ id: 'ORD-10009', region: 'EMEA', quantity: 2, unitPrice: 860, status: 'cancelled', placedAt: '2024-05-22' },
|
||||
{ id: 'ORD-10010', region: 'LATAM', quantity: 9, unitPrice: 290, status: 'paid', placedAt: '2024-05-26' }
|
||||
]
|
||||
|
||||
const REVENUE_STATUSES: OrderStatus[] = ['paid', 'shipped', 'delivered']
|
||||
|
||||
export async function main({
|
||||
from,
|
||||
to,
|
||||
region
|
||||
}: {
|
||||
from: string
|
||||
to: string
|
||||
region: string
|
||||
}): Promise<{
|
||||
totalRevenue: number
|
||||
netRevenue: number
|
||||
totalOrders: number
|
||||
averageOrderValue: number
|
||||
unitsSold: number
|
||||
refundedRevenue: number
|
||||
currency: string
|
||||
}> {
|
||||
let scoped = orders.filter((order) => order.placedAt >= from && order.placedAt <= to)
|
||||
if (region && region !== 'all') {
|
||||
scoped = scoped.filter((order) => order.region === region)
|
||||
}
|
||||
|
||||
const booked = scoped.filter((order) => REVENUE_STATUSES.includes(order.status))
|
||||
const totalRevenue = booked.reduce((acc, order) => acc + order.unitPrice * order.quantity, 0)
|
||||
const unitsSold = booked.reduce((acc, order) => acc + order.quantity, 0)
|
||||
const refundedRevenue = scoped
|
||||
.filter((order) => order.status === 'refunded')
|
||||
.reduce((acc, order) => acc + order.unitPrice * order.quantity, 0)
|
||||
|
||||
return {
|
||||
totalRevenue,
|
||||
netRevenue: totalRevenue - refundedRevenue,
|
||||
totalOrders: booked.length,
|
||||
averageOrderValue: booked.length === 0 ? 0 : Math.round(totalRevenue / booked.length),
|
||||
unitsSold,
|
||||
refundedRevenue,
|
||||
currency: 'USD'
|
||||
}
|
||||
}
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"name": "Compute Summary",
|
||||
"language": "bun"
|
||||
}
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
// Builds a downloadable report for the current dashboard view. Returns a data
|
||||
// URL the browser can open directly so the export works without object storage.
|
||||
export async function main({
|
||||
from,
|
||||
to,
|
||||
region,
|
||||
format
|
||||
}: {
|
||||
from: string
|
||||
to: string
|
||||
region: string
|
||||
format: 'csv' | 'json'
|
||||
}): Promise<{ url: string; rows: number; filename: string }> {
|
||||
const summary = {
|
||||
from,
|
||||
to,
|
||||
region: region || 'all',
|
||||
generatedAt: new Date().toISOString(),
|
||||
rows: [
|
||||
{ region: 'North America', revenue: 211_400, orders: 168 },
|
||||
{ region: 'EMEA', revenue: 142_900, orders: 121 },
|
||||
{ region: 'APAC', revenue: 86_500, orders: 78 },
|
||||
{ region: 'LATAM', revenue: 41_500, orders: 45 }
|
||||
]
|
||||
}
|
||||
|
||||
const scoped =
|
||||
region && region !== 'all'
|
||||
? summary.rows.filter((row) => row.region === region)
|
||||
: summary.rows
|
||||
|
||||
let body: string
|
||||
let mime: string
|
||||
if (format === 'csv') {
|
||||
const header = 'region,revenue,orders'
|
||||
const lines = scoped.map((row) => `${row.region},${row.revenue},${row.orders}`)
|
||||
body = [header, ...lines].join('\n')
|
||||
mime = 'text/csv'
|
||||
} else {
|
||||
body = JSON.stringify({ ...summary, rows: scoped }, null, 2)
|
||||
mime = 'application/json'
|
||||
}
|
||||
|
||||
const encoded = Buffer.from(body, 'utf-8').toString('base64')
|
||||
const filename = `revenue-report-${from}_${to}.${format}`
|
||||
return {
|
||||
url: `data:${mime};base64,${encoded}`,
|
||||
rows: scoped.length,
|
||||
filename
|
||||
}
|
||||
}
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"name": "Export Report",
|
||||
"language": "bun"
|
||||
}
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
interface MetricCardData {
|
||||
id: string
|
||||
label: string
|
||||
value: number
|
||||
unit: 'currency' | 'count' | 'percent'
|
||||
delta: number
|
||||
hint: string
|
||||
}
|
||||
|
||||
// Returns the headline metric cards for the selected range and region. Values
|
||||
// are mocked but internally consistent (revenue / orders ≈ avg order value).
|
||||
const baseByRegion: Record<string, { revenue: number; orders: number; units: number; refunds: number }> = {
|
||||
all: { revenue: 482_300, orders: 412, units: 1840, refunds: 11_900 },
|
||||
'North America': { revenue: 211_400, orders: 168, units: 770, refunds: 4_200 },
|
||||
EMEA: { revenue: 142_900, orders: 121, units: 560, refunds: 3_500 },
|
||||
APAC: { revenue: 86_500, orders: 78, units: 340, refunds: 2_600 },
|
||||
LATAM: { revenue: 41_500, orders: 45, units: 170, refunds: 1_600 }
|
||||
}
|
||||
|
||||
export async function main({
|
||||
from,
|
||||
to,
|
||||
region
|
||||
}: {
|
||||
from: string
|
||||
to: string
|
||||
region: string
|
||||
}): Promise<{ cards: MetricCardData[]; generatedAt: string }> {
|
||||
const base = baseByRegion[region] ?? baseByRegion.all
|
||||
const aov = base.orders === 0 ? 0 : Math.round(base.revenue / base.orders)
|
||||
const cards: MetricCardData[] = [
|
||||
{ id: 'revenue', label: 'Total Revenue', value: base.revenue, unit: 'currency', delta: 0.082, hint: `Booked revenue ${from} – ${to}` },
|
||||
{ id: 'orders', label: 'Orders', value: base.orders, unit: 'count', delta: 0.041, hint: 'Revenue-bearing orders in range' },
|
||||
{ id: 'aov', label: 'Avg Order Value', value: aov, unit: 'currency', delta: -0.013, hint: 'Total revenue / order count' },
|
||||
{ id: 'units', label: 'Units Sold', value: base.units, unit: 'count', delta: 0.067, hint: 'Total units in range' },
|
||||
{ id: 'refunds', label: 'Refunded', value: base.refunds, unit: 'currency', delta: -0.021, hint: 'Revenue lost to refunds' },
|
||||
{ id: 'conversion', label: 'Conversion', value: 0.187, unit: 'percent', delta: 0.009, hint: 'Sessions that became orders' }
|
||||
]
|
||||
return { cards, generatedAt: new Date().toISOString() }
|
||||
}
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"name": "Load Metrics",
|
||||
"language": "bun"
|
||||
}
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
type OrderStatus = 'paid' | 'shipped' | 'delivered' | 'pending' | 'refunded' | 'cancelled'
|
||||
|
||||
interface Order {
|
||||
id: string
|
||||
placedAt: string
|
||||
customer: string
|
||||
product: string
|
||||
sku: string
|
||||
region: string
|
||||
channel: string
|
||||
rep: string
|
||||
quantity: number
|
||||
unitPrice: number
|
||||
status: OrderStatus
|
||||
}
|
||||
|
||||
// Mocked order book. In a real deployment this would query the orders table;
|
||||
// here it returns a representative slice so the table renders in preview.
|
||||
const orders: Order[] = [
|
||||
{ id: 'ORD-10001', placedAt: '2024-05-02T09:14:00Z', customer: 'Contoso Ltd', product: 'Aurora Analytics Suite', sku: 'ANL-100', region: 'North America', channel: 'direct', rep: 'Dana Wills', quantity: 3, unitPrice: 1195, status: 'delivered' },
|
||||
{ id: 'ORD-10002', placedAt: '2024-05-03T11:42:00Z', customer: 'Fabrikam Inc', product: 'Borealis CRM', sku: 'CRM-210', region: 'EMEA', channel: 'partner', rep: 'Lena Fischer', quantity: 5, unitPrice: 880, status: 'shipped' },
|
||||
{ id: 'ORD-10003', placedAt: '2024-05-05T15:03:00Z', customer: 'Tailspin Toys', product: 'Cascade Data Pipeline', sku: 'PIPE-330', region: 'APAC', channel: 'self-serve', rep: 'Sora Tanaka', quantity: 2, unitPrice: 640, status: 'paid' },
|
||||
{ id: 'ORD-10004', placedAt: '2024-05-07T08:21:00Z', customer: 'Proseware Inc', product: 'Delta Insights', sku: 'INS-440', region: 'LATAM', channel: 'marketplace', rep: 'Diego Marin', quantity: 7, unitPrice: 315, status: 'delivered' },
|
||||
{ id: 'ORD-10005', placedAt: '2024-05-09T13:58:00Z', customer: 'Litware Inc', product: 'Echo Monitoring', sku: 'MON-550', region: 'North America', channel: 'direct', rep: 'Owen Pratt', quantity: 4, unitPrice: 150, status: 'refunded' },
|
||||
{ id: 'ORD-10006', placedAt: '2024-05-12T10:30:00Z', customer: 'Fourth Coffee', product: 'Helix Identity', sku: 'IDN-880', region: 'EMEA', channel: 'partner', rep: 'Aisha Khan', quantity: 6, unitPrice: 220, status: 'shipped' },
|
||||
{ id: 'ORD-10007', placedAt: '2024-05-15T17:11:00Z', customer: 'Coho Vineyard', product: 'Kelvin Forecasting', sku: 'FCT-202', region: 'APAC', channel: 'direct', rep: 'Priya Nair', quantity: 1, unitPrice: 980, status: 'pending' },
|
||||
{ id: 'ORD-10008', placedAt: '2024-05-18T12:05:00Z', customer: 'Alpine Ski House', product: 'Nimbus Compute', sku: 'CMP-505', region: 'North America', channel: 'self-serve', rep: 'Hugo Bernard', quantity: 8, unitPrice: 1100, status: 'delivered' },
|
||||
{ id: 'ORD-10009', placedAt: '2024-05-22T14:47:00Z', customer: 'Trey Research', product: 'Onyx Security', sku: 'SEC-606', region: 'EMEA', channel: 'direct', rep: 'Sven Olsen', quantity: 2, unitPrice: 860, status: 'cancelled' },
|
||||
{ id: 'ORD-10010', placedAt: '2024-05-26T16:39:00Z', customer: 'Blue Yonder Airlines', product: 'Polaris Reporting', sku: 'RPT-707', region: 'LATAM', channel: 'partner', rep: 'Mateo Russo', quantity: 9, unitPrice: 290, status: 'paid' }
|
||||
]
|
||||
|
||||
export async function main({
|
||||
from,
|
||||
to,
|
||||
region,
|
||||
status
|
||||
}: {
|
||||
from: string
|
||||
to: string
|
||||
region: string
|
||||
status: string
|
||||
}): Promise<{ orders: Order[]; total: number }> {
|
||||
let filtered = orders.filter((order) => {
|
||||
const day = order.placedAt.slice(0, 10)
|
||||
return day >= from && day <= to
|
||||
})
|
||||
if (region && region !== 'all') {
|
||||
filtered = filtered.filter((order) => order.region === region)
|
||||
}
|
||||
if (status && status !== 'all') {
|
||||
filtered = filtered.filter((order) => order.status === status)
|
||||
}
|
||||
return { orders: filtered, total: filtered.length }
|
||||
}
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"name": "Load Orders",
|
||||
"language": "bun"
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
import React from 'react'
|
||||
import type { DateRange } from '../lib/api'
|
||||
import { rangeForPreset } from '../lib/api'
|
||||
import { formatDateShort } from '../lib/format'
|
||||
|
||||
interface DateRangePickerProps {
|
||||
preset: string
|
||||
range: DateRange
|
||||
onPresetChange: (preset: string, range: DateRange) => void
|
||||
}
|
||||
|
||||
const PRESETS: { id: string; label: string }[] = [
|
||||
{ id: '7d', label: 'Last 7 days' },
|
||||
{ id: '14d', label: 'Last 14 days' },
|
||||
{ id: '30d', label: 'Last 30 days' },
|
||||
{ id: 'qtd', label: 'Quarter to date' }
|
||||
]
|
||||
|
||||
export const DateRangePicker: React.FC<DateRangePickerProps> = ({
|
||||
preset,
|
||||
range,
|
||||
onPresetChange
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
className="rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700"
|
||||
value={preset}
|
||||
onChange={(event) => {
|
||||
const next = event.target.value
|
||||
onPresetChange(next, rangeForPreset(next))
|
||||
}}
|
||||
>
|
||||
{PRESETS.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="text-xs text-gray-400">
|
||||
{formatDateShort(range.from)} – {formatDateShort(range.to)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
interface EmptyStateProps {
|
||||
title: string
|
||||
description?: string
|
||||
icon?: string
|
||||
action?: React.ReactNode
|
||||
}
|
||||
|
||||
export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
title,
|
||||
description,
|
||||
icon = '📊',
|
||||
action
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed border-gray-300 bg-white py-12 text-center">
|
||||
<div className="text-3xl" aria-hidden>
|
||||
{icon}
|
||||
</div>
|
||||
<h3 className="mt-3 text-sm font-semibold text-gray-700">{title}</h3>
|
||||
{description ? (
|
||||
<p className="mt-1 max-w-sm text-sm text-gray-500">{description}</p>
|
||||
) : null}
|
||||
{action ? <div className="mt-4">{action}</div> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
import React, { useState } from 'react'
|
||||
import { requestExport } from '../lib/api'
|
||||
import type { DateRange } from '../lib/api'
|
||||
|
||||
interface ExportButtonProps {
|
||||
range: DateRange
|
||||
region: string
|
||||
}
|
||||
|
||||
export const ExportButton: React.FC<ExportButtonProps> = ({ range, region }) => {
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const handleExport = async (format: 'csv' | 'json') => {
|
||||
setBusy(true)
|
||||
setError(null)
|
||||
try {
|
||||
const result = await requestExport(range, region, format)
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = result.url
|
||||
anchor.download = `revenue-report.${format}`
|
||||
anchor.click()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Export failed')
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => handleExport('csv')}
|
||||
className="rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
|
||||
>
|
||||
{busy ? 'Exporting…' : 'Export CSV'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => handleExport('json')}
|
||||
className="rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
|
||||
>
|
||||
Export JSON
|
||||
</button>
|
||||
{error ? <span className="text-xs text-rose-600">{error}</span> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
import React from 'react'
|
||||
import type { DateRange } from '../lib/api'
|
||||
import type { OrderStatus } from '../data/seedData'
|
||||
import { REGIONS, ORDER_STATUSES, STATUS_LABELS } from '../data/seedData'
|
||||
import { DateRangePicker } from './DateRangePicker'
|
||||
import { ExportButton } from './ExportButton'
|
||||
|
||||
interface FilterBarProps {
|
||||
region: string
|
||||
status: string
|
||||
preset: string
|
||||
range: DateRange
|
||||
onRegionChange: (region: string) => void
|
||||
onStatusChange: (status: string) => void
|
||||
onPresetChange: (preset: string, range: DateRange) => void
|
||||
}
|
||||
|
||||
export const FilterBar: React.FC<FilterBarProps> = ({
|
||||
region,
|
||||
status,
|
||||
preset,
|
||||
range,
|
||||
onRegionChange,
|
||||
onStatusChange,
|
||||
onPresetChange
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 bg-white px-6 py-4">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<DateRangePicker preset={preset} range={range} onPresetChange={onPresetChange} />
|
||||
<select
|
||||
className="rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700"
|
||||
value={region}
|
||||
onChange={(event) => onRegionChange(event.target.value)}
|
||||
>
|
||||
<option value="all">All regions</option>
|
||||
{REGIONS.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
className="rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700"
|
||||
value={status}
|
||||
onChange={(event) => onStatusChange(event.target.value)}
|
||||
>
|
||||
<option value="all">All statuses</option>
|
||||
{ORDER_STATUSES.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{STATUS_LABELS[item as OrderStatus]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<ExportButton range={range} region={region} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
import React from 'react'
|
||||
import type { MetricCardData } from '../data/seedData'
|
||||
import { formatCurrency, formatNumber, formatPercent, formatSignedPercent } from '../lib/format'
|
||||
|
||||
interface MetricCardProps {
|
||||
metric: MetricCardData
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
function renderValue(metric: MetricCardData): string {
|
||||
switch (metric.unit) {
|
||||
case 'currency':
|
||||
return formatCurrency(metric.value)
|
||||
case 'percent':
|
||||
return formatPercent(metric.value)
|
||||
case 'count':
|
||||
default:
|
||||
return formatNumber(metric.value)
|
||||
}
|
||||
}
|
||||
|
||||
export const MetricCard: React.FC<MetricCardProps> = ({ metric, loading }) => {
|
||||
const positive = metric.delta >= 0
|
||||
return (
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-gray-500">{metric.label}</span>
|
||||
<span
|
||||
className={`text-xs font-semibold ${positive ? 'text-emerald-600' : 'text-rose-600'}`}
|
||||
>
|
||||
{formatSignedPercent(metric.delta)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 text-2xl font-bold text-gray-900">
|
||||
{loading ? <span className="text-gray-300">…</span> : renderValue(metric)}
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-gray-400">{metric.hint}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import React from 'react'
|
||||
import type { MetricCardData } from '../data/seedData'
|
||||
import { MetricCard } from './MetricCard'
|
||||
|
||||
interface MetricGridProps {
|
||||
metrics: MetricCardData[]
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
export const MetricGrid: React.FC<MetricGridProps> = ({ metrics, loading }) => {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{metrics.map((metric) => (
|
||||
<MetricCard key={metric.id} metric={metric} loading={loading} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import type { Order } from '../data/seedData'
|
||||
import { StatusBadge } from './StatusBadge'
|
||||
import { EmptyState } from './EmptyState'
|
||||
import { formatCurrencyPrecise, formatDate, formatNumber, truncate } from '../lib/format'
|
||||
|
||||
interface OrdersTableProps {
|
||||
orders: Order[]
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
type SortKey = 'placedAt' | 'customer' | 'lineTotal' | 'quantity'
|
||||
type SortDir = 'asc' | 'desc'
|
||||
|
||||
// The per-row line total a customer was charged: unit price times quantity.
|
||||
function lineTotal(order: Order): number {
|
||||
return order.quantity * order.unitPrice
|
||||
}
|
||||
|
||||
export const OrdersTable: React.FC<OrdersTableProps> = ({ orders, loading }) => {
|
||||
const [sortKey, setSortKey] = useState<SortKey>('placedAt')
|
||||
const [sortDir, setSortDir] = useState<SortDir>('desc')
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
const copy = [...orders]
|
||||
copy.sort((a, b) => {
|
||||
let comparison = 0
|
||||
switch (sortKey) {
|
||||
case 'customer':
|
||||
comparison = a.customer.localeCompare(b.customer)
|
||||
break
|
||||
case 'lineTotal':
|
||||
comparison = lineTotal(a) - lineTotal(b)
|
||||
break
|
||||
case 'quantity':
|
||||
comparison = a.quantity - b.quantity
|
||||
break
|
||||
case 'placedAt':
|
||||
default:
|
||||
comparison = a.placedAt.localeCompare(b.placedAt)
|
||||
break
|
||||
}
|
||||
return sortDir === 'asc' ? comparison : -comparison
|
||||
})
|
||||
return copy
|
||||
}, [orders, sortKey, sortDir])
|
||||
|
||||
const toggleSort = (key: SortKey) => {
|
||||
if (key === sortKey) {
|
||||
setSortDir((dir) => (dir === 'asc' ? 'desc' : 'asc'))
|
||||
} else {
|
||||
setSortKey(key)
|
||||
setSortDir('desc')
|
||||
}
|
||||
}
|
||||
|
||||
if (!loading && orders.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
title="No orders match these filters"
|
||||
description="Try widening the date range or clearing the status filter."
|
||||
icon="🗂️"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const arrow = (key: SortKey) => (key === sortKey ? (sortDir === 'asc' ? '▲' : '▼') : '')
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
|
||||
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
||||
<thead className="bg-gray-50 text-left text-xs uppercase tracking-wide text-gray-500">
|
||||
<tr>
|
||||
<th className="cursor-pointer px-4 py-3" onClick={() => toggleSort('placedAt')}>
|
||||
Date {arrow('placedAt')}
|
||||
</th>
|
||||
<th className="cursor-pointer px-4 py-3" onClick={() => toggleSort('customer')}>
|
||||
Customer {arrow('customer')}
|
||||
</th>
|
||||
<th className="px-4 py-3">Product</th>
|
||||
<th className="px-4 py-3">Region</th>
|
||||
<th className="cursor-pointer px-4 py-3 text-right" onClick={() => toggleSort('quantity')}>
|
||||
Qty {arrow('quantity')}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right">Unit Price</th>
|
||||
<th className="cursor-pointer px-4 py-3 text-right" onClick={() => toggleSort('lineTotal')}>
|
||||
Line Total {arrow('lineTotal')}
|
||||
</th>
|
||||
<th className="px-4 py-3">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{sorted.map((order) => (
|
||||
<tr key={order.id} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3 text-gray-500">{formatDate(order.placedAt)}</td>
|
||||
<td className="px-4 py-3 font-medium text-gray-900">
|
||||
{truncate(order.customer, 24)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-600">{order.product}</td>
|
||||
<td className="px-4 py-3 text-gray-600">{order.region}</td>
|
||||
<td className="px-4 py-3 text-right text-gray-600">{formatNumber(order.quantity)}</td>
|
||||
<td className="px-4 py-3 text-right text-gray-600">
|
||||
{formatCurrencyPrecise(order.unitPrice)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right font-semibold text-gray-900">
|
||||
{formatCurrencyPrecise(lineTotal(order))}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<StatusBadge status={order.status} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import type { Order } from '../data/seedData'
|
||||
import { breakdownByRegion } from '../lib/aggregations'
|
||||
import { formatCurrency, formatNumber, formatPercent } from '../lib/format'
|
||||
import { EmptyState } from './EmptyState'
|
||||
|
||||
interface RegionTableProps {
|
||||
orders: Order[]
|
||||
}
|
||||
|
||||
export const RegionTable: React.FC<RegionTableProps> = ({ orders }) => {
|
||||
const rows = useMemo(() => breakdownByRegion(orders), [orders])
|
||||
const total = useMemo(() => rows.reduce((acc, row) => acc + row.revenue, 0), [rows])
|
||||
|
||||
if (rows.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
title="No regional revenue"
|
||||
description="No revenue-bearing orders fall in the current selection."
|
||||
icon="🌍"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<h2 className="mb-4 text-lg font-semibold text-gray-900">Revenue by Region</h2>
|
||||
<table className="min-w-full text-sm">
|
||||
<thead className="text-left text-xs uppercase tracking-wide text-gray-500">
|
||||
<tr>
|
||||
<th className="py-2">Region</th>
|
||||
<th className="py-2 text-right">Orders</th>
|
||||
<th className="py-2 text-right">Revenue</th>
|
||||
<th className="py-2 text-right">Share</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{rows.map((row) => (
|
||||
<tr key={row.region}>
|
||||
<td className="py-2 font-medium text-gray-900">{row.region}</td>
|
||||
<td className="py-2 text-right text-gray-600">{formatNumber(row.orders)}</td>
|
||||
<td className="py-2 text-right text-gray-900">{formatCurrency(row.revenue)}</td>
|
||||
<td className="py-2 text-right text-gray-500">
|
||||
{formatPercent(total === 0 ? 0 : row.revenue / total)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import type { Order } from '../data/seedData'
|
||||
import { dailyRevenue } from '../lib/aggregations'
|
||||
import { formatCompact, formatDateShort } from '../lib/format'
|
||||
import { EmptyState } from './EmptyState'
|
||||
|
||||
interface RevenueChartProps {
|
||||
orders: Order[]
|
||||
}
|
||||
|
||||
// Lightweight inline bar chart for daily revenue. Avoids a charting dependency
|
||||
// by sizing flexed columns relative to the busiest day in the window.
|
||||
export const RevenueChart: React.FC<RevenueChartProps> = ({ orders }) => {
|
||||
const points = useMemo(() => dailyRevenue(orders), [orders])
|
||||
const max = useMemo(() => points.reduce((acc, point) => Math.max(acc, point.revenue), 0), [points])
|
||||
|
||||
if (points.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
title="No revenue in range"
|
||||
description="Adjust the date range or filters to see daily revenue."
|
||||
icon="📉"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<h2 className="mb-4 text-lg font-semibold text-gray-900">Daily Revenue</h2>
|
||||
<div className="flex h-48 items-end gap-1">
|
||||
{points.map((point) => {
|
||||
const heightPct = max === 0 ? 0 : Math.round((point.revenue / max) * 100)
|
||||
return (
|
||||
<div key={point.date} className="flex flex-1 flex-col items-center justify-end">
|
||||
<div
|
||||
className="w-full rounded-t bg-indigo-400"
|
||||
style={{ height: `${Math.max(heightPct, 2)}%` }}
|
||||
title={`${point.date}: ${formatCompact(point.revenue)}`}
|
||||
/>
|
||||
<span className="mt-1 truncate text-[9px] text-gray-400">
|
||||
{formatDateShort(point.date)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
export type DashboardView = 'overview' | 'orders' | 'regions' | 'products'
|
||||
|
||||
interface SidebarProps {
|
||||
active: DashboardView
|
||||
onSelect: (view: DashboardView) => void
|
||||
}
|
||||
|
||||
const NAV_ITEMS: { id: DashboardView; label: string; icon: string }[] = [
|
||||
{ id: 'overview', label: 'Overview', icon: '📈' },
|
||||
{ id: 'orders', label: 'Orders', icon: '🧾' },
|
||||
{ id: 'regions', label: 'Regions', icon: '🌍' },
|
||||
{ id: 'products', label: 'Products', icon: '📦' }
|
||||
]
|
||||
|
||||
export const Sidebar: React.FC<SidebarProps> = ({ active, onSelect }) => {
|
||||
return (
|
||||
<aside className="flex w-56 flex-col border-r border-gray-200 bg-white">
|
||||
<div className="flex items-center gap-2 border-b border-gray-200 px-5 py-4">
|
||||
<span className="text-xl">🪁</span>
|
||||
<span className="text-sm font-bold text-gray-900">Acme Operations</span>
|
||||
</div>
|
||||
<nav className="flex-1 space-y-1 p-3">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const isActive = item.id === active
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(item.id)}
|
||||
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm font-medium transition ${
|
||||
isActive
|
||||
? 'bg-indigo-50 text-indigo-700'
|
||||
: 'text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<span aria-hidden>{item.icon}</span>
|
||||
{item.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
<div className="border-t border-gray-200 p-4 text-xs text-gray-400">
|
||||
Analytics workspace
|
||||
<div className="mt-1 font-mono text-[10px] text-gray-300">v2.4.0</div>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
import React from 'react'
|
||||
import type { OrderStatus } from '../data/seedData'
|
||||
import { STATUS_LABELS } from '../data/seedData'
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status: OrderStatus
|
||||
}
|
||||
|
||||
const STATUS_STYLES: Record<OrderStatus, string> = {
|
||||
paid: 'bg-blue-100 text-blue-700',
|
||||
shipped: 'bg-indigo-100 text-indigo-700',
|
||||
delivered: 'bg-emerald-100 text-emerald-700',
|
||||
pending: 'bg-amber-100 text-amber-700',
|
||||
refunded: 'bg-rose-100 text-rose-700',
|
||||
cancelled: 'bg-gray-200 text-gray-600'
|
||||
}
|
||||
|
||||
export const StatusBadge: React.FC<StatusBadgeProps> = ({ status }) => {
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${STATUS_STYLES[status]}`}
|
||||
>
|
||||
{STATUS_LABELS[status]}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import type { Order } from '../data/seedData'
|
||||
import { summarizeRevenue } from '../lib/aggregations'
|
||||
import { formatCurrency, formatCurrencyPrecise, formatNumber } from '../lib/format'
|
||||
|
||||
interface SummaryPanelProps {
|
||||
orders: Order[]
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
// Headline revenue panel. It re-aggregates the orders client-side via
|
||||
// summarizeRevenue so the totals stay in sync with whatever filter the user
|
||||
// has applied, without waiting for another backend round trip.
|
||||
export const SummaryPanel: React.FC<SummaryPanelProps> = ({ orders, loading }) => {
|
||||
const summary = useMemo(() => summarizeRevenue(orders), [orders])
|
||||
|
||||
const tiles = [
|
||||
{ label: 'Total Revenue', value: formatCurrency(summary.totalRevenue), emphasis: true },
|
||||
{ label: 'Net Revenue', value: formatCurrency(summary.netRevenue) },
|
||||
{ label: 'Orders', value: formatNumber(summary.totalOrders) },
|
||||
{ label: 'Avg Order Value', value: formatCurrencyPrecise(summary.averageOrderValue) },
|
||||
{ label: 'Units Sold', value: formatNumber(summary.unitsSold) },
|
||||
{ label: 'Refunded', value: formatCurrency(summary.refundedRevenue) }
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Revenue Summary</h2>
|
||||
{loading ? <span className="text-xs text-gray-400">Refreshing…</span> : null}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-3">
|
||||
{tiles.map((tile) => (
|
||||
<div
|
||||
key={tile.label}
|
||||
className={`rounded-lg p-4 ${tile.emphasis ? 'bg-indigo-50' : 'bg-gray-50'}`}
|
||||
>
|
||||
<div className="text-xs font-medium uppercase tracking-wide text-gray-500">
|
||||
{tile.label}
|
||||
</div>
|
||||
<div
|
||||
className={`mt-1 font-bold ${tile.emphasis ? 'text-2xl text-indigo-700' : 'text-xl text-gray-900'}`}
|
||||
>
|
||||
{tile.value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import type { Order } from '../data/seedData'
|
||||
import { topProducts } from '../lib/aggregations'
|
||||
import { formatCurrency } from '../lib/format'
|
||||
import { EmptyState } from './EmptyState'
|
||||
|
||||
interface TopProductsProps {
|
||||
orders: Order[]
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export const TopProducts: React.FC<TopProductsProps> = ({ orders, limit = 5 }) => {
|
||||
const products = useMemo(() => topProducts(orders, limit), [orders, limit])
|
||||
const max = useMemo(
|
||||
() => products.reduce((acc, item) => Math.max(acc, item.revenue), 0),
|
||||
[products]
|
||||
)
|
||||
|
||||
if (products.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
title="No product revenue"
|
||||
description="No revenue-bearing orders to rank by product."
|
||||
icon="📦"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<h2 className="mb-4 text-lg font-semibold text-gray-900">Top Products</h2>
|
||||
<ul className="space-y-3">
|
||||
{products.map((item, index) => {
|
||||
const widthPct = max === 0 ? 0 : Math.round((item.revenue / max) * 100)
|
||||
return (
|
||||
<li key={item.product}>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="font-medium text-gray-800">
|
||||
{index + 1}. {item.product}
|
||||
</span>
|
||||
<span className="text-gray-600">{formatCurrency(item.revenue)}</span>
|
||||
</div>
|
||||
<div className="mt-1 h-2 w-full overflow-hidden rounded-full bg-gray-100">
|
||||
<div
|
||||
className="h-full rounded-full bg-emerald-400"
|
||||
style={{ width: `${Math.max(widthPct, 2)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
-5051
File diff suppressed because it is too large
Load Diff
@@ -1,164 +0,0 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { Sidebar, type DashboardView } from './components/Sidebar'
|
||||
import { FilterBar } from './components/FilterBar'
|
||||
import { MetricGrid } from './components/MetricGrid'
|
||||
import { SummaryPanel } from './components/SummaryPanel'
|
||||
import { RevenueChart } from './components/RevenueChart'
|
||||
import { OrdersTable } from './components/OrdersTable'
|
||||
import { RegionTable } from './components/RegionTable'
|
||||
import { TopProducts } from './components/TopProducts'
|
||||
import { EmptyState } from './components/EmptyState'
|
||||
import { fetchMetrics, fetchOrders, rangeForPreset, type DateRange } from './lib/api'
|
||||
import {
|
||||
seedOrders,
|
||||
seedMetricCards,
|
||||
ordersInRange,
|
||||
ordersForRegion,
|
||||
type Order,
|
||||
type MetricCardData
|
||||
} from './data/seedData'
|
||||
|
||||
const App = () => {
|
||||
const [view, setView] = useState<DashboardView>('overview')
|
||||
const [preset, setPreset] = useState('30d')
|
||||
const [range, setRange] = useState<DateRange>(rangeForPreset('30d'))
|
||||
const [region, setRegion] = useState('all')
|
||||
const [status, setStatus] = useState('all')
|
||||
|
||||
const [metrics, setMetrics] = useState<MetricCardData[]>(seedMetricCards)
|
||||
const [orders, setOrders] = useState<Order[]>(seedOrders)
|
||||
const [loadingMetrics, setLoadingMetrics] = useState(true)
|
||||
const [loadingOrders, setLoadingOrders] = useState(true)
|
||||
const [errored, setErrored] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
setLoadingMetrics(true)
|
||||
fetchMetrics(range, region)
|
||||
.then((result) => {
|
||||
if (!cancelled) {
|
||||
setMetrics(result.cards)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
setMetrics(seedMetricCards)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) {
|
||||
setLoadingMetrics(false)
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [range, region])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
setLoadingOrders(true)
|
||||
setErrored(false)
|
||||
fetchOrders(range, region, status)
|
||||
.then((result) => {
|
||||
if (!cancelled) {
|
||||
setOrders(result.orders)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
// Fall back to the bundled seed data so the dashboard still renders.
|
||||
const scoped = ordersForRegion(
|
||||
ordersInRange(seedOrders, range.from, range.to),
|
||||
region
|
||||
).filter((order) => status === 'all' || order.status === status)
|
||||
setOrders(scoped)
|
||||
setErrored(true)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) {
|
||||
setLoadingOrders(false)
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [range, region, status])
|
||||
|
||||
const handlePresetChange = (nextPreset: string, nextRange: DateRange) => {
|
||||
setPreset(nextPreset)
|
||||
setRange(nextRange)
|
||||
}
|
||||
|
||||
// Orders that drive the summary/chart panels — the table applies the status
|
||||
// filter itself, so the panels see the same range/region scoped orders.
|
||||
const scopedOrders = useMemo(() => orders, [orders])
|
||||
|
||||
const renderView = () => {
|
||||
switch (view) {
|
||||
case 'orders':
|
||||
return <OrdersTable orders={scopedOrders} loading={loadingOrders} />
|
||||
case 'regions':
|
||||
return <RegionTable orders={scopedOrders} />
|
||||
case 'products':
|
||||
return <TopProducts orders={scopedOrders} limit={8} />
|
||||
case 'overview':
|
||||
default:
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<MetricGrid metrics={metrics} loading={loadingMetrics} />
|
||||
<SummaryPanel orders={scopedOrders} loading={loadingOrders} />
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<RevenueChart orders={scopedOrders} />
|
||||
<TopProducts orders={scopedOrders} />
|
||||
</div>
|
||||
<RegionTable orders={scopedOrders} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-100 text-gray-900">
|
||||
<Sidebar active={view} onSelect={setView} />
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<header className="border-b border-gray-200 bg-white px-6 py-5">
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-indigo-500">
|
||||
Acme Inc
|
||||
</p>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Operations Console</h1>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Revenue, orders, and regional performance at a glance.
|
||||
</p>
|
||||
</header>
|
||||
<FilterBar
|
||||
region={region}
|
||||
status={status}
|
||||
preset={preset}
|
||||
range={range}
|
||||
onRegionChange={setRegion}
|
||||
onStatusChange={setStatus}
|
||||
onPresetChange={handlePresetChange}
|
||||
/>
|
||||
<main className="flex-1 overflow-auto p-6">
|
||||
{errored ? (
|
||||
<div className="mb-4 rounded-lg border border-amber-200 bg-amber-50 px-4 py-2 text-sm text-amber-700">
|
||||
Showing locally bundled data — the live feed is unavailable.
|
||||
</div>
|
||||
) : null}
|
||||
{scopedOrders.length === 0 && !loadingOrders ? (
|
||||
<EmptyState
|
||||
title="Nothing to show yet"
|
||||
description="No data for the selected range, region, and status."
|
||||
/>
|
||||
) : (
|
||||
renderView()
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
-149
@@ -1,149 +0,0 @@
|
||||
// Aggregation helpers that turn raw order/metric rows into the numbers the
|
||||
// dashboard renders. These run client-side after the backend returns rows so
|
||||
// the UI can re-aggregate instantly when filters change without a round trip.
|
||||
|
||||
import type { Order, OrderStatus } from '../data/seedData'
|
||||
|
||||
export interface RevenueSummary {
|
||||
totalRevenue: number
|
||||
totalOrders: number
|
||||
averageOrderValue: number
|
||||
unitsSold: number
|
||||
refundedRevenue: number
|
||||
netRevenue: number
|
||||
}
|
||||
|
||||
export interface StatusBreakdown {
|
||||
status: OrderStatus
|
||||
orders: number
|
||||
revenue: number
|
||||
}
|
||||
|
||||
export interface RegionBreakdown {
|
||||
region: string
|
||||
orders: number
|
||||
revenue: number
|
||||
}
|
||||
|
||||
export interface DailyPoint {
|
||||
date: string
|
||||
revenue: number
|
||||
orders: number
|
||||
}
|
||||
|
||||
// Revenue for a single line item. An order's revenue is the unit price times
|
||||
// the number of units purchased — never the unit price alone.
|
||||
export function orderRevenue(order: Order): number {
|
||||
return order.unitPrice
|
||||
}
|
||||
|
||||
// The statuses that count toward realized (booked) revenue. Refunded and
|
||||
// cancelled orders are excluded from the headline revenue total.
|
||||
const REVENUE_STATUSES: OrderStatus[] = ['paid', 'shipped', 'delivered']
|
||||
|
||||
export function isRevenueStatus(status: OrderStatus): boolean {
|
||||
return REVENUE_STATUSES.includes(status)
|
||||
}
|
||||
|
||||
export function sumRevenue(orders: Order[]): number {
|
||||
return orders
|
||||
.filter((order) => isRevenueStatus(order.status))
|
||||
.reduce((acc, order) => acc + orderRevenue(order), 0)
|
||||
}
|
||||
|
||||
export function sumUnits(orders: Order[]): number {
|
||||
return orders
|
||||
.filter((order) => isRevenueStatus(order.status))
|
||||
.reduce((acc, order) => acc + order.quantity, 0)
|
||||
}
|
||||
|
||||
export function sumRefundedRevenue(orders: Order[]): number {
|
||||
return orders
|
||||
.filter((order) => order.status === 'refunded')
|
||||
.reduce((acc, order) => acc + order.unitPrice * order.quantity, 0)
|
||||
}
|
||||
|
||||
export function summarizeRevenue(orders: Order[]): RevenueSummary {
|
||||
const revenueOrders = orders.filter((order) => isRevenueStatus(order.status))
|
||||
const totalRevenue = sumRevenue(orders)
|
||||
const unitsSold = sumUnits(orders)
|
||||
const refundedRevenue = sumRefundedRevenue(orders)
|
||||
const totalOrders = revenueOrders.length
|
||||
return {
|
||||
totalRevenue,
|
||||
totalOrders,
|
||||
averageOrderValue: totalOrders === 0 ? 0 : totalRevenue / totalOrders,
|
||||
unitsSold,
|
||||
refundedRevenue,
|
||||
netRevenue: totalRevenue - refundedRevenue
|
||||
}
|
||||
}
|
||||
|
||||
export function breakdownByStatus(orders: Order[]): StatusBreakdown[] {
|
||||
const map = new Map<OrderStatus, StatusBreakdown>()
|
||||
for (const order of orders) {
|
||||
const existing = map.get(order.status) ?? {
|
||||
status: order.status,
|
||||
orders: 0,
|
||||
revenue: 0
|
||||
}
|
||||
existing.orders += 1
|
||||
existing.revenue += order.unitPrice * order.quantity
|
||||
map.set(order.status, existing)
|
||||
}
|
||||
return [...map.values()].sort((a, b) => b.revenue - a.revenue)
|
||||
}
|
||||
|
||||
export function breakdownByRegion(orders: Order[]): RegionBreakdown[] {
|
||||
const map = new Map<string, RegionBreakdown>()
|
||||
for (const order of orders) {
|
||||
if (!isRevenueStatus(order.status)) {
|
||||
continue
|
||||
}
|
||||
const existing = map.get(order.region) ?? {
|
||||
region: order.region,
|
||||
orders: 0,
|
||||
revenue: 0
|
||||
}
|
||||
existing.orders += 1
|
||||
existing.revenue += order.unitPrice * order.quantity
|
||||
map.set(order.region, existing)
|
||||
}
|
||||
return [...map.values()].sort((a, b) => b.revenue - a.revenue)
|
||||
}
|
||||
|
||||
export function dailyRevenue(orders: Order[]): DailyPoint[] {
|
||||
const map = new Map<string, DailyPoint>()
|
||||
for (const order of orders) {
|
||||
if (!isRevenueStatus(order.status)) {
|
||||
continue
|
||||
}
|
||||
const day = order.placedAt.slice(0, 10)
|
||||
const existing = map.get(day) ?? { date: day, revenue: 0, orders: 0 }
|
||||
existing.revenue += order.unitPrice * order.quantity
|
||||
existing.orders += 1
|
||||
map.set(day, existing)
|
||||
}
|
||||
return [...map.values()].sort((a, b) => a.date.localeCompare(b.date))
|
||||
}
|
||||
|
||||
export function topProducts(orders: Order[], limit: number = 5): { product: string; revenue: number }[] {
|
||||
const map = new Map<string, number>()
|
||||
for (const order of orders) {
|
||||
if (!isRevenueStatus(order.status)) {
|
||||
continue
|
||||
}
|
||||
map.set(order.product, (map.get(order.product) ?? 0) + order.unitPrice * order.quantity)
|
||||
}
|
||||
return [...map.entries()]
|
||||
.map(([product, revenue]) => ({ product, revenue }))
|
||||
.sort((a, b) => b.revenue - a.revenue)
|
||||
.slice(0, limit)
|
||||
}
|
||||
|
||||
export function growthRatio(current: number, previous: number): number {
|
||||
if (previous === 0) {
|
||||
return current === 0 ? 0 : 1
|
||||
}
|
||||
return (current - previous) / previous
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// Thin wrappers around the app's backend runnables. Centralizing the calls
|
||||
// here keeps the components free of `backend.*` plumbing and gives one place to
|
||||
// normalize the request/response shapes.
|
||||
|
||||
import { backend } from 'wmill'
|
||||
import type { Order, MetricCardData } from '../data/seedData'
|
||||
|
||||
export interface DateRange {
|
||||
from: string
|
||||
to: string
|
||||
}
|
||||
|
||||
export interface MetricsResponse {
|
||||
cards: MetricCardData[]
|
||||
generatedAt: string
|
||||
}
|
||||
|
||||
export interface OrdersResponse {
|
||||
orders: Order[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface SummaryResponse {
|
||||
totalRevenue: number
|
||||
netRevenue: number
|
||||
totalOrders: number
|
||||
averageOrderValue: number
|
||||
unitsSold: number
|
||||
refundedRevenue: number
|
||||
currency: string
|
||||
}
|
||||
|
||||
export async function fetchMetrics(range: DateRange, region: string): Promise<MetricsResponse> {
|
||||
return backend.loadMetrics({ from: range.from, to: range.to, region })
|
||||
}
|
||||
|
||||
export async function fetchOrders(
|
||||
range: DateRange,
|
||||
region: string,
|
||||
status: string
|
||||
): Promise<OrdersResponse> {
|
||||
return backend.loadOrders({
|
||||
from: range.from,
|
||||
to: range.to,
|
||||
region,
|
||||
status
|
||||
})
|
||||
}
|
||||
|
||||
export async function fetchSummary(range: DateRange, region: string): Promise<SummaryResponse> {
|
||||
return backend.computeSummary({ from: range.from, to: range.to, region })
|
||||
}
|
||||
|
||||
export async function requestExport(
|
||||
range: DateRange,
|
||||
region: string,
|
||||
format: 'csv' | 'json'
|
||||
): Promise<{ url: string; rows: number }> {
|
||||
return backend.exportReport({ from: range.from, to: range.to, region, format })
|
||||
}
|
||||
|
||||
export function defaultRange(): DateRange {
|
||||
return { from: '2024-05-01', to: '2024-05-31' }
|
||||
}
|
||||
|
||||
export function rangeForPreset(preset: string): DateRange {
|
||||
switch (preset) {
|
||||
case '7d':
|
||||
return { from: '2024-05-25', to: '2024-05-31' }
|
||||
case '14d':
|
||||
return { from: '2024-05-18', to: '2024-05-31' }
|
||||
case '30d':
|
||||
return { from: '2024-05-01', to: '2024-05-31' }
|
||||
case 'qtd':
|
||||
return { from: '2024-04-01', to: '2024-05-31' }
|
||||
default:
|
||||
return defaultRange()
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
// Presentation-layer formatting helpers shared across the dashboard.
|
||||
// Pure functions only — no React, no data fetching.
|
||||
|
||||
export function formatCurrency(amount: number, currency: string = 'USD'): string {
|
||||
if (!Number.isFinite(amount)) {
|
||||
return '—'
|
||||
}
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency,
|
||||
maximumFractionDigits: 0
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
export function formatCurrencyPrecise(amount: number, currency: string = 'USD'): string {
|
||||
if (!Number.isFinite(amount)) {
|
||||
return '—'
|
||||
}
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
export function formatNumber(value: number): string {
|
||||
if (!Number.isFinite(value)) {
|
||||
return '—'
|
||||
}
|
||||
return new Intl.NumberFormat('en-US').format(value)
|
||||
}
|
||||
|
||||
export function formatCompact(value: number): string {
|
||||
if (!Number.isFinite(value)) {
|
||||
return '—'
|
||||
}
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1
|
||||
}).format(value)
|
||||
}
|
||||
|
||||
export function formatPercent(ratio: number, digits: number = 1): string {
|
||||
if (!Number.isFinite(ratio)) {
|
||||
return '—'
|
||||
}
|
||||
return `${(ratio * 100).toFixed(digits)}%`
|
||||
}
|
||||
|
||||
export function formatSignedPercent(ratio: number, digits: number = 1): string {
|
||||
const sign = ratio > 0 ? '+' : ''
|
||||
return `${sign}${formatPercent(ratio, digits)}`
|
||||
}
|
||||
|
||||
export function formatDate(iso: string): string {
|
||||
const date = new Date(iso)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return iso
|
||||
}
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
export function formatDateShort(iso: string): string {
|
||||
const date = new Date(iso)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return iso
|
||||
}
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
export function titleCase(value: string): string {
|
||||
return value
|
||||
.split(/[\s_-]+/)
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
export function truncate(value: string, max: number = 32): string {
|
||||
if (value.length <= max) {
|
||||
return value
|
||||
}
|
||||
return `${value.slice(0, max - 1)}…`
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"workspace": {
|
||||
"jobs": [
|
||||
{
|
||||
"id": "01920000-0000-7000-8000-0000000000f1",
|
||||
"scriptPath": "f/etl/sync_customers",
|
||||
"jobKind": "script",
|
||||
"createdBy": "alice",
|
||||
"success": false,
|
||||
"logs": "Starting customer sync...\nFetched 0 records\nERROR: connection refused to https://api.upstream.example.com\n at fetchCustomers (sync_customers.ts:42)\nJob failed with exit code 1"
|
||||
},
|
||||
{
|
||||
"id": "01920000-0000-7000-8000-0000000000f2",
|
||||
"scriptPath": "f/reports/daily_digest",
|
||||
"jobKind": "script",
|
||||
"createdBy": "bob",
|
||||
"success": true,
|
||||
"logs": "Generating daily digest...\nDigest emailed to 12 recipients\nDone in 1.2s"
|
||||
},
|
||||
{
|
||||
"id": "01920000-0000-7000-8000-0000000000f3",
|
||||
"scriptPath": "f/billing/charge_invoices",
|
||||
"jobKind": "flow",
|
||||
"createdBy": "alice",
|
||||
"success": true,
|
||||
"logs": "Processing invoices...\nCharged 8 invoices totalling $1,240.00\nDone"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"user": {
|
||||
"username": "admin",
|
||||
"is_admin": true
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"user": {
|
||||
"username": "admin",
|
||||
"is_admin": true,
|
||||
"folders": ["marketing", "data_engineering", "shared_utils"],
|
||||
"folders_read": ["marketing", "data_engineering", "shared_utils"]
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"user": {
|
||||
"username": "bob",
|
||||
"is_admin": false,
|
||||
"folders": ["team_a"],
|
||||
"folders_read": ["team_a", "team_b"]
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,6 @@ export function createAppModeRunner(
|
||||
toolsUsed: result.toolsUsed,
|
||||
skillsInvoked: [],
|
||||
tokenUsage: result.tokenUsage,
|
||||
finalContextTokens: result.finalContextTokens,
|
||||
};
|
||||
},
|
||||
validate({ evalCase, actual, initial, expected, run }) {
|
||||
|
||||
@@ -106,7 +106,6 @@ export function createCliModeRunner(
|
||||
toolsUsed: run.trace.toolsUsed.map((entry) => entry.tool),
|
||||
skillsInvoked: run.trace.skillsInvoked,
|
||||
tokenUsage: run.tokenUsage ?? null,
|
||||
finalContextTokens: run.finalContextTokens ?? null,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
@@ -123,7 +122,6 @@ export function createCliModeRunner(
|
||||
toolsUsed: [],
|
||||
skillsInvoked: [],
|
||||
tokenUsage: null,
|
||||
finalContextTokens: null,
|
||||
};
|
||||
} finally {
|
||||
await rm(workspaceDir, { recursive: true, force: true });
|
||||
|
||||
@@ -61,7 +61,6 @@ export function createFlowModeRunner(
|
||||
toolCallDetails: result.toolCallDetails,
|
||||
skillsInvoked: [],
|
||||
tokenUsage: result.tokenUsage,
|
||||
finalContextTokens: result.finalContextTokens,
|
||||
};
|
||||
},
|
||||
validate({ evalCase, actual, initial, expected }) {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { readFile, stat } from "node:fs/promises";
|
||||
import { basename } from "node:path";
|
||||
import { loadAppFixtureForEval } from "../adapters/frontend/core/app/appFixtureLoader";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import {
|
||||
runGlobalEval,
|
||||
type GlobalLiveEditorDraftFixture,
|
||||
type GlobalUserFixture,
|
||||
} from "../adapters/frontend/core/global/globalEvalRunner";
|
||||
import type { BenchmarkWorkspaceRunnables } from "../adapters/frontend/mockBackend";
|
||||
import type { FrontendEvalModelConfig } from "../core/models";
|
||||
@@ -16,7 +13,6 @@ import { getFrontendApiKey } from "./frontendCommon";
|
||||
export interface GlobalInitialFixture {
|
||||
workspace?: BenchmarkWorkspaceRunnables;
|
||||
liveEditorDrafts?: GlobalLiveEditorDraftFixture[];
|
||||
user?: GlobalUserFixture;
|
||||
}
|
||||
|
||||
export function createGlobalModeRunner(
|
||||
@@ -40,7 +36,6 @@ export function createGlobalModeRunner(
|
||||
{
|
||||
workspaceFixtures: initial?.workspace,
|
||||
liveEditorDrafts: initial?.liveEditorDrafts,
|
||||
user: initial?.user,
|
||||
maxIterations: context.evalCase?.runtime?.maxTurns,
|
||||
provider: modelConfig.provider,
|
||||
model: modelConfig.model,
|
||||
@@ -59,7 +54,6 @@ export function createGlobalModeRunner(
|
||||
toolCallDetails: result.toolCallDetails,
|
||||
skillsInvoked: [],
|
||||
tokenUsage: result.tokenUsage,
|
||||
finalContextTokens: result.finalContextTokens,
|
||||
};
|
||||
},
|
||||
validate({ evalCase, actual, expected }) {
|
||||
@@ -81,33 +75,10 @@ export function createGlobalModeRunner(
|
||||
}
|
||||
|
||||
async function loadGlobalInitialFixture(path: string): Promise<GlobalInitialFixture> {
|
||||
if ((await stat(path)).isDirectory()) {
|
||||
const { initialFrontend, initialBackend, initialDatatables } =
|
||||
await loadAppFixtureForEval(path);
|
||||
const name = basename(path);
|
||||
return {
|
||||
workspace: {
|
||||
apps: [
|
||||
{
|
||||
path: `f/evals/global/${name}`,
|
||||
summary: name,
|
||||
value: {
|
||||
files: initialFrontend,
|
||||
runnables: initialBackend,
|
||||
data: initialDatatables,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
liveEditorDrafts: [],
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(await readFile(path, "utf8")) as GlobalInitialFixture;
|
||||
return {
|
||||
workspace: parsed.workspace ?? {},
|
||||
liveEditorDrafts: parsed.liveEditorDrafts ?? [],
|
||||
user: parsed.user,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@ export function createScriptModeRunner(
|
||||
toolCallDetails: result.toolCallDetails,
|
||||
skillsInvoked: [],
|
||||
tokenUsage: result.tokenUsage,
|
||||
finalContextTokens: result.finalContextTokens,
|
||||
};
|
||||
},
|
||||
validate({ actual, initial, expected }) {
|
||||
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n COUNT(*)::bigint AS \"total!\",\n COUNT(*) FILTER (WHERE name = ANY($2::text[]))::bigint AS \"replacing!\"\n FROM ai_skill\n WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "total!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "replacing!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "002a606e71364b0581dbc496bf4337f276861dc71d2e277a7aef711543eb14d7"
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT email, login_type::TEXT, super_admin, devops, verified, name, company, username, NULL::bool as operator_only, first_time_user, role_source, disabled, NULL::text as workspace_id FROM password WHERE email = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "login_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "super_admin",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "devops",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "verified",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "company",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "operator_only",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "first_time_user",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "role_source",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "disabled",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "0142d9dc9c1b57487dd5709a0376794f18d33e5bd6340c0189be7818cda64328"
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow\n (workspace_id, path, summary, description, archived, extra_perms, dependency_job, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, labels)\n SELECT workspace_id, $1, summary, description, archived, extra_perms, dependency_job, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, labels\n FROM flow\n WHERE path = $2 AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "03547bf921bbd4342dc8604277057336378f9e85baf14d1dc34ed7e9b42e5e72"
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM skip_workspace_diff_tally WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "03c8a797ae734ff76e227259ae011ef6d35f892fe95448c58ec13dea58eee3fa"
|
||||
}
|
||||
+1
-2
@@ -34,8 +34,7 @@
|
||||
"google",
|
||||
"ci_test",
|
||||
"github",
|
||||
"azure",
|
||||
"asset"
|
||||
"azure"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM v2_job WHERE workspace_id = $1 AND trigger_kind = 'asset'",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "09095af7cad650fb10781d9e39b0dad250c59ed0fca6cab7b5be4ee2516275d0"
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT path FROM flow WHERE workspace_id = $1 AND path = ANY($2) AND archived = false",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "0a568f630e069118fe302099a709e89cc4a702158899f7fdc66d0922e8fb9b29"
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO script\n (workspace_id, hash, path, parent_hashes, summary, description, content, created_by, schema, is_template, extra_perms, lock, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, delete_after_secs, timeout, concurrency_key, visible_to_runner_only, auto_kind, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle, modules, labels)\n\n SELECT workspace_id, $1, path, array_prepend($2::bigint, COALESCE(parent_hashes, '{}'::bigint[])), summary, description, content, created_by, schema, is_template, extra_perms, NULL, language, kind, tag, draft_only, envs, concurrent_limit, concurrency_time_window_s, cache_ttl, cache_ignore_s3_path, dedicated_worker, ws_error_handler_muted, priority, restart_unless_cancelled, delete_after_use, delete_after_secs, timeout, concurrency_key, visible_to_runner_only, auto_kind, codebase, has_preprocessor, on_behalf_of_email, schema_validation, assets, debounce_key, debounce_delay_s, runnable_settings_handle, modules, labels\n\n FROM script WHERE hash = $2 AND workspace_id = $3;\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0ca770234f3e38be3fb1c280d82e9c06440168806fe605e38808bdcb400d4034"
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO join_pending_inputs\n (workspace_id, subscriber_path, partition, trigger_ref)\n VALUES ($1, $2, $3, $4)\n ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0e7fe0e1d7aa2072a3431d081080bbc18da7e1ed758cab017fba2598c9467b7f"
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT email, login_type::text, verified, super_admin, devops, name, company, username, NULL::bool as operator_only, first_time_user, role_source, disabled FROM password ORDER BY super_admin DESC, devops DESC, email LIMIT $1 OFFSET $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "login_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "verified",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "super_admin",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "devops",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "company",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "operator_only",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "first_time_user",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "role_source",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "disabled",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "115a9cb44d0a41952c08dc36e0331410d32a8d672cfa4929e9e3763c51daa1bc"
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM workspace_diff\n WHERE source_workspace_id = 'wm-fork-test-workspace'\n OR fork_workspace_id = 'wm-fork-test-workspace'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "126cf6d54f8d2c916cd799f6663892119a94d66637062d1bf5a5fe97d89f8096"
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT email as \"email!\", login_type::text, verified as \"verified!\", super_admin as \"super_admin!\", devops as \"devops!\", name, company, username, NULL::bool as operator_only, first_time_user as \"first_time_user!\", role_source as \"role_source!\", disabled as \"disabled!\", NULL::text as workspace_id FROM password\n UNION ALL\n SELECT email as \"email!\", 'service_account'::text as login_type, true as \"verified!\", false as \"super_admin!\", false as \"devops!\", NULL::text as name, NULL::text as company, username, true as operator_only, false as \"first_time_user!\", 'service_account'::text as \"role_source!\", disabled as \"disabled!\", workspace_id\n FROM usr\n WHERE is_service_account IS true\n ORDER BY \"super_admin!\" DESC, \"devops!\" DESC, \"email!\"\n LIMIT $1 OFFSET $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "email!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "login_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "verified!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "super_admin!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "devops!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "company",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "operator_only",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "first_time_user!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "role_source!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "disabled!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "16b4496c21d0619dab4521dca22e5fe144c59156a8f06d8592291684c49b2f37"
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT DISTINCT ON (path)\n path,\n value as \"value!: sqlx::types::Json<Box<serde_json::value::RawValue>>\",\n created_at,\n typ::text as \"typ!\"\n FROM draft\n WHERE workspace_id = $1\n AND typ IN ('app', 'raw_app')\n AND (email = $2 OR email IS NULL)\n AND NOT EXISTS (\n SELECT 1 FROM app a\n WHERE a.workspace_id = draft.workspace_id\n AND a.path = draft.path\n )\n ORDER BY path, (email IS NULL), created_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "value!: sqlx::types::Json<Box<serde_json::value::RawValue>>",
|
||||
"type_info": "Json"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "typ!",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "1860c102a5309a8e1bb7288d0616f48e4330af7fbe61426e999c6073f630d88f"
|
||||
}
|
||||
+2
-2
@@ -5,7 +5,7 @@
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id!",
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
@@ -16,7 +16,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "19513c4158267cc7fe10d999ad571052c112e6bbb3cf834f16176cbb7e1ac319"
|
||||
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT kind::text as \"kind!\", parent_job, runnable_path\n FROM v2_job WHERE id = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "kind!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "parent_job",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "runnable_path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "19f1cb1c7a1974920549917a6392ff0d56f31ca5662062f4a71fed0ccf859cc4"
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH del AS (\n DELETE FROM asset WHERE workspace_id = $1 AND usage_path = $2 AND usage_kind = $3\n RETURNING usage_access_type\n )\n INSERT INTO notify_event (channel, payload)\n SELECT 'notify_asset_producer_change', $1\n WHERE $3 = 'script'\n AND EXISTS (SELECT 1 FROM del WHERE usage_access_type IN ('w', 'rw'))",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "asset_usage_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"flow",
|
||||
"job"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1acfeed9c7a5b1e3d2da262d338655dba6e43067a9912cc2b775830856390c5d"
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH del AS (\n DELETE FROM asset WHERE workspace_id = $1 AND usage_kind = 'script'\n AND usage_path = (SELECT path FROM script WHERE hash = $2 AND workspace_id = $1)\n RETURNING usage_access_type\n )\n INSERT INTO notify_event (channel, payload)\n SELECT 'notify_asset_producer_change', $1\n WHERE EXISTS (SELECT 1 FROM del WHERE usage_access_type IN ('w', 'rw'))",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1f375b37ff9f6f01972e284e84a7b2f9d2d323a3da55f20ff6671e8eba510043"
|
||||
}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO draft (workspace_id, path, typ, value, created_at, email)\n SELECT $2, path, typ, value, created_at, email\n FROM draft\n WHERE workspace_id = $1 AND (email = $3 OR email IS NULL)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1f486036b2902a0ffa0ce15f665d82c85129e2407819477adf7e87f097276ae2"
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO v2_job (id, workspace_id, kind, runnable_path, args, created_by,\n permissioned_as, permissioned_as_email, tag, script_lang)\n VALUES ($1, $2, 'script'::job_kind, $3, $4, 'test-user',\n 'u/test-user', 'test@windmill.dev', 'deno', 'bash'::script_lang)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1fc04d31ae69dbb1df9c63cb69e83e8f8e6770b78f6ed052b99afed6cea28650"
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT kind, path, script_path, is_flow FROM (\n SELECT 'schedule' AS kind, path, script_path, is_flow FROM schedule\n WHERE workspace_id = $1\n AND script_path IS NOT NULL\n UNION ALL\n SELECT 'email', path, script_path, is_flow FROM email_trigger\n WHERE workspace_id = $1\n UNION ALL\n SELECT 'kafka', path, script_path, is_flow FROM kafka_trigger\n WHERE workspace_id = $1\n UNION ALL\n SELECT 'mqtt', path, script_path, is_flow FROM mqtt_trigger\n WHERE workspace_id = $1\n UNION ALL\n SELECT 'nats', path, script_path, is_flow FROM nats_trigger\n WHERE workspace_id = $1\n UNION ALL\n SELECT 'postgres', path, script_path, is_flow FROM postgres_trigger\n WHERE workspace_id = $1\n UNION ALL\n SELECT 'sqs', path, script_path, is_flow FROM sqs_trigger\n WHERE workspace_id = $1\n UNION ALL\n SELECT 'gcp', path, script_path, is_flow FROM gcp_trigger\n WHERE workspace_id = $1\n ) t\n WHERE ($2::text IS NULL OR script_path LIKE $2)\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "kind",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "script_path",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "is_flow",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "2484323d94f249be30f4472ece89659c1e6a24d5454a691e31e0179f58c24366"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM job_perms\n WHERE ctid IN (\n SELECT jp.ctid FROM job_perms jp\n WHERE NOT EXISTS (SELECT 1 FROM v2_job_queue q WHERE q.id = jp.job_id)\n LIMIT 100000\n )",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "25ecae25ebc03d6296b0e72482a9201c2bffb0f0f7419b0b598b2786bdb326ab"
|
||||
}
|
||||
+3
-9
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT workspace_id, name, display_name, owners, extra_perms, summary, created_by, edited_at, default_permissioned_as, labels FROM folder WHERE name = $1 AND workspace_id = $2",
|
||||
"query": "SELECT workspace_id, name, display_name, owners, extra_perms, summary, created_by, edited_at, default_permissioned_as FROM folder WHERE name = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -47,11 +47,6 @@
|
||||
"ordinal": 8,
|
||||
"name": "default_permissioned_as",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "labels",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -69,9 +64,8 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "42600bdeb6b86ac306ab276cb49241508776f5f998821e04bc92df917176cea6"
|
||||
"hash": "269197b692a1e451a31f14701c8ca324c8e40d2923dfa07b8e4b3934afc16fed"
|
||||
}
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n subscriber_path AS \"subscriber_path!\",\n asset_kind AS \"asset_kind!: windmill_common::assets::AssetKind\",\n asset_path AS \"asset_path!\",\n outcome::text AS \"outcome!\",\n child_job_id,\n partition,\n received_inputs,\n required_inputs,\n debounce_s,\n reason,\n created_at AS \"created_at!\"\n FROM dispatch_event\n WHERE producer_job_id = $1 AND workspace_id = $2\n ORDER BY id",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "subscriber_path!",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "asset_kind!: windmill_common::assets::AssetKind",
|
||||
"type_info": {
|
||||
"Custom": {
|
||||
"name": "asset_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"s3object",
|
||||
"resource",
|
||||
"variable",
|
||||
"ducklake",
|
||||
"datatable",
|
||||
"volume"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "asset_path!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "outcome!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "child_job_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "partition",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "received_inputs",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "required_inputs",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "debounce_s",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "reason",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "created_at!",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "26e63135fcd8e7d48e25de190a2f72ece70ec92c5b04baad2622639850445900"
|
||||
}
|
||||
+5
-4
@@ -1,22 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT deleted FROM workspace WHERE id = $1",
|
||||
"query": "SELECT draft_only FROM app WHERE path = $1 AND workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "deleted",
|
||||
"name": "draft_only",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "ebc8f4840335dc3f4c8274fcf106825ecfdf7fbca59ea26541c5b4c0350b5502"
|
||||
"hash": "27b0c827467cc92979f094620957bc0edfa295d6c2292e509a5536765d120bd8"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n path\n FROM\n flow_version\n WHERE\n id = $1 AND\n workspace_id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "285c136fc92ce63417e4c65e657d914a6e158d636b854248e4028ed35326f3c6"
|
||||
}
|
||||
+4
-4
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT policy->>'execution_mode' = 'anonymous' FROM app WHERE path = $1 AND workspace_id = $2 FOR UPDATE",
|
||||
"query": "SELECT draft_only FROM flow WHERE path = $1 AND workspace_id = $2 AND archived = false",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"name": "draft_only",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
@@ -16,8 +16,8 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "3e82929b365a6aa7ccc39fc5615c4110e1d740e0cb0509b1c58bc6636d83cafd"
|
||||
"hash": "28f1ecca40c8b81cc59dffb75e2913c889b374999ece04173b2e67dc74005f60"
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT label, token_prefix, expiration, created_at, last_used_at, scopes, workspace_id FROM token WHERE email = $1\n ORDER BY created_at DESC LIMIT $2 OFFSET $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "label",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "token_prefix",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "expiration",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "last_used_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "scopes",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "workspace_id",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "2b5fc0500beb2f4c7cf5997f9aea48f77e2abe4523180c507a9a90570127be6d"
|
||||
}
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM draft\n WHERE workspace_id = $1\n AND email IS NOT DISTINCT FROM (CASE WHEN $7::bool THEN NULL::text ELSE $2 END)\n AND path = $3\n AND typ = $4\n AND ($6::bool = true\n OR $5::timestamptz IS NULL\n OR created_at <= $5::timestamptz)\n RETURNING now() as \"now!\"",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "now!",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "draft_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"script",
|
||||
"flow",
|
||||
"app",
|
||||
"raw_app",
|
||||
"resource",
|
||||
"variable",
|
||||
"trigger_schedule",
|
||||
"trigger_webhook",
|
||||
"trigger_default_email",
|
||||
"trigger_email",
|
||||
"trigger_http",
|
||||
"trigger_websocket",
|
||||
"trigger_postgres",
|
||||
"trigger_kafka",
|
||||
"trigger_nats",
|
||||
"trigger_mqtt",
|
||||
"trigger_sqs",
|
||||
"trigger_gcp",
|
||||
"trigger_azure",
|
||||
"trigger_poll",
|
||||
"trigger_cli",
|
||||
"trigger_nextcloud",
|
||||
"trigger_google",
|
||||
"trigger_github"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Timestamptz",
|
||||
"Bool",
|
||||
"Bool"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "2bed492ef32edf36e60e8a03268fa25bfb67dd641153d1ca23f7d0d2ae73624e"
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n path\n FROM\n flow_version\n WHERE\n id = $1 AND\n workspace_id = $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "path",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "311de4a5d2fb3066dc9e49693b9a1dd8e8e4a09200768a73c844810975741894"
|
||||
}
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM asset\n WHERE id IN (\n SELECT id FROM (\n SELECT a.id, ROW_NUMBER() OVER (\n PARTITION BY a.workspace_id, a.path, a.kind\n ORDER BY a.created_at DESC\n ) as rn,\n limits.max_n\n FROM asset a\n INNER JOIN (\n SELECT * FROM UNNEST(\n $1::varchar[],\n $2::varchar[],\n $3::asset_kind[],\n $4::int[]\n ) AS t(workspace_id, path, kind, max_n)\n ) limits\n ON a.workspace_id = limits.workspace_id\n AND a.path = limits.path\n AND a.kind = limits.kind\n WHERE a.usage_kind = 'job'\n ) ranked\n WHERE rn > max_n\n )",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"VarcharArray",
|
||||
"VarcharArray",
|
||||
{
|
||||
"Custom": {
|
||||
"name": "asset_kind[]",
|
||||
"kind": {
|
||||
"Array": {
|
||||
"Custom": {
|
||||
"name": "asset_kind",
|
||||
"kind": {
|
||||
"Enum": [
|
||||
"s3object",
|
||||
"resource",
|
||||
"variable",
|
||||
"ducklake",
|
||||
"datatable",
|
||||
"volume"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Int4Array"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "31bbd03932912df069cfc97fd1ca8c69a3151266e2bf475da10feb4916e436e9"
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT COUNT(*) FROM skip_workspace_diff_tally WHERE workspace_id = 'wm-fork-test-workspace'",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "353c6a648720e118c0d82ef055c60033f1969fb39155e7e07bb6730505e254b7"
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow\n (workspace_id, path, summary, description, archived, extra_perms, dependency_job, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, lock_error_logs)\n SELECT $1, path, summary, description, archived, extra_perms, dependency_job, tag, ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only, on_behalf_of_email, concurrency_key, versions, value, schema, edited_by, edited_at, lock_error_logs\n FROM flow WHERE workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "355e3976f0a7e3ccbb24314f951ff37dcc4c34f800ff4f385ec9f60933f83efe"
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO flow (\n workspace_id, path, summary, description, value, edited_by, edited_at,\n archived, schema, extra_perms, dependency_job, tag,\n ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only,\n concurrency_key, versions, on_behalf_of_email, lock_error_logs\n )\n SELECT $2, path, summary, description, value, edited_by, edited_at,\n archived, schema, extra_perms, NULL, tag,\n ws_error_handler_muted, dedicated_worker, timeout, visible_to_runner_only,\n concurrency_key, ARRAY[]::bigint[], on_behalf_of_email, lock_error_logs\n FROM flow\n WHERE workspace_id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "388b6af24fc77341abf7088303f1b027c022fc034dc378a071c046dc64571ac6"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user