Compare commits

...

7 Commits

Author SHA1 Message Date
hugocasa
9ad2d3e59d fix: skip nsjail uidmap/gidmap when DISABLE_NUSER=true
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:47:44 +02:00
Ruben Fiszel
a3f24aeff8 sqlx 2026-04-15 15:14:44 +00:00
centdix
f1e84cb088 chore: add backend preview validation to ai evals (#8827)
* feat: add backend preview validation to ai evals

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: refresh shared preview workspace assets

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: harden shared backend preview validation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-15 15:11:25 +00:00
Ruben Fiszel
3aa279cfd7 nit tx commit cj 2026-04-15 12:05:11 +00:00
centdix
5c179e5448 fix: preserve gemini thought signatures in ai chat (#8837)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-15 11:49:57 +00:00
Diego Imbert
12d0a3de08 fix: parse assets on inline script module creation to avoid false toast (#8835)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:01:07 +00:00
Ruben Fiszel
a98f5b9dfd chore(main): release 1.684.1 (#8834)
* chore(main): release 1.684.1

* Apply automatic changes

---------

Co-authored-by: rubenfiszel <275584+rubenfiszel@users.noreply.github.com>
2026-04-14 21:48:03 +00:00
48 changed files with 1561 additions and 237 deletions

View File

@@ -1,5 +1,12 @@
# Changelog
## [1.684.1](https://github.com/windmill-labs/windmill/compare/v1.684.0...v1.684.1) (2026-04-14)
### Bug Fixes
* stop escalating missing email recipients to critical alert ([#8833](https://github.com/windmill-labs/windmill/issues/8833)) ([6158ff2](https://github.com/windmill-labs/windmill/commit/6158ff2ebe29d6a9a7ff4d524e152bb2f7c24dfc))
## [1.684.0](https://github.com/windmill-labs/windmill/compare/v1.683.2...v1.684.0) (2026-04-14)

View File

@@ -55,6 +55,7 @@ bun run cli -- run flow flow-test4-order-processing-loop --model opus
bun run cli -- run flow flow-test0-sum-two-numbers --models haiku,opus,4o
bun run cli -- run flow flow-test0-sum-two-numbers --runs 3 --verbose
bun run cli -- run flow --record
WMILL_AI_EVAL_BACKEND_URL=http://127.0.0.1:8000 bun run cli -- run flow --backend-validation preview
bun run cli -- run cli bun-hello-script
```
@@ -72,6 +73,7 @@ Public CLI surface:
- `--models <a,b,c>`: run the same cases sequentially against several model aliases
- `--verbose`: stream assistant output for frontend runs
- `--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
## Models
@@ -114,6 +116,7 @@ Optional fields:
- `initial`: starting state fixture
- `expected`: expected artifact fixture
- `validate`: extra deterministic validation rules
- `runtime.backendPreview`: optional real backend preview config for smoke validation
For `flow` mode, `validate` can express requirements such as:
@@ -125,6 +128,23 @@ For `flow` mode, an `initial` fixture can also include a benchmark workspace cat
existing scripts and flows. That lets the real `search_workspace` and
`get_runnable_details` tools discover reusable workspace runnables during evals.
If `--backend-validation preview` is enabled:
- `script` evals run a real backend script preview in an isolated temp workspace
- `flow` evals run a real backend flow preview only for cases that define `runtime.backendPreview`
- `flow` cases with `initial.workspace` fixtures seed those scripts and flows into the preview workspace before preview
- when `WMILL_AI_EVAL_BACKEND_WORKSPACE` is set, `ai_evals` treats that workspace as a dedicated test workspace, clears managed eval assets under `f/evals/*` before each preview run, and then reseeds the current case fixtures
Supported backend validation env vars:
- `WMILL_AI_EVAL_BACKEND_VALIDATION=preview`
- `WMILL_AI_EVAL_BACKEND_URL=http://127.0.0.1:8000`
- `WMILL_AI_EVAL_BACKEND_EMAIL=admin@windmill.dev`
- `WMILL_AI_EVAL_BACKEND_PASSWORD=changeme`
- `WMILL_AI_EVAL_BACKEND_WORKSPACE=integration-tests` to reuse an existing workspace on CE installs with low workspace limits
- `WMILL_AI_EVAL_KEEP_WORKSPACES=1`
- `WMILL_AI_EVAL_WORKSPACE_PREFIX=ai-evals`
## Results And Artifacts
Every run writes:
@@ -158,6 +178,7 @@ Typical artifacts by mode:
- `script`: `script.json` plus the generated script file
- `app`: `app.json` plus frontend/backend files
- `cli`: `assistant-output.txt` plus generated workspace files
- backend-validated attempts also include `backend-preview.json`
## Layout

View File

@@ -0,0 +1,246 @@
import { afterEach, describe, expect, it } from 'bun:test'
import type { BackendValidationSettings } from '../../core/backendValidation'
import { BackendPreviewClient } from './backendPreview'
const ORIGINAL_FETCH = globalThis.fetch
afterEach(() => {
globalThis.fetch = ORIGINAL_FETCH
})
describe('BackendPreviewClient', () => {
it('updates an existing seeded script on path conflict and waits for deployment', async () => {
const requests: Array<{ url: string; init?: RequestInit }> = []
globalThis.fetch = mockFetch(
requests,
textResponse(200, 'token'),
textResponse(200, ''),
textResponse(400, 'Path conflict for f/evals/add_two_numbers with non-archived hash 123'),
jsonResponse(200, { hash: '123' }),
textResponse(200, '456'),
jsonResponse(200, { lock: 'script.lock', lock_error_logs: null })
)
const client = new BackendPreviewClient(
buildSettings({ baseUrl: 'http://backend.test/script-upsert' })
)
await client.createScript({
workspaceId: 'test',
path: 'f/evals/add_two_numbers',
summary: 'Add two numbers',
content: 'export async function main(a: number, b: number) { return a + b }',
language: 'bun'
})
expect(requests.map((entry) => entry.url)).toEqual([
'http://backend.test/script-upsert/api/auth/login',
'http://backend.test/script-upsert/api/w/test/folders/create',
'http://backend.test/script-upsert/api/w/test/scripts/create',
'http://backend.test/script-upsert/api/w/test/scripts/get/p/f/evals/add_two_numbers',
'http://backend.test/script-upsert/api/w/test/scripts/create',
'http://backend.test/script-upsert/api/w/test/scripts/deployment_status/h/456'
])
const updateRequest = requests[4]
expect(updateRequest.init?.method).toBe('POST')
expect(JSON.parse(String(updateRequest.init?.body))).toMatchObject({
path: 'f/evals/add_two_numbers',
parent_hash: '123',
language: 'bun'
})
})
it('updates an existing seeded flow on create conflict', async () => {
const requests: Array<{ url: string; init?: RequestInit }> = []
globalThis.fetch = mockFetch(
requests,
textResponse(200, 'token'),
textResponse(200, ''),
textResponse(400, 'Flow f/evals/add_numbers_flow already exists'),
textResponse(200, '')
)
const client = new BackendPreviewClient(
buildSettings({ baseUrl: 'http://backend.test/flow-upsert' })
)
await client.createFlow({
workspaceId: 'test',
path: 'f/evals/add_numbers_flow',
summary: 'Add numbers',
value: { modules: [] }
})
expect(requests.map((entry) => entry.url)).toEqual([
'http://backend.test/flow-upsert/api/auth/login',
'http://backend.test/flow-upsert/api/w/test/folders/create',
'http://backend.test/flow-upsert/api/w/test/flows/create',
'http://backend.test/flow-upsert/api/w/test/flows/update/f/evals/add_numbers_flow'
])
const updateRequest = requests[3]
expect(updateRequest.init?.method).toBe('POST')
expect(JSON.parse(String(updateRequest.init?.body))).toMatchObject({
path: 'f/evals/add_numbers_flow',
value: { modules: [] }
})
})
it('serializes shared-workspace validations inside the overridden workspace', async () => {
globalThis.fetch = async (input) => {
const url = String(input)
if (url.endsWith('/api/auth/login')) {
return textResponse(200, 'token')
}
if (url.endsWith('/api/workspaces/exists')) {
return textResponse(200, 'true')
}
if (url.endsWith('/api/w/shared-preview/flows/list_paths')) {
return jsonResponse(200, [])
}
if (url.endsWith('/api/w/shared-preview/scripts/list_paths')) {
return jsonResponse(200, [])
}
throw new Error(`Unexpected fetch: ${url}`)
}
const client = new BackendPreviewClient(
buildSettings({
baseUrl: 'http://backend.test/shared-lock',
workspaceOverride: 'shared-preview'
})
)
const order: string[] = []
let releaseFirst: (() => void) | undefined
let notifyFirstStart: (() => void) | undefined
const firstStarted = new Promise<void>((resolve) => {
notifyFirstStart = resolve
})
const first = client.withWorkspace('flow-test1', 1, async () => {
order.push('first:start')
notifyFirstStart?.()
await new Promise<void>((resolve) => {
releaseFirst = resolve
})
order.push('first:end')
})
const second = client.withWorkspace('flow-test2', 1, async () => {
order.push('second:start')
order.push('second:end')
})
await firstStarted
expect(order).toEqual(['first:start'])
releaseFirst?.()
await Promise.all([first, second])
expect(order).toEqual(['first:start', 'first:end', 'second:start', 'second:end'])
})
it('clears managed shared-workspace assets before preview runs', async () => {
const requests: Array<{ url: string; init?: RequestInit }> = []
globalThis.fetch = mockFetch(
requests,
textResponse(200, 'token'),
textResponse(200, 'true'),
jsonResponse(200, ['f/evals/old_subflow', 'u/admin/keep_flow']),
textResponse(200, ''),
jsonResponse(200, ['f/evals/old_script', 'f/shared/keep_script']),
textResponse(200, '')
)
const client = new BackendPreviewClient(
buildSettings({
baseUrl: 'http://backend.test/shared-cleanup',
workspaceOverride: 'shared-preview'
})
)
await client.withWorkspace('flow-test1', 1, async () => undefined)
expect(requests.map((entry) => entry.url)).toEqual([
'http://backend.test/shared-cleanup/api/auth/login',
'http://backend.test/shared-cleanup/api/workspaces/exists',
'http://backend.test/shared-cleanup/api/w/shared-preview/flows/list_paths',
'http://backend.test/shared-cleanup/api/w/shared-preview/flows/delete/f/evals/old_subflow',
'http://backend.test/shared-cleanup/api/w/shared-preview/scripts/list_paths',
'http://backend.test/shared-cleanup/api/w/shared-preview/scripts/delete/p/f/evals/old_script'
])
})
it('retries login after a cached login failure', async () => {
const requests: Array<{ url: string; init?: RequestInit }> = []
globalThis.fetch = mockFetch(
requests,
textResponse(503, 'backend starting'),
textResponse(200, 'token'),
textResponse(200, 'true'),
jsonResponse(200, []),
jsonResponse(200, [])
)
const client = new BackendPreviewClient(
buildSettings({
baseUrl: 'http://backend.test/login-retry',
workspaceOverride: 'shared-preview'
})
)
await expect(client.withWorkspace('flow-test1', 1, async () => undefined)).rejects.toThrow(
'login for backend validation failed'
)
await expect(client.withWorkspace('flow-test1', 1, async () => 'ok')).resolves.toBe('ok')
expect(
requests.filter((entry) => entry.url === 'http://backend.test/login-retry/api/auth/login')
).toHaveLength(2)
})
})
function buildSettings(
overrides: Partial<BackendValidationSettings> = {}
): BackendValidationSettings {
return {
mode: 'preview',
baseUrl: 'http://backend.test/default',
email: 'admin@windmill.dev',
password: 'changeme',
keepWorkspaces: true,
workspacePrefix: 'ai-evals',
pollIntervalMs: 1,
maxWaitMs: 50,
...overrides
}
}
function mockFetch(
requests: Array<{ url: string; init?: RequestInit }>,
...responses: Response[]
): typeof fetch {
const queue = [...responses]
return async (input, init) => {
const url = String(input)
requests.push({ url, init })
const next = queue.shift()
if (!next) {
throw new Error(`Unexpected fetch: ${url}`)
}
return next
}
}
function jsonResponse(status: number, body: unknown): Response {
return new Response(JSON.stringify(body), {
status,
headers: { 'Content-Type': 'application/json' }
})
}
function textResponse(status: number, body: string): Response {
return new Response(body, { status })
}

View File

@@ -0,0 +1,502 @@
import { randomUUID } from 'node:crypto'
import type { BackendValidationSettings } from '../../core/backendValidation'
interface CompletedJobResultMaybe {
completed: boolean
result: unknown
success?: boolean
started?: boolean
}
interface ScriptDeploymentStatus {
lock?: unknown
lock_error_logs?: string | null
}
export interface CompletedPreviewJob {
id: string
success: boolean
result: unknown
logs?: string | null
raw: Record<string, unknown>
}
const tokenCache = new Map<string, Promise<string>>()
const sharedWorkspaceQueue = new Map<string, Promise<void>>()
const managedSharedWorkspacePrefixes = ['f/evals/']
export class BackendPreviewClient {
constructor(private readonly settings: BackendValidationSettings) {}
async withWorkspace<T>(
caseId: string,
attempt: number,
body: (workspaceId: string) => Promise<T>
): Promise<T> {
const workspaceId =
this.settings.workspaceOverride ??
buildWorkspaceId(this.settings.workspacePrefix, caseId, attempt)
const run = async () => {
await this.ensureWorkspace(workspaceId)
if (this.settings.workspaceOverride) {
await this.clearManagedSharedWorkspaceAssets(workspaceId)
}
try {
return await body(workspaceId)
} finally {
if (!this.settings.keepWorkspaces && !this.settings.workspaceOverride) {
await this.deleteWorkspace(workspaceId).catch(() => undefined)
}
}
}
if (this.settings.workspaceOverride) {
return await withSharedWorkspaceLock(workspaceId, run)
}
return await run()
}
async createScript(input: {
workspaceId: string
path: string
summary: string
description?: string
schema?: Record<string, unknown>
content: string
language: string
}): Promise<void> {
await this.ensureFolderForPath(input.workspaceId, input.path)
const payload = {
path: input.path,
summary: input.summary,
description: input.description ?? '',
content: input.content,
schema: input.schema ?? { type: 'object', properties: {}, required: [] },
is_template: false,
language: input.language,
kind: 'script'
}
const response = await this.request(`/w/${encodeURIComponent(input.workspaceId)}/scripts/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
if (response.ok) {
await this.waitForScriptDeployment(input.workspaceId, input.path, (await response.text()).trim())
return
}
const message = await response.text()
if (!isConflictMessage(message)) {
throw new Error(`create script ${input.path} failed: ${response.status} ${response.statusText} - ${message}`)
}
const currentScript = await this.getScriptByPath(input.workspaceId, input.path)
const currentHash = readStringField(currentScript, 'hash', `script ${input.path}`)
const updateResponse = await this.request(
`/w/${encodeURIComponent(input.workspaceId)}/scripts/create`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...payload,
parent_hash: currentHash
})
}
)
await expectOk(updateResponse, `update script ${input.path}`)
await this.waitForScriptDeployment(input.workspaceId, input.path, (await updateResponse.text()).trim())
}
async createFlow(input: {
workspaceId: string
path: string
summary: string
description?: string
schema?: Record<string, unknown>
value: Record<string, unknown>
}): Promise<void> {
await this.ensureFolderForPath(input.workspaceId, input.path)
const payload = {
path: input.path,
summary: input.summary,
description: input.description ?? '',
schema: input.schema ?? { type: 'object', properties: {}, required: [] },
value: input.value
}
const response = await this.request(`/w/${encodeURIComponent(input.workspaceId)}/flows/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
if (response.ok) {
return
}
const message = await response.text()
if (!isConflictMessage(message)) {
throw new Error(`create flow ${input.path} failed: ${response.status} ${response.statusText} - ${message}`)
}
const updateResponse = await this.request(
`/w/${encodeURIComponent(input.workspaceId)}/flows/update/${input.path}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}
)
await expectOk(updateResponse, `update flow ${input.path}`)
}
async runScriptPreview(input: {
workspaceId: string
content: string
args: Record<string, unknown>
language: string
path?: string
timeoutSeconds?: number
}): Promise<CompletedPreviewJob> {
const response = await this.request(
withQuery(`/w/${encodeURIComponent(input.workspaceId)}/jobs/run/preview`, {
timeout: input.timeoutSeconds
}),
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: input.content,
args: input.args,
language: input.language,
path: input.path
})
}
)
await expectOk(response, 'start script preview')
const jobId = (await response.text()).trim()
return await this.waitForCompletedJob(input.workspaceId, jobId)
}
async runFlowPreview(input: {
workspaceId: string
value: Record<string, unknown>
args: Record<string, unknown>
timeoutSeconds?: number
path?: string
}): Promise<CompletedPreviewJob> {
const response = await this.request(
withQuery(`/w/${encodeURIComponent(input.workspaceId)}/jobs/run/preview_flow`, {
timeout: input.timeoutSeconds
}),
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
value: input.value,
args: input.args,
path: input.path
})
}
)
await expectOk(response, 'start flow preview')
const jobId = (await response.text()).trim()
return await this.waitForCompletedJob(input.workspaceId, jobId)
}
private async ensureWorkspace(workspaceId: string): Promise<void> {
const existsResponse = await this.request('/workspaces/exists', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: workspaceId })
})
await expectOk(existsResponse, `check workspace ${workspaceId}`)
if ((await existsResponse.text()).trim() === 'true') {
return
}
const createResponse = await this.request('/workspaces/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: workspaceId, name: workspaceId })
})
try {
await expectOk(createResponse, `create workspace ${workspaceId}`)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
if (message.includes('maximum number of workspaces')) {
throw new Error(
`${message}. Reuse an existing workspace with WMILL_AI_EVAL_BACKEND_WORKSPACE=<workspace-id>.`
)
}
throw error
}
}
private async deleteWorkspace(workspaceId: string): Promise<void> {
const response = await this.request(`/workspaces/delete/${encodeURIComponent(workspaceId)}`, {
method: 'DELETE'
})
await expectOk(response, `delete workspace ${workspaceId}`)
}
private async ensureFolderForPath(workspaceId: string, path: string): Promise<void> {
const folderName = extractFolderName(path)
if (!folderName) {
return
}
const response = await this.request(`/w/${encodeURIComponent(workspaceId)}/folders/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: folderName })
})
if (response.ok) {
return
}
const message = await response.text()
if (!message.toLowerCase().includes('already exists')) {
throw new Error(`Failed to create folder ${folderName}: ${message}`)
}
}
private async waitForCompletedJob(
workspaceId: string,
jobId: string
): Promise<CompletedPreviewJob> {
const deadline = Date.now() + this.settings.maxWaitMs
while (Date.now() < deadline) {
const maybeResponse = await this.request(
`/w/${encodeURIComponent(workspaceId)}/jobs_u/completed/get_result_maybe/${encodeURIComponent(jobId)}?get_started=false`
)
await expectOk(maybeResponse, `poll job ${jobId}`)
const maybeResult = (await maybeResponse.json()) as CompletedJobResultMaybe
if (maybeResult.completed) {
const completedResponse = await this.request(
`/w/${encodeURIComponent(workspaceId)}/jobs_u/completed/get/${encodeURIComponent(jobId)}`
)
await expectOk(completedResponse, `get completed job ${jobId}`)
const completedJob = (await completedResponse.json()) as Record<string, unknown>
return {
id: jobId,
success: Boolean(maybeResult.success),
result: maybeResult.result,
logs:
typeof completedJob.logs === 'string' || completedJob.logs === null
? (completedJob.logs as string | null)
: null,
raw: completedJob
}
}
await new Promise((resolve) => setTimeout(resolve, this.settings.pollIntervalMs))
}
throw new Error(`Timed out waiting for preview job ${jobId} to complete`)
}
private async getScriptByPath(workspaceId: string, path: string): Promise<Record<string, unknown>> {
const response = await this.request(`/w/${encodeURIComponent(workspaceId)}/scripts/get/p/${path}`)
await expectOk(response, `get script ${path}`)
return (await response.json()) as Record<string, unknown>
}
private async clearManagedSharedWorkspaceAssets(workspaceId: string): Promise<void> {
const flowPaths = await this.listFlowPaths(workspaceId)
for (const path of flowPaths.filter(isManagedSharedWorkspacePath)) {
await this.deleteFlowByPath(workspaceId, path)
}
const scriptPaths = await this.listScriptPaths(workspaceId)
for (const path of scriptPaths.filter(isManagedSharedWorkspacePath)) {
await this.deleteScriptByPath(workspaceId, path)
}
}
private async listFlowPaths(workspaceId: string): Promise<string[]> {
const response = await this.request(`/w/${encodeURIComponent(workspaceId)}/flows/list_paths`)
await expectOk(response, `list flows in workspace ${workspaceId}`)
return await response.json()
}
private async listScriptPaths(workspaceId: string): Promise<string[]> {
const response = await this.request(`/w/${encodeURIComponent(workspaceId)}/scripts/list_paths`)
await expectOk(response, `list scripts in workspace ${workspaceId}`)
return await response.json()
}
private async deleteFlowByPath(workspaceId: string, path: string): Promise<void> {
const response = await this.request(`/w/${encodeURIComponent(workspaceId)}/flows/delete/${path}`, {
method: 'DELETE'
})
await expectOk(response, `delete flow ${path}`)
}
private async deleteScriptByPath(workspaceId: string, path: string): Promise<void> {
const response = await this.request(`/w/${encodeURIComponent(workspaceId)}/scripts/delete/p/${path}`, {
method: 'POST'
})
await expectOk(response, `delete script ${path}`)
}
private async waitForScriptDeployment(
workspaceId: string,
path: string,
hash: string
): Promise<void> {
const deadline = Date.now() + this.settings.maxWaitMs
while (Date.now() < deadline) {
const response = await this.request(
`/w/${encodeURIComponent(workspaceId)}/scripts/deployment_status/h/${encodeURIComponent(hash)}`
)
await expectOk(response, `check deployment status for script ${path}`)
const deployment = (await response.json()) as ScriptDeploymentStatus
if (deployment.lock != null) {
return
}
if (deployment.lock_error_logs) {
throw new Error(`Script deployment failed for ${path}: ${deployment.lock_error_logs}`)
}
await new Promise((resolve) => setTimeout(resolve, this.settings.pollIntervalMs))
}
throw new Error(`Timed out waiting for script ${path} (${hash}) to deploy`)
}
private async request(path: string, init?: RequestInit): Promise<Response> {
const token = await this.getToken()
return await fetch(`${this.settings.baseUrl}/api${path}`, {
...init,
headers: {
Authorization: `Bearer ${token}`,
...(init?.headers ?? {})
}
})
}
private async getToken(): Promise<string> {
const cacheKey = `${this.settings.baseUrl}|${this.settings.email}`
let tokenPromise = tokenCache.get(cacheKey)
if (!tokenPromise) {
tokenPromise = this.login().catch((error) => {
if (tokenCache.get(cacheKey) === tokenPromise) {
tokenCache.delete(cacheKey)
}
throw error
})
tokenCache.set(cacheKey, tokenPromise)
}
return await tokenPromise
}
private async login(): Promise<string> {
const response = await fetch(`${this.settings.baseUrl}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: this.settings.email,
password: this.settings.password
})
})
await expectOk(response, 'login for backend validation')
return (await response.text()).trim()
}
}
async function withSharedWorkspaceLock<T>(workspaceId: string, body: () => Promise<T>): Promise<T> {
const previous = sharedWorkspaceQueue.get(workspaceId) ?? Promise.resolve()
let releaseCurrent: (() => void) | undefined
const current = new Promise<void>((resolve) => {
releaseCurrent = resolve
})
const tail = previous.catch(() => undefined).then(() => current)
sharedWorkspaceQueue.set(workspaceId, tail)
await previous.catch(() => undefined)
try {
return await body()
} finally {
releaseCurrent?.()
if (sharedWorkspaceQueue.get(workspaceId) === tail) {
sharedWorkspaceQueue.delete(workspaceId)
}
}
}
function buildWorkspaceId(prefix: string, caseId: string, attempt: number): string {
const caseSlug = caseId
.toLowerCase()
.replace(/[^a-z0-9-]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 30)
const suffix = randomUUID().slice(0, 8)
return `${prefix}-${caseSlug || 'case'}-a${attempt}-${suffix}`
}
function extractFolderName(path: string): string | null {
if (!path.startsWith('f/')) {
return null
}
const segments = path.split('/').slice(1, -1)
return segments.length > 0 ? segments.join('/') : null
}
function withQuery(
path: string,
params: Record<string, string | number | undefined>
): string {
const query = new URLSearchParams()
for (const [key, value] of Object.entries(params)) {
if (value === undefined) {
continue
}
query.set(key, String(value))
}
const suffix = query.toString()
return suffix ? `${path}?${suffix}` : path
}
async function expectOk(response: Response, context: string): Promise<void> {
if (response.ok) {
return
}
throw new Error(`${context} failed: ${response.status} ${response.statusText} - ${await response.text()}`)
}
function readStringField(
value: Record<string, unknown>,
field: string,
context: string
): string {
const candidate = value[field]
if (typeof candidate === 'string' && candidate.length > 0) {
return candidate
}
throw new Error(`${context} is missing string field ${field}`)
}
function isConflictMessage(message: string): boolean {
const normalized = message.toLowerCase()
return normalized.includes('already exists') || normalized.includes('path conflict')
}
function isManagedSharedWorkspacePath(path: string): boolean {
return managedSharedWorkspacePrefixes.some((prefix) => path.startsWith(prefix))
}

View File

@@ -1,4 +1,5 @@
import { loadSelectedCases } from "../../core/cases";
import { resolveBackendValidationSettings } from "../../core/backendValidation";
import {
formatRunModelLabel,
getFrontendEvalModel,
@@ -22,9 +23,13 @@ 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 model = resolveEvalModel(mode, process.env.WMILL_FRONTEND_AI_EVAL_MODEL);
const backendValidation = resolveBackendValidationSettings({
evalMode: mode,
requestedMode: process.env.WMILL_FRONTEND_AI_EVAL_BACKEND_VALIDATION,
});
const selectedCases = await loadSelectedCases(mode, caseIds);
const modeRunner = getModeRunner(mode, getFrontendEvalModel(model));
const modeRunner = getModeRunner(mode, getFrontendEvalModel(model), backendValidation);
const runModel = formatRunModelLabel(mode, model);
const caseResults = await runSuite({
modeRunner,
@@ -48,15 +53,16 @@ export async function runFrontendBenchmarkFromEnv(): Promise<BenchmarkRunResult>
function getModeRunner(
mode: FrontendBenchmarkMode,
model: ReturnType<typeof getFrontendEvalModel>
model: ReturnType<typeof getFrontendEvalModel>,
backendValidation: ReturnType<typeof resolveBackendValidationSettings>
): ModeRunner<any, any, any> {
switch (mode) {
case "flow":
return createFlowModeRunner(model);
return createFlowModeRunner(model, backendValidation);
case "app":
return createAppModeRunner(model);
case "script":
return createScriptModeRunner(model);
return createScriptModeRunner(model, backendValidation);
}
}

View File

@@ -22,6 +22,7 @@ export async function runFrontendBenchmarkAdapter(input: {
runs: number
model?: string
verbose?: boolean
backendValidation?: string
}): Promise<BenchmarkRunResult> {
const tempDir = await mkdtemp(path.join(tmpdir(), 'wmill-frontend-benchmark-'))
const outputPath = path.join(tempDir, 'result.json')
@@ -48,7 +49,8 @@ export async function runFrontendBenchmarkAdapter(input: {
WMILL_FRONTEND_AI_EVAL_RUNS: String(input.runs),
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_VERBOSE: input.verbose ? '1' : '0',
WMILL_FRONTEND_AI_EVAL_BACKEND_VALIDATION: input.backendValidation ?? ''
}
}
)

View File

@@ -3,6 +3,11 @@
Create a flow that takes two numbers, `a` and `b`, and returns their sum.
Keep it simple and use a single step named `sum_numbers`.
expected: ai_evals/fixtures/frontend/flow/expected/test0_sum_two_numbers.json
runtime:
backendPreview:
args:
a: 4
b: 5
judgeChecklist:
- "the flow takes `a` and `b` as inputs"
- "the main step is named `sum_numbers`"
@@ -15,6 +20,11 @@
The flow should take `a` and `b` as inputs and use a single step named `sum_numbers`.
initial: ai_evals/fixtures/frontend/flow/initial/test1_reuse_existing_script_initial.json
expected: ai_evals/fixtures/frontend/flow/expected/test1_reuse_existing_script.json
runtime:
backendPreview:
args:
a: 2
b: 3
judgeChecklist:
- "the flow takes `a` and `b` as inputs"
- "the main step is named `sum_numbers`"
@@ -27,6 +37,11 @@
Use a single step named `call_add_numbers`.
initial: ai_evals/fixtures/frontend/flow/initial/test2_call_existing_subflow_initial.json
expected: ai_evals/fixtures/frontend/flow/expected/test2_call_existing_subflow.json
runtime:
backendPreview:
args:
a: 7
b: 8
judgeChecklist:
- "the parent flow takes `a` and `b` as inputs"
- "the main step is named `call_add_numbers`"

View File

@@ -2,6 +2,10 @@
import { Command, InvalidArgumentError } from "commander";
import { loadCases, loadSelectedCases } from "../core/cases";
import {
BACKEND_VALIDATION_MODES,
parseBackendValidationMode,
} from "../core/backendValidation";
import {
EVAL_MODELS,
type EvalModelSpec,
@@ -43,6 +47,7 @@ async function main() {
" bun run cli -- run flow --models haiku,opus,4o",
" bun run cli -- run flow flow-test0-sum-two-numbers --verbose",
" bun run cli -- run flow --record",
" bun run cli -- run flow --backend-validation preview",
" bun run cli -- run flow flow-test5-simple-modification --runs 3",
" bun run cli -- run cli bun-hello-script",
"",
@@ -77,6 +82,10 @@ async function main() {
.option("--models <names>", "comma-separated model aliases to run sequentially")
.option("--verbose", "stream assistant output during frontend runs")
.option("--record", "append a compact summary line to ai_evals/history/<mode>.jsonl")
.option(
"--backend-validation <mode>",
`backend smoke validation (${BACKEND_VALIDATION_MODES.join(", ")})`
)
.action(
async (
mode: EvalMode,
@@ -88,6 +97,7 @@ async function main() {
models?: string;
verbose?: boolean;
record?: boolean;
backendValidation?: string;
}
) => {
await handleRun({
@@ -99,6 +109,7 @@ async function main() {
models: options.models,
verbose: options.verbose ?? false,
record: options.record ?? false,
backendValidation: options.backendValidation,
});
}
);
@@ -143,6 +154,7 @@ async function handleRun(input: {
models?: string;
verbose: boolean;
record: boolean;
backendValidation?: string;
}) {
if (input.record && input.caseIds.length > 0) {
throw new Error("--record only supports full-suite runs; omit case ids to record history");
@@ -153,9 +165,15 @@ async function handleRun(input: {
const selectedCases = await loadSelectedCases(input.mode, input.caseIds);
const models = resolveRequestedModels(input.mode, input.model, input.models);
const backendValidation = parseBackendValidationMode(
input.backendValidation ?? process.env.WMILL_AI_EVAL_BACKEND_VALIDATION
);
if (input.outputPath && models.length > 1) {
throw new Error("--output only supports a single model run");
}
if (backendValidation !== "off" && input.mode !== "flow" && input.mode !== "script") {
throw new Error("--backend-validation currently supports only flow and script modes");
}
const summaries: Array<{ label: string; passRate: number; averageDurationMs: number }> = [];
@@ -177,6 +195,7 @@ async function handleRun(input: {
runs: input.runs,
model: model.id,
verbose: input.verbose,
backendValidation,
});
const resolvedOutputPath =

View File

@@ -0,0 +1,36 @@
import { describe, expect, it } from "bun:test";
import {
parseBackendValidationMode,
resolveBackendValidationSettings,
} from "./backendValidation";
describe("parseBackendValidationMode", () => {
it("defaults to off", () => {
expect(parseBackendValidationMode(undefined)).toBe("off");
expect(parseBackendValidationMode("0")).toBe("off");
expect(parseBackendValidationMode("false")).toBe("off");
});
it("accepts preview aliases", () => {
expect(parseBackendValidationMode("preview")).toBe("preview");
expect(parseBackendValidationMode("1")).toBe("preview");
expect(parseBackendValidationMode("true")).toBe("preview");
});
it("rejects unknown modes", () => {
expect(() => parseBackendValidationMode("maybe")).toThrow(
"Unsupported backend validation mode: maybe"
);
});
});
describe("resolveBackendValidationSettings", () => {
it("rejects unsupported eval modes", () => {
expect(() =>
resolveBackendValidationSettings({
evalMode: "app",
requestedMode: "preview",
})
).toThrow('Backend validation mode "preview" is only supported for flow and script evals');
});
});

View File

@@ -0,0 +1,104 @@
import type { EvalMode } from "./types";
export const BACKEND_VALIDATION_MODES = ["off", "preview"] as const;
export type BackendValidationMode = (typeof BACKEND_VALIDATION_MODES)[number];
export interface BackendValidationSettings {
mode: BackendValidationMode;
baseUrl: string;
email: string;
password: string;
keepWorkspaces: boolean;
workspaceOverride?: string;
workspacePrefix: string;
pollIntervalMs: number;
maxWaitMs: number;
}
export function parseBackendValidationMode(value?: string | null): BackendValidationMode {
const normalized = value?.trim().toLowerCase();
if (!normalized || normalized === "off" || normalized === "false" || normalized === "0") {
return "off";
}
if (normalized === "preview" || normalized === "true" || normalized === "1") {
return "preview";
}
throw new Error(
`Unsupported backend validation mode: ${value}. Use one of: ${BACKEND_VALIDATION_MODES.join(", ")}`
);
}
export function resolveBackendValidationSettings(input: {
evalMode: EvalMode;
requestedMode?: string | null;
}): BackendValidationSettings {
const mode = parseBackendValidationMode(
input.requestedMode ?? process.env.WMILL_AI_EVAL_BACKEND_VALIDATION
);
if (mode !== "off" && input.evalMode !== "flow" && input.evalMode !== "script") {
throw new Error(
`Backend validation mode "${mode}" is only supported for flow and script evals`
);
}
return {
mode,
baseUrl: normalizeBaseUrl(
process.env.WMILL_AI_EVAL_BACKEND_URL ??
process.env.WINDMILL_URL ??
process.env.WINDMILL_BASE_URL ??
process.env.REMOTE ??
"http://127.0.0.1:8000"
),
email: process.env.WMILL_AI_EVAL_BACKEND_EMAIL ?? "admin@windmill.dev",
password: process.env.WMILL_AI_EVAL_BACKEND_PASSWORD ?? "changeme",
keepWorkspaces: isTruthy(process.env.WMILL_AI_EVAL_KEEP_WORKSPACES),
workspaceOverride: sanitizeOptionalWorkspaceId(process.env.WMILL_AI_EVAL_BACKEND_WORKSPACE),
workspacePrefix: sanitizeWorkspacePrefix(
process.env.WMILL_AI_EVAL_WORKSPACE_PREFIX ?? "ai-evals"
),
pollIntervalMs: parsePositiveInteger(
process.env.WMILL_AI_EVAL_BACKEND_POLL_INTERVAL_MS,
2000
),
maxWaitMs: parsePositiveInteger(process.env.WMILL_AI_EVAL_BACKEND_MAX_WAIT_MS, 120000),
};
}
function normalizeBaseUrl(value: string): string {
return value.replace(/\/+$/, "");
}
function sanitizeWorkspacePrefix(value: string): string {
const sanitized = value
.trim()
.toLowerCase()
.replace(/[^a-z0-9-]+/g, "-")
.replace(/^-+|-+$/g, "");
return sanitized.length > 0 ? sanitized : "ai-evals";
}
function sanitizeOptionalWorkspaceId(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed ? trimmed : undefined;
}
function isTruthy(value: string | undefined): boolean {
if (!value) {
return false;
}
return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
}
function parsePositiveInteger(value: string | undefined, fallback: number): number {
if (!value) {
return fallback;
}
const parsed = Number(value);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}

View File

@@ -0,0 +1,18 @@
import { describe, expect, it } from "bun:test";
import { loadCases } from "./cases";
describe("loadCases", () => {
it("loads backend preview runtime config for opt-in flow cases", async () => {
const flowCases = await loadCases("flow");
const caseEntry = flowCases.find((entry) => entry.id === "flow-test1-reuse-existing-script");
expect(caseEntry?.runtime).toEqual({
backendPreview: {
args: {
a: 2,
b: 3,
},
},
});
});
});

View File

@@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { parse } from "yaml";
import type { EvalCase, EvalMode, FlowValidationSpec } from "./types";
import type { EvalCase, EvalCaseRuntimeSpec, EvalMode, FlowValidationSpec } from "./types";
const REPO_ROOT = fileURLToPath(new URL("../../", import.meta.url));
const CASES_DIR = path.join(REPO_ROOT, "ai_evals", "cases");
@@ -14,6 +14,7 @@ interface RawEvalCase {
expected?: string;
validate?: FlowValidationSpec;
judgeChecklist?: string[];
runtime?: EvalCaseRuntimeSpec;
}
export function getRepoRoot(): string {
@@ -40,6 +41,7 @@ export async function loadCases(mode: EvalMode): Promise<EvalCase[]> {
expectedPath: resolveFixturePath(entry.expected),
validate: entry.validate,
judgeChecklist: entry.judgeChecklist,
runtime: entry.runtime,
}));
}

View File

@@ -155,6 +155,44 @@ async function runCaseAttempts<TInitial, TExpected, TActual>(input: {
run,
}),
];
const artifactFiles = input.modeRunner.buildArtifacts?.(run.actual) ?? [];
if (run.success && input.modeRunner.backendValidate) {
try {
const backendValidation = await input.modeRunner.backendValidate({
evalCase: input.evalCase,
prompt: input.evalCase.prompt,
initial,
expected,
actual: run.actual,
run,
context: {
caseId: input.evalCase.id,
caseNumber: input.caseIndex + 1,
totalCases: input.totalCases,
attempt,
runs: input.runs,
verbose: input.verbose,
onAssistantMessageStart: undefined,
onAssistantChunk: undefined,
onAssistantMessageEnd: undefined,
},
});
if (backendValidation) {
checks.push(...backendValidation.checks);
artifactFiles.push(...(backendValidation.artifactFiles ?? []));
}
} catch (error) {
checks.push(
buildCheck(
"backend validation succeeded",
false,
error instanceof Error ? error.message : String(error)
)
);
}
}
let judgeScore: number | null = null;
let judgeSummary: string | null = null;
@@ -182,7 +220,6 @@ async function runCaseAttempts<TInitial, TExpected, TActual>(input: {
);
}
const artifactFiles = input.modeRunner.buildArtifacts?.(run.actual) ?? [];
const attemptResult: BenchmarkAttemptResult = {
attempt,
passed: checks.every((check) => check.passed),

View File

@@ -2,6 +2,15 @@ export const EVAL_MODES = ["cli", "flow", "script", "app"] as const;
export type EvalMode = (typeof EVAL_MODES)[number];
export interface EvalCaseRuntimeBackendPreview {
args?: Record<string, unknown>;
timeoutSeconds?: number;
}
export interface EvalCaseRuntimeSpec {
backendPreview?: EvalCaseRuntimeBackendPreview;
}
export interface FlowValidationSpec {
schemaRequiredPaths?: string[];
schemaAnyOf?: Array<{
@@ -23,6 +32,7 @@ export interface EvalCase {
expectedPath?: string;
validate?: FlowValidationSpec;
judgeChecklist?: string[];
runtime?: EvalCaseRuntimeSpec;
}
export interface BenchmarkCheck {
@@ -43,6 +53,11 @@ export interface BenchmarkArtifactFile {
content: string;
}
export interface BackendValidationResult {
checks: BenchmarkCheck[];
artifactFiles?: BenchmarkArtifactFile[];
}
export interface BenchmarkTokenUsage {
prompt: number;
completion: number;
@@ -91,6 +106,15 @@ export interface ModeRunner<TInitial, TExpected, TActual> {
actual: TActual;
run: ModeRunOutput<TActual>;
}): BenchmarkCheck[];
backendValidate?(input: {
evalCase: EvalCase;
prompt: string;
initial: TInitial | undefined;
expected: TExpected | undefined;
actual: TActual;
run: ModeRunOutput<TActual>;
context: ModeRunContext;
}): Promise<BackendValidationResult | null>;
buildArtifacts?(actual: TActual): BenchmarkArtifactFile[];
}

View File

@@ -5,7 +5,7 @@
"id": "sum_numbers",
"value": {
"type": "script",
"path": "f/evals/add_two_numbers.ts",
"path": "f/evals/add_two_numbers",
"input_transforms": {
"a": {
"type": "javascript",

View File

@@ -2,7 +2,7 @@
"workspace": {
"scripts": [
{
"path": "f/evals/add_two_numbers.ts",
"path": "f/evals/add_two_numbers",
"summary": "Add two numbers",
"description": "Returns the sum of two numeric inputs.",
"language": "bun",

View File

@@ -1,4 +1,5 @@
import { readJsonFile } from "../core/files";
import type { BackendValidationSettings } from "../core/backendValidation";
import type { FrontendEvalModelConfig } from "../core/models";
import { validateFlowState, type FlowState } from "../core/validators";
import type { BenchmarkArtifactFile, ModeRunner } from "../core/types";
@@ -7,6 +8,7 @@ import {
type FlowFixture,
} from "../adapters/frontend/core/flow/flowEvalRunner";
import type { FlowWorkspaceFixtures } from "../adapters/frontend/core/flow/fileHelpers";
import { BackendPreviewClient } from "../adapters/frontend/backendPreview";
import { DEFAULT_FRONTEND_EVAL_MODEL, getFrontendApiKey } from "./frontendCommon";
interface FlowInitialFixture {
@@ -15,7 +17,8 @@ interface FlowInitialFixture {
}
export function createFlowModeRunner(
modelConfig: FrontendEvalModelConfig = DEFAULT_FRONTEND_EVAL_MODEL
modelConfig: FrontendEvalModelConfig = DEFAULT_FRONTEND_EVAL_MODEL,
backendValidation?: BackendValidationSettings
): ModeRunner<FlowInitialFixture, FlowState, FlowState> {
return {
mode: "flow",
@@ -61,6 +64,65 @@ export function createFlowModeRunner(
validate: evalCase.validate,
});
},
async backendValidate({ evalCase, initial, actual, context }) {
if (backendValidation?.mode !== "preview" || !evalCase.runtime?.backendPreview) {
return null;
}
if (!actual.value) {
return {
checks: [
{
name: "backend flow preview succeeded",
passed: false,
details: "Generated flow is missing value.modules",
},
],
};
}
const previewClient = new BackendPreviewClient(backendValidation);
return await previewClient.withWorkspace(evalCase.id, context.attempt, async (workspaceId) => {
await seedWorkspaceFixtures(previewClient, workspaceId, initial?.workspace);
const completedJob = await previewClient.runFlowPreview({
workspaceId,
value: actual.value as Record<string, unknown>,
args: evalCase.runtime?.backendPreview?.args ?? {},
timeoutSeconds: evalCase.runtime?.backendPreview?.timeoutSeconds,
});
return {
checks: [
{
name: "backend flow preview succeeded",
passed: completedJob.success,
details: completedJob.success
? `workspace=${workspaceId}`
: `workspace=${workspaceId}; job=${completedJob.id}`,
},
],
artifactFiles: [
{
path: "backend-preview.json",
content:
JSON.stringify(
{
workspaceId,
jobId: completedJob.id,
success: completedJob.success,
result: completedJob.result,
logs: completedJob.logs,
completedJob: completedJob.raw,
},
null,
2
) + "\n",
},
],
};
});
},
buildArtifacts(actual): BenchmarkArtifactFile[] {
return [
{
@@ -102,3 +164,32 @@ function normalizeFlowStateFixture(value: unknown): FlowState {
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
async function seedWorkspaceFixtures(
previewClient: BackendPreviewClient,
workspaceId: string,
fixtures?: FlowWorkspaceFixtures
): Promise<void> {
for (const script of fixtures?.scripts ?? []) {
await previewClient.createScript({
workspaceId,
path: script.path,
summary: script.summary,
description: script.description,
schema: script.schema,
content: script.content,
language: script.language,
});
}
for (const flow of fixtures?.flows ?? []) {
await previewClient.createFlow({
workspaceId,
path: flow.path,
summary: flow.summary,
description: flow.description,
schema: flow.schema,
value: flow.value as Record<string, unknown>,
});
}
}

View File

@@ -1,13 +1,16 @@
import { readJsonFile } from "../core/files";
import type { BackendValidationSettings } from "../core/backendValidation";
import type { FrontendEvalModelConfig } from "../core/models";
import { validateScriptState } from "../core/validators";
import type { BenchmarkArtifactFile, ModeRunner } from "../core/types";
import { BackendPreviewClient } from "../adapters/frontend/backendPreview";
import { runScriptEval } from "../adapters/frontend/core/script/scriptEvalRunner";
import type { ScriptEvalState } from "../adapters/frontend/core/script/fileHelpers";
import { DEFAULT_FRONTEND_EVAL_MODEL, getFrontendApiKey } from "./frontendCommon";
export function createScriptModeRunner(
modelConfig: FrontendEvalModelConfig = DEFAULT_FRONTEND_EVAL_MODEL
modelConfig: FrontendEvalModelConfig = DEFAULT_FRONTEND_EVAL_MODEL,
backendValidation?: BackendValidationSettings
): ModeRunner<ScriptEvalState, ScriptEvalState, ScriptEvalState> {
return {
mode: "script",
@@ -45,6 +48,57 @@ export function createScriptModeRunner(
validate({ actual, initial, expected }) {
return validateScriptState({ actual, initial, expected });
},
async backendValidate({ evalCase, initial, actual, context }) {
if (backendValidation?.mode !== "preview") {
return null;
}
const previewClient = new BackendPreviewClient(backendValidation);
return await previewClient.withWorkspace(evalCase.id, context.attempt, async (workspaceId) => {
const completedJob = await previewClient.runScriptPreview({
workspaceId,
content: actual.code,
args:
(evalCase.runtime?.backendPreview?.args as Record<string, unknown> | undefined) ??
actual.args ??
initial?.args ??
{},
language: normalizePreviewLanguage(actual.lang),
path: toPreviewScriptPath(actual.path),
timeoutSeconds: evalCase.runtime?.backendPreview?.timeoutSeconds,
});
return {
checks: [
{
name: "backend script preview succeeded",
passed: completedJob.success,
details: completedJob.success
? `workspace=${workspaceId}`
: `workspace=${workspaceId}; job=${completedJob.id}`,
},
],
artifactFiles: [
{
path: "backend-preview.json",
content:
JSON.stringify(
{
workspaceId,
jobId: completedJob.id,
success: completedJob.success,
result: completedJob.result,
logs: completedJob.logs,
completedJob: completedJob.raw,
},
null,
2
) + "\n",
},
],
};
});
},
buildArtifacts(actual): BenchmarkArtifactFile[] {
return [
{
@@ -59,3 +113,11 @@ export function createScriptModeRunner(
},
};
}
function normalizePreviewLanguage(language: ScriptEvalState["lang"]): string {
return language === "bunnative" ? "bun" : language;
}
function toPreviewScriptPath(filePath: string): string {
return filePath.replace(/\.[^.\/]+$/, "");
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT usage.usage + 1 FROM usage \n WHERE is_workspace IS TRUE AND\n month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND id = $1",
"query": "SELECT usage.usage + 1 FROM usage\n WHERE is_workspace IS FALSE AND\n month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND id = $1",
"describe": {
"columns": [
{
@@ -18,5 +18,5 @@
null
]
},
"hash": "58c0eb36b630d5eba9d12edca672ebd56e13193395701e26328a243055bee6b8"
"hash": "0bc5b3483b352770129962139329e28f99450f33865ceb7724fd1f378dacde26"
}

View File

@@ -1,24 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n flow_status->>'step' = '0' \n AND (\n jsonb_array_length(flow_status->'modules') = 0 \n OR flow_status->'modules'->0->>'type' = 'WaitingForPriorSteps' \n OR (\n flow_status->'modules'->0->>'type' = 'Failure' \n AND flow_status->'modules'->0->>'job' = $1\n )\n )\n FROM v2_job_completed WHERE id = $2 AND workspace_id = $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Uuid",
"Text"
]
},
"nullable": [
null
]
},
"hash": "241270e20c751806dece12fbc2de360e389da8c7f653ef8e6bc0d30c823aea51"
}

View File

@@ -15,7 +15,7 @@
]
},
"nullable": [
true
null
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"

View File

@@ -1,29 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\nWITH RECURSIVE job_tree AS (\n -- Base case: direct children of the given parent job\n SELECT id, parent_job, 1 AS depth\n FROM v2_job_queue \n INNER JOIN v2_job USING (id)\n WHERE parent_job = $1 AND v2_job.workspace_id = $2\n\n UNION ALL\n\n -- Recursive case: fetch children of previously found jobs\n SELECT q.id, j.parent_job, t.depth + 1\n FROM v2_job_queue q\n INNER JOIN v2_job j USING (id)\n INNER JOIN job_tree t ON t.id = j.parent_job\n WHERE j.workspace_id = $2 AND t.depth < 500 -- Limit recursion depth to 500\n)\nSELECT id AS id, depth\nFROM job_tree\nORDER BY depth, id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "depth",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
null,
null
]
},
"hash": "5e8ba1850b2520bd4bf030f53f1f2e6606bfc0f440fbdbbdf4dd3234068c9345"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n j.id, j.workspace_id, j.runnable_id AS \"runnable_id: ScriptHash\", q.scheduled_for, q.started_at, j.parent_job, j.flow_innermost_root_job, j.runnable_path, j.kind as \"kind!: JobKind\", j.permissioned_as, \n j.created_by, j.script_lang AS \"script_lang: ScriptLang\", j.permissioned_as_email, j.flow_step_id, j.trigger_kind AS \"trigger_kind: JobTriggerKind\", j.trigger, j.priority, j.concurrent_limit, j.tag, j.cache_ttl, q.cache_ignore_s3_path, q.runnable_settings_handle\n FROM v2_job j LEFT JOIN v2_job_queue q ON j.id = q.id\n WHERE j.id = $1 AND j.workspace_id = $2",
"query": "SELECT\n j.id, j.workspace_id, j.runnable_id AS \"runnable_id: ScriptHash\", q.scheduled_for, q.started_at, j.parent_job, j.flow_innermost_root_job, j.runnable_path, j.kind as \"kind!: JobKind\", j.permissioned_as,\n j.created_by, j.script_lang AS \"script_lang: ScriptLang\", j.permissioned_as_email, j.flow_step_id, j.trigger_kind AS \"trigger_kind: JobTriggerKind\", j.trigger, j.priority, j.concurrent_limit, j.tag, j.cache_ttl, q.cache_ignore_s3_path, q.runnable_settings_handle\n FROM v2_job j LEFT JOIN v2_job_queue q ON j.id = q.id\n WHERE j.id = $1 AND j.workspace_id = $2",
"describe": {
"columns": [
{
@@ -231,5 +231,5 @@
true
]
},
"hash": "a1745a4f525b251d2f5a602ab2b2ede46b4471e21b11f607573a844013911abe"
"hash": "67e25a7c19ea0ffaf7ea5303fcd04af5a7eb488c76f783e690af0c2153b1d6a8"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (\n id, -- 1\n workspace_id, -- 2\n raw_code, -- 3\n raw_lock, -- 4\n raw_flow, -- 5\n tag, -- 6\n parent_job, -- 7\n created_by, -- 8\n permissioned_as, -- 9\n runnable_id, -- 10\n runnable_path, -- 11\n args, -- 12\n kind, -- 13\n trigger, -- 14\n script_lang, -- 15\n same_worker, -- 16\n pre_run_error, -- 17 \n permissioned_as_email, -- 18\n visible_to_owner, -- 19\n flow_innermost_root_job, -- 20\n root_job, -- 38\n concurrent_limit, -- 21\n concurrency_time_window_s, -- 22\n timeout, -- 23\n flow_step_id, -- 24\n cache_ttl, -- 25\n priority, -- 26\n trigger_kind, -- 39\n script_entrypoint_override, -- 12\n preprocessed, -- 27,\n labels -- 44\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27, $44)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id, end_user_email) \n values ($1, $32, $33, $34, $35, $36, $37, $2, $41) \n ON CONFLICT (job_id) DO UPDATE SET email = EXCLUDED.email, username = EXCLUDED.username, is_admin = EXCLUDED.is_admin, is_operator = EXCLUDED.is_operator, folders = EXCLUDED.folders, groups = EXCLUDED.groups, workspace_id = EXCLUDED.workspace_id, end_user_email = EXCLUDED.end_user_email\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority, cache_ignore_s3_path, runnable_settings_handle)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31, $42, $43)",
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (\n id, -- 1\n workspace_id, -- 2\n raw_code, -- 3\n raw_lock, -- 4\n raw_flow, -- 5\n tag, -- 6\n parent_job, -- 7\n created_by, -- 8\n permissioned_as, -- 9\n runnable_id, -- 10\n runnable_path, -- 11\n args, -- 12\n kind, -- 13\n trigger, -- 14\n script_lang, -- 15\n same_worker, -- 16\n pre_run_error, -- 17\n permissioned_as_email, -- 18\n visible_to_owner, -- 19\n flow_innermost_root_job, -- 20\n root_job, -- 38\n concurrent_limit, -- 21\n concurrency_time_window_s, -- 22\n timeout, -- 23\n flow_step_id, -- 24\n cache_ttl, -- 25\n priority, -- 26\n trigger_kind, -- 39\n script_entrypoint_override, -- 12\n preprocessed, -- 27,\n labels -- 44\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27, $44)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id, end_user_email)\n values ($1, $32, $33, $34, $35, $36, $37, $2, $41)\n ON CONFLICT (job_id) DO UPDATE SET email = EXCLUDED.email, username = EXCLUDED.username, is_admin = EXCLUDED.is_admin, is_operator = EXCLUDED.is_operator, folders = EXCLUDED.folders, groups = EXCLUDED.groups, workspace_id = EXCLUDED.workspace_id, end_user_email = EXCLUDED.end_user_email\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority, cache_ignore_s3_path, runnable_settings_handle)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31, $42, $43)",
"describe": {
"columns": [],
"parameters": {
@@ -139,5 +139,5 @@
},
"nullable": []
},
"hash": "5425e2c5e29fc5145dde5ea53d5307ca90d8dd76da2ca560b310b12820be2576"
"hash": "756f82b72af07fd690f37b2e16ed2d390604f4fc4cb330842a88d5764cbcf0c6"
}

View File

@@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT usage.usage + 1 FROM usage \n WHERE is_workspace IS FALSE AND\n month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "94ff696b4d3904e3823ef637fa8f1f0d0bdac01040c81b31514326417eb58cee"
}

View File

@@ -0,0 +1,24 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n flow_status->>'step' = '0'\n AND (\n jsonb_array_length(flow_status->'modules') = 0\n OR flow_status->'modules'->0->>'type' = 'WaitingForPriorSteps'\n OR (\n flow_status->'modules'->0->>'type' = 'Failure'\n AND flow_status->'modules'->0->>'job' = $1\n )\n )\n FROM v2_job_completed WHERE id = $2 AND workspace_id = $3",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Text",
"Uuid",
"Text"
]
},
"nullable": [
null
]
},
"hash": "a7f4b8f5d8d9074ce60c9fb6bc09c8611ffcb833f28b497706f7630dbae33e08"
}

View File

@@ -0,0 +1,29 @@
{
"db_name": "PostgreSQL",
"query": "\nWITH RECURSIVE job_tree AS (\n -- Base case: direct children of the given parent job\n SELECT id, parent_job, 1 AS depth\n FROM v2_job_queue\n INNER JOIN v2_job USING (id)\n WHERE parent_job = $1 AND v2_job.workspace_id = $2\n\n UNION ALL\n\n -- Recursive case: fetch children of previously found jobs\n SELECT q.id, j.parent_job, t.depth + 1\n FROM v2_job_queue q\n INNER JOIN v2_job j USING (id)\n INNER JOIN job_tree t ON t.id = j.parent_job\n WHERE j.workspace_id = $2 AND t.depth < 500 -- Limit recursion depth to 500\n)\nSELECT id AS id, depth\nFROM job_tree\nORDER BY depth, id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "depth",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
null,
null
]
},
"hash": "b22e933ea5e9297dd4342310b0561fb43786010f410693e26c9d564e5eff09ff"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n v2_job_queue.workspace_id,\n v2_job_queue.id,\n v2_job.args as \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n v2_job.parent_job,\n v2_job.created_by,\n v2_job_queue.started_at,\n v2_job_queue.runnable_settings_handle,\n scheduled_for,\n runnable_path,\n kind as \"kind: JobKind\",\n runnable_id as \"runnable_id: ScriptHash\",\n canceled_reason,\n canceled_by,\n permissioned_as,\n permissioned_as_email,\n flow_status as \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n v2_job.tag,\n script_lang as \"script_lang: ScriptLang\",\n same_worker,\n pre_run_error,\n concurrent_limit,\n concurrency_time_window_s,\n flow_innermost_root_job,\n root_job,\n timeout,\n flow_step_id,\n cache_ttl,\n cache_ignore_s3_path,\n v2_job_queue.priority,\n preprocessed,\n script_entrypoint_override,\n trigger,\n trigger_kind as \"trigger_kind: JobTriggerKind\",\n visible_to_owner,\n NULL as permissioned_as_end_user_email\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id WHERE v2_job_queue.id = $1",
"query": "SELECT\n v2_job_queue.workspace_id,\n v2_job_queue.id,\n v2_job.args as \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n v2_job.parent_job,\n v2_job.created_by,\n v2_job_queue.started_at,\n v2_job_queue.runnable_settings_handle,\n scheduled_for,\n runnable_path,\n kind as \"kind: JobKind\",\n runnable_id as \"runnable_id: ScriptHash\",\n canceled_reason,\n canceled_by,\n permissioned_as,\n permissioned_as_email,\n flow_status as \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n v2_job.tag,\n script_lang as \"script_lang: ScriptLang\",\n same_worker,\n pre_run_error,\n concurrent_limit,\n concurrency_time_window_s,\n flow_innermost_root_job,\n root_job,\n timeout,\n flow_step_id,\n cache_ttl,\n cache_ignore_s3_path,\n v2_job_queue.priority,\n preprocessed,\n script_entrypoint_override,\n trigger,\n trigger_kind as \"trigger_kind: JobTriggerKind\",\n visible_to_owner,\n NULL as permissioned_as_end_user_email\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id WHERE v2_job_queue.id = $1",
"describe": {
"columns": [
{
@@ -308,5 +308,5 @@
null
]
},
"hash": "5bf200f2c8db25ddf231b564503c6c70f7f3958564a79bb0c6b3863b1ebb0cbf"
"hash": "d4211392e174a0e8f89c7fcebdf120e5b0f629f9f04e08a2982df33ff23ac7a9"
}

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT usage.usage + 1 FROM usage\n WHERE is_workspace IS TRUE AND\n month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "efc83ffbccd4e354b9afaddd25db2edf3aa5e6a330954ff81c38592c0779df4a"
}

154
backend/Cargo.lock generated
View File

@@ -10667,7 +10667,7 @@ version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
dependencies = [
"heck 0.4.1",
"heck 0.5.0",
"itertools 0.14.0",
"log",
"multimap",
@@ -16013,7 +16013,7 @@ dependencies = [
[[package]]
name = "windmill"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-nats",
@@ -16092,7 +16092,7 @@ dependencies = [
[[package]]
name = "windmill-alerting"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16105,7 +16105,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"argon2",
@@ -16246,7 +16246,7 @@ dependencies = [
[[package]]
name = "windmill-api-agent-workers"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16269,7 +16269,7 @@ dependencies = [
[[package]]
name = "windmill-api-assets"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16282,7 +16282,7 @@ dependencies = [
[[package]]
name = "windmill-api-auth"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16308,7 +16308,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -16318,7 +16318,7 @@ dependencies = [
[[package]]
name = "windmill-api-configs"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16335,7 +16335,7 @@ dependencies = [
[[package]]
name = "windmill-api-debug"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"axum 0.8.4",
"base64 0.22.1",
@@ -16358,7 +16358,7 @@ dependencies = [
[[package]]
name = "windmill-api-embeddings"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16381,7 +16381,7 @@ dependencies = [
[[package]]
name = "windmill-api-flow-conversations"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16397,7 +16397,7 @@ dependencies = [
[[package]]
name = "windmill-api-flows"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16418,7 +16418,7 @@ dependencies = [
[[package]]
name = "windmill-api-groups"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16439,7 +16439,7 @@ dependencies = [
[[package]]
name = "windmill-api-inputs"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16453,7 +16453,7 @@ dependencies = [
[[package]]
name = "windmill-api-integration-tests"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-nats",
@@ -16484,7 +16484,7 @@ dependencies = [
[[package]]
name = "windmill-api-jobs"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16509,7 +16509,7 @@ dependencies = [
[[package]]
name = "windmill-api-npm-proxy"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"axum 0.8.4",
"flate2",
@@ -16527,7 +16527,7 @@ dependencies = [
[[package]]
name = "windmill-api-openapi"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16549,7 +16549,7 @@ dependencies = [
[[package]]
name = "windmill-api-schedule"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16569,7 +16569,7 @@ dependencies = [
[[package]]
name = "windmill-api-scripts"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16599,7 +16599,7 @@ dependencies = [
[[package]]
name = "windmill-api-settings"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16626,7 +16626,7 @@ dependencies = [
[[package]]
name = "windmill-api-sse"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"lazy_static",
"serde",
@@ -16638,7 +16638,7 @@ dependencies = [
[[package]]
name = "windmill-api-users"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"argon2",
"axum 0.8.4",
@@ -16663,7 +16663,7 @@ dependencies = [
[[package]]
name = "windmill-api-workers"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16677,7 +16677,7 @@ dependencies = [
[[package]]
name = "windmill-api-workspaces"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"axum 0.8.4",
"chrono",
@@ -16710,7 +16710,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"chrono",
"lazy_static",
@@ -16724,7 +16724,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"axum 0.8.4",
@@ -16743,7 +16743,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"aes-gcm",
"aho-corasick",
@@ -16847,7 +16847,7 @@ dependencies = [
[[package]]
name = "windmill-dep-map"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"chrono",
"itertools 0.14.0",
@@ -16866,7 +16866,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"regex",
"serde",
@@ -16881,7 +16881,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -16905,7 +16905,7 @@ dependencies = [
[[package]]
name = "windmill-jseval"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"futures",
@@ -16922,7 +16922,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -16938,7 +16938,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-trait",
@@ -16959,7 +16959,7 @@ dependencies = [
[[package]]
name = "windmill-native-triggers"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-trait",
@@ -16990,7 +16990,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"arc-swap",
@@ -17015,7 +17015,7 @@ dependencies = [
[[package]]
name = "windmill-object-store"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-stream",
@@ -17049,7 +17049,7 @@ dependencies = [
[[package]]
name = "windmill-operator"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"futures",
@@ -17067,7 +17067,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -17076,7 +17076,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -17088,7 +17088,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"serde_json",
@@ -17100,7 +17100,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"gosyn",
@@ -17112,7 +17112,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -17124,7 +17124,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"serde_json",
@@ -17136,7 +17136,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"nu-parser",
@@ -17147,7 +17147,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -17158,7 +17158,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -17170,7 +17170,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-asset"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -17181,7 +17181,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -17203,7 +17203,7 @@ dependencies = [
[[package]]
name = "windmill-parser-r"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"serde_json",
@@ -17215,7 +17215,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -17229,7 +17229,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -17246,7 +17246,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -17259,7 +17259,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql-asset"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"serde",
@@ -17271,7 +17271,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -17289,7 +17289,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts-asset"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"serde-wasm-bindgen",
@@ -17305,7 +17305,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wac"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -17321,7 +17321,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"serde",
@@ -17332,7 +17332,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -17369,7 +17369,7 @@ dependencies = [
[[package]]
name = "windmill-runtime-nativets"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"const_format",
@@ -17407,7 +17407,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"getrandom 0.3.4",
"wasm-bindgen",
@@ -17418,7 +17418,7 @@ dependencies = [
[[package]]
name = "windmill-store"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -17448,7 +17448,7 @@ dependencies = [
[[package]]
name = "windmill-test-utils"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-trait",
@@ -17472,7 +17472,7 @@ dependencies = [
[[package]]
name = "windmill-trigger"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-trait",
@@ -17505,7 +17505,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-email"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-trait",
@@ -17525,7 +17525,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-gcp"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-trait",
@@ -17559,7 +17559,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-http"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-trait",
@@ -17595,7 +17595,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-kafka"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-trait",
@@ -17618,7 +17618,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-mqtt"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-trait",
@@ -17642,7 +17642,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-nats"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-nats",
@@ -17666,7 +17666,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-postgres"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-trait",
@@ -17701,7 +17701,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-sqs"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-trait",
@@ -17729,7 +17729,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-websocket"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-trait",
@@ -17752,7 +17752,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"bitflags 2.9.4",
@@ -17771,7 +17771,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"anyhow",
"async-once-cell",
@@ -17882,7 +17882,7 @@ dependencies = [
[[package]]
name = "windmill-worker-volumes"
version = "1.684.0"
version = "1.684.1"
dependencies = [
"bytes",
"futures",

View File

@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.684.0"
version = "1.684.1"
authors.workspace = true
edition.workspace = true
@@ -85,7 +85,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"]
[workspace.package]
version = "1.684.0"
version = "1.684.1"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"

View File

@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.684.0
version: 1.684.1
title: Windmill API
contact:

View File

@@ -549,15 +549,11 @@ pub fn gemini_response_to_openai(parsed: &GeminiParsedEvent, model: &str) -> ser
.iter()
.enumerate()
.map(|(i, tc)| {
serde_json::json!({
"index": i,
"id": format!("call_{}", uuid::Uuid::new_v4().simple()),
"type": "function",
"function": {
"name": tc.name,
"arguments": serde_json::to_string(&tc.args).unwrap_or_default()
}
})
openai_tool_call_json(
tc,
format!("call_{}", uuid::Uuid::new_v4().simple()),
Some(i),
)
})
.collect();
@@ -623,7 +619,6 @@ pub fn gemini_event_to_openai_sse_chunks(
}
for tc in &parsed.tool_calls {
let args_str = serde_json::to_string(&tc.args).unwrap_or_default();
let call_id = format!("call_{}", uuid::Uuid::new_v4().simple());
let chunk = serde_json::json!({
"id": id,
@@ -632,15 +627,7 @@ pub fn gemini_event_to_openai_sse_chunks(
"choices": [{
"index": 0,
"delta": {
"tool_calls": [{
"index": *tool_call_index,
"id": call_id,
"type": "function",
"function": {
"name": tc.name,
"arguments": args_str,
}
}]
"tool_calls": [openai_tool_call_json(tc, call_id, Some(*tool_call_index))]
},
"finish_reason": null,
}]
@@ -729,3 +716,102 @@ fn extract_candidates_into(candidates: &[GeminiSSECandidate], parsed: &mut Gemin
}
}
}
fn openai_tool_call_json(
tool_call: &GeminiToolCallEvent,
call_id: String,
index: Option<usize>,
) -> serde_json::Value {
let mut value = serde_json::json!({
"id": call_id,
"type": "function",
"function": {
"name": tool_call.name,
"arguments": serde_json::to_string(&tool_call.args).unwrap_or_default(),
}
});
if let Some(index) = index {
value["index"] = serde_json::json!(index);
}
if let Some(extra_content) = tool_call.to_extra_content() {
if let Ok(extra_content) = serde_json::to_value(extra_content) {
value["extra_content"] = extra_content;
}
}
value
}
#[cfg(test)]
mod tests {
use super::{
gemini_event_to_openai_sse_chunks, gemini_response_to_openai, GeminiParsedEvent,
GeminiToolCallEvent,
};
#[test]
fn gemini_response_to_openai_preserves_thought_signature() {
let parsed = GeminiParsedEvent {
tool_calls: vec![GeminiToolCallEvent {
name: "get_instructions_for_code_generation".to_string(),
args: serde_json::json!({ "language": "rust" }),
thought_signature: Some("test-thought-signature".to_string()),
}],
..Default::default()
};
let response = gemini_response_to_openai(&parsed, "gemini-3.1-pro");
let tool_call = &response["choices"][0]["message"]["tool_calls"][0];
assert_eq!(
tool_call["extra_content"]["google"]["thought_signature"],
"test-thought-signature"
);
assert_eq!(
tool_call["function"]["name"],
"get_instructions_for_code_generation"
);
assert_eq!(
tool_call["function"]["arguments"],
"{\"language\":\"rust\"}"
);
}
#[test]
fn gemini_streaming_chunks_preserve_thought_signature() {
let parsed = GeminiParsedEvent {
tool_calls: vec![GeminiToolCallEvent {
name: "get_instructions_for_code_generation".to_string(),
args: serde_json::json!({ "language": "rust" }),
thought_signature: Some("stream-thought-signature".to_string()),
}],
..Default::default()
};
let mut tool_call_index = 0;
let chunks = gemini_event_to_openai_sse_chunks(
&parsed,
"chatcmpl-test",
"gemini-3.1-pro",
&mut tool_call_index,
);
assert_eq!(chunks.len(), 1);
let payload = chunks[0]
.strip_prefix("data: ")
.and_then(|chunk| chunk.strip_suffix("\n\n"))
.expect("chunk should be wrapped as SSE data");
let parsed_chunk: serde_json::Value =
serde_json::from_str(payload).expect("chunk should be valid JSON");
let tool_call = &parsed_chunk["choices"][0]["delta"]["tool_calls"][0];
assert_eq!(
tool_call["extra_content"]["google"]["thought_signature"],
"stream-thought-signature"
);
assert_eq!(tool_call["index"], 0);
}
}

View File

@@ -328,7 +328,7 @@ pub async fn cancel_job<'c>(
WITH RECURSIVE job_tree AS (
-- Base case: direct children of the given parent job
SELECT id, parent_job, 1 AS depth
FROM v2_job_queue
FROM v2_job_queue
INNER JOIN v2_job USING (id)
WHERE parent_job = $1 AND v2_job.workspace_id = $2
@@ -913,8 +913,6 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
) -> windmill_common::error::Result<(Option<Uuid>, i64, bool, Option<serde_json::Value>)> {
// let start = std::time::Instant::now();
let mut tx = db.begin().warn_after_seconds(10).await?;
let job_id = completed_job.id;
// tracing::error!("1 {:?}", start.elapsed());
@@ -931,6 +929,8 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
return value;
}
let mut tx = db.begin().warn_after_seconds(10).await?;
let duration = sqlx::query_scalar!(
"INSERT INTO v2_job_completed AS cj
( workspace_id
@@ -1141,13 +1141,13 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|| from_cache
|| !success
&& sqlx::query_scalar!(
"SELECT
flow_status->>'step' = '0'
"SELECT
flow_status->>'step' = '0'
AND (
jsonb_array_length(flow_status->'modules') = 0
OR flow_status->'modules'->0->>'type' = 'WaitingForPriorSteps'
jsonb_array_length(flow_status->'modules') = 0
OR flow_status->'modules'->0->>'type' = 'WaitingForPriorSteps'
OR (
flow_status->'modules'->0->>'type' = 'Failure'
flow_status->'modules'->0->>'type' = 'Failure'
AND flow_status->'modules'->0->>'job' = $1
)
)
@@ -2788,7 +2788,7 @@ pub async fn get_mini_pulled_job<'c>(
) -> windmill_common::error::Result<Option<MiniPulledJob>> {
let job = sqlx::query_as!(
MiniPulledJob,
"SELECT
"SELECT
v2_job_queue.workspace_id,
v2_job_queue.id,
v2_job.args as \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",
@@ -3935,7 +3935,7 @@ pub async fn get_result_and_success_by_id_from_flow(
r#"WITH modules AS (
SELECT jsonb_array_elements(flow_status->'modules') AS module
FROM {}
WHERE id = $1
WHERE id = $1
)
SELECT module->>'type' = 'Success'
FROM modules
@@ -4248,8 +4248,8 @@ pub fn get_mini_completed_job<'a, 'e, A: sqlx::Acquire<'e, Database = Postgres>
let mut conn = db.acquire().await?;
sqlx::query_as!(
MiniCompletedJob,
"SELECT
j.id, j.workspace_id, j.runnable_id AS \"runnable_id: ScriptHash\", q.scheduled_for, q.started_at, j.parent_job, j.flow_innermost_root_job, j.runnable_path, j.kind as \"kind!: JobKind\", j.permissioned_as,
"SELECT
j.id, j.workspace_id, j.runnable_id AS \"runnable_id: ScriptHash\", q.scheduled_for, q.started_at, j.parent_job, j.flow_innermost_root_job, j.runnable_path, j.kind as \"kind!: JobKind\", j.permissioned_as,
j.created_by, j.script_lang AS \"script_lang: ScriptLang\", j.permissioned_as_email, j.flow_step_id, j.trigger_kind AS \"trigger_kind: JobTriggerKind\", j.trigger, j.priority, j.concurrent_limit, j.tag, j.cache_ttl, q.cache_ignore_s3_path, q.runnable_settings_handle
FROM v2_job j LEFT JOIN v2_job_queue q ON j.id = q.id
WHERE j.id = $1 AND j.workspace_id = $2",
@@ -4619,7 +4619,7 @@ async fn push_inner<'c, 'd>(
user_usage
} else {
sqlx::query_scalar!(
"SELECT usage.usage + 1 FROM usage
"SELECT usage.usage + 1 FROM usage
WHERE is_workspace IS FALSE AND
month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)
AND id = $1",
@@ -4675,7 +4675,7 @@ async fn push_inner<'c, 'd>(
workspace_usage
} else {
sqlx::query_scalar!(
"SELECT usage.usage + 1 FROM usage
"SELECT usage.usage + 1 FROM usage
WHERE is_workspace IS TRUE AND
month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)
AND id = $1",
@@ -5728,7 +5728,7 @@ async fn push_inner<'c, 'd>(
trigger, -- 14
script_lang, -- 15
same_worker, -- 16
pre_run_error, -- 17
pre_run_error, -- 17
permissioned_as_email, -- 18
visible_to_owner, -- 19
flow_innermost_root_job, -- 20
@@ -5751,8 +5751,8 @@ async fn push_inner<'c, 'd>(
INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)
),
inserted_job_perms AS (
INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id, end_user_email)
values ($1, $32, $33, $34, $35, $36, $37, $2, $41)
INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id, end_user_email)
values ($1, $32, $33, $34, $35, $36, $37, $2, $41)
ON CONFLICT (job_id) DO UPDATE SET email = EXCLUDED.email, username = EXCLUDED.username, is_admin = EXCLUDED.is_admin, is_operator = EXCLUDED.is_operator, folders = EXCLUDED.folders, groups = EXCLUDED.groups, workspace_id = EXCLUDED.workspace_id, end_user_email = EXCLUDED.end_user_email
)
INSERT INTO v2_job_queue

View File

@@ -15,17 +15,7 @@ clone_newnet: false
clone_newuser: {CLONE_NEWUSER}
clone_newcgroup: false
uidmap {
inside_id: "1000"
outside_id: ""
count: 1
}
gidmap {
inside_id: "1000"
outside_id: ""
count: 1
}
{UIDGIDMAP}
skip_setsid: true
keep_caps: false

View File

@@ -2079,6 +2079,9 @@ try {{
.replace("{LANG}", if annotation.nodejs { "nodejs" } else { "bun" })
.replace("{JOB_DIR}", job_dir)
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
.replace("{UIDGIDMAP}", if *DISABLE_NUSER { "" } else {
"uidmap {\n inside_id: \"1000\"\n outside_id: \"\"\n count: 1\n}\n\ngidmap {\n inside_id: \"1000\"\n outside_id: \"\"\n count: 1\n}"
})
.replace(
"{SHARED_MOUNT}",
&shared_mount.replace(

View File

@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.684.0";
export const VERSION = "v1.684.1";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({

View File

@@ -78,7 +78,7 @@ export {
token,
};
export const VERSION = "1.684.0";
export const VERSION = "1.684.1";
// Re-exported from constants.ts to maintain backwards compatibility
export { WM_FORK_PREFIX } from "./core/constants.ts";

View File

@@ -1,12 +1,12 @@
{
"name": "windmill-components",
"version": "1.684.0",
"version": "1.684.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "windmill-components",
"version": "1.684.0",
"version": "1.684.1",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@@ -844,6 +844,7 @@
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -855,6 +856,7 @@
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -865,6 +867,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1354,6 +1357,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1510,6 +1514,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1526,6 +1531,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1542,6 +1548,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1558,6 +1565,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1574,6 +1582,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1590,6 +1599,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1606,6 +1616,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1622,6 +1633,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1638,6 +1650,7 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1654,6 +1667,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1670,6 +1684,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1686,6 +1701,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1702,6 +1718,7 @@
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1718,6 +1735,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1734,6 +1752,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2039,6 +2058,7 @@
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -6814,7 +6834,7 @@
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"devOptional": true,
"dev": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
@@ -7313,6 +7333,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7333,6 +7354,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7353,6 +7375,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7373,6 +7396,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7393,6 +7417,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7413,6 +7438,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7433,6 +7459,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7453,6 +7480,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7473,6 +7501,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7493,6 +7522,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -7513,6 +7543,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -12081,6 +12112,21 @@
}
}
},
"node_modules/svelte-check/node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/svelte-eslint-parser": {
"version": "0.43.0",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz",
@@ -12811,7 +12857,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",

View File

@@ -1,6 +1,6 @@
{
"name": "windmill-components",
"version": "1.684.0",
"version": "1.684.1",
"scripts": {
"dev": "vite dev",
"build": "vite build",

View File

@@ -9,6 +9,7 @@ import {
type Script
} from '$lib/gen'
import { initialCode } from '$lib/script_helpers'
import { inferAssets } from '$lib/infer'
import { userStore, workspaceStore } from '$lib/stores'
import { getScriptByPath } from '$lib/scripts'
import { get } from 'svelte/store'
@@ -84,6 +85,11 @@ export async function createInlineScriptModule(
): Promise<[FlowModule, FlowModuleState]> {
const code = initialCode(language, kind, subkind)
// Needed when the predefined code has assets in it
const inferResult = await inferAssets(language, code)
const assets =
inferResult.status === 'ok' && inferResult.assets.length > 0 ? inferResult.assets : undefined
const flowModule: FlowModule = {
id,
summary,
@@ -92,7 +98,8 @@ export async function createInlineScriptModule(
content: code,
language,
input_transforms: {},
...(kind === 'trigger' ? { is_trigger: true } : {})
...(kind === 'trigger' ? { is_trigger: true } : {}),
...(assets ? { assets } : {})
}
}

View File

@@ -4,7 +4,7 @@ verify_ssl = true
name = "pypi"
[packages]
wmill = ">=1.684.0"
wmill = ">=1.684.1"
sendgrid = "*"
mysql-connector-python = "*"
pymongo = "*"

View File

@@ -1,7 +1,7 @@
openapi: '3.0.3'
info:
version: 1.684.0
version: 1.684.1
title: OpenFlow Spec
contact:
name: Ruben Fiszel

View File

@@ -12,7 +12,7 @@
RootModule = 'WindmillClient.psm1'
# Version number of this module.
ModuleVersion = '1.684.0'
ModuleVersion = '1.684.1'
# Supported PSEditions
# CompatiblePSEditions = @()

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "wmill"
version = "1.684.0"
version = "1.684.1"
description = "A client library for accessing Windmill server wrapping the Windmill client API"
license = "Apache-2.0"
homepage = "https://windmill.dev"

View File

@@ -1,6 +1,6 @@
{
"name": "@windmill/windmill",
"version": "1.684.0",
"version": "1.684.1",
"exports": "./src/index.ts",
"publish": {
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"]

View File

@@ -1,7 +1,7 @@
{
"name": "windmill-client",
"description": "Windmill SDK client for browsers and Node.js",
"version": "1.684.0",
"version": "1.684.1",
"author": "Ruben Fiszel",
"license": "Apache 2.0",
"sideEffects": false,

View File

@@ -1 +1 @@
1.684.0
1.684.1