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:
uppinote20 2026-06-11 17:13:58 +09:00 committed by uppinote
parent ec8a33f4f6
commit b07341701b

View 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);
});
});