Skip to content

Commit 5b40970

Browse files
Vikhyath Mondreticlaude
andcommitted
fix(workspace-forking): let an explicit identity custom-block mapping resolve
`remapForkBlockType` reported a mapping whose target equalled the source as unresolved, conflating "a mapping exists" with "the type string changed". Those are opposite states that produce an identical output `type`, and every caller uses the flag for the former — to decide whether the reference blocks a promote. The org-wide candidate list includes the source block, so binding an environment to the shared block is a normal pick. Under the old flag it raised `unmapped-custom-block` and refused the sync over a choice the user had explicitly made. The flag is now named `resolved` and reports mapping existence; whether the type moved is already visible from `type`. Reported by Cursor Bugbot on #6857. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 5f128a6 commit 5b40970

3 files changed

Lines changed: 38 additions & 12 deletions

File tree

‎apps/sim/ee/workspace-forking/lib/promote/cleared-refs.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ export function collectForkClearedRefCandidates(
226226
blockId: targetBlockId,
227227
blockName: blockLabel,
228228
})
229-
if (blockTypeRef.reference && !blockTypeRef.remapped) {
229+
if (blockTypeRef.reference && !blockTypeRef.resolved) {
230230
out.push({
231231
targetWorkflowId: item.targetWorkflowId,
232232
workflowName: item.sourceMeta.name,

‎apps/sim/ee/workspace-forking/lib/remap/remap-block-type.test.ts‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const emptyResolver: ForkReferenceResolver = () => null
2020
describe('remapForkBlockType', () => {
2121
it('leaves a non-custom block entirely alone', () => {
2222
const result = remapForkBlockType('agent', mappedResolver)
23-
expect(result).toEqual({ type: 'agent', remapped: false })
23+
expect(result).toEqual({ type: 'agent', resolved: false })
2424
expect(result.reference).toBeUndefined()
2525
})
2626

@@ -30,7 +30,7 @@ describe('remapForkBlockType', () => {
3030
blockName: 'Invoice Parser',
3131
})
3232
expect(result.type).toBe(UAT_BLOCK)
33-
expect(result.remapped).toBe(true)
33+
expect(result.resolved).toBe(true)
3434
expect(result.reference).toEqual({
3535
kind: 'custom-block',
3636
sourceId: PROD_BLOCK,
@@ -46,15 +46,18 @@ describe('remapForkBlockType', () => {
4646
// so an unmapped custom block stays put and is reported instead.
4747
const result = remapForkBlockType(PROD_BLOCK, emptyResolver)
4848
expect(result.type).toBe(PROD_BLOCK)
49-
expect(result.remapped).toBe(false)
49+
expect(result.resolved).toBe(false)
5050
expect(result.reference?.sourceId).toBe(PROD_BLOCK)
5151
})
5252

53-
it('treats a self-mapping as not remapped, so it never reports a spurious rewrite', () => {
53+
it('counts an explicit identity mapping as RESOLVED, so it never blocks the promote', () => {
54+
// The org-wide candidate list includes the source block, so binding an environment to
55+
// the shared block is a normal pick. Reporting it as unresolved would raise
56+
// `unmapped-custom-block` and refuse the sync over a choice the user explicitly made.
5457
const selfResolver: ForkReferenceResolver = () => PROD_BLOCK
5558
const result = remapForkBlockType(PROD_BLOCK, selfResolver)
5659
expect(result.type).toBe(PROD_BLOCK)
57-
expect(result.remapped).toBe(false)
60+
expect(result.resolved).toBe(true)
5861
})
5962
})
6063

@@ -92,6 +95,17 @@ describe('scanWorkflowReferences with custom blocks', () => {
9295
expect(scan.unmapped).toHaveLength(0)
9396
})
9497

98+
it('does not report an identity-mapped block as unmapped', () => {
99+
const selfResolver: ForkReferenceResolver = (kind, sourceId) =>
100+
kind === 'custom-block' ? sourceId : null
101+
const scan = scanWorkflowReferences(
102+
[{ id: 'blk-1', name: 'Shared Utility', type: PROD_BLOCK, subBlocks: {} }],
103+
selfResolver
104+
)
105+
expect(scan.references).toHaveLength(1)
106+
expect(scan.unmapped).toHaveLength(0)
107+
})
108+
95109
it('dedupes the same custom block placed in several workflows', () => {
96110
const scan = scanWorkflowReferences(
97111
[

‎apps/sim/ee/workspace-forking/lib/remap/remap-references.ts‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -223,11 +223,19 @@ export const CUSTOM_BLOCK_REFERENCE_KEY = 'type'
223223

224224
/** Outcome of remapping a placed block's own `type` across a fork edge. */
225225
export interface RemapForkBlockTypeResult {
226-
/** The type to persist. Unchanged unless a mapping resolved to a different block. */
226+
/** The type to persist. Equal to the input unless a mapping pointed elsewhere. */
227227
type: string
228228
/** Set when the block IS a custom block, so callers can aggregate/report it. */
229229
reference?: ForkReference
230-
remapped: boolean
230+
/**
231+
* Whether a mapping EXISTS for this reference — deliberately not "the type changed".
232+
* The two diverge on an identity mapping: the org-wide candidate list includes the
233+
* source block, so binding an environment to the shared block is a normal pick, and
234+
* treating it as unresolved would raise `unmapped-custom-block` and block the promote
235+
* on a choice the user explicitly made. Callers use this to decide whether the
236+
* reference is a blocker; whether the type actually moved is visible from `type`.
237+
*/
238+
resolved: boolean
231239
}
232240

233241
/**
@@ -249,14 +257,18 @@ export interface RemapForkBlockTypeResult {
249257
* step from the workflow. The reference is reported as unmapped instead, so the mapping UI
250258
* surfaces it and `sync-blockers` refuses the promote. That is what stops a uat
251259
* orchestrator from quietly invoking prod.
260+
*
261+
* An UNMAPPED reference and one mapped back to itself look identical in the output `type`
262+
* but are opposite states, which is why {@link RemapForkBlockTypeResult.resolved} reports
263+
* mapping existence rather than whether the type moved.
252264
*/
253265
export function remapForkBlockType(
254266
blockType: string | undefined,
255267
resolve: ForkReferenceResolver,
256268
context?: { blockId?: string; blockName?: string }
257269
): RemapForkBlockTypeResult {
258270
const type = blockType ?? ''
259-
if (!isCustomBlockType(type)) return { type, remapped: false }
271+
if (!isCustomBlockType(type)) return { type, resolved: false }
260272

261273
const reference: ForkReference = {
262274
kind: 'custom-block',
@@ -268,9 +280,9 @@ export function remapForkBlockType(
268280
}
269281

270282
const targetType = resolve('custom-block', type)
271-
if (!targetType || targetType === type) return { type, reference, remapped: false }
283+
if (!targetType) return { type, reference, resolved: false }
272284

273-
return { type: targetType, reference, remapped: true }
285+
return { type: targetType, reference, resolved: true }
274286
}
275287

276288
/**
@@ -1628,7 +1640,7 @@ export function scanWorkflowReferences(
16281640
if (blockTypeResult.reference) {
16291641
const key = `${blockTypeResult.reference.kind}:${blockTypeResult.reference.sourceId}`
16301642
if (!references.has(key)) references.set(key, blockTypeResult.reference)
1631-
if (!blockTypeResult.remapped && !unmapped.has(key)) {
1643+
if (!blockTypeResult.resolved && !unmapped.has(key)) {
16321644
unmapped.set(key, blockTypeResult.reference)
16331645
}
16341646
}

0 commit comments

Comments
 (0)