Skip to content

Commit b4bc9bf

Browse files
committed
fix(forking): preserve dependent-chain semantics
1 parent c141830 commit b4bc9bf

6 files changed

Lines changed: 370 additions & 197 deletions

File tree

apps/sim/ee/workspace-forking/components/fork-sync/dependent-value.test.ts

Lines changed: 75 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,13 @@ import { describe, expect, it } from 'vitest'
55
import type { ForkDependentReconfig } from '@/lib/api/contracts/workspace-fork'
66
import {
77
applyDependentRepick,
8-
DEPENDENT_CLEARED_BY_PARENT,
98
dependentKey,
109
effectiveCopyDependentValue,
1110
effectiveDependentValue,
1211
getActionableDependentFields,
1312
getDisplayedDependentFields,
14-
isDependentClearedByParent,
1513
isDependentConfigurationActionable,
16-
submittedDependentValue,
14+
isDependentInvalidated,
1715
} from '@/ee/workspace-forking/components/fork-sync/dependent-value'
1816

1917
const field = (overrides: Partial<ForkDependentReconfig> = {}): ForkDependentReconfig => ({
@@ -34,6 +32,11 @@ const field = (overrides: Partial<ForkDependentReconfig> = {}): ForkDependentRec
3432
...overrides,
3533
})
3634

35+
const mappedRepickContext = (previousValue: string) => ({
36+
previousValue,
37+
baselineValueFor: (dependent: ForkDependentReconfig) => dependent.currentValue,
38+
})
39+
3740
describe('dependentKey', () => {
3841
it('keys by target workflow + block + subblock', () => {
3942
expect(
@@ -141,14 +144,15 @@ describe('applyDependentRepick', () => {
141144
previous,
142145
site,
143146
[site, drive, spreadsheet, sheet, unrelated],
144-
'site-new'
147+
'site-new',
148+
mappedRepickContext('site-old')
145149
)
146150

147151
expect(next).toEqual({
148152
[dependentKey(site)]: 'site-new',
149-
[dependentKey(drive)]: DEPENDENT_CLEARED_BY_PARENT,
150-
[dependentKey(spreadsheet)]: DEPENDENT_CLEARED_BY_PARENT,
151-
[dependentKey(sheet)]: DEPENDENT_CLEARED_BY_PARENT,
153+
[dependentKey(drive)]: null,
154+
[dependentKey(spreadsheet)]: null,
155+
[dependentKey(sheet)]: null,
152156
[dependentKey(unrelated)]: 'still-keep-me',
153157
})
154158
expect(effectiveDependentValue(drive, next, false)).toBe('')
@@ -172,10 +176,10 @@ describe('applyDependentRepick', () => {
172176
spreadsheet,
173177
[spreadsheet, range],
174178
'sheet-doc',
175-
effectiveDependentValue(spreadsheet, {}, false)
179+
mappedRepickContext(effectiveDependentValue(spreadsheet, {}, false))
176180
)
177181

178-
expect(next).toEqual({ [dependentKey(spreadsheet)]: 'sheet-doc' })
182+
expect(next).toEqual({})
179183
expect(effectiveDependentValue(range, next, false)).toBe('Sheet1!A1:D')
180184
})
181185

@@ -188,7 +192,8 @@ describe('applyDependentRepick', () => {
188192
{ [dependentKey(unrelated)]: 'still-keep-me' },
189193
leaf,
190194
[leaf, unrelated],
191-
'ISSUE-2'
195+
'ISSUE-2',
196+
mappedRepickContext('ISSUE-1')
192197
)
193198
).toEqual({
194199
[dependentKey(leaf)]: 'ISSUE-2',
@@ -227,97 +232,105 @@ describe('applyDependentRepick', () => {
227232
previous,
228233
projectOne,
229234
[projectOne, issueOne, projectTwo, issueTwo],
230-
'P1-NEW'
235+
'P1-NEW',
236+
mappedRepickContext('INBOX')
231237
)
232238
).toEqual({
233239
[dependentKey(projectOne)]: 'P1-NEW',
234-
[dependentKey(issueOne)]: DEPENDENT_CLEARED_BY_PARENT,
240+
[dependentKey(issueOne)]: null,
235241
[dependentKey(issueTwo)]: 'P2-1',
236242
})
237243
})
238-
})
239244

240-
describe('submittedDependentValue', () => {
241-
const mappedParent = { copying: false, parentChanged: false }
242-
243-
it('omits an optional descendant a parent re-pick blanked, so the target keeps its value', () => {
245+
it('restores the stored chain when a provider is changed and then returned to baseline', () => {
244246
const spreadsheet = field({
245247
subBlockKey: 'spreadsheetId',
246248
currentValue: 'doc-old',
247249
providesContextKey: 'spreadsheetId',
248250
})
249-
const sheet = field({
250-
subBlockKey: 'sheetName',
251-
currentValue: 'Sheet1',
252-
required: true,
253-
consumesContextKeys: ['spreadsheetId'],
254-
})
255251
const range = field({
256252
subBlockKey: 'range',
257253
currentValue: 'A1:D50',
258-
required: false,
259254
consumesContextKeys: ['spreadsheetId'],
260255
})
261256

262-
const next = applyDependentRepick(
257+
const changed = applyDependentRepick(
263258
{},
264259
spreadsheet,
265-
[spreadsheet, sheet, range],
260+
[spreadsheet, range],
266261
'doc-new',
267-
effectiveDependentValue(spreadsheet, {}, false)
262+
mappedRepickContext('doc-old')
263+
)
264+
const restored = applyDependentRepick(
265+
changed,
266+
spreadsheet,
267+
[spreadsheet, range],
268+
'doc-old',
269+
mappedRepickContext('doc-new')
268270
)
269271

270-
expect(isDependentClearedByParent(range, next)).toBe(true)
271-
expect(submittedDependentValue(range, next, mappedParent)).toBeUndefined()
272-
expect(submittedDependentValue(sheet, next, mappedParent)).toBeUndefined()
273-
expect(submittedDependentValue(spreadsheet, next, mappedParent)).toBe('doc-new')
272+
expect(changed).toEqual({
273+
[dependentKey(spreadsheet)]: 'doc-new',
274+
[dependentKey(range)]: null,
275+
})
276+
expect(restored).toEqual({})
277+
expect(effectiveDependentValue(range, restored, false)).toBe('A1:D50')
274278
})
275279

276-
it('submits an empty value the user picked themselves, so an explicit clear still clears', () => {
280+
it('keeps an intentional empty pick distinct from automatic invalidation', () => {
277281
const label = field({ subBlockKey: 'label', currentValue: 'INBOX' })
278282

279-
const next = applyDependentRepick({}, label, [label], '', 'INBOX')
283+
const next = applyDependentRepick({}, label, [label], '', mappedRepickContext('INBOX'))
280284

281-
expect(isDependentClearedByParent(label, next)).toBe(false)
282-
expect(submittedDependentValue(label, next, mappedParent)).toBe('')
285+
expect(isDependentInvalidated(label, next)).toBe(false)
286+
expect(next[dependentKey(label)]).toBe('')
287+
expect(effectiveDependentValue(label, next, false)).toBe('')
283288
})
284289

285-
it('submits a re-picked descendant once the user chooses a replacement', () => {
290+
it('does not restore an Excel sheet while its drive still differs from baseline', () => {
291+
const drive = field({
292+
subBlockKey: 'driveId',
293+
currentValue: 'drive-old',
294+
providesContextKey: 'driveId',
295+
})
286296
const spreadsheet = field({
287297
subBlockKey: 'spreadsheetId',
288-
currentValue: 'doc-old',
298+
currentValue: 'workbook-old',
299+
consumesContextKeys: ['driveId'],
289300
providesContextKey: 'spreadsheetId',
290301
})
291-
const range = field({
292-
subBlockKey: 'range',
293-
currentValue: 'A1:D50',
294-
consumesContextKeys: ['spreadsheetId'],
302+
const sheet = field({
303+
subBlockKey: 'sheetName',
304+
currentValue: 'Sheet1',
305+
consumesContextKeys: ['driveId', 'spreadsheetId'],
295306
})
296307

297-
const cleared = applyDependentRepick(
308+
const driveChanged = applyDependentRepick(
298309
{},
310+
drive,
311+
[drive, spreadsheet, sheet],
312+
'drive-new',
313+
mappedRepickContext('drive-old')
314+
)
315+
const spreadsheetRepicked = applyDependentRepick(
316+
driveChanged,
299317
spreadsheet,
300-
[spreadsheet, range],
301-
'doc-new',
302-
effectiveDependentValue(spreadsheet, {}, false)
318+
[drive, spreadsheet, sheet],
319+
'workbook-new',
320+
mappedRepickContext('')
303321
)
304-
const repicked = applyDependentRepick(
305-
cleared,
306-
range,
307-
[spreadsheet, range],
308-
'A1:Z',
309-
effectiveDependentValue(range, cleared, false)
322+
const spreadsheetRestored = applyDependentRepick(
323+
spreadsheetRepicked,
324+
spreadsheet,
325+
[drive, spreadsheet, sheet],
326+
'workbook-old',
327+
mappedRepickContext('workbook-new')
310328
)
311329

312-
expect(submittedDependentValue(range, repicked, mappedParent)).toBe('A1:Z')
313-
})
314-
315-
it('falls back to the stored value under an unchanged parent and to the source when copying', () => {
316-
const untouched = field({ subBlockKey: 'label', currentValue: 'INBOX' })
317-
const copied = field({ subBlockKey: 'documentSelector', currentValue: '', sourceValue: 'doc' })
318-
319-
expect(submittedDependentValue(untouched, {}, mappedParent)).toBe('INBOX')
320-
expect(submittedDependentValue(copied, {}, { copying: true, parentChanged: false })).toBe('doc')
330+
expect(spreadsheetRestored).toEqual({
331+
[dependentKey(drive)]: 'drive-new',
332+
[dependentKey(sheet)]: null,
333+
})
321334
})
322335
})
323336

@@ -409,7 +422,7 @@ describe('isDependentConfigurationActionable', () => {
409422
spreadsheet,
410423
[spreadsheet, sheet],
411424
'doc-new',
412-
effectiveDependentValue(spreadsheet, {}, false)
425+
mappedRepickContext(effectiveDependentValue(spreadsheet, {}, false))
413426
)
414427

415428
// The sync gate reads the same blank the selector shows, so the field gates and is visible.
@@ -425,7 +438,7 @@ describe('isDependentConfigurationActionable', () => {
425438

426439
it('shows a required field the user emptied themselves (it blocks Sync)', () => {
427440
const sheet = field({ subBlockKey: 'sheetName', required: true, currentValue: 'Sheet1' })
428-
const next = applyDependentRepick({}, sheet, [sheet], '', 'Sheet1')
441+
const next = applyDependentRepick({}, sheet, [sheet], '', mappedRepickContext('Sheet1'))
429442

430443
expect(effectiveDependentValue(sheet, next, false)).toBe('')
431444
expect(

0 commit comments

Comments
 (0)