fix(copilot): warn on patch_flow_json inline_script placeholders

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-07-22 14:18:12 +02:00
parent e052d5ee45
commit c7e88e2cce
2 changed files with 67 additions and 5 deletions
@@ -2943,6 +2943,55 @@ describe('global AI tools', () => {
).rejects.toThrow(/Invalid JSON for modules.*set_flow_module_code/s)
})
it('warns when patch_flow_json adds a rawscript module left as an inline_script placeholder', async () => {
const path = 'f/flows/patch-new-module'
await callGlobalTool('write_flow', {
path,
modules: JSON.stringify([
{
id: 'call_api',
value: {
type: 'rawscript',
language: 'bun',
content: 'export async function main() { return 1 }',
input_transforms: {}
}
}
])
})
// A structural patch that adds no module must not trigger the fill-code warning.
const benign = JSON.parse(
await callGlobalTool('patch_flow_json', {
path,
old_string: '"language":"bun"',
new_string: '"language":"deno"'
})
)
expect(benign.success).toBe(true)
expect(benign.message).not.toContain('set_flow_module_code')
// Adding a new rawscript module in the compact view carries the
// inline_script.<id> placeholder as its content; the result must flag it.
const result = JSON.parse(
await callGlobalTool('patch_flow_json', {
path,
old_string: '"input_transforms":{}}}]',
new_string:
'"input_transforms":{}}},{"id":"write_to_pg","value":{"type":"rawscript","language":"postgresql","content":"inline_script.write_to_pg","input_transforms":{}}}]'
})
)
expect(result.success).toBe(true)
expect(result.message).toContain('set_flow_module_code')
expect(result.message).toContain('"write_to_pg"')
expect(result.message).not.toContain('"call_api"')
// The existing module's body must survive the patch round-trip.
await expect(
callGlobalTool('read_flow_module_code', { path, module_id: 'call_api' })
).resolves.toBe('export async function main() { return 1 }')
})
it('writes flows with flow-mode arguments and reads compact flow value', async () => {
const writeResult = JSON.parse(
await callGlobalTool('write_flow', {
@@ -487,10 +487,11 @@ function emptyInlineScriptModuleIds(editable: EditableFlowJson): string[] {
}
/**
* Fold the empty-body warning into `write_flow`'s JSON result but only when the
* save actually succeeded. `writeFlowDraft` reports conflicts/persistence errors
* as `{ success: false }` rather than throwing; telling the model to fill code on
* a flow that was never saved would send it after a stale or nonexistent draft.
* Fold the empty-body warning into a flow write tool's JSON result but only
* when the save actually succeeded. `writeFlowDraft` reports
* conflicts/persistence errors as `{ success: false }` rather than throwing;
* telling the model to fill code on a flow that was never saved would send it
* after a stale or nonexistent draft.
*/
function appendEmptyInlineScriptWarning(result: string, editable: EditableFlowJson): string {
const emptyIds = emptyInlineScriptModuleIds(editable)
@@ -1945,6 +1946,7 @@ function getFlowInstructions(): string {
- \`read_flow_module_code(path, module_id)\` — returns the raw inline script content for one module.
- \`set_flow_module_code(path, module_id, code)\` — overwrites that module's inline script content; saves to the draft.
- Use \`patch_flow_json\` for *structural* edits: module ids, paths, input_transforms, branch arrangement, summaries, preprocessor/failure swaps, schema/groups/notes. Use \`set_flow_module_code\` for changes inside a specific rawscript body.
- When **adding a new rawscript module** with \`patch_flow_json\`, set its \`content\` to the placeholder \`"inline_script.<moduleId>"\` (matching the compact view) — never real code — then **immediately fill the body** with \`set_flow_module_code(path, module_id, code)\`. Until you do, the module's saved content is the literal placeholder string and the flow cannot run; the tool result lists the module ids still awaiting code.
- \`write_flow\` is for full overwrites / create-from-scratch. Its \`modules\`, \`preprocessor_module\`, and \`failure_module\` arguments are **non-compact** flow modules, but you do NOT have to inline the actual rawscript code:
- For a short one-liner body, inline it directly in \`content\`.
- For multi-line code, or any body containing quotes, create the rawscript module with **empty content** (\`"content": ""\`) and fill each body afterwards with \`set_flow_module_code(path, module_id, code)\`. Embedding large code in the \`modules\` JSON string forces triple-nested escaping and frequently corrupts the JSON — keep code out of the flow structure. \`write_flow\` reports which modules still have empty bodies so you know what to fill.
@@ -4357,7 +4359,7 @@ async function patchFlowJson(
const patchedEditable = validateEditableFlowJson(parsedValue)
const newFlowValue = applyEditableFlowJsonToFlow(base.flow.value, patchedEditable, session)
return writeFlowDraft(
const result = await writeFlowDraft(
{
path,
summary: base.summary,
@@ -4370,6 +4372,17 @@ async function patchFlowJson(
},
ctx
)
// A patch that adds a new rawscript module carries an `inline_script.<id>`
// placeholder with no session entry, so the restore above keeps the literal
// placeholder as the saved content. Check the restored value (patchedEditable
// still holds placeholders for every rawscript) and tell the model which
// bodies to fill via set_flow_module_code.
return appendEmptyInlineScriptWarning(result, {
...patchedEditable,
modules: newFlowValue.modules,
preprocessor_module: newFlowValue.preprocessor_module ?? null,
failure_module: newFlowValue.failure_module ?? null
})
}
async function readFlowModuleCode(