Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,22 +214,46 @@ The `mainsail` CLI binary. Commands in `source/commands/` cover: `core:run`, `co
Tests use `describe` from `@mainsail/test-runner` (wraps uvu suites):

```typescript
import { Identifiers } from "@mainsail/constants";
import { Application } from "@mainsail/kernel";
import { describe } from "@mainsail/test-runner";
import { Handler } from "./handler";

describe<Context>("ComponentName", ({ it, beforeEach, assert, stub, spy, clock }) => {
describe<{
app: Application;
handler: Handler;
myService: any;
}>("Handler", ({ it, beforeEach, assert, stub, spy, clock }) => {
beforeEach((context) => {
// Set up stubs for injected dependencies
context.myService = { method: () => {} };
// Build container, bind stubs, resolve class under test

// Use Application (from @mainsail/kernel), not the raw Container — it auto-binds itself
// as Identifiers.Application.Instance and exposes resolve() (which applies autobind).
context.app = new Application();
context.app.bind(Identifiers.SomeService).toConstantValue(context.myService);

// Resolve the class under test once, here — never inline inside an it().
context.handler = context.app.resolve(Handler);
});

it("does something", async (context) => {
// arrange, act, assert
assert.equal(result, expected);
// Destructure the context in the it() callback rather than threading `context.` through.
it("does something", async ({ handler, myService }) => {
const method = spy(myService, "method");

await handler.handle();

method.calledOnce();
});
});
```

Conventions for IoC-injected classes under test:

- Bind stubs to their `Identifiers` on an `Application` instance and resolve the class with `app.resolve(Class)`. `Application.get(id)` takes only the identifier; use `resolve()` for autobinding the class under test.
- Resolve the tested class **in `beforeEach`** and store it on the context (e.g. `context.handler`). Don't resolve inline inside an `it()`.
- In `it()` callbacks, **destructure the context** — `async ({ handler, myService }) => {}` — instead of referencing `context.x`. Mutating a destructured stub (e.g. `myService.method = …`) still works since it's the same object the handler holds.

Helpers available: `assert` (custom assertions), `stub()` / `spy()` (sinon wrappers), `clock()` (sinon fake timers), `nock` (HTTP mocking), `each()` (data-driven tests), `schema` (zod).

Test factories for common entities (blocks, wallets, transactions, commits) are in `@mainsail/test-factories`. Transaction builders for tests are in `@mainsail/test-transaction-builders`.
Expand Down
4 changes: 2 additions & 2 deletions packages/transaction-pool-worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@
"@mainsail/container": "workspace:*",
"@mainsail/kernel": "workspace:*",
"@mainsail/utils": "workspace:*",
"dayjs": "1.11.20",
"joi": "18.2.1"
"dayjs": "1.11.20"
},
"devDependencies": {
"@mainsail/contracts": "workspace:*",
"@mainsail/test-runner": "workspace:*",
"esmock": "2.7.5",
"uvu": "0.5.6"
},
"engines": {
Expand Down
1 change: 0 additions & 1 deletion packages/transaction-pool-worker/source/defaults.ts

This file was deleted.

88 changes: 88 additions & 0 deletions packages/transaction-pool-worker/source/handlers/commit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Identifiers } from "@mainsail/constants";
import { Application } from "@mainsail/kernel";

import { describe } from "@mainsail/test-runner";
import { CommitHandler } from "./commit";

describe<{
app: Application;
handler: CommitHandler;
stateStore: any;
configuration: any;
transactionPoolService: any;
selector: any;
}>("CommitHandler", ({ assert, beforeEach, it, spy }) => {
beforeEach((context) => {
context.stateStore = { setBlockNumber: () => {} };
context.configuration = { isNewMilestone: () => false };
context.transactionPoolService = { commit: async () => {}, reAddTransactions: async () => {} };
context.selector = { clear: () => {} };

context.app = new Application();
context.app.bind(Identifiers.State.Store).toConstantValue(context.stateStore);
context.app.bind(Identifiers.Cryptography.Configuration).toConstantValue(context.configuration);
context.app.bind(Identifiers.TransactionPool.Service).toConstantValue(context.transactionPoolService);
context.app.bind(Identifiers.TransactionPool.Selector).toConstantValue(context.selector);

context.handler = context.app.resolve(CommitHandler);
});

it("sets the block number and clears the selector", async ({ handler, stateStore, selector }) => {
const setBlockNumber = spy(stateStore, "setBlockNumber");
const clear = spy(selector, "clear");

await handler.handle(10, ["address-1"], 1000, false);

setBlockNumber.calledOnce();
setBlockNumber.calledWith(10);
clear.calledOnce();
});

it("commits the senders, gas and syncing flag when not a new milestone", async ({
handler,
transactionPoolService,
}) => {
const commit = spy(transactionPoolService, "commit");
const reAdd = spy(transactionPoolService, "reAddTransactions");

await handler.handle(10, ["address-1", "address-2"], 5000, true);

commit.calledOnce();
commit.calledWith(["address-1", "address-2"], 5000, true);
reAdd.neverCalled();
});

it("re-adds transactions instead of committing on a new milestone", async ({
handler,
configuration,
transactionPoolService,
}) => {
configuration.isNewMilestone = () => true;
const commit = spy(transactionPoolService, "commit");
const reAdd = spy(transactionPoolService, "reAddTransactions");

await handler.handle(10, ["address-1"], 1000, false);

reAdd.calledOnce();
commit.neverCalled();
});

it("wraps a thrown error with a 'Failed to commit block' message", async ({ handler, transactionPoolService }) => {
transactionPoolService.commit = async () => {
throw new Error("boom");
};

await assert.rejects(() => handler.handle(10, ["address-1"], 1000, false), "Failed to commit block: boom");
});

it("normalizes a non-Error throw into the wrapped message", async ({ handler, selector }) => {
selector.clear = () => {
throw "string failure";
};

await assert.rejects(
() => handler.handle(10, ["address-1"], 1000, false),
"Failed to commit block: string failure",
);
});
});
3 changes: 0 additions & 3 deletions packages/transaction-pool-worker/source/handlers/commit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ export class CommitHandler {
@inject(Identifiers.TransactionPool.Selector)
private readonly selector!: Contracts.TransactionPool.Selector;

@inject(Identifiers.Services.Log.Service)
protected readonly logger!: Contracts.Kernel.Logger;

public async handle(
blockNumber: number,
sendersAddresses: string[],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Identifiers } from "@mainsail/constants";
import { Application } from "@mainsail/kernel";

import { describe } from "@mainsail/test-runner";
import { ForgetPeerHandler } from "./forget-peer";

describe<{
app: Application;
handler: ForgetPeerHandler;
peerRepository: any;
}>("ForgetPeerHandler", ({ beforeEach, it, spy }) => {
beforeEach((context) => {
context.peerRepository = { forgetPeer: () => {} };

context.app = new Application();
context.app.bind(Identifiers.TransactionPool.Peer.Repository).toConstantValue(context.peerRepository);

context.handler = context.app.resolve(ForgetPeerHandler);
});

it("forgets the peer by ip", async ({ handler, peerRepository }) => {
const forgetPeer = spy(peerRepository, "forgetPeer");

await handler.handle("127.0.0.1");

forgetPeer.calledOnce();
forgetPeer.calledWith("127.0.0.1");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Identifiers } from "@mainsail/constants";
import { Application } from "@mainsail/kernel";

import { describe } from "@mainsail/test-runner";
import { GetTransactionsHandler } from "./get-transactions";

describe<{
app: Application;
handler: GetTransactionsHandler;
selector: any;
}>("GetTransactionsHandler", ({ assert, beforeEach, it, spy }) => {
beforeEach((context) => {
context.selector = { getBatch: async () => ({ remaining: 0, transactions: [] }) };

context.app = new Application();
context.app.bind(Identifiers.TransactionPool.Selector).toConstantValue(context.selector);

context.handler = context.app.resolve(GetTransactionsHandler);
});

it("delegates to the selector and returns its batch", async ({ handler, selector }) => {
const batch = { remaining: 2, transactions: [] };
selector.getBatch = async () => batch;
const getBatch = spy(selector, "getBatch");

const options = { blockRound: "0", maxBytes: 1024, maxSize: 100 };
const result = await handler.handle(options);

getBatch.calledOnce();
getBatch.calledWith(options);
assert.equal(result, batch);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Identifiers } from "@mainsail/constants";
import { Application } from "@mainsail/kernel";

import { describe } from "@mainsail/test-runner";
import { ReloadWebhooksHandler } from "./reload-webhooks";

describe<{
app: Application;
handler: ReloadWebhooksHandler;
database: any;
}>("ReloadWebhooksHandler", ({ beforeEach, it, spy }) => {
beforeEach((context) => {
context.database = { restore: () => {} };

context.app = new Application();
context.app.bind(Identifiers.Webhooks.Database).toConstantValue(context.database);

context.handler = context.app.resolve(ReloadWebhooksHandler);
});

it("restores the webhooks database", async ({ handler, database }) => {
const restore = spy(database, "restore");

await handler.handle();

restore.calledOnce();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Identifiers } from "@mainsail/constants";
import { Application } from "@mainsail/kernel";

import { describe } from "@mainsail/test-runner";
import { RemoveTransactionHandler } from "./remove-transaction";

describe<{
app: Application;
handler: RemoveTransactionHandler;
mempool: any;
storage: any;
}>("RemoveTransactionHandler", ({ beforeEach, it, spy }) => {
beforeEach((context) => {
context.mempool = { removeTransaction: async () => {} };
context.storage = { removeTransaction: () => {} };

context.app = new Application();
context.app.bind(Identifiers.TransactionPool.Mempool).toConstantValue(context.mempool);
context.app.bind(Identifiers.TransactionPool.Storage).toConstantValue(context.storage);

context.handler = context.app.resolve(RemoveTransactionHandler);
});

it("removes the transaction from the mempool and the storage", async ({ handler, mempool, storage }) => {
const fromMempool = spy(mempool, "removeTransaction");
const fromStorage = spy(storage, "removeTransaction");

await handler.handle("address-1", "hash-1");

fromMempool.calledOnce();
fromMempool.calledWith("address-1", "hash-1");
fromStorage.calledOnce();
fromStorage.calledWith("hash-1");
});
});
29 changes: 29 additions & 0 deletions packages/transaction-pool-worker/source/handlers/set-peer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Identifiers } from "@mainsail/constants";
import { Application } from "@mainsail/kernel";

import { describe } from "@mainsail/test-runner";
import { SetPeerHandler } from "./set-peer";

describe<{
app: Application;
handler: SetPeerHandler;
peerRepository: any;
}>("SetPeerHandler", ({ beforeEach, it, spy }) => {
beforeEach((context) => {
context.peerRepository = { setPeer: () => {} };

context.app = new Application();
context.app.bind(Identifiers.TransactionPool.Peer.Repository).toConstantValue(context.peerRepository);

context.handler = context.app.resolve(SetPeerHandler);
});

it("sets the peer by ip", async ({ handler, peerRepository }) => {
const setPeer = spy(peerRepository, "setPeer");

await handler.handle("127.0.0.1");

setPeer.calledOnce();
setPeer.calledWith("127.0.0.1");
});
});
Loading
Loading