2025-05-30 04:29:13 +00:00
import { tap , expect } from '@git.zone/tstest/tapbundle' ;
2025-05-26 04:04:51 +00:00
import * as path from 'path' ;
import { EInvoice } from '../../../ts/index.js' ;
2025-05-30 04:29:13 +00:00
import { PerformanceTracker } from '../../helpers/performance.tracker.instance.js' ;
2025-05-26 04:04:51 +00:00
import { CorpusLoader } from '../../helpers/corpus.loader.js' ;
2025-05-30 04:29:13 +00:00
tap . test ( 'STD-04: ZUGFeRD 2.1 Compliance - should validate ZUGFeRD 2.1 standard compliance' , async ( ) = > {
2025-05-26 04:04:51 +00:00
const einvoice = new EInvoice ( ) ;
const corpusLoader = new CorpusLoader ( ) ;
2025-05-30 04:29:13 +00:00
const performanceTracker = new PerformanceTracker ( 'STD-04: ZUGFeRD 2.1 Compliance' ) ;
2025-05-26 04:04:51 +00:00
// Test 1: ZUGFeRD 2.1 profile validation
const profileValidation = await performanceTracker . measureAsync (
'zugferd-profile-validation' ,
async ( ) = > {
const zugferdProfiles = [
{ profile : 'MINIMUM' , mandatory : [ 'BT-1' , 'BT-2' , 'BT-9' , 'BT-112' , 'BT-115' ] , description : 'Basic booking aids' } ,
{ profile : 'BASIC-WL' , mandatory : [ 'BT-1' , 'BT-2' , 'BT-5' , 'BT-27' , 'BT-44' , 'BT-109' ] , description : 'Basic without lines' } ,
{ profile : 'BASIC' , mandatory : [ 'BT-1' , 'BT-2' , 'BT-5' , 'BT-27' , 'BT-44' , 'BT-109' , 'BT-112' ] , description : 'Basic with lines' } ,
{ profile : 'EN16931' , mandatory : [ 'BT-1' , 'BT-2' , 'BT-5' , 'BT-6' , 'BT-9' , 'BT-24' , 'BT-27' , 'BT-44' ] , description : 'EN16931 compliant' } ,
{ profile : 'EXTENDED' , mandatory : [ 'BT-1' , 'BT-2' , 'BT-5' , 'BT-27' , 'BT-44' ] , description : 'Extended with additional fields' } ,
] ;
const results = [ ] ;
for ( const profile of zugferdProfiles ) {
results . push ( {
profile : profile.profile ,
description : profile.description ,
mandatoryFieldCount : profile.mandatory.length ,
profileIdentifier : ` urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p1: ${ profile . profile . toLowerCase ( ) } ` ,
} ) ;
}
return results ;
}
) ;
2025-05-30 04:29:13 +00:00
expect ( profileValidation . length ) . toEqual ( 5 ) ;
expect ( profileValidation . find ( p = > p . profile === 'EN16931' ) ) . toBeTruthy ( ) ;
2025-05-26 04:04:51 +00:00
// Test 2: ZUGFeRD 2.1 field mapping
const fieldMapping = await performanceTracker . measureAsync (
'zugferd-field-mapping' ,
async ( ) = > {
const zugferdFieldMapping = {
// Document level
'rsm:ExchangedDocument/ram:ID' : 'BT-1' , // Invoice number
'rsm:ExchangedDocument/ram:IssueDateTime' : 'BT-2' , // Issue date
'rsm:ExchangedDocument/ram:TypeCode' : 'BT-3' , // Invoice type code
'rsm:ExchangedDocument/ram:IncludedNote' : 'BT-22' , // Invoice note
// Process control
'rsm:ExchangedDocumentContext/ram:GuidelineSpecifiedDocumentContextParameter/ram:ID' : 'BT-24' , // Specification identifier
'rsm:ExchangedDocumentContext/ram:BusinessProcessSpecifiedDocumentContextParameter/ram:ID' : 'BT-23' , // Business process
// Buyer
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty/ram:Name' : 'BT-44' , // Buyer name
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty/ram:SpecifiedLegalOrganization/ram:ID' : 'BT-47' , // Buyer legal registration
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty/ram:SpecifiedTaxRegistration/ram:ID' : 'BT-48' , // Buyer VAT identifier
// Seller
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:Name' : 'BT-27' , // Seller name
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:SpecifiedLegalOrganization/ram:ID' : 'BT-30' , // Seller legal registration
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:SellerTradeParty/ram:SpecifiedTaxRegistration/ram:ID' : 'BT-31' , // Seller VAT identifier
// Monetary totals
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:LineTotalAmount' : 'BT-106' , // Sum of line net amounts
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TaxBasisTotalAmount' : 'BT-109' , // Invoice total without VAT
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:GrandTotalAmount' : 'BT-112' , // Invoice total with VAT
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:DuePayableAmount' : 'BT-115' , // Amount due for payment
// Currency
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:InvoiceCurrencyCode' : 'BT-5' , // Invoice currency code
'rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:TaxCurrencyCode' : 'BT-6' , // VAT accounting currency code
} ;
return {
totalMappings : Object.keys ( zugferdFieldMapping ) . length ,
categories : {
document : Object . keys ( zugferdFieldMapping ) . filter ( k = > k . includes ( 'ExchangedDocument' ) ) . length ,
parties : Object.keys ( zugferdFieldMapping ) . filter ( k = > k . includes ( 'TradeParty' ) ) . length ,
monetary : Object.keys ( zugferdFieldMapping ) . filter ( k = > k . includes ( 'MonetarySummation' ) ) . length ,
process : Object.keys ( zugferdFieldMapping ) . filter ( k = > k . includes ( 'DocumentContext' ) ) . length ,
}
} ;
}
) ;
2025-05-30 04:29:13 +00:00
expect ( fieldMapping . totalMappings ) . toBeGreaterThan ( 15 ) ;
expect ( fieldMapping . categories . document ) . toBeGreaterThan ( 0 ) ;
2025-05-26 04:04:51 +00:00
// Test 3: ZUGFeRD 2.1 namespace validation
const namespaceValidation = await performanceTracker . measureAsync (
'zugferd-namespace-validation' ,
async ( ) = > {
const zugferdNamespaces = {
'rsm' : 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100' ,
'ram' : 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100' ,
'qdt' : 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100' ,
'udt' : 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100' ,
'xsi' : 'http://www.w3.org/2001/XMLSchema-instance' ,
} ;
const schemaLocations = [
'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100 CrossIndustryInvoice_100pD16B.xsd' ,
'urn:un:unece:uncefact:data:draft:ReusableAggregateBusinessInformationEntity:100 ReusableAggregateBusinessInformationEntity_100pD16B.xsd' ,
] ;
return {
namespaceCount : Object.keys ( zugferdNamespaces ) . length ,
requiredNamespaces : Object.entries ( zugferdNamespaces ) . map ( ( [ prefix , uri ] ) = > ( {
prefix ,
uri ,
required : [ 'rsm' , 'ram' ] . includes ( prefix )
} ) ) ,
schemaLocationCount : schemaLocations.length ,
rootElement : 'rsm:CrossIndustryInvoice' ,
} ;
}
) ;
2025-05-30 04:29:13 +00:00
expect ( namespaceValidation . namespaceCount ) . toBeGreaterThan ( 4 ) ;
expect ( namespaceValidation . rootElement ) . toEqual ( 'rsm:CrossIndustryInvoice' ) ;
2025-05-26 04:04:51 +00:00
// Test 4: ZUGFeRD 2.1 code list validation
const codeListValidation = await performanceTracker . measureAsync (
'zugferd-code-list-validation' ,
async ( ) = > {
const zugferdCodeLists = {
// Document type codes (BT-3)
documentTypeCodes : [ '380' , '381' , '384' , '389' , '751' ] ,
// Currency codes (ISO 4217)
currencyCodes : [ 'EUR' , 'USD' , 'GBP' , 'CHF' , 'JPY' , 'CNY' ] ,
// Country codes (ISO 3166-1)
countryCodes : [ 'DE' , 'FR' , 'IT' , 'ES' , 'NL' , 'BE' , 'AT' , 'CH' ] ,
// Tax category codes (UNCL5305)
taxCategoryCodes : [ 'S' , 'Z' , 'E' , 'AE' , 'K' , 'G' , 'O' , 'L' , 'M' ] ,
// Payment means codes (UNCL4461)
paymentMeansCodes : [ '10' , '20' , '30' , '42' , '48' , '49' , '58' , '59' ] ,
// Unit codes (UN/ECE Recommendation 20)
unitCodes : [ 'C62' , 'DAY' , 'HAR' , 'HUR' , 'KGM' , 'KTM' , 'KWH' , 'LS' , 'LTR' , 'MIN' , 'MMT' , 'MTK' , 'MTQ' , 'MTR' , 'NAR' , 'NPR' , 'P1' , 'PCE' , 'SET' , 'TNE' , 'WEE' ] ,
// Charge/allowance reason codes
chargeReasonCodes : [ 'AA' , 'AAA' , 'AAC' , 'AAD' , 'AAE' , 'AAF' , 'AAH' , 'AAI' ] ,
allowanceReasonCodes : [ '41' , '42' , '60' , '62' , '63' , '64' , '65' , '66' , '67' , '68' , '70' , '71' , '88' , '95' , '100' , '102' , '103' , '104' , '105' ] ,
} ;
return {
codeListCount : Object.keys ( zugferdCodeLists ) . length ,
totalCodes : Object.values ( zugferdCodeLists ) . reduce ( ( sum , list ) = > sum + list . length , 0 ) ,
codeLists : Object.entries ( zugferdCodeLists ) . map ( ( [ name , codes ] ) = > ( {
name ,
codeCount : codes.length ,
examples : codes.slice ( 0 , 3 )
} ) )
} ;
}
) ;
2025-05-30 04:29:13 +00:00
expect ( codeListValidation . codeListCount ) . toBeGreaterThan ( 7 ) ;
expect ( codeListValidation . totalCodes ) . toBeGreaterThan ( 50 ) ;
2025-05-26 04:04:51 +00:00
// Test 5: ZUGFeRD 2.1 calculation rules
const calculationRules = await performanceTracker . measureAsync (
'zugferd-calculation-rules' ,
async ( ) = > {
const rules = [
{
rule : 'BR-CO-10' ,
description : 'Sum of line net amounts = Σ(line net amounts)' ,
formula : 'BT-106 = Σ(BT-131)' ,
} ,
{
rule : 'BR-CO-11' ,
description : 'Sum of allowances on document level = Σ(document level allowance amounts)' ,
formula : 'BT-107 = Σ(BT-92)' ,
} ,
{
rule : 'BR-CO-12' ,
description : 'Sum of charges on document level = Σ(document level charge amounts)' ,
formula : 'BT-108 = Σ(BT-99)' ,
} ,
{
rule : 'BR-CO-13' ,
description : 'Invoice total without VAT = Sum of line net amounts - Sum of allowances + Sum of charges' ,
formula : 'BT-109 = BT-106 - BT-107 + BT-108' ,
} ,
{
rule : 'BR-CO-15' ,
description : 'Invoice total with VAT = Invoice total without VAT + Invoice total VAT amount' ,
formula : 'BT-112 = BT-109 + BT-110' ,
} ,
{
rule : 'BR-CO-16' ,
description : 'Amount due for payment = Invoice total with VAT - Paid amount' ,
formula : 'BT-115 = BT-112 - BT-113' ,
} ,
] ;
return {
ruleCount : rules.length ,
rules : rules ,
validationTypes : [ 'arithmetic' , 'consistency' , 'completeness' ] ,
} ;
}
) ;
2025-05-30 04:29:13 +00:00
expect ( calculationRules . ruleCount ) . toBeGreaterThan ( 5 ) ;
expect ( calculationRules . validationTypes ) . toContain ( 'arithmetic' ) ;
2025-05-26 04:04:51 +00:00
// Test 6: ZUGFeRD 2.1 business rules
const businessRules = await performanceTracker . measureAsync (
'zugferd-business-rules' ,
async ( ) = > {
const businessRuleCategories = {
documentLevel : [
'Invoice number must be unique' ,
'Issue date must not be in the future' ,
'Due date must be on or after issue date' ,
'Specification identifier must match ZUGFeRD 2.1 profile' ,
] ,
partyInformation : [
'Seller must have name' ,
'Buyer must have name' ,
'VAT identifiers must be valid format' ,
'Legal registration identifiers must be valid' ,
] ,
lineLevel : [
'Each line must have unique identifier' ,
'Line net amount must equal quantity × net price' ,
'Line VAT must be calculated correctly' ,
'Item description or name must be provided' ,
] ,
vatBreakdown : [
'VAT category taxable base must equal sum of line amounts in category' ,
'VAT category tax amount must be calculated correctly' ,
'Sum of VAT category amounts must equal total VAT' ,
] ,
paymentTerms : [
'Payment terms must be clearly specified' ,
'Bank account details must be valid if provided' ,
'Payment means code must be valid' ,
] ,
} ;
const ruleCount = Object . values ( businessRuleCategories ) . reduce ( ( sum , rules ) = > sum + rules . length , 0 ) ;
return {
totalRules : ruleCount ,
categories : Object.entries ( businessRuleCategories ) . map ( ( [ category , rules ] ) = > ( {
category ,
ruleCount : rules.length ,
examples : rules.slice ( 0 , 2 )
} ) ) ,
validationLevels : [ 'syntax' , 'schema' , 'business' , 'profile' ] ,
} ;
}
) ;
2025-05-30 04:29:13 +00:00
expect ( businessRules . totalRules ) . toBeGreaterThan ( 15 ) ;
expect ( businessRules . categories . length ) . toBeGreaterThan ( 4 ) ;
2025-05-26 04:04:51 +00:00
// Test 7: ZUGFeRD 2.1 attachment handling
const attachmentHandling = await performanceTracker . measureAsync (
'zugferd-attachment-handling' ,
async ( ) = > {
const attachmentRequirements = {
xmlAttachment : {
filename : 'factur-x.xml' ,
alternativeFilenames : [ 'ZUGFeRD-invoice.xml' , 'zugferd-invoice.xml' , 'xrechnung.xml' ] ,
mimeType : 'text/xml' ,
relationship : 'Alternative' ,
afRelationship : 'Alternative' ,
description : 'Factur-X/ZUGFeRD 2.1 invoice data' ,
} ,
pdfRequirements : {
version : 'PDF/A-3' ,
conformanceLevel : [ 'a' , 'b' , 'u' ] ,
maxFileSize : '50MB' ,
compressionAllowed : true ,
encryptionAllowed : false ,
} ,
additionalAttachments : {
allowed : true ,
types : [ 'images' , 'documents' , 'spreadsheets' ] ,
maxCount : 99 ,
maxTotalSize : '100MB' ,
} ,
} ;
return {
xmlFilename : attachmentRequirements.xmlAttachment.filename ,
pdfVersion : attachmentRequirements.pdfRequirements.version ,
additionalAttachmentsAllowed : attachmentRequirements.additionalAttachments.allowed ,
requirements : attachmentRequirements ,
} ;
}
) ;
2025-05-30 04:29:13 +00:00
expect ( attachmentHandling . xmlFilename ) . toEqual ( 'factur-x.xml' ) ;
expect ( attachmentHandling . pdfVersion ) . toEqual ( 'PDF/A-3' ) ;
2025-05-26 04:04:51 +00:00
// Test 8: Profile-specific validation
const profileSpecificValidation = await performanceTracker . measureAsync (
'profile-specific-validation' ,
async ( ) = > {
const profileRules = {
'MINIMUM' : {
forbidden : [ 'Line items' , 'VAT breakdown' , 'Payment terms details' ] ,
required : [ 'Invoice number' , 'Issue date' , 'Due date' , 'Grand total' , 'Due amount' ] ,
optional : [ 'Buyer reference' , 'Seller tax registration' ] ,
} ,
'BASIC-WL' : {
forbidden : [ 'Line items' ] ,
required : [ 'Invoice number' , 'Issue date' , 'Currency' , 'Seller' , 'Buyer' , 'VAT breakdown' ] ,
optional : [ 'Payment terms' , 'Delivery information' ] ,
} ,
'BASIC' : {
forbidden : [ 'Product characteristics' , 'Attached documents' ] ,
required : [ 'Line items' , 'VAT breakdown' , 'All EN16931 mandatory fields' ] ,
optional : [ 'Allowances/charges on line level' ] ,
} ,
'EN16931' : {
forbidden : [ 'Extensions beyond EN16931' ] ,
required : [ 'All EN16931 mandatory fields' ] ,
optional : [ 'All EN16931 optional fields' ] ,
} ,
'EXTENDED' : {
forbidden : [ ] ,
required : [ 'All BASIC fields' ] ,
optional : [ 'All ZUGFeRD extensions' , 'Additional trader parties' , 'Product characteristics' ] ,
} ,
} ;
return {
profileCount : Object.keys ( profileRules ) . length ,
profiles : Object.entries ( profileRules ) . map ( ( [ profile , rules ] ) = > ( {
profile ,
forbiddenCount : rules.forbidden.length ,
requiredCount : rules.required.length ,
optionalCount : rules.optional.length ,
} ) ) ,
} ;
}
) ;
2025-05-30 04:29:13 +00:00
expect ( profileSpecificValidation . profileCount ) . toEqual ( 5 ) ;
expect ( profileSpecificValidation . profiles . find ( p = > p . profile === 'EXTENDED' ) ? . forbiddenCount ) . toEqual ( 0 ) ;
2025-05-26 04:04:51 +00:00
// Test 9: Corpus validation - ZUGFeRD 2.1 files
const corpusValidation = await performanceTracker . measureAsync (
'corpus-validation' ,
async ( ) = > {
const results = {
total : 0 ,
byProfile : { } as Record < string , number > ,
byType : {
valid : 0 ,
invalid : 0 ,
pdf : 0 ,
xml : 0 ,
}
} ;
// Process ZUGFeRD 2.1 corpus files
const zugferd21Pattern = '**/zugferd_2p1_*.pdf' ;
2025-05-30 04:29:13 +00:00
const zugferd21Files = await CorpusLoader . loadPattern ( zugferd21Pattern , 'ZUGFERD_V2_CORRECT' ) ;
2025-05-26 04:04:51 +00:00
results . total = zugferd21Files . length ;
// Count by profile
for ( const file of zugferd21Files ) {
2025-05-30 04:29:13 +00:00
const filename = path . basename ( file . path ) ;
2025-05-26 04:04:51 +00:00
results . byType . pdf ++ ;
if ( filename . includes ( 'MINIMUM' ) ) results . byProfile [ 'MINIMUM' ] = ( results . byProfile [ 'MINIMUM' ] || 0 ) + 1 ;
else if ( filename . includes ( 'BASIC-WL' ) ) results . byProfile [ 'BASIC-WL' ] = ( results . byProfile [ 'BASIC-WL' ] || 0 ) + 1 ;
else if ( filename . includes ( 'BASIC' ) ) results . byProfile [ 'BASIC' ] = ( results . byProfile [ 'BASIC' ] || 0 ) + 1 ;
else if ( filename . includes ( 'EN16931' ) ) results . byProfile [ 'EN16931' ] = ( results . byProfile [ 'EN16931' ] || 0 ) + 1 ;
else if ( filename . includes ( 'EXTENDED' ) ) results . byProfile [ 'EXTENDED' ] = ( results . byProfile [ 'EXTENDED' ] || 0 ) + 1 ;
// Check if in correct/fail directory
2025-05-30 04:29:13 +00:00
if ( file . path . includes ( '/correct/' ) ) results . byType . valid ++ ;
else if ( file . path . includes ( '/fail/' ) ) results . byType . invalid ++ ;
2025-05-26 04:04:51 +00:00
}
// Also check for XML files
2025-05-30 04:29:13 +00:00
const xmlFiles = await CorpusLoader . loadPattern ( '**/*.xml' , 'ZUGFERD_V2_CORRECT' ) ;
2025-05-26 04:04:51 +00:00
results . byType . xml = xmlFiles . length ;
return results ;
}
) ;
2025-05-30 04:29:13 +00:00
expect ( corpusValidation . total ) . toBeGreaterThan ( 0 ) ;
expect ( Object . keys ( corpusValidation . byProfile ) . length ) . toBeGreaterThan ( 0 ) ;
2025-05-26 04:04:51 +00:00
// Test 10: XRechnung compatibility
const xrechnungCompatibility = await performanceTracker . measureAsync (
'xrechnung-compatibility' ,
async ( ) = > {
const xrechnungRequirements = {
guideline : 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.3' ,
profile : 'EN16931' ,
additionalFields : [
'BT-10' , // Buyer reference (mandatory in XRechnung)
'BT-34' , // Seller electronic address
'BT-49' , // Buyer electronic address
] ,
leitweg : {
pattern : /^[0-9]{2,12}-[0-9A-Z]{1,30}-[0-9]{2,12}$/ ,
location : 'BT-10' ,
mandatory : true ,
} ,
electronicAddress : {
schemes : [ 'EM' , 'GLN' , 'DUNS' ] ,
mandatory : true ,
} ,
} ;
return {
compatible : true ,
guideline : xrechnungRequirements.guideline ,
profile : xrechnungRequirements.profile ,
additionalRequirements : xrechnungRequirements.additionalFields.length ,
leitwegPattern : xrechnungRequirements.leitweg.pattern.toString ( ) ,
} ;
}
) ;
2025-05-30 04:29:13 +00:00
expect ( xrechnungCompatibility . compatible ) . toBeTrue ( ) ;
expect ( xrechnungCompatibility . profile ) . toEqual ( 'EN16931' ) ;
2025-05-26 04:04:51 +00:00
// Generate performance summary
console . log ( '\n📊 ZUGFeRD 2.1 Compliance Test Summary:' ) ;
2025-05-30 04:29:13 +00:00
console . log ( ` 🏁 Profile validation: ${ profileValidation . length } profiles validated ` ) ;
console . log ( ` 🗺️ Field mappings: ${ fieldMapping . totalMappings } fields mapped ` ) ;
console . log ( ` 📋 Code lists: ${ codeListValidation . codeListCount } lists, ${ codeListValidation . totalCodes } codes ` ) ;
console . log ( ` 📐 Business rules: ${ businessRules . totalRules } rules across ${ businessRules . categories . length } categories ` ) ;
console . log ( ` 📎 Attachment handling: PDF/ ${ attachmentHandling . pdfVersion } with ${ attachmentHandling . xmlFilename } ` ) ;
console . log ( ` 📁 Corpus files: ${ corpusValidation . total } ZUGFeRD 2.1 files found ` ) ;
console . log ( ` 🔄 XRechnung compatible: ${ xrechnungCompatibility . compatible ? 'Yes' : 'No' } ` ) ;
// Test completed
2025-05-26 04:04:51 +00:00
} ) ;
2025-05-30 04:29:13 +00:00
// Start the tests
tap . start ( ) ;
2025-05-26 04:04:51 +00:00
// Export for test runner compatibility
export default tap ;