fix(diff): merge template records sharing name+type into one RRset

Two template rows at the same name and type (e.g. two MX) produced two
diffs with one key; each PATCH replaced the whole provider RRset, so only
the last value survived. Merge values by key before diffing.

docs: add lint/single-test commands and compose files to CLAUDE.md

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
vasyanskandClaude Sonnet 5.5 committed 2026-11-02 17:26:01 +07:00
1 parent 5e12c4729b
commit c5f1e64873
3 files changed
+66 -5

No files matched your search

+38
View File
@@ -90,6 +90,7 @@ func (c Changeset) Prunes() []RecordDiff {
// Diff compares a template against the actual zone records.
// Records present in the zone but absent from the template yield Delete.
func Diff(template, actual []model.Record) Changeset {
template = mergeByKey(template)
current := index(actual)
seen := make(map[string]bool, len(template))
var diffs []RecordDiff
@@ -120,6 +121,43 @@ func Diff(template, actual []model.Record) Changeset {
return Changeset{Diffs: diffs}
}
// mergeByKey folds template records sharing a Key() into one RRset whose
// Values are the union (deduplicated by normalized value) in first-seen order.
// A template may list several records at the same name+type (two MX rows);
// the provider models them as ONE RRset, so applying them separately would
// make each replace the previous. TTL of the first record wins. Order of
// first occurrences is preserved.
func mergeByKey(recs []model.Record) []model.Record {
out := make([]model.Record, 0, len(recs))
pos := make(map[string]int, len(recs))
for _, r := range recs {
k := r.Key()
i, ok := pos[k]
if !ok {
r.Values = append([]string(nil), r.Values...)
pos[k] = len(out)
out = append(out, r)
continue
}
have := make(map[string]bool, len(out[i].Values))
for _, v := range out[i].Values {
have[normalizeValueOf(out[i].Type, v)] = true
}
for _, v := range r.Values {
nv := normalizeValueOf(r.Type, v)
if !have[nv] {
have[nv] = true
out[i].Values = append(out[i].Values, v)
}
}
}
return out
}
func normalizeValueOf(t model.RecordType, v string) string {
return model.Record{Type: t, Values: []string{v}}.NormalizedValues()[0]
}
// index builds a lookup of records by Key(). If two records in recs share
// the same Key() (e.g. a provider returning a duplicate RRset entry), this
// is a deliberate, documented choice, not an oversight: the LAST record with