Files
autofirmer-expanded/scripts/check-catalog.mjs
T
Brandon LiandClaude Opus 5.5 952e4412c4 Catalog: a build that learns a platform fetches the firms an older one skipped
A firm on a platform the build didn't know was skipped, yet the cache was
stamped current, so after updating to a build that knows the platform the
sync sent ?since= and the master answered "unchanged": the firm (Vest, on
machines that synced before 96fdd9c) stayed missing until an unrelated
catalog edit. The parser version goes to 3, so those caches fetch in full
once, and a payload now names the platforms it skipped (skippedPlatforms):
a cache whose skipped platform this build knows is no longer current. Kept
rule ids still report from such a cache. scripts/check-catalog.mjs syncs
against a stand-in master on a local port.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 14:14:43 -05:00

108 lines
5.0 KiB
JavaScript

// Checks for the firm catalog sync in lib/firm-catalog.ts, against a stand-in
// master on a local port and a throwaway database — never the live DB or the
// real master.
//
// npm run check
import { after, test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { registerHooks } from 'node:module';
import { createServer } from 'node:http';
import { once } from 'node:events';
// Resolve the extensionless local imports used by Next, without a build.
function resolveTypes(specifier, context, nextResolve) {
try {
return nextResolve(specifier, context);
} catch (err) {
if (err.code !== 'ERR_MODULE_NOT_FOUND' || !specifier.startsWith('.')) throw err;
return nextResolve(`${specifier}.ts`, context);
}
}
registerHooks({ resolve: resolveTypes });
const directory = mkdtempSync(join(tmpdir(), 'autofirmer-catalog-'));
process.env.AUTOFIRMER_DB = join(directory, 'test.sqlite');
after(() => rmSync(directory, { recursive: true, force: true }));
const { getSetting, setSetting } = await import('../lib/db.ts');
const { syncCatalog, getCachedCatalog, getKeptRuleIdsForFirm, CATALOG_PARSER_VERSION } = await import('../lib/firm-catalog.ts');
const firm = (id, name, platform) => ({
id, name, platform, keptAccountRules: [], updatedAt: '2026-10-26T19:00:00.000Z',
accountTypes: [{
id: `${id}-50k`, firmId: id, label: '50k', prefix: `${id.toUpperCase()}-`, updatedAt: '2026-10-26T19:00:00.000Z',
accountSize: 50000, profitTarget: 3000, consistency: 0, minDayPnL: 0, minTradingDays: 0, maxLoss: 3000,
maxPositionSize: 0, withdrawalStages: [],
}],
});
const apex = firm('apex', 'Apex', 'tradovate');
const vest = firm('vest', 'VestMarkets', 'vest');
// The master's catalog, answering ?since= as app/api/firms/catalog does, and
// the `since` each sync sent (null = a full fetch).
let served = { version: 41, updatedAt: '2026-10-26T19:20:00.000Z', firms: [apex, vest] };
const sinceSent = [];
const server = createServer((req, res) => {
const since = new URL(req.url, 'http://master').searchParams.get('since');
sinceSent.push(since);
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(since !== null && Number(since) === served.version
? { version: served.version, updatedAt: served.updatedAt, unchanged: true }
: served));
});
server.listen(0, '127.0.0.1');
await once(server, 'listening');
after(() => server.close());
const master = `http://127.0.0.1:${server.address().port}`;
/** This machine's cache as some build stored it, read afresh as after a restart. */
function storedCache(payload) {
setSetting('master_dashboard_url', master);
setSetting('catalog_source_url', master);
setSetting('catalog_version', String(payload.version));
setSetting('catalog_payload', JSON.stringify(payload));
globalThis.__firmCatalog = undefined;
sinceSent.length = 0;
}
const names = () => getCachedCatalog().map((f) => f.name);
test('a cache a pre-Vest build stored is fetched in full by a build that knows Vest', async (t) => {
t.mock.method(console, 'log', () => {});
// Parser 2 skipped the Vest firm and said nothing of it; the master's
// catalog hasn't changed since, so ?since= would answer "unchanged".
storedCache({ version: 41, updatedAt: served.updatedAt, firms: [apex], parser: 2 });
assert.equal((await syncCatalog()).ok, true);
assert.deepEqual(sinceSent, [null]);
assert.deepEqual(names(), ['Apex', 'VestMarkets']);
// Current from then on: the next sync asks ?since= again.
await syncCatalog();
assert.deepEqual(sinceSent, [null, '41']);
});
test('a skipped platform is named in the cache, and a build that knows it fetches in full', async (t) => {
t.mock.method(console, 'log', () => {});
t.mock.method(console, 'warn', () => {});
// The master adds a firm on a platform this build doesn't know.
served = { version: 42, updatedAt: '2026-10-27T15:00:00.000Z', firms: [apex, vest, firm('px', 'PX Firm', 'projectx')] };
storedCache({ version: 41, updatedAt: '2026-10-26T19:20:00.000Z', firms: [apex, vest], parser: CATALOG_PARSER_VERSION });
await syncCatalog();
assert.deepEqual(names(), ['Apex', 'VestMarkets']);
assert.deepEqual(JSON.parse(getSetting('catalog_payload')).skippedPlatforms, ['projectx']);
// While this build still doesn't know it, syncs stay incremental, and the
// cache still vouches for the kept rule sets of the firms it holds.
globalThis.__firmCatalog = undefined;
sinceSent.length = 0;
await syncCatalog();
assert.deepEqual(sinceSent, ['42']);
assert.deepEqual(getKeptRuleIdsForFirm('apex'), []);
// A build that knows the platform it skipped (Vest here) fetches in full.
storedCache({ version: 42, updatedAt: served.updatedAt, firms: [apex], parser: CATALOG_PARSER_VERSION, skippedPlatforms: ['vest'] });
await syncCatalog();
assert.deepEqual(sinceSent, [null]);
assert.ok(names().includes('VestMarkets'));
});