Files
SubMiner/src/core/services/field-grouping.test.ts
T

273 lines
8.4 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { KikuFieldGroupingChoice, KikuFieldGroupingRequestData } from '../../types';
import { createFieldGroupingCallback } from './field-grouping';
function makeRequestData(): KikuFieldGroupingRequestData {
return {
original: {
noteId: 1,
expression: 'a',
sentencePreview: 'a',
hasAudio: false,
hasImage: false,
isOriginal: true,
},
duplicate: {
noteId: 2,
expression: 'a',
sentencePreview: 'b',
hasAudio: false,
hasImage: false,
isOriginal: false,
},
};
}
/**
* Mirrors how main stores the resolver: it wraps the callback's resolver in a
* sequence-guarded closure, so the value read back is never identity-equal to the
* callback's own `finish`. The old `getResolver() === finish` clear-guard therefore
* never matched and leaked the resolver, wedging every later grouping attempt.
*/
function createWrappedResolverStore() {
let stored: ((choice: KikuFieldGroupingChoice) => void) | null = null;
return {
getResolver: () => stored,
setResolver: (resolver: ((choice: KikuFieldGroupingChoice) => void) | null) => {
stored = resolver ? (choice) => resolver(choice) : null;
},
respond: (choice: KikuFieldGroupingChoice) => {
stored?.(choice);
},
};
}
test('field grouping callback clears the wrapped resolver after a renderer response', async () => {
const store = createWrappedResolverStore();
let visible = false;
const callback = createFieldGroupingCallback({
getVisibleOverlayVisible: () => visible,
setVisibleOverlayVisible: (next) => {
visible = next;
},
getResolver: store.getResolver,
setResolver: store.setResolver,
sendRequestToVisibleOverlay: () => true,
});
const pending = callback(makeRequestData());
await Promise.resolve();
assert.notEqual(store.getResolver(), null);
const choice: KikuFieldGroupingChoice = {
keepNoteId: 1,
deleteNoteId: 2,
deleteDuplicate: true,
cancelled: false,
};
store.respond(choice);
assert.deepEqual(await pending, choice);
assert.equal(store.getResolver(), null);
});
test('field grouping callback does not reject the next request after a response', async () => {
const store = createWrappedResolverStore();
const callback = createFieldGroupingCallback({
getVisibleOverlayVisible: () => false,
setVisibleOverlayVisible: () => {},
getResolver: store.getResolver,
setResolver: store.setResolver,
sendRequestToVisibleOverlay: () => true,
});
const first = callback(makeRequestData());
await Promise.resolve();
store.respond({ keepNoteId: 1, deleteNoteId: 2, deleteDuplicate: true, cancelled: false });
const firstChoice = await first;
assert.equal(firstChoice.cancelled, false);
// The second attempt must reach the renderer, not short-circuit to an instant cancel.
const second = callback(makeRequestData());
await Promise.resolve();
assert.notEqual(store.getResolver(), null);
store.respond({ keepNoteId: 2, deleteNoteId: 1, deleteDuplicate: false, cancelled: false });
const secondChoice = await second;
assert.equal(secondChoice.cancelled, false);
assert.equal(secondChoice.keepNoteId, 2);
});
test('field grouping callback dismisses the modal UI when the send fails', async () => {
const store = createWrappedResolverStore();
let dismissed = 0;
const callback = createFieldGroupingCallback({
getVisibleOverlayVisible: () => false,
setVisibleOverlayVisible: () => {},
getResolver: store.getResolver,
setResolver: store.setResolver,
sendRequestToVisibleOverlay: () => false,
dismissModalUi: () => {
dismissed += 1;
},
});
const result = await callback(makeRequestData());
assert.equal(result.cancelled, true);
assert.equal(dismissed, 1);
assert.equal(store.getResolver(), null);
});
test('field grouping callback handles modal dismiss failures on send failure', async () => {
const store = createWrappedResolverStore();
const originalConsoleError = console.error;
console.error = () => {};
try {
const callback = createFieldGroupingCallback({
getVisibleOverlayVisible: () => false,
setVisibleOverlayVisible: () => {},
getResolver: store.getResolver,
setResolver: store.setResolver,
sendRequestToVisibleOverlay: () => false,
dismissModalUi: () => {
throw new Error('dismiss failed');
},
});
const result = await callback(makeRequestData());
assert.equal(result.cancelled, true);
assert.equal(store.getResolver(), null);
} finally {
console.error = originalConsoleError;
}
});
test('field grouping callback handles modal dismiss failures on timeout', async () => {
const store = createWrappedResolverStore();
const originalConsoleError = console.error;
console.error = () => {};
try {
const callback = createFieldGroupingCallback({
getVisibleOverlayVisible: () => false,
setVisibleOverlayVisible: () => {},
getResolver: store.getResolver,
setResolver: store.setResolver,
sendRequestToVisibleOverlay: () => true,
dismissModalUi: () => {
throw new Error('dismiss failed');
},
responseTimeoutMs: 5,
});
const result = await callback(makeRequestData());
assert.equal(result.cancelled, true);
assert.equal(store.getResolver(), null);
} finally {
console.error = originalConsoleError;
}
});
test('field grouping callback reports modal dismiss failures', async () => {
const store = createWrappedResolverStore();
const errors: unknown[] = [];
const originalConsoleError = console.error;
console.error = (...args: unknown[]) => {
errors.push(args);
};
try {
const callback = createFieldGroupingCallback({
getVisibleOverlayVisible: () => false,
setVisibleOverlayVisible: () => {},
getResolver: store.getResolver,
setResolver: store.setResolver,
sendRequestToVisibleOverlay: () => false,
dismissModalUi: () => {
throw new Error('dismiss failed');
},
});
await callback(makeRequestData());
assert.equal(errors.length, 1);
} finally {
console.error = originalConsoleError;
}
});
test('field grouping callback dismisses the modal UI when the response times out', async () => {
const store = createWrappedResolverStore();
let dismissed = 0;
const callback = createFieldGroupingCallback({
getVisibleOverlayVisible: () => false,
setVisibleOverlayVisible: () => {},
getResolver: store.getResolver,
setResolver: store.setResolver,
sendRequestToVisibleOverlay: () => true,
dismissModalUi: () => {
dismissed += 1;
},
responseTimeoutMs: 5,
});
const result = await callback(makeRequestData());
assert.equal(result.cancelled, true);
assert.equal(dismissed, 1);
assert.equal(store.getResolver(), null);
});
test('field grouping callback does not dismiss the modal UI on a normal response', async () => {
const store = createWrappedResolverStore();
let dismissed = 0;
const callback = createFieldGroupingCallback({
getVisibleOverlayVisible: () => false,
setVisibleOverlayVisible: () => {},
getResolver: store.getResolver,
setResolver: store.setResolver,
sendRequestToVisibleOverlay: () => true,
dismissModalUi: () => {
dismissed += 1;
},
responseTimeoutMs: 10000,
});
const pending = callback(makeRequestData());
await Promise.resolve();
store.respond({ keepNoteId: 1, deleteNoteId: 2, deleteDuplicate: true, cancelled: false });
await pending;
assert.equal(dismissed, 0);
});
test('field grouping callback rejects a concurrent request while one is pending', async () => {
const store = createWrappedResolverStore();
let sends = 0;
let dismissed = 0;
const callback = createFieldGroupingCallback({
getVisibleOverlayVisible: () => false,
setVisibleOverlayVisible: () => {},
getResolver: store.getResolver,
setResolver: store.setResolver,
sendRequestToVisibleOverlay: () => {
sends += 1;
return true;
},
dismissModalUi: () => {
dismissed += 1;
},
responseTimeoutMs: 10000,
});
const first = callback(makeRequestData());
await Promise.resolve();
assert.equal(sends, 1);
const second = await callback(makeRequestData());
assert.equal(second.cancelled, true);
assert.equal(sends, 1);
assert.equal(dismissed, 0);
store.respond({ keepNoteId: 1, deleteNoteId: 2, deleteDuplicate: true, cancelled: false });
await first;
});