mirror of
https://github.com/uppinote20/obsidian-auto-note-importer.git
synced 2026-07-22 05:48:42 +00:00
test(field-mapper): add cross-provider parity contract
provider별 테스트는 각자의 불변식만 검증할 뿐, 새 provider가 공통 계약을 어겨도 잡지 못하는 갭이 있었다. provider-registry에 등록된 모든 매퍼를 동적으로 탐색해 공유 계약을 한곳에서 검증한다. - filename-safe ⊆ subfolder-safe, subfolder-safe 타입이 안전한 표준 타입으로 매핑되는지, 프로토타입 체인 이름(toString 등)의 fail-closed, 열거 메서드의 sorted+unique 검증 - airtable/seatable/supabase 존재를 floor guard로 고정해 등록 회귀 시 it.each([])가 빈 통과(vacuous pass)하는 것을 방지
This commit is contained in:
parent
ec8a33f4f6
commit
b07341701b
1 changed files with 109 additions and 0 deletions
109
tests/services/mapper-parity.test.ts
Normal file
109
tests/services/mapper-parity.test.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/**
|
||||
* Cross-provider FieldTypeMapper contract tests.
|
||||
* @covers src/services/provider-registry.ts
|
||||
* @covers src/services/airtable-field-mapper.ts
|
||||
* @covers src/services/seatable-field-mapper.ts
|
||||
* @covers src/services/supabase-field-mapper.ts
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
CREDENTIAL_TYPES,
|
||||
type CredentialType,
|
||||
type FieldTypeMapper,
|
||||
type StandardFieldType,
|
||||
} from '../../src/types';
|
||||
import {
|
||||
getFieldTypeMapper,
|
||||
hasFieldTypeMapper,
|
||||
} from '../../src/services/provider-registry';
|
||||
|
||||
const PROTOTYPE_CHAIN_NAMES = [
|
||||
'toString',
|
||||
'constructor',
|
||||
'hasOwnProperty',
|
||||
'valueOf',
|
||||
'__proto__',
|
||||
] as const;
|
||||
|
||||
const UNSAFE_SUBFOLDER_STANDARD_TYPES: ReadonlySet<StandardFieldType> = new Set([
|
||||
'attachment',
|
||||
'link',
|
||||
'unknown',
|
||||
]);
|
||||
|
||||
type MapperCase = {
|
||||
type: CredentialType;
|
||||
mapper: FieldTypeMapper;
|
||||
};
|
||||
|
||||
function getRegisteredMapperCases(): MapperCase[] {
|
||||
return CREDENTIAL_TYPES
|
||||
.filter(hasFieldTypeMapper)
|
||||
.map(type => ({
|
||||
type,
|
||||
mapper: getFieldTypeMapper(type),
|
||||
}));
|
||||
}
|
||||
|
||||
function expectSortedUnique(provider: CredentialType, method: string, types: readonly string[]): void {
|
||||
expect(
|
||||
[...types],
|
||||
`${provider}.${method}() should be sorted`,
|
||||
).toEqual([...types].sort());
|
||||
expect(
|
||||
new Set(types).size,
|
||||
`${provider}.${method}() should not contain duplicates`,
|
||||
).toBe(types.length);
|
||||
}
|
||||
|
||||
function assertFieldTypeMapperContract(provider: CredentialType, mapper: FieldTypeMapper): void {
|
||||
for (const type of mapper.getFilenameSafeTypes()) {
|
||||
expect(
|
||||
mapper.isSubfolderSafe(type),
|
||||
`${provider}: filename-safe type "${type}" must also be subfolder-safe`,
|
||||
).toBe(true);
|
||||
}
|
||||
|
||||
for (const type of mapper.getSubfolderSafeTypes()) {
|
||||
const standardType = mapper.mapToStandardType(type);
|
||||
expect(
|
||||
UNSAFE_SUBFOLDER_STANDARD_TYPES.has(standardType),
|
||||
`${provider}: subfolder-safe type "${type}" maps to unsafe standard type "${standardType}"`,
|
||||
).toBe(false);
|
||||
}
|
||||
|
||||
for (const type of PROTOTYPE_CHAIN_NAMES) {
|
||||
expect(
|
||||
mapper.isReadOnly(type),
|
||||
`${provider}: inherited name "${type}" must fail closed as read-only`,
|
||||
).toBe(true);
|
||||
expect(
|
||||
mapper.isFilenameSafe(type),
|
||||
`${provider}: inherited name "${type}" must not be filename-safe`,
|
||||
).toBe(false);
|
||||
expect(
|
||||
mapper.isSubfolderSafe(type),
|
||||
`${provider}: inherited name "${type}" must not be subfolder-safe`,
|
||||
).toBe(false);
|
||||
}
|
||||
|
||||
expectSortedUnique(provider, 'getFilenameSafeTypes', mapper.getFilenameSafeTypes());
|
||||
expectSortedUnique(provider, 'getSubfolderSafeTypes', mapper.getSubfolderSafeTypes());
|
||||
}
|
||||
|
||||
describe('FieldTypeMapper parity', () => {
|
||||
const mapperCases = getRegisteredMapperCases();
|
||||
|
||||
it('discovers all production mappers registered in provider-registry', () => {
|
||||
expect(mapperCases.map(c => c.type)).toEqual(expect.arrayContaining([
|
||||
'airtable',
|
||||
'seatable',
|
||||
'supabase',
|
||||
]));
|
||||
});
|
||||
|
||||
it.each(mapperCases)('enforces shared mapper contract for $type', ({ type, mapper }) => {
|
||||
assertFieldTypeMapperContract(type, mapper);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue