@@ -45,3 +45,81 @@ func TestManifestRecordGetParentNil(t *testing.T) {
4545 assert .Nil (t , parent )
4646 require .NoError (t , err )
4747}
48+
49+ // TestSetRecords_OrphanedScheduler_SiblingGetsPathResolved is a regression test
50+ // for the panic in AddRelatedPath that occurred when a manifest contained both an
51+ // orphaned scheduler and sibling configs that sorted after it.
52+ //
53+ // The pre-fix code did an early return from SetRecords on the first PersistRecord
54+ // failure. Any record that had not yet been processed was left with
55+ // parentPathSet=false. When a subsequent pull tried to write those records' files,
56+ // Path() returned only the bare relative path (without the branch prefix), so the
57+ // filesystem.IsFrom check in AddRelatedPath panicked.
58+ //
59+ // The fix continues processing all records and deletes only the failing ones, so
60+ // every surviving record has its parent path resolved before SetRecords returns.
61+ func TestSetRecords_OrphanedScheduler_SiblingGetsPathResolved (t * testing.T ) {
62+ t .Parallel ()
63+ r := NewRecords (model .SortByID )
64+
65+ branchManifest := & model.BranchManifest {
66+ BranchKey : model.BranchKey {ID : 1 },
67+ // Simulate JSON-deserialized state: only RelativePath is set,
68+ // parentPath and parentPathSet are zero values.
69+ Paths : model.Paths {AbsPath : model.AbsPath {RelativePath : "main" }},
70+ }
71+ // Scheduler whose target orchestrator is absent from the manifest.
72+ // Placed before the extractor so that the pre-fix early-return would leave
73+ // the extractor unprocessed (parentPathSet=false).
74+ schedulerManifest := & model.ConfigManifest {
75+ ConfigKey : model.ConfigKey {
76+ BranchID : 1 ,
77+ ComponentID : "keboola.scheduler" ,
78+ ID : "456" ,
79+ },
80+ Paths : model.Paths {AbsPath : model.AbsPath {RelativePath : "schedules/scheduler" }},
81+ Relations : model.Relations {
82+ & model.SchedulerForRelation {
83+ ComponentID : "keboola.orchestrator" ,
84+ ConfigID : "999" , // deliberately absent from the manifest
85+ },
86+ },
87+ }
88+ // Sibling extractor with no special relations — its parent is the branch.
89+ // After SetRecords it must have IsParentPathSet()==true and the full path
90+ // "main/extractor/ex-generic-v2/empty" (branch prefix included).
91+ // With the pre-fix code this record would have parentPathSet=false, causing
92+ // a panic in AddRelatedPath when the pull wrote meta.json under the config.
93+ extractorManifest := & model.ConfigManifest {
94+ ConfigKey : model.ConfigKey {
95+ BranchID : 1 ,
96+ ComponentID : "ex-generic-v2" ,
97+ ID : "789" ,
98+ },
99+ Paths : model.Paths {AbsPath : model.AbsPath {RelativePath : "extractor/ex-generic-v2/empty" }},
100+ }
101+
102+ err := r .SetRecords ([]model.ObjectManifest {branchManifest , schedulerManifest , extractorManifest })
103+
104+ // The orphaned scheduler is reported but does not hard-fail the whole load.
105+ require .Error (t , err )
106+ assert .Contains (t , err .Error (), `manifest record for config "branch:1/component:keboola.orchestrator/config:999" not found` )
107+
108+ // Orphaned scheduler must be deleted.
109+ _ , found := r .GetRecord (schedulerManifest .Key ())
110+ assert .False (t , found , "orphaned scheduler must be removed from records" )
111+
112+ // Branch must survive and be resolved.
113+ branchRecord , found := r .GetRecord (branchManifest .Key ())
114+ require .True (t , found , "branch record must remain" )
115+ assert .True (t , branchRecord .IsParentPathSet ())
116+
117+ // Sibling extractor must survive with its parent path correctly resolved so
118+ // that Path() returns "main/extractor/ex-generic-v2/empty" and not the bare
119+ // relative path "extractor/ex-generic-v2/empty" (which would panic later).
120+ extRecord , found := r .GetRecord (extractorManifest .Key ())
121+ require .True (t , found , "sibling extractor must not be removed" )
122+ assert .True (t , extRecord .IsParentPathSet (),
123+ "sibling extractor must have parent path resolved even after the orphaned scheduler fails" )
124+ assert .Equal (t , "main/extractor/ex-generic-v2/empty" , extRecord .Path ())
125+ }
0 commit comments