Skip to content

Implements TestJam, Testing helpers and Testing docs#106

Open
DrEverr wants to merge 7 commits intomainfrom
maso-jam-test
Open

Implements TestJam, Testing helpers and Testing docs#106
DrEverr wants to merge 7 commits intomainfrom
maso-jam-test

Conversation

@DrEverr
Copy link
Member

@DrEverr DrEverr commented Feb 4, 2026

Resolves: #99

Implements testing framework for JAM service development.

Updated typeberry/lib

@coderabbitai
Copy link

coderabbitai bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

This PR introduces comprehensive testing infrastructure for the jammin-sdk, including a new TestJam class for fluent test simulation APIs, a testing-helpers module with assertion utilities, detailed documentation on writing tests, and updates to work-report type definitions and module exports.

Changes

Cohort / File(s) Summary
Documentation
docs/src/testing.md, docs/src/SUMMARY.md
Added comprehensive testing guide documenting TestJam usage, test patterns, troubleshooting, and best practices; updated navigation index to reference testing guide.
Testing Helpers Module
packages/jammin-sdk/testing-helpers.ts, packages/jammin-sdk/testing-helpers.test.ts
New testing-helpers module providing assertion utilities (expectAccumulationSuccess, expectWorkItemCount, expectStateChange, expectServiceInfoChange) and re-exporting simulator and work-report types; includes comprehensive test coverage for all helpers.
Test Simulator Refactor
packages/jammin-sdk/simulator.ts, packages/jammin-sdk/simulator.test.ts
Introduced TestJam class with fluent API (static create/empty, withOptions, withWorkReport, accumulation, state getters) to replace direct simulateAccumulation usage; updated test suite to use new TestJam API; expanded GuaranteeOptions and SimulatorOptions configuration interfaces; refactored credential/guarantee handling with public-facing types.
Public API Updates
packages/jammin-sdk/index.ts
Removed exports of state module and work-report; added export of new testing-helpers module to expose testing utilities at SDK level.
Work Report Type System
packages/jammin-sdk/work-report.ts
Expanded WorkResultStatus union with digestTooBig and incorrectNumberOfExports variants; replaced direct object instantiation with static factory methods (WorkExecResult.ok/error, WorkRefineLoad.create); re-exported WorkReport type for external consumption; updated context construction with lookupAnchor and prerequisites fields.
Module Imports & Dependencies
packages/jammin-sdk/genesis-state-generator.test.ts, packages/jammin-sdk/package.json
Updated test import to use ServiceBuildOutput from utils type import instead of genesis-state-generator; upgraded @typeberry/lib dependency from 0.5.2-9afefa7 to 0.5.4-e1cdb43.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Possibly related PRs

  • Work Report helper jammin-sdk #78: Modifies work-report.ts and packages/jammin-sdk/index.ts exports, overlapping with this PR's work-report type changes and module export adjustments.
  • Accumulate Simulator #86: Introduces/modifies simulator and genesis-state-generator functionality including TestJam and generateGuarantees, directly related to this PR's testing infrastructure additions.
  • Add generateGuarantees for dev/tests #92: Adds/modifies generateGuarantees and GuaranteeOptions type in simulator.ts and exports, overlapping with this PR's simulator refactoring.

Suggested reviewers

  • tomusdrw
  • skoszuta
  • mateuszsikora
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes some changes beyond the core requirements: removed exports (state from typeberry/lib, work-report from index.ts), updated @typeberry/lib dependency version, and modified work-report.ts public API (added WorkResultStatus variants, refactored constructors). Review whether removing state/work-report exports and adding work-report type variants are intentional scope changes or dependencies of the testing framework implementation. Document any breaking API changes separately.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR successfully addresses all requirements from issue #99: TestJam class created with fluent API [simulator.ts], testing-helpers.ts module with expectAccumulationSuccess and expectStateChange functions implemented [testing-helpers.ts], comprehensive testing documentation added [testing.md], and exports configured properly [index.ts].
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title accurately summarizes the main changes: implementing TestJam (a test framework class), testing helpers (assertion utilities), and testing documentation (comprehensive guide).
Description check ✅ Passed The PR description directly relates to the changeset by referencing issue #99 and stating the implementation of a testing framework with updated dependencies.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch maso-jam-test

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

# Conflicts:
#	packages/jammin-sdk/genesis-state-generator.test.ts
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

🤖 Fix all issues with AI agents
In `@docs/src/testing.md`:
- Around line 53-55: The docs reference two different config filenames
("jam.toml" and "jammin.build.yml") which is inconsistent; update all mentions
to use the canonical filename (choose one and apply consistently) — e.g., change
the setup example comment and any troubleshooting text that refers to
"jammin.build.yml" to the canonical name so the example with TestJam.create()
and all other occurrences (including lines around the cited blocks and lines
461-465) consistently reference the same filename.

In `@packages/jammin-sdk/index.ts`:
- Line 11: The public API change removed the top-level re-export "export * as
state from \"@typeberry/lib/state\"" (and replaced work-report exports by
re-exporting them via testing-helpers.js); confirm intent and either restore the
state re-export or document the breaking change: if you intended to keep the SDK
surface, re-add the top-level export (the missing symbol is the namespace export
"state" from "@typeberry/lib/state") so consumers can import sdk.state;
otherwise update the package release notes/CHANGELOG to call out removal of
"state" from the public API and ensure no internal imports rely on that export.
🧹 Nitpick comments (5)
packages/jammin-sdk/simulator.ts (2)

298-301: Consider merging options rather than replacing them.

The withOptions() method replaces all options. If users want to change only one option (e.g., just debug), they lose any previously set options. Consider merging instead:

♻️ Proposed fix to merge options
  withOptions(options: SimulatorOptions): this {
-   this.options = options;
+   this.options = { ...this.options, ...options };
    return this;
  }

343-358: Side effect: chainSpec is mutated on this.options.

When this.options.chainSpec is undefined, Line 353-354 mutates this.options by setting chainSpec = tinyChainSpec. This persists across future accumulation() calls, which may be unexpected since options are supposed to be explicitly set via withOptions().

Consider using a local variable instead:

♻️ Proposed fix to avoid mutating options
  async accumulation(): Promise<AccumulateResult> {
    const result = await simulateAccumulation(this.state, this.workReports, this.options);
    this.workReports = [];
    if (this.state instanceof InMemoryState) {
      this.state.applyUpdate(result);
    } else {
      if (!this.blake2b) {
        this.blake2b = await Blake2b.createHasher();
      }
-     if (!this.options.chainSpec) {
-       this.options.chainSpec = tinyChainSpec;
-     }
-     this.state.backend.applyUpdate(serializeStateUpdate(this.options.chainSpec, this.blake2b, result));
+     const chainSpec = this.options.chainSpec ?? tinyChainSpec;
+     this.state.backend.applyUpdate(serializeStateUpdate(chainSpec, this.blake2b, result));
    }
    return result;
  }
packages/jammin-sdk/testing-helpers.test.ts (2)

17-22: Consider using beforeEach to isolate test state.

The jam instance is created once in beforeAll and reused across all tests. Since accumulation() applies state updates to jam.state, later tests may be affected by state changes from earlier tests, potentially causing test interdependencies.

♻️ Proposed fix to isolate tests
  describe("testing-helpers", () => {
    let jam: TestJam;

-   beforeAll(() => {
+   beforeEach(() => {
      jam = TestJam.empty();
    });

96-103: Test may silently pass if no exception is thrown.

The test at Lines 96-103 uses try/catch to verify the error message, but if expectStateChange doesn't throw, the test passes without any assertions executing.

♻️ Proposed fix to ensure exception is thrown
    test("should include custom error message", () => {
+     let caught = false;
      try {
        expectStateChange(10, 5, (before, after) => after > before, "Custom error");
      } catch (e) {
+       caught = true;
        expect(e).toBeInstanceOf(StateChangeAssertionError);
        expect((e as Error).message).toBe("Custom error");
      }
+     expect(caught).toBe(true);
    });

Or use expect().toThrow() pattern consistently:

    test("should include custom error message", () => {
-     try {
-       expectStateChange(10, 5, (before, after) => after > before, "Custom error");
-     } catch (e) {
-       expect(e).toBeInstanceOf(StateChangeAssertionError);
-       expect((e as Error).message).toBe("Custom error");
-     }
+     expect(() => {
+       expectStateChange(10, 5, (before, after) => after > before, "Custom error");
+     }).toThrow("Custom error");
    });
packages/jammin-sdk/simulator.test.ts (1)

1-12: Isolate TestJam per test to avoid state leakage.

accumulation() mutates state and withOptions() can persist; reusing a single instance via beforeAll can make tests order-dependent. Prefer beforeEach (or instantiating inside each test) for isolation.

♻️ Suggested change
-import { beforeAll, describe, expect, test } from "bun:test";
+import { beforeEach, describe, expect, test } from "bun:test";
 import * as config from "@typeberry/lib/config";
 import { generateGuarantees, TestJam } from "./simulator.js";
 import { CoreId, Gas, ServiceId, Slot } from "./types.js";
 import { createWorkReportAsync } from "./work-report.js";

 describe("simulateAccumulation", () => {
   let jam: TestJam;

-  beforeAll(() => {
+  beforeEach(() => {
     jam = TestJam.empty();
   });

@DrEverr DrEverr requested review from skoszuta and tomusdrw February 4, 2026 18:41
@DrEverr DrEverr changed the title Implementa TestJam, Testing helpers and Testing docs Implements TestJam, Testing helpers and Testing docs Feb 4, 2026

1. Your `jammin.build.yml` file is properly configured
2. You're using `TestJam.create()` (not `TestJam.empty()`)
3. The service ID in your test matches the service ID in your configuration
Copy link
Contributor

@skoszuta skoszuta Feb 5, 2026

Choose a reason for hiding this comment

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

it looks like the user should also make sure the services are built

Copy link
Member

Choose a reason for hiding this comment

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

as suggested in other comment I'd be in favour of having build command produce a typescript file with service details (like typesafe name->id mapping)

const result = await jam.withWorkReport(report).accumulation();

// Verify results
expect(result).toBeDefined();
Copy link
Contributor

Choose a reason for hiding this comment

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

it would be nice if this first example showed the user one of our custom assertions


// Create a work report
const report = await createWorkReportAsync({
results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }],
Copy link
Member

Choose a reason for hiding this comment

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

build command should generate a typescript file with details about all the genesis state (we can even have it loaded there by default). This way user will have a type-safe API with the service ids and we will not need to rely on hardcoded values.

- `pvmBackend?: PvmBackend` - PVM backend to use (`PvmBackend.Ananas` or `PvmBackend.BuiltIn`)
- `sequential?: boolean` - Use sequential accumulation mode (default: `true`)
- `entropy?: EntropyHash` - Entropy for randomness (defaults to zero hash for deterministic tests)
- `chainSpec?: ChainSpec` - Chain specification to use
Copy link
Member

Choose a reason for hiding this comment

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

chain spec should be defined top-level. maybe instead of having a static TestJam class we could rather have a typescript file with an instance created by build that would contain the chainspec already?

```typescript
const info = jam.getServiceInfo(ServiceId(0));
console.log(`Balance: ${info?.balance}`);
console.log(`Gas used: ${info?.gasUsed}`);
Copy link
Member

Choose a reason for hiding this comment

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

what's 'gas used' in service info? Are you sure it's actually there?


1. Your `jammin.build.yml` file is properly configured
2. You're using `TestJam.create()` (not `TestJam.empty()`)
3. The service ID in your test matches the service ID in your configuration
Copy link
Member

Choose a reason for hiding this comment

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

as suggested in other comment I'd be in favour of having build command produce a typescript file with service details (like typesafe name->id mapping)


### Import Errors

Make sure you're importing from the correct module:
Copy link
Member

Choose a reason for hiding this comment

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

what's "incorrect" then?


### Debug Logging

Enable debug logging to see what's happening during accumulation:
Copy link
Member

Choose a reason for hiding this comment

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

can we have that enabled by default? What kind of logs does it display? I think it would be good to have more fine-grained control over debug logs to be able to see ecalli-trace logging, but also pvm-level logging if needed.

```typescript
import { generateState, loadServices } from "@fluffylabs/jammin-sdk";

const services = await loadServices();
Copy link
Member

Choose a reason for hiding this comment

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

imho this should rather be in the auto-generated typescript file that is created by build.

* console.log(`Processed ${result.accumulationStatistics.size} work items`);
* ```
*/
async accumulation(): Promise<AccumulateResult> {
Copy link
Member

Choose a reason for hiding this comment

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

My preference would be to call it using a verb (i.e. accumulate), but I'm not strongly opinionated on this.

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.

Create testing helpers and documentation

3 participants