Skip to content

fix(review): linkedIssueSatisfactionCacheInputFingerprint's delimiter-joined payload allows cache-key collisions across different prompts #5939

Description

@JSONbored

Context

linkedIssueSatisfactionCacheInputFingerprint in src/review/linked-issue-satisfaction-cache-input.ts (lines 20-30) builds the fingerprint payload used to cache/replay a linked-issue-satisfaction AI verdict:

export async function linkedIssueSatisfactionCacheInputFingerprint(input: LinkedIssueSatisfactionCacheInput): Promise<string> {
  const payload = [
    LINKED_ISSUE_SATISFACTION_CACHE_INPUT_VERSION,
    input.byok ? "1" : "0",
    input.provider ?? "",
    input.model ?? "",
    input.issueText ?? "",
    input.prTitle ?? "",
    input.prBody ?? "",
    input.diff ?? "",
  ].join("|");
  return `${LINKED_ISSUE_SATISFACTION_CACHE_INPUT_VERSION}:${await sha256Hex(payload)}`;
}

The payload is a bare "|"-joined string of free-form GitHub text fields (issueText, prTitle, prBody, diff). None of that text is escaped, so a | character occurring naturally inside one field can shift the field boundary and produce the exact same joined string for two genuinely different inputs — e.g. {issueText: "foo|bar", prTitle: "baz", ...} and {issueText: "foo", prTitle: "bar|baz", ...} (other fields held equal) serialize identically and hash to the same fingerprint/cache key.

Its own file comment says it "mirrors ai-slop-cache-input.ts's fingerprint discipline" — but that sibling module (src/review/ai-slop-cache-input.ts) actually builds its payload via JSON.stringify({...}), which structurally escapes delimiters and can't collide this way. src/review/ai-review-cache-input.ts follows the same JSON.stringify discipline. linked-issue-satisfaction-cache-input.ts is the one outlier still using a raw string join.

Effect: two structurally different (issue text, PR title/body, diff) combinations can collide on the same cache key, causing getCachedLinkedIssueSatisfaction/putCachedLinkedIssueSatisfaction (src/db/repositories) to serve a stale AI verdict written for a different prompt — silently wrong linked-issue-satisfaction gate results.

test/unit/linked-issue-satisfaction-cache.test.ts asserts that edits to individual fields change the fingerprint, but no case exercises a field value containing |, so this collision is untested.

Requirements

  • Change linkedIssueSatisfactionCacheInputFingerprint's payload construction from a raw "|"-joined string to a JSON.stringify-based structured payload, mirroring the pattern already used in src/review/ai-slop-cache-input.ts and src/review/ai-review-cache-input.ts.
  • Bump LINKED_ISSUE_SATISFACTION_CACHE_INPUT_VERSION to a new version string (e.g. linked-issue-satisfaction-input:v3) so existing cached rows keyed on the old, collision-prone fingerprint format are not accidentally treated as valid under the new scheme.
  • Preserve the existing field semantics (?? fallback to ""/null as appropriate) — only the serialization mechanism changes, not which fields are included or their null-handling meaning.
  • Do not change the function's public signature, return type, or the surrounding cache read/write call sites in src/db/repositories.

Deliverables

  • linkedIssueSatisfactionCacheInputFingerprint builds its payload via JSON.stringify instead of a delimiter-joined string.
  • LINKED_ISSUE_SATISFACTION_CACHE_INPUT_VERSION bumped to a new version string.
  • A regression test in test/unit/linked-issue-satisfaction-cache.test.ts proving two inputs that previously collided under the raw-join scheme (a |-containing field value shifted across two different field boundaries) now produce distinct fingerprints.
  • Existing fingerprint-stability tests (same input → same fingerprint; single-field edit → different fingerprint) still pass unmodified in intent.

Test Coverage Requirements

Aim for 99%+ Codecov patch coverage (100% including the new branch/invariant) on the touched lines in src/review/linked-issue-satisfaction-cache-input.ts. This is a fix for a real cache-key collision, so the delimiter-collision regression test is required, not just incidental line coverage.

Expected Outcome

Two structurally different linked-issue-satisfaction prompt inputs can no longer produce the same cache fingerprint because of an unescaped | inside free-form GitHub text, matching the collision-resistant discipline already used by the sibling ai-slop-cache-input.ts and ai-review-cache-input.ts modules.

Links & Resources

  • src/review/linked-issue-satisfaction-cache-input.ts (linkedIssueSatisfactionCacheInputFingerprint, lines 20-30)
  • src/review/ai-slop-cache-input.ts (aiSlopCacheInputFingerprint — the JSON.stringify pattern to mirror)
  • src/review/ai-review-cache-input.ts (same pattern)
  • test/unit/linked-issue-satisfaction-cache.test.ts (existing test suite)

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions