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:
AlexRV12
2026-09-14 15:55:40 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 80eba80d6e
commit 56dd940e34
3 changed files with 54 additions and 11 deletions
@@ -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
}