diff --git a/scripts/audit-ocf-schema-alignment.ts b/scripts/audit-ocf-schema-alignment.ts index 901c0415..0d4eec5d 100644 --- a/scripts/audit-ocf-schema-alignment.ts +++ b/scripts/audit-ocf-schema-alignment.ts @@ -121,14 +121,17 @@ function extractSdkFields(interfaceName: string): Map left.localeCompare(right)); + const ocfFieldNames = new Set(ocfFields.map(([name]) => name)); const sdkFieldNames = new Set(sdkFields.keys()); - for (const ocfField of [...ocfFieldNames].sort()) { + for (const [ocfField, prop] of ocfFields) { if (ocfField === 'object_type') continue; // Output discriminator, not input - const prop = properties[ocfField]; const ocfType = getOcfType(prop); const ocfReq = required.has(ocfField); const sdkField = findSdkField(sdkFields, ocfField, sdkInterface); diff --git a/scripts/optimize-fixtures.ts b/scripts/optimize-fixtures.ts index d8809018..a36fbe11 100755 --- a/scripts/optimize-fixtures.ts +++ b/scripts/optimize-fixtures.ts @@ -15,6 +15,7 @@ import { execSync } from 'child_process'; import fs from 'fs'; import path from 'path'; +import { requireDefined } from '../src/utils/requireDefined'; interface CoverageData { statements: number; @@ -61,10 +62,10 @@ function parseCoverage(coverageOutput: string): CoverageData { } return { - statements: parseFloat(percentages[0]), - branches: parseFloat(percentages[1]), - functions: parseFloat(percentages[2]), - lines: parseFloat(percentages[3]), + statements: parseFloat(requireDefined(percentages[0], 'statement coverage percentage')), + branches: parseFloat(requireDefined(percentages[1], 'branch coverage percentage')), + functions: parseFloat(requireDefined(percentages[2], 'function coverage percentage')), + lines: parseFloat(requireDefined(percentages[3], 'line coverage percentage')), }; } @@ -154,8 +155,7 @@ function main() { const ESTIMATION_SAMPLE_SIZE = 5; // Test each file - for (let i = 0; i < files.length; i++) { - const file = files[i]; + for (const [i, file] of files.entries()) { const progress = `[${i + 1}/${files.length}]`; const sizeKB = (file.size / 1024).toFixed(2); diff --git a/scripts/prepare-release.ts b/scripts/prepare-release.ts index d2cc28e5..8588746a 100644 --- a/scripts/prepare-release.ts +++ b/scripts/prepare-release.ts @@ -75,7 +75,11 @@ function parseVersion(version: string): { major: number; minor: number; patch: n if (!parts.every((part) => Number.isInteger(part) && part >= 0)) { return null; } - return { major: parts[0], minor: parts[1], patch: parts[2] }; + const [major, minor, patch] = parts; + if (major === undefined || minor === undefined || patch === undefined) { + return null; + } + return { major, minor, patch }; } interface ParsedVersion { diff --git a/src/functions/OpenCapTable/capTable/archiveFullCapTable.ts b/src/functions/OpenCapTable/capTable/archiveFullCapTable.ts index 51a21c66..90b77448 100644 --- a/src/functions/OpenCapTable/capTable/archiveFullCapTable.ts +++ b/src/functions/OpenCapTable/capTable/archiveFullCapTable.ts @@ -56,7 +56,8 @@ function getArchiveMatchByContractId( } const matchedContracts = candidates.filter((match) => match.capTableContractId === expectedCapTableContractId); - return matchedContracts.length === 1 ? matchedContracts[0] : null; + const [matchedContract] = matchedContracts; + return matchedContracts.length === 1 && matchedContract !== undefined ? matchedContract : null; } function getSingletonArchiveMatch(classification: { diff --git a/src/functions/OpenCapTable/capTable/getCapTableState.ts b/src/functions/OpenCapTable/capTable/getCapTableState.ts index 889478f6..46a30e01 100644 --- a/src/functions/OpenCapTable/capTable/getCapTableState.ts +++ b/src/functions/OpenCapTable/capTable/getCapTableState.ts @@ -475,7 +475,14 @@ export async function classifyIssuerCapTables( if (currentRows.length === 0) { return { status: 'none', current: null }; } - return { status: 'current', current: currentRows[0] }; + const [current] = currentRows; + if (current === undefined) { + throw new OcpContractError('CapTable query returned an inconsistent non-empty result', { + code: OcpErrorCodes.SCHEMA_MISMATCH, + contractId: 'unknown', + }); + } + return { status: 'current', current }; } /** diff --git a/src/functions/OpenCapTable/convertibleConversion/getConvertibleConversionAsOcf.ts b/src/functions/OpenCapTable/convertibleConversion/getConvertibleConversionAsOcf.ts index 7cb31a51..5ca81a18 100644 --- a/src/functions/OpenCapTable/convertibleConversion/getConvertibleConversionAsOcf.ts +++ b/src/functions/OpenCapTable/convertibleConversion/getConvertibleConversionAsOcf.ts @@ -110,7 +110,7 @@ export async function getConvertibleConversionAsOcf( const event: OcfConvertibleConversionEvent = { object_type: 'TX_CONVERTIBLE_CONVERSION', id: d.id, - date: d.date.split('T')[0], + date: d.date.split('T')[0] ?? d.date, reason_text: d.reason_text, security_id: d.security_id, trigger_id: d.trigger_id, diff --git a/src/functions/OpenCapTable/convertibleIssuance/getConvertibleIssuanceAsOcf.ts b/src/functions/OpenCapTable/convertibleIssuance/getConvertibleIssuanceAsOcf.ts index 826d3438..fb8f675e 100644 --- a/src/functions/OpenCapTable/convertibleIssuance/getConvertibleIssuanceAsOcf.ts +++ b/src/functions/OpenCapTable/convertibleIssuance/getConvertibleIssuanceAsOcf.ts @@ -636,7 +636,7 @@ export function damlConvertibleIssuanceDataToNative(d: Record): const issuance = { object_type: 'TX_CONVERTIBLE_ISSUANCE', id: d.id, - date: d.date.split('T')[0], + date: d.date.split('T')[0] ?? d.date, security_id: d.security_id, custom_id: d.custom_id as string, stakeholder_id: d.stakeholder_id as string, diff --git a/src/functions/OpenCapTable/convertibleTransfer/getConvertibleTransferAsOcf.ts b/src/functions/OpenCapTable/convertibleTransfer/getConvertibleTransferAsOcf.ts index 2bbd7b3d..3d59eb4a 100644 --- a/src/functions/OpenCapTable/convertibleTransfer/getConvertibleTransferAsOcf.ts +++ b/src/functions/OpenCapTable/convertibleTransfer/getConvertibleTransferAsOcf.ts @@ -42,7 +42,7 @@ export async function getConvertibleTransferAsOcf( const event: OcfConvertibleTransferEvent = { object_type: 'TX_CONVERTIBLE_TRANSFER', id: data.id, - date: data.date.split('T')[0], + date: data.date.split('T')[0] ?? data.date, security_id: data.security_id, amount: { amount: normalizeNumericString(amountStr), diff --git a/src/functions/OpenCapTable/equityCompensationCancellation/getEquityCompensationCancellationAsOcf.ts b/src/functions/OpenCapTable/equityCompensationCancellation/getEquityCompensationCancellationAsOcf.ts index 9dabdaf0..e79d38cf 100644 --- a/src/functions/OpenCapTable/equityCompensationCancellation/getEquityCompensationCancellationAsOcf.ts +++ b/src/functions/OpenCapTable/equityCompensationCancellation/getEquityCompensationCancellationAsOcf.ts @@ -54,7 +54,7 @@ export async function getEquityCompensationCancellationAsOcf( const event: OcfEquityCompensationCancellationEvent = { object_type: 'TX_EQUITY_COMPENSATION_CANCELLATION', id: data.id, - date: data.date.split('T')[0], + date: data.date.split('T')[0] ?? data.date, security_id: data.security_id, quantity: normalizeNumericString(quantityStr), ...(data.balance_security_id ? { balance_security_id: data.balance_security_id } : {}), diff --git a/src/functions/OpenCapTable/equityCompensationExercise/getEquityCompensationExerciseAsOcf.ts b/src/functions/OpenCapTable/equityCompensationExercise/getEquityCompensationExerciseAsOcf.ts index e48a4e91..7653bbc6 100644 --- a/src/functions/OpenCapTable/equityCompensationExercise/getEquityCompensationExerciseAsOcf.ts +++ b/src/functions/OpenCapTable/equityCompensationExercise/getEquityCompensationExerciseAsOcf.ts @@ -50,7 +50,7 @@ export function damlEquityCompensationExerciseDataToNative(d: Record = { * @throws OcpParseError if the DAML type is unknown */ export function damlValuationTypeToNative(damlType: string): ValuationType { - if (!(damlType in DAML_VALUATION_TYPE_MAP)) { + const valuationType = DAML_VALUATION_TYPE_MAP[damlType]; + if (!Object.prototype.hasOwnProperty.call(DAML_VALUATION_TYPE_MAP, damlType) || valuationType === undefined) { throw new OcpParseError(`Unknown DAML valuation type: ${damlType}`, { source: 'valuation.valuation_type', code: OcpErrorCodes.UNKNOWN_ENUM_VALUE, }); } - return DAML_VALUATION_TYPE_MAP[damlType]; + return valuationType; } /** diff --git a/src/functions/OpenCapTable/warrantCancellation/getWarrantCancellationAsOcf.ts b/src/functions/OpenCapTable/warrantCancellation/getWarrantCancellationAsOcf.ts index 1c5b40cc..2aae819d 100644 --- a/src/functions/OpenCapTable/warrantCancellation/getWarrantCancellationAsOcf.ts +++ b/src/functions/OpenCapTable/warrantCancellation/getWarrantCancellationAsOcf.ts @@ -53,7 +53,7 @@ export async function getWarrantCancellationAsOcf( const event: OcfWarrantCancellationEvent = { object_type: 'TX_WARRANT_CANCELLATION', id: data.id, - date: data.date.split('T')[0], + date: data.date.split('T')[0] ?? data.date, security_id: data.security_id, quantity: normalizeNumericString(quantityStr), ...(data.balance_security_id ? { balance_security_id: data.balance_security_id } : {}), diff --git a/src/functions/OpenCapTable/warrantIssuance/getWarrantIssuanceAsOcf.ts b/src/functions/OpenCapTable/warrantIssuance/getWarrantIssuanceAsOcf.ts index 156bb937..436cbba1 100644 --- a/src/functions/OpenCapTable/warrantIssuance/getWarrantIssuanceAsOcf.ts +++ b/src/functions/OpenCapTable/warrantIssuance/getWarrantIssuanceAsOcf.ts @@ -414,7 +414,7 @@ export function damlWarrantIssuanceDataToNative(d: Record): Ocf }); } return { - date: v.date.split('T')[0], + date: v.date.split('T')[0] ?? v.date, amount: normalizeNumericString(amountStr), }; }) @@ -455,7 +455,7 @@ export function damlWarrantIssuanceDataToNative(d: Record): Ocf return { object_type: 'TX_WARRANT_ISSUANCE', id: d.id, - date: d.date.split('T')[0], + date: d.date.split('T')[0] ?? d.date, security_id: d.security_id, custom_id: d.custom_id, stakeholder_id: d.stakeholder_id, diff --git a/src/functions/OpenCapTable/warrantTransfer/getWarrantTransferAsOcf.ts b/src/functions/OpenCapTable/warrantTransfer/getWarrantTransferAsOcf.ts index 088c6073..23a067c7 100644 --- a/src/functions/OpenCapTable/warrantTransfer/getWarrantTransferAsOcf.ts +++ b/src/functions/OpenCapTable/warrantTransfer/getWarrantTransferAsOcf.ts @@ -42,7 +42,7 @@ export async function getWarrantTransferAsOcf( const event: OcfWarrantTransferEvent = { object_type: 'TX_WARRANT_TRANSFER', id: data.id, - date: data.date.split('T')[0], + date: data.date.split('T')[0] ?? data.date, security_id: data.security_id, quantity: normalizeNumericString(quantityStr), resulting_security_ids: data.resulting_security_ids, diff --git a/src/utils/cantonOcfExtractor.ts b/src/utils/cantonOcfExtractor.ts index 677d1a83..c0bbdca7 100644 --- a/src/utils/cantonOcfExtractor.ts +++ b/src/utils/cantonOcfExtractor.ts @@ -437,7 +437,14 @@ export async function extractCantonOcfManifest( // we skip it here to avoid a redundant 404 that would abort extraction. const issuerContractEntry = cantonState.contractIds.get('issuer'); if (issuerContractEntry && issuerContractEntry.size > 0) { - const [[issuerId, issuerCid]] = issuerContractEntry; + const issuerIteratorResult = issuerContractEntry.entries().next(); + if (issuerIteratorResult.done) { + throw new OcpValidationError('contractIds.issuer', 'Expected a non-empty issuer contract entry', { + code: OcpErrorCodes.INVALID_FORMAT, + receivedValue: issuerContractEntry, + }); + } + const [issuerId, issuerCid] = issuerIteratorResult.value; let issuerLastError: Error | null = null; let issuerAttempts = 0; for (let attempt = 0; attempt < 2; attempt++) { diff --git a/src/utils/ocfComparison.ts b/src/utils/ocfComparison.ts index 86b1279f..68d22b51 100644 --- a/src/utils/ocfComparison.ts +++ b/src/utils/ocfComparison.ts @@ -98,7 +98,7 @@ const ISO_DATE_REGEX = /^(\d{4}-\d{2}-\d{2})(?:T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?( function tryNormalizeDateString(value: string): string | null { const match = ISO_DATE_REGEX.exec(value); if (!match) return null; - return match[1]; // YYYY-MM-DD (strips time portion if present) + return match[1] ?? null; // YYYY-MM-DD (strips time portion if present) } /** diff --git a/src/utils/ocfMetadata.ts b/src/utils/ocfMetadata.ts index 31136019..efefeded 100644 --- a/src/utils/ocfMetadata.ts +++ b/src/utils/ocfMetadata.ts @@ -26,7 +26,7 @@ interface OcfTypeMetadata { /** DAML template ID */ templateId: string; /** Path to extract the OCF ID from a created contract's arguments */ - ocfIdPath: string[]; + ocfIdPath: readonly [dataFieldName: string, ...remainingPath: string[]]; } /** Central registry of OCF type metadata Maps each OCF object type to its DAML template ID and OCF ID extraction path */ diff --git a/src/utils/ocfZodSchemas.ts b/src/utils/ocfZodSchemas.ts index 8e2ba682..5fb3f7d0 100644 --- a/src/utils/ocfZodSchemas.ts +++ b/src/utils/ocfZodSchemas.ts @@ -273,7 +273,13 @@ function convertZodErrorToValidationError(error: ZodError, contextField: string) }); } - const firstIssue = error.issues[0]; + const [firstIssue] = error.issues; + if (firstIssue === undefined) { + return new OcpValidationError(contextField, 'OCF schema validation failed', { + code: OcpErrorCodes.INVALID_FORMAT, + receivedValue: error.issues, + }); + } const firstIssuePath = firstIssue.path.join('.'); const issuePath = firstIssuePath.length > 0 ? firstIssuePath : contextField; const issueMessage = error.issues diff --git a/src/utils/planSecurityAliases.ts b/src/utils/planSecurityAliases.ts index e7362cdc..2f3a81f0 100644 --- a/src/utils/planSecurityAliases.ts +++ b/src/utils/planSecurityAliases.ts @@ -678,9 +678,12 @@ export function deepNormalizeNumericStrings(value: unknown): unknown { } if (typeof value === 'object' && value !== null) { const entries = Object.entries(value); - const normalized = entries.map(([k, v]) => [k, deepNormalizeNumericStrings(v)] as const); - if (normalized.every(([, v], i) => v === entries[i][1])) return value; - return Object.fromEntries(normalized); + const normalized = entries.map(([k, v]) => { + const normalizedValue = deepNormalizeNumericStrings(v); + return { entry: [k, normalizedValue] as const, changed: normalizedValue !== v }; + }); + if (normalized.every(({ changed }) => !changed)) return value; + return Object.fromEntries(normalized.map(({ entry }) => entry)); } return value; } @@ -892,7 +895,8 @@ function normalizeConversionMechanismRoundTrip(data: Record): R // Schema-default: single 1:1 RATIO_CONVERSION → empty (matches DB omission) if (normalized.length === 1) { - const right = normalized[0]; + const [right] = normalized; + if (right === undefined) return { ...data, conversion_rights: normalized }; const mech = right.conversion_mechanism as Record | undefined; if (mech?.type === 'RATIO_CONVERSION') { const ratio = mech.ratio as { numerator?: string | number; denominator?: string | number } | undefined; diff --git a/src/utils/replicationHelpers.ts b/src/utils/replicationHelpers.ts index e5f6bd6b..42ee085f 100644 --- a/src/utils/replicationHelpers.ts +++ b/src/utils/replicationHelpers.ts @@ -129,6 +129,11 @@ export const TRANSACTION_SUBTYPE_MAP: Record = { CE_STAKEHOLDER_STATUS: 'stakeholderStatusChangeEvent', }; +/** Read only mappings owned by the registry object, never inherited prototype properties. */ +function getOwnEntityType(mapping: Readonly>, key: string): OcfEntityType | undefined { + return Object.prototype.hasOwnProperty.call(mapping, key) ? mapping[key] : undefined; +} + /** * Map categorized OCF type/subtype to OcfEntityType. * @@ -151,18 +156,19 @@ export const TRANSACTION_SUBTYPE_MAP: Record = { */ export function mapCategorizedTypeToEntityType(categoryType: string, subtype: string | null): OcfEntityType | null { // Direct mappings - if (categoryType in DIRECT_TYPE_MAP) { - return DIRECT_TYPE_MAP[categoryType]; + const directType = getOwnEntityType(DIRECT_TYPE_MAP, categoryType); + if (directType !== undefined) { + return directType; } // Object subtypes if (categoryType === 'OBJECT' && subtype) { - return OBJECT_SUBTYPE_MAP[subtype] ?? null; + return getOwnEntityType(OBJECT_SUBTYPE_MAP, subtype) ?? null; } // Transaction subtypes if (categoryType === 'TRANSACTION' && subtype) { - return TRANSACTION_SUBTYPE_MAP[subtype] ?? null; + return getOwnEntityType(TRANSACTION_SUBTYPE_MAP, subtype) ?? null; } return null; @@ -356,10 +362,10 @@ export function buildCantonOcfDataMap(manifest: OcfManifest): CantonOcfDataMap { const normalizedObjectType = normalizeObjectType(objectType); // Check if the normalized object type is a known transaction type - if (!(normalizedObjectType in TRANSACTION_SUBTYPE_MAP)) { + const entityType = getOwnEntityType(TRANSACTION_SUBTYPE_MAP, normalizedObjectType); + if (entityType === undefined) { throw new Error(`Unsupported transaction object_type: ${objectType}`); } - const entityType = TRANSACTION_SUBTYPE_MAP[normalizedObjectType]; addItem(entityType, tx, `transaction (${objectType})`); } diff --git a/src/utils/requireDefined.ts b/src/utils/requireDefined.ts new file mode 100644 index 00000000..e11267c9 --- /dev/null +++ b/src/utils/requireDefined.ts @@ -0,0 +1,19 @@ +/** + * Return a value after proving that it is neither `null` nor `undefined`. + * + * This is intentionally small and internal. It is useful at script and test + * boundaries where an earlier assertion (for example, an array length check) + * cannot be carried through an indexed access by TypeScript. + */ +export function requireDefined(value: T | null | undefined, context: string): T { + if (value === null || value === undefined) { + throw new Error(`Expected ${context} to be defined`); + } + + return value; +} + +/** Return the first array element or fail with a contextual invariant message. */ +export function requireFirst(values: readonly T[], context: string): T { + return requireDefined(values[0], context); +} diff --git a/src/utils/transactionHelpers.ts b/src/utils/transactionHelpers.ts index d5f14d2e..c925d139 100644 --- a/src/utils/transactionHelpers.ts +++ b/src/utils/transactionHelpers.ts @@ -21,11 +21,13 @@ export interface CreatedTreeEvent { * @param path - Array of keys representing the path to the desired property * @returns The value at the path, or undefined if not found */ -export function safeGet(obj: unknown, path: string[]): unknown { - let curr = obj as Record | undefined; +export function safeGet(obj: unknown, path: readonly string[]): unknown { + let curr: unknown = obj; for (const key of path) { - if (!curr || typeof curr !== 'object' || !(key in curr)) return undefined; - curr = curr[key] as Record | undefined; + if (curr === null || typeof curr !== 'object' || !Object.prototype.hasOwnProperty.call(curr, key)) { + return undefined; + } + curr = (curr as Record)[key]; } return curr; } diff --git a/src/utils/typeConversions.ts b/src/utils/typeConversions.ts index 5e8d0a3e..bed6f470 100644 --- a/src/utils/typeConversions.ts +++ b/src/utils/typeConversions.ts @@ -48,7 +48,7 @@ export function relTimeToDAML(microseconds: string): { microseconds: string } { /** Convert a DAML Time string back to a date string (YYYY-MM-DD) Extract only the date portion and return as string */ export function damlTimeToDateString(timeString: string): string { // Extract just the date portion (YYYY-MM-DD) - return timeString.split('T')[0]; + return timeString.split('T')[0] ?? timeString; } /** diff --git a/test/batch/CapTableBatch.test.ts b/test/batch/CapTableBatch.test.ts index 066cc04c..25bc8650 100644 --- a/test/batch/CapTableBatch.test.ts +++ b/test/batch/CapTableBatch.test.ts @@ -9,6 +9,7 @@ import type { OcfStockClassConversionRatioAdjustment, OcfStockClassSplit, } from '../../src/types'; +import { requireDefined } from '../../src/utils/requireDefined'; describe('CapTableBatch', () => { describe('fluent builder API', () => { @@ -256,7 +257,7 @@ describe('CapTableBatch', () => { }; expect(choiceArg.edits).toHaveLength(1); - expect(choiceArg.edits[0].tag).toBe('OcfEditIssuer'); + expect(requireDefined(choiceArg.edits[0], 'first edit operation').tag).toBe('OcfEditIssuer'); }); }); @@ -302,7 +303,7 @@ describe('CapTableBatch', () => { }; expect(choiceArg.creates).toHaveLength(1); - expect(choiceArg.creates[0].tag).toBe('OcfCreateStakeholder'); + expect(requireDefined(choiceArg.creates[0], 'first create operation').tag).toBe('OcfCreateStakeholder'); expect(choiceArg.edits).toHaveLength(0); expect(choiceArg.deletes).toHaveLength(0); @@ -336,7 +337,7 @@ describe('CapTableBatch', () => { expect(choiceArg.creates).toHaveLength(0); expect(choiceArg.edits).toHaveLength(1); - expect(choiceArg.edits[0].tag).toBe('OcfEditStakeholder'); + expect(requireDefined(choiceArg.edits[0], 'first edit operation').tag).toBe('OcfEditStakeholder'); expect(choiceArg.deletes).toHaveLength(0); }); @@ -360,8 +361,9 @@ describe('CapTableBatch', () => { expect(choiceArg.creates).toHaveLength(0); expect(choiceArg.edits).toHaveLength(0); expect(choiceArg.deletes).toHaveLength(1); - expect(choiceArg.deletes[0].tag).toBe('OcfDeleteDocument'); - expect(choiceArg.deletes[0].value).toBe('doc-123'); + const firstDelete = requireDefined(choiceArg.deletes[0], 'first delete operation'); + expect(firstDelete.tag).toBe('OcfDeleteDocument'); + expect(firstDelete.value).toBe('doc-123'); }); it('should preserve raw package-id templateId when capTableContractDetails provided', () => { diff --git a/test/batch/damlToOcfConverters.test.ts b/test/batch/damlToOcfConverters.test.ts index 827db7b6..105d5fa8 100644 --- a/test/batch/damlToOcfConverters.test.ts +++ b/test/batch/damlToOcfConverters.test.ts @@ -9,6 +9,7 @@ import { damlStakeholderStatusToNative, type DamlStakeholderRelationshipType, } from '../../src/utils/enumConversions'; +import { requireDefined } from '../../src/utils/requireDefined'; describe('DAML to OCF Converters', () => { describe('damlStakeholderStatusToNative', () => { @@ -92,10 +93,10 @@ describe('DAML to OCF Converters', () => { ]; // Verify each DAML status converts to expected native status - for (let i = 0; i < damlStatuses.length; i++) { - expect( - damlStakeholderStatusToNative(damlStatuses[i] as Parameters[0]) - ).toBe(statuses[i]); + for (const [i, damlStatus] of damlStatuses.entries()) { + expect(damlStakeholderStatusToNative(damlStatus as Parameters[0])).toBe( + requireDefined(statuses[i], `native status at index ${i}`) + ); } }); @@ -113,8 +114,10 @@ describe('DAML to OCF Converters', () => { ]; // Verify each DAML relationship converts to expected native relationship - for (let i = 0; i < damlRelationships.length; i++) { - expect(damlStakeholderRelationshipToNative(damlRelationships[i])).toBe(relationships[i]); + for (const [i, damlRelationship] of damlRelationships.entries()) { + expect(damlStakeholderRelationshipToNative(damlRelationship)).toBe( + requireDefined(relationships[i], `native relationship at index ${i}`) + ); } }); }); diff --git a/test/batch/damlToOcfDispatcher.test.ts b/test/batch/damlToOcfDispatcher.test.ts index c59cf7be..c8ca3ad5 100644 --- a/test/batch/damlToOcfDispatcher.test.ts +++ b/test/batch/damlToOcfDispatcher.test.ts @@ -160,7 +160,7 @@ describe('damlToOcf dispatcher', () => { }); it.each(entityTypes)('maps %s to its generated OCF template identity', (entityType) => { - const generatedName = `${entityType[0].toUpperCase()}${entityType.slice(1)}`; + const generatedName = `${entityType.charAt(0).toUpperCase()}${entityType.slice(1)}`; const expectedModuleEntityPath = `Fairmint.OpenCapTable.OCF.${generatedName}:${generatedName}`; expect(ENTITY_TEMPLATE_ID_MAP[entityType]).toBe(ENTITY_REGISTRY[entityType].templateId); diff --git a/test/batch/remainingOcfTypes.test.ts b/test/batch/remainingOcfTypes.test.ts index eb64a183..6ed5fcff 100644 --- a/test/batch/remainingOcfTypes.test.ts +++ b/test/batch/remainingOcfTypes.test.ts @@ -20,6 +20,7 @@ import type { OcfStockRetraction, OcfWarrantRetraction, } from '../../src/types'; +import { requireDefined } from '../../src/utils/requireDefined'; describe('Retraction Type Converters', () => { describe('stockRetraction', () => { @@ -48,9 +49,9 @@ describe('Retraction Type Converters', () => { }; expect(choiceArg.creates).toHaveLength(1); - expect(choiceArg.creates[0].tag).toBe('OcfCreateStockRetraction'); + expect(requireDefined(choiceArg.creates[0], 'first create operation').tag).toBe('OcfCreateStockRetraction'); - const value = choiceArg.creates[0].value as Record; + const value = requireDefined(choiceArg.creates[0], 'first create operation').value as Record; expect(value.id).toBe('sr-123'); expect(value.date).toBe('2024-01-15T00:00:00.000Z'); expect(value.security_id).toBe('sec-001'); @@ -91,9 +92,9 @@ describe('Retraction Type Converters', () => { }; expect(choiceArg.creates).toHaveLength(1); - expect(choiceArg.creates[0].tag).toBe('OcfCreateWarrantRetraction'); + expect(requireDefined(choiceArg.creates[0], 'first create operation').tag).toBe('OcfCreateWarrantRetraction'); - const value = choiceArg.creates[0].value as Record; + const value = requireDefined(choiceArg.creates[0], 'first create operation').value as Record; expect(value.id).toBe('wr-123'); expect(value.security_id).toBe('warrant-001'); expect(value.reason_text).toBe('Duplicate issuance'); @@ -134,7 +135,7 @@ describe('Retraction Type Converters', () => { }; expect(choiceArg.creates).toHaveLength(1); - expect(choiceArg.creates[0].tag).toBe('OcfCreateConvertibleRetraction'); + expect(requireDefined(choiceArg.creates[0], 'first create operation').tag).toBe('OcfCreateConvertibleRetraction'); }); it('should have correct ENTITY_TAG_MAP entry', () => { @@ -171,7 +172,9 @@ describe('Retraction Type Converters', () => { }; expect(choiceArg.creates).toHaveLength(1); - expect(choiceArg.creates[0].tag).toBe('OcfCreateEquityCompensationRetraction'); + expect(requireDefined(choiceArg.creates[0], 'first create operation').tag).toBe( + 'OcfCreateEquityCompensationRetraction' + ); }); it('should have correct ENTITY_TAG_MAP entry', () => { @@ -214,9 +217,11 @@ describe('Equity Compensation Event Converters', () => { }; expect(choiceArg.creates).toHaveLength(1); - expect(choiceArg.creates[0].tag).toBe('OcfCreateEquityCompensationRelease'); + expect(requireDefined(choiceArg.creates[0], 'first create operation').tag).toBe( + 'OcfCreateEquityCompensationRelease' + ); - const value = choiceArg.creates[0].value as Record; + const value = requireDefined(choiceArg.creates[0], 'first create operation').value as Record; expect(value.id).toBe('rel-123'); expect(value.quantity).toBe('1000'); expect(value.resulting_security_ids).toEqual(['stock-001']); @@ -262,9 +267,11 @@ describe('Equity Compensation Event Converters', () => { }; expect(choiceArg.creates).toHaveLength(1); - expect(choiceArg.creates[0].tag).toBe('OcfCreateEquityCompensationRepricing'); + expect(requireDefined(choiceArg.creates[0], 'first create operation').tag).toBe( + 'OcfCreateEquityCompensationRepricing' + ); - const value = choiceArg.creates[0].value as Record; + const value = requireDefined(choiceArg.creates[0], 'first create operation').value as Record; expect(value.id).toBe('rep-123'); expect(value.new_exercise_price).toEqual({ amount: '0.2', @@ -311,9 +318,9 @@ describe('Stock Plan Event Converters', () => { }; expect(choiceArg.creates).toHaveLength(1); - expect(choiceArg.creates[0].tag).toBe('OcfCreateStockPlanReturnToPool'); + expect(requireDefined(choiceArg.creates[0], 'first create operation').tag).toBe('OcfCreateStockPlanReturnToPool'); - const value = choiceArg.creates[0].value as Record; + const value = requireDefined(choiceArg.creates[0], 'first create operation').value as Record; expect(value.id).toBe('rtp-123'); expect(value.security_id).toBe('sec-123'); expect(value.stock_plan_id).toBe('plan-2024'); @@ -358,9 +365,11 @@ describe('Stakeholder Change Event Converters', () => { }; expect(choiceArg.creates).toHaveLength(1); - expect(choiceArg.creates[0].tag).toBe('OcfCreateStakeholderRelationshipChangeEvent'); + expect(requireDefined(choiceArg.creates[0], 'first create operation').tag).toBe( + 'OcfCreateStakeholderRelationshipChangeEvent' + ); - const value = choiceArg.creates[0].value as Record; + const value = requireDefined(choiceArg.creates[0], 'first create operation').value as Record; expect(value.id).toBe('rce-123'); expect(value.stakeholder_id).toBe('sh-001'); expect(value.relationship_started).toBe('OcfRelEmployee'); @@ -391,7 +400,7 @@ describe('Stakeholder Change Event Converters', () => { creates: Array<{ tag: string; value: unknown }>; }; - const value = choiceArg.creates[0].value as Record; + const value = requireDefined(choiceArg.creates[0], 'first create operation').value as Record; expect(value.relationship_started).toBe('OcfRelFounder'); expect(value.relationship_ended).toBe('OcfRelInvestor'); }); @@ -447,9 +456,11 @@ describe('Stakeholder Change Event Converters', () => { }; expect(choiceArg.creates).toHaveLength(1); - expect(choiceArg.creates[0].tag).toBe('OcfCreateStakeholderStatusChangeEvent'); + expect(requireDefined(choiceArg.creates[0], 'first create operation').tag).toBe( + 'OcfCreateStakeholderStatusChangeEvent' + ); - const value = choiceArg.creates[0].value as Record; + const value = requireDefined(choiceArg.creates[0], 'first create operation').value as Record; expect(value.id).toBe('sce-123'); expect(value.stakeholder_id).toBe('sh-001'); expect(value.new_status).toBe('OcfStakeholderStatusLeaveOfAbsence'); @@ -500,7 +511,7 @@ describe('Stakeholder Change Event Converters', () => { creates: Array<{ tag: string; value: unknown }>; }; - const value = choiceArg.creates[0].value as Record; + const value = requireDefined(choiceArg.creates[0], 'first create operation').value as Record; expect(value.new_status).toBe(expected); } }); diff --git a/test/capTable/archiveFullCapTable.test.ts b/test/capTable/archiveFullCapTable.test.ts index 3dfe00a8..94f1bf34 100644 --- a/test/capTable/archiveFullCapTable.test.ts +++ b/test/capTable/archiveFullCapTable.test.ts @@ -6,6 +6,7 @@ import { getSystemOperatorPartyId, } from '../../src/functions/OpenCapTable/capTable/archiveFullCapTable'; import type { OcfEntityType } from '../../src/functions/OpenCapTable/capTable/batchTypes'; +import { requireDefined } from '../../src/utils/requireDefined'; const mockArchiveCapTable = jest.fn(); const mockBatchDelete = jest.fn(); @@ -26,7 +27,10 @@ jest.mock('../../src/functions/OpenCapTable/capTable/CapTableBatch', () => ({ })); const CURRENT_CAP_TABLE_TEMPLATE_ID = OCP_TEMPLATES.capTable; -const CURRENT_OCP_PACKAGE_NAME = CURRENT_CAP_TABLE_TEMPLATE_ID.replace(/^#/, '').split(':')[0]; +const CURRENT_OCP_PACKAGE_NAME = requireDefined( + CURRENT_CAP_TABLE_TEMPLATE_ID.replace(/^#/, '').split(':')[0], + 'current OCP package name' +); const HASH_FORM_CAP_TABLE_TEMPLATE_ID = CapTable.templateIdWithPackageId; function isCurrentTemplateQuery(templateIds: string[] | undefined): boolean { diff --git a/test/capTable/getCapTableState.test.ts b/test/capTable/getCapTableState.test.ts index b0b652f7..89fa499d 100644 --- a/test/capTable/getCapTableState.test.ts +++ b/test/capTable/getCapTableState.test.ts @@ -11,6 +11,7 @@ import { OCP_TEMPLATES } from '@fairmint/open-captable-protocol-daml-js'; import { CapTable } from '@fairmint/open-captable-protocol-daml-js/lib/Fairmint/OpenCapTable/CapTable/module'; import { OcpErrorCodes } from '../../src/errors'; import { classifyIssuerCapTables, getCapTableState } from '../../src/functions/OpenCapTable/capTable'; +import { requireDefined } from '../../src/utils/requireDefined'; // Mock the canton-node-sdk jest.mock('@fairmint/canton-node-sdk'); @@ -18,7 +19,10 @@ jest.mock('@fairmint/canton-node-sdk'); const CURRENT_CAP_TABLE_TEMPLATE_ID = OCP_TEMPLATES.capTable; const NON_CURRENT_CAP_TABLE_TEMPLATE_ID = '#OpenCapTable-other:Fairmint.OpenCapTable.CapTable:CapTable'; /** Package segment from the pinned CapTable template (tracks daml-js upgrades). */ -const CURRENT_OCP_PACKAGE_NAME = CURRENT_CAP_TABLE_TEMPLATE_ID.replace(/^#/, '').split(':')[0]; +const CURRENT_OCP_PACKAGE_NAME = requireDefined( + CURRENT_CAP_TABLE_TEMPLATE_ID.replace(/^#/, '').split(':')[0], + 'current OCP package name' +); const NON_CURRENT_CAP_TABLE_PACKAGE_NAME = 'OpenCapTable-other'; /** Package-id form of the pinned CapTable template (same build as `OCP_TEMPLATES.capTable`). */ @@ -344,7 +348,10 @@ describe('getCapTableState', () => { }); it('should reject templateId with empty module path after package reference', async () => { - const badTemplateId = `${HASH_FORM_CAP_TABLE_TEMPLATE_ID.split(':')[0]}:`; + const badTemplateId = `${requireDefined( + HASH_FORM_CAP_TABLE_TEMPLATE_ID.split(':')[0], + 'package reference in hash-form CapTable template id' + )}:`; mockActiveContractsForCapTableState(mockClient, { current: [ buildMockCapTableContract({ diff --git a/test/converters/convertibleIssuanceConverters.test.ts b/test/converters/convertibleIssuanceConverters.test.ts index 16822a0c..e3afd99c 100644 --- a/test/converters/convertibleIssuanceConverters.test.ts +++ b/test/converters/convertibleIssuanceConverters.test.ts @@ -13,6 +13,7 @@ import { convertibleIssuanceDataToDaml } from '../../src/functions/OpenCapTable/convertibleIssuance/createConvertibleIssuance'; import { damlConvertibleIssuanceDataToNative } from '../../src/functions/OpenCapTable/convertibleIssuance/getConvertibleIssuanceAsOcf'; +import { requireFirst } from '../../src/utils/requireDefined'; import { loadProductionFixture } from '../utils/productionFixtures'; const BASE_INPUT = { @@ -60,7 +61,7 @@ describe('SAFE conversion_timing DAML constructor names', () => { }; const daml = convertibleIssuanceDataToDaml(input); - const trigger = daml.conversion_triggers[0]; + const trigger = requireFirst(daml.conversion_triggers, 'converted SAFE trigger'); const mech = ( trigger.conversion_right as { conversion_mechanism: { tag: string; value: { conversion_timing: string | null } } } ).conversion_mechanism; @@ -88,7 +89,7 @@ describe('SAFE conversion_timing DAML constructor names', () => { }; const daml = convertibleIssuanceDataToDaml(input); - const trigger = daml.conversion_triggers[0]; + const trigger = requireFirst(daml.conversion_triggers, 'converted SAFE trigger'); const mech = ( trigger.conversion_right as { conversion_mechanism: { tag: string; value: { conversion_timing: string | null } } } ).conversion_mechanism; @@ -105,7 +106,7 @@ describe('SAFE conversion_timing DAML constructor names', () => { }; const daml = convertibleIssuanceDataToDaml(input); - const trigger = daml.conversion_triggers[0]; + const trigger = requireFirst(daml.conversion_triggers, 'converted SAFE trigger'); const mech = ( trigger.conversion_right as { conversion_mechanism: { tag: string; value: { conversion_timing: unknown } } } ).conversion_mechanism; @@ -151,7 +152,7 @@ describe('ConvertibleConversionMechanismInput bare string handling', () => { }; const daml = convertibleIssuanceDataToDaml(input); - const trigger = daml.conversion_triggers[0]; + const trigger = requireFirst(daml.conversion_triggers, 'converted SAFE trigger'); const mech = (trigger.conversion_right as { conversion_mechanism: { tag: string } }).conversion_mechanism; expect(mech.tag).toBe('OcfConvMechSAFE'); @@ -222,7 +223,8 @@ describe('read-side: conversion_timing exact DAML constructor matching', () => { ...BASE_DAML, conversion_triggers: [buildDamlSafeTrigger('OcfConvTimingPreMoney')], }); - const mech = result.conversion_triggers[0].conversion_right.conversion_mechanism as { + const mech = requireFirst(result.conversion_triggers, 'native conversion trigger').conversion_right + .conversion_mechanism as { type: string; conversion_timing?: string; }; @@ -234,7 +236,8 @@ describe('read-side: conversion_timing exact DAML constructor matching', () => { ...BASE_DAML, conversion_triggers: [buildDamlSafeTrigger('OcfConvTimingPostMoney')], }); - const mech = result.conversion_triggers[0].conversion_right.conversion_mechanism as { + const mech = requireFirst(result.conversion_triggers, 'native conversion trigger').conversion_right + .conversion_mechanism as { type: string; conversion_timing?: string; }; @@ -246,7 +249,8 @@ describe('read-side: conversion_timing exact DAML constructor matching', () => { ...BASE_DAML, conversion_triggers: [buildDamlSafeTrigger()], }); - const mech = result.conversion_triggers[0].conversion_right.conversion_mechanism as { + const mech = requireFirst(result.conversion_triggers, 'native conversion trigger').conversion_right + .conversion_mechanism as { type: string; conversion_timing?: string; }; @@ -259,7 +263,8 @@ describe('read-side: conversion_timing exact DAML constructor matching', () => { ...BASE_DAML, conversion_triggers: [buildDamlSafeTrigger('OcfConversionTimingPreMoney')], }); - const mech = result.conversion_triggers[0].conversion_right.conversion_mechanism as { + const mech = requireFirst(result.conversion_triggers, 'native conversion trigger').conversion_right + .conversion_mechanism as { type: string; conversion_timing?: string; }; @@ -271,7 +276,8 @@ describe('read-side: conversion_timing exact DAML constructor matching', () => { ...BASE_DAML, conversion_triggers: [buildDamlSafeTrigger('OcfConversionTimingPostMoney')], }); - const mech = result.conversion_triggers[0].conversion_right.conversion_mechanism as { + const mech = requireFirst(result.conversion_triggers, 'native conversion trigger').conversion_right + .conversion_mechanism as { type: string; conversion_timing?: string; }; @@ -295,7 +301,8 @@ describe('read-side: day_count_convention and interest_payout exact DAML constru convertible_type: 'OcfConvertibleNote', conversion_triggers: [buildDamlNoteTrigger('OcfDayCountActual365', 'OcfInterestPayoutDeferred')], }); - const mech = result.conversion_triggers[0].conversion_right.conversion_mechanism as { + const mech = requireFirst(result.conversion_triggers, 'native conversion trigger').conversion_right + .conversion_mechanism as { type: string; day_count_convention?: string; interest_payout?: string; @@ -309,7 +316,8 @@ describe('read-side: day_count_convention and interest_payout exact DAML constru convertible_type: 'OcfConvertibleNote', conversion_triggers: [buildDamlNoteTrigger('OcfDayCount30_360', 'OcfInterestPayoutCash')], }); - const mech = result.conversion_triggers[0].conversion_right.conversion_mechanism as { + const mech = requireFirst(result.conversion_triggers, 'native conversion trigger').conversion_right + .conversion_mechanism as { type: string; day_count_convention?: string; }; @@ -322,7 +330,8 @@ describe('read-side: day_count_convention and interest_payout exact DAML constru convertible_type: 'OcfConvertibleNote', conversion_triggers: [buildDamlNoteTrigger('OcfDayCountActual365', 'OcfInterestPayoutDeferred')], }); - const mech = result.conversion_triggers[0].conversion_right.conversion_mechanism as { + const mech = requireFirst(result.conversion_triggers, 'native conversion trigger').conversion_right + .conversion_mechanism as { type: string; interest_payout?: string; }; @@ -335,7 +344,8 @@ describe('read-side: day_count_convention and interest_payout exact DAML constru convertible_type: 'OcfConvertibleNote', conversion_triggers: [buildDamlNoteTrigger('OcfDayCountActual365', 'OcfInterestPayoutCash')], }); - const mech = result.conversion_triggers[0].conversion_right.conversion_mechanism as { + const mech = requireFirst(result.conversion_triggers, 'native conversion trigger').conversion_right + .conversion_mechanism as { type: string; interest_payout?: string; }; @@ -386,7 +396,8 @@ describe('SAFE conversion_timing round-trip', () => { it('POST_MONEY survives OCF → DAML → OCF round-trip', () => { const result = roundTrip('POST_MONEY'); - const mech = result.conversion_triggers[0].conversion_right.conversion_mechanism as { + const mech = requireFirst(result.conversion_triggers, 'native conversion trigger').conversion_right + .conversion_mechanism as { type: string; conversion_timing?: string; }; @@ -396,7 +407,8 @@ describe('SAFE conversion_timing round-trip', () => { it('PRE_MONEY survives OCF → DAML → OCF round-trip', () => { const result = roundTrip('PRE_MONEY'); - const mech = result.conversion_triggers[0].conversion_right.conversion_mechanism as { + const mech = requireFirst(result.conversion_triggers, 'native conversion trigger').conversion_right + .conversion_mechanism as { type: string; conversion_timing?: string; }; @@ -445,7 +457,7 @@ describe('POST_MONEY SAFE – production fixture round-trip', () => { ); const result = damlConvertibleIssuanceDataToNative(daml); - const trigger = result.conversion_triggers[0]; + const trigger = requireFirst(result.conversion_triggers, 'production fixture conversion trigger'); const right = trigger.conversion_right; const mech = right.conversion_mechanism as { type: string; diff --git a/test/converters/valuationVestingConverters.test.ts b/test/converters/valuationVestingConverters.test.ts index bccab1ab..5e3c74d5 100644 --- a/test/converters/valuationVestingConverters.test.ts +++ b/test/converters/valuationVestingConverters.test.ts @@ -39,6 +39,7 @@ import type { OcfVestingStart, OcfVestingTerms, } from '../../src/types'; +import { requireFirst } from '../../src/utils/requireDefined'; describe('Valuation Converters', () => { describe('OCF → DAML (valuationDataToDaml)', () => { @@ -187,6 +188,10 @@ describe('Valuation Converters', () => { expect(() => damlValuationTypeToNative('UnknownType')).toThrow(OcpParseError); expect(() => damlValuationTypeToNative('UnknownType')).toThrow('Unknown DAML valuation type'); }); + + test('rejects inherited object properties as unknown valuation types', () => { + expect(() => damlValuationTypeToNative('toString')).toThrow(OcpParseError); + }); }); describe('round-trip conversion', () => { @@ -332,7 +337,7 @@ describe('VestingTerms Converters', () => { vesting_conditions: Array<{ portion: { numerator: string; denominator: string; remainder: boolean } }>; }; - expect(damlData.vesting_conditions[0].portion).toEqual({ + expect(requireFirst(damlData.vesting_conditions, 'converted vesting condition').portion).toEqual({ numerator: '1', denominator: '4', remainder: false, @@ -368,7 +373,7 @@ describe('VestingTerms Converters', () => { vesting_conditions: Array<{ portion: { numerator: string; denominator: string; remainder: boolean } }>; }; - expect(damlData.vesting_conditions[0].portion).toEqual({ + expect(requireFirst(damlData.vesting_conditions, 'converted vesting condition').portion).toEqual({ numerator: '1', denominator: '4', remainder: true, @@ -642,19 +647,22 @@ describe('VestingTerms drift regression', () => { test('preserves remainder: false when explicitly set (truthiness fix)', () => { const result = damlVestingTermsDataToNative(makeDamlVestingTerms()); - expect(result.vesting_conditions[0].portion).toBeDefined(); - expect(result.vesting_conditions[0].portion!.numerator).toBe('1'); - expect(result.vesting_conditions[0].portion!.denominator).toBe('4'); - expect(result.vesting_conditions[0].portion!.remainder).toBe(false); + const { portion } = requireFirst(result.vesting_conditions, 'native vesting condition'); + expect(portion).toBeDefined(); + expect(portion?.numerator).toBe('1'); + expect(portion?.denominator).toBe('4'); + expect(portion?.remainder).toBe(false); }); test('preserves remainder: true', () => { const daml = makeDamlVestingTerms(); - ( - daml as unknown as { vesting_conditions: Array<{ portion: { remainder: boolean } }> } - ).vesting_conditions[0].portion.remainder = true; + const damlCondition = requireFirst( + (daml as unknown as { vesting_conditions: Array<{ portion: { remainder: boolean } }> }).vesting_conditions, + 'DAML vesting condition' + ); + damlCondition.portion.remainder = true; const result = damlVestingTermsDataToNative(daml); - expect(result.vesting_conditions[0].portion!.remainder).toBe(true); + expect(requireFirst(result.vesting_conditions, 'native vesting condition').portion?.remainder).toBe(true); }); test('strips empty comments array', () => { @@ -690,9 +698,13 @@ describe('VestingTerms drift regression', () => { damlData as unknown as Parameters[0] ); - expect(roundTripped.vesting_conditions[0].portion).toBeDefined(); + const roundTrippedPortion = requireFirst( + roundTripped.vesting_conditions, + 'round-tripped vesting condition' + ).portion; + expect(roundTrippedPortion).toBeDefined(); // When OCF omits remainder, convertToDaml may add false as DAML default; we preserve it (truthiness fix) - expect(roundTripped.vesting_conditions[0].portion!.remainder).toBe(false); + expect(roundTrippedPortion?.remainder).toBe(false); }); test('round-trip OCF → DAML → OCF preserves omitted comments', () => { diff --git a/test/converters/warrantIssuanceConverters.test.ts b/test/converters/warrantIssuanceConverters.test.ts index 10073d8a..633aab62 100644 --- a/test/converters/warrantIssuanceConverters.test.ts +++ b/test/converters/warrantIssuanceConverters.test.ts @@ -10,6 +10,7 @@ import { OcpParseError, OcpValidationError } from '../../src/errors'; import { warrantIssuanceDataToDaml } from '../../src/functions/OpenCapTable/warrantIssuance/createWarrantIssuance'; import { damlWarrantIssuanceDataToNative } from '../../src/functions/OpenCapTable/warrantIssuance/getWarrantIssuanceAsOcf'; import { ocfDeepEqual } from '../../src/utils/ocfComparison'; +import { requireFirst } from '../../src/utils/requireDefined'; /** Helper: round-trip OCF data through DAML and back to OCF */ function roundTrip(ocfInput: Parameters[0]): Record { @@ -48,6 +49,7 @@ describe('WarrantIssuance round-trip equivalence', () => { ], object_type: 'TX_WARRANT_ISSUANCE' as const, }; + const baseExerciseTrigger = requireFirst(baseWarrantIssuance.exercise_triggers, 'base warrant exercise trigger'); test('basic warrant issuance survives round-trip', () => { const dbData = { ...baseWarrantIssuance, object_type: 'TX_WARRANT_ISSUANCE' } as Record; @@ -126,9 +128,9 @@ describe('WarrantIssuance round-trip equivalence', () => { ...input, exercise_triggers: [ { - ...input.exercise_triggers[0], + ...requireFirst(input.exercise_triggers, 'input warrant exercise trigger'), conversion_right: { - ...input.exercise_triggers[0].conversion_right, + ...requireFirst(input.exercise_triggers, 'input warrant exercise trigger').conversion_right, converts_to_future_round: null, }, }, @@ -205,7 +207,7 @@ describe('WarrantIssuance round-trip equivalence', () => { }; const daml = warrantIssuanceDataToDaml(input); - const trig = daml.exercise_triggers[0]; + const trig = requireFirst(daml.exercise_triggers, 'converted warrant exercise trigger'); expect(trig.conversion_right.tag).toBe('OcfRightStockClass'); const sr = trig.conversion_right.value as { type_: string; @@ -254,17 +256,18 @@ describe('WarrantIssuance round-trip equivalence', () => { const daml = warrantIssuanceDataToDaml(input); const payload = JSON.parse(JSON.stringify(daml)) as Record; const trig = payload.exercise_triggers as Array>; - const cr = trig[0].conversion_right as Record; + const cr = requireFirst(trig, 'serialized warrant exercise trigger').conversion_right as Record; const stockVal = cr.value as Record; stockVal.conversion_mechanism = { tag: 'OcfConversionMechanismRatioConversion' }; const native = damlWarrantIssuanceDataToNative(payload); - expect(native.exercise_triggers[0].conversion_right.type).toBe('STOCK_CLASS_CONVERSION_RIGHT'); - if (native.exercise_triggers[0].conversion_right.type !== 'STOCK_CLASS_CONVERSION_RIGHT') { + const nativeTrigger = requireFirst(native.exercise_triggers, 'native warrant exercise trigger'); + expect(nativeTrigger.conversion_right.type).toBe('STOCK_CLASS_CONVERSION_RIGHT'); + if (nativeTrigger.conversion_right.type !== 'STOCK_CLASS_CONVERSION_RIGHT') { throw new Error('expected stock class conversion right'); } - expect(native.exercise_triggers[0].conversion_right.converts_to_stock_class_id).toBe(stockClassId); - expect(native.exercise_triggers[0].conversion_right.conversion_mechanism.type).toBe('RATIO_CONVERSION'); + expect(nativeTrigger.conversion_right.converts_to_stock_class_id).toBe(stockClassId); + expect(nativeTrigger.conversion_right.conversion_mechanism.type).toBe('RATIO_CONVERSION'); }); test('STOCK_CLASS_CONVERSION_RIGHT with unsupported mechanism throws OcpParseError', () => { @@ -297,9 +300,9 @@ describe('WarrantIssuance round-trip equivalence', () => { ...baseWarrantIssuance, exercise_triggers: [ { - ...baseWarrantIssuance.exercise_triggers[0], + ...baseExerciseTrigger, conversion_right: { - ...baseWarrantIssuance.exercise_triggers[0].conversion_right, + ...baseExerciseTrigger.conversion_right, conversion_mechanism: { type: 'SAFE_CONVERSION' as unknown as 'CUSTOM_CONVERSION', custom_conversion_description: '', @@ -317,9 +320,9 @@ describe('WarrantIssuance round-trip equivalence', () => { ...baseWarrantIssuance, exercise_triggers: [ { - ...baseWarrantIssuance.exercise_triggers[0], + ...baseExerciseTrigger, conversion_right: { - ...baseWarrantIssuance.exercise_triggers[0].conversion_right, + ...baseExerciseTrigger.conversion_right, conversion_mechanism: { type: 'CONVERTIBLE_NOTE_CONVERSION' as unknown as 'CUSTOM_CONVERSION', custom_conversion_description: '', @@ -337,9 +340,9 @@ describe('WarrantIssuance round-trip equivalence', () => { ...baseWarrantIssuance, exercise_triggers: [ { - ...baseWarrantIssuance.exercise_triggers[0], + ...baseExerciseTrigger, conversion_right: { - ...baseWarrantIssuance.exercise_triggers[0].conversion_right, + ...baseExerciseTrigger.conversion_right, conversion_mechanism: null as unknown as (typeof baseWarrantIssuance.exercise_triggers)[0]['conversion_right']['conversion_mechanism'], }, @@ -354,9 +357,9 @@ describe('WarrantIssuance round-trip equivalence', () => { ...baseWarrantIssuance, exercise_triggers: [ { - ...baseWarrantIssuance.exercise_triggers[0], + ...baseExerciseTrigger, conversion_right: { - ...baseWarrantIssuance.exercise_triggers[0].conversion_right, + ...baseExerciseTrigger.conversion_right, conversion_mechanism: { type: 'NOT_A_REAL_MECHANISM' as unknown as 'CUSTOM_CONVERSION', custom_conversion_description: '', @@ -375,9 +378,9 @@ describe('WarrantIssuance round-trip equivalence', () => { ...baseWarrantIssuance, exercise_triggers: [ { - ...baseWarrantIssuance.exercise_triggers[0], + ...baseExerciseTrigger, conversion_right: { - ...baseWarrantIssuance.exercise_triggers[0].conversion_right, + ...baseExerciseTrigger.conversion_right, conversion_mechanism: { type: 'FIXED_AMOUNT_CONVERSION' as const, converts_to_quantity: '22500', @@ -391,9 +394,9 @@ describe('WarrantIssuance round-trip equivalence', () => { ...input, exercise_triggers: [ { - ...input.exercise_triggers[0], + ...requireFirst(input.exercise_triggers, 'input warrant exercise trigger'), conversion_right: { - ...input.exercise_triggers[0].conversion_right, + ...requireFirst(input.exercise_triggers, 'input warrant exercise trigger').conversion_right, conversion_mechanism: { type: 'FIXED_AMOUNT_CONVERSION', converts_to_quantity: 22500, diff --git a/test/createOcf/falsyFieldRoundtrip.test.ts b/test/createOcf/falsyFieldRoundtrip.test.ts index 4ad2d6ac..52b74f95 100644 --- a/test/createOcf/falsyFieldRoundtrip.test.ts +++ b/test/createOcf/falsyFieldRoundtrip.test.ts @@ -8,6 +8,7 @@ import { damlConvertibleIssuanceDataToNative } from '../../src/functions/OpenCap import { damlStockClassDataToNative } from '../../src/functions/OpenCapTable/stockClass/getStockClassAsOcf'; import { damlStockIssuanceDataToNative } from '../../src/functions/OpenCapTable/stockIssuance/getStockIssuanceAsOcf'; import { damlVestingTermsDataToNative } from '../../src/functions/OpenCapTable/vestingTerms/getVestingTermsAsOcf'; +import { requireFirst } from '../../src/utils/requireDefined'; describe('falsy field preservation in DAML-to-OCF converters', () => { describe('boolean false fields', () => { @@ -39,7 +40,8 @@ describe('falsy field preservation in DAML-to-OCF converters', () => { security_law_exemptions: [], }; const result = damlConvertibleIssuanceDataToNative(daml); - const mechanism = result.conversion_triggers[0]?.conversion_right.conversion_mechanism; + const mechanism = requireFirst(result.conversion_triggers, 'native conversion trigger').conversion_right + .conversion_mechanism; expect(mechanism).toBeDefined(); expect('conversion_mfn' in mechanism).toBe(true); expect((mechanism as unknown as Record).conversion_mfn).toBe(false); @@ -70,7 +72,8 @@ describe('falsy field preservation in DAML-to-OCF converters', () => { security_law_exemptions: [], }; const result = damlConvertibleIssuanceDataToNative(daml); - const mechanism = result.conversion_triggers[0]?.conversion_right.conversion_mechanism; + const mechanism = requireFirst(result.conversion_triggers, 'native conversion trigger').conversion_right + .conversion_mechanism; expect(mechanism).toBeDefined(); expect('conversion_mfn' in mechanism).toBe(true); expect((mechanism as unknown as Record).conversion_mfn).toBe(false); diff --git a/test/integration/entities/exerciseConversionTypes.integration.test.ts b/test/integration/entities/exerciseConversionTypes.integration.test.ts index 77688344..a5a7a148 100644 --- a/test/integration/entities/exerciseConversionTypes.integration.test.ts +++ b/test/integration/entities/exerciseConversionTypes.integration.test.ts @@ -18,6 +18,7 @@ * ``` */ +import { requireFirst } from '../../../src/utils/requireDefined'; import { createIntegrationTestSuite } from '../setup'; import { createTestConvertibleConversionData, @@ -88,7 +89,7 @@ createIntegrationTestSuite('Exercise and Conversion Types', (getContext) => { // Read back as OCF const ocfResult = await ctx.ocp.OpenCapTable.warrantExercise.get({ - contractId: extractContractIdString(result.createdCids[0]), + contractId: extractContractIdString(requireFirst(result.createdCids, 'created warrant exercise contract')), }); expect(ocfResult.data.object_type).toBe('TX_WARRANT_EXERCISE'); @@ -141,7 +142,7 @@ createIntegrationTestSuite('Exercise and Conversion Types', (getContext) => { // Read back as OCF const ocfResult = await ctx.ocp.OpenCapTable.convertibleConversion.get({ - contractId: extractContractIdString(result.createdCids[0]), + contractId: extractContractIdString(requireFirst(result.createdCids, 'created convertible conversion contract')), }); expect(ocfResult.data.object_type).toBe('TX_CONVERTIBLE_CONVERSION'); @@ -195,7 +196,7 @@ createIntegrationTestSuite('Exercise and Conversion Types', (getContext) => { // Read back as OCF const ocfResult = await ctx.ocp.OpenCapTable.stockConversion.get({ - contractId: extractContractIdString(result.createdCids[0]), + contractId: extractContractIdString(requireFirst(result.createdCids, 'created stock conversion contract')), }); expect(ocfResult.data.object_type).toBe('TX_STOCK_CONVERSION'); diff --git a/test/integration/entities/valuationVesting.integration.test.ts b/test/integration/entities/valuationVesting.integration.test.ts index f6cbd6e1..5f0b028d 100644 --- a/test/integration/entities/valuationVesting.integration.test.ts +++ b/test/integration/entities/valuationVesting.integration.test.ts @@ -17,6 +17,7 @@ * ``` */ +import { requireFirst } from '../../../src/utils/requireDefined'; import { createIntegrationTestSuite } from '../setup'; import { createTestValuationData, @@ -81,7 +82,7 @@ createIntegrationTestSuite('Valuation and Vesting types via batch API', (getCont expect(result.updatedCapTableCid).toBeTruthy(); const ocfResult = await ctx.ocp.OpenCapTable.valuation.get({ - contractId: extractContractIdString(result.createdCids[0]), + contractId: extractContractIdString(requireFirst(result.createdCids, 'created valuation contract')), }); expect(ocfResult.data.object_type).toBe('VALUATION'); expect(ocfResult.data.stock_class_id).toBe(valuationData.stock_class_id); @@ -202,7 +203,7 @@ createIntegrationTestSuite('Valuation and Vesting types via batch API', (getCont expect(result.updatedCapTableCid).toBeTruthy(); const ocfResult = await ctx.ocp.OpenCapTable.vestingStart.get({ - contractId: extractContractIdString(result.createdCids[0]), + contractId: extractContractIdString(requireFirst(result.createdCids, 'created vesting start contract')), }); expect(ocfResult.data.object_type).toBe('TX_VESTING_START'); expect(ocfResult.data.security_id).toBe(vestingStartData.security_id); @@ -263,7 +264,7 @@ createIntegrationTestSuite('Valuation and Vesting types via batch API', (getCont expect(result.updatedCapTableCid).toBeTruthy(); const ocfResult = await ctx.ocp.OpenCapTable.vestingEvent.get({ - contractId: extractContractIdString(result.createdCids[0]), + contractId: extractContractIdString(requireFirst(result.createdCids, 'created vesting event contract')), }); expect(ocfResult.data.object_type).toBe('TX_VESTING_EVENT'); expect(ocfResult.data.security_id).toBe(vestingEventData.security_id); @@ -323,7 +324,7 @@ createIntegrationTestSuite('Valuation and Vesting types via batch API', (getCont expect(result.updatedCapTableCid).toBeTruthy(); const ocfResult = await ctx.ocp.OpenCapTable.vestingAcceleration.get({ - contractId: extractContractIdString(result.createdCids[0]), + contractId: extractContractIdString(requireFirst(result.createdCids, 'created vesting acceleration contract')), }); expect(ocfResult.data.object_type).toBe('TX_VESTING_ACCELERATION'); expect(ocfResult.data.security_id).toBe(vestingAccelerationData.security_id); @@ -473,7 +474,7 @@ createIntegrationTestSuite('Valuation and Vesting types via batch API', (getCont expect(result.updatedCapTableCid).toBeTruthy(); const valuationResult = await ctx.ocp.OpenCapTable.valuation.get({ - contractId: extractContractIdString(result.createdCids[0]), + contractId: extractContractIdString(requireFirst(result.createdCids, 'created valuation contract')), }); expect(valuationResult.data.object_type).toBe('VALUATION'); expect(valuationResult.data.stock_class_id).toBe(stockSecurity.stockClassId); diff --git a/test/integration/production/productionDataRoundtrip.integration.test.ts b/test/integration/production/productionDataRoundtrip.integration.test.ts index bd6ce3d1..8bace9b9 100644 --- a/test/integration/production/productionDataRoundtrip.integration.test.ts +++ b/test/integration/production/productionDataRoundtrip.integration.test.ts @@ -22,6 +22,7 @@ import { ocfCompare, stripInternalFields, } from '../../../src/utils/ocfComparison'; +import { requireFirst } from '../../../src/utils/requireDefined'; import { validateOcfObject } from '../../utils/ocfSchemaValidator'; import { loadProductionFixture, loadSyntheticFixture, stripSourceMetadata } from '../../utils/productionFixtures'; import { createIntegrationTestSuite, type IntegrationTestContext } from '../setup'; @@ -263,7 +264,7 @@ createIntegrationTestSuite('Production Data Round-Trip Tests', (getContext) => { // Read back as OCF const readBack = await ctx.ocp.OpenCapTable.stakeholder.get({ - contractId: extractContractIdString(result.createdCids[0]), + contractId: extractContractIdString(requireFirst(result.createdCids, 'created production fixture contract')), }); // Validate OCF schema @@ -296,7 +297,7 @@ createIntegrationTestSuite('Production Data Round-Trip Tests', (getContext) => { expect(result.createdCids).toHaveLength(1); const readBack = await ctx.ocp.OpenCapTable.stakeholder.get({ - contractId: extractContractIdString(result.createdCids[0]), + contractId: extractContractIdString(requireFirst(result.createdCids, 'created production fixture contract')), }); await validateOcfObject(readBack.data as unknown as Record); @@ -324,7 +325,7 @@ createIntegrationTestSuite('Production Data Round-Trip Tests', (getContext) => { expect(result.createdCids).toHaveLength(1); const readBack = await ctx.ocp.OpenCapTable.stockLegendTemplate.get({ - contractId: extractContractIdString(result.createdCids[0]), + contractId: extractContractIdString(requireFirst(result.createdCids, 'created production fixture contract')), }); await validateOcfObject(readBack.data as unknown as Record); @@ -352,7 +353,7 @@ createIntegrationTestSuite('Production Data Round-Trip Tests', (getContext) => { expect(result.createdCids).toHaveLength(1); const readBack = await ctx.ocp.OpenCapTable.document.get({ - contractId: extractContractIdString(result.createdCids[0]), + contractId: extractContractIdString(requireFirst(result.createdCids, 'created production fixture contract')), }); await validateOcfObject(readBack.data as unknown as Record); @@ -385,7 +386,7 @@ createIntegrationTestSuite('Production Data Round-Trip Tests', (getContext) => { expect(result.createdCids).toHaveLength(1); const readBack = await ctx.ocp.OpenCapTable.vestingTerms.get({ - contractId: extractContractIdString(result.createdCids[0]), + contractId: extractContractIdString(requireFirst(result.createdCids, 'created production fixture contract')), }); await validateOcfObject(readBack.data as unknown as Record); @@ -413,7 +414,7 @@ createIntegrationTestSuite('Production Data Round-Trip Tests', (getContext) => { expect(result.createdCids).toHaveLength(1); const readBack = await ctx.ocp.OpenCapTable.stockPlan.get({ - contractId: extractContractIdString(result.createdCids[0]), + contractId: extractContractIdString(requireFirst(result.createdCids, 'created production fixture contract')), }); await validateOcfObject(readBack.data as unknown as Record); @@ -1987,7 +1988,7 @@ createIntegrationTestSuite('Production Data Round-Trip Tests', (getContext) => { expect(result.createdCids).toHaveLength(1); const readBack = await ctx.ocp.OpenCapTable.stakeholderRelationshipChangeEvent.get({ - contractId: extractContractIdString(result.createdCids[0]), + contractId: extractContractIdString(requireFirst(result.createdCids, 'created production fixture contract')), }); await validateOcfObject(readBack.data as unknown as Record); @@ -2034,7 +2035,7 @@ createIntegrationTestSuite('Production Data Round-Trip Tests', (getContext) => { expect(result.createdCids).toHaveLength(1); const readBack = await ctx.ocp.OpenCapTable.stakeholderStatusChangeEvent.get({ - contractId: extractContractIdString(result.createdCids[0]), + contractId: extractContractIdString(requireFirst(result.createdCids, 'created production fixture contract')), }); await validateOcfObject(readBack.data as unknown as Record); diff --git a/test/integration/setup/contractDeployment.ts b/test/integration/setup/contractDeployment.ts index 625a6218..c9ac14e9 100644 --- a/test/integration/setup/contractDeployment.ts +++ b/test/integration/setup/contractDeployment.ts @@ -9,6 +9,7 @@ import type { DisclosedContract } from '@fairmint/canton-node-sdk/build/src/clie import { Fairmint } from '@fairmint/open-captable-protocol-daml-js'; import { resolveOpenCapTableDarForOcpSdkRepo } from '../../../scripts/lib/resolveOpenCapTableDarForOcpSdkRepo'; +import { requireFirst } from '../../../src/utils/requireDefined'; /** Result of deploying contracts and creating the factory. */ export interface DeploymentResult { @@ -108,8 +109,7 @@ async function createOcpFactory( throw new Error('No events found in OcpFactory creation response'); } - const eventKeys = Object.keys(eventsById); - const firstEvent = eventsById[eventKeys[0]]; + const firstEvent = requireFirst(Object.values(eventsById), 'OcpFactory creation event'); if (!('CreatedTreeEvent' in firstEvent)) { throw new Error('First event is not a CreatedTreeEvent'); diff --git a/test/integration/setup/integrationTestHarness.ts b/test/integration/setup/integrationTestHarness.ts index dc9b3a9e..66eb1e97 100644 --- a/test/integration/setup/integrationTestHarness.ts +++ b/test/integration/setup/integrationTestHarness.ts @@ -15,6 +15,7 @@ import type { DisclosedContract } from '@fairmint/canton-node-sdk/build/src/clients/ledger-json-api/schemas/api/commands'; import { OcpClient } from '../../../src/OcpClient'; +import { requireFirst } from '../../../src/utils/requireDefined'; import { createLedgerAndValidatorClients } from '../../utils/cantonNodeSdkCompat'; import { buildIntegrationTestClientConfig } from '../../utils/testConfig'; import { createFeaturedAppRight, deployAndCreateFactory, type DeploymentResult } from './contractDeployment'; @@ -115,7 +116,7 @@ async function initializeHarness(): Promise { const appProviderParty = partyDetails.find( (p) => p.party.toLowerCase().includes('app_provider') || p.party.toLowerCase().includes('provider') ); - const authenticatedParty = appProviderParty?.party ?? partyDetails[0].party; + const authenticatedParty = appProviderParty?.party ?? requireFirst(partyDetails, 'authenticated party').party; console.log(` Available parties: ${partyDetails.map((p) => p.party.split('::')[0]).join(', ')}`); console.log(` Using party: ${authenticatedParty}`); diff --git a/test/integration/utils/setupTestData.ts b/test/integration/utils/setupTestData.ts index aa1e4529..e511f528 100644 --- a/test/integration/utils/setupTestData.ts +++ b/test/integration/utils/setupTestData.ts @@ -110,7 +110,8 @@ export async function getCapTableDetails( export function generateDateString(daysFromNow = 0): string { const date = new Date(); date.setDate(date.getDate() + daysFromNow); - return date.toISOString().split('T')[0]; + const isoString = date.toISOString(); + return isoString.split('T')[0] ?? isoString; } /** Create test issuer data with optional overrides. */ diff --git a/test/integration/workflows/capTableWorkflow.integration.test.ts b/test/integration/workflows/capTableWorkflow.integration.test.ts index 0420e8bb..e14f9f86 100644 --- a/test/integration/workflows/capTableWorkflow.integration.test.ts +++ b/test/integration/workflows/capTableWorkflow.integration.test.ts @@ -14,6 +14,7 @@ * ``` */ +import { requireDefined, requireFirst } from '../../../src/utils/requireDefined'; import { validateOcfObject } from '../../utils/ocfSchemaValidator'; import { createIntegrationTestSuite } from '../setup'; import { @@ -126,13 +127,13 @@ createIntegrationTestSuite('Cap Table Workflow', (getContext) => { // Verify all stakeholders can be read back as valid OCF const founder1Ocf = await ctx.ocp.OpenCapTable.stakeholder.get({ - contractId: extractContractIdString(result1.createdCids[0]), + contractId: extractContractIdString(requireDefined(result1.createdCids[0], 'first founder contract')), }); expect(founder1Ocf.data.name.legal_name).toBe('Alice Founder'); await validateOcfObject(founder1Ocf.data as unknown as Record); const founder2Ocf = await ctx.ocp.OpenCapTable.stakeholder.get({ - contractId: extractContractIdString(result1.createdCids[1]), + contractId: extractContractIdString(requireDefined(result1.createdCids[1], 'second founder contract')), }); expect(founder2Ocf.data.name.legal_name).toBe('Bob Cofounder'); await validateOcfObject(founder2Ocf.data as unknown as Record); @@ -211,7 +212,9 @@ createIntegrationTestSuite('Cap Table Workflow', (getContext) => { const result = await batch.create('stakeholder', stakeholderData).execute(); expect(result.createdCids).toHaveLength(1); - createdStakeholderIds.push(extractContractIdString(result.createdCids[0])); + createdStakeholderIds.push( + extractContractIdString(requireFirst(result.createdCids, 'created sequential stakeholder contract')) + ); // Update for next iteration currentCapTableCid = result.updatedCapTableCid; @@ -223,9 +226,9 @@ createIntegrationTestSuite('Cap Table Workflow', (getContext) => { } // Verify all stakeholders exist and are readable - for (let i = 0; i < createdStakeholderIds.length; i++) { + for (const [i, contractId] of createdStakeholderIds.entries()) { const ocf = await ctx.ocp.OpenCapTable.stakeholder.get({ - contractId: createdStakeholderIds[i], + contractId, }); expect(ocf.data.name.legal_name).toBe(`Sequential Stakeholder ${i + 1}`); } @@ -385,7 +388,7 @@ createIntegrationTestSuite('Cap Table Workflow', (getContext) => { // Verify the edit worked - use editDeleteResult.editedCids[0] since DAML archives the original // contract and creates a new one with a new contract ID after an edit const editedOcf = await ctx.ocp.OpenCapTable.stakeholder.get({ - contractId: extractContractIdString(editDeleteResult.editedCids[0]), + contractId: extractContractIdString(requireFirst(editDeleteResult.editedCids, 'edited stakeholder contract')), }); expect(editedOcf.data.name.legal_name).toBe('Successfully Edited'); }); diff --git a/test/schemaAlignment/enumAlignment.test.ts b/test/schemaAlignment/enumAlignment.test.ts index 4c8851c1..15701cfc 100644 --- a/test/schemaAlignment/enumAlignment.test.ts +++ b/test/schemaAlignment/enumAlignment.test.ts @@ -1,5 +1,6 @@ import fs from 'fs'; import path from 'path'; +import { requireDefined } from '../../src/utils/requireDefined'; const ENUM_SCHEMA_DIR = path.join(__dirname, '../../libs/Open-Cap-Format-OCF/schema/enums'); @@ -190,13 +191,13 @@ function getOcfEnumValues(schemaPath: string): string[] { describe('OCF Enum Schema Alignment', () => { it('AuthorizedShares values use OCF-canonical space form', () => { - const mapping = ENUM_MAPPINGS['AuthorizedShares']; + const mapping = requireDefined(ENUM_MAPPINGS['AuthorizedShares'], 'AuthorizedShares enum mapping'); expect(mapping.sdkValues).toContain('NOT APPLICABLE'); expect(mapping.sdkValues).not.toContain('NOT_APPLICABLE'); }); it('ConversionMechanismType covers all 8 OCF values', () => { - const mapping = ENUM_MAPPINGS['ConversionMechanismType']; + const mapping = requireDefined(ENUM_MAPPINGS['ConversionMechanismType'], 'ConversionMechanismType enum mapping'); const expected = [ 'FIXED_AMOUNT_CONVERSION', 'FIXED_PERCENT_OF_CAPITALIZATION_CONVERSION', diff --git a/test/utils/ocfZodSchemas.test.ts b/test/utils/ocfZodSchemas.test.ts index 9514e8cf..192390e4 100644 --- a/test/utils/ocfZodSchemas.test.ts +++ b/test/utils/ocfZodSchemas.test.ts @@ -6,6 +6,7 @@ import { } from '../../src/functions/OpenCapTable/capTable/batchTypes'; import { parseOcfEntityInput, parseOcfObject, resolveOcfSchemaDir } from '../../src/utils/ocfZodSchemas'; import { PLAN_SECURITY_OBJECT_TYPE_MAP, type PlanSecurityObjectType } from '../../src/utils/planSecurityAliases'; +import { requireDefined } from '../../src/utils/requireDefined'; import { ZERO_UUID } from '../../src/utils/zeroUuidNormalization'; import { loadProductionFixture, loadSyntheticFixture, stripSourceMetadata } from './productionFixtures'; @@ -39,7 +40,8 @@ const entityDiscriminatorCases = entityTypes.map((entityType, index) => { return { entityType, expectedObjectType: ENTITY_OBJECT_TYPE_MAP[entityType], - mismatchedObjectType: ENTITY_OBJECT_TYPE_MAP[mismatchedEntityType], + mismatchedObjectType: + ENTITY_OBJECT_TYPE_MAP[requireDefined(mismatchedEntityType, `mismatched entity type for ${entityType}`)], }; }); diff --git a/test/utils/planSecurityAliases.test.ts b/test/utils/planSecurityAliases.test.ts index dc06672e..ffe609fe 100644 --- a/test/utils/planSecurityAliases.test.ts +++ b/test/utils/planSecurityAliases.test.ts @@ -11,6 +11,7 @@ import { PLAN_SECURITY_OBJECT_TYPE_MAP, PLAN_SECURITY_TO_EQUITY_COMPENSATION_MAP, } from '../../src/utils/planSecurityAliases'; +import { requireDefined, requireFirst } from '../../src/utils/requireDefined'; import { ZERO_UUID } from '../../src/utils/zeroUuidNormalization'; import { validateOcfObject } from './ocfSchemaValidator'; @@ -883,8 +884,8 @@ describe('PlanSecurity alias utilities', () => { items: [{ rate: '0.10' }, { rate: '5.00' }], }); const items = result.items as Array>; - expect(items[0].rate).toBe('0.1'); - expect(items[1].rate).toBe('5'); + expect(requireDefined(items[0], 'first normalized item').rate).toBe('0.1'); + expect(requireDefined(items[1], 'second normalized item').rate).toBe('5'); }); it('normalizes values three levels deep', () => { @@ -902,10 +903,13 @@ describe('PlanSecurity alias utilities', () => { ], }); const triggers = result.conversion_triggers as Array>; - const right = triggers[0].conversion_right as Record; + const right = requireFirst(triggers, 'normalized conversion trigger').conversion_right as Record< + string, + unknown + >; const mechanism = right.conversion_mechanism as Record; const rates = mechanism.interest_rates as Array>; - expect(rates[0].rate).toBe('0.1'); + expect(requireFirst(rates, 'normalized interest rate').rate).toBe('0.1'); }); }); @@ -938,13 +942,18 @@ describe('PlanSecurity alias utilities', () => { expect((result.investment_amount as Record).amount).toBe('100000'); const triggers = result.conversion_triggers as Array>; - const right = triggers[0].conversion_right as Record; + const right = requireFirst(triggers, 'normalized conversion trigger').conversion_right as Record< + string, + unknown + >; const mechanism = right.conversion_mechanism as Record; const rates = mechanism.interest_rates as Array>; - expect(rates[0].rate).toBe('0'); - expect(rates[0].accrual_start_date).toBe('2024-01-15'); - expect(rates[1].rate).toBe('0.1'); - expect(rates[1].accrual_start_date).toBe('2024-06-15'); + const firstRate = requireDefined(rates[0], 'first normalized interest rate'); + const secondRate = requireDefined(rates[1], 'second normalized interest rate'); + expect(firstRate.rate).toBe('0'); + expect(firstRate.accrual_start_date).toBe('2024-01-15'); + expect(secondRate.rate).toBe('0.1'); + expect(secondRate.accrual_start_date).toBe('2024-06-15'); expect(result.date).toBe('2024-01-15'); expect(result.id).toBe('ci-001'); expect(result.security_id).toBe('sec-001'); @@ -1079,10 +1088,12 @@ describe('PlanSecurity alias utilities', () => { it('strips remainder: false from vesting condition portions', () => { const result = normalizeOcfData(makeVestingTerms()); const conditions = result.vesting_conditions as Array<{ portion?: Record }>; - expect(conditions[1].portion).toBeDefined(); - expect('remainder' in conditions[1].portion!).toBe(false); - expect(conditions[2].portion).toBeDefined(); - expect('remainder' in conditions[2].portion!).toBe(false); + const cliffPortion = requireDefined(conditions[1], 'cliff vesting condition').portion; + const monthlyPortion = requireDefined(conditions[2], 'monthly vesting condition').portion; + expect(cliffPortion).toBeDefined(); + expect(cliffPortion === undefined ? true : 'remainder' in cliffPortion).toBe(false); + expect(monthlyPortion).toBeDefined(); + expect(monthlyPortion === undefined ? true : 'remainder' in monthlyPortion).toBe(false); }); it('preserves remainder: true', () => { @@ -1098,7 +1109,7 @@ describe('PlanSecurity alias utilities', () => { }); const result = normalizeOcfData(input); const conditions = result.vesting_conditions as Array<{ portion?: Record }>; - expect(conditions[0].portion!.remainder).toBe(true); + expect(requireFirst(conditions, 'normalized vesting condition').portion?.remainder).toBe(true); }); it('strips empty comments array', () => { @@ -1155,7 +1166,7 @@ describe('PlanSecurity alias utilities', () => { const result = normalizeOcfData(input); const triggers = result.conversion_triggers as Array>; - const right = triggers[0].conversion_right as Record; + const right = requireFirst(triggers, 'normalized conversion trigger').conversion_right as Record; const mechanism = right.conversion_mechanism as Record; const rules = mechanism.capitalization_definition_rules as Record; @@ -1211,7 +1222,7 @@ describe('PlanSecurity alias utilities', () => { const result = normalizeOcfData(input); const triggers = result.conversion_triggers as Array>; - const right = triggers[0].conversion_right as Record; + const right = requireFirst(triggers, 'normalized conversion trigger').conversion_right as Record; const mechanism = right.conversion_mechanism as Record; const rules = mechanism.capitalization_definition_rules as Record; @@ -1265,7 +1276,10 @@ describe('PlanSecurity alias utilities', () => { } as Record; const result = normalizeOcfData(stockClass); expect(result.conversion_rights).toHaveLength(1); - expect((result.conversion_rights as Array>)[0].conversion_mechanism).toMatchObject({ + expect( + requireFirst(result.conversion_rights as Array>, 'normalized conversion right') + .conversion_mechanism + ).toMatchObject({ type: 'RATIO_CONVERSION', ratio: { numerator: '2', denominator: '1' }, }); diff --git a/test/utils/replicationHelpers.test.ts b/test/utils/replicationHelpers.test.ts index cba33303..5315b2e0 100644 --- a/test/utils/replicationHelpers.test.ts +++ b/test/utils/replicationHelpers.test.ts @@ -14,6 +14,7 @@ import { type SourceReplicationItem, TRANSACTION_SUBTYPE_MAP, } from '../../src/utils/replicationHelpers'; +import { requireFirst } from '../../src/utils/requireDefined'; import { validateOcfObject } from './ocfSchemaValidator'; // ============================================================================ @@ -210,6 +211,10 @@ describe('mapCategorizedTypeToEntityType', () => { expect(mapCategorizedTypeToEntityType('OBJECT', 'UNKNOWN')).toBeNull(); }); + it('does not treat inherited object properties as OBJECT subtypes', () => { + expect(mapCategorizedTypeToEntityType('OBJECT', 'toString')).toBeNull(); + }); + it('returns null for OBJECT without subtype', () => { expect(mapCategorizedTypeToEntityType('OBJECT', null)).toBeNull(); }); @@ -240,6 +245,10 @@ describe('mapCategorizedTypeToEntityType', () => { expect(mapCategorizedTypeToEntityType('TRANSACTION', 'TX_UNKNOWN')).toBeNull(); }); + it('does not treat inherited object properties as TRANSACTION subtypes', () => { + expect(mapCategorizedTypeToEntityType('TRANSACTION', 'toString')).toBeNull(); + }); + it('returns null for TRANSACTION without subtype', () => { expect(mapCategorizedTypeToEntityType('TRANSACTION', null)).toBeNull(); }); @@ -250,6 +259,10 @@ describe('mapCategorizedTypeToEntityType', () => { expect(mapCategorizedTypeToEntityType('UNKNOWN', null)).toBeNull(); }); + it('does not treat inherited object properties as direct types', () => { + expect(mapCategorizedTypeToEntityType('toString', null)).toBeNull(); + }); + it('returns null for empty string', () => { expect(mapCategorizedTypeToEntityType('', null)).toBeNull(); }); @@ -520,6 +533,13 @@ describe('buildCantonOcfDataMap', () => { expect(() => buildCantonOcfDataMap(manifest)).toThrow('Unsupported transaction object_type: TX_UNKNOWN_TYPE'); }); + it('throws when transaction object_type is an inherited object property', () => { + const manifest = createEmptyManifest(); + manifest.transactions = [{ id: 'tx-1', object_type: 'toString' }]; + + expect(() => buildCantonOcfDataMap(manifest)).toThrow('Unsupported transaction object_type: toString'); + }); + it('throws when transaction has no id', () => { const manifest = createEmptyManifest(); manifest.transactions = [{ object_type: 'TX_STOCK_ISSUANCE' }]; @@ -698,7 +718,7 @@ describe('computeReplicationDiff', () => { const diff = computeReplicationDiff(sourceItems, cantonState, { cantonOcfData }); expect(diff.edits).toHaveLength(1); - expect(diff.edits[0].id).toBe('tx-1'); + expect(requireFirst(diff.edits, 'replication edit').id).toBe('tx-1'); }); it('treats stakeholder current_relationship as equivalent to current_relationships', async () => { @@ -876,7 +896,7 @@ describe('computeReplicationDiff', () => { const diff = computeReplicationDiff(sourceItems, cantonState); expect(diff.deletes).toHaveLength(1); - expect(diff.deletes[0].id).toBe('sh-2'); + expect(requireFirst(diff.deletes, 'replication delete').id).toBe('sh-2'); }); }); @@ -891,7 +911,7 @@ describe('computeReplicationDiff', () => { const diff = computeReplicationDiff(sourceItems, cantonState); expect(diff.creates).toHaveLength(1); - expect(diff.creates[0].data).toEqual({ id: 'sh-1', version: 1 }); // First occurrence wins + expect(requireFirst(diff.creates, 'replication create').data).toEqual({ id: 'sh-1', version: 1 }); // First occurrence wins }); }); @@ -931,8 +951,9 @@ describe('computeReplicationDiff', () => { expect(diff.creates).toHaveLength(0); expect(diff.edits).toHaveLength(1); - expect(diff.edits[0].id).toBe('eq-1'); - expect(diff.edits[0].entityType).toBe('equityCompensationIssuance'); + const edit = requireFirst(diff.edits, 'replication edit'); + expect(edit.id).toBe('eq-1'); + expect(edit.entityType).toBe('equityCompensationIssuance'); }); }); @@ -951,14 +972,15 @@ describe('computeReplicationDiff', () => { // Item still appears in creates (the canonical object ID is missing from Canton) expect(diff.creates).toHaveLength(1); - expect(diff.creates[0].id).toBe('tx-new'); + expect(requireFirst(diff.creates, 'replication create').id).toBe('tx-new'); // But conflict is flagged expect(diff.conflicts).toHaveLength(1); - expect(diff.conflicts[0].id).toBe('tx-new'); - expect(diff.conflicts[0].securityId).toBe('sec-existing'); - expect(diff.conflicts[0].entityType).toBe('stockIssuance'); - expect(diff.conflicts[0].message).toContain('security_id="sec-existing"'); - expect(diff.conflicts[0].message).toContain('already exists on Canton'); + const conflict = requireFirst(diff.conflicts, 'replication conflict'); + expect(conflict.id).toBe('tx-new'); + expect(conflict.securityId).toBe('sec-existing'); + expect(conflict.entityType).toBe('stockIssuance'); + expect(conflict.message).toContain('security_id="sec-existing"'); + expect(conflict.message).toContain('already exists on Canton'); }); it('detects conflict for convertibleIssuance', () => { @@ -973,7 +995,7 @@ describe('computeReplicationDiff', () => { }); expect(diff.conflicts).toHaveLength(1); - expect(diff.conflicts[0].entityType).toBe('convertibleIssuance'); + expect(requireFirst(diff.conflicts, 'replication conflict').entityType).toBe('convertibleIssuance'); }); it('detects conflict for equityCompensationIssuance', () => { @@ -988,8 +1010,9 @@ describe('computeReplicationDiff', () => { }); expect(diff.conflicts).toHaveLength(1); - expect(diff.conflicts[0].entityType).toBe('equityCompensationIssuance'); - expect(diff.conflicts[0].message).toContain('Equity Compensation Issuance'); + const conflict = requireFirst(diff.conflicts, 'replication conflict'); + expect(conflict.entityType).toBe('equityCompensationIssuance'); + expect(conflict.message).toContain('Equity Compensation Issuance'); }); it('no conflict when security_id is new', () => { @@ -1179,7 +1202,7 @@ describe('computeReplicationDiff', () => { // md5 actually changed → should detect an edit expect(diff.edits).toHaveLength(1); - expect(diff.edits[0].id).toBe('doc-1'); + expect(requireFirst(diff.edits, 'replication edit').id).toBe('doc-1'); }); }); diff --git a/test/utils/transactionHelpers.test.ts b/test/utils/transactionHelpers.test.ts new file mode 100644 index 00000000..b3912762 --- /dev/null +++ b/test/utils/transactionHelpers.test.ts @@ -0,0 +1,14 @@ +import { safeGet } from '../../src/utils/transactionHelpers'; + +describe('transaction helpers', () => { + describe('safeGet', () => { + it('returns an own nested leaf value', () => { + expect(safeGet({ nested: { value: 'ocf-id' } }, ['nested', 'value'])).toBe('ocf-id'); + }); + + it('rejects inherited path segments', () => { + expect(safeGet({}, ['toString'])).toBeUndefined(); + expect(safeGet(Object.create({ inherited: { value: 'ocf-id' } }), ['inherited', 'value'])).toBeUndefined(); + }); + }); +}); diff --git a/test/utils/transactionSorting.test.ts b/test/utils/transactionSorting.test.ts index a88131e5..2522c96c 100644 --- a/test/utils/transactionSorting.test.ts +++ b/test/utils/transactionSorting.test.ts @@ -13,6 +13,7 @@ import { sortTransactions, txWeight, } from '../../src/utils/cantonOcfExtractor'; +import { requireDefined } from '../../src/utils/requireDefined'; describe('getTimestampOrNull', () => { it('returns null for null input', () => { @@ -262,10 +263,10 @@ describe('sortTransactions', () => { const sorted = sortTransactions(transactions); // sec-a comes before sec-b alphabetically, then by created timestamp / id - expect(sorted[0].security_id).toBe('sec-a'); - expect(sorted[1].security_id).toBe('sec-a'); - expect(sorted[2].security_id).toBe('sec-b'); - expect(sorted[3].security_id).toBe('sec-b'); + expect(requireDefined(sorted[0], 'first sorted transaction').security_id).toBe('sec-a'); + expect(requireDefined(sorted[1], 'second sorted transaction').security_id).toBe('sec-a'); + expect(requireDefined(sorted[2], 'third sorted transaction').security_id).toBe('sec-b'); + expect(requireDefined(sorted[3], 'fourth sorted transaction').security_id).toBe('sec-b'); }); it('sorts by createdAt within same day, weight, and security_id', () => { diff --git a/test/validation/boundaries.test.ts b/test/validation/boundaries.test.ts index a1889f2e..e10d4487 100644 --- a/test/validation/boundaries.test.ts +++ b/test/validation/boundaries.test.ts @@ -11,12 +11,13 @@ import { damlStakeholderRelationshipToNative, type DamlStakeholderRelationshipType, } from '../../src/utils/enumConversions'; +import { requireFirst } from '../../src/utils/requireDefined'; import { normalizeNumericString, optionalString } from '../../src/utils/typeConversions'; function getDashboardPrimaryRelationshipFromDb(stakeholder: OcfStakeholder): string { const relationships = stakeholder.current_relationships ?? []; if (relationships.length > 0) { - return relationships[0]; + return requireFirst(relationships, 'stakeholder relationship'); } return stakeholder.current_relationship ?? 'OTHER'; } @@ -25,7 +26,7 @@ function getDashboardPrimaryRelationshipFromDaml(damlRelationships: DamlStakehol if (damlRelationships.length === 0) { return 'OTHER'; } - return damlStakeholderRelationshipToNative(damlRelationships[0]); + return damlStakeholderRelationshipToNative(requireFirst(damlRelationships, 'DAML stakeholder relationship')); } describe('Boundary Condition Tests', () => { diff --git a/tsconfig.json b/tsconfig.json index 6f119664..0d965d86 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "noImplicitOverride": true, "noUnusedLocals": true, "noUnusedParameters": true, + "noUncheckedIndexedAccess": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true,