Skip to content

WIP - test: Add Vitest test suite.#114

Open
junhaoliao wants to merge 62 commits intoy-scope:mainfrom
junhaoliao:vitest
Open

WIP - test: Add Vitest test suite.#114
junhaoliao wants to merge 62 commits intoy-scope:mainfrom
junhaoliao:vitest

Conversation

@junhaoliao
Copy link
Copy Markdown
Member

@junhaoliao junhaoliao commented Feb 1, 2026

PR Split Plan for vitest Branch

Prerequisites

Source Branch

All changes originate from the vitest branch. Base comparison commit:
fe1d1684b379ebc116e3e22e2bc7a361a9edfd46.

Total diff: ~914 lines (excluding package-lock.json).

Checklist


PR 1: Build infrastructure — MERGED

PR: #115
Status: Merged into main

Files

File Lines Notes
.gitignore +6 Add node_modules, /test/data entries
package.json +14/-1 Add devDependencies and scripts
package-lock.json Regenerated via npm install
tsconfig.json +38 New file
eslint.config.mjs +20 New file
vitest.config.ts +36 New file — unified config with node + browser projects
lint-tasks.yml +15 Add check-js, fix-js tasks
Taskfile.yml +1 Add test: "taskfiles/test.yaml" include
taskfiles/test.yaml +20 New file

PR 2: Test utilities and globalSetup — OPEN

PR: #118
Branch name: test/utilities
Base: main
Title: test: Add test utilities and ClpStreamReader tests
Diff size: ~219 lines

Files

File Lines Notes
test/utils.ts +143 Unified utilities: createModule, loadTestData, createReader, assertNonNull
test/globalSetup.ts +45 Downloads test data to test/data/ before tests run
test/ClpStreamReader.test.ts +30 Tests for module-level constants (IrStreamType, MERGED_KV_PAIRS_*)
vitest.config.ts +1 Add globalSetup reference

Notes

  • globalSetup.ts downloads test data once; Node reads from disk, browser fetches from Vite's
    dev server.
  • utils.ts uses runtime detection (isNodeRuntime()) to select Node.js or browser code paths.

Validation

task lint:check-js
task test:js

Expected: 2 tests pass.


PR 3: Playwright browser config and CI workflow — MERGED

PR: #125
Status: Merged into main

Files

File Lines Notes
.github/workflows/test.yaml +51 CI workflow for task test:js
package.json +3/-1 Add @vitest/browser-playwright, test:init script
package-lock.json Regenerated
vitest.config.ts +29/-3 Add Playwright browser project config (Chromium, Firefox, WebKit)
taskfiles/test.yaml +5/-1 Add init task for Playwright browser install
test/ClpStreamReader.test.ts +11 Dummy test (expect(true).toBe(true))
test/globalSetup.ts +5 No-op global setup

Notes


PR 4: Test constants

Branch name: test/constants
Base: main (after PRs #118 and #125 are merged)
Title: test: Add test constants
Diff size: ~86 lines

Files

File Lines Notes
test/constants.ts +86 Test constants: stream types, chunk sizes, log levels, event counts

Validation

task lint:check-js
task test:js

Expected: 2 tests pass (from PR #118).


PR 5: Common stream reader tests

Branch name: test/common-stream-tests
Base: PR 4's branch
Title: test: Add shared stream reader test suite
Diff size: ~160 lines

Files

File Lines Notes
test/commonStreamReaderTests.ts +160 describeCommonTests() — 9 shared tests for both structured and unstructured streams

Notes

  • Exports describeCommonTests(params) which registers shared it() tests.
  • Called inside describe() blocks in both stream reader test files.
  • Tests: stream type, metadata, event count, numEventsBuffered, decode first/last events, reset
    filter, find nearest timestamp, out-of-bounds decodeRange.

Validation

task lint:check-js

No runnable tests yet (shared suite only).


PR 6: Unstructured IR stream tests

Branch name: test/unstructured-ir
Base: PR 5's branch
Title: test: Add unstructured IR stream reader tests
Diff size: ~96 lines

Files

File Lines Notes
test/UnstructuredIrStreamReader.test.ts +96 12 tests (9 common + 3 unique)

Notes

  • Uses loadTestData("unstructured-yarn.clp.zst").
  • Unique tests: log level filtering, filtered decoding with useFilter, KQL ignored on unstructured.

Validation

task lint:check-js
task test:js

Expected: 14 tests pass (2 + 12).


PR 7: Structured IR stream tests

Branch name: test/structured-ir
Base: PR 6's branch
Title: test: Add structured IR stream reader tests
Diff size: ~160 lines

Files

File Lines Notes
test/StructuredIrStreamReader.test.ts +160 16 tests (9 common + 3 unique + 4 logLevelKey)

Notes

  • Uses loadTestData("structured-cockroachdb.clp.zst").
  • Two describe blocks:
    • Default null keys: 9 common + 3 unique (empty filter map, KQL query, combined KQL + log level
      without extracted keys).
    • With logLevelKey: 4 tests (extract log levels, filter INFO events, filtered decoding,
      combined KQL + log level with extracted keys).

Validation

task lint:check-js
task test:js

Expected: 30 tests pass (2 + 12 + 16).


Summary

| PR | Title | Status | Cumulative Tests |
|----|--------------------------------------------- --|---------|:----------------:|
| 1 | Build infrastructure (#115) | Merged | 0 |
| 2 | Test utilities and ClpStreamReader tests (#118) | Open | 2 |
| 3 | Playwright browser config and CI (#125) | Merged | 2 |
| 4 | Test constants | Pending | 2 |
| 5 | Common stream reader tests | Pending | 2 |
| 6 | Unstructured IR stream tests | Pending | 14 |
| 7 | Structured IR stream tests | Pending | 30 |

File Distribution

File PR
.gitignore 1
package.json 1
package-lock.json 1
tsconfig.json 1
eslint.config.mjs 1
vitest.config.ts 1
lint-tasks.yml 1
taskfile.yaml 1
taskfiles/test.yaml 1
test/utils.ts 2
test/globalSetup.ts 2
.github/workflows/test.yaml 3
test/constants.ts 3
test/ClpStreamReader.test.ts 3
test/commonStreamReaderTests.ts 4
test/UnstructuredIrStreamReader.test.ts 5
test/StructuredIrStreamReader.test.ts 6

@junhaoliao junhaoliao requested a review from a team as a code owner February 1, 2026 04:36
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 1, 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

Walkthrough

This pull request adds CMake build configuration for multiple dependencies (Boost, ystdlib, ANTLR, date, fmt, SIMDJSON, SPDLOG) and introduces a comprehensive test suite infrastructure for CLP stream readers with utilities, test data setup, and test cases for both structured and unstructured IR stream formats.

Changes

Cohort / File(s) Summary
Build Configuration
CMakeLists.txt
Added Boost interface header target, configured ystdlib, ANTLR, date, fmt, SIMDJSON, and SPDLOG as subdirectories with public build inputs. Disabled fmt installation (FMT_INSTALL OFF).
Test Infrastructure
test/constants.ts, test/utils.ts, test/globalSetup.ts
Added test constants (chunk sizes, IR stream types, log levels, event counts), utility functions for module creation, reader initialization, test data loading, and global setup for downloading missing test data files.
Test Implementations
test/ClpStreamReader.test.ts, test/StructuredIrStreamReader.test.ts, test/UnstructuredIrStreamReader.test.ts
Added three new test suites validating stream reader functionality: module constant verification, structured IR stream filtering and decoding, and unstructured IR stream filtering and event handling.
Shared Test Utilities
test/commonStreamReaderTests.ts
Added reusable test helper module defining CommonTestParams interface and describeCommonTests function for consistent stream reader behaviour validation across multiple test suites.
CI/CD Workflow
.github/workflows/tests.yaml
Added GitHub Actions workflow triggered on pull requests, pushes, and daily schedule that checks out code, installs task CLI, logs tool versions, and executes JavaScript tests via task test:js.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

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.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title includes a WIP prefix and refers to adding a Vitest test suite, which accurately describes the primary change of introducing comprehensive test infrastructure across multiple test files and configuration.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 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.

Copy link
Copy Markdown

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

🤖 Fix all issues with AI agents
In `@package.json`:
- Around line 16-18: The "lint" npm script references a missing "lint:check"
script, causing npm run lint to fail; update package.json scripts so
"lint:check" exists (e.g., add a "lint:check" script that runs "npm run
lint:check:js") or change the "lint" script to call "lint:check:js" directly;
modify the scripts section to include the new "lint:check" key (or update
"lint") so "lint", "lint:check", "lint:check:js", and "lint:fix:js" are
consistent.

In `@test/StructuredIrStreamReader.test.ts`:
- Around line 171-183: The test uses a tautological assertion
(toBeGreaterThanOrEqual(0)); replace it with meaningful bounds by asserting the
filtered result length is between 0 and the total number of events: after
calling reader.deserializeStream(), call reader.filterLogEvents(...) and then
getFilteredLogEventMap(); assert combinedMap.length is >= 0 and <= the original
total event count obtained from the reader (e.g. reader.getLogEventMap() or
whatever method returns the unfiltered event list), or assert an expected exact
count when known; apply the same change to the other test block that uses the
same tautological check.

In `@test/UnstructuredIrStreamReader.test.ts`:
- Around line 75-93: Add an explicit guard before destructuring the decoded
events: after calling reader.decodeRange(...) and the
expect(...).toHaveLength(...) assertion, check that events is non-null and
events.length > 0 (or call expect(events && events.length).toBeGreaterThan(0))
before doing const [event] = events; this ensures the test fails with a clear
message rather than throwing on destructuring; reference reader.decodeRange,
events, DECODE_CHUNK_SIZE and numEvents when locating where to add the guard.

In `@test/utils.ts`:
- Around line 30-38: downloadFileIfNeeded currently performs an unbounded stream
download directly to the final path with no status check or hard timeout; change
it to create the directory, start an AbortController-based hard-deadline timer
(e.g., 30_000ms) and pass its signal into axios.get, validate response.status
(expect 200) before streaming, stream into a temporary file (e.g., filePath +
".tmp") via pipeline, clear the timer and atomically rename the temp file to
filePath on success, and on any error/abort ensure the controller is aborted,
the temp file is removed and the write stream is closed; keep references to
axios.get, AbortController, pipeline, fs.createWriteStream, fs.rename and
fs.unlink when implementing these steps.

Comment on lines +75 to +93
it("should decode first events with expected fields", () => {
const numEvents = reader.deserializeStream();

const events = reader.decodeRange(0, Math.min(DECODE_CHUNK_SIZE, numEvents), false);

expect(events).not.toBeNull();
expect(events).toHaveLength(Math.min(DECODE_CHUNK_SIZE, numEvents));

const [event] = events as NonNullable<typeof events>;

expect(typeof event.logEventNum).toBe("number");
expect(typeof event.logLevel).toBe("number");
expect(typeof event.message).toBe("string");
expect(typeof event.timestamp).toBe("bigint");
expect([
"bigint",
"number",
]).toContain(typeof event.utcOffset);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding a guard before destructuring.

While the expect(events).toHaveLength(...) assertion on line 81 ensures the array has elements, the destructuring on line 83 happens unconditionally. If the assertion fails, the test would throw a different error on destructuring rather than a clear test failure.

This is a minor robustness concern, but acceptable given the assertion order.

♻️ Optional: Add explicit length check before destructuring
         expect(events).not.toBeNull();
         expect(events).toHaveLength(Math.min(DECODE_CHUNK_SIZE, numEvents));
 
-        const [event] = events as NonNullable<typeof events>;
+        const nonNullEvents = events as NonNullable<typeof events>;
+        expect(nonNullEvents.length).toBeGreaterThan(0);
+        const [event] = nonNullEvents;
📝 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
it("should decode first events with expected fields", () => {
const numEvents = reader.deserializeStream();
const events = reader.decodeRange(0, Math.min(DECODE_CHUNK_SIZE, numEvents), false);
expect(events).not.toBeNull();
expect(events).toHaveLength(Math.min(DECODE_CHUNK_SIZE, numEvents));
const [event] = events as NonNullable<typeof events>;
expect(typeof event.logEventNum).toBe("number");
expect(typeof event.logLevel).toBe("number");
expect(typeof event.message).toBe("string");
expect(typeof event.timestamp).toBe("bigint");
expect([
"bigint",
"number",
]).toContain(typeof event.utcOffset);
});
it("should decode first events with expected fields", () => {
const numEvents = reader.deserializeStream();
const events = reader.decodeRange(0, Math.min(DECODE_CHUNK_SIZE, numEvents), false);
expect(events).not.toBeNull();
expect(events).toHaveLength(Math.min(DECODE_CHUNK_SIZE, numEvents));
const nonNullEvents = events as NonNullable<typeof events>;
expect(nonNullEvents.length).toBeGreaterThan(0);
const [event] = nonNullEvents;
expect(typeof event.logEventNum).toBe("number");
expect(typeof event.logLevel).toBe("number");
expect(typeof event.message).toBe("string");
expect(typeof event.timestamp).toBe("bigint");
expect([
"bigint",
"number",
]).toContain(typeof event.utcOffset);
});
🤖 Prompt for AI Agents
In `@test/UnstructuredIrStreamReader.test.ts` around lines 75 - 93, Add an
explicit guard before destructuring the decoded events: after calling
reader.decodeRange(...) and the expect(...).toHaveLength(...) assertion, check
that events is non-null and events.length > 0 (or call expect(events &&
events.length).toBeGreaterThan(0)) before doing const [event] = events; this
ensures the test fails with a clear message rather than throwing on
destructuring; reference reader.decodeRange, events, DECODE_CHUNK_SIZE and
numEvents when locating where to add the guard.

test/utils.ts Outdated
Comment on lines +30 to +38
const downloadFileIfNeeded = async (url: string, filePath: string): Promise<void> => {
if (fs.existsSync(filePath)) {
return;
}

fs.mkdirSync(path.dirname(filePath), {recursive: true});

const response = await axios.get(url, {responseType: "stream"});
await pipeline(response.data, fs.createWriteStream(filePath));
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

🧩 Analysis chain

🌐 Web query:

Axios 1.13.4 timeout behavior responseType stream Node.js download

💡 Result:

In Axios v1.x (incl. 1.13.4) on Node.js with responseType: 'stream', timeout is not a “total download time limit.” In practice it behaves like a response/socket inactivity timeout: if the server keeps sending chunks, the timer effectively never trips, so a long streaming download can run well past timeout and only fail if the connection goes idle/stalls. This is a known/raised behavior for chunked/streaming responses. [1][2]

If you need a hard deadline for a streaming download, use cancellation (AbortController / signal) and abort on your own timer (or AbortSignal.timeout() where available). Axios docs explicitly recommend combining timeout with cancellation to avoid hangs. [3]

Example (hard 30s cap, then abort the stream request):

import axios from "axios";
import { pipeline } from "node:stream/promises";
import fs from "node:fs";

const controller = new AbortController();
const t = setTimeout(() => controller.abort(new Error("Download deadline exceeded")), 30_000);

try {
  const res = await axios.get(url, { responseType: "stream", signal: controller.signal });
  await pipeline(res.data, fs.createWriteStream("file.bin"));
} finally {
  clearTimeout(t);
}

Sources:
[1] Axios request config (timeout, responseType: 'stream') https://axios.rest/pages/advanced/request-config
[2] Axios GitHub issue: timeout not respected for chunked/streaming responses #5886 axios/axios#5886
[3] Axios docs: Cancellation (combine timeout + signal) https://axios-http.com/docs/cancellation


Harden test-data downloads to avoid hangs and partial files.

Right now there's no timeout, status validation, and the stream writes directly to the final path. This can hang CI or leave corrupted files if the download fails or multiple workers run in parallel.

The proposed timeout: 30_000 alone is insufficient—in Axios 1.13.4 with responseType: "stream", timeout is only a socket inactivity threshold, not a hard deadline. Long downloads can exceed it while chunks still arrive. Enforce a hard deadline using AbortController with a separate timer instead.

🛠️ Proposed fix (hard timeout + status validation + atomic write)
 const downloadFileIfNeeded = async (url: string, filePath: string): Promise<void> => {
     if (fs.existsSync(filePath)) {
         return;
     }

     fs.mkdirSync(path.dirname(filePath), {recursive: true});

-    const response = await axios.get(url, {responseType: "stream"});
-    await pipeline(response.data, fs.createWriteStream(filePath));
+    const tmpPath = `${filePath}.tmp`;
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(new Error("Download timeout")), 30_000);
+    try {
+        const response = await axios.get(url, {
+            responseType: "stream",
+            validateStatus: (status) => 200 <= status && status < 300,
+            signal: controller.signal,
+        });
+        await pipeline(response.data, fs.createWriteStream(tmpPath));
+        fs.renameSync(tmpPath, filePath);
+    } catch (error) {
+        if (fs.existsSync(tmpPath)) {
+            fs.unlinkSync(tmpPath);
+        }
+        throw error;
+    } finally {
+        clearTimeout(timeout);
+    }
 };
📝 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
const downloadFileIfNeeded = async (url: string, filePath: string): Promise<void> => {
if (fs.existsSync(filePath)) {
return;
}
fs.mkdirSync(path.dirname(filePath), {recursive: true});
const response = await axios.get(url, {responseType: "stream"});
await pipeline(response.data, fs.createWriteStream(filePath));
const downloadFileIfNeeded = async (url: string, filePath: string): Promise<void> => {
if (fs.existsSync(filePath)) {
return;
}
fs.mkdirSync(path.dirname(filePath), {recursive: true});
const tmpPath = `${filePath}.tmp`;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(new Error("Download timeout")), 30_000);
try {
const response = await axios.get(url, {
responseType: "stream",
validateStatus: (status) => 200 <= status && status < 300,
signal: controller.signal,
});
await pipeline(response.data, fs.createWriteStream(tmpPath));
fs.renameSync(tmpPath, filePath);
} catch (error) {
if (fs.existsSync(tmpPath)) {
fs.unlinkSync(tmpPath);
}
throw error;
} finally {
clearTimeout(timeout);
}
};
🤖 Prompt for AI Agents
In `@test/utils.ts` around lines 30 - 38, downloadFileIfNeeded currently performs
an unbounded stream download directly to the final path with no status check or
hard timeout; change it to create the directory, start an AbortController-based
hard-deadline timer (e.g., 30_000ms) and pass its signal into axios.get,
validate response.status (expect 200) before streaming, stream into a temporary
file (e.g., filePath + ".tmp") via pipeline, clear the timer and atomically
rename the temp file to filePath on success, and on any error/abort ensure the
controller is aborted, the temp file is removed and the write stream is closed;
keep references to axios.get, AbortController, pipeline, fs.createWriteStream,
fs.rename and fs.unlink when implementing these steps.

`vitest.config.ts` did not match `vitest.browser.config.ts` or
`vitest.node.config.ts`, so changes to those files would not trigger CI.
Copy link
Copy Markdown

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

🤖 Fix all issues with AI agents
In `@package.json`:
- Line 18: The lint:fix:js script incorrectly forwards --fix to lint:check:js
causing tsc to receive an unknown --fix flag; update package.json so lint:fix:js
does not append --fix to the command that runs tsc. For example, create a
dedicated lint:fix:js script that runs the type check and lint fix separately
(invoke tsc --noEmit or npm run lint:check:types, then run eslint . --fix
--max-warnings 0), or change lint:check:js and lint:fix:js so only the eslint
invocation receives --fix; reference the npm script names lint:fix:js,
lint:check:js and the tsc invocation to locate where to split the commands.
- Line 19: The package.json currently uses the "prepare" lifecycle script to run
"npx playwright install", which triggers heavy browser downloads for every npm
install; remove the "prepare" entry and instead add a developer-only script like
"setup:test" (or "setup:playwright") that runs "npx playwright install", update
README/contributing docs to instruct contributors to run that script before
running browser tests, or conditionally run the install only in CI/dev by
checking an env var if you need automation.

In `@tsconfig.json`:
- Around line 33-37: The tsconfig's "include" array currently lists
"eslint.config.mjs", which is a JavaScript module and has no effect for
TypeScript compilation; either remove "eslint.config.mjs" from the "include"
array in tsconfig.json or rename the ESLint config file to a TypeScript module
(e.g., "eslint.config.ts") if you intend TypeScript to type-check the ESLint
config—update the "include" entry accordingly.
- Around line 16-31: The tsconfig currently sets "strict": true and also
explicitly repeats individual strict flags (e.g., "alwaysStrict",
"strictNullChecks", "strictBindCallApply", "strictFunctionTypes",
"strictPropertyInitialization", "noImplicitAny", "noImplicitThis",
"useUnknownInCatchVariables"); remove the redundant explicit keys to reduce
verbosity by keeping only "strict": true (or alternatively remove "strict" and
keep the explicit list if you prefer self-documenting flags) — update the
tsconfig entries around "strict" and the listed option names accordingly.

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.

3 participants