fix(dashboard): materialise an immutable leaf whose ancestor is gone

overlayImmutable stopped walking as soon as the submitted body had
nothing at a path segment, so a YAML edit that dropped a whole parent
object also dropped the immutable leaf under it -- reachable through
foundationdb storage.storageClass and kafka kafka.storageClass. Create
the missing ancestor when the persisted spec has one, and turn the
pinned FIXME test into a test of the fixed behaviour.

Fixes #3102

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
This commit is contained in:
Myasnikov Daniil authored and Aleksei Sviridkin committed 2026-10-07 05:03:19 +03:00
1 parent 4a5e18b6e1
commit 2da6594466
2 files changed
+25 -10

No files matched your search

@@ -377,20 +377,31 @@ describe("overlayImmutable", () => {
})
})
it("does NOT materialise an immutable leaf when its ancestor is missing in target (pinned current behaviour)", () => {
// Tracked in cozystack/cozystack-ui#9.
// Defence-in-depth gap: if the YAML editor strips the parent object
// entirely, overlay cannot reach the leaf to copy the original value.
// Pin behaviour so a future contributor fixing the gap notices the
// test and revises it.
it("materialises an immutable leaf when its ancestor is missing in target", () => {
// A YAML edit that strips the parent object must not strip the immutable
// leaf with it. foundationdb's storage.storageClass is a shipped path of
// this shape.
const submitted = { spec: {} } as Record<string, unknown>
const original = {
spec: { backup: { storageClass: "slow" } },
spec: { storage: { storageClass: "replicated", size: "10Gi" } },
}
const result = overlayImmutable(submitted, original, [
["spec", "backup", "storageClass"],
["spec", "storage", "storageClass"],
])
expect(result).toEqual({ spec: {} })
expect(result).toEqual({ spec: { storage: { storageClass: "replicated" } } })
})
it("materialises an immutable leaf when its ancestor is null in target", () => {
// `storage:` with nothing under it parses to null, not to a missing key,
// so the walker has to treat it the same as absent.
const submitted = { spec: { storage: null } } as Record<string, unknown>
const original = {
spec: { storage: { storageClass: "replicated", size: "10Gi" } },
}
const result = overlayImmutable(submitted, original, [
["spec", "storage", "storageClass"],
])
expect(result).toEqual({ spec: { storage: { storageClass: "replicated" } } })
})
it("array reordering by the user with index-aligned overlay re-anchors source values to the new index (pinned current behaviour)", () => {
@@ -174,7 +174,11 @@ function overlayPath(
const sourceVal = sourceObj ? sourceObj[seg] : undefined
const targetObj = isPlainObject(target)
? (target as Record<string, unknown>)
: null
: // A YAML edit can drop the whole intermediate object; materialise it so
// the immutable leaf underneath is still restored from source.
sourceVal !== undefined && (target === undefined || target === null)
? {}
: null
if (!targetObj) return target
if (
sourceVal === undefined &&