Skip to content

chore: eliminate array coercion in fillStoreCache path construction#2973

Open
joonas wants to merge 3 commits into
defenseunicorns:mainfrom
joonas:fix/storage-clear-malformed-json-patch
Open

chore: eliminate array coercion in fillStoreCache path construction#2973
joonas wants to merge 3 commits into
defenseunicorns:mainfrom
joonas:fix/storage-clear-malformed-json-patch

Conversation

@joonas
Copy link
Copy Markdown
Member

@joonas joonas commented Feb 28, 2026

Description

Storage.clear() collects all store keys into a single string[] and dispatches one ("remove", ["key1", "key2", ...]) call through to fillStoreCache.

The path construction placed cacheItem.key as a single element inside another array literal:

[`/data/${capabilityName}`, cacheItem.version, cacheItem.key]

When .join("-") encountered the nested array, it called .toString(), coercing the keys into a comma-separated string and producing one malformed JSON Patch path like
/data/cap-v2-key1,v2-key2,v2-key3 instead of three individual remove operations. The Kubernetes API rejects or misapplies this path, so Storage.clear() never actually removed anything from the PeprStore resource.

Move path computation out of the shared preamble and into each branch:

  • The remove branch now iterates over cacheItem.key, computing a separate path and cache entry per key.

  • The add branch uses cacheItem.key[0] (a string) instead of cacheItem.key (a string[]), and guards with an explicit length check to prevent the same class of bug from reappearing silently.

The migrateStore test expectations are updated to match the corrected paths — the old trailing dashes (e.g.,
remove:/data/hello-pepr-example-1-) were artifacts of the array coercion producing [""].toString()"" which survived the .filter() as a nested array but became an empty segment after .join("-").

End to End Test:
(See Pepr Excellent Examples)

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Other (security config, docs update, etc)

Checklist before merging

`Storage.clear()` collects all store keys into a single `string[]`
and dispatches one `("remove", ["key1", "key2", ...])` call through
to `fillStoreCache`.

The path construction placed `cacheItem.key` as a single element inside
another array literal:

[`/data/${capabilityName}`, cacheItem.version, cacheItem.key]

When `.join("-")` encountered the nested array, it called
`.toString()`, coercing the keys into a comma-separated string and
producing one malformed JSON Patch path like
`/data/cap-v2-key1,v2-key2,v2-key3` instead of three individual
remove operations. The Kubernetes API rejects or misapplies this
path, so `Storage.clear()` never actually removed anything from
the PeprStore resource.

Move path computation out of the shared preamble and into each
branch:

- The `remove` branch now iterates over `cacheItem.key`, computing
  a separate path and cache entry per key.

- The `add` branch uses `cacheItem.key[0]` (a `string`) instead of
  `cacheItem.key` (a `string[]`), and guards with an explicit
  length check to prevent the same class of bug from reappearing
  silently.

The `migrateStore` test expectations are updated to match the
corrected paths — the old trailing dashes (e.g.,
`remove:/data/hello-pepr-example-1-`) were artifacts of the array
coercion producing `[""].toString()` → `""` which survived the
`.filter()` as a nested array but became an empty segment after
`.join("-")`.

Signed-off-by: Joonas Bergius <joonas@defenseunicorns.com>
@samayer12
Copy link
Copy Markdown
Contributor

@greptileai

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 22, 2026

Greptile Summary

This PR fixes Storage.clear() never actually removing keys from the PeprStore resource. The root cause was that cacheItem.key (a string[]) was placed inside the path array before .join("-"), causing JavaScript to coerce the whole array to a comma-separated string and produce a single, malformed JSON Patch path. The fix moves path construction into each operation branch: remove now iterates every key individually, and add explicitly indexes key[0] with a length guard.

Confidence Score: 5/5

Safe to merge — the fix is correct, well-scoped, and regression-tested; the single comment is a minor test-precision suggestion.

All remaining findings are P2 style suggestions that do not affect correctness or data integrity. The core bug fix and path construction logic are sound, and the new multi-key test provides direct regression coverage.

migrateStore.test.ts — the stringMatching overlap is low-risk but worth tightening before future refactors obscure it.

Important Files Changed

Filename Overview
src/lib/controller/storeCache.ts Core bug fix: remove branch now iterates individual keys to produce one JSON Patch entry per key; add branch uses key[0] with an explicit length guard. Logic and filter chain are correct.
src/lib/controller/storeCache.test.ts New test case directly exercises the Storage.clear() multi-key dispatch path and asserts three individual remove operations — good regression coverage for the bug.
src/lib/controller/migrateStore.test.ts Updated expectations remove spurious trailing dashes from the old coercion bug; one matcher is still a substring match that overlaps with the -data key, which could produce a false-green if the bare remove entry disappears.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["fillStoreCache(cache, capabilityName, op, cacheItem)"] --> B{op}
    B -->|"add"| C{key.length === 1?}
    C -->|No| D["throw Error: ADD expects exactly one key"]
    C -->|Yes| E["path = join(['/data/'+capabilityName, version, key[0]])"]
    E --> F["cache[add:path:value] = {op, path, value}"]
    B -->|"remove"| G{key.length >= 1?}
    G -->|No| H["throw Error: Key is required"]
    G -->|Yes| I["for each key in cacheItem.key"]
    I --> J["path = join(['/data/'+capabilityName, version, key])"]
    J --> K["cache[remove:path] = {op, path}"]
    K --> I
    B -->|other| L["throw Error: Unsupported operation"]
    F --> M["return cache"]
    K --> M
Loading

Reviews (1): Last reviewed commit: "Merge branch 'main' into fix/storage-cle..." | Re-trigger Greptile

Comment on lines +98 to +99
expect.stringMatching("remove:/data/hello-pepr-example-1"),
expect.stringMatching("add:/data/hello-pepr-example-1-v2:was-here"),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Test matcher is too broad — may silently pass even if one remove is missing

expect.stringMatching("remove:/data/hello-pepr-example-1") is a substring match, so it also matches "remove:/data/hello-pepr-example-1-data". With arrayContaining, both matchers could satisfy themselves against the same "-data" key, meaning the test would pass even if the bare "remove:/data/hello-pepr-example-1" entry was never emitted. Using an anchored regex prevents this ambiguity.

Suggested change
expect.stringMatching("remove:/data/hello-pepr-example-1"),
expect.stringMatching("add:/data/hello-pepr-example-1-v2:was-here"),
expect.stringMatching(/^remove:\/data\/hello-pepr-example-1$/),
expect.stringMatching("add:/data/hello-pepr-example-1-v2:was-here"),

@AmberFryar AmberFryar changed the title fix: eliminate array coercion in fillStoreCache path construction chore: eliminate array coercion in fillStoreCache path construction May 6, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

❌ Patch coverage is 77.77778% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.37%. Comparing base (28928f7) to head (34b8379).

Files with missing lines Patch % Lines
src/lib/controller/storeCache.ts 77.77% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2973      +/-   ##
==========================================
- Coverage   77.40%   77.37%   -0.04%     
==========================================
  Files          93       93              
  Lines        2536     2541       +5     
  Branches      532      533       +1     
==========================================
+ Hits         1963     1966       +3     
- Misses        446      447       +1     
- Partials      127      128       +1     
Files with missing lines Coverage Δ
src/lib/controller/storeCache.ts 91.17% <77.77%> (-5.38%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants