Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Prettier churn cannot be undone by running oxfmt afterwards

**Symptom.** A role agent's diff on its owned files is far larger than the planned
change (thousands of lines across untouched regions): single quotes flipped to double
quotes, object literals rewrapped multi-line. The agent ran Prettier (defaults or its
own config) on repo files.

**Cause.** This repository's formatter is **oxfmt** (root `pnpm format`, config
`.oxfmtrc.json`: `singleQuote`, `printWidth: 100`). Prettier with defaults
(`printWidth: 80`, double quotes) reformats whole files. The trap: running oxfmt
afterwards does NOT restore the original. Prettier expands object literals to one
property per line, and oxfmt (like Prettier) preserves object literals that are
already expanded in the source — so the double-quote churn reverts but the object
expansion stays. Both states are formatter fixed points; the original single-line
form is unrecoverable by formatting.

**Fix.**

- Recovery: `git checkout HEAD -- <files>` and re-apply the functional edits. Do not
attempt formatter-based repair.
- Prevention (dispatchers): every implementer handoff that touches files pins —
never run `prettier`/`npx prettier`; the repo formatter is oxfmt
(`pnpm -w exec oxfmt <file>`); before finishing, run
`git diff HEAD --stat -- <owned paths>` and confirm the diff is limited to the
functional edits.
- Detection (orchestrator): compare the emitted slice diff size against the planned
change size BEFORE dispatching the reviewer. A diff many times larger than the
plan's edit description is churn — revert and redo the round; do not send churn to
the reviewer and do not commit it.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Running the full web jest suite locally: workers, timeouts, and run collisions

**Symptom 1 — mass per-test 5s timeouts and 60s `beforeAll` hook timeouts across
unrelated suites when running `pnpm --filter web test` locally.** The failures name
`workerSetup.ts` `beforeAll` or `cleanupDbForTest` in `beforeEach`, in suites far from
whatever you changed.

**Cause.** `apps/web/jest.config.ts` defaults to `maxWorkers: '50%'`. Every worker's
first suite cold-starts a per-worker database
(`DROP DATABASE … WITH (FORCE)` + `CREATE DATABASE` + full drizzle migrate + partition
provisioning, `apps/web/src/tests/setup/workerSetup.ts`). On a laptop docker postgres,
N parallel cold starts exceed the hardcoded 60s hook timeout; the setup flag file is
only written after success, so every subsequent suite re-runs the full setup and times
out again — the whole run is poisoned from the start. Even after setup, 200-table
`TRUNCATE` cleanup per test can exceed the 5s default test timeout under load.

**Fix.** Run the full suite as `JEST_MAX_WORKERS=1 pnpm --filter web test` with nothing
else DB-heavy on the machine, and expect it to take hours (~680 suites). Per-file
(`pnpm --filter web test -- <file>`) runs are fine with default workers. If a full-run
failure is in a suite your diff does not touch, re-run that suite alone before
believing it.

**Symptom 2 — a second concurrent DB-test run crashes with `ERR_UNHANDLED_ERROR` /
`Connection terminated unexpectedly`, and both runs produce garbage results.**

**Cause.** With `JEST_MAX_WORKERS=1` (or any equal worker count), both runs use the
same `JEST_WORKER_ID`s and therefore the SAME `postgres-<workerId>` databases: each
run's setup `DROP DATABASE … WITH (FORCE)` terminates the other run's connections
mid-flight.

**Fix.** Never run two web jest invocations (or anything else using the shared
docker postgres test setup) concurrently on one worktree. One DB-test run at a time,
machine-wide.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import * as React from 'react';
import { type Purchase } from 'expo-iap';
import { toast } from 'sonner-native';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
captureEvent,
KILO_PASS_PURCHASE_COMPLETED_EVENT,
KILO_PASS_PURCHASE_FAILED_EVENT,
KILO_PASS_PURCHASE_STARTED_EVENT,
} from '@/lib/analytics/posthog';
import {
createAppStoreKiloPassPurchaseActions,
resetInlinePurchaseErrorOwnership,
Expand Down Expand Up @@ -902,6 +908,34 @@ describe('StoreKiloPassPurchaseProvider', () => {
expect(mockedIap.requestPurchase).toHaveBeenCalledTimes(2);
});

it('emits started and failed client events but not purchase_completed', async () => {
const provider = renderStoreKiloPassPurchaseProvider();
const initialValue = provider.render();

const completedSpy = vi.fn();
await initialValue.purchase(product, {
onCompleted: () => {
completedSpy();
},
});
expect(captureEvent).toHaveBeenCalledWith(KILO_PASS_PURCHASE_STARTED_EVENT);

mockedIap.handlers?.onPurchaseSuccess(createPurchase());
await flushPromises();

// Anchor: proves the completion callback (where the client capture used to
// live) actually ran, so the negative assertion below is not vacuous.
expect(completedSpy).toHaveBeenCalledTimes(1);
expect(captureEvent).not.toHaveBeenCalledWith(KILO_PASS_PURCHASE_COMPLETED_EVENT);
Comment thread
iscekic marked this conversation as resolved.

const afterSuccess = provider.render();
await afterSuccess.purchase(product);
mockedIap.handlers?.onPurchaseError(new Error('StoreKit failed'));

expect(captureEvent).toHaveBeenCalledWith(KILO_PASS_PURCHASE_FAILED_EVENT);
expect(captureEvent).not.toHaveBeenCalledWith(KILO_PASS_PURCHASE_COMPLETED_EVENT);
});

it('toasts a purchase error when no screen owns inline feedback', async () => {
const provider = renderStoreKiloPassPurchaseProvider();

Expand Down
6 changes: 2 additions & 4 deletions apps/mobile/src/lib/kilo-pass/use-store-kilo-pass-purchase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import { z } from 'zod';

import {
captureEvent,
KILO_PASS_PURCHASE_COMPLETED_EVENT,
KILO_PASS_PURCHASE_FAILED_EVENT,
KILO_PASS_PURCHASE_STARTED_EVENT,
} from '@/lib/analytics/posthog';
Expand Down Expand Up @@ -498,9 +497,8 @@ function IosStoreKiloPassPurchaseProvider({ children }: { children: ReactNode })
finishTransaction,
invalidateAfterCompletion,
onPurchaseCompleted: () => {
// Only user-initiated purchases reach here — recovery and restore
// flows pass notifyCompletion: false.
captureEvent(KILO_PASS_PURCHASE_COMPLETED_EVENT);
// Completed is emitted server-side by completeAppStorePurchase — do not
// re-add a client capture (double counting).
setErrorMessage(null);
const onCompleted = pendingPurchaseCompletedCallbackRef.current;
pendingPurchaseCompletedCallbackRef.current = null;
Expand Down
232 changes: 229 additions & 3 deletions apps/web/src/lib/kilo-pass/apple-store-notifications.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { afterAll, beforeAll, describe, expect, it, jest } from '@jest/globals';
import { afterAll, beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals';
import {
DeliveryStatus,
NotificationTypeV2,
Expand Down Expand Up @@ -27,11 +27,31 @@ import {
KiloPassPaymentProvider,
KiloPassTier,
} from './enums';
import { processAppStoreKiloPassNotification } from './apple-store-notifications';
import type * as AppleStoreNotifications from './apple-store-notifications';
import type { AppleStoreDecodedNotification } from './apple-store-notifications';
import type { AppleStoreDecodedTransaction } from './apple-store-verifier';
import { toMicrodollars } from '@/lib/utils';

// SWC + static ESM imports do not see jest.mock replacements on the same module id.
// Dynamic-import the SUT after the mock (same pattern as stripe-handlers-invoice-paid.test.ts).
jest.mock('@/lib/kilo-pass/posthog-tracking', () => ({
runAfterResponse: async (work: () => Promise<void>) => {
await work();
},
trackKiloPassPurchaseCompleted: jest.fn(),
}));

type PosthogTrackingMock = {
trackKiloPassPurchaseCompleted: jest.Mock;
runAfterResponse: (work: () => Promise<void>) => Promise<void>;
};

function getPosthogTrackingMock(): PosthogTrackingMock {
return jest.requireMock('@/lib/kilo-pass/posthog-tracking') as PosthogTrackingMock;
}

let processAppStoreKiloPassNotification: typeof AppleStoreNotifications.processAppStoreKiloPassNotification;

const APP_STORE_NOTIFICATION_TEST_NOW_MS = Date.parse('2026-05-15T00:00:00.000Z');

function notification(
Expand Down Expand Up @@ -99,14 +119,220 @@ async function insertProviderScopedSubscriptionRows(providerSubscriptionId: stri
describe('processAppStoreKiloPassNotification', () => {
let dateNowSpy: jest.SpiedFunction<typeof Date.now>;

beforeAll(() => {
beforeAll(async () => {
dateNowSpy = jest.spyOn(Date, 'now').mockReturnValue(APP_STORE_NOTIFICATION_TEST_NOW_MS);
({ processAppStoreKiloPassNotification } = await import('./apple-store-notifications'));
});

afterAll(() => {
dateNowSpy.mockRestore();
});

beforeEach(() => {
getPosthogTrackingMock().trackKiloPassPurchaseCompleted.mockClear();
});

describe('kilo_pass_purchase_completed tracking', () => {
it('does not track when the notification UUID was already processed', async () => {
const trackingMock = getPosthogTrackingMock();
const user = await insertTestUser();
const decodedNotification = notification();
const decodedTransaction = transaction({
appAccountToken: user.app_store_account_token,
});
const params = {
signedPayload: 'payload',
decodeNotification: async () => decodedNotification,
decodeTransaction: async () => decodedTransaction,
};

await processAppStoreKiloPassNotification(params);
trackingMock.trackKiloPassPurchaseCompleted.mockClear();

const replay = await processAppStoreKiloPassNotification(params);
expect(replay).toEqual({ processed: true, status: 'already_processed' });
expect(trackingMock.trackKiloPassPurchaseCompleted).not.toHaveBeenCalled();
});

it('does not track when the provider transaction was already recorded by the app', async () => {
const trackingMock = getPosthogTrackingMock();
const user = await insertTestUser();
const providerSubscriptionId = `orig-${crypto.randomUUID()}`;
const providerTransactionId = `tx-${crypto.randomUUID()}`;
const decodedTransaction = transaction({
originalTransactionId: providerSubscriptionId,
transactionId: providerTransactionId,
appAccountToken: user.app_store_account_token,
});

// App path records the purchase first (same provider transaction id).
await processAppStoreKiloPassNotification({
signedPayload: 'app-first-initial',
decodeNotification: async () =>
notification({
notificationUUID: `note-${crypto.randomUUID()}`,
notificationType: NotificationTypeV2.SUBSCRIBED,
subtype: Subtype.INITIAL_BUY,
}),
decodeTransaction: async () => decodedTransaction,
});
trackingMock.trackKiloPassPurchaseCompleted.mockClear();

const result = await processAppStoreKiloPassNotification({
signedPayload: 'assn-same-tx',
decodeNotification: async () =>
notification({
notificationUUID: `note-${crypto.randomUUID()}`,
notificationType: NotificationTypeV2.DID_RENEW,
}),
decodeTransaction: async () => decodedTransaction,
});

expect(result).toEqual({ processed: true });
expect(trackingMock.trackKiloPassPurchaseCompleted).not.toHaveBeenCalled();
});

it('tracks DID_RENEW with a new transaction as renewal', async () => {
const trackingMock = getPosthogTrackingMock();
const user = await insertTestUser();
const providerSubscriptionId = `orig-${crypto.randomUUID()}`;

await processAppStoreKiloPassNotification({
signedPayload: 'renewal-initial',
decodeNotification: async () =>
notification({
notificationUUID: `note-${crypto.randomUUID()}`,
notificationType: NotificationTypeV2.SUBSCRIBED,
subtype: Subtype.INITIAL_BUY,
}),
decodeTransaction: async () =>
transaction({
originalTransactionId: providerSubscriptionId,
appAccountToken: user.app_store_account_token,
}),
});
trackingMock.trackKiloPassPurchaseCompleted.mockClear();

const renewalTransaction = transaction({
originalTransactionId: providerSubscriptionId,
transactionId: `tx-${crypto.randomUUID()}`,
appAccountToken: user.app_store_account_token,
});
const result = await processAppStoreKiloPassNotification({
signedPayload: 'renewal',
decodeNotification: async () =>
notification({
notificationUUID: `note-${crypto.randomUUID()}`,
notificationType: NotificationTypeV2.DID_RENEW,
}),
decodeTransaction: async () => renewalTransaction,
});

expect(result).toEqual({ processed: true });
expect(trackingMock.trackKiloPassPurchaseCompleted).toHaveBeenCalledTimes(1);
expect(trackingMock.trackKiloPassPurchaseCompleted).toHaveBeenCalledWith(
expect.objectContaining({
channel: 'app_store',
distinctId: user.google_user_email,
userId: user.id,
purchaseKind: 'renewal',
providerTransactionId: renewalTransaction.transactionId,
productId: renewalTransaction.productId,
environment: renewalTransaction.environment,
})
);
});

it('tracks SUBSCRIBED with a resolved user and new transaction as initial', async () => {
const trackingMock = getPosthogTrackingMock();
const user = await insertTestUser();
const decodedTransaction = transaction({
appAccountToken: user.app_store_account_token,
});

const result = await processAppStoreKiloPassNotification({
signedPayload: 'subscribed-initial',
decodeNotification: async () =>
notification({
notificationUUID: `note-${crypto.randomUUID()}`,
notificationType: NotificationTypeV2.SUBSCRIBED,
subtype: Subtype.INITIAL_BUY,
}),
decodeTransaction: async () => decodedTransaction,
});

expect(result).toEqual({ processed: true });
expect(trackingMock.trackKiloPassPurchaseCompleted).toHaveBeenCalledTimes(1);
expect(trackingMock.trackKiloPassPurchaseCompleted).toHaveBeenCalledWith(
expect.objectContaining({
channel: 'app_store',
distinctId: user.google_user_email,
userId: user.id,
purchaseKind: 'initial',
providerTransactionId: decodedTransaction.transactionId,
productId: decodedTransaction.productId,
environment: decodedTransaction.environment,
})
);
});

it('forwards purchaseKind from completion on DID_CHANGE_RENEWAL_PREF UPGRADE', async () => {
const trackingMock = getPosthogTrackingMock();
const user = await insertTestUser();
const providerSubscriptionId = `orig-${crypto.randomUUID()}`;

await processAppStoreKiloPassNotification({
signedPayload: 'upgrade-initial',
decodeNotification: async () =>
notification({
notificationUUID: `note-${crypto.randomUUID()}`,
notificationType: NotificationTypeV2.SUBSCRIBED,
subtype: Subtype.INITIAL_BUY,
}),
decodeTransaction: async () =>
transaction({
originalTransactionId: providerSubscriptionId,
appAccountToken: user.app_store_account_token,
}),
});
trackingMock.trackKiloPassPurchaseCompleted.mockClear();

const upgradeTransaction = transaction({
originalTransactionId: providerSubscriptionId,
transactionId: `tx-${crypto.randomUUID()}`,
productId: 'kilopass.tier49.monthly.v1',
appAccountToken: user.app_store_account_token,
});
const result = await processAppStoreKiloPassNotification({
signedPayload: 'upgrade-pref',
decodeNotification: async () =>
notification({
notificationUUID: `note-${crypto.randomUUID()}`,
notificationType: NotificationTypeV2.DID_CHANGE_RENEWAL_PREF,
subtype: Subtype.UPGRADE,
}),
decodeTransaction: async () => upgradeTransaction,
});

expect(result).toEqual({ processed: true });
expect(trackingMock.trackKiloPassPurchaseCompleted).toHaveBeenCalledTimes(1);
// tier19 → tier49 within the previous purchase's period classifies as a
// same-period upgrade in completeStoreKiloPassPurchase (pinned in slice 1);
// this asserts the kind is forwarded through the ASSN path.
expect(trackingMock.trackKiloPassPurchaseCompleted).toHaveBeenCalledWith(
expect.objectContaining({
channel: 'app_store',
distinctId: user.google_user_email,
userId: user.id,
purchaseKind: 'upgrade',
providerTransactionId: upgradeTransaction.transactionId,
productId: upgradeTransaction.productId,
environment: upgradeTransaction.environment,
})
);
});
});

it('records a renewal notification and completes the subscription once', async () => {
const user = await insertTestUser();
const decodedNotification = notification();
Expand Down
Loading
Loading