ProvenPin Install

sinon stubs and fake timers: working versions (tested 2026-10-18)

Stub object methods with resolves/rejects and onCall sequences, record spy call counts and args, and drive debounce and retry logic with sinon fake timers and clock.tickAsync under node:test, fully offline.

passed1 passed · 0 failed2026-10-18 03:58 UTC
Status
passed
Last passed
2026-10-18 03:58 UTC
Tests
1 / 1
Language
TypeScript
Runtime
node v22.23.3
pinned versions3
sinon@22.1.0@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 39095442401

The code

Exactly the files that passed. Your agent can pull them with get_recipe("sinon-stubs-fake-timers") (install), or see them on GitHub.

package.json
{
  "name": "sinon-stubs-fake-timers",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "devDependencies": {
    "sinon": "22.1.0",
    "@types/node": "26.6.5",
    "tsx": "4.23.15"
  }
}
src/service.ts
export interface ApiClient {
  fetchJob(id: string): Promise<{ id: string; status: string }>;
}

export async function loadStatus(client: ApiClient, id: string): Promise<string> {
  const job = await client.fetchJob(id);
  return job.status;
}

export function debounce<A extends unknown[]>(
  fn: (...args: A) => void,
  waitMs: number,
): (...args: A) => void {
  let timer: ReturnType<typeof setTimeout> | undefined;
  return (...args: A) => {
    if (timer !== undefined) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      timer = undefined;
      fn(...args);
    }, waitMs);
  };
}

export async function retry<T>(
  fn: () => Promise<T>,
  opts: { attempts: number; delayMs: number },
): Promise<T> {
  let lastError: unknown;
  for (let attempt = 1; attempt <= opts.attempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      if (attempt < opts.attempts) {
        await new Promise((resolve) => setTimeout(resolve, opts.delayMs * attempt));
      }
    }
  }
  throw lastError;
}
test/service.test.ts
import { test, afterEach } from "node:test";
import assert from "node:assert/strict";
import sinon from "sinon";
import { debounce, loadStatus, retry } from "../src/service.ts";

afterEach(() => {
  sinon.restore();
});

test("stub resolves and rejects an object method", async () => {
  const client = { fetchJob: sinon.stub() };
  client.fetchJob.resolves({ id: "j1", status: "done" });
  assert.equal(await loadStatus(client, "j1"), "done");
  assert.deepEqual(client.fetchJob.firstCall.args, ["j1"]);

  client.fetchJob.rejects(new Error("boom"));
  await assert.rejects(() => loadStatus(client, "j2"), /boom/);
});

test("stub onCall returns a scripted sequence", async () => {
  const fetchJob = sinon.stub();
  fetchJob.onFirstCall().resolves({ id: "1", status: "queued" });
  fetchJob.onSecondCall().rejects(new Error("nope"));
  fetchJob.onThirdCall().resolves({ id: "1", status: "done" });

  const client = { fetchJob };
  assert.equal(await loadStatus(client, "1"), "queued");
  await assert.rejects(() => loadStatus(client, "1"), /nope/);
  assert.equal(await loadStatus(client, "1"), "done");
  assert.equal(fetchJob.callCount, 3);
});

test("spy records call count and arguments", () => {
  const logger = { log: (_msg: string): void => undefined };
  const spy = sinon.spy(logger, "log");

  logger.log("hello");
  logger.log("world");

  assert.equal(spy.callCount, 2);
  assert.deepEqual(spy.firstCall.args, ["hello"]);
  assert.deepEqual(spy.secondCall.args, ["world"]);
  assert.ok(spy.calledWith("hello"));
});

test("debounce collapses calls using fake timers", async () => {
  const clock = sinon.useFakeTimers();
  const handler = sinon.spy();
  const debounced = debounce(handler, 100);

  debounced("a");
  clock.tick(50);
  debounced("b");
  assert.equal(handler.callCount, 0);

  await clock.tickAsync(100);
  assert.equal(handler.callCount, 1);
  assert.deepEqual(handler.firstCall.args, ["b"]);

  clock.restore();
});

test("retry backs off between attempts using tickAsync", async () => {
  const clock = sinon.useFakeTimers();
  const fn = sinon.stub();
  fn.onFirstCall().rejects(new Error("temporary"));
  fn.onSecondCall().resolves("ok");

  const pending = retry(fn, { attempts: 3, delayMs: 100 });
  await clock.tickAsync(100);

  assert.equal(await pending, "ok");
  assert.equal(fn.callCount, 2);

  clock.restore();
});

test("sandbox restore returns original behavior", async () => {
  const sandbox = sinon.createSandbox();
  const repo = { find: async (): Promise<string> => "real" };

  sandbox.stub(repo, "find").resolves("stubbed");
  assert.equal(await repo.find(), "stubbed");

  sandbox.restore();
  assert.equal(await repo.find(), "real");
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.