mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
fix: keep a script draft's password marking through the chat's run form (#11110)
* fix: keep a script draft's password marking through the chat's run form Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dx9oMWHnKywoqooZBeaPCy * docs: describe what the inferArgs test mock actually lets the suite pin Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dx9oMWHnKywoqooZBeaPCy --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
80eba80d6e
commit
56dd940e34
@@ -321,8 +321,9 @@ vi.mock('./rawAppBundlerBridge', () => ({
|
||||
|
||||
vi.mock('$lib/infer', async () => ({
|
||||
...(await vi.importActual<any>('$lib/infer')),
|
||||
// Avoid the wasm parser in unit tests: the script deploy path infers the arg
|
||||
// schema but tolerates failure, and these tests don't assert on the schema.
|
||||
// Avoid the wasm parser in unit tests. A no-op passes the seeded schema through
|
||||
// untouched, which is what lets the password-marking test pin how a schema is
|
||||
// seeded in and carried out without pinning inference's own merge rules.
|
||||
inferArgs: vi.fn(async () => {})
|
||||
}))
|
||||
|
||||
@@ -4358,6 +4359,43 @@ describe('global AI tools', () => {
|
||||
expect(result).toContain('test logs')
|
||||
})
|
||||
|
||||
// No parser emits `password`, so the stored schema is the only thing carrying it: an edit
|
||||
// that rewrites the draft's schema from scratch, or a draft read that drops it, unmarks
|
||||
// the field — and the form then takes the secret as a plain literal into the job's args.
|
||||
it('test_run_script keeps the password marking of the script it previews', async () => {
|
||||
vi.mocked(ScriptService.existsScriptByPath).mockResolvedValueOnce(true)
|
||||
vi.mocked(ScriptService.getScriptByPath).mockResolvedValueOnce({
|
||||
path: 'f/scripts/secretful',
|
||||
language: 'bun',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: { token: { type: 'string', password: true } },
|
||||
required: ['token']
|
||||
}
|
||||
} as any)
|
||||
|
||||
await callGlobalTool('write_script', {
|
||||
path: 'f/scripts/secretful',
|
||||
language: 'bun',
|
||||
content: 'export async function main(token: string) { return 1 }'
|
||||
})
|
||||
|
||||
let form: any
|
||||
await callGlobalTool(
|
||||
'test_run_script',
|
||||
{ path: 'f/scripts/secretful' },
|
||||
{
|
||||
...toolCallbacks,
|
||||
requestRunArgs: async (_toolId, opened) => {
|
||||
form = opened
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
expect(form?.schema?.properties).toMatchObject({ token: { password: true } })
|
||||
})
|
||||
|
||||
it('test_run_script previews deployed script content when no draft exists', async () => {
|
||||
vi.mocked(ScriptService.getScriptByPath).mockResolvedValueOnce({
|
||||
path: 'f/scripts/deployed-test',
|
||||
|
||||
@@ -148,6 +148,7 @@ import type { SessionArtifactsStore } from '../artifacts/artifactsState.svelte'
|
||||
import type { Runnable } from '$lib/components/apps/inputType'
|
||||
import { UserDraft } from '$lib/userDraft.svelte'
|
||||
import { emptySchema } from '$lib/utils'
|
||||
import type { Schema } from '$lib/common'
|
||||
import { inferArgs } from '$lib/infer'
|
||||
import {
|
||||
resourceRequestSchema,
|
||||
@@ -3424,7 +3425,8 @@ export const globalTools: Tool<{}>[] = [
|
||||
draftCountByType.set(draft.type, count + 1)
|
||||
byKey.set(getWorkspaceItemKey(draft.type, draft.path, draft.triggerKind), {
|
||||
...draft,
|
||||
value: undefined
|
||||
value: undefined,
|
||||
schema: undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5033,11 +5035,14 @@ const SCRIPT_SPEC: WriteSpec<NewScript, ScriptDraftArgs> = {
|
||||
language: args.language,
|
||||
kind: 'script'
|
||||
}
|
||||
// Infer the arg schema from the content at save time, like the editor does,
|
||||
// so the persisted draft is the single source of truth at deploy. Keep the
|
||||
// previous schema (or empty) on failure rather than blanking it.
|
||||
// Into the schema the base carries, as the editor does at save: `inferArgs` re-seeds
|
||||
// each arg from the properties it is handed, and those are the only copy of
|
||||
// `password`, enums, formats and titles — no parser emits them. A clone, so a parse
|
||||
// failure leaves the previous schema rather than half of one.
|
||||
try {
|
||||
const schema = emptySchema()
|
||||
const schema = structuredClone(
|
||||
draft.schema?.properties ? draft.schema : emptySchema()
|
||||
) as Schema
|
||||
await inferArgs(draft.language, draft.content, schema)
|
||||
draft.schema = schema
|
||||
} catch (e) {
|
||||
@@ -5224,10 +5229,9 @@ async function loadScriptForEdit(
|
||||
}
|
||||
}
|
||||
|
||||
/** The fields a test form offers, for code that may never have been deployed. A draft the
|
||||
* chat wrote carries the schema it inferred at write time; anything else — a draft written
|
||||
* elsewhere, a deployed script whose schema predates an edit — is inferred here from the
|
||||
* content that is about to run, so the form cannot offer a field the code no longer takes. */
|
||||
/** The fields a test form offers, for code that may never have been deployed. The stored
|
||||
* schema wins wherever it declares fields — a draft's or the deployed script's; only one
|
||||
* declaring nothing is inferred here, from the content about to run. */
|
||||
async function schemaForTestRun(script: {
|
||||
content: string
|
||||
language: ScriptLang
|
||||
|
||||
@@ -141,6 +141,7 @@ function scriptDraftToWorkspaceItem(path: string, draft: NewScript): WorkspaceIt
|
||||
summary: draft.summary,
|
||||
language: draft.language,
|
||||
value: draft.content,
|
||||
schema: draft.schema,
|
||||
parentHash: draft.parent_hash,
|
||||
isDraft: true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user