Align shared reference and opaque cursor contracts
This commit is contained in:
@@ -25,7 +25,7 @@
|
||||
|
||||
`InventoryQuery` is the first query surface: resolved resource selection, validated filters (character/type/name/category/group/quantity/location/system/region/blueprint state), stable sort, bounded page size and a pinned cursor. `InventoryQueryResult` has explicit ready, partial, stale, authorization-required and unavailable states.
|
||||
|
||||
`ReferenceReaderPort` mirrors T03’s indexed `ReferenceReader`: type search, category/group, region/constellation/system and station methods use its bounded selection/record shapes. `pin()` returns a `ReferenceReaderHandle` with immutable `pinnedVersion` and explicit close ownership, so multiple calls share one artifact. `LocationReader` is only a narrow adapter over that pinned reader and synthesizes explicit `kind: 'unknown'` facts for empty ID lookups; it is not a competing reference implementation. Private structure names/evidence are not public reference facts.
|
||||
`src/lib/domain/reference.ts` is the canonical pure reference-reader contract shared with T03. It preserves narrow `ReferenceRepository.types(ids)/version()`, adds T03-compatible bounded offset search and list selections, and exposes independently closable `pin()`/`snapshot()` handles with immutable `pinnedVersion`. Inventory pagination remains a service-level opaque cursor, not a reference-reader adapter. `ReferenceStationRecord.evidence` (`verified` or `system_listed`) is authoritative, with `sourceVerified` retained as its compatibility projection and nullable names preserved. `LocationReader` returns immutable-versioned facts; verified/system-listed station presence uses `sourceVerified: true/false`, while absent IDs are explicit `kind: 'unknown'` with `sourceVerified: null`, `name: null`, and no guessed parent. Private structure names/evidence are not public reference facts.
|
||||
|
||||
`WalletBalanceResult` is opt-in and resource-scoped. Each component carries exact `Isk`, local fetch time, nullable upstream effective time, coverage and authorization revision. Partial character coverage is visible.
|
||||
|
||||
|
||||
@@ -5,9 +5,12 @@ import type {
|
||||
EntityResolution,
|
||||
InventoryCursor,
|
||||
InventoryCursorState,
|
||||
InventoryCursorLookup,
|
||||
InventoryQuery,
|
||||
InventoryRow,
|
||||
InventoryQueryResult,
|
||||
LocationFact,
|
||||
ReferenceStationRecord,
|
||||
ResourceAuthorization,
|
||||
SignedMoneyNormalizationPolicy,
|
||||
WalletBalanceResult,
|
||||
@@ -62,6 +65,17 @@ export const singletonRows: readonly Pick<InventoryRow, 'isSingleton'>[] = [
|
||||
{ isSingleton: null },
|
||||
];
|
||||
|
||||
export const verifiedNamelessStation: ReferenceStationRecord = {
|
||||
id: 60000001, systemId: 30000142, systemName: 'Jita', regionId: 10000002,
|
||||
name: null, typeId: 1531, ownerId: 1000125, evidence: 'verified', sourceVerified: true, x: null, y: null, z: null,
|
||||
};
|
||||
export const systemListedStationPlaceholder: ReferenceStationRecord = {
|
||||
...verifiedNamelessStation, id: 60000002, evidence: 'system_listed', sourceVerified: false,
|
||||
};
|
||||
export const absentLocation: LocationFact = {
|
||||
kind: 'unknown', id: '60000003', name: null, parentId: null, sourceVerified: null, referenceVersion: 'reference-g1',
|
||||
};
|
||||
|
||||
export const ambiguousType: EntityResolution<{ id: number; name: string }> = {
|
||||
status: 'ambiguous',
|
||||
candidates: [{ id: 34, name: 'Tritanium' }, { id: 35, name: 'Pyerite' }],
|
||||
@@ -133,6 +147,26 @@ export const inventoryCursorState: InventoryCursorState = {
|
||||
},
|
||||
};
|
||||
|
||||
export const inventoryCursorLookup: InventoryCursorLookup = {
|
||||
token: inventoryCursor.token,
|
||||
callerUserId: 'fixture-user-a',
|
||||
queryFingerprint: 'sha256:fixture-query',
|
||||
selectedResources: [syntheticResource],
|
||||
authorizationRevisions: { 'character:90000000000000001': 7 },
|
||||
authorizationEpochs: { 'character:90000000000000001': 1 },
|
||||
snapshotGenerations: { assets: 'assets-g7' },
|
||||
referenceVersion: 'reference-g1',
|
||||
priceVersion: 'prices-g1',
|
||||
now: 1_700_000_200_000,
|
||||
};
|
||||
|
||||
export const borrowedInventoryCursorLookup: InventoryCursorLookup = { ...inventoryCursorLookup, callerUserId: 'fixture-user-b' };
|
||||
export const changedQueryInventoryCursorLookup: InventoryCursorLookup = { ...inventoryCursorLookup, queryFingerprint: 'sha256:changed-query' };
|
||||
export const changedRevisionInventoryCursorLookup: InventoryCursorLookup = {
|
||||
...inventoryCursorLookup,
|
||||
authorizationRevisions: { 'character:90000000000000001': 8 },
|
||||
};
|
||||
|
||||
export const inventoryQuery: InventoryQuery = {
|
||||
callerUserId: 'fixture-user-a',
|
||||
selectedResources: [syntheticResource],
|
||||
|
||||
@@ -9,6 +9,8 @@ import type {
|
||||
DatasetJob,
|
||||
DatasetKind,
|
||||
DatasetSyncState,
|
||||
InventoryCursorLookup,
|
||||
InventoryCursorState,
|
||||
MissingRequirement,
|
||||
Requirement,
|
||||
ResourceAuthorization,
|
||||
@@ -79,6 +81,10 @@ const SYSTEM_JOB_DATASETS: Readonly<Record<'reference' | 'public_feed' | 'mainte
|
||||
maintenance: ['assets', 'blueprints', 'skills', 'market_data', 'reference_data', 'wallet_balance', 'wallet_journal', 'wallet_transactions'],
|
||||
};
|
||||
|
||||
export type InventoryCursorValidation =
|
||||
| { valid: true }
|
||||
| { valid: false; reason: 'token_mismatch' | 'expired' | 'exhausted' | 'caller_mismatch' | 'query_mismatch' | 'resource_mismatch' | 'authorization_mismatch' | 'input_version_mismatch' };
|
||||
|
||||
export type CompleteReportPinValidation =
|
||||
| { valid: true }
|
||||
| { valid: false; reason: 'missing_pin' | 'unexpected_pin' | 'duplicate_pin' | 'principal_mismatch' | 'revision_mismatch' | 'incomplete_coverage' };
|
||||
@@ -202,6 +208,31 @@ function sameStrings(left: readonly string[], right: readonly string[]): boolean
|
||||
return left.length === right.length && left.every((value) => right.includes(value));
|
||||
}
|
||||
|
||||
/** Validate every server-resolved value before consuming an opaque cursor. */
|
||||
export function validateInventoryCursorUse(state: InventoryCursorState, lookup: InventoryCursorLookup): InventoryCursorValidation {
|
||||
if (state.token !== lookup.token) return { valid: false, reason: 'token_mismatch' };
|
||||
if (state.status === 'exhausted') return { valid: false, reason: 'exhausted' };
|
||||
if (!Number.isFinite(lookup.now) || !Number.isFinite(state.pin.expiresAt) || lookup.now >= state.pin.expiresAt) return { valid: false, reason: 'expired' };
|
||||
if (state.pin.callerUserId !== lookup.callerUserId) return { valid: false, reason: 'caller_mismatch' };
|
||||
if (state.pin.queryFingerprint !== lookup.queryFingerprint) return { valid: false, reason: 'query_mismatch' };
|
||||
if (!sameSelectedResourceList(state.pin.selectedResources, lookup.selectedResources)) return { valid: false, reason: 'resource_mismatch' };
|
||||
if (!sameRecord(state.pin.authorizationRevisions, lookup.authorizationRevisions)
|
||||
|| !sameRecord(state.pin.authorizationEpochs, lookup.authorizationEpochs)) return { valid: false, reason: 'authorization_mismatch' };
|
||||
if (!sameRecord(state.pin.snapshotGenerations, lookup.snapshotGenerations)
|
||||
|| state.pin.referenceVersion !== lookup.referenceVersion || state.pin.priceVersion !== lookup.priceVersion) return { valid: false, reason: 'input_version_mismatch' };
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
function sameSelectedResourceList(left: readonly SelectedResource[], right: readonly SelectedResource[]): boolean {
|
||||
return left.length === right.length && left.every((resource) => right.some((candidate) => sameSelectedResource([resource], candidate)));
|
||||
}
|
||||
|
||||
function sameRecord<T extends string | number | null>(left: Readonly<Record<string, T>>, right: Readonly<Record<string, T>>): boolean {
|
||||
const leftKeys = Object.keys(left);
|
||||
const rightKeys = Object.keys(right);
|
||||
return leftKeys.length === rightKeys.length && leftKeys.every((key) => right[key] === left[key]);
|
||||
}
|
||||
|
||||
/** Validate the exact selected-resource × required-dataset pin set. */
|
||||
export function validateCompleteReportPins(input: CompleteReportInputPin): CompleteReportPinValidation {
|
||||
if (input.pinSet !== 'exact_selected_resource_dataset_set') return { valid: false, reason: 'unexpected_pin' };
|
||||
|
||||
+34
-55
@@ -27,6 +27,27 @@ export type InventoryType = {
|
||||
groupId?: number;
|
||||
volume?: number;
|
||||
};
|
||||
export type {
|
||||
LocationFact,
|
||||
LocationReader,
|
||||
ReferenceCategoryRecord,
|
||||
ReferenceConstellationRecord,
|
||||
ReferenceGroupRecord,
|
||||
ReferenceIdSelection,
|
||||
ReferenceListOptions,
|
||||
ReferenceReader,
|
||||
ReferenceReaderHandle,
|
||||
ReferenceReaderPort,
|
||||
ReferenceRegionRecord,
|
||||
ReferenceRepository,
|
||||
ReferenceSearchOptions,
|
||||
ReferenceStationEvidence,
|
||||
ReferenceStationRecord,
|
||||
ReferenceStationSelection,
|
||||
ReferenceSystemRecord,
|
||||
ReferenceSystemSelection,
|
||||
ReferenceTypeRecord,
|
||||
} from './reference';
|
||||
/** Integer cents (0.01 ISK), serialized as decimal text. No floating-point accumulation. */
|
||||
export type Isk = string;
|
||||
export type ValuationMethod = 'reference_estimate' | 'immediate_liquidation' | 'expected_sell_order' | 'replacement';
|
||||
@@ -41,10 +62,6 @@ export interface CharacterAssetRepository {
|
||||
characters(userId: UserId): Promise<Character[]>;
|
||||
assets(userId: UserId, characterId: number): Promise<Asset[]>;
|
||||
}
|
||||
export interface ReferenceRepository {
|
||||
types(ids: number[]): Promise<InventoryType[]>;
|
||||
version(): Promise<string>;
|
||||
}
|
||||
export interface MarketEstimates {
|
||||
estimates(typeIds: number[]): Promise<PriceSnapshot>;
|
||||
}
|
||||
@@ -401,6 +418,19 @@ export type InventoryCursorPin = {
|
||||
export type InventoryCursorPosition = { offset: number; lastItemId: DecimalId | null };
|
||||
export type InventoryCursor = { token: string };
|
||||
export type InventoryCursorState = { token: string; pin: InventoryCursorPin; status: 'active' | 'exhausted'; position: InventoryCursorPosition };
|
||||
/** Server-resolved values supplied on every continuation lookup; no caller signature is required. */
|
||||
export type InventoryCursorLookup = {
|
||||
token: string;
|
||||
callerUserId: UserId;
|
||||
queryFingerprint: string;
|
||||
selectedResources: readonly SelectedResource[];
|
||||
authorizationRevisions: Readonly<Record<string, AuthorizationRevision>>;
|
||||
authorizationEpochs: Readonly<Record<string, AuthorizationEpoch>>;
|
||||
snapshotGenerations: Readonly<Record<string, SnapshotGeneration>>;
|
||||
referenceVersion: ReferenceVersion | null;
|
||||
priceVersion: PriceVersion | null;
|
||||
now: number;
|
||||
};
|
||||
export type InventoryQuery = {
|
||||
/** Resolved from the verified session; never accepted as caller authority. */
|
||||
callerUserId: UserId;
|
||||
@@ -436,57 +466,6 @@ export type InventoryQueryResult =
|
||||
| { status: 'ready'; rows: readonly InventoryRow[]; summary: InventorySummary; nextCursor: null; pageState: 'after_end'; provenance: CompleteReportProvenance }
|
||||
| { status: 'partial' | 'stale' | 'authorization_required' | 'unavailable'; rows: readonly InventoryRow[]; summary: InventorySummary | null; reason: string; provenance?: ReportProvenance };
|
||||
|
||||
export type ReferenceCategoryRecord = { id: number; name: string; published?: boolean };
|
||||
export type ReferenceGroupRecord = { id: number; categoryId: number; name: string; published?: boolean };
|
||||
export type ReferenceTypeRecord = { id: number; name: string; categoryId: number; groupId: number; published?: boolean; volume?: number };
|
||||
export type ReferenceRegionRecord = { id: number; name: string | null };
|
||||
export type ReferenceConstellationRecord = { id: number; name: string | null; regionId: number | null };
|
||||
export type ReferenceSystemRecord = {
|
||||
id: number; name: string; constellationId: number; regionId: number | null;
|
||||
x: number; y: number; z: number; securityStatus: number; securityClass: string | null;
|
||||
};
|
||||
export type ReferenceStationRecord = {
|
||||
id: number; systemId: number; systemName: string | null; regionId: number | null;
|
||||
name: string | null; typeId: number | null; ownerId: number | null;
|
||||
x: number | null; y: number | null; z: number | null;
|
||||
};
|
||||
export type ReferenceSearchOptions = { query: string; categoryId?: number; groupId?: number; published?: boolean; limit?: number; offset?: number };
|
||||
export type ReferenceListOptions = { ids?: number[]; query?: string; limit?: number; offset?: number };
|
||||
export type ReferenceIdSelection = number[] | ReferenceListOptions | undefined;
|
||||
export type ReferenceGroupSelection = number[] | (ReferenceListOptions & { categoryId?: number }) | undefined;
|
||||
export type ReferenceSystemSelection = number[] | (ReferenceListOptions & { constellationId?: number; regionId?: number }) | undefined;
|
||||
export type ReferenceStationSelection = number[] | (ReferenceListOptions & { systemId?: number }) | undefined;
|
||||
|
||||
/** Structural mirror of T03's indexed reader; records stay immutable/version-pinned. */
|
||||
export interface ReferenceReader extends ReferenceRepository {
|
||||
searchTypes(options: ReferenceSearchOptions): Promise<ReferenceTypeRecord[]>;
|
||||
searchTypesByName(query: string, options?: Omit<ReferenceSearchOptions, 'query'>): Promise<ReferenceTypeRecord[]>;
|
||||
categories(selection?: ReferenceIdSelection): Promise<ReferenceCategoryRecord[]>;
|
||||
groups(selection?: ReferenceGroupSelection): Promise<ReferenceGroupRecord[]>;
|
||||
regions(selection?: ReferenceIdSelection): Promise<ReferenceRegionRecord[]>;
|
||||
constellations(selection?: ReferenceIdSelection): Promise<ReferenceConstellationRecord[]>;
|
||||
systems(selection?: ReferenceSystemSelection): Promise<ReferenceSystemRecord[]>;
|
||||
stations(selection?: ReferenceStationSelection): Promise<ReferenceStationRecord[]>;
|
||||
}
|
||||
export interface ReferenceReaderHandle extends ReferenceReader {
|
||||
readonly pinnedVersion: ReferenceVersion;
|
||||
close(): void;
|
||||
}
|
||||
/** Main reader owns a manifest-pinned handle; callers use one handle across calls. */
|
||||
export interface ReferenceReaderPort extends ReferenceReader {
|
||||
pin(): ReferenceReaderHandle;
|
||||
}
|
||||
export type LocationFact = {
|
||||
kind: 'region' | 'system' | 'station' | 'unknown';
|
||||
id: DecimalId;
|
||||
name: string | null;
|
||||
parentId: DecimalId | null;
|
||||
referenceVersion: ReferenceVersion;
|
||||
};
|
||||
/** Narrow adapter over a pinned T03 reader; unknown IDs remain explicit. */
|
||||
export interface LocationReader {
|
||||
location(id: DecimalId): Promise<LocationFact>;
|
||||
}
|
||||
|
||||
export type WalletBalanceComponent = {
|
||||
component: 'wallet_balance';
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { DecimalId, InventoryType, ReferenceVersion } from './model';
|
||||
|
||||
/** Narrow compatibility boundary retained for existing asset consumers. */
|
||||
export interface ReferenceRepository {
|
||||
types(ids: number[]): Promise<InventoryType[]>;
|
||||
version(): Promise<string>;
|
||||
}
|
||||
|
||||
export type ReferenceCategoryRecord = { id: number; name: string; published?: boolean };
|
||||
export type ReferenceGroupRecord = { id: number; categoryId: number; name: string; published?: boolean };
|
||||
export type ReferenceTypeRecord = { id: number; name: string; categoryId: number; groupId: number; published?: boolean; volume?: number };
|
||||
export type ReferenceRegionRecord = { id: number; name: string | null };
|
||||
export type ReferenceConstellationRecord = { id: number; name: string | null; regionId: number | null };
|
||||
export type ReferenceSystemRecord = {
|
||||
id: number; name: string; constellationId: number; regionId: number | null;
|
||||
x: number; y: number; z: number; securityStatus: number; securityClass: string | null;
|
||||
};
|
||||
|
||||
export type ReferenceStationEvidence = 'verified' | 'system_listed';
|
||||
export type ReferenceStationRecord = {
|
||||
id: number; systemId: number; systemName: string | null; regionId: number | null;
|
||||
name: string | null; typeId: number | null; ownerId: number | null;
|
||||
/** Evidence distinguishes authoritative station rows from system-list-only IDs. */
|
||||
evidence: ReferenceStationEvidence;
|
||||
/** Compatibility projection; true exactly when evidence is `verified`. */
|
||||
sourceVerified: boolean;
|
||||
x: number | null; y: number | null; z: number | null;
|
||||
};
|
||||
|
||||
/** T03-compatible bounded offset search; inventory cursors remain service-level. */
|
||||
export type ReferenceSearchOptions = { query: string; categoryId?: number; groupId?: number; published?: boolean; limit?: number; offset?: number };
|
||||
export type ReferenceListOptions = { ids?: number[]; query?: string; limit?: number; offset?: number };
|
||||
export type ReferenceIdSelection = number[] | ReferenceListOptions | undefined;
|
||||
export type ReferenceGroupSelection = number[] | (ReferenceListOptions & { categoryId?: number }) | undefined;
|
||||
export type ReferenceSystemSelection = number[] | (ReferenceListOptions & { constellationId?: number; regionId?: number }) | undefined;
|
||||
export type ReferenceStationSelection = number[] | (ReferenceListOptions & { systemId?: number }) | undefined;
|
||||
|
||||
/** Structural mirror of T03's indexed reader; records stay immutable/version-pinned. */
|
||||
export interface ReferenceReader extends ReferenceRepository {
|
||||
searchTypes(options: ReferenceSearchOptions): Promise<ReferenceTypeRecord[]>;
|
||||
searchTypesByName(query: string, options?: Omit<ReferenceSearchOptions, 'query'>): Promise<ReferenceTypeRecord[]>;
|
||||
categories(selection?: ReferenceIdSelection): Promise<ReferenceCategoryRecord[]>;
|
||||
groups(selection?: ReferenceGroupSelection): Promise<ReferenceGroupRecord[]>;
|
||||
regions(selection?: ReferenceIdSelection): Promise<ReferenceRegionRecord[]>;
|
||||
constellations(selection?: ReferenceIdSelection): Promise<ReferenceConstellationRecord[]>;
|
||||
systems(selection?: ReferenceSystemSelection): Promise<ReferenceSystemRecord[]>;
|
||||
stations(selection?: ReferenceStationSelection): Promise<ReferenceStationRecord[]>;
|
||||
}
|
||||
|
||||
export interface ReferenceReaderHandle extends ReferenceReader {
|
||||
readonly pinnedVersion: ReferenceVersion;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
/** Main reader owns a manifest-pinned handle; callers use one handle across calls. */
|
||||
export interface ReferenceReaderPort extends ReferenceReader {
|
||||
pin(): ReferenceReaderHandle;
|
||||
snapshot(): ReferenceReaderHandle;
|
||||
}
|
||||
|
||||
export type LocationFact = {
|
||||
kind: 'region' | 'system' | 'station' | 'unknown';
|
||||
id: DecimalId;
|
||||
name: string | null;
|
||||
parentId: DecimalId | null;
|
||||
/** Null means the ID is absent; otherwise it preserves station source evidence. */
|
||||
sourceVerified: boolean | null;
|
||||
referenceVersion: ReferenceVersion;
|
||||
};
|
||||
|
||||
/** Narrow service adapter; unknown IDs are explicit and names are never synthesized. */
|
||||
export interface LocationReader {
|
||||
location(id: DecimalId): Promise<LocationFact>;
|
||||
}
|
||||
+17
-1
@@ -10,8 +10,9 @@ import {
|
||||
validateConsentCallback,
|
||||
validateDatasetJobFence,
|
||||
validateCompleteReportPins,
|
||||
validateInventoryCursorUse,
|
||||
} from '../src/lib/domain/capabilities';
|
||||
import { afterEndInventoryCursorState, afterEndInventoryResult, ambiguousType, appendDatasetState, authorization, consent, datasetState, inventoryCursor, inventoryCursorState, inventoryQuery, inventoryResult, approvedMoneyPolicy, singletonRows, syntheticResource, systemJob, walletResult } from '../fixtures/contracts/t02';
|
||||
import { absentLocation, afterEndInventoryCursorState, afterEndInventoryResult, ambiguousType, appendDatasetState, authorization, borrowedInventoryCursorLookup, changedQueryInventoryCursorLookup, changedRevisionInventoryCursorLookup, consent, datasetState, inventoryCursor, inventoryCursorLookup, inventoryCursorState, inventoryQuery, inventoryResult, approvedMoneyPolicy, singletonRows, systemListedStationPlaceholder, syntheticResource, systemJob, verifiedNamelessStation, walletResult } from '../fixtures/contracts/t02';
|
||||
|
||||
describe('T02 typed contract fixtures', () => {
|
||||
test('separates snapshot generations from append checkpoints and unknown upstream time', () => {
|
||||
@@ -48,6 +49,10 @@ describe('T02 typed contract fixtures', () => {
|
||||
expect(inventoryQuery.sort.tieBreaker).toBe('item_id');
|
||||
expect(inventoryQuery.authorizationRevisions['character:90000000000000001']).toBe(7);
|
||||
expect(inventoryCursorState.pin.callerUserId).toBe('fixture-user-a');
|
||||
expect(validateInventoryCursorUse(inventoryCursorState, inventoryCursorLookup)).toEqual({ valid: true });
|
||||
expect(validateInventoryCursorUse(inventoryCursorState, borrowedInventoryCursorLookup)).toEqual({ valid: false, reason: 'caller_mismatch' });
|
||||
expect(validateInventoryCursorUse(inventoryCursorState, changedQueryInventoryCursorLookup)).toEqual({ valid: false, reason: 'query_mismatch' });
|
||||
expect(validateInventoryCursorUse(inventoryCursorState, changedRevisionInventoryCursorLookup)).toEqual({ valid: false, reason: 'authorization_mismatch' });
|
||||
expect(inventoryCursorState.pin.authorizationEpochs['character:90000000000000001']).toBe(1);
|
||||
expect(inventoryResult.status).toBe('ready');
|
||||
if (inventoryResult.status === 'ready') {
|
||||
@@ -76,6 +81,17 @@ describe('T02 typed contract fixtures', () => {
|
||||
expect(singletonRows.map((row) => row.isSingleton)).toEqual([true, false, null]);
|
||||
});
|
||||
|
||||
test('station evidence preserves verified, system-listed, and absent states', () => {
|
||||
expect(verifiedNamelessStation.name).toBeNull();
|
||||
expect(verifiedNamelessStation.evidence).toBe('verified');
|
||||
expect(verifiedNamelessStation.sourceVerified).toBe(true);
|
||||
expect(systemListedStationPlaceholder.evidence).toBe('system_listed');
|
||||
expect(systemListedStationPlaceholder.sourceVerified).toBe(false);
|
||||
expect(absentLocation.kind).toBe('unknown');
|
||||
expect(absentLocation.sourceVerified).toBeNull();
|
||||
expect(absentLocation.parentId).toBeNull();
|
||||
});
|
||||
|
||||
test('ambiguous entity matches remain explicit instead of selecting by name', () => {
|
||||
expect(ambiguousType.status).toBe('ambiguous');
|
||||
if (ambiguousType.status === 'ambiguous') expect(ambiguousType.candidates).toHaveLength(2);
|
||||
|
||||
Reference in New Issue
Block a user