mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-18 16:03:24 +00:00
* fix: disable a dynamic input when its schema field is disabled Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * refactor: extract the run form's argument hygiene into job_args Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * feat: give Tabs an opt-in sliding selection indicator Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * refactor: share the chat's scroll-fade measurement Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * feat: give the chat a run-form contract and incremental job output Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * feat: run and test a script from the chat through an argument form Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * feat: carry a chat run's card and job across saves and reloads Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * feat: render a chat run as a tool call row with its form, logs and result Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * feat: open a pending run form in the sessions preview pane Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * test: benchmark running a deployed script from the chat Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ERed9zo2oJjpSczzMayaNh * feat: offer a test run's dynamic options from the draft it previews Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs: say that a test run's dynselect helper executes on form display * fix: send a schema default the model omitted when yolo skips the form * fix: infer a test run's schema when the stored one declares no properties * docs: tighten the note on the form's mount-time helper job * fix: apply a nested schema default the bypass posture counts as answered * fix: apply a declared default to a null value and an optional nested field * fix: check required fields inside a supplied optional object before bypassing * fix: read required args as own properties before bypassing the form * fix: stop the turn from the run form's action row in the preview panel * refactor: drop the run-form prediction and share its secret minting * refactor: prefill a proposed secret instead of emptying the field * docs: correct the comments the run-form prediction left behind * fix: keep a proposed secret out of the chat's stored messages * docs: say what a literal secret argument now does * test: restore the copilotInfo export the aiStore mock omits * docs: cut the run form's helper-script note to its constraints * refactor: settle a run form from one entry and fetch a job's logs once * fix: separate colliding secret paths, gate plan mode, keep polled logs * fix: mint before the form opens, skip empty fields, show what ran * revert: mint a run form's secrets at submit, not before it opens * fix: settle a cancelled run card on the form's arguments, not the proposal * fix: settle a stopped run form like a cancelled one, and keep an empty secret empty * fix: snapshot a run's arguments before minting its secrets --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
131 lines
4.8 KiB
TypeScript
131 lines
4.8 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const created: { path: string; value: string; is_secret?: boolean }[] = []
|
|
|
|
vi.mock('$lib/gen', () => ({
|
|
VariableService: {
|
|
createVariable: vi.fn(async ({ requestBody }: any) => {
|
|
created.push(requestBody)
|
|
return requestBody.path
|
|
})
|
|
}
|
|
}))
|
|
|
|
vi.mock('$lib/stores', async () => {
|
|
const { writable } = await import('svelte/store')
|
|
return { workspaceStore: writable('test-ws'), userStore: writable({ username: 'ada' }) }
|
|
})
|
|
|
|
import { processSecretArgs } from './secretArgUtils'
|
|
|
|
describe('processSecretArgs', () => {
|
|
beforeEach(() => (created.length = 0))
|
|
|
|
const schema = {
|
|
properties: {
|
|
token: { type: 'string', password: true },
|
|
creds: { type: 'object', password: true, properties: { user: { type: 'string' } } },
|
|
nested: { type: 'object', properties: { inner: { type: 'string', password: true } } },
|
|
plain: { type: 'string' }
|
|
}
|
|
} as any
|
|
|
|
// Nothing else turns a proposed secret into a reference when no form mounts, so a literal
|
|
// left alone here is a plaintext credential stored on the job for anyone who can see it.
|
|
it('mints a reference for a literal at every level, leaving other arguments alone', async () => {
|
|
const out = await processSecretArgs(
|
|
{ token: 'hunter2', creds: { user: 'ada' }, nested: { inner: 'deep' }, plain: 'kept' },
|
|
schema
|
|
)
|
|
expect(out.token).toMatch(/^\$var:u\/ada\/secret_arg\//)
|
|
expect(out.creds).toMatch(/^\$jsonvar:u\/ada\/secret_arg\//)
|
|
expect(out.nested.inner).toMatch(/^\$var:u\/ada\/secret_arg\//)
|
|
expect(out.plain).toBe('kept')
|
|
// The object goes into the variable as JSON, which is what `$jsonvar:` parses back.
|
|
expect(created.map((c) => c.value).sort()).toEqual(['deep', 'hunter2', '{"user":"ada"}'])
|
|
expect(created.every((c) => c.is_secret)).toBe(true)
|
|
})
|
|
|
|
it('leaves a reference the caller already named alone', async () => {
|
|
const out = await processSecretArgs(
|
|
{ token: '$var:f/team/api_token', creds: '$jsonvar:u/ada/existing' },
|
|
schema
|
|
)
|
|
expect(out).toEqual({ token: '$var:f/team/api_token', creds: '$jsonvar:u/ada/existing' })
|
|
expect(created).toEqual([])
|
|
})
|
|
|
|
// `$var:` hands the job the variable's text; a field declaring an object needs it parsed,
|
|
// which is the same variable read the other way rather than a secret the caller cannot see.
|
|
it('reads a plain variable as JSON where the field cannot hold a string', async () => {
|
|
const out = await processSecretArgs({ creds: '$var:u/ada/stripe' }, schema)
|
|
expect(out.creds).toBe('$jsonvar:u/ada/stripe')
|
|
expect(created).toEqual([])
|
|
})
|
|
|
|
// Reached by every run form in the product, not only the ones a chat opens.
|
|
it('leaves an absent, null or empty secret alone', async () => {
|
|
expect(await processSecretArgs({ token: null, plain: 'kept' }, schema)).toEqual({
|
|
token: null,
|
|
plain: 'kept'
|
|
})
|
|
expect(await processSecretArgs({ token: '', plain: 'kept' }, schema)).toEqual({
|
|
token: '',
|
|
plain: 'kept'
|
|
})
|
|
expect(created).toEqual([])
|
|
})
|
|
|
|
// A property name can itself contain a dot. Reported under one label these two leaves
|
|
// would share a mint, and the flat field would run on the nested field's secret.
|
|
it("tells apart a key that spells another key's path", async () => {
|
|
const out = await processSecretArgs({ 'db.password': 'FLAT', db: { password: 'NESTED' } }, {
|
|
properties: {
|
|
'db.password': { type: 'string', password: true },
|
|
db: {
|
|
type: 'object',
|
|
properties: { password: { type: 'string', password: true } }
|
|
}
|
|
}
|
|
} as any)
|
|
const flat = out['db.password'].slice('$var:'.length)
|
|
const nested = out.db.password.slice('$var:'.length)
|
|
expect(flat).not.toBe(nested)
|
|
expect(created.find((c) => c.path === flat)?.value).toBe('FLAT')
|
|
expect(created.find((c) => c.path === nested)?.value).toBe('NESTED')
|
|
})
|
|
|
|
// Callers bind a form that stays editable while the mints are in flight, and a leaf is
|
|
// addressed by its path: a row moved between the two walks would take the other row's
|
|
// reference and the job would run it on the wrong credentials.
|
|
it('ignores the caller mutating the arguments while minting', async () => {
|
|
const rows = {
|
|
creds: [
|
|
{ name: 'alpha', secret: 'FIRST' },
|
|
{ name: 'beta', secret: 'SECOND' }
|
|
]
|
|
}
|
|
const arraySchema = {
|
|
properties: {
|
|
creds: {
|
|
type: 'array',
|
|
items: { properties: { name: {}, secret: { password: true } } }
|
|
}
|
|
}
|
|
} as any
|
|
|
|
const pending = processSecretArgs(rows, arraySchema)
|
|
rows.creds.reverse()
|
|
const out = await pending
|
|
|
|
// Keyed by the row's own name, not its index: a substitution by position lands the
|
|
// first-minted reference on index 0 either way.
|
|
const secretOf = (name: string) => {
|
|
const row = out.creds.find((c: any) => c.name === name)
|
|
return created.find((c) => c.path === row.secret.slice('$var:'.length))?.value
|
|
}
|
|
expect(secretOf('alpha')).toBe('FIRST')
|
|
expect(secretOf('beta')).toBe('SECOND')
|
|
})
|
|
})
|