From 6d05398293e65320dffee13987a34a3d7b94182e Mon Sep 17 00:00:00 2001 From: HeyImStas Date: Wed, 4 Feb 2026 18:11:05 +0100 Subject: [PATCH 01/18] implement TestJam on newer typeberry/lib --- bun.lock | 8 +- packages/jammin-sdk/package.json | 2 +- packages/jammin-sdk/simulator.test.ts | 39 +++++----- packages/jammin-sdk/simulator.ts | 108 +++++++++++++++++++++++--- packages/jammin-sdk/work-report.ts | 71 +++++++++-------- 5 files changed, 163 insertions(+), 65 deletions(-) diff --git a/bun.lock b/bun.lock index 360cc24..d94eb66 100644 --- a/bun.lock +++ b/bun.lock @@ -38,7 +38,7 @@ "name": "@fluffylabs/jammin-sdk", "version": "0.1.0", "dependencies": { - "@typeberry/lib": "0.5.2-9afefa7", + "@typeberry/lib": "0.5.4-e1cdb43", "yaml": "^2.8.2", "zod": "^4.3.6", }, @@ -84,7 +84,7 @@ "@tsconfig/node20": ["@tsconfig/node20@20.1.8", "", {}, "sha512-Em+IdPfByIzWRRpqWL4Z7ArLHZGxmc36BxE3jCz9nBFSm+5aLaPMZyjwu4yetvyKXeogWcxik4L1jB5JTWfw7A=="], - "@typeberry/lib": ["@typeberry/lib@0.5.2-9afefa7", "", { "dependencies": { "@fluffylabs/anan-as": "^1.1.5", "@noble/ed25519": "2.2.3", "@typeberry/native": "0.0.4-4c0cd28", "hash-wasm": "4.12.0" } }, "sha512-8RgtMK29flVgYRF8FEoTuwc249NLajBuQ7/fc+24bc8gNDkJH39mJiPz5TQKPcOPNQ6MSKnayMNO6u+i125RJA=="], + "@typeberry/lib": ["@typeberry/lib@0.5.4-e1cdb43", "", { "dependencies": { "@fluffylabs/anan-as": "^1.1.5", "@noble/ed25519": "2.2.3", "@typeberry/native": "0.0.4-4c0cd28", "hash-wasm": "4.12.0" } }, "sha512-gyUWD2j/SOFMnOTmfxIdlbRFdO5guwQrWB/2jBBf8rVu+Lys4M0jfqmralL16EIzbiapfwMbE+yrHfs8040ZkA=="], "@typeberry/native": ["@typeberry/native@0.0.4-4c0cd28", "", {}, "sha512-VhZiiSYex3/jDk1I8PlcwPxiM9GslryGxdG+4sbbjNvpr1JxRkH0fAdUnKzxAxGYPNz2MOfwHTmTdVcrk1a5rA=="], @@ -113,5 +113,9 @@ "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "@fluffylabs/jammin-sdk/@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + + "@fluffylabs/jammin-sdk/@types/bun/bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], } } diff --git a/packages/jammin-sdk/package.json b/packages/jammin-sdk/package.json index a3bc944..3c774b1 100644 --- a/packages/jammin-sdk/package.json +++ b/packages/jammin-sdk/package.json @@ -30,7 +30,7 @@ "type": "module", "types": "./dist/index.d.ts", "dependencies": { - "@typeberry/lib": "0.5.2-9afefa7", + "@typeberry/lib": "0.5.4-e1cdb43", "yaml": "^2.8.2", "zod": "^4.3.6" } diff --git a/packages/jammin-sdk/simulator.test.ts b/packages/jammin-sdk/simulator.test.ts index 687a410..58beb72 100644 --- a/packages/jammin-sdk/simulator.test.ts +++ b/packages/jammin-sdk/simulator.test.ts @@ -1,16 +1,14 @@ import { beforeAll, describe, expect, test } from "bun:test"; import * as config from "@typeberry/lib/config"; -import type * as state from "@typeberry/lib/state"; -import { generateState } from "./genesis-state-generator.js"; -import { generateGuarantees, simulateAccumulation } from "./simulator.js"; +import { generateGuarantees, TestJam } from "./simulator.js"; import { CoreId, Gas, ServiceId, Slot } from "./types.js"; import { createWorkReportAsync } from "./work-report.js"; describe("simulateAccumulation", () => { - let initialState: state.InMemoryState; + let jam: TestJam; - beforeAll(async () => { - initialState = generateState([]); + beforeAll(() => { + jam = TestJam.empty(); }); test("simulates accumulation with minimal configuration", async () => { @@ -18,12 +16,11 @@ describe("simulateAccumulation", () => { results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], }); - const result = await simulateAccumulation(initialState, [report]); + const result = await jam.withWorkReport(report).accumulation(); expect(result).toBeDefined(); expect(result.stateUpdate).toBeDefined(); expect(result.accumulationStatistics).toBeDefined(); - expect(result.pendingTransfers).toBeDefined(); expect(result.accumulationOutputLog).toBeDefined(); expect(result.accumulationStatistics.size).toBe(1); }); @@ -36,7 +33,7 @@ describe("simulateAccumulation", () => { results: [{ serviceId: ServiceId(1), gas: Gas(750n) }], }); - const result = await simulateAccumulation(initialState, [report1, report2]); + const result = await jam.withWorkReport(report1).withWorkReport(report2).accumulation(); expect(result.accumulationStatistics).toBeDefined(); expect(result.accumulationStatistics.size).toBe(2); @@ -51,16 +48,14 @@ describe("simulateAccumulation", () => { ], }); - const result = await simulateAccumulation(initialState, [report], { - chainSpec: config.tinyChainSpec, - }); + const result = await jam.withWorkReport(report).accumulation(); expect(result).toBeDefined(); expect(result.accumulationStatistics.size).toBe(3); }); test("handles empty reports array", async () => { - const result = await simulateAccumulation(initialState, []); + const result = await jam.accumulation(); expect(result).toBeDefined(); expect(result.stateUpdate).toBeDefined(); @@ -73,9 +68,12 @@ describe("simulateAccumulation", () => { }); const customSlot = Slot(100); - const result = await simulateAccumulation(initialState, [report], { - slot: customSlot, - }); + const result = await jam + .withWorkReport(report) + .withOptions({ + slot: customSlot, + }) + .accumulation(); expect(result.stateUpdate.timeslot).toBeDefined(); // The timeslot in the update should be >= the slot we passed @@ -87,9 +85,12 @@ describe("simulateAccumulation", () => { results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], }); - const result = await simulateAccumulation(initialState, [report], { - pvmBackend: config.PvmBackend.BuiltIn, - }); + const result = await jam + .withWorkReport(report) + .withOptions({ + pvmBackend: config.PvmBackend.BuiltIn, + }) + .accumulation(); expect(result).toBeDefined(); }); diff --git a/packages/jammin-sdk/simulator.ts b/packages/jammin-sdk/simulator.ts index 5a9d9f3..a7366b6 100644 --- a/packages/jammin-sdk/simulator.ts +++ b/packages/jammin-sdk/simulator.ts @@ -1,19 +1,24 @@ -import type { EntropyHash, TimeSlot } from "@typeberry/lib/block"; import * as jamBlock from "@typeberry/lib/block"; +import { Credential, type EntropyHash, ReportGuarantee, type TimeSlot } from "@typeberry/lib/block"; +import { Bytes, type BytesBlob } from "@typeberry/lib/bytes"; import { Encoder } from "@typeberry/lib/codec"; import { asKnownSize } from "@typeberry/lib/collections"; import { type ChainSpec, PvmBackend, tinyChainSpec } from "@typeberry/lib/config"; import { ed25519, keyDerivation } from "@typeberry/lib/crypto"; -import { Blake2b, ZERO_HASH } from "@typeberry/lib/hash"; +import { Blake2b, type OpaqueHash, ZERO_HASH } from "@typeberry/lib/hash"; import * as jamNumbers from "@typeberry/lib/numbers"; -import type { State } from "@typeberry/lib/state"; +import { InMemoryState, type LookupHistorySlots, type ServiceAccountInfo, type State } from "@typeberry/lib/state"; +import { type SerializedState, type StateEntries, serializeStateUpdate } from "@typeberry/lib/state-merkleization"; import { Accumulate, type AccumulateInput, type AccumulateResult, type AccumulateState, } from "@typeberry/lib/transition"; +import { asOpaqueType } from "@typeberry/lib/utils"; +import { generateState } from "./genesis-state-generator.js"; import { Slot } from "./types.js"; +import { loadServices } from "./utils/generate-service-output.js"; import type { WorkReport } from "./work-report.js"; // Re-export types for convenience @@ -88,7 +93,7 @@ async function signWorkReport( keyPair: Awaited, blake2b: Blake2b, ): Promise { - const reportBlob = Encoder.encodeObject(jamBlock.workReport.WorkReport.Codec, workReport); + const reportBlob = Encoder.encodeObject(jamBlock.WorkReport.Codec, workReport); const reportHash = blake2b.hashBytes(reportBlob); @@ -116,17 +121,17 @@ async function signWorkReport( export async function generateGuarantees( workReports: WorkReport[], options: GuaranteeOptions = {}, -): Promise { +): Promise { const slot = options.slot ?? Slot(0); const credentialCount = 3; const startValidatorIndex = 0; const blake2b = await Blake2b.createHasher(); - const guarantees: jamBlock.guarantees.ReportGuarantee[] = []; + const guarantees: ReportGuarantee[] = []; for (const workReport of workReports) { - const credentials: jamBlock.guarantees.Credential[] = []; + const credentials: Credential[] = []; for (let i = 0; i < credentialCount; i++) { const validatorIndexNum = startValidatorIndex + i; @@ -135,7 +140,7 @@ export async function generateGuarantees( const signature = await signWorkReport(workReport, keyPair, blake2b); credentials.push( - jamBlock.guarantees.Credential.create({ + Credential.create({ validatorIndex, signature, }), @@ -143,7 +148,7 @@ export async function generateGuarantees( } guarantees.push( - jamBlock.guarantees.ReportGuarantee.create({ + ReportGuarantee.create({ report: workReport, slot, credentials: asKnownSize(credentials), @@ -205,3 +210,88 @@ async function enableLogs() { console.warn("Warning: Could not configure typeberry logger"); } } + +/** + * Test helper class for simulating JAM accumulation in Bun tests. + * Automatically generates state on construction and provides a fluent API + * for injecting work reports and running accumulation. + * + * @example + * ```typescript + * const jam = await TestJam.create(); + * const report = await createWorkReportAsync({ + * results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], + * }); + * const result = await jam.withWorkReport(report).accumulation(); + * ``` + */ +export class TestJam { + public readonly state: InMemoryState | SerializedState; + private workReports: WorkReport[] = []; + private options: SimulatorOptions = {}; + private blake2b?: Blake2b; + + private constructor(state: InMemoryState | SerializedState) { + this.state = state; + } + + static async create(): Promise { + const state = generateState(await loadServices()); + return new TestJam(state); + } + + static empty(): TestJam { + const state = generateState([]); + return new TestJam(state); + } + + withOptions(options: SimulatorOptions): this { + this.options = options; + return this; + } + + /** Inject a work report to be used in the next accumulation */ + withWorkReport(report: WorkReport): this { + this.workReports.push(report); + return this; + } + + /** Run accumulation with the injected work reports, then clear them */ + async accumulation(): Promise { + 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)); + } + return result; + } + + getServiceInfo(id: jamBlock.ServiceId): ServiceAccountInfo | undefined { + return this.state.getService(id)?.getInfo(); + } + + getServiceStorage(id: jamBlock.ServiceId, key: BytesBlob): BytesBlob | undefined | null { + return this.state.getService(id)?.getStorage(asOpaqueType(key)); + } + + getServicePreimage(id: jamBlock.ServiceId, hash: OpaqueHash): BytesBlob | undefined | null { + Bytes.parseBytes("", 32).asOpaque(); + return this.state.getService(id)?.getPreimage(hash.asOpaque()); + } + + getServicePreimageLookup( + id: jamBlock.ServiceId, + hash: OpaqueHash, + len: jamNumbers.U32, + ): LookupHistorySlots | undefined | null { + return this.state.getService(id)?.getLookupHistory(hash.asOpaque(), len); + } +} diff --git a/packages/jammin-sdk/work-report.ts b/packages/jammin-sdk/work-report.ts index 701c36e..21e46c9 100644 --- a/packages/jammin-sdk/work-report.ts +++ b/packages/jammin-sdk/work-report.ts @@ -1,36 +1,36 @@ import { type CoreIndex, - refineContext, + MAX_NUMBER_OF_WORK_ITEMS, + MIN_NUMBER_OF_WORK_ITEMS, + RefineContext, type ServiceGas, type ServiceId, - workPackage, - workReport, - workResult, + WorkExecResult, + WorkExecResultKind, + type WorkPackageInfo, + WorkPackageSpec, + WorkRefineLoad, + WorkReport, + WorkResult, } from "@typeberry/lib/block"; + +// Re-export WorkReport type for use in other modules +export type { WorkReport }; + import { BytesBlob } from "@typeberry/lib/bytes"; import type { CodecRecord } from "@typeberry/lib/codec"; import { FixedSizeArray } from "@typeberry/lib/collections"; import { type Blake2b, type Blake2bHash, ZERO_HASH } from "@typeberry/lib/hash"; import { CoreId, Gas, Slot, U8, U16, U32 } from "./types.js"; -type RefineContext = ReturnType; -type WorkPackageInfo = ReturnType; -type WorkPackageSpec = ReturnType; -type WorkReport = ReturnType; -type WorkRefineLoad = ReturnType; -type WorkResult = ReturnType; - -// Re-export types for convenience -export type { RefineContext, WorkPackageInfo, WorkPackageSpec, WorkReport, WorkRefineLoad, WorkResult }; - -const { WorkExecResult, WorkExecResultKind } = workResult; - /** Work result status types */ export type WorkResultStatus = | { type: "ok"; output?: BytesBlob } | { type: "panic" } | { type: "outOfGas" } | { type: "badCode" } + | { type: "digestTooBig" } + | { type: "incorrectNumberOfExports" } | { type: "codeOversize" }; /** Configuration for a single work result with optional fields */ @@ -91,38 +91,41 @@ export function createWorkResult(blake2b: Blake2b, config: WorkResultConfig): Wo const payloadHash = blake2b.hashBytes(payloadBlob); const resultStatus = config.result ?? { type: "ok" }; - let execResult: InstanceType; + let execResult: WorkExecResult; switch (resultStatus.type) { case "ok": - execResult = new WorkExecResult( - WorkExecResultKind.ok, - resultStatus.output ?? BytesBlob.blobFrom(new Uint8Array()), - ); + execResult = WorkExecResult.ok(resultStatus.output ?? BytesBlob.blobFrom(new Uint8Array())); break; case "panic": - execResult = new WorkExecResult(WorkExecResultKind.panic); + execResult = WorkExecResult.error(WorkExecResultKind.panic); break; case "outOfGas": - execResult = new WorkExecResult(WorkExecResultKind.outOfGas); + execResult = WorkExecResult.error(WorkExecResultKind.outOfGas); break; case "badCode": - execResult = new WorkExecResult(WorkExecResultKind.badCode); + execResult = WorkExecResult.error(WorkExecResultKind.badCode); + break; + case "digestTooBig": + execResult = WorkExecResult.error(WorkExecResultKind.digestTooBig); + break; + case "incorrectNumberOfExports": + execResult = WorkExecResult.error(WorkExecResultKind.incorrectNumberOfExports); break; case "codeOversize": - execResult = new WorkExecResult(WorkExecResultKind.codeOversize); + execResult = WorkExecResult.error(WorkExecResultKind.codeOversize); break; } const load = config.load ?? {}; - return workResult.WorkResult.create({ + return WorkResult.create({ serviceId: config.serviceId, codeHash: (config.codeHash ?? ZERO_HASH).asOpaque(), payloadHash, gas: config.gas ?? Gas(0n), result: execResult, - load: workResult.WorkRefineLoad.create({ + load: WorkRefineLoad.create({ gasUsed: load.gasUsed ?? Gas(0n), importedSegments: load.importedSegments ?? U32(0), exportedSegments: load.exportedSegments ?? U32(0), @@ -155,12 +158,12 @@ export function createWorkResult(blake2b: Blake2b, config: WorkResultConfig): Wo * ``` */ export function createWorkReport(blake2b: Blake2b, config: WorkReportConfig): WorkReport { - if (config.results.length < workPackage.MIN_NUMBER_OF_WORK_ITEMS) { - throw new Error(`WorkReport cannot contain less than ${workPackage.MIN_NUMBER_OF_WORK_ITEMS} results`); + if (config.results.length < MIN_NUMBER_OF_WORK_ITEMS) { + throw new Error(`WorkReport cannot contain less than ${MIN_NUMBER_OF_WORK_ITEMS} results`); } - if (config.results.length > workPackage.MAX_NUMBER_OF_WORK_ITEMS) { - throw new Error(`WorkReport cannot contain more than ${workPackage.MAX_NUMBER_OF_WORK_ITEMS} results`); + if (config.results.length > MAX_NUMBER_OF_WORK_ITEMS) { + throw new Error(`WorkReport cannot contain more than ${MAX_NUMBER_OF_WORK_ITEMS} results`); } const results = config.results.map((resultConfig) => createWorkResult(blake2b, resultConfig)); @@ -168,15 +171,15 @@ export function createWorkReport(blake2b: Blake2b, config: WorkReportConfig): Wo const wpSpec = config.workPackageSpec ?? {}; const ctx = config.context ?? {}; - return workReport.WorkReport.create({ - workPackageSpec: workReport.WorkPackageSpec.create({ + return WorkReport.create({ + workPackageSpec: WorkPackageSpec.create({ hash: wpSpec.hash ?? ZERO_HASH.asOpaque(), length: wpSpec.length ?? U32(0), erasureRoot: wpSpec.erasureRoot ?? ZERO_HASH.asOpaque(), exportsRoot: wpSpec.exportsRoot ?? ZERO_HASH.asOpaque(), exportsCount: wpSpec.exportsCount ?? U16(0), }), - context: refineContext.RefineContext.create({ + context: RefineContext.create({ anchor: ctx.anchor ?? ZERO_HASH.asOpaque(), stateRoot: ctx.stateRoot ?? ZERO_HASH.asOpaque(), beefyRoot: ctx.beefyRoot ?? ZERO_HASH.asOpaque(), From 30031b63b9be3f684b28e0634286f3bd8bce26a0 Mon Sep 17 00:00:00 2001 From: HeyImStas Date: Wed, 4 Feb 2026 19:24:16 +0100 Subject: [PATCH 02/18] TestJam, Testing helpers and docs --- docs/src/SUMMARY.md | 1 + docs/src/testing.md | 606 ++++++++++++++++++ .../genesis-state-generator.test.ts | 3 +- packages/jammin-sdk/index.ts | 4 +- packages/jammin-sdk/simulator.ts | 140 +++- packages/jammin-sdk/testing-helpers.test.ts | 136 ++++ packages/jammin-sdk/testing-helpers.ts | 196 ++++++ 7 files changed, 1078 insertions(+), 8 deletions(-) create mode 100644 docs/src/testing.md create mode 100644 packages/jammin-sdk/testing-helpers.test.ts create mode 100644 packages/jammin-sdk/testing-helpers.ts diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index afa6ce1..5b4f827 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -4,6 +4,7 @@ - [Requirements](requirements.md) - [Getting Started](getting-started.md) - [Service SDK examples](service-examples.md) +- [Testing JAM Services](testing.md) - [Contributing](contributing.md) - [Bootstrap](bootstrap/jammin-suite.md) - [Proposed API](bootstrap/jammin-examples.md) diff --git a/docs/src/testing.md b/docs/src/testing.md new file mode 100644 index 0000000..204fd1c --- /dev/null +++ b/docs/src/testing.md @@ -0,0 +1,606 @@ +# Testing JAM Services + +This guide covers how to write integration tests for JAM services using the jammin SDK testing utilities. + +## Overview + +The jammin SDK provides a comprehensive testing framework for simulating JAM accumulation in your test environment. The core of this framework is the `TestJam` class, which provides a fluent API for creating work reports, running accumulation, and inspecting state changes. + +## Setup + +### Installation + +The testing utilities are included in the `@fluffylabs/jammin-sdk` package: + +```bash +bun add -d @fluffylabs/jammin-sdk +``` + +### Basic Test Structure + +Here's a minimal test using Bun's built-in test runner: + +```typescript +import { test, expect } from "bun:test"; +import { TestJam, createWorkReportAsync, ServiceId, Gas } from "@fluffylabs/jammin-sdk"; + +test("should process work report", async () => { + // Create test instance with loaded services + const jam = await TestJam.create(); + + // Create a work report + const report = await createWorkReportAsync({ + results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], + }); + + // Execute accumulation + const result = await jam.withWorkReport(report).accumulation(); + + // Verify results + expect(result).toBeDefined(); + expect(result.accumulationStatistics.size).toBe(1); +}); +``` + +## TestJam Class + +The `TestJam` class is the main entry point for testing JAM services. It manages state, work reports, and accumulation execution. + +### Creating a TestJam Instance + +#### With Loaded Services + +```typescript +// Loads all services from your project's jam.toml files +const jam = await TestJam.create(); +``` + +This is the recommended approach for most tests as it automatically discovers and loads your service configurations. + +#### With Empty State + +```typescript +// Creates an empty state with no services +const jam = TestJam.empty(); +``` + +Useful for testing edge cases or when you don't need any services. + +### Adding Work Reports + +Work reports can be added using the fluent `withWorkReport()` API: + +```typescript +const report = await createWorkReportAsync({ + results: [ + { serviceId: ServiceId(0), gas: Gas(1000n) }, + { serviceId: ServiceId(1), gas: Gas(2000n) }, + ], +}); + +const result = await jam.withWorkReport(report).accumulation(); +``` + +#### Multiple Work Reports + +Chain multiple `withWorkReport()` calls to process multiple reports in a single accumulation: + +```typescript +const report1 = await createWorkReportAsync({ + results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], +}); + +const report2 = await createWorkReportAsync({ + results: [{ serviceId: ServiceId(1), gas: Gas(2000n) }], +}); + +const result = await jam + .withWorkReport(report1) + .withWorkReport(report2) + .accumulation(); + +console.log(`Processed ${result.accumulationStatistics.size} work items`); +``` + +### Configuring Accumulation Options + +Use `withOptions()` to configure accumulation behavior: + +```typescript +import { PvmBackend } from "@fluffylabs/jammin-sdk/config"; + +const result = await jam + .withWorkReport(report) + .withOptions({ + slot: Slot(100), // Custom time slot + debug: true, // Enable debug logging + pvmBackend: PvmBackend.BuiltIn, // Use built-in PVM + sequential: true, // Sequential accumulation (default) + }) + .accumulation(); +``` + +#### Available Options + +- `slot?: TimeSlot` - Time slot for accumulation (defaults to state's current timeslot) +- `debug?: boolean` - Enable debug logging for accumulation and PVM host calls +- `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 + +### Querying State + +After accumulation, you can query the service state: + +#### Get Service Info + +```typescript +const info = jam.getServiceInfo(ServiceId(0)); +console.log(`Balance: ${info?.balance}`); +console.log(`Gas used: ${info?.gasUsed}`); +``` + +#### Get Service Storage + +```typescript +import { BytesBlob } from "@fluffylabs/jammin-sdk/bytes"; + +const key = BytesBlob.blobFrom(new Uint8Array([1, 2, 3])); +const value = jam.getServiceStorage(ServiceId(0), key); +``` + +#### Get Preimage Data + +```typescript +const preimage = jam.getServicePreimage(ServiceId(0), someHash); +``` + +## Creating Work Reports + +The SDK provides flexible utilities for creating work reports with varying levels of detail. + +### Simple Work Report + +```typescript +import { createWorkReportAsync, ServiceId, Gas } from "@fluffylabs/jammin-sdk"; + +const report = await createWorkReportAsync({ + results: [ + { serviceId: ServiceId(0), gas: Gas(1000n) }, + ], +}); +``` + +### Work Report with Multiple Work Items + +```typescript +const report = await createWorkReportAsync({ + results: [ + { + serviceId: ServiceId(0), + gas: Gas(1000n), + result: { type: "ok", output: BytesBlob.blobFrom(new Uint8Array([1, 2, 3])) } + }, + { + serviceId: ServiceId(1), + gas: Gas(2000n), + result: { type: "ok" } + }, + { + serviceId: ServiceId(2), + gas: Gas(500n), + result: { type: "panic" } // Simulate a panic + }, + ], +}); +``` + +### Work Report with Custom Configuration + +```typescript +import { CoreId, Slot } from "@fluffylabs/jammin-sdk"; + +const report = await createWorkReportAsync({ + coreIndex: CoreId(5), + results: [ + { + serviceId: ServiceId(0), + gas: Gas(1000n), + payload: BytesBlob.blobFrom(new Uint8Array([4, 5, 6])), + }, + ], + context: { + lookupAnchorSlot: Slot(42), + }, +}); +``` + +### Work Result Types + +Work results can have different status types: + +```typescript +// Successful execution +{ type: "ok", output: BytesBlob.blobFrom(...) } + +// Execution errors +{ type: "panic" } +{ type: "outOfGas" } +{ type: "badCode" } +{ type: "digestTooBig" } +{ type: "incorrectNumberOfExports" } +{ type: "codeOversize" } +``` + +## Testing Helpers + +The SDK provides assertion helpers to simplify test writing. + +### expectAccumulationSuccess + +Assert that accumulation completed with a valid structure: + +```typescript +import { expectAccumulationSuccess } from "@fluffylabs/jammin-sdk/testing-helpers"; + +const result = await jam.withWorkReport(report).accumulation(); +expectAccumulationSuccess(result); // Throws if result is invalid +``` + +### expectWorkItemCount + +Assert that the expected number of work items were processed: + +```typescript +import { expectWorkItemCount } from "@fluffylabs/jammin-sdk/testing-helpers"; + +const result = await jam + .withWorkReport(report1) + .withWorkReport(report2) + .accumulation(); + +expectWorkItemCount(result, 2); // Throws if count doesn't match +``` + +### expectStateChange + +Assert that state changed according to a predicate: + +```typescript +import { expectStateChange } from "@fluffylabs/jammin-sdk/testing-helpers"; + +const beforeBalance = jam.getServiceInfo(ServiceId(0))?.balance; + +await jam.withWorkReport(report).accumulation(); + +const afterBalance = jam.getServiceInfo(ServiceId(0))?.balance; + +expectStateChange( + beforeBalance, + afterBalance, + (before, after) => after !== undefined && before !== undefined && after > before, + "Balance should increase" +); +``` + +### expectServiceInfoChange + +Specialized helper for validating service account changes: + +```typescript +import { expectServiceInfoChange } from "@fluffylabs/jammin-sdk/testing-helpers"; + +const before = jam.getServiceInfo(ServiceId(0)); +await jam.withWorkReport(report).accumulation(); +const after = jam.getServiceInfo(ServiceId(0)); + +expectServiceInfoChange( + before, + after, + (b, a) => a && b && a.gasUsed > b.gasUsed, + "Service should consume gas" +); +``` + +## Common Test Patterns + +### Testing Service Execution + +```typescript +import { test, expect } from "bun:test"; +import { TestJam, createWorkReportAsync, ServiceId, Gas } from "@fluffylabs/jammin-sdk"; +import { expectAccumulationSuccess } from "@fluffylabs/jammin-sdk/testing-helpers"; + +test("service should execute successfully", async () => { + const jam = await TestJam.create(); + + const report = await createWorkReportAsync({ + results: [{ serviceId: ServiceId(0), gas: Gas(100000n) }], + }); + + const result = await jam.withWorkReport(report).accumulation(); + + expectAccumulationSuccess(result); + expect(result.accumulationStatistics.size).toBe(1); +}); +``` + +### Testing State Changes + +```typescript +import { test, expect } from "bun:test"; +import { TestJam, createWorkReportAsync, ServiceId, Gas, BytesBlob } from "@fluffylabs/jammin-sdk"; + +test("service storage should update", async () => { + const jam = await TestJam.create(); + + const storageKey = BytesBlob.blobFromString("myKey"); + const beforeValue = jam.getServiceStorage(ServiceId(0), storageKey); + + const report = await createWorkReportAsync({ + results: [{ serviceId: ServiceId(0), gas: Gas(50000n) }], + }); + + await jam.withWorkReport(report).accumulation(); + + const afterValue = jam.getServiceStorage(ServiceId(0), storageKey); + expect(afterValue).not.toEqual(beforeValue); +}); +``` + +### Testing Multiple Services + +```typescript +import { test, expect } from "bun:test"; +import { TestJam, createWorkReportAsync, ServiceId, Gas } from "@fluffylabs/jammin-sdk"; + +test("should process multiple services", async () => { + const jam = await TestJam.create(); + + const report = await createWorkReportAsync({ + results: [ + { serviceId: ServiceId(0), gas: Gas(10000n) }, + { serviceId: ServiceId(1), gas: Gas(20000n) }, + { serviceId: ServiceId(2), gas: Gas(15000n) }, + ], + }); + + const result = await jam.withWorkReport(report).accumulation(); + + expect(result.accumulationStatistics.size).toBe(3); +}); +``` + +### Testing Error Conditions + +```typescript +import { test, expect } from "bun:test"; +import { TestJam, createWorkReportAsync, ServiceId, Gas } from "@fluffylabs/jammin-sdk"; + +test("should handle panic gracefully", async () => { + const jam = await TestJam.create(); + + const report = await createWorkReportAsync({ + results: [ + { + serviceId: ServiceId(0), + gas: Gas(1000n), + result: { type: "panic" } + }, + ], + }); + + const result = await jam.withWorkReport(report).accumulation(); + + // Accumulation should complete even with panicked work items + expect(result).toBeDefined(); + expect(result.accumulationStatistics.size).toBe(1); +}); +``` + +### Testing Sequential Accumulations + +```typescript +import { test, expect } from "bun:test"; +import { TestJam, createWorkReportAsync, ServiceId, Gas } from "@fluffylabs/jammin-sdk"; + +test("state should persist across accumulations", async () => { + const jam = await TestJam.create(); + + // First accumulation + const report1 = await createWorkReportAsync({ + results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], + }); + await jam.withWorkReport(report1).accumulation(); + + const midInfo = jam.getServiceInfo(ServiceId(0)); + + // Second accumulation + const report2 = await createWorkReportAsync({ + results: [{ serviceId: ServiceId(0), gas: Gas(2000n) }], + }); + await jam.withWorkReport(report2).accumulation(); + + const finalInfo = jam.getServiceInfo(ServiceId(0)); + + // State should have accumulated from both operations + expect(finalInfo).toBeDefined(); + expect(midInfo).toBeDefined(); +}); +``` + +### Testing with Guarantees + +```typescript +import { test, expect } from "bun:test"; +import { TestJam, createWorkReportAsync, generateGuarantees, ServiceId, Gas, Slot } from "@fluffylabs/jammin-sdk"; + +test("should generate valid guarantees", async () => { + const jam = await TestJam.create(); + + const report = await createWorkReportAsync({ + results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], + }); + + // Generate guarantees with validator signatures + const guarantees = await generateGuarantees([report], { + slot: Slot(100), + }); + + expect(guarantees).toHaveLength(1); + expect(guarantees[0]?.credentials.length).toBe(3); // Default 3 validators + expect(Number(guarantees[0]?.slot)).toBe(100); +}); +``` + +## Troubleshooting + +### Service Not Found + +If you see errors like "Service with id X not found", ensure that: + +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 + +### Import Errors + +Make sure you're importing from the correct module: + +```typescript +// Correct +import { TestJam, createWorkReportAsync } from "@fluffylabs/jammin-sdk"; +import { expectAccumulationSuccess } from "@fluffylabs/jammin-sdk/testing-helpers"; + +// Also correct (testing-helpers re-exports everything) +import { + TestJam, + createWorkReportAsync, + expectAccumulationSuccess +} from "@fluffylabs/jammin-sdk/testing-helpers"; +``` + +### Type Errors with Branded Types + +The SDK uses branded types for safety. Use the helper functions to create them: + +```typescript +import { ServiceId, Gas, CoreId, Slot, U32 } from "@fluffylabs/jammin-sdk"; + +// Correct +const serviceId = ServiceId(0); +const gas = Gas(1000n); +const coreId = CoreId(5); +const slot = Slot(100); +const value = U32(42); + +// Incorrect - will cause type errors +const serviceId = 0; // Type error +const gas = 1000n; // Type error +``` + +### Debug Logging + +Enable debug logging to see what's happening during accumulation: + +```typescript +const result = await jam + .withWorkReport(report) + .withOptions({ debug: true }) + .accumulation(); +``` + +This will output detailed logs including: +- Accumulation steps +- PVM host calls + +### State Not Persisting + +Remember that `accumulation()` automatically applies state updates. If you need to inspect state at different points: + +```typescript +// Check initial state +const initialInfo = jam.getServiceInfo(ServiceId(0)); + +// Run first accumulation (state is updated) +await jam.withWorkReport(report1).accumulation(); + +// Check intermediate state +const midInfo = jam.getServiceInfo(ServiceId(0)); + +// Run second accumulation (state is updated again) +await jam.withWorkReport(report2).accumulation(); + +// Check final state +const finalInfo = jam.getServiceInfo(ServiceId(0)); +``` + +## Advanced Usage + +### Custom Blake2b Hasher + +For more control over work report creation: + +```typescript +import { Blake2b } from "@fluffylabs/jammin-sdk/hash"; +import { createWorkReport } from "@fluffylabs/jammin-sdk"; + +const blake2b = await Blake2b.createHasher(); + +const report = createWorkReport(blake2b, { + results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], +}); +``` + +### Manual State Management + +For advanced use cases, you can manually manage state: + +```typescript +import { generateState, loadServices } from "@fluffylabs/jammin-sdk"; + +const services = await loadServices(); +const state = generateState(services); + +// Use state directly with simulateAccumulation +import { simulateAccumulation } from "@fluffylabs/jammin-sdk"; + +const result = await simulateAccumulation(state, [report], { + debug: true, +}); +``` + +### Custom Chain Specifications + +Override the default chain spec: + +```typescript +import { tinyChainSpec } from "@fluffylabs/jammin-sdk/config"; + +const customSpec = { + ...tinyChainSpec, + // Customize as needed +}; + +const result = await jam + .withWorkReport(report) + .withOptions({ chainSpec: customSpec }) + .accumulation(); +``` + +## Best Practices + +1. **Use `TestJam.create()` by default** - It automatically loads your services +2. **Chain method calls** - The fluent API makes tests more readable +3. **Use helper assertions** - They provide better error messages than raw `expect()` +4. **Test state changes explicitly** - Don't assume accumulation modified state +5. **Use branded types** - They prevent common mistakes with raw numbers +6. **Enable debug logging when troubleshooting** - It shows exactly what's happening +7. **Test both success and failure cases** - Include tests for panics and out-of-gas scenarios +8. **Keep tests isolated** - Create a new `TestJam` instance for each test + +## Next Steps + +- Review the [Service SDK examples](service-examples.md) for service implementation patterns +- Explore the [jammin suite](bootstrap/jammin-suite.md) for more tools and features diff --git a/packages/jammin-sdk/genesis-state-generator.test.ts b/packages/jammin-sdk/genesis-state-generator.test.ts index 592a7fc..b74e704 100644 --- a/packages/jammin-sdk/genesis-state-generator.test.ts +++ b/packages/jammin-sdk/genesis-state-generator.test.ts @@ -1,7 +1,8 @@ import { describe, expect, test } from "bun:test"; import { BytesBlob } from "@typeberry/lib/bytes"; -import { generateGenesis, type ServiceBuildOutput, toJip4Schema } from "./genesis-state-generator"; +import { generateGenesis, toJip4Schema } from "./genesis-state-generator"; import { ServiceId } from "./types"; +import type { ServiceBuildOutput } from "./utils"; describe("genesis-generator", () => { const services: ServiceBuildOutput[] = [ diff --git a/packages/jammin-sdk/index.ts b/packages/jammin-sdk/index.ts index 0057430..f4ecf12 100644 --- a/packages/jammin-sdk/index.ts +++ b/packages/jammin-sdk/index.ts @@ -5,14 +5,12 @@ export * as config from "@typeberry/lib/config"; export * as config_node from "@typeberry/lib/config-node"; export * as hash from "@typeberry/lib/hash"; export * as numbers from "@typeberry/lib/numbers"; -export * as state from "@typeberry/lib/state"; export * as state_merkleization from "@typeberry/lib/state-merkleization"; export * from "./config/index.js"; export * from "./genesis-state-generator.js"; -export * from "./simulator.js"; +export * from "./testing-helpers.js"; export * from "./types.js"; export * from "./utils/index.js"; -export * from "./work-report.js"; export function getSDKInfo() { return { diff --git a/packages/jammin-sdk/simulator.ts b/packages/jammin-sdk/simulator.ts index a7366b6..b02892a 100644 --- a/packages/jammin-sdk/simulator.ts +++ b/packages/jammin-sdk/simulator.ts @@ -1,6 +1,6 @@ import * as jamBlock from "@typeberry/lib/block"; import { Credential, type EntropyHash, ReportGuarantee, type TimeSlot } from "@typeberry/lib/block"; -import { Bytes, type BytesBlob } from "@typeberry/lib/bytes"; +import type { BytesBlob } from "@typeberry/lib/bytes"; import { Encoder } from "@typeberry/lib/codec"; import { asKnownSize } from "@typeberry/lib/collections"; import { type ChainSpec, PvmBackend, tinyChainSpec } from "@typeberry/lib/config"; @@ -218,11 +218,24 @@ async function enableLogs() { * * @example * ```typescript + * // Create a test instance with loaded services * const jam = await TestJam.create(); + * + * // Create and submit a work report * const report = await createWorkReportAsync({ * results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], * }); * const result = await jam.withWorkReport(report).accumulation(); + * + * // Chain multiple work reports + * const result2 = await jam + * .withWorkReport(report1) + * .withWorkReport(report2) + * .withOptions({ debug: true }) + * .accumulation(); + * + * // Query service state after accumulation + * const serviceInfo = jam.getServiceInfo(ServiceId(0)); * ``` */ export class TestJam { @@ -235,28 +248,98 @@ export class TestJam { this.state = state; } + /** + * Create a new TestJam instance with services loaded from the project. + * This is the recommended way to initialize TestJam for most tests. + * + * @returns Promise resolving to a new TestJam instance + * + * @example + * ```typescript + * const jam = await TestJam.create(); + * ``` + */ static async create(): Promise { const state = generateState(await loadServices()); return new TestJam(state); } + /** + * Create a new TestJam instance with empty state (no services). + * Useful for testing edge cases or when services are not needed. + * + * @returns A new TestJam instance with empty state + * + * @example + * ```typescript + * const jam = TestJam.empty(); + * ``` + */ static empty(): TestJam { const state = generateState([]); return new TestJam(state); } + /** + * Configure simulator options for the next accumulation. + * Options persist across multiple accumulation() calls until changed. + * + * @param options - Simulator configuration options + * @returns This instance for method chaining + * + * @example + * ```typescript + * const result = await jam + * .withOptions({ debug: true, slot: Slot(100) }) + * .withWorkReport(report) + * .accumulation(); + * ``` + */ withOptions(options: SimulatorOptions): this { this.options = options; return this; } - /** Inject a work report to be used in the next accumulation */ + /** + * Add a work report to be processed in the next accumulation. + * Multiple work reports can be chained before calling accumulation(). + * + * @param report - Work report to process + * @returns This instance for method chaining + * + * @example + * ```typescript + * const report1 = await createWorkReportAsync({ + * results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], + * }); + * const report2 = await createWorkReportAsync({ + * results: [{ serviceId: ServiceId(1), gas: Gas(2000n) }], + * }); + * + * const result = await jam + * .withWorkReport(report1) + * .withWorkReport(report2) + * .accumulation(); + * ``` + */ withWorkReport(report: WorkReport): this { this.workReports.push(report); return this; } - /** Run accumulation with the injected work reports, then clear them */ + /** + * Execute accumulation with all queued work reports and apply state changes. + * Work reports are automatically cleared after accumulation completes. + * + * @returns Promise resolving to accumulation result including state updates + * @throws Error if accumulation fails + * + * @example + * ```typescript + * const result = await jam.withWorkReport(report).accumulation(); + * console.log(`Processed ${result.accumulationStatistics.size} work items`); + * ``` + */ async accumulation(): Promise { const result = await simulateAccumulation(this.state, this.workReports, this.options); this.workReports = []; @@ -274,19 +357,68 @@ export class TestJam { return result; } + /** + * Get service account information for a specific service ID. + * + * @param id - Service ID to query + * @returns Service account info or undefined if not found + * + * @example + * ```typescript + * const info = jam.getServiceInfo(ServiceId(0)); + * console.log(`Service balance: ${info?.balance}`); + * ``` + */ getServiceInfo(id: jamBlock.ServiceId): ServiceAccountInfo | undefined { return this.state.getService(id)?.getInfo(); } + /** + * Get storage value for a specific key in a service's storage. + * + * @param id - Service ID + * @param key - Storage key + * @returns Storage value, undefined if service not found, null if key not found + * + * @example + * ```typescript + * const key = BytesBlob.blobFrom(new Uint8Array([1, 2, 3])); + * const value = jam.getServiceStorage(ServiceId(0), key); + * ``` + */ getServiceStorage(id: jamBlock.ServiceId, key: BytesBlob): BytesBlob | undefined | null { return this.state.getService(id)?.getStorage(asOpaqueType(key)); } + /** + * Get preimage data for a given hash in a service's preimage store. + * + * @param id - Service ID + * @param hash - Preimage hash + * @returns Preimage data, undefined if service not found, null if preimage not found + * + * @example + * ```typescript + * const preimage = jam.getServicePreimage(ServiceId(0), someHash); + * ``` + */ getServicePreimage(id: jamBlock.ServiceId, hash: OpaqueHash): BytesBlob | undefined | null { - Bytes.parseBytes("", 32).asOpaque(); return this.state.getService(id)?.getPreimage(hash.asOpaque()); } + /** + * Get preimage lookup history for a given hash in a service's preimage store. + * + * @param id - Service ID + * @param hash - Preimage hash + * @param len - Length parameter + * @returns Lookup history, undefined if service not found, null if not found + * + * @example + * ```typescript + * const history = jam.getServicePreimageLookup(ServiceId(0), someHash, U32(32)); + * ``` + */ getServicePreimageLookup( id: jamBlock.ServiceId, hash: OpaqueHash, diff --git a/packages/jammin-sdk/testing-helpers.test.ts b/packages/jammin-sdk/testing-helpers.test.ts new file mode 100644 index 0000000..376399a --- /dev/null +++ b/packages/jammin-sdk/testing-helpers.test.ts @@ -0,0 +1,136 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { ZERO_HASH } from "@typeberry/lib/hash"; +import { ServiceAccountInfo } from "@typeberry/lib/state"; +import { + AccumulationAssertionError, + expectAccumulationSuccess, + expectServiceInfoChange, + expectStateChange, + expectWorkItemCount, + generateGuarantees, + StateChangeAssertionError, + TestJam, +} from "./testing-helpers.js"; +import { CoreId, Gas, ServiceId, Slot, U32, U64 } from "./types.js"; +import { createWorkReportAsync } from "./work-report.js"; + +describe("testing-helpers", () => { + let jam: TestJam; + + beforeAll(() => { + jam = TestJam.empty(); + }); + + describe("generateGuarantees re-export", () => { + test("should generate guarantees from testing-helpers", async () => { + const report = await createWorkReportAsync({ + coreIndex: CoreId(0), + results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], + }); + + const guarantees = await generateGuarantees([report], { + slot: Slot(42), + }); + + expect(guarantees).toHaveLength(1); + expect(guarantees[0]?.credentials.length).toBe(3); + }); + }); + + describe("expectAccumulationSuccess", () => { + test("should pass for valid accumulation result", async () => { + const report = await createWorkReportAsync({ + results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], + }); + + const result = await jam.withWorkReport(report).accumulation(); + + // Should not throw + expectAccumulationSuccess(result); + }); + }); + + describe("expectWorkItemCount", () => { + test("should pass when count matches", async () => { + const report = await createWorkReportAsync({ + results: [ + { serviceId: ServiceId(0), gas: Gas(1000n) }, + { serviceId: ServiceId(1), gas: Gas(2000n) }, + ], + }); + + const result = await jam.withWorkReport(report).accumulation(); + + expectWorkItemCount(result, 2); + }); + + test("should throw when count does not match", async () => { + const report = await createWorkReportAsync({ + results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], + }); + + const result = await jam.withWorkReport(report).accumulation(); + + expect(() => { + expectWorkItemCount(result, 5); + }).toThrow(AccumulationAssertionError); + }); + }); + + describe("expectStateChange", () => { + test("should pass when predicate returns true", () => { + expectStateChange(10, 20, (before, after) => after > before); + }); + + test("should pass when predicate returns undefined (no explicit return)", () => { + // If predicate doesn't return false, it passes + expectStateChange(10, 20, (_before, _after) => undefined); + }); + + test("should throw when predicate returns false", () => { + expect(() => { + expectStateChange(10, 5, (before, after) => after > before, "Should increase"); + }).toThrow(StateChangeAssertionError); + }); + + 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"); + } + }); + }); + + describe("expectServiceInfoChange", () => { + const zeroService = ServiceAccountInfo.create({ + accumulateMinGas: Gas(0), + balance: U64(0), + codeHash: ZERO_HASH.asOpaque(), + created: Slot(0), + gratisStorage: U64(0), + lastAccumulation: Slot(0), + onTransferMinGas: Gas(0), + parentService: ServiceId(0), + storageUtilisationBytes: U64(0), + storageUtilisationCount: U32(0), + }); + + test("should pass when service info predicate is satisfied", () => { + const before = ServiceAccountInfo.create({ ...zeroService, balance: U64(1000) }); + const after = ServiceAccountInfo.create({ ...zeroService, balance: U64(1500) }); + + expectServiceInfoChange(before, after, (b, a) => a && b && a.balance > b.balance, "Balance should increase"); + }); + + test("should throw when predicate fails", () => { + const before = ServiceAccountInfo.create({ ...zeroService, balance: U64(1000) }); + const after = ServiceAccountInfo.create({ ...zeroService, balance: U64(500) }); + + expect(() => { + expectServiceInfoChange(before, after, (b, a) => a && b && a.balance > b.balance, "Balance should increase"); + }).toThrow(StateChangeAssertionError); + }); + }); +}); diff --git a/packages/jammin-sdk/testing-helpers.ts b/packages/jammin-sdk/testing-helpers.ts new file mode 100644 index 0000000..b801955 --- /dev/null +++ b/packages/jammin-sdk/testing-helpers.ts @@ -0,0 +1,196 @@ +/** + * Testing helpers for JAM service development and integration tests. + * Provides utilities for assertions, state comparisons, and common test patterns. + * + * @module testing-helpers + */ + +import type { ServiceAccountInfo } from "@typeberry/lib/state"; +import type { AccumulateResult } from "@typeberry/lib/transition"; + +export type { AccumulateResult, AccumulateState, GuaranteeOptions, SimulatorOptions, State } from "./simulator.js"; +export { generateGuarantees, simulateAccumulation, TestJam } from "./simulator.js"; +export type { WorkReport, WorkReportConfig, WorkResultConfig, WorkResultStatus } from "./work-report.js"; +export { createWorkReport, createWorkReportAsync, createWorkResult } from "./work-report.js"; + +/** + * Custom error thrown when accumulation assertions fail + */ +export class AccumulationAssertionError extends Error { + constructor( + message: string, + public result: AccumulateResult, + ) { + super(message); + this.name = "AccumulationAssertionError"; + } +} + +/** + * Custom error thrown when state change assertions fail + */ +export class StateChangeAssertionError extends Error { + constructor( + message: string, + public before: unknown, + public after: unknown, + ) { + super(message); + this.name = "StateChangeAssertionError"; + } +} + +/** + * Assert that accumulation completed successfully without errors. + * Validates that the accumulation result has the expected structure. + * + * @param result - Accumulation result to validate + * @throws AccumulationAssertionError if accumulation structure is invalid + * + * @example + * ```typescript + * import { expectAccumulationSuccess } from "@fluffylabs/jammin-sdk/testing-helpers"; + * + * const jam = await TestJam.create(); + * const report = await createWorkReportAsync({ + * results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], + * }); + * const result = await jam.withWorkReport(report).accumulation(); + * + * // Assert accumulation completed + * expectAccumulationSuccess(result); + * ``` + */ +export function expectAccumulationSuccess(result: AccumulateResult): void { + if (!result.stateUpdate) { + throw new AccumulationAssertionError("Accumulation result missing stateUpdate", result); + } + + if (!result.accumulationStatistics) { + throw new AccumulationAssertionError("Accumulation result missing accumulationStatistics", result); + } + + if (!result.accumulationOutputLog) { + throw new AccumulationAssertionError("Accumulation result missing accumulationOutputLog", result); + } +} + +/** + * Assert that accumulation processed the expected number of work items. + * + * @param result - Accumulation result to validate + * @param expectedCount - Expected number of work items processed + * @throws AccumulationAssertionError if count doesn't match + * + * @example + * ```typescript + * import { expectWorkItemCount } from "@fluffylabs/jammin-sdk/testing-helpers"; + * + * const result = await jam + * .withWorkReport(report1) + * .withWorkReport(report2) + * .accumulation(); + * + * expectWorkItemCount(result, 2); + * ``` + */ +export function expectWorkItemCount(result: AccumulateResult, expectedCount: number): void { + const actualCount = result.accumulationStatistics.size; + if (actualCount !== expectedCount) { + throw new AccumulationAssertionError(`Expected ${expectedCount} work items but got ${actualCount}`, result); + } +} + +/** + * Predicate function for validating state changes. + * Returns true if the state change is valid, false or throws an error otherwise. + */ +export type StateChangePredicate = (before: T, after: T) => boolean | undefined; + +/** + * Assert that a state change satisfies a given predicate. + * Useful for validating service state transitions after accumulation. + * + * @param before - State before accumulation + * @param after - State after accumulation + * @param predicate - Function that validates the state change + * @param errorMessage - Custom error message if assertion fails + * @throws StateChangeAssertionError if predicate returns false + * + * @example + * ```typescript + * import { expectStateChange } from "@fluffylabs/jammin-sdk/testing-helpers"; + * + * const jam = await TestJam.create(); + * const beforeInfo = jam.getServiceInfo(ServiceId(0)); + * + * const report = await createWorkReportAsync({ + * results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], + * }); + * await jam.withWorkReport(report).accumulation(); + * + * const afterInfo = jam.getServiceInfo(ServiceId(0)); + * + * // Assert that balance increased + * expectStateChange( + * beforeInfo?.balance, + * afterInfo?.balance, + * (before, after) => after > before, + * "Service balance should increase after work" + * ); + * ``` + */ +export function expectStateChange( + before: T, + after: T, + predicate: StateChangePredicate, + errorMessage?: string, +): void { + const result = predicate(before, after); + + // If predicate returns false explicitly, throw error + if (result === false) { + throw new StateChangeAssertionError(errorMessage ?? "State change validation failed", before, after); + } +} + +/** + * Assert that service account information changed as expected. + * Common use case for validating balance changes, gas refunds, etc. + * + * @param before - Service info before accumulation + * @param after - Service info after accumulation + * @param predicate - Function that validates the info change + * @param errorMessage - Custom error message if assertion fails + * @throws StateChangeAssertionError if predicate returns false + * + * @example + * ```typescript + * import { expectServiceInfoChange } from "@fluffylabs/jammin-sdk/testing-helpers"; + * + * const jam = await TestJam.create(); + * const before = jam.getServiceInfo(ServiceId(0)); + * + * await jam.withWorkReport(report).withOptions({ slot: Slot(100) }).accumulation(); + * + * const after = jam.getServiceInfo(ServiceId(0)); + * + * // Validate that gas was consumed + * expectServiceInfoChange( + * before, + * after, + * (b, a) => { + * return a && b && a.lastAccumulation > b.lastAccumulation; + * }, + * "Service should update last accumulation during execution" + * ); + * ``` + */ +export function expectServiceInfoChange( + before: ServiceAccountInfo | undefined, + after: ServiceAccountInfo | undefined, + predicate: StateChangePredicate, + errorMessage?: string, +): void { + expectStateChange(before, after, predicate, errorMessage); +} From 755a6664c41e6ecf1c418d9e0962b46cd8b0aec8 Mon Sep 17 00:00:00 2001 From: HeyImStas Date: Wed, 4 Feb 2026 19:31:18 +0100 Subject: [PATCH 03/18] import fix --- packages/jammin-sdk/utils/generate-service-output.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/jammin-sdk/utils/generate-service-output.ts b/packages/jammin-sdk/utils/generate-service-output.ts index fb6876b..532e543 100644 --- a/packages/jammin-sdk/utils/generate-service-output.ts +++ b/packages/jammin-sdk/utils/generate-service-output.ts @@ -1,6 +1,6 @@ import { join, resolve } from "node:path"; import { - type preimage, + type PreimageHash, type ServiceId as ServiceIdType, tryAsServiceGas, tryAsServiceId, @@ -19,8 +19,8 @@ export interface ServiceBuildOutput { code: BytesBlob; storage?: Record; info?: Partial; - preimageBlobs?: HashDictionary; - preimageRequests?: Map; + preimageBlobs?: HashDictionary; + preimageRequests?: Map; } /** @@ -111,7 +111,7 @@ export async function generateServiceOutput( ]), ); - const preimageRequestsMap = new Map( + const preimageRequestsMap = new Map( Object.entries(preimageRequests ?? {}).map(([hash, slots]) => [ Bytes.parseBytes(hash, HASH_SIZE).asOpaque(), tryAsLookupHistorySlots(slots.map((slot) => Slot(slot))), From f080e0c88c8d599d70d86bac2fa39f1fe3298a94 Mon Sep 17 00:00:00 2001 From: HeyImStas Date: Wed, 4 Feb 2026 19:33:29 +0100 Subject: [PATCH 04/18] apply rabbit suggestion --- docs/src/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/testing.md b/docs/src/testing.md index 204fd1c..3351374 100644 --- a/docs/src/testing.md +++ b/docs/src/testing.md @@ -51,7 +51,7 @@ The `TestJam` class is the main entry point for testing JAM services. It manages #### With Loaded Services ```typescript -// Loads all services from your project's jam.toml files +// Loads all services from your project's jammin.build.yml files const jam = await TestJam.create(); ``` From 43c10c8b75a1c8c8931a9ffd0f1f7081911e4c3d Mon Sep 17 00:00:00 2001 From: HeyImStas Date: Wed, 4 Feb 2026 19:39:39 +0100 Subject: [PATCH 05/18] self review --- packages/jammin-sdk/testing-helpers.test.ts | 28 --------------------- packages/jammin-sdk/testing-helpers.ts | 26 ------------------- 2 files changed, 54 deletions(-) diff --git a/packages/jammin-sdk/testing-helpers.test.ts b/packages/jammin-sdk/testing-helpers.test.ts index 376399a..aaca55e 100644 --- a/packages/jammin-sdk/testing-helpers.test.ts +++ b/packages/jammin-sdk/testing-helpers.test.ts @@ -6,7 +6,6 @@ import { expectAccumulationSuccess, expectServiceInfoChange, expectStateChange, - expectWorkItemCount, generateGuarantees, StateChangeAssertionError, TestJam, @@ -50,33 +49,6 @@ describe("testing-helpers", () => { }); }); - describe("expectWorkItemCount", () => { - test("should pass when count matches", async () => { - const report = await createWorkReportAsync({ - results: [ - { serviceId: ServiceId(0), gas: Gas(1000n) }, - { serviceId: ServiceId(1), gas: Gas(2000n) }, - ], - }); - - const result = await jam.withWorkReport(report).accumulation(); - - expectWorkItemCount(result, 2); - }); - - test("should throw when count does not match", async () => { - const report = await createWorkReportAsync({ - results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], - }); - - const result = await jam.withWorkReport(report).accumulation(); - - expect(() => { - expectWorkItemCount(result, 5); - }).toThrow(AccumulationAssertionError); - }); - }); - describe("expectStateChange", () => { test("should pass when predicate returns true", () => { expectStateChange(10, 20, (before, after) => after > before); diff --git a/packages/jammin-sdk/testing-helpers.ts b/packages/jammin-sdk/testing-helpers.ts index b801955..1da2f7d 100644 --- a/packages/jammin-sdk/testing-helpers.ts +++ b/packages/jammin-sdk/testing-helpers.ts @@ -75,32 +75,6 @@ export function expectAccumulationSuccess(result: AccumulateResult): void { } } -/** - * Assert that accumulation processed the expected number of work items. - * - * @param result - Accumulation result to validate - * @param expectedCount - Expected number of work items processed - * @throws AccumulationAssertionError if count doesn't match - * - * @example - * ```typescript - * import { expectWorkItemCount } from "@fluffylabs/jammin-sdk/testing-helpers"; - * - * const result = await jam - * .withWorkReport(report1) - * .withWorkReport(report2) - * .accumulation(); - * - * expectWorkItemCount(result, 2); - * ``` - */ -export function expectWorkItemCount(result: AccumulateResult, expectedCount: number): void { - const actualCount = result.accumulationStatistics.size; - if (actualCount !== expectedCount) { - throw new AccumulationAssertionError(`Expected ${expectedCount} work items but got ${actualCount}`, result); - } -} - /** * Predicate function for validating state changes. * Returns true if the state change is valid, false or throws an error otherwise. From e7e389242ccf6f3a1784a68bd76b18a7109f4114 Mon Sep 17 00:00:00 2001 From: HeyImStas Date: Wed, 4 Feb 2026 19:40:43 +0100 Subject: [PATCH 06/18] qa-fix --- packages/jammin-sdk/testing-helpers.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/jammin-sdk/testing-helpers.test.ts b/packages/jammin-sdk/testing-helpers.test.ts index aaca55e..b840525 100644 --- a/packages/jammin-sdk/testing-helpers.test.ts +++ b/packages/jammin-sdk/testing-helpers.test.ts @@ -2,7 +2,6 @@ import { beforeAll, describe, expect, test } from "bun:test"; import { ZERO_HASH } from "@typeberry/lib/hash"; import { ServiceAccountInfo } from "@typeberry/lib/state"; import { - AccumulationAssertionError, expectAccumulationSuccess, expectServiceInfoChange, expectStateChange, From e40a3539b5efb09011a6eac4d8b8d87ec9b0afe3 Mon Sep 17 00:00:00 2001 From: HeyImStas Date: Tue, 10 Feb 2026 09:29:17 +0100 Subject: [PATCH 07/18] Implement PR 106 review feedback: auto-generated test config, rename accumulation() to accumulate(), fine-grained logging control, and documentation updates Addresses feedback from tomusdrw and skoszuta on PR 106: Phase 1: Code Generation Enhancement - Create generate-test-config.ts utility to generate TypeScript files with type-safe service ID mappings - Enhance build-command to automatically generate config/jammin.test.config.ts with service details and chainspec - Services now have a type-safe mapping instead of hardcoded values Phase 2: API Refinement - Rename accumulation() method to accumulate() for more concise verb-based API - Update all tests and documentation to use new method name - Improves code readability and aligns with reviewer preference Phase 3: Debug Logging Enhancement - Add DebugLoggingOptions interface for fine-grained control over logging categories - Support both boolean (enable all) and object (selective) debug modes - Available options: - pvmExecution: PVM execution details including instruction traces - ecalliTrace: Ecalli host call traces for service execution - hostCalls: Host calls during service execution - accumulate: Accumulation process details and state transitions - safrole: Randomness and validator selection operations - refine: Work report refinement and validation - stateTransitions: State transitions and root changes - Backward compatible with existing debug: true usage Phase 4: Documentation Updates - Add prerequisite section about building services before testing - Include custom assertions in basic test example - Document fine-grained logging control with examples - Update available options to reflect new debug capabilities - Emphasize use of generated test config for type safety All tests pass, quality checks pass, build succeeds. --- bin/cli/src/commands/build-command.ts | 21 +++- docs/src/testing.md | 108 +++++++++------- packages/jammin-sdk/simulator.test.ts | 12 +- packages/jammin-sdk/simulator.ts | 107 ++++++++++++++-- packages/jammin-sdk/testing-helpers.test.ts | 2 +- packages/jammin-sdk/testing-helpers.ts | 6 +- .../jammin-sdk/utils/generate-test-config.ts | 115 ++++++++++++++++++ packages/jammin-sdk/utils/index.ts | 1 + 8 files changed, 309 insertions(+), 63 deletions(-) create mode 100644 packages/jammin-sdk/utils/generate-test-config.ts diff --git a/bin/cli/src/commands/build-command.ts b/bin/cli/src/commands/build-command.ts index e2fee22..6e41319 100644 --- a/bin/cli/src/commands/build-command.ts +++ b/bin/cli/src/commands/build-command.ts @@ -2,7 +2,14 @@ import { mkdir } from "node:fs/promises"; import { join, relative, resolve } from "node:path"; import * as p from "@clack/prompts"; import type { ServiceConfig } from "@fluffylabs/jammin-sdk"; -import { copyJamToDist, getJamFiles, getServiceConfigs, SDK_CONFIGS } from "@fluffylabs/jammin-sdk"; +import { + copyJamToDist, + generateTestConfigInProjectDir, + getJamFiles, + getServiceConfigs, + loadServices, + SDK_CONFIGS, +} from "@fluffylabs/jammin-sdk"; import { Command } from "commander"; /** @@ -107,6 +114,18 @@ Examples: p.log.info("--------------------------------"); + // Generate test configuration file + try { + s.start("Generating test configuration..."); + const services = await loadServices(projectRoot); + await generateTestConfigInProjectDir(services, projectRoot); + s.stop("✅ Test configuration generated"); + p.log.message("📝 Generated: config/jammin.test.config.ts"); + } catch (_error) { + s.stop("⚠️ Could not generate test configuration"); + p.log.warn("Test configuration generation failed (this is optional)"); + } + if (buildFailed) { p.outro("❌ Build failed. See the output above and check the logs for more details."); process.exit(1); diff --git a/docs/src/testing.md b/docs/src/testing.md index 3351374..c262600 100644 --- a/docs/src/testing.md +++ b/docs/src/testing.md @@ -8,6 +8,16 @@ The jammin SDK provides a comprehensive testing framework for simulating JAM acc ## Setup +### Prerequisites + +Before writing tests, ensure your services are built: + +```bash +jammin build +``` + +This generates service binaries and creates `config/jammin.test.config.ts` with type-safe service mappings for use in tests. + ### Installation The testing utilities are included in the `@fluffylabs/jammin-sdk` package: @@ -18,11 +28,17 @@ bun add -d @fluffylabs/jammin-sdk ### Basic Test Structure -Here's a minimal test using Bun's built-in test runner: +Here's a minimal test using Bun's built-in test runner with custom assertions: ```typescript import { test, expect } from "bun:test"; -import { TestJam, createWorkReportAsync, ServiceId, Gas } from "@fluffylabs/jammin-sdk"; +import { + TestJam, + createWorkReportAsync, + expectAccumulationSuccess, + ServiceId, + Gas, +} from "@fluffylabs/jammin-sdk"; test("should process work report", async () => { // Create test instance with loaded services @@ -34,10 +50,10 @@ test("should process work report", async () => { }); // Execute accumulation - const result = await jam.withWorkReport(report).accumulation(); + const result = await jam.withWorkReport(report).accumulate(); - // Verify results - expect(result).toBeDefined(); + // Use custom assertion helper + expectAccumulationSuccess(result); expect(result.accumulationStatistics.size).toBe(1); }); ``` @@ -78,7 +94,7 @@ const report = await createWorkReportAsync({ ], }); -const result = await jam.withWorkReport(report).accumulation(); +const result = await jam.withWorkReport(report).accumulate(); ``` #### Multiple Work Reports @@ -97,7 +113,7 @@ const report2 = await createWorkReportAsync({ const result = await jam .withWorkReport(report1) .withWorkReport(report2) - .accumulation(); + .accumulate(); console.log(`Processed ${result.accumulationStatistics.size} work items`); ``` @@ -117,13 +133,13 @@ const result = await jam pvmBackend: PvmBackend.BuiltIn, // Use built-in PVM sequential: true, // Sequential accumulation (default) }) - .accumulation(); + .accumulate(); ``` #### Available Options - `slot?: TimeSlot` - Time slot for accumulation (defaults to state's current timeslot) -- `debug?: boolean` - Enable debug logging for accumulation and PVM host calls +- `debug?: boolean | DebugLoggingOptions` - Enable debug logging (can be a boolean for all logs, or an object for fine-grained control) - `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) @@ -138,7 +154,7 @@ After accumulation, you can query the service state: ```typescript const info = jam.getServiceInfo(ServiceId(0)); console.log(`Balance: ${info?.balance}`); -console.log(`Gas used: ${info?.gasUsed}`); +console.log(`Code hash: ${info?.codeHash}`); ``` #### Get Service Storage @@ -244,7 +260,7 @@ Assert that accumulation completed with a valid structure: ```typescript import { expectAccumulationSuccess } from "@fluffylabs/jammin-sdk/testing-helpers"; -const result = await jam.withWorkReport(report).accumulation(); +const result = await jam.withWorkReport(report).accumulate(); expectAccumulationSuccess(result); // Throws if result is invalid ``` @@ -258,7 +274,7 @@ import { expectWorkItemCount } from "@fluffylabs/jammin-sdk/testing-helpers"; const result = await jam .withWorkReport(report1) .withWorkReport(report2) - .accumulation(); + .accumulate(); expectWorkItemCount(result, 2); // Throws if count doesn't match ``` @@ -272,7 +288,7 @@ import { expectStateChange } from "@fluffylabs/jammin-sdk/testing-helpers"; const beforeBalance = jam.getServiceInfo(ServiceId(0))?.balance; -await jam.withWorkReport(report).accumulation(); +await jam.withWorkReport(report).accumulate(); const afterBalance = jam.getServiceInfo(ServiceId(0))?.balance; @@ -292,7 +308,7 @@ Specialized helper for validating service account changes: import { expectServiceInfoChange } from "@fluffylabs/jammin-sdk/testing-helpers"; const before = jam.getServiceInfo(ServiceId(0)); -await jam.withWorkReport(report).accumulation(); +await jam.withWorkReport(report).accumulate(); const after = jam.getServiceInfo(ServiceId(0)); expectServiceInfoChange( @@ -319,7 +335,7 @@ test("service should execute successfully", async () => { results: [{ serviceId: ServiceId(0), gas: Gas(100000n) }], }); - const result = await jam.withWorkReport(report).accumulation(); + const result = await jam.withWorkReport(report).accumulate(); expectAccumulationSuccess(result); expect(result.accumulationStatistics.size).toBe(1); @@ -342,7 +358,7 @@ test("service storage should update", async () => { results: [{ serviceId: ServiceId(0), gas: Gas(50000n) }], }); - await jam.withWorkReport(report).accumulation(); + await jam.withWorkReport(report).accumulate(); const afterValue = jam.getServiceStorage(ServiceId(0), storageKey); expect(afterValue).not.toEqual(beforeValue); @@ -366,7 +382,7 @@ test("should process multiple services", async () => { ], }); - const result = await jam.withWorkReport(report).accumulation(); + const result = await jam.withWorkReport(report).accumulate(); expect(result.accumulationStatistics.size).toBe(3); }); @@ -391,7 +407,7 @@ test("should handle panic gracefully", async () => { ], }); - const result = await jam.withWorkReport(report).accumulation(); + const result = await jam.withWorkReport(report).accumulate(); // Accumulation should complete even with panicked work items expect(result).toBeDefined(); @@ -412,7 +428,7 @@ test("state should persist across accumulations", async () => { const report1 = await createWorkReportAsync({ results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], }); - await jam.withWorkReport(report1).accumulation(); + await jam.withWorkReport(report1).accumulate(); const midInfo = jam.getServiceInfo(ServiceId(0)); @@ -420,7 +436,7 @@ test("state should persist across accumulations", async () => { const report2 = await createWorkReportAsync({ results: [{ serviceId: ServiceId(0), gas: Gas(2000n) }], }); - await jam.withWorkReport(report2).accumulation(); + await jam.withWorkReport(report2).accumulate(); const finalInfo = jam.getServiceInfo(ServiceId(0)); @@ -464,23 +480,6 @@ If you see errors like "Service with id X not found", ensure that: 2. You're using `TestJam.create()` (not `TestJam.empty()`) 3. The service ID in your test matches the service ID in your configuration -### Import Errors - -Make sure you're importing from the correct module: - -```typescript -// Correct -import { TestJam, createWorkReportAsync } from "@fluffylabs/jammin-sdk"; -import { expectAccumulationSuccess } from "@fluffylabs/jammin-sdk/testing-helpers"; - -// Also correct (testing-helpers re-exports everything) -import { - TestJam, - createWorkReportAsync, - expectAccumulationSuccess -} from "@fluffylabs/jammin-sdk/testing-helpers"; -``` - ### Type Errors with Branded Types The SDK uses branded types for safety. Use the helper functions to create them: @@ -508,29 +507,54 @@ Enable debug logging to see what's happening during accumulation: const result = await jam .withWorkReport(report) .withOptions({ debug: true }) - .accumulation(); + .accumulate(); ``` This will output detailed logs including: - Accumulation steps - PVM host calls +#### Fine-Grained Logging Control + +You can enable only specific log categories for more focused debugging: + +```typescript +// Enable only ecalli (host call) traces +const result = await jam + .withWorkReport(report) + .withOptions({ + debug: { + ecalliTrace: true, + }, + }) + .accumulate(); +``` + +Available debug options: +- `pvmExecution` - PVM (Polkadot Virtual Machine) execution details including instruction traces and memory access patterns +- `ecalliTrace` - Ecalli (host call) traces for service execution and debugging service interactions +- `hostCalls` - Host calls made during service execution +- `accumulate` - Accumulation process details and state transitions showing how work items are processed +- `safrole` - Safrole (randomness and validator selection) operations for consensus-related debugging +- `refine` - Refinement process for work reports including validation and processing +- `stateTransitions` - State transitions and state root changes during processing + ### State Not Persisting -Remember that `accumulation()` automatically applies state updates. If you need to inspect state at different points: +Remember that `accumulate()` automatically applies state updates. If you need to inspect state at different points: ```typescript // Check initial state const initialInfo = jam.getServiceInfo(ServiceId(0)); // Run first accumulation (state is updated) -await jam.withWorkReport(report1).accumulation(); +await jam.withWorkReport(report1).accumulate(); // Check intermediate state const midInfo = jam.getServiceInfo(ServiceId(0)); // Run second accumulation (state is updated again) -await jam.withWorkReport(report2).accumulation(); +await jam.withWorkReport(report2).accumulate(); // Check final state const finalInfo = jam.getServiceInfo(ServiceId(0)); @@ -586,7 +610,7 @@ const customSpec = { const result = await jam .withWorkReport(report) .withOptions({ chainSpec: customSpec }) - .accumulation(); + .accumulate(); ``` ## Best Practices diff --git a/packages/jammin-sdk/simulator.test.ts b/packages/jammin-sdk/simulator.test.ts index 58beb72..92af325 100644 --- a/packages/jammin-sdk/simulator.test.ts +++ b/packages/jammin-sdk/simulator.test.ts @@ -16,7 +16,7 @@ describe("simulateAccumulation", () => { results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], }); - const result = await jam.withWorkReport(report).accumulation(); + const result = await jam.withWorkReport(report).accumulate(); expect(result).toBeDefined(); expect(result.stateUpdate).toBeDefined(); @@ -33,7 +33,7 @@ describe("simulateAccumulation", () => { results: [{ serviceId: ServiceId(1), gas: Gas(750n) }], }); - const result = await jam.withWorkReport(report1).withWorkReport(report2).accumulation(); + const result = await jam.withWorkReport(report1).withWorkReport(report2).accumulate(); expect(result.accumulationStatistics).toBeDefined(); expect(result.accumulationStatistics.size).toBe(2); @@ -48,14 +48,14 @@ describe("simulateAccumulation", () => { ], }); - const result = await jam.withWorkReport(report).accumulation(); + const result = await jam.withWorkReport(report).accumulate(); expect(result).toBeDefined(); expect(result.accumulationStatistics.size).toBe(3); }); test("handles empty reports array", async () => { - const result = await jam.accumulation(); + const result = await jam.accumulate(); expect(result).toBeDefined(); expect(result.stateUpdate).toBeDefined(); @@ -73,7 +73,7 @@ describe("simulateAccumulation", () => { .withOptions({ slot: customSlot, }) - .accumulation(); + .accumulate(); expect(result.stateUpdate.timeslot).toBeDefined(); // The timeslot in the update should be >= the slot we passed @@ -90,7 +90,7 @@ describe("simulateAccumulation", () => { .withOptions({ pvmBackend: config.PvmBackend.BuiltIn, }) - .accumulation(); + .accumulate(); expect(result).toBeDefined(); }); diff --git a/packages/jammin-sdk/simulator.ts b/packages/jammin-sdk/simulator.ts index b02892a..016e031 100644 --- a/packages/jammin-sdk/simulator.ts +++ b/packages/jammin-sdk/simulator.ts @@ -32,6 +32,53 @@ export interface GuaranteeOptions { slot?: TimeSlot; } +/** + * Fine-grained debug logging options + */ +export interface DebugLoggingOptions { + /** + * Log PVM (Polkadot Virtual Machine) execution details. + * Includes instruction traces, memory access patterns, etc. + */ + pvmExecution?: boolean; + + /** + * Log ecalli traces for service execution. (Prettier HostCalls) + * Useful for debugging service interactions. + */ + ecalliTrace?: boolean; + + /** + * Log host calls made during service execution. + * Useful for debugging service interactions. + */ + hostCalls?: boolean; + + /** + * Log accumulation process details and state transitions. + * Shows how work items are processed and state is updated. + */ + accumulate?: boolean; + + /** + * Log safrole (randomness and validator selection) operations. + * Helps debug randomness and consensus-related issues. + */ + safrole?: boolean; + + /** + * Log refinement process for work reports. + * Useful for understanding work report validation and processing. + */ + refine?: boolean; + + /** + * Log state transitions and state root changes. + * Helps understand how state evolves during processing. + */ + stateTransitions?: boolean; +} + /** * Configuration options for the accumulation simulator. */ @@ -68,9 +115,20 @@ export interface SimulatorOptions { /** * Enable debug logging for accumulation and PVM host calls. + * When true, enables logging for all available debug categories. + * Can also pass specific logging options for fine-grained control. * Defaults to false. + * + * @example + * ```typescript + * // Enable all debug logs + * .withOptions({ debug: true }) + * + * // Enable only specific logs + * .withOptions({ debug: { ecalliTrace: true, stateTransitions: true } }) + * ``` */ - debug?: boolean; + debug?: boolean | DebugLoggingOptions; } /** @@ -175,7 +233,8 @@ export async function simulateAccumulation( // Configure logging if debug is enabled if (options.debug) { - await enableLogs(); + const loggingOptions = typeof options.debug === "boolean" ? undefined : options.debug; + await enableLogs(loggingOptions); } const blake2b = await Blake2b.createHasher(); @@ -200,11 +259,39 @@ export async function simulateAccumulation( return result.ok; } -async function enableLogs() { +async function enableLogs(options?: DebugLoggingOptions) { try { const loggerModule = await import("@typeberry/lib/logger"); if (loggerModule.Logger && loggerModule.Level) { - loggerModule.Logger.configureAll("info,host-calls=trace,accumulate=trace", loggerModule.Level.LOG, process.cwd()); + // Build logging configuration based on enabled options + const enabledLoggers: string[] = ["info"]; + + // Add loggers based on fine-grained options + if (options?.pvmExecution !== false) { + enabledLoggers.push("pvm=trace"); + } + if (options?.ecalliTrace !== false) { + enabledLoggers.push("ecalli=trace"); + } + if (options?.hostCalls !== false) { + enabledLoggers.push("host-calls=trace"); + } + if (options?.refine !== false) { + enabledLoggers.push("refine=trace"); + } + if (options?.accumulate !== false) { + enabledLoggers.push("accumulate=trace"); + } + if (options?.safrole !== false) { + enabledLoggers.push("safrole=trace"); + } + if (options?.stateTransitions !== false) { + enabledLoggers.push("stf=trace"); + } + + const logConfig = enabledLoggers.join(","); + + loggerModule.Logger.configureAll(logConfig, loggerModule.Level.LOG, process.cwd()); } } catch { console.warn("Warning: Could not configure typeberry logger"); @@ -282,7 +369,7 @@ export class TestJam { /** * Configure simulator options for the next accumulation. - * Options persist across multiple accumulation() calls until changed. + * Options persist across multiple accumulate() calls until changed. * * @param options - Simulator configuration options * @returns This instance for method chaining @@ -292,7 +379,7 @@ export class TestJam { * const result = await jam * .withOptions({ debug: true, slot: Slot(100) }) * .withWorkReport(report) - * .accumulation(); + * .accumulate(); * ``` */ withOptions(options: SimulatorOptions): this { @@ -302,7 +389,7 @@ export class TestJam { /** * Add a work report to be processed in the next accumulation. - * Multiple work reports can be chained before calling accumulation(). + * Multiple work reports can be chained before calling accumulate(). * * @param report - Work report to process * @returns This instance for method chaining @@ -319,7 +406,7 @@ export class TestJam { * const result = await jam * .withWorkReport(report1) * .withWorkReport(report2) - * .accumulation(); + * .accumulate(); * ``` */ withWorkReport(report: WorkReport): this { @@ -336,11 +423,11 @@ export class TestJam { * * @example * ```typescript - * const result = await jam.withWorkReport(report).accumulation(); + * const result = await jam.withWorkReport(report).accumulate(); * console.log(`Processed ${result.accumulationStatistics.size} work items`); * ``` */ - async accumulation(): Promise { + async accumulate(): Promise { const result = await simulateAccumulation(this.state, this.workReports, this.options); this.workReports = []; if (this.state instanceof InMemoryState) { diff --git a/packages/jammin-sdk/testing-helpers.test.ts b/packages/jammin-sdk/testing-helpers.test.ts index b840525..58cfc45 100644 --- a/packages/jammin-sdk/testing-helpers.test.ts +++ b/packages/jammin-sdk/testing-helpers.test.ts @@ -41,7 +41,7 @@ describe("testing-helpers", () => { results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], }); - const result = await jam.withWorkReport(report).accumulation(); + const result = await jam.withWorkReport(report).accumulate(); // Should not throw expectAccumulationSuccess(result); diff --git a/packages/jammin-sdk/testing-helpers.ts b/packages/jammin-sdk/testing-helpers.ts index 1da2f7d..c59972b 100644 --- a/packages/jammin-sdk/testing-helpers.ts +++ b/packages/jammin-sdk/testing-helpers.ts @@ -55,7 +55,7 @@ export class StateChangeAssertionError extends Error { * const report = await createWorkReportAsync({ * results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], * }); - * const result = await jam.withWorkReport(report).accumulation(); + * const result = await jam.withWorkReport(report).accumulate(); * * // Assert accumulation completed * expectAccumulationSuccess(result); @@ -101,7 +101,7 @@ export type StateChangePredicate = (before: T, after: T) => boolean | undefin * const report = await createWorkReportAsync({ * results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], * }); - * await jam.withWorkReport(report).accumulation(); + * await jam.withWorkReport(report).accumulate(); * * const afterInfo = jam.getServiceInfo(ServiceId(0)); * @@ -145,7 +145,7 @@ export function expectStateChange( * const jam = await TestJam.create(); * const before = jam.getServiceInfo(ServiceId(0)); * - * await jam.withWorkReport(report).withOptions({ slot: Slot(100) }).accumulation(); + * await jam.withWorkReport(report).withOptions({ slot: Slot(100) }).accumulate(); * * const after = jam.getServiceInfo(ServiceId(0)); * diff --git a/packages/jammin-sdk/utils/generate-test-config.ts b/packages/jammin-sdk/utils/generate-test-config.ts new file mode 100644 index 0000000..9c8e3b4 --- /dev/null +++ b/packages/jammin-sdk/utils/generate-test-config.ts @@ -0,0 +1,115 @@ +import { resolve } from "node:path"; +import type { tinyChainSpec } from "@typeberry/lib/config"; +import { loadBuildConfig } from "../config/config-loader.js"; +import type { ServiceBuildOutput } from "./generate-service-output.js"; + +/** + * Generated test configuration with service mappings + */ +export interface GeneratedTestConfig { + services: Record; + chainSpec: typeof tinyChainSpec; +} + +/** + * Generate a TypeScript file with test configuration including: + * - Service ID mappings + * - ChainSpec configuration + * - Service names and IDs + * + * @param services - Array of service build outputs + * @param outputPath - Path where to write the generated test config file + */ +export async function generateTestConfigFile(services: ServiceBuildOutput[], outputPath: string): Promise { + // Create service mapping: service name -> { id, name } + const serviceMap: Record = {}; + + // Load the config to get service names + const config = await loadBuildConfig(); + + // Match services from config with their IDs from build outputs + for (let i = 0; i < config.services.length; i++) { + const service = config.services[i]; + const buildOutput = services[i]; + if (service && buildOutput) { + serviceMap[service.name] = { + id: buildOutput.id as unknown as number, + name: service.name, + }; + } + } + + // Generate TypeScript code + const tsCode = generateTestConfigCode(serviceMap); + + // Write to file + await Bun.write(outputPath, tsCode); +} + +/** + * Generate test config file in the project's config directory + * This is typically called after the build command completes + */ +export async function generateTestConfigInProjectDir( + services: ServiceBuildOutput[], + projectRoot: string = process.cwd(), +): Promise { + const configPath = resolve(projectRoot, "config", "jammin.test.config.ts"); + await generateTestConfigFile(services, configPath); + return configPath; +} + +/** + * Generate the TypeScript code for test configuration + */ +function generateTestConfigCode(serviceMap: Record): string { + const serviceEntries = Object.entries(serviceMap) + .map(([name, config]) => ` ${name}: { id: ${config.id}, name: "${config.name}" },`) + .join("\n"); + + return `/** + * Auto-generated test configuration + * This file is generated by the jammin build command + * Do not edit manually + */ +import { config, ServiceId } from "@fluffylabs/jammin-sdk"; + +/** + * Type-safe service ID mappings + * Use these constants instead of hardcoding service IDs in tests + */ +export const SERVICES = { +${serviceEntries} +} as const; + +/** + * Chain specification for tests + */ +export const TEST_CHAIN_SPEC = config.tinyChainSpec; + +/** + * Get a typed service ID from the services map + * @param serviceName - Name of the service (autocompleted) + * @returns The service configuration with typed ID + */ +export function getService(serviceName: T) { + const service = SERVICES[serviceName]; + return { + id: ServiceId(service.id), + name: service.name, + }; +} + +/** + * Get all available service names for reference + */ +export function getServiceNames(): Array { + return Object.keys(SERVICES) as Array; +} + +/** + * Pre-configured TestJam instance with all services loaded in genesis state + */ +export const testJam = await TestJam.create(); +`; +} diff --git a/packages/jammin-sdk/utils/index.ts b/packages/jammin-sdk/utils/index.ts index 8aadb0d..1a359b6 100644 --- a/packages/jammin-sdk/utils/index.ts +++ b/packages/jammin-sdk/utils/index.ts @@ -1,4 +1,5 @@ export * from "./fetch-repo.js"; export * from "./file-utils.js"; export * from "./generate-service-output.js"; +export * from "./generate-test-config.js"; export * from "./get-service-configs.js"; From 12bed97260c3c7341faf05ef4df4c5a2c2e2207b Mon Sep 17 00:00:00 2001 From: HeyImStas Date: Tue, 10 Feb 2026 14:03:52 +0100 Subject: [PATCH 08/18] self review --- bin/cli/src/commands/build-command.ts | 1 - packages/jammin-sdk/index.ts | 44 ++++++++++++++++++++++++++- packages/jammin-sdk/simulator.ts | 39 +++++++++++------------- 3 files changed, 61 insertions(+), 23 deletions(-) diff --git a/bin/cli/src/commands/build-command.ts b/bin/cli/src/commands/build-command.ts index 6e41319..ae4342b 100644 --- a/bin/cli/src/commands/build-command.ts +++ b/bin/cli/src/commands/build-command.ts @@ -114,7 +114,6 @@ Examples: p.log.info("--------------------------------"); - // Generate test configuration file try { s.start("Generating test configuration..."); const services = await loadServices(projectRoot); diff --git a/packages/jammin-sdk/index.ts b/packages/jammin-sdk/index.ts index f4ecf12..04c06b2 100644 --- a/packages/jammin-sdk/index.ts +++ b/packages/jammin-sdk/index.ts @@ -1,5 +1,6 @@ +import { BytesBlob } from "@typeberry/lib/bytes"; + export * as block from "@typeberry/lib/block"; -export * as bytes from "@typeberry/lib/bytes"; export * as codec from "@typeberry/lib/codec"; export * as config from "@typeberry/lib/config"; export * as config_node from "@typeberry/lib/config-node"; @@ -19,3 +20,44 @@ export function getSDKInfo() { description: "JAM SDK for e2e integration tests and object encoding", }; } + +/** + * Create `BytesBlob` by converting given UTF-u encoded string into bytes. + * + * @example + * ```typescript + * const blob = StringToBytes("Hello, World!"); + * ``` + */ +export function StringToBytes(data: string): BytesBlob { + return BytesBlob.blobFromString(data); +} + +/** + * Create `BytesBlob` from an array of bytes. + * + * @example + * ```typescript + * const blob = NumbersToBytes([1, 2, 3]); + * ``` + */ +export function NumbersToBytes(data: number[]): BytesBlob { + return BytesBlob.blobFromNumbers(data); +} + +/** + * Create `BytesBlob` from hex-encoded bytes string. + * + * @example + * ```typescript + * const blob = HexToBytes("deadface"); + * // or + * const blob = HexToBytes("0x1337beef"); + * ``` + */ +export function HexToBytes(hex: string): BytesBlob { + if (!hex.startsWith("0x")) { + return BytesBlob.parseBlobNoPrefix(hex); + } + return BytesBlob.parseBlob(hex); +} diff --git a/packages/jammin-sdk/simulator.ts b/packages/jammin-sdk/simulator.ts index 016e031..0c41e20 100644 --- a/packages/jammin-sdk/simulator.ts +++ b/packages/jammin-sdk/simulator.ts @@ -43,7 +43,7 @@ export interface DebugLoggingOptions { pvmExecution?: boolean; /** - * Log ecalli traces for service execution. (Prettier HostCalls) + * Log ecalli traces for service execution. * Useful for debugging service interactions. */ ecalliTrace?: boolean; @@ -61,7 +61,7 @@ export interface DebugLoggingOptions { accumulate?: boolean; /** - * Log safrole (randomness and validator selection) operations. + * Log safrole operations. * Helps debug randomness and consensus-related issues. */ safrole?: boolean; @@ -91,7 +91,7 @@ export interface SimulatorOptions { /** * PVM backend for executing accumulation logic. - * Defaults to PvmBackend.Ananas (Assembly script interpreter). + * Defaults to Ananas (Assembly script interpreter). */ pvmBackend?: PvmBackend; @@ -117,7 +117,7 @@ export interface SimulatorOptions { * Enable debug logging for accumulation and PVM host calls. * When true, enables logging for all available debug categories. * Can also pass specific logging options for fine-grained control. - * Defaults to false. + * Defaults to true. * * @example * ```typescript @@ -125,7 +125,7 @@ export interface SimulatorOptions { * .withOptions({ debug: true }) * * // Enable only specific logs - * .withOptions({ debug: { ecalliTrace: true, stateTransitions: true } }) + * .withOptions({ debug: { hostCalls: true, stateTransitions: true } }) * ``` */ debug?: boolean | DebugLoggingOptions; @@ -230,12 +230,11 @@ export async function simulateAccumulation( const isSequential = options.sequential ?? true; const slot = options.slot ?? state.timeslot; const entropy = options.entropy ?? ZERO_HASH.asOpaque(); + const debug = options.debug ?? true; - // Configure logging if debug is enabled - if (options.debug) { - const loggingOptions = typeof options.debug === "boolean" ? undefined : options.debug; - await enableLogs(loggingOptions); - } + const loggingOptions: DebugLoggingOptions = + typeof debug === "boolean" ? (debug === true ? { refine: true, accumulate: true, hostCalls: true } : {}) : debug; + await enableLogs(loggingOptions); const blake2b = await Blake2b.createHasher(); @@ -259,33 +258,31 @@ export async function simulateAccumulation( return result.ok; } -async function enableLogs(options?: DebugLoggingOptions) { +async function enableLogs(options: DebugLoggingOptions) { try { const loggerModule = await import("@typeberry/lib/logger"); if (loggerModule.Logger && loggerModule.Level) { - // Build logging configuration based on enabled options - const enabledLoggers: string[] = ["info"]; + const enabledLoggers: string[] = ["error"]; - // Add loggers based on fine-grained options - if (options?.pvmExecution !== false) { + if (options.pvmExecution === true) { enabledLoggers.push("pvm=trace"); } - if (options?.ecalliTrace !== false) { + if (options.ecalliTrace === true) { enabledLoggers.push("ecalli=trace"); } - if (options?.hostCalls !== false) { + if (options.hostCalls === true) { enabledLoggers.push("host-calls=trace"); } - if (options?.refine !== false) { + if (options.refine === true) { enabledLoggers.push("refine=trace"); } - if (options?.accumulate !== false) { + if (options.accumulate === true) { enabledLoggers.push("accumulate=trace"); } - if (options?.safrole !== false) { + if (options.safrole === true) { enabledLoggers.push("safrole=trace"); } - if (options?.stateTransitions !== false) { + if (options.stateTransitions === true) { enabledLoggers.push("stf=trace"); } From c1b8699cd9b6ae9bb3e52176b304b834e6252393 Mon Sep 17 00:00:00 2001 From: HeyImStas Date: Tue, 10 Feb 2026 09:29:17 +0100 Subject: [PATCH 09/18] Implement PR 106 review feedback: auto-generated test config, rename accumulation() to accumulate(), fine-grained logging control, and documentation updates Addresses feedback from tomusdrw and skoszuta on PR 106: Phase 1: Code Generation Enhancement - Create generate-test-config.ts utility to generate TypeScript files with type-safe service ID mappings - Enhance build-command to automatically generate config/jammin.test.config.ts with service details and chainspec - Services now have a type-safe mapping instead of hardcoded values Phase 2: API Refinement - Rename accumulation() method to accumulate() for more concise verb-based API - Update all tests and documentation to use new method name - Improves code readability and aligns with reviewer preference Phase 3: Debug Logging Enhancement - Add DebugLoggingOptions interface for fine-grained control over logging categories - Support both boolean (enable all) and object (selective) debug modes - Available options: - pvmExecution: PVM execution details including instruction traces - ecalliTrace: Ecalli host call traces for service execution - hostCalls: Host calls during service execution - accumulate: Accumulation process details and state transitions - safrole: Randomness and validator selection operations - refine: Work report refinement and validation - stateTransitions: State transitions and root changes - Backward compatible with existing debug: true usage Phase 4: Documentation Updates - Add prerequisite section about building services before testing - Include custom assertions in basic test example - Document fine-grained logging control with examples - Update available options to reflect new debug capabilities - Emphasize use of generated test config for type safety All tests pass, quality checks pass, build succeeds. --- bin/cli/README.md | 1 + bin/cli/src/commands/build-command.ts | 21 ++- docs/src/testing.md | 157 +++++++++++++----- packages/jammin-sdk/simulator.test.ts | 12 +- packages/jammin-sdk/simulator.ts | 107 ++++++++++-- packages/jammin-sdk/testing-helpers.test.ts | 2 +- packages/jammin-sdk/testing-helpers.ts | 6 +- .../jammin-sdk/utils/generate-test-config.ts | 115 +++++++++++++ packages/jammin-sdk/utils/index.ts | 1 + 9 files changed, 359 insertions(+), 63 deletions(-) create mode 100644 packages/jammin-sdk/utils/generate-test-config.ts diff --git a/bin/cli/README.md b/bin/cli/README.md index d01c8ee..fea2f7e 100644 --- a/bin/cli/README.md +++ b/bin/cli/README.md @@ -83,6 +83,7 @@ jammin build auth-service --config ./custom.build.yml - Build logs are automatically saved to a `logs/` directory in your project root. - The command uses Docker to build services based on their SDK configuration. - Output `.jam` files are detected and listed after each successful build. +- Automatically generates `config/jammin.test.config.ts` with type-safe service ID mappings for testing. - If any service fails to build, the command will exit with an error code. ### `test` diff --git a/bin/cli/src/commands/build-command.ts b/bin/cli/src/commands/build-command.ts index e2fee22..6e41319 100644 --- a/bin/cli/src/commands/build-command.ts +++ b/bin/cli/src/commands/build-command.ts @@ -2,7 +2,14 @@ import { mkdir } from "node:fs/promises"; import { join, relative, resolve } from "node:path"; import * as p from "@clack/prompts"; import type { ServiceConfig } from "@fluffylabs/jammin-sdk"; -import { copyJamToDist, getJamFiles, getServiceConfigs, SDK_CONFIGS } from "@fluffylabs/jammin-sdk"; +import { + copyJamToDist, + generateTestConfigInProjectDir, + getJamFiles, + getServiceConfigs, + loadServices, + SDK_CONFIGS, +} from "@fluffylabs/jammin-sdk"; import { Command } from "commander"; /** @@ -107,6 +114,18 @@ Examples: p.log.info("--------------------------------"); + // Generate test configuration file + try { + s.start("Generating test configuration..."); + const services = await loadServices(projectRoot); + await generateTestConfigInProjectDir(services, projectRoot); + s.stop("✅ Test configuration generated"); + p.log.message("📝 Generated: config/jammin.test.config.ts"); + } catch (_error) { + s.stop("⚠️ Could not generate test configuration"); + p.log.warn("Test configuration generation failed (this is optional)"); + } + if (buildFailed) { p.outro("❌ Build failed. See the output above and check the logs for more details."); process.exit(1); diff --git a/docs/src/testing.md b/docs/src/testing.md index 3351374..ce113cc 100644 --- a/docs/src/testing.md +++ b/docs/src/testing.md @@ -8,6 +8,16 @@ The jammin SDK provides a comprehensive testing framework for simulating JAM acc ## Setup +### Prerequisites + +Before writing tests, ensure your services are built: + +```bash +jammin build +``` + +This generates service binaries and creates `config/jammin.test.config.ts` with type-safe service mappings for use in tests. + ### Installation The testing utilities are included in the `@fluffylabs/jammin-sdk` package: @@ -18,11 +28,17 @@ bun add -d @fluffylabs/jammin-sdk ### Basic Test Structure -Here's a minimal test using Bun's built-in test runner: +Here's a minimal test using Bun's built-in test runner with custom assertions: ```typescript import { test, expect } from "bun:test"; -import { TestJam, createWorkReportAsync, ServiceId, Gas } from "@fluffylabs/jammin-sdk"; +import { + TestJam, + createWorkReportAsync, + expectAccumulationSuccess, + ServiceId, + Gas, +} from "@fluffylabs/jammin-sdk"; test("should process work report", async () => { // Create test instance with loaded services @@ -34,10 +50,10 @@ test("should process work report", async () => { }); // Execute accumulation - const result = await jam.withWorkReport(report).accumulation(); + const result = await jam.withWorkReport(report).accumulate(); - // Verify results - expect(result).toBeDefined(); + // Use custom assertion helper + expectAccumulationSuccess(result); expect(result.accumulationStatistics.size).toBe(1); }); ``` @@ -66,6 +82,55 @@ const jam = TestJam.empty(); Useful for testing edge cases or when you don't need any services. +### Using Generated Service Configuration + +After building your project with `jammin build`, a type-safe configuration file is automatically generated at `config/jammin.test.config.ts`. This file provides convenient mappings and a pre-configured TestJam instance: + +#### Option 1: Use Pre-configured TestJam Instance + +The simplest approach - the generated config includes a ready-to-use TestJam instance: + +```typescript +import { testJam, SERVICES } from "./config/jammin.test.config.js"; + +test("should process work report for auth service", async () => { + // testJam is already created with all services loaded + const report = await createWorkReportAsync({ + results: [{ serviceId: ServiceId(SERVICES.auth.id), gas: Gas(1000n) }], + }); + + const result = await testJam.withWorkReport(report).accumulate(); + expectAccumulationSuccess(result); +}); +``` + +#### Option 2: Create Individual TestJam Instances + +For test isolation, create a new instance in each test: + +```typescript +import { SERVICES } from "./config/jammin.test.config.js"; + +test("should process work report for auth service", async () => { + const jam = await TestJam.create(); + + // Use the type-safe service mapping + const report = await createWorkReportAsync({ + results: [{ serviceId: ServiceId(SERVICES.auth.id), gas: Gas(1000n) }], + }); + + const result = await jam.withWorkReport(report).accumulate(); + expectAccumulationSuccess(result); +}); +``` + +The generated configuration includes: +- `testJam` - Pre-configured TestJam instance with all services loaded (use for simplicity) +- `SERVICES` - Type-safe mapping of service names to IDs +- `TEST_CHAIN_SPEC` - Pre-configured chain specification +- `getService(name)` - Helper function to get service configuration with typed ID +- `getServiceNames()` - Get list of all available service names + ### Adding Work Reports Work reports can be added using the fluent `withWorkReport()` API: @@ -78,7 +143,7 @@ const report = await createWorkReportAsync({ ], }); -const result = await jam.withWorkReport(report).accumulation(); +const result = await jam.withWorkReport(report).accumulate(); ``` #### Multiple Work Reports @@ -97,7 +162,7 @@ const report2 = await createWorkReportAsync({ const result = await jam .withWorkReport(report1) .withWorkReport(report2) - .accumulation(); + .accumulate(); console.log(`Processed ${result.accumulationStatistics.size} work items`); ``` @@ -117,13 +182,13 @@ const result = await jam pvmBackend: PvmBackend.BuiltIn, // Use built-in PVM sequential: true, // Sequential accumulation (default) }) - .accumulation(); + .accumulate(); ``` #### Available Options - `slot?: TimeSlot` - Time slot for accumulation (defaults to state's current timeslot) -- `debug?: boolean` - Enable debug logging for accumulation and PVM host calls +- `debug?: boolean | DebugLoggingOptions` - Enable debug logging (can be a boolean for all logs, or an object for fine-grained control) - `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) @@ -138,7 +203,7 @@ After accumulation, you can query the service state: ```typescript const info = jam.getServiceInfo(ServiceId(0)); console.log(`Balance: ${info?.balance}`); -console.log(`Gas used: ${info?.gasUsed}`); +console.log(`Code hash: ${info?.codeHash}`); ``` #### Get Service Storage @@ -244,7 +309,7 @@ Assert that accumulation completed with a valid structure: ```typescript import { expectAccumulationSuccess } from "@fluffylabs/jammin-sdk/testing-helpers"; -const result = await jam.withWorkReport(report).accumulation(); +const result = await jam.withWorkReport(report).accumulate(); expectAccumulationSuccess(result); // Throws if result is invalid ``` @@ -258,7 +323,7 @@ import { expectWorkItemCount } from "@fluffylabs/jammin-sdk/testing-helpers"; const result = await jam .withWorkReport(report1) .withWorkReport(report2) - .accumulation(); + .accumulate(); expectWorkItemCount(result, 2); // Throws if count doesn't match ``` @@ -272,7 +337,7 @@ import { expectStateChange } from "@fluffylabs/jammin-sdk/testing-helpers"; const beforeBalance = jam.getServiceInfo(ServiceId(0))?.balance; -await jam.withWorkReport(report).accumulation(); +await jam.withWorkReport(report).accumulate(); const afterBalance = jam.getServiceInfo(ServiceId(0))?.balance; @@ -292,7 +357,7 @@ Specialized helper for validating service account changes: import { expectServiceInfoChange } from "@fluffylabs/jammin-sdk/testing-helpers"; const before = jam.getServiceInfo(ServiceId(0)); -await jam.withWorkReport(report).accumulation(); +await jam.withWorkReport(report).accumulate(); const after = jam.getServiceInfo(ServiceId(0)); expectServiceInfoChange( @@ -319,7 +384,7 @@ test("service should execute successfully", async () => { results: [{ serviceId: ServiceId(0), gas: Gas(100000n) }], }); - const result = await jam.withWorkReport(report).accumulation(); + const result = await jam.withWorkReport(report).accumulate(); expectAccumulationSuccess(result); expect(result.accumulationStatistics.size).toBe(1); @@ -342,7 +407,7 @@ test("service storage should update", async () => { results: [{ serviceId: ServiceId(0), gas: Gas(50000n) }], }); - await jam.withWorkReport(report).accumulation(); + await jam.withWorkReport(report).accumulate(); const afterValue = jam.getServiceStorage(ServiceId(0), storageKey); expect(afterValue).not.toEqual(beforeValue); @@ -366,7 +431,7 @@ test("should process multiple services", async () => { ], }); - const result = await jam.withWorkReport(report).accumulation(); + const result = await jam.withWorkReport(report).accumulate(); expect(result.accumulationStatistics.size).toBe(3); }); @@ -391,7 +456,7 @@ test("should handle panic gracefully", async () => { ], }); - const result = await jam.withWorkReport(report).accumulation(); + const result = await jam.withWorkReport(report).accumulate(); // Accumulation should complete even with panicked work items expect(result).toBeDefined(); @@ -412,7 +477,7 @@ test("state should persist across accumulations", async () => { const report1 = await createWorkReportAsync({ results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], }); - await jam.withWorkReport(report1).accumulation(); + await jam.withWorkReport(report1).accumulate(); const midInfo = jam.getServiceInfo(ServiceId(0)); @@ -420,7 +485,7 @@ test("state should persist across accumulations", async () => { const report2 = await createWorkReportAsync({ results: [{ serviceId: ServiceId(0), gas: Gas(2000n) }], }); - await jam.withWorkReport(report2).accumulation(); + await jam.withWorkReport(report2).accumulate(); const finalInfo = jam.getServiceInfo(ServiceId(0)); @@ -464,23 +529,6 @@ If you see errors like "Service with id X not found", ensure that: 2. You're using `TestJam.create()` (not `TestJam.empty()`) 3. The service ID in your test matches the service ID in your configuration -### Import Errors - -Make sure you're importing from the correct module: - -```typescript -// Correct -import { TestJam, createWorkReportAsync } from "@fluffylabs/jammin-sdk"; -import { expectAccumulationSuccess } from "@fluffylabs/jammin-sdk/testing-helpers"; - -// Also correct (testing-helpers re-exports everything) -import { - TestJam, - createWorkReportAsync, - expectAccumulationSuccess -} from "@fluffylabs/jammin-sdk/testing-helpers"; -``` - ### Type Errors with Branded Types The SDK uses branded types for safety. Use the helper functions to create them: @@ -508,29 +556,54 @@ Enable debug logging to see what's happening during accumulation: const result = await jam .withWorkReport(report) .withOptions({ debug: true }) - .accumulation(); + .accumulate(); ``` This will output detailed logs including: - Accumulation steps - PVM host calls +#### Fine-Grained Logging Control + +You can enable only specific log categories for more focused debugging: + +```typescript +// Enable only ecalli (host call) traces +const result = await jam + .withWorkReport(report) + .withOptions({ + debug: { + ecalliTrace: true, + }, + }) + .accumulate(); +``` + +Available debug options: +- `pvmExecution` - PVM (Polkadot Virtual Machine) execution details including instruction traces and memory access patterns +- `ecalliTrace` - Ecalli (host call) traces for service execution and debugging service interactions +- `hostCalls` - Host calls made during service execution +- `accumulate` - Accumulation process details and state transitions showing how work items are processed +- `safrole` - Safrole (randomness and validator selection) operations for consensus-related debugging +- `refine` - Refinement process for work reports including validation and processing +- `stateTransitions` - State transitions and state root changes during processing + ### State Not Persisting -Remember that `accumulation()` automatically applies state updates. If you need to inspect state at different points: +Remember that `accumulate()` automatically applies state updates. If you need to inspect state at different points: ```typescript // Check initial state const initialInfo = jam.getServiceInfo(ServiceId(0)); // Run first accumulation (state is updated) -await jam.withWorkReport(report1).accumulation(); +await jam.withWorkReport(report1).accumulate(); // Check intermediate state const midInfo = jam.getServiceInfo(ServiceId(0)); // Run second accumulation (state is updated again) -await jam.withWorkReport(report2).accumulation(); +await jam.withWorkReport(report2).accumulate(); // Check final state const finalInfo = jam.getServiceInfo(ServiceId(0)); @@ -586,7 +659,7 @@ const customSpec = { const result = await jam .withWorkReport(report) .withOptions({ chainSpec: customSpec }) - .accumulation(); + .accumulate(); ``` ## Best Practices diff --git a/packages/jammin-sdk/simulator.test.ts b/packages/jammin-sdk/simulator.test.ts index 58beb72..92af325 100644 --- a/packages/jammin-sdk/simulator.test.ts +++ b/packages/jammin-sdk/simulator.test.ts @@ -16,7 +16,7 @@ describe("simulateAccumulation", () => { results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], }); - const result = await jam.withWorkReport(report).accumulation(); + const result = await jam.withWorkReport(report).accumulate(); expect(result).toBeDefined(); expect(result.stateUpdate).toBeDefined(); @@ -33,7 +33,7 @@ describe("simulateAccumulation", () => { results: [{ serviceId: ServiceId(1), gas: Gas(750n) }], }); - const result = await jam.withWorkReport(report1).withWorkReport(report2).accumulation(); + const result = await jam.withWorkReport(report1).withWorkReport(report2).accumulate(); expect(result.accumulationStatistics).toBeDefined(); expect(result.accumulationStatistics.size).toBe(2); @@ -48,14 +48,14 @@ describe("simulateAccumulation", () => { ], }); - const result = await jam.withWorkReport(report).accumulation(); + const result = await jam.withWorkReport(report).accumulate(); expect(result).toBeDefined(); expect(result.accumulationStatistics.size).toBe(3); }); test("handles empty reports array", async () => { - const result = await jam.accumulation(); + const result = await jam.accumulate(); expect(result).toBeDefined(); expect(result.stateUpdate).toBeDefined(); @@ -73,7 +73,7 @@ describe("simulateAccumulation", () => { .withOptions({ slot: customSlot, }) - .accumulation(); + .accumulate(); expect(result.stateUpdate.timeslot).toBeDefined(); // The timeslot in the update should be >= the slot we passed @@ -90,7 +90,7 @@ describe("simulateAccumulation", () => { .withOptions({ pvmBackend: config.PvmBackend.BuiltIn, }) - .accumulation(); + .accumulate(); expect(result).toBeDefined(); }); diff --git a/packages/jammin-sdk/simulator.ts b/packages/jammin-sdk/simulator.ts index b02892a..016e031 100644 --- a/packages/jammin-sdk/simulator.ts +++ b/packages/jammin-sdk/simulator.ts @@ -32,6 +32,53 @@ export interface GuaranteeOptions { slot?: TimeSlot; } +/** + * Fine-grained debug logging options + */ +export interface DebugLoggingOptions { + /** + * Log PVM (Polkadot Virtual Machine) execution details. + * Includes instruction traces, memory access patterns, etc. + */ + pvmExecution?: boolean; + + /** + * Log ecalli traces for service execution. (Prettier HostCalls) + * Useful for debugging service interactions. + */ + ecalliTrace?: boolean; + + /** + * Log host calls made during service execution. + * Useful for debugging service interactions. + */ + hostCalls?: boolean; + + /** + * Log accumulation process details and state transitions. + * Shows how work items are processed and state is updated. + */ + accumulate?: boolean; + + /** + * Log safrole (randomness and validator selection) operations. + * Helps debug randomness and consensus-related issues. + */ + safrole?: boolean; + + /** + * Log refinement process for work reports. + * Useful for understanding work report validation and processing. + */ + refine?: boolean; + + /** + * Log state transitions and state root changes. + * Helps understand how state evolves during processing. + */ + stateTransitions?: boolean; +} + /** * Configuration options for the accumulation simulator. */ @@ -68,9 +115,20 @@ export interface SimulatorOptions { /** * Enable debug logging for accumulation and PVM host calls. + * When true, enables logging for all available debug categories. + * Can also pass specific logging options for fine-grained control. * Defaults to false. + * + * @example + * ```typescript + * // Enable all debug logs + * .withOptions({ debug: true }) + * + * // Enable only specific logs + * .withOptions({ debug: { ecalliTrace: true, stateTransitions: true } }) + * ``` */ - debug?: boolean; + debug?: boolean | DebugLoggingOptions; } /** @@ -175,7 +233,8 @@ export async function simulateAccumulation( // Configure logging if debug is enabled if (options.debug) { - await enableLogs(); + const loggingOptions = typeof options.debug === "boolean" ? undefined : options.debug; + await enableLogs(loggingOptions); } const blake2b = await Blake2b.createHasher(); @@ -200,11 +259,39 @@ export async function simulateAccumulation( return result.ok; } -async function enableLogs() { +async function enableLogs(options?: DebugLoggingOptions) { try { const loggerModule = await import("@typeberry/lib/logger"); if (loggerModule.Logger && loggerModule.Level) { - loggerModule.Logger.configureAll("info,host-calls=trace,accumulate=trace", loggerModule.Level.LOG, process.cwd()); + // Build logging configuration based on enabled options + const enabledLoggers: string[] = ["info"]; + + // Add loggers based on fine-grained options + if (options?.pvmExecution !== false) { + enabledLoggers.push("pvm=trace"); + } + if (options?.ecalliTrace !== false) { + enabledLoggers.push("ecalli=trace"); + } + if (options?.hostCalls !== false) { + enabledLoggers.push("host-calls=trace"); + } + if (options?.refine !== false) { + enabledLoggers.push("refine=trace"); + } + if (options?.accumulate !== false) { + enabledLoggers.push("accumulate=trace"); + } + if (options?.safrole !== false) { + enabledLoggers.push("safrole=trace"); + } + if (options?.stateTransitions !== false) { + enabledLoggers.push("stf=trace"); + } + + const logConfig = enabledLoggers.join(","); + + loggerModule.Logger.configureAll(logConfig, loggerModule.Level.LOG, process.cwd()); } } catch { console.warn("Warning: Could not configure typeberry logger"); @@ -282,7 +369,7 @@ export class TestJam { /** * Configure simulator options for the next accumulation. - * Options persist across multiple accumulation() calls until changed. + * Options persist across multiple accumulate() calls until changed. * * @param options - Simulator configuration options * @returns This instance for method chaining @@ -292,7 +379,7 @@ export class TestJam { * const result = await jam * .withOptions({ debug: true, slot: Slot(100) }) * .withWorkReport(report) - * .accumulation(); + * .accumulate(); * ``` */ withOptions(options: SimulatorOptions): this { @@ -302,7 +389,7 @@ export class TestJam { /** * Add a work report to be processed in the next accumulation. - * Multiple work reports can be chained before calling accumulation(). + * Multiple work reports can be chained before calling accumulate(). * * @param report - Work report to process * @returns This instance for method chaining @@ -319,7 +406,7 @@ export class TestJam { * const result = await jam * .withWorkReport(report1) * .withWorkReport(report2) - * .accumulation(); + * .accumulate(); * ``` */ withWorkReport(report: WorkReport): this { @@ -336,11 +423,11 @@ export class TestJam { * * @example * ```typescript - * const result = await jam.withWorkReport(report).accumulation(); + * const result = await jam.withWorkReport(report).accumulate(); * console.log(`Processed ${result.accumulationStatistics.size} work items`); * ``` */ - async accumulation(): Promise { + async accumulate(): Promise { const result = await simulateAccumulation(this.state, this.workReports, this.options); this.workReports = []; if (this.state instanceof InMemoryState) { diff --git a/packages/jammin-sdk/testing-helpers.test.ts b/packages/jammin-sdk/testing-helpers.test.ts index b840525..58cfc45 100644 --- a/packages/jammin-sdk/testing-helpers.test.ts +++ b/packages/jammin-sdk/testing-helpers.test.ts @@ -41,7 +41,7 @@ describe("testing-helpers", () => { results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], }); - const result = await jam.withWorkReport(report).accumulation(); + const result = await jam.withWorkReport(report).accumulate(); // Should not throw expectAccumulationSuccess(result); diff --git a/packages/jammin-sdk/testing-helpers.ts b/packages/jammin-sdk/testing-helpers.ts index 1da2f7d..c59972b 100644 --- a/packages/jammin-sdk/testing-helpers.ts +++ b/packages/jammin-sdk/testing-helpers.ts @@ -55,7 +55,7 @@ export class StateChangeAssertionError extends Error { * const report = await createWorkReportAsync({ * results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], * }); - * const result = await jam.withWorkReport(report).accumulation(); + * const result = await jam.withWorkReport(report).accumulate(); * * // Assert accumulation completed * expectAccumulationSuccess(result); @@ -101,7 +101,7 @@ export type StateChangePredicate = (before: T, after: T) => boolean | undefin * const report = await createWorkReportAsync({ * results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], * }); - * await jam.withWorkReport(report).accumulation(); + * await jam.withWorkReport(report).accumulate(); * * const afterInfo = jam.getServiceInfo(ServiceId(0)); * @@ -145,7 +145,7 @@ export function expectStateChange( * const jam = await TestJam.create(); * const before = jam.getServiceInfo(ServiceId(0)); * - * await jam.withWorkReport(report).withOptions({ slot: Slot(100) }).accumulation(); + * await jam.withWorkReport(report).withOptions({ slot: Slot(100) }).accumulate(); * * const after = jam.getServiceInfo(ServiceId(0)); * diff --git a/packages/jammin-sdk/utils/generate-test-config.ts b/packages/jammin-sdk/utils/generate-test-config.ts new file mode 100644 index 0000000..9c8e3b4 --- /dev/null +++ b/packages/jammin-sdk/utils/generate-test-config.ts @@ -0,0 +1,115 @@ +import { resolve } from "node:path"; +import type { tinyChainSpec } from "@typeberry/lib/config"; +import { loadBuildConfig } from "../config/config-loader.js"; +import type { ServiceBuildOutput } from "./generate-service-output.js"; + +/** + * Generated test configuration with service mappings + */ +export interface GeneratedTestConfig { + services: Record; + chainSpec: typeof tinyChainSpec; +} + +/** + * Generate a TypeScript file with test configuration including: + * - Service ID mappings + * - ChainSpec configuration + * - Service names and IDs + * + * @param services - Array of service build outputs + * @param outputPath - Path where to write the generated test config file + */ +export async function generateTestConfigFile(services: ServiceBuildOutput[], outputPath: string): Promise { + // Create service mapping: service name -> { id, name } + const serviceMap: Record = {}; + + // Load the config to get service names + const config = await loadBuildConfig(); + + // Match services from config with their IDs from build outputs + for (let i = 0; i < config.services.length; i++) { + const service = config.services[i]; + const buildOutput = services[i]; + if (service && buildOutput) { + serviceMap[service.name] = { + id: buildOutput.id as unknown as number, + name: service.name, + }; + } + } + + // Generate TypeScript code + const tsCode = generateTestConfigCode(serviceMap); + + // Write to file + await Bun.write(outputPath, tsCode); +} + +/** + * Generate test config file in the project's config directory + * This is typically called after the build command completes + */ +export async function generateTestConfigInProjectDir( + services: ServiceBuildOutput[], + projectRoot: string = process.cwd(), +): Promise { + const configPath = resolve(projectRoot, "config", "jammin.test.config.ts"); + await generateTestConfigFile(services, configPath); + return configPath; +} + +/** + * Generate the TypeScript code for test configuration + */ +function generateTestConfigCode(serviceMap: Record): string { + const serviceEntries = Object.entries(serviceMap) + .map(([name, config]) => ` ${name}: { id: ${config.id}, name: "${config.name}" },`) + .join("\n"); + + return `/** + * Auto-generated test configuration + * This file is generated by the jammin build command + * Do not edit manually + */ +import { config, ServiceId } from "@fluffylabs/jammin-sdk"; + +/** + * Type-safe service ID mappings + * Use these constants instead of hardcoding service IDs in tests + */ +export const SERVICES = { +${serviceEntries} +} as const; + +/** + * Chain specification for tests + */ +export const TEST_CHAIN_SPEC = config.tinyChainSpec; + +/** + * Get a typed service ID from the services map + * @param serviceName - Name of the service (autocompleted) + * @returns The service configuration with typed ID + */ +export function getService(serviceName: T) { + const service = SERVICES[serviceName]; + return { + id: ServiceId(service.id), + name: service.name, + }; +} + +/** + * Get all available service names for reference + */ +export function getServiceNames(): Array { + return Object.keys(SERVICES) as Array; +} + +/** + * Pre-configured TestJam instance with all services loaded in genesis state + */ +export const testJam = await TestJam.create(); +`; +} diff --git a/packages/jammin-sdk/utils/index.ts b/packages/jammin-sdk/utils/index.ts index 8aadb0d..1a359b6 100644 --- a/packages/jammin-sdk/utils/index.ts +++ b/packages/jammin-sdk/utils/index.ts @@ -1,4 +1,5 @@ export * from "./fetch-repo.js"; export * from "./file-utils.js"; export * from "./generate-service-output.js"; +export * from "./generate-test-config.js"; export * from "./get-service-configs.js"; From c2ab020223ec19dae8897ba76afcb28e39207e47 Mon Sep 17 00:00:00 2001 From: HeyImStas Date: Tue, 10 Feb 2026 14:24:48 +0100 Subject: [PATCH 10/18] Add BytesBlob converter wrappers: stringToBlob, numbersToBlob, hexToBlob Create convenient wrapper functions for common BytesBlob conversions: - stringToBlob(): Convert UTF-8 strings to BytesBlob - numbersToBlob(): Convert number arrays to BytesBlob - hexToBlob(): Flexible hex parsing (with or without 0x prefix) - hexToBlobWithPrefix(): Explicit wrapper for parseBlob() - hexToBlobNoPrefix(): Explicit wrapper for parseBlobNoPrefix() These wrappers provide a cleaner API and reduce boilerplate in tests and service code. Export from jammin-sdk utils module for easy access. --- packages/jammin-sdk/utils/blob-converters.ts | 55 +++++++++++++++++++ .../jammin-sdk/utils/generate-test-config.ts | 22 +------- packages/jammin-sdk/utils/index.ts | 1 + 3 files changed, 57 insertions(+), 21 deletions(-) create mode 100644 packages/jammin-sdk/utils/blob-converters.ts diff --git a/packages/jammin-sdk/utils/blob-converters.ts b/packages/jammin-sdk/utils/blob-converters.ts new file mode 100644 index 0000000..aad318b --- /dev/null +++ b/packages/jammin-sdk/utils/blob-converters.ts @@ -0,0 +1,55 @@ +import { BytesBlob } from "@typeberry/lib/bytes"; + +/** + * Convert a UTF-8 string to a BytesBlob + * + * @param str - UTF-8 string to convert + * @returns BytesBlob representation of the string + * + * @example + * ```typescript + * import { stringToBlob } from "@fluffylabs/jammin-sdk"; + * const blob = stringToBlob("hello"); + * ``` + */ +export function stringToBlob(str: string): BytesBlob { + return BytesBlob.blobFromString(str); +} + +/** + * Convert an array of numbers to a BytesBlob + * + * @param numbers - Array of numbers (0-255) to convert + * @returns BytesBlob representation of the numbers + * + * @example + * ```typescript + * import { numbersToBlob } from "@fluffylabs/jammin-sdk"; + * const blob = numbersToBlob([1, 2, 3]); + * ``` + */ +export function numbersToBlob(numbers: number[]): BytesBlob { + return BytesBlob.blobFromNumbers(numbers); +} + +/** + * Parse a hex string (with or without 0x prefix) to a BytesBlob + * + * @param hex - Hex string to parse (can include 0x prefix) + * @returns BytesBlob representation of the hex data + * + * @example + * ```typescript + * import { hexToBlob } from "@fluffylabs/jammin-sdk"; + * const blob = hexToBlob("0xaabbccdd"); + * const blob2 = hexToBlob("aabbccdd"); // Also works without 0x prefix + * ``` + */ +export function hexToBlob(hex: string): BytesBlob { + // If hex has 0x prefix, use parseBlob (with prefix) + if (hex.startsWith("0x")) { + return BytesBlob.parseBlob(hex); + } + // Otherwise, use parseBlobNoPrefix (without prefix) + return BytesBlob.parseBlobNoPrefix(hex); +} diff --git a/packages/jammin-sdk/utils/generate-test-config.ts b/packages/jammin-sdk/utils/generate-test-config.ts index 9c8e3b4..769eb32 100644 --- a/packages/jammin-sdk/utils/generate-test-config.ts +++ b/packages/jammin-sdk/utils/generate-test-config.ts @@ -64,7 +64,7 @@ export async function generateTestConfigInProjectDir( */ function generateTestConfigCode(serviceMap: Record): string { const serviceEntries = Object.entries(serviceMap) - .map(([name, config]) => ` ${name}: { id: ${config.id}, name: "${config.name}" },`) + .map(([name, config]) => ` ${name}: { id: ServiceId(${config.id}), name: "${config.name}" },`) .join("\n"); return `/** @@ -87,26 +87,6 @@ ${serviceEntries} */ export const TEST_CHAIN_SPEC = config.tinyChainSpec; -/** - * Get a typed service ID from the services map - * @param serviceName - Name of the service (autocompleted) - * @returns The service configuration with typed ID - */ -export function getService(serviceName: T) { - const service = SERVICES[serviceName]; - return { - id: ServiceId(service.id), - name: service.name, - }; -} - -/** - * Get all available service names for reference - */ -export function getServiceNames(): Array { - return Object.keys(SERVICES) as Array; -} - /** * Pre-configured TestJam instance with all services loaded in genesis state */ diff --git a/packages/jammin-sdk/utils/index.ts b/packages/jammin-sdk/utils/index.ts index 1a359b6..2ddce57 100644 --- a/packages/jammin-sdk/utils/index.ts +++ b/packages/jammin-sdk/utils/index.ts @@ -1,3 +1,4 @@ +export * from "./blob-converters.js"; export * from "./fetch-repo.js"; export * from "./file-utils.js"; export * from "./generate-service-output.js"; From 67f6421230733c6aef68e25726e1e4e0dfcd1102 Mon Sep 17 00:00:00 2001 From: HeyImStas Date: Tue, 10 Feb 2026 14:48:50 +0100 Subject: [PATCH 11/18] clean --- packages/jammin-sdk/index.ts | 51 ------------------- .../jammin-sdk/utils/generate-test-config.ts | 4 +- 2 files changed, 2 insertions(+), 53 deletions(-) diff --git a/packages/jammin-sdk/index.ts b/packages/jammin-sdk/index.ts index 04c06b2..dd7c16e 100644 --- a/packages/jammin-sdk/index.ts +++ b/packages/jammin-sdk/index.ts @@ -1,5 +1,3 @@ -import { BytesBlob } from "@typeberry/lib/bytes"; - export * as block from "@typeberry/lib/block"; export * as codec from "@typeberry/lib/codec"; export * as config from "@typeberry/lib/config"; @@ -12,52 +10,3 @@ export * from "./genesis-state-generator.js"; export * from "./testing-helpers.js"; export * from "./types.js"; export * from "./utils/index.js"; - -export function getSDKInfo() { - return { - name: "@fluffylabs/jammin-sdk", - version: "0.0.2", - description: "JAM SDK for e2e integration tests and object encoding", - }; -} - -/** - * Create `BytesBlob` by converting given UTF-u encoded string into bytes. - * - * @example - * ```typescript - * const blob = StringToBytes("Hello, World!"); - * ``` - */ -export function StringToBytes(data: string): BytesBlob { - return BytesBlob.blobFromString(data); -} - -/** - * Create `BytesBlob` from an array of bytes. - * - * @example - * ```typescript - * const blob = NumbersToBytes([1, 2, 3]); - * ``` - */ -export function NumbersToBytes(data: number[]): BytesBlob { - return BytesBlob.blobFromNumbers(data); -} - -/** - * Create `BytesBlob` from hex-encoded bytes string. - * - * @example - * ```typescript - * const blob = HexToBytes("deadface"); - * // or - * const blob = HexToBytes("0x1337beef"); - * ``` - */ -export function HexToBytes(hex: string): BytesBlob { - if (!hex.startsWith("0x")) { - return BytesBlob.parseBlobNoPrefix(hex); - } - return BytesBlob.parseBlob(hex); -} diff --git a/packages/jammin-sdk/utils/generate-test-config.ts b/packages/jammin-sdk/utils/generate-test-config.ts index 769eb32..449e952 100644 --- a/packages/jammin-sdk/utils/generate-test-config.ts +++ b/packages/jammin-sdk/utils/generate-test-config.ts @@ -33,7 +33,7 @@ export async function generateTestConfigFile(services: ServiceBuildOutput[], out const buildOutput = services[i]; if (service && buildOutput) { serviceMap[service.name] = { - id: buildOutput.id as unknown as number, + id: buildOutput.id, name: service.name, }; } @@ -72,7 +72,7 @@ function generateTestConfigCode(serviceMap: Record Date: Tue, 10 Feb 2026 14:51:47 +0100 Subject: [PATCH 12/18] move genesis state generator to utils --- packages/jammin-sdk/index.ts | 1 - packages/jammin-sdk/simulator.ts | 3 +-- .../jammin-sdk/{ => utils}/genesis-state-generator.test.ts | 0 packages/jammin-sdk/{ => utils}/genesis-state-generator.ts | 0 packages/jammin-sdk/utils/index.ts | 1 + 5 files changed, 2 insertions(+), 3 deletions(-) rename packages/jammin-sdk/{ => utils}/genesis-state-generator.test.ts (100%) rename packages/jammin-sdk/{ => utils}/genesis-state-generator.ts (100%) diff --git a/packages/jammin-sdk/index.ts b/packages/jammin-sdk/index.ts index dd7c16e..726f656 100644 --- a/packages/jammin-sdk/index.ts +++ b/packages/jammin-sdk/index.ts @@ -6,7 +6,6 @@ export * as hash from "@typeberry/lib/hash"; export * as numbers from "@typeberry/lib/numbers"; export * as state_merkleization from "@typeberry/lib/state-merkleization"; export * from "./config/index.js"; -export * from "./genesis-state-generator.js"; export * from "./testing-helpers.js"; export * from "./types.js"; export * from "./utils/index.js"; diff --git a/packages/jammin-sdk/simulator.ts b/packages/jammin-sdk/simulator.ts index 0c41e20..c667404 100644 --- a/packages/jammin-sdk/simulator.ts +++ b/packages/jammin-sdk/simulator.ts @@ -16,9 +16,8 @@ import { type AccumulateState, } from "@typeberry/lib/transition"; import { asOpaqueType } from "@typeberry/lib/utils"; -import { generateState } from "./genesis-state-generator.js"; import { Slot } from "./types.js"; -import { loadServices } from "./utils/generate-service-output.js"; +import { loadServices, generateState } from "./utils/index.js"; import type { WorkReport } from "./work-report.js"; // Re-export types for convenience diff --git a/packages/jammin-sdk/genesis-state-generator.test.ts b/packages/jammin-sdk/utils/genesis-state-generator.test.ts similarity index 100% rename from packages/jammin-sdk/genesis-state-generator.test.ts rename to packages/jammin-sdk/utils/genesis-state-generator.test.ts diff --git a/packages/jammin-sdk/genesis-state-generator.ts b/packages/jammin-sdk/utils/genesis-state-generator.ts similarity index 100% rename from packages/jammin-sdk/genesis-state-generator.ts rename to packages/jammin-sdk/utils/genesis-state-generator.ts diff --git a/packages/jammin-sdk/utils/index.ts b/packages/jammin-sdk/utils/index.ts index 2ddce57..134f74a 100644 --- a/packages/jammin-sdk/utils/index.ts +++ b/packages/jammin-sdk/utils/index.ts @@ -3,4 +3,5 @@ export * from "./fetch-repo.js"; export * from "./file-utils.js"; export * from "./generate-service-output.js"; export * from "./generate-test-config.js"; +export * from "./genesis-state-generator.js"; export * from "./get-service-configs.js"; From 2110b38d0a12c197a08743c5bd2ca3e4e1939302 Mon Sep 17 00:00:00 2001 From: HeyImStas Date: Tue, 10 Feb 2026 15:18:07 +0100 Subject: [PATCH 13/18] update test docs --- docs/src/testing.md | 215 ++++++++++++++------------------------------ 1 file changed, 69 insertions(+), 146 deletions(-) diff --git a/docs/src/testing.md b/docs/src/testing.md index ce113cc..ebed1db 100644 --- a/docs/src/testing.md +++ b/docs/src/testing.md @@ -31,30 +31,27 @@ bun add -d @fluffylabs/jammin-sdk Here's a minimal test using Bun's built-in test runner with custom assertions: ```typescript -import { test, expect } from "bun:test"; +import { test } from "bun:test"; import { - TestJam, + CoreId, createWorkReportAsync, expectAccumulationSuccess, - ServiceId, Gas, + stringToBlob, } from "@fluffylabs/jammin-sdk"; +import { SERVICES, testJam } from "../config/jammin.test.config"; test("should process work report", async () => { - // Create test instance with loaded services - const jam = await TestJam.create(); - // Create a work report const report = await createWorkReportAsync({ - results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], + results: [{ serviceId: SERVICES.test.id, gas: Gas(1000n) }], }); // Execute accumulation - const result = await jam.withWorkReport(report).accumulate(); + const result = await testJam.withWorkReport(report).accumulate(); // Use custom assertion helper expectAccumulationSuccess(result); - expect(result.accumulationStatistics.size).toBe(1); }); ``` @@ -96,7 +93,7 @@ import { testJam, SERVICES } from "./config/jammin.test.config.js"; test("should process work report for auth service", async () => { // testJam is already created with all services loaded const report = await createWorkReportAsync({ - results: [{ serviceId: ServiceId(SERVICES.auth.id), gas: Gas(1000n) }], + results: [{ serviceId: SERVICES.auth.id, gas: Gas(1000n) }], }); const result = await testJam.withWorkReport(report).accumulate(); @@ -116,7 +113,7 @@ test("should process work report for auth service", async () => { // Use the type-safe service mapping const report = await createWorkReportAsync({ - results: [{ serviceId: ServiceId(SERVICES.auth.id), gas: Gas(1000n) }], + results: [{ serviceId: SERVICES.auth.id, gas: Gas(1000n) }], }); const result = await jam.withWorkReport(report).accumulate(); @@ -128,8 +125,6 @@ The generated configuration includes: - `testJam` - Pre-configured TestJam instance with all services loaded (use for simplicity) - `SERVICES` - Type-safe mapping of service names to IDs - `TEST_CHAIN_SPEC` - Pre-configured chain specification -- `getService(name)` - Helper function to get service configuration with typed ID -- `getServiceNames()` - Get list of all available service names ### Adding Work Reports @@ -138,8 +133,8 @@ Work reports can be added using the fluent `withWorkReport()` API: ```typescript const report = await createWorkReportAsync({ results: [ - { serviceId: ServiceId(0), gas: Gas(1000n) }, - { serviceId: ServiceId(1), gas: Gas(2000n) }, + { serviceId: SERVICES.auth.id, gas: Gas(1000n) }, + { serviceId: SERVICES.bank.id, gas: Gas(2000n) }, ], }); @@ -152,11 +147,11 @@ Chain multiple `withWorkReport()` calls to process multiple reports in a single ```typescript const report1 = await createWorkReportAsync({ - results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], + results: [{ serviceId: SERVICES.auth.id, gas: Gas(1000n) }], }); const report2 = await createWorkReportAsync({ - results: [{ serviceId: ServiceId(1), gas: Gas(2000n) }], + results: [{ serviceId: SERVICES.bank.id, gas: Gas(2000n) }], }); const result = await jam @@ -201,7 +196,7 @@ After accumulation, you can query the service state: #### Get Service Info ```typescript -const info = jam.getServiceInfo(ServiceId(0)); +const info = jam.getServiceInfo(SERVICES.auth.id); console.log(`Balance: ${info?.balance}`); console.log(`Code hash: ${info?.codeHash}`); ``` @@ -209,16 +204,16 @@ console.log(`Code hash: ${info?.codeHash}`); #### Get Service Storage ```typescript -import { BytesBlob } from "@fluffylabs/jammin-sdk/bytes"; +import { stringToBlob } from "@fluffylabs/jammin-sdk"; -const key = BytesBlob.blobFrom(new Uint8Array([1, 2, 3])); -const value = jam.getServiceStorage(ServiceId(0), key); +const key = stringToBlob("testKey"); +const value = jam.getServiceStorage(SERVICES.auth.id, key); ``` #### Get Preimage Data ```typescript -const preimage = jam.getServicePreimage(ServiceId(0), someHash); +const preimage = jam.getServicePreimage(SERVICES.auth.id, someHash); ``` ## Creating Work Reports @@ -228,11 +223,11 @@ The SDK provides flexible utilities for creating work reports with varying level ### Simple Work Report ```typescript -import { createWorkReportAsync, ServiceId, Gas } from "@fluffylabs/jammin-sdk"; +import { createWorkReportAsync, Gas } from "@fluffylabs/jammin-sdk"; const report = await createWorkReportAsync({ results: [ - { serviceId: ServiceId(0), gas: Gas(1000n) }, + { serviceId: SERVICES.auth.id, gas: Gas(1000n) }, ], }); ``` @@ -243,17 +238,17 @@ const report = await createWorkReportAsync({ const report = await createWorkReportAsync({ results: [ { - serviceId: ServiceId(0), + serviceId: SERVICES.auth.id, gas: Gas(1000n), - result: { type: "ok", output: BytesBlob.blobFrom(new Uint8Array([1, 2, 3])) } + result: { type: "ok", output: numbersToBlob([1, 2, 3]) } }, { - serviceId: ServiceId(1), + serviceId: SERVICES.auth.id, gas: Gas(2000n), result: { type: "ok" } }, { - serviceId: ServiceId(2), + serviceId: SERVICES.auth.id, gas: Gas(500n), result: { type: "panic" } // Simulate a panic }, @@ -270,9 +265,9 @@ const report = await createWorkReportAsync({ coreIndex: CoreId(5), results: [ { - serviceId: ServiceId(0), + serviceId: SERVICES.auth.id, gas: Gas(1000n), - payload: BytesBlob.blobFrom(new Uint8Array([4, 5, 6])), + payload: numbersToBlob([4, 5, 6]), }, ], context: { @@ -287,7 +282,7 @@ Work results can have different status types: ```typescript // Successful execution -{ type: "ok", output: BytesBlob.blobFrom(...) } +{ type: "ok", output: hexToBlob(...) } // Execution errors { type: "panic" } @@ -313,21 +308,6 @@ const result = await jam.withWorkReport(report).accumulate(); expectAccumulationSuccess(result); // Throws if result is invalid ``` -### expectWorkItemCount - -Assert that the expected number of work items were processed: - -```typescript -import { expectWorkItemCount } from "@fluffylabs/jammin-sdk/testing-helpers"; - -const result = await jam - .withWorkReport(report1) - .withWorkReport(report2) - .accumulate(); - -expectWorkItemCount(result, 2); // Throws if count doesn't match -``` - ### expectStateChange Assert that state changed according to a predicate: @@ -335,11 +315,11 @@ Assert that state changed according to a predicate: ```typescript import { expectStateChange } from "@fluffylabs/jammin-sdk/testing-helpers"; -const beforeBalance = jam.getServiceInfo(ServiceId(0))?.balance; +const beforeBalance = jam.getServiceInfo(SERVICES.auth.id)?.balance; await jam.withWorkReport(report).accumulate(); -const afterBalance = jam.getServiceInfo(ServiceId(0))?.balance; +const afterBalance = jam.getServiceInfo(SERVICES.auth.id)?.balance; expectStateChange( beforeBalance, @@ -356,14 +336,14 @@ Specialized helper for validating service account changes: ```typescript import { expectServiceInfoChange } from "@fluffylabs/jammin-sdk/testing-helpers"; -const before = jam.getServiceInfo(ServiceId(0)); +const before = jam.getServiceInfo(SERVICES.auth.id); await jam.withWorkReport(report).accumulate(); -const after = jam.getServiceInfo(ServiceId(0)); +const after = jam.getServiceInfo(SERVICES.auth.id); expectServiceInfoChange( before, after, - (b, a) => a && b && a.gasUsed > b.gasUsed, + (b, a) => a && b && a.balance > b.balance, "Service should consume gas" ); ``` @@ -374,20 +354,18 @@ expectServiceInfoChange( ```typescript import { test, expect } from "bun:test"; -import { TestJam, createWorkReportAsync, ServiceId, Gas } from "@fluffylabs/jammin-sdk"; +import {createWorkReportAsync, Gas } from "@fluffylabs/jammin-sdk"; import { expectAccumulationSuccess } from "@fluffylabs/jammin-sdk/testing-helpers"; +import { testJam, SERVICES } from "./config/jammin.test.config.js"; test("service should execute successfully", async () => { - const jam = await TestJam.create(); - const report = await createWorkReportAsync({ - results: [{ serviceId: ServiceId(0), gas: Gas(100000n) }], + results: [{ serviceId: SERVICES.auth.id, gas: Gas(100000n) }], }); - const result = await jam.withWorkReport(report).accumulate(); + const result = await testJam.withWorkReport(report).accumulate(); expectAccumulationSuccess(result); - expect(result.accumulationStatistics.size).toBe(1); }); ``` @@ -395,21 +373,22 @@ test("service should execute successfully", async () => { ```typescript import { test, expect } from "bun:test"; -import { TestJam, createWorkReportAsync, ServiceId, Gas, BytesBlob } from "@fluffylabs/jammin-sdk"; +import { createWorkReportAsync, Gas, stringToBlob } from "@fluffylabs/jammin-sdk"; +import { testJam as jam, SERVICES } from "./config/jammin.test.config.js"; test("service storage should update", async () => { - const jam = await TestJam.create(); + const authId = SERVICES.auth.id; - const storageKey = BytesBlob.blobFromString("myKey"); - const beforeValue = jam.getServiceStorage(ServiceId(0), storageKey); + const storageKey = stringToBlob("myKey"); + const beforeValue = jam.getServiceStorage(authId, storageKey); const report = await createWorkReportAsync({ - results: [{ serviceId: ServiceId(0), gas: Gas(50000n) }], + results: [{ serviceId: authId, gas: Gas(50000n) }], }); await jam.withWorkReport(report).accumulate(); - const afterValue = jam.getServiceStorage(ServiceId(0), storageKey); + const afterValue = jam.getServiceStorage(authId, storageKey); expect(afterValue).not.toEqual(beforeValue); }); ``` @@ -418,22 +397,22 @@ test("service storage should update", async () => { ```typescript import { test, expect } from "bun:test"; -import { TestJam, createWorkReportAsync, ServiceId, Gas } from "@fluffylabs/jammin-sdk"; +import { createWorkReportAsync, Gas, stringToBlob } from "@fluffylabs/jammin-sdk"; +import { expectAccumulationSuccess } from "@fluffylabs/jammin-sdk/testing-helpers"; +import { testJam as jam, SERVICES } from "./config/jammin.test.config.js"; test("should process multiple services", async () => { - const jam = await TestJam.create(); - const report = await createWorkReportAsync({ results: [ - { serviceId: ServiceId(0), gas: Gas(10000n) }, - { serviceId: ServiceId(1), gas: Gas(20000n) }, - { serviceId: ServiceId(2), gas: Gas(15000n) }, + { serviceId: SERVICES.auth.id, gas: Gas(10000n) }, + { serviceId: SERVICES.bank.id, gas: Gas(20000n) }, + { serviceId: SERVICES.exchange.id, gas: Gas(15000n) }, ], }); const result = await jam.withWorkReport(report).accumulate(); - expect(result.accumulationStatistics.size).toBe(3); + expectAccumulationSuccess(result); }); ``` @@ -441,11 +420,11 @@ test("should process multiple services", async () => { ```typescript import { test, expect } from "bun:test"; -import { TestJam, createWorkReportAsync, ServiceId, Gas } from "@fluffylabs/jammin-sdk"; +import { createWorkReportAsync, Gas, stringToBlob } from "@fluffylabs/jammin-sdk"; +import { expectAccumulationSuccess } from "@fluffylabs/jammin-sdk/testing-helpers"; +import { testJam as jam, SERVICES } from "./config/jammin.test.config.js"; test("should handle panic gracefully", async () => { - const jam = await TestJam.create(); - const report = await createWorkReportAsync({ results: [ { @@ -458,64 +437,7 @@ test("should handle panic gracefully", async () => { const result = await jam.withWorkReport(report).accumulate(); - // Accumulation should complete even with panicked work items - expect(result).toBeDefined(); - expect(result.accumulationStatistics.size).toBe(1); -}); -``` - -### Testing Sequential Accumulations - -```typescript -import { test, expect } from "bun:test"; -import { TestJam, createWorkReportAsync, ServiceId, Gas } from "@fluffylabs/jammin-sdk"; - -test("state should persist across accumulations", async () => { - const jam = await TestJam.create(); - - // First accumulation - const report1 = await createWorkReportAsync({ - results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], - }); - await jam.withWorkReport(report1).accumulate(); - - const midInfo = jam.getServiceInfo(ServiceId(0)); - - // Second accumulation - const report2 = await createWorkReportAsync({ - results: [{ serviceId: ServiceId(0), gas: Gas(2000n) }], - }); - await jam.withWorkReport(report2).accumulate(); - - const finalInfo = jam.getServiceInfo(ServiceId(0)); - - // State should have accumulated from both operations - expect(finalInfo).toBeDefined(); - expect(midInfo).toBeDefined(); -}); -``` - -### Testing with Guarantees - -```typescript -import { test, expect } from "bun:test"; -import { TestJam, createWorkReportAsync, generateGuarantees, ServiceId, Gas, Slot } from "@fluffylabs/jammin-sdk"; - -test("should generate valid guarantees", async () => { - const jam = await TestJam.create(); - - const report = await createWorkReportAsync({ - results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], - }); - - // Generate guarantees with validator signatures - const guarantees = await generateGuarantees([report], { - slot: Slot(100), - }); - - expect(guarantees).toHaveLength(1); - expect(guarantees[0]?.credentials.length).toBe(3); // Default 3 validators - expect(Number(guarantees[0]?.slot)).toBe(100); + expectAccumulationSuccess(result); }); ``` @@ -526,8 +448,8 @@ test("should generate valid guarantees", async () => { If you see errors like "Service with id X not found", ensure that: 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 +2. You've run `jammin build` command +3. You're using `SERVICES` from generated `config/jammin.test.config.js`; ### Type Errors with Branded Types @@ -568,7 +490,7 @@ This will output detailed logs including: You can enable only specific log categories for more focused debugging: ```typescript -// Enable only ecalli (host call) traces +// Enable only ecalli traces const result = await jam .withWorkReport(report) .withOptions({ @@ -594,19 +516,19 @@ Remember that `accumulate()` automatically applies state updates. If you need to ```typescript // Check initial state -const initialInfo = jam.getServiceInfo(ServiceId(0)); +const initialInfo = jam.getServiceInfo(SERVICES.auth.id); // Run first accumulation (state is updated) await jam.withWorkReport(report1).accumulate(); // Check intermediate state -const midInfo = jam.getServiceInfo(ServiceId(0)); +const midInfo = jam.getServiceInfo(SERVICES.auth.id); // Run second accumulation (state is updated again) await jam.withWorkReport(report2).accumulate(); // Check final state -const finalInfo = jam.getServiceInfo(ServiceId(0)); +const finalInfo = jam.getServiceInfo(SERVICES.auth.id); ``` ## Advanced Usage @@ -622,7 +544,7 @@ import { createWorkReport } from "@fluffylabs/jammin-sdk"; const blake2b = await Blake2b.createHasher(); const report = createWorkReport(blake2b, { - results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], + results: [{ serviceId: SERVICES.auth.id, gas: Gas(1000n) }], }); ``` @@ -664,14 +586,15 @@ const result = await jam ## Best Practices -1. **Use `TestJam.create()` by default** - It automatically loads your services -2. **Chain method calls** - The fluent API makes tests more readable -3. **Use helper assertions** - They provide better error messages than raw `expect()` -4. **Test state changes explicitly** - Don't assume accumulation modified state -5. **Use branded types** - They prevent common mistakes with raw numbers -6. **Enable debug logging when troubleshooting** - It shows exactly what's happening -7. **Test both success and failure cases** - Include tests for panics and out-of-gas scenarios -8. **Keep tests isolated** - Create a new `TestJam` instance for each test +1. **Use `testJam` by default** - It automatically loads your services +2. **Use preconfigured `SERVICES` instead of manually writing service ids** - It prevents changing tests when reasigning + service ids +3. **Chain method calls** - The fluent API makes tests more readable +4. **Use helper assertions** - They provide better error messages than raw `expect()` +5. **Test state changes explicitly** - Don't assume accumulation modified state +6. **Use branded types** - They prevent common mistakes with raw numbers +7. **Enable debug logging when troubleshooting** - It shows exactly what's happening +8. **Test both success and failure cases** - Include tests for panics and out-of-gas scenarios ## Next Steps From 655c0bff69e283a01a25f4c2c68f70b6a27fbcd5 Mon Sep 17 00:00:00 2001 From: HeyImStas Date: Tue, 10 Feb 2026 15:26:33 +0100 Subject: [PATCH 14/18] qa-fix --- packages/jammin-sdk/simulator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jammin-sdk/simulator.ts b/packages/jammin-sdk/simulator.ts index c667404..4fb6cdc 100644 --- a/packages/jammin-sdk/simulator.ts +++ b/packages/jammin-sdk/simulator.ts @@ -17,7 +17,7 @@ import { } from "@typeberry/lib/transition"; import { asOpaqueType } from "@typeberry/lib/utils"; import { Slot } from "./types.js"; -import { loadServices, generateState } from "./utils/index.js"; +import { generateState, loadServices } from "./utils/index.js"; import type { WorkReport } from "./work-report.js"; // Re-export types for convenience From 270dd95fa20df10334ee855735094191ac0e4c7f Mon Sep 17 00:00:00 2001 From: HeyImStas Date: Tue, 10 Feb 2026 17:26:22 +0100 Subject: [PATCH 15/18] fix paths --- packages/jammin-sdk/utils/genesis-state-generator.test.ts | 4 ++-- packages/jammin-sdk/utils/genesis-state-generator.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/jammin-sdk/utils/genesis-state-generator.test.ts b/packages/jammin-sdk/utils/genesis-state-generator.test.ts index 7dfc034..4fe455a 100644 --- a/packages/jammin-sdk/utils/genesis-state-generator.test.ts +++ b/packages/jammin-sdk/utils/genesis-state-generator.test.ts @@ -4,9 +4,9 @@ import { HashDictionary } from "@typeberry/lib/collections"; import { Blake2b, HASH_SIZE } from "@typeberry/lib/hash"; import { type StorageKey, tryAsLookupHistorySlots } from "@typeberry/lib/state"; import { asOpaqueType } from "@typeberry/lib/utils"; +import { Gas, ServiceId, Slot, U32, U64 } from "../types"; +import type { ServiceBuildOutput } from "./generate-service-output"; import { generateGenesis, generateState, toJip4Schema } from "./genesis-state-generator"; -import { Gas, ServiceId, Slot, U32, U64 } from "./types"; -import type { ServiceBuildOutput } from "./utils/generate-service-output"; const blake2b = await Blake2b.createHasher(); diff --git a/packages/jammin-sdk/utils/genesis-state-generator.ts b/packages/jammin-sdk/utils/genesis-state-generator.ts index 20ad724..325d354 100644 --- a/packages/jammin-sdk/utils/genesis-state-generator.ts +++ b/packages/jammin-sdk/utils/genesis-state-generator.ts @@ -21,8 +21,8 @@ import { } from "@typeberry/lib/state"; import { StateEntries } from "@typeberry/lib/state-merkleization"; import { asOpaqueType } from "@typeberry/lib/utils"; -import { Gas, ServiceId, Slot, U32, U64 } from "./types.js"; -import type { ServiceBuildOutput } from "./utils/generate-service-output.js"; +import { Gas, ServiceId, Slot, U32, U64 } from "../types.js"; +import type { ServiceBuildOutput } from "./generate-service-output.js"; const blake2b = await Blake2b.createHasher(); const spec = tinyChainSpec; From 4ec59bbee4e8ce880cf158bb47473ef8ad057373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Soko=C5=82owski?= Date: Tue, 17 Feb 2026 08:28:27 +0100 Subject: [PATCH 16/18] Update packages/jammin-sdk/simulator.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tomek Drwięga --- packages/jammin-sdk/simulator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jammin-sdk/simulator.ts b/packages/jammin-sdk/simulator.ts index 4fb6cdc..008455b 100644 --- a/packages/jammin-sdk/simulator.ts +++ b/packages/jammin-sdk/simulator.ts @@ -308,7 +308,7 @@ async function enableLogs(options: DebugLoggingOptions) { * const report = await createWorkReportAsync({ * results: [{ serviceId: ServiceId(0), gas: Gas(1000n) }], * }); - * const result = await jam.withWorkReport(report).accumulation(); + * const result = await jam.withWorkReport(report).accumulate(); * * // Chain multiple work reports * const result2 = await jam From 6896b8644fceea7be471b69e90892f298253a8ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Soko=C5=82owski?= Date: Tue, 17 Feb 2026 08:28:42 +0100 Subject: [PATCH 17/18] Update packages/jammin-sdk/simulator.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tomek Drwięga --- packages/jammin-sdk/simulator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jammin-sdk/simulator.ts b/packages/jammin-sdk/simulator.ts index 008455b..3bbbc68 100644 --- a/packages/jammin-sdk/simulator.ts +++ b/packages/jammin-sdk/simulator.ts @@ -315,7 +315,7 @@ async function enableLogs(options: DebugLoggingOptions) { * .withWorkReport(report1) * .withWorkReport(report2) * .withOptions({ debug: true }) - * .accumulation(); + * .accumulate(); * * // Query service state after accumulation * const serviceInfo = jam.getServiceInfo(ServiceId(0)); From 9698fc65071c90c412b377e512cb7459ac7fc418 Mon Sep 17 00:00:00 2001 From: HeyImStas Date: Thu, 19 Feb 2026 09:43:47 +0100 Subject: [PATCH 18/18] extract hardcoded strings, moved blob utils under TestJam, add test for generated config, update typeberry/lib to 0.5.7, remove unnececerry workreport check --- bun.lock | 16 +++-- docs/src/testing.md | 22 +++--- packages/jammin-sdk/package.json | 2 +- packages/jammin-sdk/simulator.ts | 57 +++++++++++++-- packages/jammin-sdk/testing-helpers.test.ts | 7 +- packages/jammin-sdk/utils/blob-converters.ts | 55 -------------- .../utils/generate-test-config.test.ts | 72 +++++++++++++++++++ .../jammin-sdk/utils/generate-test-config.ts | 15 +++- packages/jammin-sdk/utils/index.ts | 1 - packages/jammin-sdk/work-report.test.ts | 21 ++---- packages/jammin-sdk/work-report.ts | 10 --- 11 files changed, 166 insertions(+), 112 deletions(-) delete mode 100644 packages/jammin-sdk/utils/blob-converters.ts create mode 100644 packages/jammin-sdk/utils/generate-test-config.test.ts diff --git a/bun.lock b/bun.lock index d94eb66..466f497 100644 --- a/bun.lock +++ b/bun.lock @@ -38,7 +38,7 @@ "name": "@fluffylabs/jammin-sdk", "version": "0.1.0", "dependencies": { - "@typeberry/lib": "0.5.4-e1cdb43", + "@typeberry/lib": "0.5.7", "yaml": "^2.8.2", "zod": "^4.3.6", }, @@ -82,11 +82,17 @@ "@noble/ed25519": ["@noble/ed25519@2.2.3", "", {}, "sha512-iHV8eI2mRcUmOx159QNrU8vTpQ/Xm70yJ2cTk3Trc86++02usfqFoNl6x0p3JN81ZDS/1gx6xiK0OwrgqCT43g=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "@tsconfig/node20": ["@tsconfig/node20@20.1.8", "", {}, "sha512-Em+IdPfByIzWRRpqWL4Z7ArLHZGxmc36BxE3jCz9nBFSm+5aLaPMZyjwu4yetvyKXeogWcxik4L1jB5JTWfw7A=="], - "@typeberry/lib": ["@typeberry/lib@0.5.4-e1cdb43", "", { "dependencies": { "@fluffylabs/anan-as": "^1.1.5", "@noble/ed25519": "2.2.3", "@typeberry/native": "0.0.4-4c0cd28", "hash-wasm": "4.12.0" } }, "sha512-gyUWD2j/SOFMnOTmfxIdlbRFdO5guwQrWB/2jBBf8rVu+Lys4M0jfqmralL16EIzbiapfwMbE+yrHfs8040ZkA=="], + "@typeberry/bandersnatch-native-darwin-arm64": ["@typeberry/bandersnatch-native-darwin-arm64@0.2.0-74dd7d7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eRzvfdoEDeRIHhMzzCX/0rPVlrHMIH0Ja0FkA/OC3vr9XkaJXp9DW3HmOHbPZPdt3T2ySwqrv9P3NxUHIIvUIg=="], + + "@typeberry/bandersnatch-native-linux-x64-gnu": ["@typeberry/bandersnatch-native-linux-x64-gnu@0.2.0-74dd7d7", "", { "os": "linux", "cpu": "x64" }, "sha512-kPTDJ6YYPghxMubhxMsn9wBea/aH9/hIaiG66PW7skRwDM9D5wNzZrmSD7BlpSKv9ayK2hIQiSL1amCiTS9mXQ=="], + + "@typeberry/lib": ["@typeberry/lib@0.5.7", "", { "dependencies": { "@fluffylabs/anan-as": "^1.1.5", "@noble/ed25519": "2.2.3", "@opentelemetry/api": "1.9.0", "@typeberry/native": "0.2.0-74dd7d7", "eventemitter3": "^5.0.1", "hash-wasm": "4.12.0" } }, "sha512-q5g8jWeAeGBwh4Ywdjtc9R8R5xIkChD5uIpjvG8tEFIUGyN0/rf+e4wbrB71dRxJEojKK4ZPYt3gvuk9eWQI4w=="], - "@typeberry/native": ["@typeberry/native@0.0.4-4c0cd28", "", {}, "sha512-VhZiiSYex3/jDk1I8PlcwPxiM9GslryGxdG+4sbbjNvpr1JxRkH0fAdUnKzxAxGYPNz2MOfwHTmTdVcrk1a5rA=="], + "@typeberry/native": ["@typeberry/native@0.2.0-74dd7d7", "", { "optionalDependencies": { "@typeberry/bandersnatch-native-darwin-arm64": "0.2.0-74dd7d7", "@typeberry/bandersnatch-native-linux-x64-gnu": "0.2.0-74dd7d7" } }, "sha512-b7qq7cIO30KKUVe2WISd+WmipurlzVePz7zFWFkZ2g8nqvfAt8l3upANC1Yxg+LPFLQLWUbEJ7vZ0reOX68Vqg=="], "@types/blake2b": ["@types/blake2b@2.1.3", "", {}, "sha512-MFCdX0MNxFBP/xEILO5Td0kv6nI7+Q2iRWZbTL/yzH2/eDVZS5Wd1LHdsmXClvsCyzqaZfHFzZaN6BUeUCfSDA=="], @@ -114,8 +120,8 @@ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "@fluffylabs/jammin-sdk/@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], + "@fluffylabs/jammin-sdk/@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], - "@fluffylabs/jammin-sdk/@types/bun/bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], + "@fluffylabs/jammin-sdk/@types/bun/bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], } } diff --git a/docs/src/testing.md b/docs/src/testing.md index ebed1db..efa6d83 100644 --- a/docs/src/testing.md +++ b/docs/src/testing.md @@ -37,7 +37,7 @@ import { createWorkReportAsync, expectAccumulationSuccess, Gas, - stringToBlob, + TestJam, } from "@fluffylabs/jammin-sdk"; import { SERVICES, testJam } from "../config/jammin.test.config"; @@ -204,9 +204,7 @@ console.log(`Code hash: ${info?.codeHash}`); #### Get Service Storage ```typescript -import { stringToBlob } from "@fluffylabs/jammin-sdk"; - -const key = stringToBlob("testKey"); +const key = TestJam.stringToBlob("testKey"); const value = jam.getServiceStorage(SERVICES.auth.id, key); ``` @@ -240,7 +238,7 @@ const report = await createWorkReportAsync({ { serviceId: SERVICES.auth.id, gas: Gas(1000n), - result: { type: "ok", output: numbersToBlob([1, 2, 3]) } + result: { type: "ok", output: TestJam.numbersToBlob([1, 2, 3]) } }, { serviceId: SERVICES.auth.id, @@ -267,7 +265,7 @@ const report = await createWorkReportAsync({ { serviceId: SERVICES.auth.id, gas: Gas(1000n), - payload: numbersToBlob([4, 5, 6]), + payload: TestJam.numbersToBlob([4, 5, 6]), }, ], context: { @@ -282,7 +280,7 @@ Work results can have different status types: ```typescript // Successful execution -{ type: "ok", output: hexToBlob(...) } +{ type: "ok", output: TestJam.hexToBlob("0xaabbccdd") } // Execution errors { type: "panic" } @@ -373,13 +371,13 @@ test("service should execute successfully", async () => { ```typescript import { test, expect } from "bun:test"; -import { createWorkReportAsync, Gas, stringToBlob } from "@fluffylabs/jammin-sdk"; +import { createWorkReportAsync, Gas, TestJam } from "@fluffylabs/jammin-sdk"; import { testJam as jam, SERVICES } from "./config/jammin.test.config.js"; test("service storage should update", async () => { const authId = SERVICES.auth.id; - const storageKey = stringToBlob("myKey"); + const storageKey = TestJam.stringToBlob("myKey"); const beforeValue = jam.getServiceStorage(authId, storageKey); const report = await createWorkReportAsync({ @@ -397,7 +395,7 @@ test("service storage should update", async () => { ```typescript import { test, expect } from "bun:test"; -import { createWorkReportAsync, Gas, stringToBlob } from "@fluffylabs/jammin-sdk"; +import { createWorkReportAsync, Gas } from "@fluffylabs/jammin-sdk"; import { expectAccumulationSuccess } from "@fluffylabs/jammin-sdk/testing-helpers"; import { testJam as jam, SERVICES } from "./config/jammin.test.config.js"; @@ -420,7 +418,7 @@ test("should process multiple services", async () => { ```typescript import { test, expect } from "bun:test"; -import { createWorkReportAsync, Gas, stringToBlob } from "@fluffylabs/jammin-sdk"; +import { createWorkReportAsync, Gas } from "@fluffylabs/jammin-sdk"; import { expectAccumulationSuccess } from "@fluffylabs/jammin-sdk/testing-helpers"; import { testJam as jam, SERVICES } from "./config/jammin.test.config.js"; @@ -587,7 +585,7 @@ const result = await jam ## Best Practices 1. **Use `testJam` by default** - It automatically loads your services -2. **Use preconfigured `SERVICES` instead of manually writing service ids** - It prevents changing tests when reasigning +2. **Use preconfigured `SERVICES` instead of manually writing service ids** - It prevents changing tests when reassigning service ids 3. **Chain method calls** - The fluent API makes tests more readable 4. **Use helper assertions** - They provide better error messages than raw `expect()` diff --git a/packages/jammin-sdk/package.json b/packages/jammin-sdk/package.json index 3c774b1..f0e3c75 100644 --- a/packages/jammin-sdk/package.json +++ b/packages/jammin-sdk/package.json @@ -30,7 +30,7 @@ "type": "module", "types": "./dist/index.d.ts", "dependencies": { - "@typeberry/lib": "0.5.4-e1cdb43", + "@typeberry/lib": "0.5.7", "yaml": "^2.8.2", "zod": "^4.3.6" } diff --git a/packages/jammin-sdk/simulator.ts b/packages/jammin-sdk/simulator.ts index 3bbbc68..85802af 100644 --- a/packages/jammin-sdk/simulator.ts +++ b/packages/jammin-sdk/simulator.ts @@ -1,6 +1,6 @@ import * as jamBlock from "@typeberry/lib/block"; import { Credential, type EntropyHash, ReportGuarantee, type TimeSlot } from "@typeberry/lib/block"; -import type { BytesBlob } from "@typeberry/lib/bytes"; +import { BytesBlob } from "@typeberry/lib/bytes"; import { Encoder } from "@typeberry/lib/codec"; import { asKnownSize } from "@typeberry/lib/collections"; import { type ChainSpec, PvmBackend, tinyChainSpec } from "@typeberry/lib/config"; @@ -363,6 +363,55 @@ export class TestJam { return new TestJam(state); } + /** + * Convert a UTF-8 string to a BytesBlob. + * + * @param str - UTF-8 string to convert + * @returns BytesBlob representation of the string + * + * @example + * ```typescript + * const blob = TestJam.stringToBlob("hello"); + * ``` + */ + static stringToBlob(str: string): BytesBlob { + return BytesBlob.blobFromString(str); + } + + /** + * Convert an array of numbers to a BytesBlob. + * + * @param numbers - Array of numbers (0-255) to convert + * @returns BytesBlob representation of the numbers + * + * @example + * ```typescript + * const blob = TestJam.numbersToBlob([1, 2, 3]); + * ``` + */ + static numbersToBlob(numbers: number[]): BytesBlob { + return BytesBlob.blobFromNumbers(numbers); + } + + /** + * Parse a hex string (with or without 0x prefix) to a BytesBlob. + * + * @param hex - Hex string to parse (can include 0x prefix) + * @returns BytesBlob representation of the hex data + * + * @example + * ```typescript + * const blob = TestJam.hexToBlob("0xaabbccdd"); + * const blob2 = TestJam.hexToBlob("aabbccdd"); + * ``` + */ + static hexToBlob(hex: string): BytesBlob { + if (hex.startsWith("0x")) { + return BytesBlob.parseBlob(hex); + } + return BytesBlob.parseBlobNoPrefix(hex); + } + /** * Configure simulator options for the next accumulation. * Options persist across multiple accumulate() calls until changed. @@ -432,10 +481,8 @@ export class TestJam { 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; } diff --git a/packages/jammin-sdk/testing-helpers.test.ts b/packages/jammin-sdk/testing-helpers.test.ts index 58cfc45..d7702c3 100644 --- a/packages/jammin-sdk/testing-helpers.test.ts +++ b/packages/jammin-sdk/testing-helpers.test.ts @@ -65,12 +65,9 @@ describe("testing-helpers", () => { }); test("should include custom error message", () => { - try { + expect(() => { expectStateChange(10, 5, (before, after) => after > before, "Custom error"); - } catch (e) { - expect(e).toBeInstanceOf(StateChangeAssertionError); - expect((e as Error).message).toBe("Custom error"); - } + }).toThrow("Custom error"); }); }); diff --git a/packages/jammin-sdk/utils/blob-converters.ts b/packages/jammin-sdk/utils/blob-converters.ts deleted file mode 100644 index aad318b..0000000 --- a/packages/jammin-sdk/utils/blob-converters.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { BytesBlob } from "@typeberry/lib/bytes"; - -/** - * Convert a UTF-8 string to a BytesBlob - * - * @param str - UTF-8 string to convert - * @returns BytesBlob representation of the string - * - * @example - * ```typescript - * import { stringToBlob } from "@fluffylabs/jammin-sdk"; - * const blob = stringToBlob("hello"); - * ``` - */ -export function stringToBlob(str: string): BytesBlob { - return BytesBlob.blobFromString(str); -} - -/** - * Convert an array of numbers to a BytesBlob - * - * @param numbers - Array of numbers (0-255) to convert - * @returns BytesBlob representation of the numbers - * - * @example - * ```typescript - * import { numbersToBlob } from "@fluffylabs/jammin-sdk"; - * const blob = numbersToBlob([1, 2, 3]); - * ``` - */ -export function numbersToBlob(numbers: number[]): BytesBlob { - return BytesBlob.blobFromNumbers(numbers); -} - -/** - * Parse a hex string (with or without 0x prefix) to a BytesBlob - * - * @param hex - Hex string to parse (can include 0x prefix) - * @returns BytesBlob representation of the hex data - * - * @example - * ```typescript - * import { hexToBlob } from "@fluffylabs/jammin-sdk"; - * const blob = hexToBlob("0xaabbccdd"); - * const blob2 = hexToBlob("aabbccdd"); // Also works without 0x prefix - * ``` - */ -export function hexToBlob(hex: string): BytesBlob { - // If hex has 0x prefix, use parseBlob (with prefix) - if (hex.startsWith("0x")) { - return BytesBlob.parseBlob(hex); - } - // Otherwise, use parseBlobNoPrefix (without prefix) - return BytesBlob.parseBlobNoPrefix(hex); -} diff --git a/packages/jammin-sdk/utils/generate-test-config.test.ts b/packages/jammin-sdk/utils/generate-test-config.test.ts new file mode 100644 index 0000000..c88b6a8 --- /dev/null +++ b/packages/jammin-sdk/utils/generate-test-config.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, test } from "bun:test"; +import { generateTestConfigCode } from "./generate-test-config.js"; + +describe("generateTestConfigCode", () => { + test("generates valid TypeScript for empty service map", () => { + const code = generateTestConfigCode({}); + + // Should parse without throwing + const transpiler = new Bun.Transpiler({ loader: "ts" }); + expect(() => transpiler.transformSync(code)).not.toThrow(); + }); + + test("generates valid TypeScript for single service", () => { + const code = generateTestConfigCode({ + auth: { id: 0, name: "auth" }, + }); + + const transpiler = new Bun.Transpiler({ loader: "ts" }); + expect(() => transpiler.transformSync(code)).not.toThrow(); + }); + + test("generates valid TypeScript for multiple services", () => { + const code = generateTestConfigCode({ + auth: { id: 0, name: "auth" }, + bank: { id: 1, name: "bank" }, + exchange: { id: 2, name: "exchange" }, + }); + + const transpiler = new Bun.Transpiler({ loader: "ts" }); + expect(() => transpiler.transformSync(code)).not.toThrow(); + }); + + test("includes correct import from SDK package", () => { + const code = generateTestConfigCode({}); + + expect(code).toContain('from "@fluffylabs/jammin-sdk"'); + expect(code).toContain("import { config, ServiceId, TestJam }"); + }); + + test("includes SERVICES constant with correct service entries", () => { + const code = generateTestConfigCode({ + auth: { id: 0, name: "auth" }, + bank: { id: 1, name: "bank" }, + }); + + expect(code).toContain("export const SERVICES"); + expect(code).toContain('auth: { id: ServiceId(0), name: "auth" }'); + expect(code).toContain('bank: { id: ServiceId(1), name: "bank" }'); + }); + + test("includes TEST_CHAIN_SPEC export", () => { + const code = generateTestConfigCode({}); + + expect(code).toContain("export const TEST_CHAIN_SPEC = config.tinyChainSpec"); + }); + + test("includes pre-configured testJam instance", () => { + const code = generateTestConfigCode({}); + + expect(code).toContain("export const testJam = await TestJam.create()"); + }); + + test("handles service names with special characters", () => { + const code = generateTestConfigCode({ + my_service: { id: 42, name: "my_service" }, + }); + + const transpiler = new Bun.Transpiler({ loader: "ts" }); + expect(() => transpiler.transformSync(code)).not.toThrow(); + expect(code).toContain('my_service: { id: ServiceId(42), name: "my_service" }'); + }); +}); diff --git a/packages/jammin-sdk/utils/generate-test-config.ts b/packages/jammin-sdk/utils/generate-test-config.ts index 449e952..bc990f3 100644 --- a/packages/jammin-sdk/utils/generate-test-config.ts +++ b/packages/jammin-sdk/utils/generate-test-config.ts @@ -3,6 +3,15 @@ import type { tinyChainSpec } from "@typeberry/lib/config"; import { loadBuildConfig } from "../config/config-loader.js"; import type { ServiceBuildOutput } from "./generate-service-output.js"; +/** Directory name for generated config output */ +const CONFIG_DIR = "config"; + +/** Generated test config filename */ +const TEST_CONFIG_FILENAME = "jammin.test.config.ts"; + +/** SDK package name used in generated import statements */ +const SDK_PACKAGE_NAME = "@fluffylabs/jammin-sdk"; + /** * Generated test configuration with service mappings */ @@ -54,7 +63,7 @@ export async function generateTestConfigInProjectDir( services: ServiceBuildOutput[], projectRoot: string = process.cwd(), ): Promise { - const configPath = resolve(projectRoot, "config", "jammin.test.config.ts"); + const configPath = resolve(projectRoot, CONFIG_DIR, TEST_CONFIG_FILENAME); await generateTestConfigFile(services, configPath); return configPath; } @@ -62,7 +71,7 @@ export async function generateTestConfigInProjectDir( /** * Generate the TypeScript code for test configuration */ -function generateTestConfigCode(serviceMap: Record): string { +export function generateTestConfigCode(serviceMap: Record): string { const serviceEntries = Object.entries(serviceMap) .map(([name, config]) => ` ${name}: { id: ServiceId(${config.id}), name: "${config.name}" },`) .join("\n"); @@ -72,7 +81,7 @@ function generateTestConfigCode(serviceMap: Record { expect(report.results[0]?.load.gasUsed).toBe(Gas(800n)); }); - test("throws error when results array is empty", () => { - expect(() => { - createWorkReport(blake2b, { - results: [], - }); - }).toThrow("WorkReport cannot contain less than 1 results"); - }); - - test("throws error when results array exceeds maximum", () => { - const results = Array.from({ length: 17 }, (_, i) => ({ serviceId: ServiceId(i) })); - expect(() => { - createWorkReport(blake2b, { - results, - }); - }).toThrow("WorkReport cannot contain more than 16 results"); + test("creates work report with empty results array", () => { + const report = createWorkReport(blake2b, { + results: [], + }); + expect(report).toBeDefined(); + expect(report.results.length).toBe(0); }); }); diff --git a/packages/jammin-sdk/work-report.ts b/packages/jammin-sdk/work-report.ts index 21e46c9..1fba1f5 100644 --- a/packages/jammin-sdk/work-report.ts +++ b/packages/jammin-sdk/work-report.ts @@ -1,7 +1,5 @@ import { type CoreIndex, - MAX_NUMBER_OF_WORK_ITEMS, - MIN_NUMBER_OF_WORK_ITEMS, RefineContext, type ServiceGas, type ServiceId, @@ -158,14 +156,6 @@ export function createWorkResult(blake2b: Blake2b, config: WorkResultConfig): Wo * ``` */ export function createWorkReport(blake2b: Blake2b, config: WorkReportConfig): WorkReport { - if (config.results.length < MIN_NUMBER_OF_WORK_ITEMS) { - throw new Error(`WorkReport cannot contain less than ${MIN_NUMBER_OF_WORK_ITEMS} results`); - } - - if (config.results.length > MAX_NUMBER_OF_WORK_ITEMS) { - throw new Error(`WorkReport cannot contain more than ${MAX_NUMBER_OF_WORK_ITEMS} results`); - } - const results = config.results.map((resultConfig) => createWorkResult(blake2b, resultConfig)); const wpSpec = config.workPackageSpec ?? {};