Compare commits

...

5 Commits

Author SHA1 Message Date
Cursor Agent
4e2f4ffdcb [AI] Replace test() with it() to follow repo convention
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-03-05 19:00:42 +00:00
github-actions[bot]
cc4d640cf7 Add release notes for PR #7125 2026-03-04 20:38:03 +00:00
Claude
715d17f232 Remove @ts-strict-ignore from bank sync tests
Use proper non-null assertions instead of disabling strict mode.

https://claude.ai/code/session_011ebiiXRMmbiKxYMohVXL6o
2026-03-04 14:16:30 +00:00
Claude
01dbb4169c Fix shared error array in SimpleFin batch sync catch block
When simpleFinBatchSync() threw an error, all accounts received the
same errors array by reference and errors accumulated across accounts.
Each account now gets its own isolated errors array with a single error
specific to that account, matching the pattern used by accountsBankSync().

Fixes #6623

https://claude.ai/code/session_011ebiiXRMmbiKxYMohVXL6o
2026-03-04 13:38:45 +00:00
Claude
e1e839b5d1 Add failing tests for SimpleFin batch sync shared error array bug
Tests prove two bugs in simpleFinBatchSync() catch block (app.ts:1100-1115):
1. All accounts share the same errors array reference
2. Errors accumulate across accounts instead of being isolated

Related: #6623, #6651, #7114

https://claude.ai/code/session_011ebiiXRMmbiKxYMohVXL6o
2026-03-04 12:21:57 +00:00
3 changed files with 144 additions and 4 deletions

View File

@@ -0,0 +1,136 @@
import * as db from '../db';
import { loadMappings } from '../db/mappings';
import { app } from './app';
import * as bankSync from './sync';
vi.mock('./sync', async () => ({
...(await vi.importActual('./sync')),
simpleFinBatchSync: vi.fn(),
syncAccount: vi.fn(),
}));
const simpleFinBatchSyncHandler = app.handlers['simplefin-batch-sync'];
function insertBank(bank: { id: string; bank_id: string; name: string }) {
db.runQuery(
'INSERT INTO banks (id, bank_id, name, tombstone) VALUES (?, ?, ?, 0)',
[bank.id, bank.bank_id, bank.name],
);
}
async function setupSimpleFinAccounts(
accounts: Array<{
id: string;
name: string;
accountId: string;
}>,
) {
insertBank({ id: 'bank1', bank_id: 'sfin-bank', name: 'SimpleFin' });
for (const acct of accounts) {
await db.insertAccount({
id: acct.id,
name: acct.name,
bank: 'bank1',
account_id: acct.accountId,
account_sync_source: 'simpleFin',
});
}
}
beforeEach(async () => {
vi.resetAllMocks();
await global.emptyDatabase()();
await loadMappings();
});
describe('simpleFinBatchSync', () => {
describe('when batch sync throws an error', () => {
it('each account gets its own isolated errors array', async () => {
await setupSimpleFinAccounts([
{ id: 'acct1', name: 'Checking', accountId: 'ext-1' },
{ id: 'acct2', name: 'Savings', accountId: 'ext-2' },
{ id: 'acct3', name: 'Credit Card', accountId: 'ext-3' },
]);
vi.mocked(bankSync.simpleFinBatchSync).mockRejectedValue(
new Error('connection timeout'),
);
const result = await simpleFinBatchSyncHandler({ ids: [] });
expect(result).toHaveLength(3);
// Each account must have its own errors array (not shared references)
expect(result[0].res.errors).not.toBe(result[1].res.errors);
expect(result[1].res.errors).not.toBe(result[2].res.errors);
expect(result[0].res.errors).not.toBe(result[2].res.errors);
// Each account must have exactly 1 error, not N errors
expect(result[0].res.errors).toHaveLength(1);
expect(result[1].res.errors).toHaveLength(1);
expect(result[2].res.errors).toHaveLength(1);
});
it('each error references its own account', async () => {
await setupSimpleFinAccounts([
{ id: 'acct1', name: 'Checking', accountId: 'ext-1' },
{ id: 'acct2', name: 'Savings', accountId: 'ext-2' },
]);
vi.mocked(bankSync.simpleFinBatchSync).mockRejectedValue(
new Error('server error'),
);
const result = await simpleFinBatchSyncHandler({ ids: [] });
expect(result).toHaveLength(2);
// Each error must reference only the account it belongs to
expect(result[0].res.errors).toHaveLength(1);
expect(result[0].res.errors[0].accountId).toBe('acct1');
expect(result[1].res.errors).toHaveLength(1);
expect(result[1].res.errors[0].accountId).toBe('acct2');
});
});
describe('when individual accounts have errors in the response', () => {
it('per-account error_code only affects that account', async () => {
await setupSimpleFinAccounts([
{ id: 'acct1', name: 'Checking', accountId: 'ext-1' },
{ id: 'acct2', name: 'Savings', accountId: 'ext-2' },
]);
vi.mocked(bankSync.simpleFinBatchSync).mockResolvedValue([
{
accountId: 'acct1',
res: {
error_code: 'ITEM_ERROR',
error_type: 'Connection',
},
},
{
accountId: 'acct2',
res: {
added: [],
updated: [],
},
},
]);
const result = await simpleFinBatchSyncHandler({ ids: [] });
expect(result).toHaveLength(2);
// Account 1 should have an error
const acct1Result = result.find(r => r.accountId === 'acct1');
expect(acct1Result!.res.errors).toHaveLength(1);
expect(acct1Result!.res.errors[0].accountId).toBe('acct1');
// Account 2 should have no errors
const acct2Result = result.find(r => r.accountId === 'acct2');
expect(acct2Result!.res.errors).toHaveLength(0);
});
});
});

View File

@@ -1098,19 +1098,17 @@ async function simpleFinBatchSync({
});
}
} catch (err) {
const errors = [];
for (const account of accounts) {
const error = err as Error;
retVal.push({
accountId: account.id,
res: {
errors,
errors: [handleSyncError(error, account)],
newTransactions: [],
matchedTransactions: [],
updatedAccounts: [],
},
});
const error = err as Error;
errors.push(handleSyncError(error, account));
}
}

View File

@@ -0,0 +1,6 @@
---
category: Bugfixes
authors: [MatissJanis]
---
Refactor error handling to isolate errors per account in SimpleFin batch synchronization.