Smithery Logo
MCPsSkillsDocsPricing
Login
NewFlame, an assistant that learns and improves. Available onTelegramSlack
    useautumn

    write-test

    useautumn/write-test
    Coding
    2,333

    About

    SKILL.md

    Install

    • Telegram
      Telegram
    • Slack
      Slack
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    • Download skill
    ├─
    ├─
    └─
    Smithery Logo

    Give agents more agency

    Resources

    DocumentationPrivacy PolicySystem Status

    Company

    PricingAboutBlog

    Connect

    © 2026 Smithery. All rights reserved.

    About

    Write integration tests for the Autumn billing system...

    SKILL.md

    Test Writing Guide

    Before Writing ANY Test

    1. Search for duplicate scenarios — grep the test directory for similar setups
    2. Read the rules file .claude/rules/write-tests.mdc — the 20 rules agents ALWAYS get wrong

    Minimal Template

    import { expect, test } from "bun:test";
    import { type ApiCustomerV3 } from "@autumn/shared";
    import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect";
    import { expectStripeSubscriptionCorrect } from "@tests/integration/billing/utils/expectStripeSubCorrect";
    import { TestFeature } from "@tests/setup/v2Features.js";
    import { items } from "@tests/utils/fixtures/items.js";
    import { products } from "@tests/utils/fixtures/products.js";
    import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
    import chalk from "chalk";
    
    test.concurrent(`${chalk.yellowBright("feature: description")}`, async () => {
      const messagesItem = items.monthlyMessages({ includedUsage: 100 });
      const pro = products.pro({ items: [messagesItem] });
    
      const { customerId, autumnV1, ctx } = await initScenario({
        customerId: "unique-test-id",
        setup: [s.customer({ paymentMethod: "success" }), s.products({ list: [pro] })],
        actions: [s.billing.attach({ productId: pro.id })],
      });
    
      const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
      expectCustomerFeatureCorrect({ customer, featureId: TestFeature.Messages, balance: 100 });
      await expectStripeSubscriptionCorrect({ ctx, customerId });
    });
    

    initScenario — The Core System

    initScenario creates customers, products, entities, and runs actions sequentially. It returns everything you need.

    Returned Values

    const {
      customerId,     // Customer ID (auto-prefixed products)
      autumnV1,       // V1.2 API client
      autumnV2,       // V2.0 API client
      ctx,            // { db, stripeCli, org, env, features }
      testClockId,    // Stripe test clock ID
      customer,       // Customer object after creation
      entities,       // [{ id: "ent-1", name: "Entity 1", featureId }]
      advancedTo,     // Current test clock timestamp (ms)
      otherCustomers, // Map<string, OtherCustomerResult>
    } = await initScenario({ ... });
    

    Setup Functions

    Function Purpose Notes
    s.customer({ paymentMethod?, testClock?, data?, withDefault?, skipWebhooks? }) Configure customer testClock defaults true. Use paymentMethod: "success" for any paid product
    s.products({ list, customerIdsToDelete? }) Products to create Auto-prefixed with customerId
    s.entities({ count, featureId }) Generate entities Creates "ent-1" through "ent-N"
    s.otherCustomers([{ id, paymentMethod? }]) Additional customers Share same test clock as primary
    s.deleteCustomer({ customerId } | { email }) Pre-test cleanup Delete before creating
    s.reward({ reward, productId }) Standalone reward ID auto-suffixed
    s.referralProgram({ reward, program }) Referral program IDs auto-suffixed

    Action Functions — WITH TIMEOUT BEHAVIOR

    CRITICAL: Know which actions have built-in timeouts and which don't.

    Function Built-in Timeout Notes
    s.billing.attach({ productId, options?, planSchedule?, items?, newBillingSubscription? }) 5-8s V2 endpoint. Use for new billing tests
    s.attach({ productId, entityIndex?, options?, newBillingSubscription? }) 4-5s V1 endpoint. Use for legacy/update-subscription setup
    s.billing.multiAttach({ plans, entityIndex?, freeTrial? }) 2-5s plans: [{ productId, featureQuantities? }]
    s.cancel({ productId, entityIndex? }) None No timeout
    s.track({ featureId, value, entityIndex?, timeout? }) None Must pass timeout explicitly if needed
    s.advanceTestClock({ days?, weeks?, hours?, months? }) Waits for Stripe Cumulative from advancedTo
    s.advanceToNextInvoice({ withPause? }) 30s Advances 1 month + 96h for invoice finalization
    s.updateSubscription({ productId, entityIndex?, cancelAction?, items? }) None cancel_end_of_cycle, cancel_immediately, uncancel
    s.attachPaymentMethod({ type }) None "success", "fail", "authenticate"
    s.removePaymentMethod() None Remove all PMs
    s.resetFeature({ featureId, productId?, timeout? }) 2s default For FREE products only. Use s.advanceToNextInvoice for paid
    s.referral.createCode() None Create referral code
    s.referral.redeem({ customerId }) None Redeem for another customer

    s.billing.attach vs s.attach — They Are DIFFERENT

    s.attach s.billing.attach
    Endpoint V1 /attach V2 /billing.attach
    Extra params none planSchedule, items (custom plan)
    Prepaid quantity Exclusive of includedUsage Inclusive of includedUsage
    Use when Legacy tests, update-subscription setup New billing/attach tests

    Product ID Prefixing

    initScenario mutates product objects in-place: product.id becomes "${product.id}_${customerId}". So pro.id after initScenario already includes the prefix. Use product.id everywhere — in s.attach(), in direct API calls, and in assertions.

    Multiple Customers — NEVER Call initScenario Twice

    // Use s.otherCustomers in setup
    const { autumnV1, otherCustomers } = await initScenario({
      customerId: "cus-a",
      setup: [
        s.customer({ paymentMethod: "success" }),
        s.products({ list: [pro] }),
        s.otherCustomers([{ id: "cus-b", paymentMethod: "success" }]),
      ],
      actions: [s.billing.attach({ productId: pro.id })],
    });
    
    // Or create manually after initScenario
    await autumnV1.customers.create("cus-b", { name: "B" });
    await autumnV1.billing.attach({ customer_id: "cus-b", product_id: pro.id });
    

    Assertion Utilities — ALWAYS Use These

    Product State

    import { expectCustomerProducts, expectProductActive, expectProductCanceling,
      expectProductScheduled, expectProductNotPresent } from "@tests/integration/billing/utils/expectCustomerProductCorrect";
    
    // PREFERRED — batch check multiple products in one call
    await expectCustomerProducts({
      customer,
      active: [pro.id],
      canceling: [premium.id],    // "canceling" = status:active + canceled_at set
      scheduled: [free.id],
      notPresent: [oldProduct.id],
    });
    
    // Single product checks
    await expectProductActive({ customer, productId: pro.id });
    await expectProductCanceling({ customer, productId: premium.id });
    await expectProductScheduled({ customer, productId: free.id });
    await expectProductNotPresent({ customer, productId: pro.id });
    

    Features

    import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect";
    
    // IMPORTANT: requires `customer` object, does NOT fetch from API
    expectCustomerFeatureCorrect({
      customer,                        // MUST be fetched customer object, not customerId
      featureId: TestFeature.Messages,
      includedUsage: 100,              // optional
      balance: 100,                    // optional
      usage: 0,                        // optional
      resetsAt: advancedTo + ms.days(30), // optional, 10min tolerance
    });
    

    Invoices

    import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
    
    expectCustomerInvoiceCorrect({
      customer,          // ApiCustomerV3
      count: 2,          // Total invoice count
      latestTotal: 30,   // Most recent invoice total ($), +-0.01 tolerance
      latestStatus: "paid",
    });
    

    Stripe Subscription (ALWAYS call after billing actions)

    import { expectStripeSubscriptionCorrect } from "@tests/integration/billing/utils/expectStripeSubCorrect";
    
    // Basic — verify all subscriptions match expected state
    await expectStripeSubscriptionCorrect({ ctx, customerId });
    
    // With options
    await expectStripeSubscriptionCorrect({
      ctx, customerId,
      options: { subCount: 1, status: "trialing", debug: true },
    });
    

    For free products, use expectNoStripeSubscription instead:

    import { expectNoStripeSubscription } from "@tests/integration/billing/utils/expectNoStripeSubscription";
    await expectNoStripeSubscription({ db: ctx.db, customerId, org: ctx.org, env: ctx.env });
    

    Trials

    import { expectProductTrialing, expectProductNotTrialing } from "@tests/integration/billing/utils/expectCustomerProductTrialing";
    
    const trialEndsAt = await expectProductTrialing({
      customer, productId: pro.id, trialEndsAt: advancedTo + ms.days(7),
    });
    await expectProductNotTrialing({ customer, productId: pro.id });
    

    Preview Next Cycle

    import { expectPreviewNextCycleCorrect } from "@tests/integration/billing/utils/expectPreviewNextCycleCorrect";
    
    expectPreviewNextCycleCorrect({ preview, startsAt: addMonths(advancedTo, 1).getTime(), total: 20 });
    // Or when next_cycle should NOT exist:
    expectPreviewNextCycleCorrect({ preview, expectDefined: false });
    

    Proration

    import { calculateProratedDiff } from "@tests/integration/billing/utils/proration";
    
    const expected = await calculateProratedDiff({
      customerId, advancedTo, oldAmount: 20, newAmount: 50,
    });
    expect(preview.total).toBeCloseTo(expected, 0);
    

    Invoice Line Items (for tests verifying stored line items)

    import { expectInvoiceLineItemsCorrect, expectBasePriceLineItem } from "@tests/integration/billing/utils/expectInvoiceLineItemsCorrect";
    
    // Full check with per-item expectations
    await expectInvoiceLineItemsCorrect({
      stripeInvoiceId: invoice.stripe_id,
      expectedTotal: 20,
      expectedCount: 2,
      expectedLineItems: [
        { isBasePrice: true, amount: 20, direction: "charge" },
        { featureId: TestFeature.Messages, totalAmount: 0 },
      ],
    });
    
    // Quick base price check
    await expectBasePriceLineItem({ stripeInvoiceId, amount: 20 });
    

    Error Testing

    import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils";
    
    await expectAutumnError({
      errCode: ErrCode.CustomerNotFound,
      func: () => autumnV1.customers.get("invalid-id"),
    });
    

    Cache vs DB Verification

    import { expectFeatureCachedAndDb } from "@tests/integration/billing/utils/expectFeatureCachedAndDb";
    
    await expectFeatureCachedAndDb({
      autumn: autumnV1, customerId,
      featureId: TestFeature.Messages, balance: 90, usage: 10,
    });
    

    Rollovers

    import { expectCustomerRolloverCorrect, expectNoRollovers } from "@tests/integration/billing/utils/rollover/expectCustomerRolloverCorrect";
    
    expectCustomerRolloverCorrect({
      customer, featureId: TestFeature.Messages,
      expectedRollovers: [{ balance: 150 }], totalBalance: 550,
    });
    

    Item & Product Fixtures — Quick Reference

    Items (@tests/utils/fixtures/items)

    Item Feature Default Notes
    items.dashboard() Dashboard boolean On/off access
    items.monthlyMessages({ includedUsage? }) Messages 100 Resets monthly
    items.monthlyWords({ includedUsage? }) Words 100 Resets monthly
    items.monthlyCredits({ includedUsage? }) Credits 100 Resets monthly
    items.monthlyUsers({ includedUsage? }) Users 5 Resets monthly
    items.unlimitedMessages() Messages unlimited No cap
    items.lifetimeMessages({ includedUsage? }) Messages 100 Never resets (interval: null)
    items.prepaidMessages({ includedUsage?, billingUnits?, price? }) Messages 0, 100, $10 Buy upfront in packs
    items.prepaid({ featureId, includedUsage?, billingUnits?, price? }) any 0, 100, $10 Generic prepaid
    items.prepaidUsers({ includedUsage?, billingUnits? }) Users 0, 1 Per-seat prepaid
    items.consumableMessages({ includedUsage? }) Messages 0 $0.10/unit overage
    items.consumableWords({ includedUsage? }) Words 0 $0.05/unit overage
    items.consumable({ featureId, includedUsage?, price?, billingUnits? }) any 0, $0.10, 1 Generic consumable
    items.allocatedUsers({ includedUsage? }) Users 0 $10/seat prorated
    items.allocatedWorkflows({ includedUsage? }) Workflows 0 $10/workflow prorated
    items.freeAllocatedUsers({ includedUsage? }) Users 5 Free seats (no price)
    items.oneOffMessages({ includedUsage?, billingUnits?, price? }) Messages 0, 100, $10 One-time purchase
    items.monthlyPrice({ price? }) - $20 Base price item
    items.annualPrice({ price? }) - $200 Annual base price
    items.oneOffPrice({ price? }) - $50 One-time base price
    items.monthlyMessagesWithRollover({ includedUsage?, rolloverConfig }) Messages 100 With rollover
    items.tieredPrepaidMessages({ includedUsage?, billingUnits?, tiers? }) Messages - Graduated tier prepaid
    items.tieredConsumableMessages({ includedUsage?, billingUnits?, tiers? }) Messages - Graduated tier consumable

    Products (@tests/utils/fixtures/products)

    Product Built-in Base Price Default ID
    products.base({ items, id?, isDefault?, isAddOn? }) None (free) "base"
    products.pro({ items, id? }) $20/mo "pro"
    products.premium({ items, id? }) $50/mo "premium"
    products.growth({ items, id? }) $100/mo "growth"
    products.ultra({ items, id? }) $200/mo "ultra"
    products.proAnnual({ items, id? }) $200/yr "pro-annual"
    products.proWithTrial({ items, id?, trialDays?, cardRequired? }) $20/mo + trial "pro-trial"
    products.baseWithTrial({ items, id?, trialDays?, cardRequired? }) None + trial "base-trial"
    products.oneOff({ items, id? }) $10 one-time "one-off"
    products.recurringAddOn({ items, id? }) $20/mo add-on "addon"
    products.oneOffAddOn({ items, id? }) $10 one-time add-on "one-off-addon"

    NEVER add items.monthlyPrice() to products.pro() — it already has $20/mo built in.

    Common Test Patterns

    Attach Test (Upgrade)

    test.concurrent(`${chalk.yellowBright("upgrade: free to pro")}`, async () => {
      const messagesItem = items.monthlyMessages({ includedUsage: 100 });
      const free = products.base({ id: "free", items: [messagesItem] });
      const pro = products.pro({ items: [messagesItem] });
    
      const { customerId, autumnV1, ctx } = await initScenario({
        customerId: "upgrade-free-pro",
        setup: [s.customer({ paymentMethod: "success" }), s.products({ list: [free, pro] })],
        actions: [s.billing.attach({ productId: free.id })],
      });
    
      await autumnV1.billing.attach({
        customer_id: customerId, product_id: pro.id, redirect_mode: "if_required",
      });
    
      const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
      await expectCustomerProducts({ customer, active: [pro.id], notPresent: [free.id] });
      expectCustomerInvoiceCorrect({ customer, count: 1, latestTotal: 20 });
      await expectStripeSubscriptionCorrect({ ctx, customerId });
    });
    

    Downgrade Test (Scheduled)

    test.concurrent(`${chalk.yellowBright("downgrade: pro to free")}`, async () => {
      const messagesItem = items.monthlyMessages({ includedUsage: 100 });
      const pro = products.pro({ items: [messagesItem] });
      const free = products.base({ id: "free", items: [messagesItem] });
    
      const { customerId, autumnV1, ctx } = await initScenario({
        customerId: "downgrade-pro-free",
        setup: [s.customer({ paymentMethod: "success" }), s.products({ list: [pro, free] })],
        actions: [s.billing.attach({ productId: pro.id })],
      });
    
      await autumnV1.billing.attach({
        customer_id: customerId, product_id: free.id, redirect_mode: "if_required",
      });
    
      const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
      await expectCustomerProducts({
        customer,
        canceling: [pro.id],   // NOT active — canceling means active + canceled_at set
        scheduled: [free.id],
      });
      await expectStripeSubscriptionCorrect({ ctx, customerId });
    });
    

    Track Test (Decimal.js Required)

    import { Decimal } from "decimal.js";
    
    test.concurrent(`${chalk.yellowBright("track: basic deduction")}`, async () => {
      const messagesItem = items.monthlyMessages({ includedUsage: 100 });
      const free = products.base({ items: [messagesItem] });
    
      const { customerId, autumnV1 } = await initScenario({
        customerId: "track-basic",
        setup: [s.customer({}), s.products({ list: [free] })],
        actions: [s.attach({ productId: free.id })],
      });
    
      await autumnV1.track({ customer_id: customerId, feature_id: TestFeature.Messages, value: 23.47 });
    
      const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
      expect(customer.features[TestFeature.Messages].balance).toBe(
        new Decimal(100).sub(23.47).toNumber()
      );
    });
    

    Prepaid Test

    test.concurrent(`${chalk.yellowBright("prepaid: attach with quantity")}`, async () => {
      const prepaidItem = items.prepaidMessages({ includedUsage: 0, billingUnits: 100, price: 10 });
      const pro = products.base({ id: "prepaid-pro", items: [prepaidItem] });
    
      const { customerId, autumnV1, ctx } = await initScenario({
        customerId: "prepaid-attach",
        setup: [s.customer({ paymentMethod: "success" }), s.products({ list: [pro] })],
        actions: [
          s.billing.attach({
            productId: pro.id,
            options: [{ feature_id: TestFeature.Messages, quantity: 200 }], // inclusive of includedUsage
          }),
        ],
      });
    
      const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
      // quantity 200 → rounded to nearest billingUnit (200), purchased_balance: 200
      expectCustomerFeatureCorrect({ customer, featureId: TestFeature.Messages, balance: 200 });
      await expectStripeSubscriptionCorrect({ ctx, customerId });
    });
    

    Test Type Decision Tree

    Writing a... Use in initScenario actions Test body calls
    Billing attach test s.billing.attach() for setup autumnV1.billing.attach() for action under test
    Multi-attach test s.billing.attach() for setup autumnV1.billing.multiAttach()
    Update subscription test s.attach() for initial attach autumnV1.subscriptions.update()
    Cancel test s.billing.attach() for setup autumnV1.subscriptions.update({ cancel: "end_of_cycle" })
    Track/check test s.attach() for product setup autumnV1.track() / autumnV1.check()
    Prepaid test s.billing.attach({ options }) autumnV1.billing.attach() or subscriptions.update()
    Entity test s.entities() in setup, entityIndex in actions Entity-specific API calls
    Webhook test s.customer({ skipWebhooks: true }) Manual customer create with skipWebhooks: false

    Balance Calculation Rules

    Feature Type Balance Formula Use Decimal.js?
    Free metered includedUsage - usage Yes
    Prepaid includedUsage + purchasedQuantity - usage Yes
    Consumable + Prepaid same feature consumable.includedUsage + prepaid.purchasedQuantity - usage Yes
    Allocated includedUsage + purchasedSeats - currentSeats Yes
    Credit system creditBalance - sum(action * credit_cost) Yes, + getCreditCost()

    Resetting Features: Free vs Paid

    • Free products (no Stripe sub): Use s.resetFeature({ featureId, productId }) — simulates cron job
    • Paid products (has Stripe sub): Use s.advanceToNextInvoice() — advances test clock, triggers invoice.paid webhook

    Running Tests

    CRITICAL: NEVER run tests automatically. Always ask the user for permission before running any test command. The user likely has a dev server running and needs to coordinate test execution.

    Commands (run from repo root)

    # Run a single test file
    bun test server/tests/integration/billing/attach/my-test.test.ts --timeout 60000
    
    # Run a specific test by name pattern within a file
    bun test server/tests/integration/billing/attach/my-test.test.ts -t "upgrade: free to pro" --timeout 60000
    
    # Run all tests in a directory
    bun test server/tests/integration/billing/attach/ --timeout 60000
    
    # Using the package.json script (loads env via infisical)
    bun run --cwd server test:integration server/tests/integration/billing/attach/my-test.test.ts
    

    Key Points

    • --timeout 60000 (or higher) is essential — billing tests involve Stripe test clocks and can take 30s+
    • bunfig.toml sets timeout = 0 (infinite) and preloads env + test setup automatically
    • Run one test file at a time during development to avoid test clock conflicts
    • All server-side console.log output goes to the server's logs, not the test output — ask the user to paste server logs if debugging

    After Writing Tests

    Always run a typecheck:

    bun ts
    

    This runs bunx tsgo --build --noEmit in the server directory. Fix all type errors before considering the task done.

    References (Load On-Demand for Edge Cases)

    • references/SCENARIO.md — Full initScenario details, all builder params
    • references/FIXTURES.md — Complete item/product fixture params
    • references/ENTITIES.md — Entity-based testing (entity-products vs per-entity features)
    • references/EXPECTATIONS.md — All expectation utility signatures
    • references/PRORATION.md — Proration calculation utilities
    • references/GOTCHAS.md — Expanded wrong/right examples for every common mistake
    • references/TRACK-CHECK.md — Track/check endpoint testing, credit systems
    • references/WEBHOOKS.md — Outbound webhook testing with Svix Play
    • references/STRIPE-BEHAVIORS.md — Stripe webhook behaviors
    Recommended Servers
    bugAgent
    bugAgent
    OpenZeppelin
    OpenZeppelin
    Repository
    useautumn/autumn
    Files