Skip to content

ci: run the full offline test suite and auto-rebuild stale dist before tests - #212

Merged
sherifahmed990 merged 2 commits into
devfrom
ci/offline-tests
Jun 12, 2026
Merged

ci: run the full offline test suite and auto-rebuild stale dist before tests#212
sherifahmed990 merged 2 commits into
devfrom
ci/offline-tests

Conversation

@Sednaoui

@Sednaoui Sednaoui commented Jun 11, 2026

Copy link
Copy Markdown
Member
  • CI now runs yarn test (all suites outside test/integration/ — transport, safe, calibur, paymaster, signer, utilities; 468 tests, ~5s, all offline) instead of only test:signer.
  • New jest globalSetup rebuilds dist/ automatically when src/ (or build inputs) is newer, so any jest invocation — including npx jest <path> — tests current code instead of a stale build. No-op when fresh.
  • Renamed the integration job label integration/chainIdintegration (it runs the whole suite, not just chainId). If branch protection lists integration/chainId as a required check, update it to integration.

Verified: full suite passes locally (468/468); guard rebuilds exactly once on stale dist and skips when fresh.

Summary by CodeRabbit

  • Chores

    • Updated CI workflow test organization, renaming steps to better reflect their scope and improving overall test categorization for developers.
    • Modified integration workflow job naming convention for improved clarity and consistency in the Actions UI.
  • Tests

    • Implemented automatic build freshness validation to ensure the distribution package is rebuilt whenever source files change before test execution.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Jest configuration now includes a global setup hook to verify the built package is fresh before test execution. A new freshness-checking script compares dist/index.cjs modification time against source and build input files, rebuilding automatically if needed. CI workflows are updated to document offline test suites and clarify the integration job name.

Changes

Test Infrastructure Freshness Enforcement

Layer / File(s) Summary
Fresh build verification system
jest.config.cjs, test/_ensureFreshDist.cjs
Jest configuration adds globalSetup pointing to a freshness-checking script that computes the newest modification time among build inputs (src/, package.json, tsconfig.json, tsdown.config.ts), checks if dist/index.cjs is stale, and rebuilds via npm run build if necessary before tests run.
CI workflow updates
.github/workflows/ci.yml, .github/workflows/integration.yml
The offline unit tests step in the CI workflow is renamed with documentation listing offline-only suites (transport, safe, calibur, paymaster, signer, utility), and the integration job name is simplified from integration/chainId to integration.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • candidelabs/abstractionkit#157: Both PRs modify the test/CI harness for running signer/unit tests—specifically jest.config.cjs (main adds globalSetup: test/_ensureFreshDist.cjs, retrieved adds Node/ignore patterns) and .github/workflows/ci.yml (both adjust how signer/unit test suites are executed).

Suggested reviewers

  • sherifahmed990

Poem

🐰 Fresh builds guaranteed, no stale code here,
Jest's guardian spirit keeps packages clear,
When source files change, the dist rebuilds fast,
Tests always run true, not shackled to the past! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description provides detailed changes and verification but does not follow the required template structure with Summary, Test, and Risk/Compatibility sections. Reorganize the description to match the required template format with dedicated Summary, Test, and Risk/Compatibility sections for clarity.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: running the full offline test suite and auto-rebuilding stale dist before tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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


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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6babbf7d54

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread test/_ensureFreshDist.cjs
Comment on lines +17 to +21
let newest = 0;
for (const name of fs.readdirSync(entry)) {
newest = Math.max(newest, newestMtimeMs(path.join(entry, name)));
}
return newest;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include directory mtimes when checking stale dist

When a source file is deleted or renamed without touching another input, this recursion ignores directory mtimes and only considers the remaining files. In that refactor scenario the containing directory's mtime is the only timestamp newer than dist/index.cjs, so globalSetup skips the rebuild and targeted Jest runs keep testing a bundle that still contains the removed code. Seed the directory case with stat.mtimeMs (or otherwise include directory mtimes) so deletions invalidate dist/ too.

Useful? React with 👍 / 👎.

@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: 1

🧹 Nitpick comments (2)
test/_ensureFreshDist.cjs (2)

24-24: 💤 Low value

Remove unnecessary async keyword.

The exported function is declared async but does not use await or return a Promise. Jest's globalSetup accepts both sync and async functions, so the async keyword is unnecessary here.

♻️ Simplify function signature
-module.exports = async () => {
+module.exports = () => {
 	const distEntry = path.join(ROOT, "dist", "index.cjs");
🤖 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 `@test/_ensureFreshDist.cjs` at line 24, The exported function is declared
async but never uses await or returns a Promise; remove the unnecessary async to
make it a synchronous export by changing the declaration from "module.exports =
async () => { ... }" to "module.exports = () => { ... }" (locate the export
assigned to module.exports in the _ensureFreshDist module and update that arrow
function signature).

32-32: 💤 Low value

Add error handling for build failures.

If npm run build fails, execSync will throw but the error message may not clearly indicate that the freshness check triggered the build. Consider wrapping in try-catch to provide clearer context or allow the natural error to propagate with stdio context.

♻️ Optional: add error context
 	if (!fs.existsSync(distEntry) || fs.statSync(distEntry).mtimeMs < newestInput) {
 		console.log("\ndist/ is stale relative to src/ — rebuilding before tests...");
-		execSync("npm run build", { stdio: "inherit", cwd: ROOT });
+		try {
+			execSync("npm run build", { stdio: "inherit", cwd: ROOT });
+		} catch (err) {
+			console.error("\n[_ensureFreshDist] Build failed during freshness check.");
+			throw err;
+		}
 	}
🤖 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 `@test/_ensureFreshDist.cjs` at line 32, Wrap the execSync call that runs "npm
run build" (the execSync("npm run build", { stdio: "inherit", cwd: ROOT }) line)
in a try-catch; on catch, either log or rethrow an error that adds clear context
such as "Build failed during freshness check" while preserving the original
error details/stack and keeping stdio behavior, so failures clearly indicate
they were triggered by the freshness check (do this where the execSync call
resides in test/_ensureFreshDist.cjs).
🤖 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 `@test/_ensureFreshDist.cjs`:
- Around line 12-22: The recursive function newestMtimeMs currently follows
symlinks and can loop on cycles; change its file checks to use fs.lstatSync
instead of fs.statSync and skip entries where lstat.isSymbolicLink() is true
before recursing, so in newestMtimeMs call lstatSync on entry to decide
directory vs file and when iterating fs.readdirSync(entry) lstatSync each child
and continue (skip) if isSymbolicLink(); keep existing mtimeMs handling for
regular files/directories.

---

Nitpick comments:
In `@test/_ensureFreshDist.cjs`:
- Line 24: The exported function is declared async but never uses await or
returns a Promise; remove the unnecessary async to make it a synchronous export
by changing the declaration from "module.exports = async () => { ... }" to
"module.exports = () => { ... }" (locate the export assigned to module.exports
in the _ensureFreshDist module and update that arrow function signature).
- Line 32: Wrap the execSync call that runs "npm run build" (the execSync("npm
run build", { stdio: "inherit", cwd: ROOT }) line) in a try-catch; on catch,
either log or rethrow an error that adds clear context such as "Build failed
during freshness check" while preserving the original error details/stack and
keeping stdio behavior, so failures clearly indicate they were triggered by the
freshness check (do this where the execSync call resides in
test/_ensureFreshDist.cjs).
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2645d023-97df-4e10-a7bb-6b7250fa74d4

📥 Commits

Reviewing files that changed from the base of the PR and between 42edb77 and 6babbf7.

📒 Files selected for processing (4)
  • .github/workflows/ci.yml
  • .github/workflows/integration.yml
  • jest.config.cjs
  • test/_ensureFreshDist.cjs

Comment thread test/_ensureFreshDist.cjs
Comment on lines +12 to +22
function newestMtimeMs(entry) {
const stat = fs.statSync(entry);
if (!stat.isDirectory()) {
return stat.mtimeMs;
}
let newest = 0;
for (const name of fs.readdirSync(entry)) {
newest = Math.max(newest, newestMtimeMs(path.join(entry, name)));
}
return newest;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add symlink protection to prevent infinite loops.

The recursive directory traversal does not check for symbolic links before descending. If src/ or any subdirectory contains a symlink that creates a cycle, newestMtimeMs will loop infinitely and hang the test suite.

🛡️ Proposed fix to skip symlinks
 function newestMtimeMs(entry) {
 	const stat = fs.statSync(entry);
-	if (!stat.isDirectory()) {
+	if (stat.isSymbolicLink() || !stat.isDirectory()) {
 		return stat.mtimeMs;
 	}
 	let newest = 0;

Alternatively, use lstatSync instead of statSync to avoid following symlinks:

 function newestMtimeMs(entry) {
-	const stat = fs.statSync(entry);
+	const stat = fs.lstatSync(entry);
-	if (!stat.isDirectory()) {
+	if (stat.isSymbolicLink() || !stat.isDirectory()) {
 		return stat.mtimeMs;
 	}
 	let newest = 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function newestMtimeMs(entry) {
const stat = fs.statSync(entry);
if (!stat.isDirectory()) {
return stat.mtimeMs;
}
let newest = 0;
for (const name of fs.readdirSync(entry)) {
newest = Math.max(newest, newestMtimeMs(path.join(entry, name)));
}
return newest;
}
function newestMtimeMs(entry) {
const stat = fs.lstatSync(entry);
if (stat.isSymbolicLink() || !stat.isDirectory()) {
return stat.mtimeMs;
}
let newest = 0;
for (const name of fs.readdirSync(entry)) {
newest = Math.max(newest, newestMtimeMs(path.join(entry, name)));
}
return newest;
}
🤖 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 `@test/_ensureFreshDist.cjs` around lines 12 - 22, The recursive function
newestMtimeMs currently follows symlinks and can loop on cycles; change its file
checks to use fs.lstatSync instead of fs.statSync and skip entries where
lstat.isSymbolicLink() is true before recursing, so in newestMtimeMs call
lstatSync on entry to decide directory vs file and when iterating
fs.readdirSync(entry) lstatSync each child and continue (skip) if
isSymbolicLink(); keep existing mtimeMs handling for regular files/directories.

@sherifahmed990 sherifahmed990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@sherifahmed990
sherifahmed990 merged commit abef7ee into dev Jun 12, 2026
3 checks passed
@sherifahmed990
sherifahmed990 deleted the ci/offline-tests branch June 12, 2026 23:41
@Sednaoui Sednaoui mentioned this pull request Jul 27, 2026
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.

2 participants