Skip to content

fix: throw on missing content blob oids#68

Merged
flyingrobots merged 10 commits intomainfrom
feature/issue-46-get-content-missing-blob
Mar 14, 2026
Merged

fix: throw on missing content blob oids#68
flyingrobots merged 10 commits intomainfrom
feature/issue-46-get-content-missing-blob

Conversation

@flyingrobots
Copy link
Member

@flyingrobots flyingrobots commented Mar 13, 2026

Summary

  • fix GitGraphAdapter.readBlob() so swallowed missing-object reads no longer surface as empty bytes
  • preserve legitimate zero-byte blobs by verifying object existence only on ambiguous empty reads
  • add adapter and integration regressions, and align the content docs/changelog with the throwing contract

Closes #46.

Testing

  • npx vitest run test/unit/domain/services/GitGraphAdapter.test.js test/unit/domain/WarpGraph.content.test.js test/integration/api/content-attachment.test.js
  • npx markdownlint CHANGELOG.md README.md ROADMAP.md docs/specs/CONTENT_ATTACHMENT.md
  • git push -u origin feature/issue-46-get-content-missing-blob

Summary by CodeRabbit

  • Breaking Changes

    • getContent() return type narrowed from Buffer | null to Uint8Array | null.
    • getContent() and getEdgeContent() now throw a persistence error when content references a missing blob instead of returning empty bytes.
  • Bug Fixes

    • Missing or corrupted content references are now surfaced reliably through the API.
  • Documentation

    • Specs and decoding guidance updated to reflect new return type and error semantics.
  • Tests

    • New and updated tests cover missing-blob error paths.
  • Chores

    • Pre-push hook tooling and configuration improved for linting and test runs.

@github-actions
Copy link

Release Preflight

  • package version: 14.0.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v14.0.0, release workflow will publish.

@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 69947d65-117f-4c22-9c6b-a2213f38c172

📥 Commits

Reviewing files that changed from the base of the PR and between 0976680 and c109c99.

📒 Files selected for processing (2)
  • src/infrastructure/adapters/GitGraphAdapter.js
  • test/unit/domain/services/GitGraphAdapter.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/unit/domain/services/GitGraphAdapter.test.js

📝 Walkthrough

Walkthrough

Missing content blob OIDs now surface as PersistenceError(E_MISSING_OBJECT) instead of returning empty bytes; getContent()/getEdgeContent() return Uint8Array | null; GitGraphAdapter adds an existence check for empty reads; pre-push hook gains configurable launcher/binary wrappers and run_tool/command_exists helpers.

Changes

Cohort / File(s) Summary
Documentation
CHANGELOG.md, README.md, docs/specs/CONTENT_ATTACHMENT.md
Narrowed content return type to `Uint8Array
Infrastructure Adapters
src/infrastructure/adapters/CasBlobAdapter.js, src/infrastructure/adapters/GitGraphAdapter.js
CasBlobAdapter now throws PersistenceError(E_MISSING_OBJECT) for missing blobs; GitGraphAdapter adds _assertBlobExistsForEmptyRead(oid) and invokes it when readBlob yields zero bytes to distinguish empty blob vs missing object and wrap git errors.
Domain / API JSDoc
src/domain/warp/query.methods.js
JSDoc for getContent and getEdgeContent updated to indicate PersistenceError with code E_MISSING_OBJECT may be thrown for missing blob references.
Build / Hooks
scripts/hooks/pre-push
Added command_exists() and run_tool() helpers; introduced configurable NPM_BIN, NPM_LAUNCHER, LINKCHECK_BIN, LINKCHECK_LAUNCHER; replaced direct tool calls with run_tool; extended QUICK-mode handling for Gate 8.
Integration Tests
test/integration/api/content-attachment.test.js
Added tests asserting getContent() and getEdgeContent() reject with PersistenceError.E_MISSING_OBJECT when _content points to a non-existent blob OID.
Unit Tests — Domain & Adapters
test/unit/domain/WarpGraph.content.test.js, test/unit/infrastructure/adapters/CasBlobAdapter.test.js, test/unit/domain/services/GitGraphAdapter.test.js
Added/updated tests to assert E_MISSING_OBJECT is preserved/surfaced from blobStorage and Git read paths; GitGraphAdapter tests cover empty-read existence check and error propagation.
Unit Tests — Hook
test/unit/scripts/pre-push-hook.test.js
Updated test harness for new WARP_NPM_BIN, WARP_NPM_LAUNCHER, WARP_LINKCHECK_BIN, WARP_LINKCHECK_LAUNCHER; added scenario for unreadable launcher skipping Gate 0.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant GitGraphAdapter as Warp (GitGraphAdapter)
    participant Git as "git (cat-file / cat-file -e)"
    Client->>GitGraphAdapter: readBlob(oid)
    GitGraphAdapter->>Git: stream blob data (cat-file -p oid)
    alt blob data non-empty
        Git-->>GitGraphAdapter: returns bytes
        GitGraphAdapter-->>Client: return Uint8Array
    else blob data empty
        Git-->>GitGraphAdapter: returns zero bytes
        GitGraphAdapter->>Git: cat-file -e oid (existence check)
        alt object exists
            Git-->>GitGraphAdapter: exit 0 (exists)
            GitGraphAdapter-->>Client: return empty Uint8Array
        else object missing
            Git-->>GitGraphAdapter: exit non-zero / not found
            GitGraphAdapter-->>Client: throw PersistenceError(E_MISSING_OBJECT, { oid })
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰
I nibbled logs where bytes were thin,
Found missing blobs that hid within.
Now when they vanish, I loudly shout,
"PersistenceError — find me or rout!" ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description provides a clear summary and test plan but omits all ADR checks required by the template, leaving them unchecked or unaddressed. Complete the ADR checks section by explicitly checking/unchecking each checkbox and providing brief notes if any PR aspects require ADR verification.
Out of Scope Changes check ⚠️ Warning The PR includes out-of-scope changes to the pre-push hook (scripts/hooks/pre-push) involving launcher configuration and refactoring that are unrelated to the content blob OID error handling objectives. Remove or isolate changes to scripts/hooks/pre-push in a separate PR focused on launcher configuration, keeping this PR focused solely on the content blob missing-object error handling.
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: throw on missing content blob oids' directly and concisely summarizes the main change: making the API throw when content blob OIDs are missing rather than returning empty bytes.
Linked Issues check ✅ Passed The PR fully addresses issue #46 by implementing all coding objectives: throwing PersistenceError on missing blob OIDs, preserving zero-byte blobs, adding comprehensive tests, and updating documentation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/issue-46-get-content-missing-blob
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

Release Preflight

  • package version: 14.0.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v14.0.0, release workflow will publish.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
scripts/hooks/pre-push (1)

59-60: Use configured binary name in Gate 0 skip message.

The message always says “lychee not installed”, even when a custom link checker is configured, which can mislead debugging.

♻️ Suggested tweak
-  echo "[Gate 0] Link check skipped (lychee not installed)"
+  echo "[Gate 0] Link check skipped ($LINKCHECK_BIN not installed)"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/hooks/pre-push` around lines 59 - 60, The Gate 0 skip echo is
hardcoded to "lychee not installed"; update the echo in the Gate 0 skip path to
interpolate the configured link-checker variable (e.g. use
${LINK_CHECKER:-lychee} or ${LINK_CHECKER_BIN:-lychee}) instead of the literal
"lychee" so the message shows the actual configured binary name when skipping
the link check.
test/integration/api/content-attachment.test.js (1)

243-245: Prefer asserting the typed error code in integration tests.

The current regex is permissive. Since this PR standardizes E_MISSING_OBJECT, asserting err.code here would better protect the contract from regressions.

✅ Suggested assertion style
-    await expect(graph.getContent('doc:1'))
-      .rejects.toThrow(/Missing Git object|Blob not found|bad object/i);
+    await expect(graph.getContent('doc:1'))
+      .rejects.toMatchObject({ code: 'E_MISSING_OBJECT' });

-    await expect(graph.getEdgeContent('a', 'b', 'rel'))
-      .rejects.toThrow(/Missing Git object|Blob not found|bad object/i);
+    await expect(graph.getEdgeContent('a', 'b', 'rel'))
+      .rejects.toMatchObject({ code: 'E_MISSING_OBJECT' });

Also applies to: 263-264

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/api/content-attachment.test.js` around lines 243 - 245,
Replace the loose regex-based rejection assertion with an assertion that checks
the error's typed code (E_MISSING_OBJECT) when calling graph.getContent('doc:1')
in the content-attachment integration test; specifically, change the await
expect(graph.getContent(...)).rejects.toThrow(/.../) to await
expect(graph.getContent(...)).rejects.toMatchObject({ code: 'E_MISSING_OBJECT'
}) or await expect(graph.getContent(...)).rejects.toHaveProperty('code',
'E_MISSING_OBJECT') for both occurrences (around the getContent call and the
other assertion at lines ~263-264) so the test explicitly asserts err.code
instead of a permissive regex.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/specs/CONTENT_ATTACHMENT.md`:
- Around line 115-117: Update the spec to explicitly state throw behavior for
getEdgeContent() to match getContent(): document that when an _edgeContent or
referenced blob OID is missing, getEdgeContent() will throw an error rather than
returning empty bytes, and mirror the guidance about consumers decoding bytes
(e.g., new TextDecoder().decode(buffer)); reference the same error semantics and
examples used for getContent() so the contract is unambiguous between
getContent() and getEdgeContent().

In `@src/infrastructure/adapters/GitGraphAdapter.js`:
- Around line 376-382: The current logic treats any git error with exit code 1
or 128 as a missing object; narrow this by checking the git error message/output
for evidence it’s actually a missing object (e.g., strings like "bad object",
"fatal: ambiguous argument", "not found", or similar in
gitErr.message/gitErr.stderr) before mapping to
PersistenceError.E_MISSING_OBJECT; if that additional check fails, do not
classify as E_MISSING_OBJECT — instead rethrow the original error or wrap it in
a different PersistenceError (preserving cause) so repo/access errors aren’t
misreported; update the block around
wrapped/gitErr/getExitCode/PersistenceError/E_MISSING_OBJECT/oid to implement
this conditional check.

---

Nitpick comments:
In `@scripts/hooks/pre-push`:
- Around line 59-60: The Gate 0 skip echo is hardcoded to "lychee not
installed"; update the echo in the Gate 0 skip path to interpolate the
configured link-checker variable (e.g. use ${LINK_CHECKER:-lychee} or
${LINK_CHECKER_BIN:-lychee}) instead of the literal "lychee" so the message
shows the actual configured binary name when skipping the link check.

In `@test/integration/api/content-attachment.test.js`:
- Around line 243-245: Replace the loose regex-based rejection assertion with an
assertion that checks the error's typed code (E_MISSING_OBJECT) when calling
graph.getContent('doc:1') in the content-attachment integration test;
specifically, change the await
expect(graph.getContent(...)).rejects.toThrow(/.../) to await
expect(graph.getContent(...)).rejects.toMatchObject({ code: 'E_MISSING_OBJECT'
}) or await expect(graph.getContent(...)).rejects.toHaveProperty('code',
'E_MISSING_OBJECT') for both occurrences (around the getContent call and the
other assertion at lines ~263-264) so the test explicitly asserts err.code
instead of a permissive regex.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 67447dea-19f6-45c7-9b30-5ff78973bcc3

📥 Commits

Reviewing files that changed from the base of the PR and between 7de235e and 84c90a0.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • README.md
  • ROADMAP.md
  • docs/specs/CONTENT_ATTACHMENT.md
  • scripts/hooks/pre-push
  • src/infrastructure/adapters/CasBlobAdapter.js
  • src/infrastructure/adapters/GitGraphAdapter.js
  • test/integration/api/content-attachment.test.js
  • test/unit/domain/WarpGraph.content.test.js
  • test/unit/domain/services/GitGraphAdapter.test.js
  • test/unit/infrastructure/adapters/CasBlobAdapter.test.js
  • test/unit/scripts/pre-push-hook.test.js

Comment on lines +115 to 117
`getContent()` returns raw `Uint8Array` bytes. Consumers wanting text should decode with `new TextDecoder().decode(buffer)`.
If `_content` points at a missing blob OID, `getContent()` throws instead of silently returning empty bytes.

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Document edge-content throw semantics explicitly too.

This section now states throw behavior for getContent(), but not getEdgeContent(). If both throw on missing blob OIDs, document both to avoid contract ambiguity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/specs/CONTENT_ATTACHMENT.md` around lines 115 - 117, Update the spec to
explicitly state throw behavior for getEdgeContent() to match getContent():
document that when an _edgeContent or referenced blob OID is missing,
getEdgeContent() will throw an error rather than returning empty bytes, and
mirror the guidance about consumers decoding bytes (e.g., new
TextDecoder().decode(buffer)); reference the same error semantics and examples
used for getContent() so the contract is unambiguous between getContent() and
getEdgeContent().

@github-actions
Copy link

Release Preflight

  • package version: 14.0.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v14.0.0, release workflow will publish.

@github-actions
Copy link

Release Preflight

  • package version: 14.0.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v14.0.0, release workflow will publish.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
test/unit/domain/services/GitGraphAdapter.test.js (1)

5-64: Consider adding a test for the exit-code-1 fallback path.

The current tests exercise the wrapGitError pattern-matching path (exit 128 + "bad object") but not the ambiguousMissingObject fallback (exit code 1 with empty stderr). While this is an edge case, adding a test would ensure the fallback logic remains correct:

📝 Suggested test
it('throws E_MISSING_OBJECT for exit-1 with empty stderr (ambiguous case)', async () => {
  mockPlumbing.executeStream.mockResolvedValue({
    collect: vi.fn().mockResolvedValue(Buffer.alloc(0)),
  });
  const err = /** `@type` {any} */ (new Error(''));
  err.details = { code: 1, stderr: '' };
  mockPlumbing.execute.mockRejectedValue(err);

  await expect(adapter.readBlob('deadbeef'))
    .rejects.toMatchObject({
      code: 'E_MISSING_OBJECT',
      message: 'Missing Git object: deadbeef',
    });
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/domain/services/GitGraphAdapter.test.js` around lines 5 - 64, Add a
unit test to cover the exit-code-1 ambiguous-missing-object fallback in
GitGraphAdapter.readBlob: simulate executeStream.collect returning an empty
Buffer and mock plumbing.execute to reject with an Error whose details are {
code: 1, stderr: '' }, then assert adapter.readBlob('deadbeef') rejects with {
code: 'E_MISSING_OBJECT', message: 'Missing Git object: deadbeef' }; place this
alongside the existing readBlob tests to ensure the
ambiguousMissingObject/wrapGitError fallback path is exercised.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/unit/domain/services/GitGraphAdapter.test.js`:
- Around line 5-64: Add a unit test to cover the exit-code-1
ambiguous-missing-object fallback in GitGraphAdapter.readBlob: simulate
executeStream.collect returning an empty Buffer and mock plumbing.execute to
reject with an Error whose details are { code: 1, stderr: '' }, then assert
adapter.readBlob('deadbeef') rejects with { code: 'E_MISSING_OBJECT', message:
'Missing Git object: deadbeef' }; place this alongside the existing readBlob
tests to ensure the ambiguousMissingObject/wrapGitError fallback path is
exercised.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9284ec66-5d09-4dce-b137-43c63ec5a1c7

📥 Commits

Reviewing files that changed from the base of the PR and between d5258ff and 27ae8c4.

📒 Files selected for processing (3)
  • docs/specs/CONTENT_ATTACHMENT.md
  • src/infrastructure/adapters/GitGraphAdapter.js
  • test/unit/domain/services/GitGraphAdapter.test.js

@github-actions
Copy link

Release Preflight

  • package version: 14.0.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v14.0.0, release workflow will publish.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
scripts/hooks/pre-push (1)

55-60: Consider clarifying the skip message for non-readable scenarios.

The message "lychee not installed" is technically imprecise when the skip reason is that the target file isn't readable (e.g., permissions issue). A more accurate message might be "lychee not available". However, this is a minor UX consideration and the current simplification is reasonable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/hooks/pre-push` around lines 55 - 60, Summary: The skip message
"lychee not installed" can be misleading when the real reason is that lychee
isn't accessible/readable; update the message to be more accurate. Change the
echo in the else branch that currently prints "[Gate 0] Link check skipped
(lychee not installed)" to a clearer message such as "[Gate 0] Link check
skipped (lychee not available)" or "[Gate 0] Link check skipped (lychee not
available or not readable)" so it covers both installation and accessibility
issues; update the echo near the command_exists check for LINKCHECK_LAUNCHER and
LINKCHECK_BIN that precedes run_tool to reflect this wording. Ensure only the
message text is changed and the logic around command_exists, LINKCHECK_LAUNCHER,
LINKCHECK_BIN, and run_tool remains untouched.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@scripts/hooks/pre-push`:
- Around line 55-60: Summary: The skip message "lychee not installed" can be
misleading when the real reason is that lychee isn't accessible/readable; update
the message to be more accurate. Change the echo in the else branch that
currently prints "[Gate 0] Link check skipped (lychee not installed)" to a
clearer message such as "[Gate 0] Link check skipped (lychee not available)" or
"[Gate 0] Link check skipped (lychee not available or not readable)" so it
covers both installation and accessibility issues; update the echo near the
command_exists check for LINKCHECK_LAUNCHER and LINKCHECK_BIN that precedes
run_tool to reflect this wording. Ensure only the message text is changed and
the logic around command_exists, LINKCHECK_LAUNCHER, LINKCHECK_BIN, and run_tool
remains untouched.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0b6dc83e-53e8-49a7-ade5-4bd06ae6dd27

📥 Commits

Reviewing files that changed from the base of the PR and between 27ae8c4 and 0976680.

📒 Files selected for processing (4)
  • scripts/hooks/pre-push
  • src/domain/warp/query.methods.js
  • test/unit/domain/services/GitGraphAdapter.test.js
  • test/unit/scripts/pre-push-hook.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/unit/domain/services/GitGraphAdapter.test.js

@github-actions
Copy link

Release Preflight

  • package version: 14.0.0
  • prerelease: false
  • npm dist-tag on release: latest
  • npm pack dry-run: passed
  • jsr publish dry-run: passed

If you tag this commit as v14.0.0, release workflow will publish.

@flyingrobots flyingrobots merged commit 4586045 into main Mar 14, 2026
7 checks passed
@flyingrobots flyingrobots deleted the feature/issue-46-get-content-missing-blob branch March 14, 2026 00:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

getContent() returns empty Buffer instead of throwing/null for missing blob OID

1 participant