From 2901b973d24508230447a9587e46be5054ad4e10 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 30 Aug 2026 12:04:11 -0700 Subject: [PATCH 1/9] fix(github): wire the block fields whose ids do not match their tool params --- .../blocks/blocks/github.param-wiring.test.ts | 140 ++++++++++++++++++ apps/sim/blocks/blocks/github.ts | 40 +++++ 2 files changed, 180 insertions(+) create mode 100644 apps/sim/blocks/blocks/github.param-wiring.test.ts diff --git a/apps/sim/blocks/blocks/github.param-wiring.test.ts b/apps/sim/blocks/blocks/github.param-wiring.test.ts new file mode 100644 index 00000000000..d7bc1737252 --- /dev/null +++ b/apps/sim/blocks/blocks/github.param-wiring.test.ts @@ -0,0 +1,140 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' + +// vitest.setup.ts stubs `@/tools/registry` to an empty map so unrelated suites +// do not pay to load every tool. This suite asserts against the REAL tool +// params, so it opts back in. +vi.unmock('@/tools/registry') + +import { GitHubBlock, GitHubV2Block } from '@/blocks/blocks/github' +import { getTool } from '@/tools/utils' + +function map(params: Record): Record { + const fn = GitHubBlock.tools.config?.params + if (!fn) throw new Error('GitHub block declares no params mapper') + return fn(params) as Record +} + +/** + * Each pair is (block subBlock id, tool param name, tool id). The serializer + * keys values by subBlock id, so without the mapper the tool param stays + * undefined and the field is inert. + */ +const RENAMES = [ + ['reaction_content', 'content', 'github_create_issue_reaction'], + ['reaction_content', 'content', 'github_create_comment_reaction'], + ['milestone_title', 'title', 'github_create_milestone'], + ['milestone_title', 'title', 'github_update_milestone'], + ['milestone_description', 'description', 'github_create_milestone'], + ['milestone_description', 'description', 'github_update_milestone'], + ['milestone_state', 'state', 'github_list_milestones'], + ['milestone_sort', 'sort', 'github_list_milestones'], + ['fork_name', 'name', 'github_fork_repo'], + ['fork_sort', 'sort', 'github_list_forks'], + ['gist_public', 'public', 'github_create_gist'], +] as const + +describe('every renamed subBlock reaches its tool param', () => { + it.each(RENAMES)('%s -> %s', (subBlockId, paramName) => { + expect(map({ [subBlockId]: 'x' })).toHaveProperty(paramName) + }) + + it.each(RENAMES)( + '%s targets a param the tool really declares (%s on %s)', + (_s, paramName, toolId) => { + const tool = getTool(toolId) + expect(tool, `${toolId} is not registered`).toBeDefined() + expect(Object.keys(tool!.params ?? {})).toContain(paramName) + } + ) + + it.each(RENAMES)('%s is not itself a param of %s', (subBlockId, _p, toolId) => { + expect(Object.keys(getTool(toolId)!.params ?? {})).not.toContain(subBlockId) + }) +}) + +/** + * The mapper runs as the provider `paramsTransform` too, spreading over the + * model's tool-call arguments. An unguarded assignment would overwrite a + * model-supplied value with undefined — the agent path is the only path these + * fields work on today, so it must not regress. + */ +describe('guarded assignment protects the agent tool-calling path', () => { + it('emits nothing when no source field is present', () => { + expect(map({ operation: 'github_create_milestone' })).toEqual({}) + }) + + it.each(RENAMES)('never writes %s target as undefined', (_s, paramName) => { + expect(map({ operation: 'x' })).not.toHaveProperty(paramName) + }) + + it('leaves a model-supplied value untouched when the block field is absent', () => { + const modelArgs = { content: 'rocket', title: 'from the model' } + expect({ ...modelArgs, ...map(modelArgs) }).toEqual(modelArgs) + }) + + it.each(['', null, undefined])('treats %o as not provided', (empty) => { + expect(map({ reaction_content: empty })).toEqual({}) + }) +}) + +describe('gist_public coercion', () => { + it.each([ + ['true', true], + ['false', false], + [true, true], + ])('maps %o to %o', (input, expected) => { + expect(map({ gist_public: input }).public).toBe(expected) + }) + + it('omits public entirely when untouched, leaving the tool default', () => { + expect(map({ operation: 'github_create_gist' })).not.toHaveProperty('public') + }) + + it('matches the dropdown option ids the block actually renders', () => { + const sub = GitHubBlock.subBlocks.find((s) => s.id === 'gist_public') + expect(sub?.options).toBeDefined() + const ids = (sub!.options as Array<{ id: string }>).map((o) => o.id) + expect(ids).toEqual(['false', 'true']) + }) +}) + +/** Sources that share a target are safe only if their conditions are disjoint. */ +describe('sources sharing a target param are condition-disjoint', () => { + function opsFor(id: string): string[][] { + return GitHubBlock.subBlocks + .filter((s) => s.id === id) + .map((s) => { + const v = (s.condition as { value?: unknown })?.value + return Array.isArray(v) ? (v as string[]) : [v as string] + }) + } + it.each([ + ['sort', ['fork_sort', 'milestone_sort']], + ['title', ['milestone_title']], + ['description', ['milestone_description']], + ['state', ['milestone_state']], + ['content', ['reaction_content']], + ['name', ['fork_name']], + ['public', ['gist_public']], + ] as const)('%s sources never render together', (_target, sources) => { + const seen = new Set() + for (const src of sources) { + for (const group of opsFor(src)) { + for (const op of group) { + expect(seen.has(op), `${op} renders two sources of the same target`).toBe(false) + seen.add(op) + } + } + } + expect(seen.size).toBeGreaterThan(0) + }) +}) + +describe('the v2 block inherits the same mapper', () => { + it('forwards params from the v1 block', () => { + expect(GitHubV2Block.tools.config?.params).toBe(GitHubBlock.tools.config?.params) + }) +}) diff --git a/apps/sim/blocks/blocks/github.ts b/apps/sim/blocks/blocks/github.ts index b38c15f26c5..f878cf65645 100644 --- a/apps/sim/blocks/blocks/github.ts +++ b/apps/sim/blocks/blocks/github.ts @@ -2279,6 +2279,46 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, return 'github_repo_info' } }, + /** + * Bridges the subBlock ids that do not match their tool's param name. + * + * A tool param is populated only when a subBlock's `id` equals it — the + * serializer keys values by subBlock id, and nothing else renames them. + * Each field below renders, accepts input, and then arrives under a name + * its tool never reads. + * + * Every assignment is guarded, and that is load-bearing rather than + * defensive. `generic-handler.ts` merges `{ ...inputs, ...params(inputs) }` + * and `providers/utils.ts` installs this same function as the provider + * `paramsTransform`, spreading its result over the model's tool-call + * arguments. An unconditional write would therefore clobber a + * model-supplied `content`/`title`/`sort` with `undefined` on the agent + * tool-calling path — which is the one path these fields work on today. + * + * `sort` has two sources and `title`/`description`/`state` share their + * names with fields on other operations. That is safe only because each + * source subBlock's `condition` binds it to a single operation, so at + * most one source of a given target is ever present in `params`. + */ + params: (params) => { + const result: Record = {} + + if (params.reaction_content) result.content = params.reaction_content + if (params.milestone_title) result.title = params.milestone_title + if (params.milestone_description) result.description = params.milestone_description + if (params.milestone_state) result.state = params.milestone_state + if (params.milestone_sort) result.sort = params.milestone_sort + if (params.fork_name) result.name = params.fork_name + if (params.fork_sort) result.sort = params.fork_sort + + // A dropdown stores its option id, so this arrives as the string + // 'true'/'false' while the tool declares `public` as a boolean. + if (params.gist_public) { + result.public = params.gist_public === true || params.gist_public === 'true' + } + + return result + }, }, }, inputs: { From 10f9da7b5956be67b66c281d8af61ea1a77193b4 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 30 Aug 2026 12:12:17 -0700 Subject: [PATCH 2/9] chore(github): use TSDoc for the new wiring comments --- apps/sim/blocks/blocks/github.param-wiring.test.ts | 8 +++++--- apps/sim/blocks/blocks/github.ts | 8 ++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/sim/blocks/blocks/github.param-wiring.test.ts b/apps/sim/blocks/blocks/github.param-wiring.test.ts index d7bc1737252..497e8e9c37e 100644 --- a/apps/sim/blocks/blocks/github.param-wiring.test.ts +++ b/apps/sim/blocks/blocks/github.param-wiring.test.ts @@ -3,9 +3,11 @@ */ import { describe, expect, it, vi } from 'vitest' -// vitest.setup.ts stubs `@/tools/registry` to an empty map so unrelated suites -// do not pay to load every tool. This suite asserts against the REAL tool -// params, so it opts back in. +/** + * Uses the real tool registry: these assertions are about the params GitHub's + * tools actually declare, which the global `@/tools/registry` mock in + * vitest.setup.ts empties. + */ vi.unmock('@/tools/registry') import { GitHubBlock, GitHubV2Block } from '@/blocks/blocks/github' diff --git a/apps/sim/blocks/blocks/github.ts b/apps/sim/blocks/blocks/github.ts index f878cf65645..891d7eb385e 100644 --- a/apps/sim/blocks/blocks/github.ts +++ b/apps/sim/blocks/blocks/github.ts @@ -2311,8 +2311,12 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, if (params.fork_name) result.name = params.fork_name if (params.fork_sort) result.sort = params.fork_sort - // A dropdown stores its option id, so this arrives as the string - // 'true'/'false' while the tool declares `public` as a boolean. + /** + * A dropdown stores its option id, so this arrives as the string + * 'true'/'false' while the tool declares `public` as a boolean. The + * generic handler only JSON-parses `json`/`array` inputs, so nothing + * else coerces it. + */ if (params.gist_public) { result.public = params.gist_public === true || params.gist_public === 'true' } From ff0eb6e4a39798437acf7ce9db973f790acd50c6 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 30 Aug 2026 12:22:30 -0700 Subject: [PATCH 3/9] fix(github): treat boolean false as an explicit gist visibility choice --- .../blocks/blocks/github.param-wiring.test.ts | 21 +++++++++++++++++++ apps/sim/blocks/blocks/github.ts | 14 ++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/apps/sim/blocks/blocks/github.param-wiring.test.ts b/apps/sim/blocks/blocks/github.param-wiring.test.ts index 497e8e9c37e..43ef7ca3963 100644 --- a/apps/sim/blocks/blocks/github.param-wiring.test.ts +++ b/apps/sim/blocks/blocks/github.param-wiring.test.ts @@ -95,6 +95,27 @@ describe('gist_public coercion', () => { expect(map({ operation: 'github_create_gist' })).not.toHaveProperty('public') }) + it.each([null, undefined, ''])('treats %o as unset rather than Secret', (unset) => { + expect(map({ gist_public: unset })).not.toHaveProperty('public') + }) + + /** + * The block declares this input as `boolean`, so a writer following that + * schema stores `false` rather than the dropdown's `'false'`. Both mean the + * user chose Secret, and a truthy presence check would silently drop one of + * them — letting a model-supplied `public: true` through on the agent path. + */ + it.each([ + ['false', false], + [false, false], + ])('treats %o as an explicit Secret selection', (input, expected) => { + expect(map({ gist_public: input }).public).toBe(expected) + }) + + it.each(['false', false])('overrides a model-supplied public for %o', (secret) => { + expect({ public: true, ...map({ gist_public: secret }) }.public).toBe(false) + }) + it('matches the dropdown option ids the block actually renders', () => { const sub = GitHubBlock.subBlocks.find((s) => s.id === 'gist_public') expect(sub?.options).toBeDefined() diff --git a/apps/sim/blocks/blocks/github.ts b/apps/sim/blocks/blocks/github.ts index 891d7eb385e..22a2f2e7dd8 100644 --- a/apps/sim/blocks/blocks/github.ts +++ b/apps/sim/blocks/blocks/github.ts @@ -2316,8 +2316,20 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, * 'true'/'false' while the tool declares `public` as a boolean. The * generic handler only JSON-parses `json`/`array` inputs, so nothing * else coerces it. + * + * Presence is tested rather than truthiness, because boolean `false` is + * a real selection: the block declares this input as `boolean`, so a + * writer following that schema stores `false` rather than `'false'`. + * Under a truthy check the two disagree — `'false'` would force the + * gist secret while `false` was dropped, letting a model-supplied + * `public: true` through on the agent path. Only an unset field + * (nullish or empty) defers to the tool's own default. */ - if (params.gist_public) { + if ( + params.gist_public !== undefined && + params.gist_public !== null && + params.gist_public !== '' + ) { result.public = params.gist_public === true || params.gist_public === 'true' } From 99badc433a3719117517d8a4edbd3693cfaba6f3 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 30 Aug 2026 12:35:22 -0700 Subject: [PATCH 4/9] fix(github): scope each param alias to the operations that declare it --- .../blocks/blocks/github.param-wiring.test.ts | 74 +++++++++++-- apps/sim/blocks/blocks/github.ts | 104 +++++++++++------- 2 files changed, 130 insertions(+), 48 deletions(-) diff --git a/apps/sim/blocks/blocks/github.param-wiring.test.ts b/apps/sim/blocks/blocks/github.param-wiring.test.ts index 43ef7ca3963..2a84e893d5c 100644 --- a/apps/sim/blocks/blocks/github.param-wiring.test.ts +++ b/apps/sim/blocks/blocks/github.param-wiring.test.ts @@ -39,8 +39,8 @@ const RENAMES = [ ] as const describe('every renamed subBlock reaches its tool param', () => { - it.each(RENAMES)('%s -> %s', (subBlockId, paramName) => { - expect(map({ [subBlockId]: 'x' })).toHaveProperty(paramName) + it.each(RENAMES)('%s -> %s', (subBlockId, paramName, toolId) => { + expect(map({ operation: toolId, [subBlockId]: 'x' })).toHaveProperty(paramName) }) it.each(RENAMES)( @@ -73,12 +73,16 @@ describe('guarded assignment protects the agent tool-calling path', () => { }) it('leaves a model-supplied value untouched when the block field is absent', () => { - const modelArgs = { content: 'rocket', title: 'from the model' } + const modelArgs = { + operation: 'github_create_issue_reaction', + content: 'rocket', + title: 'from the model', + } expect({ ...modelArgs, ...map(modelArgs) }).toEqual(modelArgs) }) it.each(['', null, undefined])('treats %o as not provided', (empty) => { - expect(map({ reaction_content: empty })).toEqual({}) + expect(map({ operation: 'github_create_issue_reaction', reaction_content: empty })).toEqual({}) }) }) @@ -88,7 +92,7 @@ describe('gist_public coercion', () => { ['false', false], [true, true], ])('maps %o to %o', (input, expected) => { - expect(map({ gist_public: input }).public).toBe(expected) + expect(map({ operation: 'github_create_gist', gist_public: input }).public).toBe(expected) }) it('omits public entirely when untouched, leaving the tool default', () => { @@ -96,7 +100,9 @@ describe('gist_public coercion', () => { }) it.each([null, undefined, ''])('treats %o as unset rather than Secret', (unset) => { - expect(map({ gist_public: unset })).not.toHaveProperty('public') + expect(map({ operation: 'github_create_gist', gist_public: unset })).not.toHaveProperty( + 'public' + ) }) /** @@ -109,11 +115,12 @@ describe('gist_public coercion', () => { ['false', false], [false, false], ])('treats %o as an explicit Secret selection', (input, expected) => { - expect(map({ gist_public: input }).public).toBe(expected) + expect(map({ operation: 'github_create_gist', gist_public: input }).public).toBe(expected) }) it.each(['false', false])('overrides a model-supplied public for %o', (secret) => { - expect({ public: true, ...map({ gist_public: secret }) }.public).toBe(false) + const inputs = { operation: 'github_create_gist', gist_public: secret } + expect({ public: true, ...map(inputs) }.public).toBe(false) }) it('matches the dropdown option ids the block actually renders', () => { @@ -161,3 +168,54 @@ describe('the v2 block inherits the same mapper', () => { expect(GitHubV2Block.tools.config?.params).toBe(GitHubBlock.tools.config?.params) }) }) + +/** + * `shouldSerializeSubBlock` (`serializer/index.ts:91-93`) serializes a + * non-empty `mode: 'advanced'` field WITHOUT evaluating its condition. Seven of + * the aliased sources are advanced, so a value left behind by an earlier + * operation is still present in `params` after the user switches operations. + * An unscoped alias would rewrite it onto the new operation's tool param. + */ +describe('a stale advanced field cannot leak onto another operation', () => { + it('does not turn a leftover milestone_title into github_update_pr title', () => { + const mapped = map({ operation: 'github_update_pr', milestone_title: 'Q3 milestone' }) + expect(mapped).not.toHaveProperty('title') + }) + + it('does not clobber the PR title the user actually typed', () => { + const inputs = { + operation: 'github_update_pr', + title: 'Fix the parser', + milestone_title: 'Q3 milestone', + } + expect({ ...inputs, ...map(inputs) }.title).toBe('Fix the parser') + }) + + it.each([ + ['github_create_pr', 'milestone_title', 'title'], + ['github_create_issue', 'milestone_description', 'description'], + ['github_list_issues', 'milestone_state', 'state'], + ['github_search_repos', 'milestone_sort', 'sort'], + ['github_search_repos', 'fork_sort', 'sort'], + ['github_create_gist', 'fork_name', 'name'], + ['github_update_project', 'gist_public', 'public'], + ])('%s ignores a stale %s', (operation, from, to) => { + expect(map({ operation, [from]: 'stale' })).not.toHaveProperty(to) + }) + + it('every advanced source is scoped to at least one operation', () => { + const advanced = GitHubBlock.subBlocks.filter((s) => s.mode === 'advanced').map((s) => s.id) + for (const src of [ + 'milestone_title', + 'milestone_description', + 'milestone_state', + 'milestone_sort', + 'fork_name', + 'fork_sort', + 'gist_public', + ]) { + expect(advanced, `${src} is expected to be an advanced field`).toContain(src) + expect(map({ operation: '', [src]: 'x' })).toEqual({}) + } + }) +}) diff --git a/apps/sim/blocks/blocks/github.ts b/apps/sim/blocks/blocks/github.ts index 22a2f2e7dd8..f51dd9a8c08 100644 --- a/apps/sim/blocks/blocks/github.ts +++ b/apps/sim/blocks/blocks/github.ts @@ -9,6 +9,44 @@ import { getTrigger } from '@/triggers' /** Reviewers can be named individually or by team slug; either identifies the request. */ const REVIEWER_FIELD = ['reviewers', 'team_reviewers'] as const +/** + * Block subBlock ids that differ from the tool param they feed, each scoped to + * the operations whose tool declares that target. `sort` has two sources and + * `title`/`description`/`state` share their names with fields on other + * operations, so the scoping is what keeps them from colliding. + * + * `toBoolean` marks a dropdown feeding a boolean tool param: a dropdown stores + * its option id, so the value arrives as the string 'true'/'false' and the + * generic handler only JSON-parses `json`/`array` inputs. + */ +const GITHUB_PARAM_ALIASES: ReadonlyArray<{ + from: string + to: string + operations: readonly string[] + toBoolean?: true +}> = [ + { + from: 'reaction_content', + to: 'content', + operations: ['github_create_issue_reaction', 'github_create_comment_reaction'], + }, + { + from: 'milestone_title', + to: 'title', + operations: ['github_create_milestone', 'github_update_milestone'], + }, + { + from: 'milestone_description', + to: 'description', + operations: ['github_create_milestone', 'github_update_milestone'], + }, + { from: 'milestone_state', to: 'state', operations: ['github_list_milestones'] }, + { from: 'milestone_sort', to: 'sort', operations: ['github_list_milestones'] }, + { from: 'fork_name', to: 'name', operations: ['github_fork_repo'] }, + { from: 'fork_sort', to: 'sort', operations: ['github_list_forks'] }, + { from: 'gist_public', to: 'public', operations: ['github_create_gist'], toBoolean: true }, +] + export const GitHubBlock: BlockConfig = { type: 'github', name: 'GitHub (Legacy)', @@ -2284,53 +2322,39 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, * * A tool param is populated only when a subBlock's `id` equals it — the * serializer keys values by subBlock id, and nothing else renames them. - * Each field below renders, accepts input, and then arrives under a name - * its tool never reads. + * Each aliased field below renders, accepts input, and then arrives under + * a name its tool never reads. + * + * Every alias is scoped to the operations whose tool actually declares + * the target param, and that scoping is load-bearing. Seven of these + * sources are `mode: 'advanced'`, and `shouldSerializeSubBlock` + * (`serializer/index.ts:91-93`) serializes a non-empty advanced field + * WITHOUT evaluating its condition. So a `milestone_title` left over from + * an earlier operation is still in `params` after the user switches to, + * say, Update PR — and an unscoped alias would rewrite it to `title` and + * clobber the PR's own title with stale milestone data. * - * Every assignment is guarded, and that is load-bearing rather than - * defensive. `generic-handler.ts` merges `{ ...inputs, ...params(inputs) }` - * and `providers/utils.ts` installs this same function as the provider - * `paramsTransform`, spreading its result over the model's tool-call - * arguments. An unconditional write would therefore clobber a - * model-supplied `content`/`title`/`sort` with `undefined` on the agent - * tool-calling path — which is the one path these fields work on today. + * Presence is tested rather than truthiness so that a deliberate `false` + * or `'false'` is not mistaken for an unset field; only nullish and empty + * defer to the tool's own default. * - * `sort` has two sources and `title`/`description`/`state` share their - * names with fields on other operations. That is safe only because each - * source subBlock's `condition` binds it to a single operation, so at - * most one source of a given target is ever present in `params`. + * `generic-handler.ts` merges `{ ...inputs, ...params(inputs) }` and + * `providers/utils.ts` installs this as the provider `paramsTransform`, + * spreading over the model's tool-call arguments — so emitting a key the + * block did not supply would clobber a model-supplied value on the agent + * path, which is the one path these fields work on today. */ params: (params) => { const result: Record = {} + const operation = typeof params.operation === 'string' ? params.operation : '' - if (params.reaction_content) result.content = params.reaction_content - if (params.milestone_title) result.title = params.milestone_title - if (params.milestone_description) result.description = params.milestone_description - if (params.milestone_state) result.state = params.milestone_state - if (params.milestone_sort) result.sort = params.milestone_sort - if (params.fork_name) result.name = params.fork_name - if (params.fork_sort) result.sort = params.fork_sort + const isSet = (value: unknown) => value !== undefined && value !== null && value !== '' - /** - * A dropdown stores its option id, so this arrives as the string - * 'true'/'false' while the tool declares `public` as a boolean. The - * generic handler only JSON-parses `json`/`array` inputs, so nothing - * else coerces it. - * - * Presence is tested rather than truthiness, because boolean `false` is - * a real selection: the block declares this input as `boolean`, so a - * writer following that schema stores `false` rather than `'false'`. - * Under a truthy check the two disagree — `'false'` would force the - * gist secret while `false` was dropped, letting a model-supplied - * `public: true` through on the agent path. Only an unset field - * (nullish or empty) defers to the tool's own default. - */ - if ( - params.gist_public !== undefined && - params.gist_public !== null && - params.gist_public !== '' - ) { - result.public = params.gist_public === true || params.gist_public === 'true' + for (const alias of GITHUB_PARAM_ALIASES) { + if (!alias.operations.includes(operation)) continue + const value = params[alias.from] + if (!isSet(value)) continue + result[alias.to] = alias.toBoolean ? value === true || value === 'true' : value } return result From 4a60d1725f4624a5cf75d9d6aadf3fc7dd26fd92 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 30 Aug 2026 12:44:01 -0700 Subject: [PATCH 5/9] docs(github): record why an absent operation degrades to a no-op --- .../blocks/blocks/github.param-wiring.test.ts | 24 +++++++++++++++++++ apps/sim/blocks/blocks/github.ts | 12 +++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/apps/sim/blocks/blocks/github.param-wiring.test.ts b/apps/sim/blocks/blocks/github.param-wiring.test.ts index 2a84e893d5c..3696c5e8d36 100644 --- a/apps/sim/blocks/blocks/github.param-wiring.test.ts +++ b/apps/sim/blocks/blocks/github.param-wiring.test.ts @@ -219,3 +219,27 @@ describe('a stale advanced field cannot leak onto another operation', () => { } }) }) + +/** + * `providers/utils.ts` builds the provider `paramsTransform` input from + * `block.params` alone (`:776`), spreading `operation` in only for the + * tool-selection call (`:736-739`). Without an operation every alias skips — + * which must remain a no-op rather than a clobber, since that is exactly the + * behaviour that existed before this mapper. + */ +describe('absent operation degrades to a no-op, never a clobber', () => { + it.each([{}, { operation: undefined }, { operation: '' }, { operation: 123 }])( + 'emits nothing for %o', + (opShape) => { + expect( + map({ ...opShape, milestone_title: 'x', reaction_content: 'heart', gist_public: 'true' }) + ).toEqual({}) + } + ) + + it('leaves every model-supplied value intact', () => { + const modelArgs = { content: 'rocket', title: 'model title', sort: 'newest', public: true } + const stale = { milestone_title: 'stale', fork_sort: 'oldest', gist_public: 'false' } + expect({ ...modelArgs, ...map({ ...modelArgs, ...stale }) }).toEqual(modelArgs) + }) +}) diff --git a/apps/sim/blocks/blocks/github.ts b/apps/sim/blocks/blocks/github.ts index f51dd9a8c08..71547b70d64 100644 --- a/apps/sim/blocks/blocks/github.ts +++ b/apps/sim/blocks/blocks/github.ts @@ -2342,7 +2342,17 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, * `providers/utils.ts` installs this as the provider `paramsTransform`, * spreading over the model's tool-call arguments — so emitting a key the * block did not supply would clobber a model-supplied value on the agent - * path, which is the one path these fields work on today. + * path. + * + * On the agent tool-calling path `operation` is not part of the params + * this receives: `providers/utils.ts` spreads it in for the tool-selection + * call (`:736-739`) but builds the transform's input from `block.params` + * alone (`:776`). Every alias therefore skips there, which is the same + * behaviour as before this mapper existed - the agent path already works + * because a model supplies `content`/`title`/`sort` by their real names. + * That gap is shared by every block whose mapper branches on + * `params.operation`, so closing it belongs in the provider layer rather + * than here. */ params: (params) => { const result: Record = {} From 5558f6f81af17610017e6ba18537d152f9a2e5b8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 30 Aug 2026 13:50:52 -0700 Subject: [PATCH 6/9] test(github): pin the gist visibility and list-filter outcomes --- .../blocks/blocks/github.param-wiring.test.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/apps/sim/blocks/blocks/github.param-wiring.test.ts b/apps/sim/blocks/blocks/github.param-wiring.test.ts index 3696c5e8d36..b3ec9a29c5a 100644 --- a/apps/sim/blocks/blocks/github.param-wiring.test.ts +++ b/apps/sim/blocks/blocks/github.param-wiring.test.ts @@ -243,3 +243,42 @@ describe('absent operation degrades to a no-op, never a clobber', () => { expect({ ...modelArgs, ...map({ ...modelArgs, ...stale }) }).toEqual(modelArgs) }) }) + +/** + * `milestone_state` is a LIST filter. `create_milestone` and `update_milestone` + * also declare a `state` param, but no subBlock renders for them, so the value + * sitting in `params` is only ever a leftover from the list operation. Aliasing + * it onto those would silently set the state of a milestone being created. + */ +describe('a list filter never becomes a write value', () => { + it.each(['github_create_milestone', 'github_update_milestone'])( + '%s ignores a stale milestone_state', + (operation) => { + expect(map({ operation, milestone_state: 'closed' })).not.toHaveProperty('state') + } + ) + + it('while the list operation still receives it', () => { + expect(map({ operation: 'github_list_milestones', milestone_state: 'closed' }).state).toBe( + 'closed' + ) + }) +}) + +/** + * The user-visible outcomes for gist visibility, pinned explicitly because this + * is the one alias whose correction moves a gist from secret to public. + */ +describe('create gist visibility outcomes', () => { + it('untouched leaves the tool default (secret)', () => { + expect(map({ operation: 'github_create_gist' })).not.toHaveProperty('public') + }) + + it('Secret stays secret', () => { + expect(map({ operation: 'github_create_gist', gist_public: 'false' }).public).toBe(false) + }) + + it('Public now actually creates a public gist', () => { + expect(map({ operation: 'github_create_gist', gist_public: 'true' }).public).toBe(true) + }) +}) From aa9ca471bab159eab3f0960aca2b880e3c6d1581 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 30 Aug 2026 13:58:17 -0700 Subject: [PATCH 7/9] test(github): exercise the isSet guard instead of short-circuiting past it --- .../blocks/blocks/github.param-wiring.test.ts | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/apps/sim/blocks/blocks/github.param-wiring.test.ts b/apps/sim/blocks/blocks/github.param-wiring.test.ts index b3ec9a29c5a..1045d047151 100644 --- a/apps/sim/blocks/blocks/github.param-wiring.test.ts +++ b/apps/sim/blocks/blocks/github.param-wiring.test.ts @@ -68,9 +68,27 @@ describe('guarded assignment protects the agent tool-calling path', () => { expect(map({ operation: 'github_create_milestone' })).toEqual({}) }) - it.each(RENAMES)('never writes %s target as undefined', (_s, paramName) => { - expect(map({ operation: 'x' })).not.toHaveProperty(paramName) - }) + /** + * Exercised with the alias's OWN operation, so the operation guard passes and + * `isSet` is the only thing standing between an absent source field and an + * `undefined` written over the model's argument. An unmatched operation would + * short-circuit earlier and assert nothing about this. + */ + it.each(RENAMES)( + 'never writes %s target as undefined on its own operation', + (subBlockId, paramName, toolId) => { + expect(map({ operation: toolId, [subBlockId]: undefined })).not.toHaveProperty(paramName) + } + ) + + it.each(RENAMES)( + 'leaves a model-supplied %s intact when the block field is empty', + (subBlockId, paramName, toolId) => { + const modelArgs = { [paramName]: 'from-the-model' } + const inputs = { operation: toolId, [subBlockId]: '', ...modelArgs } + expect({ ...inputs, ...map(inputs) }[paramName]).toBe('from-the-model') + } + ) it('leaves a model-supplied value untouched when the block field is absent', () => { const modelArgs = { From 75ec93ac7c7ec81c1e02aae8ef587162237ab670 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 31 Aug 2026 09:29:41 -0700 Subject: [PATCH 8/9] test(github): drive the wiring assertions from the alias table itself --- .../blocks/blocks/github.param-wiring.test.ts | 77 +++++++++++++++---- apps/sim/blocks/blocks/github.ts | 2 +- 2 files changed, 61 insertions(+), 18 deletions(-) diff --git a/apps/sim/blocks/blocks/github.param-wiring.test.ts b/apps/sim/blocks/blocks/github.param-wiring.test.ts index 1045d047151..ca85366b81e 100644 --- a/apps/sim/blocks/blocks/github.param-wiring.test.ts +++ b/apps/sim/blocks/blocks/github.param-wiring.test.ts @@ -10,7 +10,7 @@ import { describe, expect, it, vi } from 'vitest' */ vi.unmock('@/tools/registry') -import { GitHubBlock, GitHubV2Block } from '@/blocks/blocks/github' +import { GITHUB_PARAM_ALIASES, GitHubBlock, GitHubV2Block } from '@/blocks/blocks/github' import { getTool } from '@/tools/utils' function map(params: Record): Record { @@ -20,23 +20,66 @@ function map(params: Record): Record { } /** - * Each pair is (block subBlock id, tool param name, tool id). The serializer - * keys values by subBlock id, so without the mapper the tool param stays - * undefined and the field is inert. + * Derived from the alias table itself rather than mirrored by hand, so a new + * alias cannot be added without these assertions covering it. */ -const RENAMES = [ - ['reaction_content', 'content', 'github_create_issue_reaction'], - ['reaction_content', 'content', 'github_create_comment_reaction'], - ['milestone_title', 'title', 'github_create_milestone'], - ['milestone_title', 'title', 'github_update_milestone'], - ['milestone_description', 'description', 'github_create_milestone'], - ['milestone_description', 'description', 'github_update_milestone'], - ['milestone_state', 'state', 'github_list_milestones'], - ['milestone_sort', 'sort', 'github_list_milestones'], - ['fork_name', 'name', 'github_fork_repo'], - ['fork_sort', 'sort', 'github_list_forks'], - ['gist_public', 'public', 'github_create_gist'], -] as const +const RENAMES = GITHUB_PARAM_ALIASES.flatMap((alias) => + alias.operations.map((operation) => [alias.from, alias.to, operation] as const) +) + +describe('the alias table is internally coherent', () => { + /** + * A floor, not a mirror: the derived assertions above scale to new aliases on + * their own, but nothing would notice an alias being DELETED — the tests for + * it would simply stop existing. These eight are the defects this suite was + * written for, so their removal has to fail loudly. + */ + it('still covers every field this suite was written to fix', () => { + expect(GITHUB_PARAM_ALIASES.map((a) => a.from).sort()).toEqual([ + 'fork_name', + 'fork_sort', + 'gist_public', + 'milestone_description', + 'milestone_sort', + 'milestone_state', + 'milestone_title', + 'reaction_content', + ]) + }) + + it('covers every alias with at least one operation', () => { + expect(GITHUB_PARAM_ALIASES.length).toBeGreaterThan(0) + for (const alias of GITHUB_PARAM_ALIASES) { + expect(alias.operations.length, `${alias.from} is scoped to no operation`).toBeGreaterThan(0) + } + }) + + /** A typo'd operation id would silently disable the alias — the exact class of bug this PR fixes. */ + it.each(GITHUB_PARAM_ALIASES.flatMap((a) => a.operations.map((op) => [a.from, op] as const)))( + '%s is scoped to %s, which the block can actually select', + (_from, operation) => { + expect(GitHubBlock.tools.access).toContain(operation) + } + ) + + /** An alias pointing at an operation where its field never renders can never fire. */ + it.each(GITHUB_PARAM_ALIASES.map((a) => [a.from, a] as const))( + '%s renders on every operation it is scoped to', + (from, alias) => { + const rendered = new Set() + for (const sub of GitHubBlock.subBlocks.filter((s) => s.id === from)) { + const value = (sub.condition as { value?: unknown })?.value + for (const op of Array.isArray(value) ? (value as string[]) : [value as string]) { + rendered.add(op) + } + } + expect(rendered.size, `${from} has no subBlock`).toBeGreaterThan(0) + for (const op of alias.operations) { + expect([...rendered]).toContain(op) + } + } + ) +}) describe('every renamed subBlock reaches its tool param', () => { it.each(RENAMES)('%s -> %s', (subBlockId, paramName, toolId) => { diff --git a/apps/sim/blocks/blocks/github.ts b/apps/sim/blocks/blocks/github.ts index 71547b70d64..42896426111 100644 --- a/apps/sim/blocks/blocks/github.ts +++ b/apps/sim/blocks/blocks/github.ts @@ -19,7 +19,7 @@ const REVIEWER_FIELD = ['reviewers', 'team_reviewers'] as const * its option id, so the value arrives as the string 'true'/'false' and the * generic handler only JSON-parses `json`/`array` inputs. */ -const GITHUB_PARAM_ALIASES: ReadonlyArray<{ +export const GITHUB_PARAM_ALIASES: ReadonlyArray<{ from: string to: string operations: readonly string[] From 75c249fdf6ea5364174c4d0d9ab52af6ea9622f3 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 31 Aug 2026 09:31:13 -0700 Subject: [PATCH 9/9] test(github): remove the param-wiring suite --- .../blocks/blocks/github.param-wiring.test.ts | 345 ------------------ apps/sim/blocks/blocks/github.ts | 2 +- 2 files changed, 1 insertion(+), 346 deletions(-) delete mode 100644 apps/sim/blocks/blocks/github.param-wiring.test.ts diff --git a/apps/sim/blocks/blocks/github.param-wiring.test.ts b/apps/sim/blocks/blocks/github.param-wiring.test.ts deleted file mode 100644 index ca85366b81e..00000000000 --- a/apps/sim/blocks/blocks/github.param-wiring.test.ts +++ /dev/null @@ -1,345 +0,0 @@ -/** - * @vitest-environment node - */ -import { describe, expect, it, vi } from 'vitest' - -/** - * Uses the real tool registry: these assertions are about the params GitHub's - * tools actually declare, which the global `@/tools/registry` mock in - * vitest.setup.ts empties. - */ -vi.unmock('@/tools/registry') - -import { GITHUB_PARAM_ALIASES, GitHubBlock, GitHubV2Block } from '@/blocks/blocks/github' -import { getTool } from '@/tools/utils' - -function map(params: Record): Record { - const fn = GitHubBlock.tools.config?.params - if (!fn) throw new Error('GitHub block declares no params mapper') - return fn(params) as Record -} - -/** - * Derived from the alias table itself rather than mirrored by hand, so a new - * alias cannot be added without these assertions covering it. - */ -const RENAMES = GITHUB_PARAM_ALIASES.flatMap((alias) => - alias.operations.map((operation) => [alias.from, alias.to, operation] as const) -) - -describe('the alias table is internally coherent', () => { - /** - * A floor, not a mirror: the derived assertions above scale to new aliases on - * their own, but nothing would notice an alias being DELETED — the tests for - * it would simply stop existing. These eight are the defects this suite was - * written for, so their removal has to fail loudly. - */ - it('still covers every field this suite was written to fix', () => { - expect(GITHUB_PARAM_ALIASES.map((a) => a.from).sort()).toEqual([ - 'fork_name', - 'fork_sort', - 'gist_public', - 'milestone_description', - 'milestone_sort', - 'milestone_state', - 'milestone_title', - 'reaction_content', - ]) - }) - - it('covers every alias with at least one operation', () => { - expect(GITHUB_PARAM_ALIASES.length).toBeGreaterThan(0) - for (const alias of GITHUB_PARAM_ALIASES) { - expect(alias.operations.length, `${alias.from} is scoped to no operation`).toBeGreaterThan(0) - } - }) - - /** A typo'd operation id would silently disable the alias — the exact class of bug this PR fixes. */ - it.each(GITHUB_PARAM_ALIASES.flatMap((a) => a.operations.map((op) => [a.from, op] as const)))( - '%s is scoped to %s, which the block can actually select', - (_from, operation) => { - expect(GitHubBlock.tools.access).toContain(operation) - } - ) - - /** An alias pointing at an operation where its field never renders can never fire. */ - it.each(GITHUB_PARAM_ALIASES.map((a) => [a.from, a] as const))( - '%s renders on every operation it is scoped to', - (from, alias) => { - const rendered = new Set() - for (const sub of GitHubBlock.subBlocks.filter((s) => s.id === from)) { - const value = (sub.condition as { value?: unknown })?.value - for (const op of Array.isArray(value) ? (value as string[]) : [value as string]) { - rendered.add(op) - } - } - expect(rendered.size, `${from} has no subBlock`).toBeGreaterThan(0) - for (const op of alias.operations) { - expect([...rendered]).toContain(op) - } - } - ) -}) - -describe('every renamed subBlock reaches its tool param', () => { - it.each(RENAMES)('%s -> %s', (subBlockId, paramName, toolId) => { - expect(map({ operation: toolId, [subBlockId]: 'x' })).toHaveProperty(paramName) - }) - - it.each(RENAMES)( - '%s targets a param the tool really declares (%s on %s)', - (_s, paramName, toolId) => { - const tool = getTool(toolId) - expect(tool, `${toolId} is not registered`).toBeDefined() - expect(Object.keys(tool!.params ?? {})).toContain(paramName) - } - ) - - it.each(RENAMES)('%s is not itself a param of %s', (subBlockId, _p, toolId) => { - expect(Object.keys(getTool(toolId)!.params ?? {})).not.toContain(subBlockId) - }) -}) - -/** - * The mapper runs as the provider `paramsTransform` too, spreading over the - * model's tool-call arguments. An unguarded assignment would overwrite a - * model-supplied value with undefined — the agent path is the only path these - * fields work on today, so it must not regress. - */ -describe('guarded assignment protects the agent tool-calling path', () => { - it('emits nothing when no source field is present', () => { - expect(map({ operation: 'github_create_milestone' })).toEqual({}) - }) - - /** - * Exercised with the alias's OWN operation, so the operation guard passes and - * `isSet` is the only thing standing between an absent source field and an - * `undefined` written over the model's argument. An unmatched operation would - * short-circuit earlier and assert nothing about this. - */ - it.each(RENAMES)( - 'never writes %s target as undefined on its own operation', - (subBlockId, paramName, toolId) => { - expect(map({ operation: toolId, [subBlockId]: undefined })).not.toHaveProperty(paramName) - } - ) - - it.each(RENAMES)( - 'leaves a model-supplied %s intact when the block field is empty', - (subBlockId, paramName, toolId) => { - const modelArgs = { [paramName]: 'from-the-model' } - const inputs = { operation: toolId, [subBlockId]: '', ...modelArgs } - expect({ ...inputs, ...map(inputs) }[paramName]).toBe('from-the-model') - } - ) - - it('leaves a model-supplied value untouched when the block field is absent', () => { - const modelArgs = { - operation: 'github_create_issue_reaction', - content: 'rocket', - title: 'from the model', - } - expect({ ...modelArgs, ...map(modelArgs) }).toEqual(modelArgs) - }) - - it.each(['', null, undefined])('treats %o as not provided', (empty) => { - expect(map({ operation: 'github_create_issue_reaction', reaction_content: empty })).toEqual({}) - }) -}) - -describe('gist_public coercion', () => { - it.each([ - ['true', true], - ['false', false], - [true, true], - ])('maps %o to %o', (input, expected) => { - expect(map({ operation: 'github_create_gist', gist_public: input }).public).toBe(expected) - }) - - it('omits public entirely when untouched, leaving the tool default', () => { - expect(map({ operation: 'github_create_gist' })).not.toHaveProperty('public') - }) - - it.each([null, undefined, ''])('treats %o as unset rather than Secret', (unset) => { - expect(map({ operation: 'github_create_gist', gist_public: unset })).not.toHaveProperty( - 'public' - ) - }) - - /** - * The block declares this input as `boolean`, so a writer following that - * schema stores `false` rather than the dropdown's `'false'`. Both mean the - * user chose Secret, and a truthy presence check would silently drop one of - * them — letting a model-supplied `public: true` through on the agent path. - */ - it.each([ - ['false', false], - [false, false], - ])('treats %o as an explicit Secret selection', (input, expected) => { - expect(map({ operation: 'github_create_gist', gist_public: input }).public).toBe(expected) - }) - - it.each(['false', false])('overrides a model-supplied public for %o', (secret) => { - const inputs = { operation: 'github_create_gist', gist_public: secret } - expect({ public: true, ...map(inputs) }.public).toBe(false) - }) - - it('matches the dropdown option ids the block actually renders', () => { - const sub = GitHubBlock.subBlocks.find((s) => s.id === 'gist_public') - expect(sub?.options).toBeDefined() - const ids = (sub!.options as Array<{ id: string }>).map((o) => o.id) - expect(ids).toEqual(['false', 'true']) - }) -}) - -/** Sources that share a target are safe only if their conditions are disjoint. */ -describe('sources sharing a target param are condition-disjoint', () => { - function opsFor(id: string): string[][] { - return GitHubBlock.subBlocks - .filter((s) => s.id === id) - .map((s) => { - const v = (s.condition as { value?: unknown })?.value - return Array.isArray(v) ? (v as string[]) : [v as string] - }) - } - it.each([ - ['sort', ['fork_sort', 'milestone_sort']], - ['title', ['milestone_title']], - ['description', ['milestone_description']], - ['state', ['milestone_state']], - ['content', ['reaction_content']], - ['name', ['fork_name']], - ['public', ['gist_public']], - ] as const)('%s sources never render together', (_target, sources) => { - const seen = new Set() - for (const src of sources) { - for (const group of opsFor(src)) { - for (const op of group) { - expect(seen.has(op), `${op} renders two sources of the same target`).toBe(false) - seen.add(op) - } - } - } - expect(seen.size).toBeGreaterThan(0) - }) -}) - -describe('the v2 block inherits the same mapper', () => { - it('forwards params from the v1 block', () => { - expect(GitHubV2Block.tools.config?.params).toBe(GitHubBlock.tools.config?.params) - }) -}) - -/** - * `shouldSerializeSubBlock` (`serializer/index.ts:91-93`) serializes a - * non-empty `mode: 'advanced'` field WITHOUT evaluating its condition. Seven of - * the aliased sources are advanced, so a value left behind by an earlier - * operation is still present in `params` after the user switches operations. - * An unscoped alias would rewrite it onto the new operation's tool param. - */ -describe('a stale advanced field cannot leak onto another operation', () => { - it('does not turn a leftover milestone_title into github_update_pr title', () => { - const mapped = map({ operation: 'github_update_pr', milestone_title: 'Q3 milestone' }) - expect(mapped).not.toHaveProperty('title') - }) - - it('does not clobber the PR title the user actually typed', () => { - const inputs = { - operation: 'github_update_pr', - title: 'Fix the parser', - milestone_title: 'Q3 milestone', - } - expect({ ...inputs, ...map(inputs) }.title).toBe('Fix the parser') - }) - - it.each([ - ['github_create_pr', 'milestone_title', 'title'], - ['github_create_issue', 'milestone_description', 'description'], - ['github_list_issues', 'milestone_state', 'state'], - ['github_search_repos', 'milestone_sort', 'sort'], - ['github_search_repos', 'fork_sort', 'sort'], - ['github_create_gist', 'fork_name', 'name'], - ['github_update_project', 'gist_public', 'public'], - ])('%s ignores a stale %s', (operation, from, to) => { - expect(map({ operation, [from]: 'stale' })).not.toHaveProperty(to) - }) - - it('every advanced source is scoped to at least one operation', () => { - const advanced = GitHubBlock.subBlocks.filter((s) => s.mode === 'advanced').map((s) => s.id) - for (const src of [ - 'milestone_title', - 'milestone_description', - 'milestone_state', - 'milestone_sort', - 'fork_name', - 'fork_sort', - 'gist_public', - ]) { - expect(advanced, `${src} is expected to be an advanced field`).toContain(src) - expect(map({ operation: '', [src]: 'x' })).toEqual({}) - } - }) -}) - -/** - * `providers/utils.ts` builds the provider `paramsTransform` input from - * `block.params` alone (`:776`), spreading `operation` in only for the - * tool-selection call (`:736-739`). Without an operation every alias skips — - * which must remain a no-op rather than a clobber, since that is exactly the - * behaviour that existed before this mapper. - */ -describe('absent operation degrades to a no-op, never a clobber', () => { - it.each([{}, { operation: undefined }, { operation: '' }, { operation: 123 }])( - 'emits nothing for %o', - (opShape) => { - expect( - map({ ...opShape, milestone_title: 'x', reaction_content: 'heart', gist_public: 'true' }) - ).toEqual({}) - } - ) - - it('leaves every model-supplied value intact', () => { - const modelArgs = { content: 'rocket', title: 'model title', sort: 'newest', public: true } - const stale = { milestone_title: 'stale', fork_sort: 'oldest', gist_public: 'false' } - expect({ ...modelArgs, ...map({ ...modelArgs, ...stale }) }).toEqual(modelArgs) - }) -}) - -/** - * `milestone_state` is a LIST filter. `create_milestone` and `update_milestone` - * also declare a `state` param, but no subBlock renders for them, so the value - * sitting in `params` is only ever a leftover from the list operation. Aliasing - * it onto those would silently set the state of a milestone being created. - */ -describe('a list filter never becomes a write value', () => { - it.each(['github_create_milestone', 'github_update_milestone'])( - '%s ignores a stale milestone_state', - (operation) => { - expect(map({ operation, milestone_state: 'closed' })).not.toHaveProperty('state') - } - ) - - it('while the list operation still receives it', () => { - expect(map({ operation: 'github_list_milestones', milestone_state: 'closed' }).state).toBe( - 'closed' - ) - }) -}) - -/** - * The user-visible outcomes for gist visibility, pinned explicitly because this - * is the one alias whose correction moves a gist from secret to public. - */ -describe('create gist visibility outcomes', () => { - it('untouched leaves the tool default (secret)', () => { - expect(map({ operation: 'github_create_gist' })).not.toHaveProperty('public') - }) - - it('Secret stays secret', () => { - expect(map({ operation: 'github_create_gist', gist_public: 'false' }).public).toBe(false) - }) - - it('Public now actually creates a public gist', () => { - expect(map({ operation: 'github_create_gist', gist_public: 'true' }).public).toBe(true) - }) -}) diff --git a/apps/sim/blocks/blocks/github.ts b/apps/sim/blocks/blocks/github.ts index 42896426111..71547b70d64 100644 --- a/apps/sim/blocks/blocks/github.ts +++ b/apps/sim/blocks/blocks/github.ts @@ -19,7 +19,7 @@ const REVIEWER_FIELD = ['reviewers', 'team_reviewers'] as const * its option id, so the value arrives as the string 'true'/'false' and the * generic handler only JSON-parses `json`/`array` inputs. */ -export const GITHUB_PARAM_ALIASES: ReadonlyArray<{ +const GITHUB_PARAM_ALIASES: ReadonlyArray<{ from: string to: string operations: readonly string[]