Skip to content

test: add build diagnostic metadata tests#42

Open
dev-khimesh wants to merge 10 commits into
jackjin1997:mainfrom
dev-khimesh:main
Open

test: add build diagnostic metadata tests#42
dev-khimesh wants to merge 10 commits into
jackjin1997:mainfrom
dev-khimesh:main

Conversation

@dev-khimesh

@dev-khimesh dev-khimesh commented Jul 9, 2026

Copy link
Copy Markdown

Summary

Add 18 deterministic tests for build diagnostic report generation metadata.

Changes

  • Added tests/test_diagnostic_metadata.py with 18 tests covering:
    • Report metadata shape: commit ID, module summaries (name/status/elapsed/artifact), diagnostic_logd path, pass/fail counts, IS0 timestamp
    • Module ordering: preserved through report
    • logd failure: diagnostic_logd=None, diagnostic_logd_error set, no password/decrypt_command when archive fails, message_blocker populated
    • Chunked references: single → string, multi → list, chunk_size_bytes, forward-slash paths
    • Decrypt command: generated with password, None without
    • Determinism: current_commit_id() format, path naming patterns
  • All tests deterministic, no external toolchain or network required

Validation

python3 -m pytest tests/test_diagnostic_metadata.py -v  # 18/18 pass

Closes #3

Summary by CodeRabbit

  • New Features

    • Added a preflight check for encryption-related diagnostics before builds start.
    • Diagnostic reports now include clearer blocker messages and richer archive/metadata details.
  • Bug Fixes

    • Improved error handling for API responses so failures are reported with better messages and suggestions.
    • Made diagnostic artifact commits more reliable and ensured build failures stop the process early when needed.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

build.py adds an encryptly preflight check, a message_blocker diagnostic field, and git-based commit logic for diagnostic artifacts, gating the build in main(). New tests validate diagnostic report shape. Separately, frontend/src/services/api.ts restructures HTTP error handling into structured ApiError objects with interceptor processing and new parsing helpers.

Changes

Build diagnostics and encryptly preflight

Layer / File(s) Summary
Encryptly blocker constant and preflight check
build.py
Adds ENCRYPTLY_BLOCKER_MESSAGE and check_encryptly_runs(), which runs encryptly pack in a temporary workspace and verifies a .logd is produced.
Diagnostic report message_blocker field
build.py
build_diagnostic_report() gains an optional message_blocker parameter that is emitted in the generated JSON report.
Commit diagnostic artifacts and wire into generate_logd
build.py
Adds commit_diagnostic_artifacts() (git status/add/commit) and wires it into the encryptly-missing, pack-failure, and split-success branches of generate_logd; raises --max-file-size to 61440.
main() preflight gate and exit code logic
build.py
main() runs the encryptly preflight check before building modules, exits with code 1 on failure, tweaks a warning message, and updates the final exit code to require both diagnostics success and passing module builds.
Diagnostic metadata regression tests
tests/test_diagnostic_metadata.py
New pytest suite covering commit id, module summaries, diagnostic_logd/error fields, chunked references, decrypt command generation, and id/path determinism.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Frontend API Error Handling

Layer / File(s) Summary
Structured ApiError construction and error body parsing
frontend/src/services/api.ts
request() checks response.ok, builds an ApiError from the response body via safeParseErrorBody() and getDefaultSuggestion(), runs it through errorInterceptors, and throws it; catch logic re-throws already-structured HTTP-like errors (code 100–599) without renormalization.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant apiRequest as request()
  participant Fetch as fetch API
  participant ErrorParser as safeParseErrorBody()
  participant Interceptors as errorInterceptors

  Client->>apiRequest: call request()
  apiRequest->>Fetch: fetch(url, options)
  Fetch-->>apiRequest: response
  alt response not ok
    apiRequest->>ErrorParser: parse error body
    ErrorParser-->>apiRequest: message/details/suggestion
    apiRequest->>apiRequest: build ApiError (code, message, requestId)
    apiRequest->>Interceptors: apply errorInterceptors(error)
    Interceptors-->>apiRequest: processed ApiError
    apiRequest-->>Client: throw ApiError
  else response ok
    apiRequest-->>Client: return parsed data
  end
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The frontend/src/services/api.ts changes are unrelated to build diagnostic metadata tests and fall outside issue #3's scope. Remove the frontend API error-handling changes from this PR or split them into a separate PR focused on that behavior.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately describes the main change: adding build diagnostic metadata tests.
Description check ✅ Passed The description covers the summary, changes, and local test command, but it omits the required checklist section.
Linked Issues check ✅ Passed The new tests cover metadata shape, failure handling, chunked refs, and determinism as required by issue #3.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Warning

⚠️ This pull request shows signs of AI-generated slop (description_diff_mismatch). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
tests/test_diagnostic_metadata.py (1)

320-326: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider avoiding filesystem side effects in test_diagnostic_path_pattern.

diagnostic_paths_for_commit() calls DIAGNOSTIC_DIR.mkdir(parents=True, exist_ok=True), so this test creates a diagnostic/ directory in the project root. Using pytest's tmp_path fixture with monkeypatch to redirect DIAGNOSTIC_DIR would keep the test self-contained and avoid leaving artifacts behind.

♻️ Optional refactor using tmp_path and monkeypatch
-def test_diagnostic_path_pattern():
+def test_diagnostic_path_pattern(tmp_path, monkeypatch):
     """diagnostic_paths_for_commit() returns paths matching build-PATTERN.logd/json."""
-    from build import diagnostic_paths_for_commit
+    from build import diagnostic_paths_for_commit, diagnostic_paths_for_commit as _dpfc
+    import build
+
+    monkeypatch.setattr(build, "DIAGNOSTIC_DIR", tmp_path / "diagnostic")
 
     logd, meta, commit_id = diagnostic_paths_for_commit()
     assert logd.name == f"build-{commit_id}.logd"
     assert meta.name == f"build-{commit_id}.json"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_diagnostic_metadata.py` around lines 320 - 326,
`test_diagnostic_path_pattern` is creating a real `diagnostic/` directory
because `diagnostic_paths_for_commit()` uses `DIAGNOSTIC_DIR.mkdir(...)`. Update
the test to stay isolated by using pytest’s `tmp_path` and `monkeypatch` to
redirect `build.DIAGNOSTIC_DIR` to a temporary location before calling
`diagnostic_paths_for_commit()`, so the assertions on `logd`, `meta`, and
`commit_id` remain the same without leaving filesystem artifacts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@build.py`:
- Around line 883-887: Persist the encryptly preflight blocker into the
diagnostic metadata path used by build.py. In the `check_encryptly_runs()`
failure branch, the `blocker` is currently only printed and stored in `results`,
so `generate_logd()` leaves `diagnostic_logd_error` and `message_blocker` empty;
update the `generate_logd` call or its inputs so the blocker is carried through
and written into the JSON report, or emit the blocked metadata report directly
from this branch. Use the existing `blocker`, `results`, and `generate_logd`
flow to keep the top-level blocker contract intact.
- Around line 231-256: The preflight pack step in the preflight check is
including the output bundle path itself via workspace, and the non-zero
return-code validation is disabled. Update the preflight logic around
subprocess.run and logd_path so the generated preflight.logd is excluded from
the pack inputs, then restore the result.returncode failure check in this block
to fail fast on any pack error before checking logd_path.exists().
- Around line 553-579: The git subprocess calls in commit_diagnostic_artifacts
can still raise exceptions before a returncode is available, so generate_logd
may fail instead of handling the error cleanly. Update the subprocess.run usage
for the status, add, and commit steps in commit_diagnostic_artifacts to catch
OSError and SubprocessError/TimeoutExpired, log a concise failure message, and
return False. Keep the behavior consistent with the existing return-false paths
and use the commit_diagnostic_artifacts function name to locate the changes.

In `@frontend/src/services/api.ts`:
- Around line 307-329: The fallback in the error parsing logic is broken because
calling cloned.json() consumes the body before cloned.text() can run. Update the
response handling in api.ts so the raw text is read first (or use a separate
clone for JSON parsing), then parse JSON from that string inside the existing
response parsing flow, preserving the message/details/suggestion extraction in
the same error-shaping logic.

---

Nitpick comments:
In `@tests/test_diagnostic_metadata.py`:
- Around line 320-326: `test_diagnostic_path_pattern` is creating a real
`diagnostic/` directory because `diagnostic_paths_for_commit()` uses
`DIAGNOSTIC_DIR.mkdir(...)`. Update the test to stay isolated by using pytest’s
`tmp_path` and `monkeypatch` to redirect `build.DIAGNOSTIC_DIR` to a temporary
location before calling `diagnostic_paths_for_commit()`, so the assertions on
`logd`, `meta`, and `commit_id` remain the same without leaving filesystem
artifacts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 97e0d9ce-623a-4df2-b21f-e417678f4a3f

📥 Commits

Reviewing files that changed from the base of the PR and between 1462fe7 and ec4b07f.

⛔ Files ignored due to path filters (2)
  • tools/encryptly/windows-arm64/encryptly.exe is excluded by !**/*.exe
  • tools/encryptly/windows-x64/encryptly.exe is excluded by !**/*.exe
📒 Files selected for processing (8)
  • build.py
  • frontend/src/services/api.ts
  • tests/__init__.py
  • tests/test_diagnostic_metadata.py
  • tools/encryptly/linux-arm64/encryptly
  • tools/encryptly/linux-x64/encryptly
  • tools/encryptly/macos-arm64/encryptly
  • tools/encryptly/macos-x64/encryptly

Comment thread build.py
Comment on lines +231 to +256
workspace = Path.home() / ".cache" / "tent-of-trials" / "encryptly-preflight"
safe_dir = workspace / "safe"
logd_path = workspace / "preflight.logd"
try:
shutil.rmtree(workspace, ignore_errors=True)
safe_dir.mkdir(parents=True, exist_ok=True)
(safe_dir / "preflight.txt").write_text("encryptly preflight\n", encoding="utf-8")
result = subprocess.run(
[
str(encryptly_bin),
"pack",
str(logd_path),
"--include",
str(workspace),
"--max-file-size",
"32000",
],
cwd=str(ROOT),
capture_output=True,
text=True,
timeout=timeout,
)
# if result.returncode != 0:
# output = result.stderr.strip() or result.stdout.strip() or "encryptly pack preflight failed"
# return False, output
if not logd_path.exists():

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant section of build.py with line numbers.
sed -n '190,290p' build.py

# Find any other handling of result.returncode or preflight .logd paths.
rg -n "returncode|preflight\.logd|--include|encryptly pack preflight failed" build.py

Repository: jackjin1997/TentOfTrials

Length of output: 3966


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the other pack call for comparison.
sed -n '650,700p' build.py

# Search for any documentation/comments explaining the preflight pack behavior.
rg -n "preflight|encryptly pack|include .*workspace|safe_dir|logd_path" .

Repository: jackjin1997/TentOfTrials

Length of output: 4789


Exclude the output bundle from preflight inputs and fail on pack errors.
preflight.logd sits under workspace, so --include workspace can pull the file being written into the pack; restore the non-zero return-code check so a partial .logd can’t pass preflight.

🧰 Tools
🪛 ast-grep (0.44.1)

[error] 237-251: Command coming from incoming request
Context: subprocess.run(
[
str(encryptly_bin),
"pack",
str(logd_path),
"--include",
str(workspace),
"--max-file-size",
"32000",
],
cwd=str(ROOT),
capture_output=True,
text=True,
timeout=timeout,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🪛 Ruff (0.15.20)

[error] 238-238: subprocess call: check for execution of untrusted input

(S603)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@build.py` around lines 231 - 256, The preflight pack step in the preflight
check is including the output bundle path itself via workspace, and the non-zero
return-code validation is disabled. Update the preflight logic around
subprocess.run and logd_path so the generated preflight.logd is excluded from
the pack inputs, then restore the result.returncode failure check in this block
to fail fast on any pack error before checking logd_path.exists().

Comment thread build.py
Comment on lines +553 to +579
status = subprocess.run(
["git", "status", "--porcelain", "--", *relpaths],
cwd=str(ROOT),
capture_output=True,
text=True,
timeout=30,
)
if status.returncode != 0:
print(f" {color('✗', Colors.RED)} Could not inspect diagnostic git status: {status.stderr.strip()}")
return False
if not status.stdout.strip():
print(f" {color('✓', Colors.GREEN)} Diagnostic artifacts already committed")
return True

add = subprocess.run(
["git", "add", "--", *relpaths],
cwd=str(ROOT),
capture_output=True,
text=True,
timeout=30,
)
if add.returncode != 0:
print(f" {color('✗', Colors.RED)} Could not stage diagnostic artifacts: {add.stderr.strip()}")
return False

commit = subprocess.run(
["git", "commit", "-m", f"Add build diagnostics for {commit_id}", "--", *relpaths],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '500,650p' build.py

Repository: jackjin1997/TentOfTrials

Length of output: 6318


🏁 Script executed:

rg -n "def generate_logd|generate_logd\(" build.py

Repository: jackjin1997/TentOfTrials

Length of output: 296


🏁 Script executed:

rg -n "generate_logd|diagnostic artifacts|Could not inspect diagnostic git status|Could not stage diagnostic artifacts|Could not commit diagnostic artifacts" build.py

Repository: jackjin1997/TentOfTrials

Length of output: 809


🏁 Script executed:

sed -n '594,760p' build.py

Repository: jackjin1997/TentOfTrials

Length of output: 6982


Catch git subprocess failures in commit_diagnostic_artifacts. subprocess.run(..., timeout=...) can raise TimeoutExpired or OSError before returncode exists, so a missing or hung git can escape generate_logd instead of returning False. Wrap these calls and return False on OSError/SubprocessError.

🧰 Tools
🪛 ast-grep (0.44.1)

[error] 566-572: Command coming from incoming request
Context: subprocess.run(
["git", "add", "--", *relpaths],
cwd=str(ROOT),
capture_output=True,
text=True,
timeout=30,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🪛 Ruff (0.15.20)

[error] 553-553: subprocess call: check for execution of untrusted input

(S603)


[error] 554-554: Starting a process with a partial executable path

(S607)


[error] 567-567: subprocess call: check for execution of untrusted input

(S603)


[error] 568-568: Starting a process with a partial executable path

(S607)


[error] 578-578: subprocess call: check for execution of untrusted input

(S603)


[error] 579-579: Starting a process with a partial executable path

(S607)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@build.py` around lines 553 - 579, The git subprocess calls in
commit_diagnostic_artifacts can still raise exceptions before a returncode is
available, so generate_logd may fail instead of handling the error cleanly.
Update the subprocess.run usage for the status, add, and commit steps in
commit_diagnostic_artifacts to catch OSError and SubprocessError/TimeoutExpired,
log a concise failure message, and return False. Keep the behavior consistent
with the existing return-false paths and use the commit_diagnostic_artifacts
function name to locate the changes.

Source: Linters/SAST tools

Comment thread build.py
Comment on lines +883 to +887
blocker = f"{ENCRYPTLY_BLOCKER_MESSAGE} {encryptly_message}"
print(f" {color('✗ encryptly cannot run', Colors.RED)}")
print(f" {color('BLOCKER:', Colors.RED)} {blocker}")
results = [("encryptly-preflight", False, elapsed, blocker, None)]
generate_logd(results, args.verbose)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Persist the preflight blocker in the diagnostic metadata.

Here blocker is only placed in the module output. If check_encryptly_runs() fails but the later generate_logd() call succeeds, the JSON report has diagnostic_logd_error=None and message_blocker=None, losing the top-level blocker contract.

Pass the preflight error/blocker into diagnostic report generation, or write the blocked metadata report directly on this path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@build.py` around lines 883 - 887, Persist the encryptly preflight blocker
into the diagnostic metadata path used by build.py. In the
`check_encryptly_runs()` failure branch, the `blocker` is currently only printed
and stored in `results`, so `generate_logd()` leaves `diagnostic_logd_error` and
`message_blocker` empty; update the `generate_logd` call or its inputs so the
blocker is carried through and written into the JSON report, or emit the blocked
metadata report directly from this branch. Use the existing `blocker`,
`results`, and `generate_logd` flow to keep the top-level blocker contract
intact.

Comment on lines +307 to +329
const contentType = response.headers.get('content-type') || '';
const cloned = response.clone();

try {
if (contentType.includes('application/json')) {
const body = await cloned.json();
if (body && typeof body === 'object') {
return {
message: typeof body.message === 'string' ? body.message
: typeof body.error === 'string' ? body.error
: typeof body.error?.message === 'string' ? body.error.message
: undefined,
details: body.details || body.errors || undefined,
suggestion: typeof body.suggestion === 'string' ? body.suggestion : undefined,
};
}
}
} catch {
// JSON parse failed, try as text
}

try {
const text = await cloned.text();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Fetch API Response.json() consume the response body so a later Response.text() call on the same clone fails after JSON parse failure?

💡 Result:

The Fetch API's Response body is a stream that can be consumed only once [1][2][3]. When you call a method such as.json,.text, or.blob, the underlying stream is read to completion and then locked [4][5][2]. Regarding your specific scenario: If you create a clone of a Response using Response.clone before consuming it, you can successfully read the body of the clone independently [6][7]. However, if you attempt to call a second consumption method (like.text) on the same Response object (or the same clone) after a previous consumption method (like.json) has already initiated or completed, it will fail [1][8][2]. If the initial.json call fails due to a parsing error, the stream is still considered "used" or "locked" because the consumption process was already initiated [1][5]. Consequently, a subsequent.text call on that same object will still result in an error (typically a TypeError stating the stream is already read or locked) [1][8][9]. To read the data in multiple formats or to handle potential parse failures while retaining access to the raw body, the recommended approach is to clone the response before the first attempt to read it [1][6][7]. For example: const response = await fetch(url); const clone = response.clone; try { const data = await response.json; } catch (e) { // Parsing failed, but you can still read from the clone const rawText = await clone.text; console.error("JSON parse failed, raw body:", rawText); }

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="frontend/src/services/api.ts"

# Show the relevant function with line numbers
wc -l "$FILE"
sed -n '280,360p' "$FILE" | cat -n

# Look for other body-reading patterns on Response in this file
rg -n "response\.clone\(\)|\.json\(\)|\.text\(\)|safeParseErrorBody" "$FILE"

Repository: jackjin1997/TentOfTrials

Length of output: 3769


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="frontend/src/services/api.ts"

wc -l "$FILE"
sed -n '280,360p' "$FILE" | cat -n

rg -n "response\.clone\(\)|\.json\(\)|\.text\(\)|safeParseErrorBody" "$FILE"

Repository: jackjin1997/TentOfTrials

Length of output: 3769


Read the body into text before parsing JSON

cloned.json() consumes the body, so the later cloned.text() fallback can’t recover the raw error message after a JSON parse failure. Read text first (or use a second clone) and parse JSON from that string instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/services/api.ts` around lines 307 - 329, The fallback in the
error parsing logic is broken because calling cloned.json() consumes the body
before cloned.text() can run. Update the response handling in api.ts so the raw
text is read first (or use a separate clone for JSON parsing), then parse JSON
from that string inside the existing response parsing flow, preserving the
message/details/suggestion extraction in the same error-shaping logic.

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.

[$30 BOUNTY] [Python] Add build diagnostic metadata tests

1 participant