Skip to content

Commit 0f63f72

Browse files
committed
test(self-custodial): cover Important assertion gaps I3, I5, I6, I7, I8, I9, I10
1 parent 21c0b8c commit 0f63f72

6 files changed

Lines changed: 152 additions & 0 deletions

File tree

__tests__/self-custodial/auto-convert/executor.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,22 @@ describe("waitForPaymentCompleted", () => {
9797
expect(settled).toBe(true)
9898
expect(sdk.getPayment).toHaveBeenCalledTimes(2)
9999
})
100+
101+
it("does not sleep when maxAttempts is 1 (Important #9)", async () => {
102+
const setTimeoutSpy = jest.spyOn(global, "setTimeout")
103+
setTimeoutSpy.mockClear()
104+
const sdk = {
105+
getPayment: jest.fn().mockResolvedValue({ payment: { status: "Pending" } }),
106+
}
107+
108+
await waitForPaymentCompleted(sdk as never, "pid", { maxAttempts: 1, intervalMs: 10 })
109+
110+
// The `if (attempt < options.maxAttempts - 1)` guard prevents the sleep
111+
// on the final attempt, so the degenerate single-attempt case has no
112+
// setTimeout call at all.
113+
expect(setTimeoutSpy).not.toHaveBeenCalled()
114+
setTimeoutSpy.mockRestore()
115+
})
100116
})
101117

102118
describe("fetchAutoConvertMinSats", () => {

__tests__/self-custodial/auto-convert/storage.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,22 @@ describe("auto-convert storage", () => {
195195
expect(mockGetItem).toHaveBeenCalledWith(STORAGE_KEY)
196196
expect(mockSetItem.mock.calls[0][0]).toBe(STORAGE_KEY)
197197
})
198+
199+
it("withWriteLock keeps draining when a prior write rejects (Important #6)", async () => {
200+
// First setItem rejects; the chain must absorb the rejection so the
201+
// next write still gets through instead of deadlocking the queue.
202+
mockSetItem
203+
.mockRejectedValueOnce(new Error("storage transient"))
204+
.mockResolvedValue(undefined)
205+
mockGetItem.mockResolvedValue(null)
206+
207+
await addPendingAutoConvert(makeRecord({ paymentRequest: "lnbc1A" }))
208+
await addPendingAutoConvert(makeRecord({ paymentRequest: "lnbc1B" }))
209+
210+
// Two writes attempted (both reached AsyncStorage.setItem); the chain
211+
// didn't get stuck on the first rejection.
212+
expect(mockSetItem).toHaveBeenCalledTimes(2)
213+
})
198214
})
199215

200216
describe("AutoConvertPairings (Critical #2 dedup correlation)", () => {

__tests__/self-custodial/bridge/convert.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,23 @@ describe("createGetConversionQuote — BTC → USD", () => {
153153

154154
expect(quote).not.toBeNull()
155155
})
156+
157+
it("clamps the discovery destination up to minToAmount when it dominates (Important #8)", async () => {
158+
// Target=2000 cents → halfDestination=1000; minFromAmount=0 → fromMin=0;
159+
// minToAmount=1500 dominates the Math.max so the discovery destination
160+
// is the to-side floor (1500 cents → 15_000_000 token base units at 6 decimals).
161+
mockFetchLimits.mockResolvedValue({ minFromAmount: 0, minToAmount: 1500 })
162+
const sdk = createSdk()
163+
164+
await createGetConversionQuote(sdk as never)({
165+
fromAmount: toBtcMoneyAmount(5000),
166+
toAmount: toUsdMoneyAmount(2000),
167+
direction: ConvertDirection.BtcToUsd,
168+
})
169+
170+
const prepArg = sdk.prepareSendPayment.mock.calls[0][0]
171+
expect(prepArg.amount).toBe(BigInt(1500 * 10 ** 4))
172+
})
156173
})
157174

158175
describe("createGetConversionQuote — USD → BTC", () => {

__tests__/self-custodial/components/auto-convert-listener-mount.spec.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,16 @@ describe("AutoConvertListenerMount", () => {
2323
const { toJSON } = render(<AutoConvertListenerMount />)
2424
expect(toJSON()).toBeNull()
2525
})
26+
27+
it("does not swallow errors thrown by the listener hook (Important #10)", () => {
28+
// The wrapper has no try/catch around the hook; a crash must propagate
29+
// to React rather than be silently absorbed by the wrapper.
30+
mockListener.mockImplementationOnce(() => {
31+
throw new Error("listener crashed")
32+
})
33+
34+
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})
35+
expect(() => render(<AutoConvertListenerMount />)).toThrow("listener crashed")
36+
consoleErrorSpy.mockRestore()
37+
})
2638
})

__tests__/self-custodial/hooks/use-auto-convert-listener.spec.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,4 +724,79 @@ describe("useAutoConvertListener — mount replay", () => {
724724
})
725725
expect(mockExecuteAutoConvert).toHaveBeenCalledTimes(1)
726726
})
727+
728+
it("processes pending records sequentially via reduce, not in parallel (Important #3)", async () => {
729+
const sdk = makeSdk({
730+
listPayments: jest.fn().mockResolvedValue({
731+
payments: [
732+
{
733+
id: "pid-A",
734+
amount: 5000n,
735+
details: { tag: "Lightning", inner: { invoice: "lnbc1A" } },
736+
},
737+
{
738+
id: "pid-B",
739+
amount: 5000n,
740+
details: { tag: "Lightning", inner: { invoice: "lnbc1B" } },
741+
},
742+
],
743+
}),
744+
})
745+
setupDefaults(sdk)
746+
mockListPendingAutoConverts.mockResolvedValue([
747+
makeRecord({ paymentRequest: "lnbc1A" }),
748+
makeRecord({ paymentRequest: "lnbc1B" }),
749+
])
750+
751+
let resolveFirst!: (value: boolean) => void
752+
const firstSettled = new Promise<boolean>((resolve) => {
753+
resolveFirst = resolve
754+
})
755+
mockWaitForPaymentCompleted
756+
.mockImplementationOnce(() => firstSettled)
757+
.mockResolvedValueOnce(true)
758+
759+
renderHook(() => useAutoConvertListener())
760+
761+
// The reduce chain awaits the first record's processing before starting
762+
// the second; until we resolve the first, the second must not have started.
763+
await new Promise((resolve) => {
764+
setTimeout(resolve, 10)
765+
})
766+
expect(mockWaitForPaymentCompleted).toHaveBeenCalledTimes(1)
767+
768+
resolveFirst(true)
769+
770+
await waitFor(() => {
771+
expect(mockWaitForPaymentCompleted).toHaveBeenCalledTimes(2)
772+
})
773+
})
774+
775+
it("removes the record on SkippedBelowMin without firing the success toast (Important #7)", async () => {
776+
const sdk = makeSdk({
777+
listPayments: jest.fn().mockResolvedValue({
778+
payments: [
779+
{
780+
id: "pid-low",
781+
amount: 100n,
782+
details: { tag: "Lightning", inner: { invoice: "lnbc1low" } },
783+
},
784+
],
785+
}),
786+
})
787+
setupDefaults(sdk)
788+
mockListPendingAutoConverts.mockResolvedValue([
789+
makeRecord({ paymentRequest: "lnbc1low" }),
790+
])
791+
mockExecuteAutoConvert.mockResolvedValue({
792+
status: AutoConvertStatus.SkippedBelowMin,
793+
})
794+
795+
renderHook(() => useAutoConvertListener())
796+
797+
await waitFor(() => {
798+
expect(mockRemovePendingAutoConvert).toHaveBeenCalledWith("lnbc1low")
799+
})
800+
expect(mockToastShow).not.toHaveBeenCalled()
801+
})
727802
})

__tests__/self-custodial/hooks/use-receive-asset-mode.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,22 @@ describe("useReceiveAssetMode", () => {
5555
expect(result.current.assetMode).toBe("dollar")
5656
expect(result.current.isToggleDisabled).toBe(true)
5757
})
58+
59+
it("does NOT reset Dollar to Bitcoin when stable balance toggles OFF (Important #5)", () => {
60+
// Asymmetry is intentional: ON re-aligns to Dollar to enforce the new
61+
// sweep policy, but OFF preserves the user's last explicit choice so a
62+
// future "fix" that resets to BTC would silently regress receive intent.
63+
mockSelfCustodialWallet.mockReturnValue({ isStableBalanceActive: true })
64+
const { result, rerender } = renderHook(() => useReceiveAssetMode())
65+
66+
expect(result.current.assetMode).toBe("dollar")
67+
68+
mockSelfCustodialWallet.mockReturnValue({ isStableBalanceActive: false })
69+
rerender({})
70+
71+
expect(result.current.assetMode).toBe("dollar")
72+
expect(result.current.isToggleDisabled).toBe(false)
73+
})
5874
})
5975

6076
describe("loading flag (Critical #7 boot window)", () => {

0 commit comments

Comments
 (0)