@@ -135,6 +135,116 @@ func TestE2E_JSONGetSetMerge(t *testing.T) {
135135 }
136136}
137137
138+ // TestE2E_YAMLSetPreserve verifies that `--preserve` performs a surgical in-place
139+ // edit: only the targeted value changes, while comments, key order, and quoting
140+ // of every other line are left byte-for-byte intact.
141+ func TestE2E_YAMLSetPreserve (t * testing.T ) {
142+ dir := t .TempDir ()
143+ values := filepath .Join (dir , "values.yaml" )
144+ original := `# Helm values
145+ image:
146+ repository: myapp # do not touch
147+ tag: "v1.0.0"
148+ replicas: 3
149+ `
150+ if err := os .WriteFile (values , []byte (original ), 0644 ); err != nil {
151+ t .Fatal (err )
152+ }
153+
154+ _ , stderr , code := runPipekit (t ,
155+ []string {"yaml" , "set" , values , "--path" , ".image.tag" , "--value" , "v2.0.0" , "--in-place" , "--preserve" }, "" )
156+ if code != 0 {
157+ t .Fatalf ("preserve set exit %d, stderr: %s" , code , stderr )
158+ }
159+ got , _ := os .ReadFile (values )
160+ gotStr := string (got )
161+
162+ for _ , want := range []string {"# Helm values" , "repository: myapp # do not touch" , `tag: "v2.0.0"` , "replicas: 3" } {
163+ if ! strings .Contains (gotStr , want ) {
164+ t .Errorf ("preserve lost %q:\n %s" , want , gotStr )
165+ }
166+ }
167+ if strings .Contains (gotStr , "v1.0.0" ) {
168+ t .Errorf ("old value should be gone:\n %s" , gotStr )
169+ }
170+
171+ // Backward-compat sanity: the same edit WITHOUT --preserve still works,
172+ // just normalizing formatting (comments dropped).
173+ if err := os .WriteFile (values , []byte (original ), 0644 ); err != nil {
174+ t .Fatal (err )
175+ }
176+ _ , _ , code = runPipekit (t ,
177+ []string {"yaml" , "set" , values , "--path" , ".image.tag" , "--value" , "v2.0.0" , "--in-place" }, "" )
178+ if code != 0 {
179+ t .Fatalf ("legacy set exit %d" , code )
180+ }
181+ legacy , _ := os .ReadFile (values )
182+ if ! strings .Contains (string (legacy ), "v2.0.0" ) {
183+ t .Errorf ("legacy set failed:\n %s" , legacy )
184+ }
185+ }
186+
187+ // TestE2E_JSONDelPreserve verifies surgical key removal keeps surrounding JSON
188+ // formatting intact.
189+ func TestE2E_JSONDelPreserve (t * testing.T ) {
190+ dir := t .TempDir ()
191+ cfg := filepath .Join (dir , "config.json" )
192+ original := "{\n \" a\" : 1,\n \" b\" : 2,\n \" c\" : 3\n }\n "
193+ if err := os .WriteFile (cfg , []byte (original ), 0644 ); err != nil {
194+ t .Fatal (err )
195+ }
196+ _ , stderr , code := runPipekit (t ,
197+ []string {"json" , "del" , cfg , "--path" , ".b" , "--in-place" , "--preserve" }, "" )
198+ if code != 0 {
199+ t .Fatalf ("preserve del exit %d, stderr: %s" , code , stderr )
200+ }
201+ want := "{\n \" a\" : 1,\n \" c\" : 3\n }\n "
202+ if got , _ := os .ReadFile (cfg ); string (got ) != want {
203+ t .Errorf ("got:\n %s\n want:\n %s" , got , want )
204+ }
205+ }
206+
207+ // TestE2E_JSONSetPreserveInsert verifies `set --preserve` can add a new key to
208+ // an existing object, formatting-matched to its siblings.
209+ func TestE2E_JSONSetPreserveInsert (t * testing.T ) {
210+ dir := t .TempDir ()
211+ cfg := filepath .Join (dir , "config.json" )
212+ if err := os .WriteFile (cfg , []byte ("{\n \" a\" : 1\n }\n " ), 0644 ); err != nil {
213+ t .Fatal (err )
214+ }
215+ _ , stderr , code := runPipekit (t ,
216+ []string {"json" , "set" , cfg , "--path" , ".b" , "--json-value" , "2" , "--in-place" , "--preserve" }, "" )
217+ if code != 0 {
218+ t .Fatalf ("insert exit %d, stderr: %s" , code , stderr )
219+ }
220+ want := "{\n \" a\" : 1,\n \" b\" : 2\n }\n "
221+ if got , _ := os .ReadFile (cfg ); string (got ) != want {
222+ t .Errorf ("got:\n %s\n want:\n %s" , got , want )
223+ }
224+ }
225+
226+ // TestE2E_PreservePreservesFileMode verifies the atomic in-place write keeps the
227+ // original file's permission bits.
228+ func TestE2E_PreservePreservesFileMode (t * testing.T ) {
229+ dir := t .TempDir ()
230+ f := filepath .Join (dir , "values.yaml" )
231+ if err := os .WriteFile (f , []byte ("tag: v1\n " ), 0640 ); err != nil {
232+ t .Fatal (err )
233+ }
234+ _ , _ , code := runPipekit (t ,
235+ []string {"yaml" , "set" , f , "--path" , ".tag" , "--value" , "v2" , "--in-place" , "--preserve" }, "" )
236+ if code != 0 {
237+ t .Fatalf ("exit %d" , code )
238+ }
239+ info , err := os .Stat (f )
240+ if err != nil {
241+ t .Fatal (err )
242+ }
243+ if info .Mode ().Perm () != 0640 {
244+ t .Errorf ("mode changed: got %o want 640" , info .Mode ().Perm ())
245+ }
246+ }
247+
138248func TestE2E_RenderFile (t * testing.T ) {
139249 dir := t .TempDir ()
140250 tmpl := filepath .Join (dir , "v.tpl" )
0 commit comments