feat: add pinned inventory location enrichment
This commit is contained in:
@@ -3,6 +3,8 @@ import { makeFunctionReference } from 'convex/server';
|
||||
import { convexTest } from 'convex-test';
|
||||
import schema from '../src/convex/schema';
|
||||
import { EsiRateLimitError } from '../src/lib/server/esi';
|
||||
import { createReferenceEnrichmentPin, deterministicEnrichmentVersion } from '../src/lib/domain/inventory-location';
|
||||
import { InventoryLocationService } from '../src/lib/server/inventory/location-service';
|
||||
import { processSyncJob } from '../workers/sync';
|
||||
|
||||
process.env.CONVEX_BRIDGE_SECRET = 'test-bridge-secret';
|
||||
@@ -163,6 +165,146 @@ describe('T07 durable asset scheduling', () => {
|
||||
await expect(t.mutation(mutation('bridgeFailSyncJob'), { ...worker, jobId: cancelled.jobId, leaseToken: cancelled.leaseToken })).rejects.toThrow();
|
||||
});
|
||||
|
||||
test('stages private enrichment with the exact generation and exposes an atomic read pin', async () => {
|
||||
const t = convexTest({ schema, modules });
|
||||
const account = await login(t, { characterId: 726, sessionHash: 'enrichment-session' });
|
||||
await enqueue(t, account, 726, 'enrichment-session');
|
||||
const claimed = await t.mutation(mutation('bridgeClaimSyncJob'), { ...worker, workerId: 'worker-enrichment' });
|
||||
const generation = await t.mutation(mutation('bridgeBeginSyncJob'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken });
|
||||
const asset = { itemId: '1', characterId: 726, typeId: 34, quantity: 1, locationId: '60003760', locationType: 'station', locationFlag: 'Hangar' };
|
||||
await t.mutation(mutation('bridgeStageSyncAssets'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken, index: 0, assets: [asset] });
|
||||
const row = {
|
||||
...asset, parentItemId: null, root: { kind: 'external', id: '60003760', locationType: 'station', locationFlag: 'Hangar' }, depth: 0, breadcrumbs: [],
|
||||
fitted: { classification: 'unknown', rawFlag: 'Hangar', reason: 'ambiguous_special_bay' }, fittedClassification: 'unknown', isFitted: null,
|
||||
unresolved: [], customName: null, nameStatus: 'not_requested', position: null, locationStatus: 'not_requested',
|
||||
location: { kind: 'station', id: '60003760', name: 'Jita', parentId: null, evidence: 'system_listed', sourceVerified: false, referenceVersion: 'ref-v1', dockingPermission: 'unknown', system: null, region: null },
|
||||
};
|
||||
await t.mutation(mutation('bridgeStageSyncEnrichment'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken, index: 0, rows: [row] });
|
||||
const inputPin = createReferenceEnrichmentPin({ assetGeneration: generation, referenceVersion: 'ref-v1', authority: { userId: account.userId, principal: 'queue-owner', characterId: 726, scopes: ['esi-assets.read_assets.v1'], grantRevision: 1, authorizationEpoch: 1, credentialVersion: 1 }, referenceFacts: [{ kind: 'station', id: '60003760', name: 'Jita', parentId: null, evidence: 'system_listed', sourceVerified: false, systemId: null, regionId: null, referenceVersion: 'ref-v1' }] });
|
||||
await t.mutation(mutation('bridgeCommitSyncJob'), {
|
||||
...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken, chunkCount: 1, syncedAt: Date.now() - 1_000, expiresAt: Date.now() + 60_000,
|
||||
enrichmentStatus: 'partial', enrichmentVersion: deterministicEnrichmentVersion(inputPin), enrichmentInputPin: inputPin, enrichmentChunkCount: 1, enrichmentUnresolvedCount: 0,
|
||||
});
|
||||
const page = await t.query(query('bridgeAssetsPage'), { ...bridge, userId: account.userId, characterId: 726, paginationOpts: { numItems: 10, cursor: null } });
|
||||
expect(page).toMatchObject({ generation, enrichmentStatus: 'partial', enrichmentVersion: deterministicEnrichmentVersion(inputPin) });
|
||||
const manifest = await t.query(query('bridgeEnrichmentManifest'), { ...bridge, userId: account.userId, characterId: 726, expectedGeneration: generation });
|
||||
expect(manifest).toMatchObject({ generation, status: 'partial', version: deterministicEnrichmentVersion(inputPin), unresolvedCount: 0 });
|
||||
const chunks = await t.query(query('bridgeEnrichmentChunksPage'), { ...bridge, userId: account.userId, characterId: 726, expectedGeneration: generation, paginationOpts: { numItems: 10, cursor: null } });
|
||||
expect(chunks.chunks[0]?.rows[0]).toMatchObject({ itemId: '1', location: { dockingPermission: 'unknown' } });
|
||||
await expect(t.query(query('bridgeEnrichmentManifest'), { ...bridge, userId: 'other-user', characterId: 726, expectedGeneration: generation })).resolves.toBeNull();
|
||||
});
|
||||
|
||||
test('enrichment first page reports the active generation after a rollover', async () => {
|
||||
const t = convexTest({ schema, modules });
|
||||
const account = await login(t, { characterId: 728, sessionHash: 'enrichment-rollover-session' });
|
||||
await enqueue(t, account, 728, 'enrichment-rollover-session');
|
||||
const first = await t.mutation(mutation('bridgeClaimSyncJob'), { ...worker, workerId: 'enrichment-rollover-worker-1' });
|
||||
const generationOne = await t.mutation(mutation('bridgeBeginSyncJob'), { ...worker, jobId: first.jobId, leaseToken: first.leaseToken });
|
||||
await t.mutation(mutation('bridgeCommitSyncJob'), { ...worker, jobId: first.jobId, leaseToken: first.leaseToken, chunkCount: 0, syncedAt: Date.now() - 1_000, expiresAt: Date.now() + 60_000 });
|
||||
await enqueue(t, account, 728, 'enrichment-rollover-session');
|
||||
const second = await t.mutation(mutation('bridgeClaimSyncJob'), { ...worker, workerId: 'enrichment-rollover-worker-2' });
|
||||
const generationTwo = await t.mutation(mutation('bridgeBeginSyncJob'), { ...worker, jobId: second.jobId, leaseToken: second.leaseToken });
|
||||
await t.mutation(mutation('bridgeCommitSyncJob'), { ...worker, jobId: second.jobId, leaseToken: second.leaseToken, chunkCount: 0, syncedAt: Date.now() - 1_000, expiresAt: Date.now() + 60_000 });
|
||||
expect(generationTwo).not.toBe(generationOne);
|
||||
const page = await t.query(query('bridgeEnrichmentChunksPage'), { ...bridge, userId: account.userId, characterId: 728, expectedGeneration: generationOne, paginationOpts: { numItems: 10, cursor: null } });
|
||||
expect(page).toMatchObject({ chunks: [], generation: generationTwo, isDone: true });
|
||||
});
|
||||
|
||||
test('commit rejects forged public metadata even when the root ID is correct', async () => {
|
||||
const t = convexTest({ schema, modules });
|
||||
const account = await login(t, { characterId: 727, sessionHash: 'forged-reference-facts-session' });
|
||||
await enqueue(t, account, 727, 'forged-reference-facts-session');
|
||||
const claimed = await t.mutation(mutation('bridgeClaimSyncJob'), { ...worker, workerId: 'forged-reference-facts-worker' });
|
||||
const generation = await t.mutation(mutation('bridgeBeginSyncJob'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken });
|
||||
const asset = { itemId: '2', characterId: 727, typeId: 34, quantity: 1, locationId: '60003760', locationType: 'station', locationFlag: 'Hangar' };
|
||||
await t.mutation(mutation('bridgeStageSyncAssets'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken, index: 0, assets: [asset] });
|
||||
const row = { ...asset, parentItemId: null, root: { kind: 'external', id: '60003760', locationType: 'station', locationFlag: 'Hangar' }, depth: 0, breadcrumbs: [], fitted: { classification: 'unknown', rawFlag: 'Hangar', reason: 'ambiguous_special_bay' }, fittedClassification: 'unknown', isFitted: null, unresolved: [], customName: null, nameStatus: 'not_requested', position: null, locationStatus: 'not_requested', location: { kind: 'station', id: '60003760', name: 'Forged', parentId: null, evidence: 'system_listed', sourceVerified: false, referenceVersion: 'ref-v1', dockingPermission: 'unknown', system: null, region: null } };
|
||||
await t.mutation(mutation('bridgeStageSyncEnrichment'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken, index: 0, rows: [row] });
|
||||
const inputPin = createReferenceEnrichmentPin({ assetGeneration: generation, referenceVersion: 'ref-v1', authority: { userId: account.userId, principal: 'queue-owner', characterId: 727, scopes: ['esi-assets.read_assets.v1'], grantRevision: 1, authorizationEpoch: 1, credentialVersion: 1 }, referenceFacts: [{ kind: 'station', id: '60003760', name: 'Jita', parentId: null, evidence: 'system_listed', sourceVerified: false, systemId: null, regionId: null, referenceVersion: 'ref-v1' }] });
|
||||
await expect(t.mutation(mutation('bridgeCommitSyncJob'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken, chunkCount: 1, syncedAt: Date.now() - 1_000, expiresAt: Date.now() + 60_000, enrichmentStatus: 'partial', enrichmentVersion: deterministicEnrichmentVersion(inputPin), enrichmentInputPin: inputPin, enrichmentChunkCount: 1, enrichmentUnresolvedCount: 0 })).rejects.toThrow('Public location facts');
|
||||
});
|
||||
|
||||
test('private structure rows must match pinned source facts at commit', async () => {
|
||||
const t = convexTest({ schema, modules });
|
||||
const priorAllowlist = process.env.APP_CAPABILITY_ALLOWLIST;
|
||||
process.env.APP_CAPABILITY_ALLOWLIST = `${priorAllowlist ?? ''},inventory_locations`;
|
||||
const account = await login(t, { characterId: 731, sessionHash: 'private-facts-session', capability: 'inventory_locations', scopes: ['esi-assets.read_assets.v1', 'esi-universe.read_structures.v1'], requestedScopes: ['esi-assets.read_assets.v1', 'esi-universe.read_structures.v1'] });
|
||||
process.env.APP_CAPABILITY_ALLOWLIST = priorAllowlist;
|
||||
await enqueue(t, account, 731, 'private-facts-session');
|
||||
const claimed = await t.mutation(mutation('bridgeClaimSyncJob'), { ...worker, workerId: 'private-facts-worker' });
|
||||
const generation = await t.mutation(mutation('bridgeBeginSyncJob'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken });
|
||||
const asset = { itemId: '8000000000000001', characterId: 731, typeId: 34, quantity: 1, locationId: '8000000000000001', locationType: 'other', locationFlag: 'Hangar' };
|
||||
await t.mutation(mutation('bridgeStageSyncAssets'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken, index: 0, assets: [asset] });
|
||||
const service = new InventoryLocationService({ reference: { pinnedVersion: 'ref-v1', pin() { return this; }, close() {}, async types() { return []; }, async systems() { return []; }, async regions() { return []; }, async stations() { return []; }, async constellations() { return []; }, async searchTypes() { return []; }, async searchTypesByName() { return []; } } as any, structures: { async access() { return { structureId: asset.locationId, characterId: 731, status: 'accessible', evidence: 'esi_structure_access', name: 'TRUSTED PRIVATE', observedAt: 100, expiresAt: 1_000, dockingPermission: 'unknown' }; } }, now: () => 200 });
|
||||
const authority = { userId: account.userId, principal: 'queue-owner', characterId: 731, scopes: ['esi-assets.read_assets.v1', 'esi-universe.read_structures.v1'], grantRevision: 1, authorizationEpoch: 1, credentialVersion: 1 };
|
||||
const enrichment = await service.enrich({ assets: [asset], authority, assetGeneration: generation });
|
||||
const forged = structuredClone(enrichment.rows) as any[];
|
||||
forged[0] = { ...forged[0], location: { ...forged[0].location, name: 'FORGED PRIVATE' } };
|
||||
await t.mutation(mutation('bridgeStageSyncEnrichment'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken, index: 0, rows: forged });
|
||||
await expect(t.mutation(mutation('bridgeCommitSyncJob'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken, chunkCount: 1, syncedAt: Date.now() - 1_000, expiresAt: Date.now() + 60_000, enrichmentStatus: 'partial', enrichmentVersion: enrichment.enrichmentVersion, enrichmentInputPin: enrichment.inputPin, enrichmentChunkCount: 1, enrichmentUnresolvedCount: enrichment.unresolved.length })).rejects.toThrow('Structure row facts');
|
||||
});
|
||||
|
||||
test('service output with forbidden structure evidence stages and commits as explicit partial enrichment', async () => {
|
||||
const t = convexTest({ schema, modules });
|
||||
const account = await login(t, { characterId: 729, sessionHash: 'forbidden-persistence-session' });
|
||||
await enqueue(t, account, 729, 'forbidden-persistence-session');
|
||||
const claimed = await t.mutation(mutation('bridgeClaimSyncJob'), { ...worker, workerId: 'forbidden-persistence-worker' });
|
||||
const generation = await t.mutation(mutation('bridgeBeginSyncJob'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken });
|
||||
const asset = { itemId: '8000000000000001', characterId: 729, typeId: 34, quantity: 1, locationId: '8000000000000001', locationType: 'other', locationFlag: 'Hangar' };
|
||||
await t.mutation(mutation('bridgeStageSyncAssets'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken, index: 0, assets: [asset] });
|
||||
const service = new InventoryLocationService({
|
||||
reference: { pinnedVersion: 'ref-v1', pin() { return this; }, close() {}, async types() { return []; }, async systems() { return []; }, async regions() { return []; }, async stations() { return []; }, async constellations() { return []; }, async searchTypes() { return []; }, async searchTypesByName() { return []; } } as any,
|
||||
structures: { async access() { return { structureId: asset.locationId, characterId: 729, status: 'forbidden', evidence: 'esi_forbidden', name: null, observedAt: 100, expiresAt: 1_000, dockingPermission: 'unknown' }; } },
|
||||
now: () => 200,
|
||||
});
|
||||
const enrichment = await service.enrich({ assets: [asset], authority: { userId: account.userId, principal: 'queue-owner', characterId: 729, scopes: ['esi-assets.read_assets.v1'], grantRevision: 1, authorizationEpoch: 1, credentialVersion: 1 }, assetGeneration: generation });
|
||||
expect(enrichment.rows[0]?.location).toMatchObject({ kind: 'structure', evidence: 'unknown', name: null });
|
||||
expect(enrichment.rows[0]?.unresolved).toContain('forbidden_structure');
|
||||
await t.mutation(mutation('bridgeStageSyncEnrichment'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken, index: 0, rows: enrichment.rows });
|
||||
await t.mutation(mutation('bridgeCommitSyncJob'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken, chunkCount: 1, syncedAt: Date.now() - 1_000, expiresAt: Date.now() + 60_000, enrichmentStatus: enrichment.status, enrichmentVersion: enrichment.enrichmentVersion, enrichmentInputPin: enrichment.inputPin, enrichmentChunkCount: 1, enrichmentUnresolvedCount: enrichment.unresolved.length });
|
||||
const chunks = await t.query(query('bridgeEnrichmentChunksPage'), { ...bridge, userId: account.userId, characterId: 729, expectedGeneration: generation, paginationOpts: { numItems: 10, cursor: null } });
|
||||
expect(chunks.chunks[0]?.rows[0]).toMatchObject({ location: { evidence: 'unknown', name: null }, unresolved: ['forbidden_structure'] });
|
||||
});
|
||||
|
||||
test('indexed persistence bounds a 2.5k raw generation without repeated full scans', async () => {
|
||||
const t = convexTest({ schema, modules });
|
||||
const account = await login(t, { characterId: 750, sessionHash: 'large-persistence-session' });
|
||||
await enqueue(t, account, 750, 'large-persistence-session');
|
||||
const claimed = await t.mutation(mutation('bridgeClaimSyncJob'), { ...worker, workerId: 'large-persistence-worker' });
|
||||
await t.mutation(mutation('bridgeBeginSyncJob'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken });
|
||||
for (let index = 0; index < 10; index += 1) {
|
||||
const assets = Array.from({ length: 250 }, (_, offset) => ({ itemId: String(index * 250 + offset + 1), characterId: 750, typeId: 34, quantity: 1, locationId: '60003760', locationType: 'station', locationFlag: 'Hangar' }));
|
||||
await t.mutation(mutation('bridgeStageSyncAssets'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken, index, assets });
|
||||
}
|
||||
await t.mutation(mutation('bridgeCommitSyncJob'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken, chunkCount: 10, syncedAt: Date.now() - 1_000, expiresAt: Date.now() + 60_000, enrichmentStatus: 'failed', enrichmentChunkCount: 0, enrichmentUnresolvedCount: 2_500, enrichmentError: 'bounded test fallback' });
|
||||
const page = await t.query(query('bridgeAssetsPage'), { ...bridge, userId: account.userId, characterId: 750, paginationOpts: { numItems: 1, cursor: null } });
|
||||
expect(page.page).toHaveLength(250);
|
||||
});
|
||||
|
||||
test('missing parents, unknown references, and helper misses persist as explicit partial rows', async () => {
|
||||
const t = convexTest({ schema, modules });
|
||||
const account = await login(t, { characterId: 730, sessionHash: 'partial-row-session' });
|
||||
await enqueue(t, account, 730, 'partial-row-session');
|
||||
const claimed = await t.mutation(mutation('bridgeClaimSyncJob'), { ...worker, workerId: 'partial-row-worker' });
|
||||
const generation = await t.mutation(mutation('bridgeBeginSyncJob'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken });
|
||||
const rows = [
|
||||
{ itemId: '100', characterId: 730, typeId: 34, quantity: 1, locationId: '999', locationType: 'item', locationFlag: 'Cargo' },
|
||||
{ itemId: '101', characterId: 730, typeId: 35, quantity: 1, locationId: '60009999', locationType: 'station', locationFlag: 'Hangar' },
|
||||
];
|
||||
await t.mutation(mutation('bridgeStageSyncAssets'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken, index: 0, assets: rows });
|
||||
const observation = { requestedIds: ['100'], names: [], missing: ['100'], retrievedAt: 100, expiresAt: 1_000, complete: false };
|
||||
const forbiddenObservation = { state: 'forbidden' as const, requestedIds: ['101'], names: [], missing: [], retrievedAt: 100, expiresAt: 1_000, complete: false };
|
||||
const locationObservation = { requestedIds: ['100'], locations: [], missing: ['100'], retrievedAt: 100, expiresAt: 1_000, complete: false };
|
||||
const unavailableLocationObservation = { state: 'unavailable' as const, requestedIds: ['101'], locations: [], missing: [], retrievedAt: 100, expiresAt: 1_000, complete: false };
|
||||
const service = new InventoryLocationService({ reference: { pinnedVersion: 'ref-v1', pin() { return this; }, close() {}, async types() { return []; }, async systems() { return []; }, async regions() { return []; }, async stations() { return []; }, async constellations() { return []; }, async searchTypes() { return []; }, async searchTypesByName() { return []; } } as any, now: () => 200 });
|
||||
const enrichment = await service.enrich({ assets: rows, authority: { userId: account.userId, principal: 'queue-owner', characterId: 730, scopes: ['esi-assets.read_assets.v1'], grantRevision: 1, authorizationEpoch: 1, credentialVersion: 1 }, assetGeneration: generation, namesBatches: [observation, forbiddenObservation], locationsBatches: [locationObservation, unavailableLocationObservation] });
|
||||
expect(enrichment.rows.map((row) => row.unresolved).flat()).toEqual(expect.arrayContaining(['missing_parent', 'reference_unknown']));
|
||||
await t.mutation(mutation('bridgeStageSyncEnrichment'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken, index: 0, rows: enrichment.rows });
|
||||
await t.mutation(mutation('bridgeCommitSyncJob'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken, chunkCount: 1, syncedAt: Date.now() - 1_000, expiresAt: Date.now() + 60_000, enrichmentStatus: 'partial', enrichmentVersion: enrichment.enrichmentVersion, enrichmentInputPin: enrichment.inputPin, enrichmentChunkCount: 1, enrichmentUnresolvedCount: enrichment.unresolved.length });
|
||||
const chunks = await t.query(query('bridgeEnrichmentChunksPage'), { ...bridge, userId: account.userId, characterId: 730, expectedGeneration: generation, paginationOpts: { numItems: 10, cursor: null } });
|
||||
expect(chunks.chunks[0]?.rows.flatMap((row: any) => row.unresolved)).toEqual(expect.arrayContaining(['missing_parent', 'reference_unknown']));
|
||||
});
|
||||
|
||||
test('authorization quarantine fences both stale success and stale failure', async () => {
|
||||
const t = convexTest({ schema, modules });
|
||||
const account = await login(t, { characterId: 727, sessionHash: 'quarantine-session' });
|
||||
@@ -174,6 +316,38 @@ describe('T07 durable asset scheduling', () => {
|
||||
await expect(t.mutation(mutation('bridgeFailSyncJob'), { ...worker, jobId: claimed.jobId, leaseToken: claimed.leaseToken })).rejects.toThrow();
|
||||
});
|
||||
|
||||
test('worker batches helper calls and publishes enrichment with the staged authority pin', async () => {
|
||||
const calls: string[] = [];
|
||||
const claimed = { id: 'job-location', jobId: 'job-location', userId: 'user-location', characterId: 730, leaseToken: 'lease-location', status: 'running', attempts: 1, dueAt: Date.now(), createdAt: Date.now(), updatedAt: Date.now(), grantRevision: 2, authorizationEpoch: 3, credentialVersion: 4, scopes: ['esi-assets.read_assets.v1'] } as any;
|
||||
const assets = Array.from({ length: 1_001 }, (_, index) => ({ itemId: String(index + 1), characterId: 730, typeId: 34, quantity: 1, locationId: '60003760', locationType: 'station', locationFlag: 'Hangar' }));
|
||||
const store = {
|
||||
claimSyncJob: async () => claimed,
|
||||
renewSyncJob: async () => true,
|
||||
acquireRefreshLease: async () => ({ leaseToken: 'refresh-location', expiresAt: Date.now() + 120_000, expectedVersion: 1, encryptedTokens: 'sealed', tokenExpiresAt: Date.now() + 120_000, scopes: claimed.scopes, grantRevision: 2, authorizationEpoch: 3, credentialVersion: 4 }),
|
||||
refreshSyncJobAuthority: async () => ({ grantRevision: 2, authorizationEpoch: 3, credentialVersion: 4, scopes: claimed.scopes }),
|
||||
beginSyncJob: async () => 'generation-location', cooldown: async () => null,
|
||||
stageSyncAssets: async () => {},
|
||||
stageSyncEnrichment: async (_job: string, _lease: string, index: number) => { calls.push(`enrichment:${index}`); },
|
||||
commitSyncJob: async (_job: string, _lease: string, _chunks: number, _synced: number, _expires: number, enrichment: any) => { calls.push(`commit:${enrichment.status}:${enrichment.chunkCount}`); return true; },
|
||||
failSyncJob: async () => true, deferSyncJob: async () => true,
|
||||
};
|
||||
const esi = {
|
||||
getAssets: async () => ({ assets, syncedAt: Date.now() - 1_000, expiresAt: Date.now() + 60_000 }),
|
||||
getAssetNames: async (_character: number, ids: string[]) => ({ requestedIds: ids, names: [], missing: ids, retrievedAt: 1, expiresAt: 60_000, complete: false }),
|
||||
getAssetLocations: async (_character: number, ids: string[]) => ({ requestedIds: ids, locations: [], missing: ids, retrievedAt: 1, expiresAt: 60_000, complete: false }),
|
||||
};
|
||||
let capturedNames = 0; let capturedLocations = 0;
|
||||
await processSyncJob({
|
||||
store: store as any,
|
||||
cipher: { open: async () => ({ characterId: 730, ownerHash: 'owner', accessToken: 'access', refreshToken: 'refresh', expiresAt: Date.now() + 120_000, scopes: claimed.scopes }) } as any,
|
||||
sso: {} as any, esi: esi as any, workerId: 'worker-location', pollMs: 1, leaseMs: 600_000, heartbeatMs: 60_000, sleep: async () => {},
|
||||
locationServiceFactory: () => ({ enrich: async (request: any) => { capturedNames = request.namesBatches.length; capturedLocations = request.locationsBatches.length; return { status: 'partial', rows: [], unresolved: [{ itemId: null, reasons: ['unavailable_structure'] }], enrichmentVersion: 'containment-v1-worker', inputPin: { assetGeneration: 'generation-location' } }; } } as any),
|
||||
});
|
||||
expect(capturedNames).toBe(2);
|
||||
expect(capturedLocations).toBe(2);
|
||||
expect(calls).toEqual(['commit:partial:0']);
|
||||
});
|
||||
|
||||
test('worker retries applied-then-lost staging and commit acknowledgements idempotently', async () => {
|
||||
const events: string[] = [];
|
||||
let stageCalls = 0; let commitCalls = 0;
|
||||
|
||||
Reference in New Issue
Block a user