@@ -41,17 +41,10 @@ import {
4141const PUBLISH_EPOCHS = Number ( process . env . PUBLISH_EPOCHS || 2 ) ;
4242const KA_COUNT = Number ( process . env . TEST_KA_BATCHES || 10 ) ;
4343const OP_TIMEOUT_MS = Number ( process . env . V10_OP_TIMEOUT_MS || 6 * 60 * 1000 ) ;
44- // 10.0.5 mainnet cores hang the sync publish HTTP response in the post-confirm
45- // swm-share fanout: the publish CONFIRMS on-chain in ~1 min (node log:
46- // "On-chain confirmed: UAL=...") but the response never arrives — observed on
47- // all 4 mainnet cores 2026-07-10, every KA, 11-min client timeouts. So: wait a
48- // BOUNDED time for the response, then VERIFY the real outcome by reading THIS
49- // KA's root entity back from the node's Verifiable Memory. Data present ==
50- // the publish objectively succeeded (count success, flag response_lost); data
51- // absent after the verify window == real failure with the timeout as evidence.
52- const PUBLISH_RESPONSE_TIMEOUT_MS = Number ( process . env . V10_PUBLISH_RESPONSE_TIMEOUT_MS || 150 * 1000 ) ;
53- const PUBLISH_VERIFY_TIMEOUT_MS = Number ( process . env . V10_PUBLISH_VERIFY_TIMEOUT_MS || 240 * 1000 ) ;
54- const PUBLISH_VERIFY_POLL_MS = 10 * 1000 ;
44+ // publish must outlast the node's ACK collection (~8 min worst case) so the
45+ // 207 with the node's real error (storage_ack_insufficient, ...) arrives
46+ // instead of a client-side timeout that hides it.
47+ const PUBLISH_TIMEOUT_MS = Math . max ( OP_TIMEOUT_MS , Number ( process . env . V10_PUBLISH_TIMEOUT_MS || 11 * 60 * 1000 ) ) ;
5548
5649// Context-graph provisioning. Publishing to Verifiable Memory needs the CG to be
5750// registered on-chain. V10_CG_REGISTER=true registers a fresh CG (~100 TRAC on the
@@ -150,13 +143,13 @@ export function makeNodeClient(baseUrl, token) {
150143 // via alsoShareSwm/alsoPublishVm, and returns kaId/ual/txHash/authorAddress
151144 // with status "vm-confirmed". We map that back to the old response shape so
152145 // the summary/report code stays unchanged.
153- publish : ( contextGraphId , quads , name ) =>
146+ publish : ( contextGraphId , quads ) =>
154147 req (
155148 'POST' ,
156149 '/api/knowledge-assets' ,
157150 {
158151 contextGraphId,
159- name : name || `jenkins-${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ,
152+ name : `jenkins-${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ,
160153 quads,
161154 alsoShareSwm : true ,
162155 alsoPublishVm : { publishEpochs : PUBLISH_EPOCHS } ,
@@ -170,10 +163,6 @@ export function makeNodeClient(baseUrl, token) {
170163 } ) ) ,
171164 query : ( sparql , contextGraphId , view = 'verifiable-memory' ) =>
172165 req ( 'POST' , '/api/query' , { sparql, contextGraphId, view } ) . then ( ( r ) => r . data ) ,
173- // named-KA descriptor (state + UAL once published) — used to recover the
174- // UAL when the publish response was lost to the swm-share response hang.
175- asset : ( name , contextGraphId ) =>
176- req ( 'GET' , `/api/knowledge-assets/${ encodeURIComponent ( name ) } ?contextGraphId=${ encodeURIComponent ( contextGraphId ) } ` ) . then ( ( r ) => r . data ) ,
177166 // cross-node read: ask a PEER (by peerId) for a KA's triples. lookup is
178167 // { lookupType, ual } for ENTITY_BY_UAL (resolves a UAL → triples; works for
179168 // any node's KA) or { lookupType:'ENTITY_TRIPLES', entityUri } for an entity.
@@ -304,17 +293,7 @@ export function defineChainPublishSuite(config) {
304293 : `✅ present (length ${ trimmedToken . length } )` ;
305294 console . log ( `🔑 ${ name } bearer token: ${ tokenState } ` ) ;
306295
307- // Log the node's version+commit — when publishes hang, the FIRST question
308- // is "which release is this node actually running?" (operator nodes may
309- // lag the npm mainnet tag), so put the answer in every build log.
310- try {
311- const st = await makeNodeClient ( hostname , token ) . status ( ) ;
312- console . log ( `📟 ${ name } node version: ${ st . version } ${ st . commitShort ? ` (commit ${ st . commitShort } )` : '' } ${ st . distTag ? ` [${ st . distTag } ]` : '' } ` ) ;
313- } catch ( error ) {
314- console . log ( `📟 ${ name } node version: unreadable (${ String ( error . message ) . slice ( 0 , 80 ) } )` ) ;
315- }
316-
317- let publishSuccess = 0 , publishFail = 0 , publishRescued = 0 ;
296+ let publishSuccess = 0 , publishFail = 0 ;
318297 let querySuccess = 0 , queryFail = 0 ;
319298 let vmGetSuccess = 0 , vmGetFail = 0 ;
320299 let queryRemoteSuccess = 0 , queryRemoteFail = 0 ;
@@ -390,16 +369,14 @@ export function defineChainPublishSuite(config) {
390369 for ( let i = 0 ; i < KA_COUNT ; i ++ ) {
391370 console . log ( `\nPublishing KA #${ i + 1 } on ${ name } ` ) ;
392371 const { quads, rootEntity } = buildQuads ( name , i + 1 ) ;
393- const kaName = `jenkins-${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ;
394372
395373 let ual = null ;
396- let publishOk = false ;
397374 let step = 'publish' ;
398375
399376 // ── 1. publish (mint to Verifiable Memory) ─────────────────────────
400377 const pubStart = Date . now ( ) ;
401378 try {
402- const result = await withTimeout ( client . publish ( contextGraphId , quads , kaName ) , 'publish' , name , PUBLISH_RESPONSE_TIMEOUT_MS ) ;
379+ const result = await withTimeout ( client . publish ( contextGraphId , quads ) , 'publish' , name , PUBLISH_TIMEOUT_MS ) ;
403380 assert . ok ( result , 'Publish returned no result' ) ;
404381 if ( result . status !== 'confirmed' ) {
405382 // Test-side message stays SHORT; the node's own lifecycle error
@@ -432,52 +409,13 @@ export function defineChainPublishSuite(config) {
432409 console . log ( ` ↳ first ${ sample . length } child KA UAL(s):` ) ;
433410 sample . forEach ( ( u ) => console . log ( ` ${ u } ` ) ) ;
434411 }
435- publishOk = true ;
436412 publishSuccess ++ ;
437413 publishDurations . push ( Date . now ( ) - pubStart ) ;
438414 } catch ( error ) {
439- // Response lost (timeout / connection drop)? The node may still
440- // have confirmed the publish on-chain — 10.0.5 hangs the sync
441- // response in the post-confirm swm-share fanout. Read THIS KA's
442- // root entity back from the node's VM to learn the real outcome:
443- // present == success (flagged response-lost), absent == failure.
444- const responseLost = / T i m e o u t a f t e r / . test ( error ?. message ?? '' ) || error ?. network === true ;
445- let rescued = false ;
446- if ( responseLost ) {
447- console . log ( `⏳ Publish response lost on ${ name } — verifying the real outcome via VM read-back (up to ${ Math . round ( PUBLISH_VERIFY_TIMEOUT_MS / 1000 ) } s)…` ) ;
448- const deadline = Date . now ( ) + PUBLISH_VERIFY_TIMEOUT_MS ;
449- while ( Date . now ( ) < deadline ) {
450- try {
451- const probe = await client . query ( `SELECT ?p ?o WHERE { <${ rootEntity } > ?p ?o } LIMIT 1` , contextGraphId , 'verifiable-memory' ) ;
452- if ( queryHasData ( probe ) ) { rescued = true ; break ; }
453- } catch { /* transient read error — keep polling */ }
454- await sleep ( PUBLISH_VERIFY_POLL_MS ) ;
455- }
456- }
457- if ( rescued ) {
458- publishOk = true ;
459- publishSuccess ++ ;
460- publishRescued ++ ;
461- // The lifecycle record can flip to `published` a beat after the
462- // VM data lands — retry the descriptor read briefly.
463- for ( let attempt = 0 ; attempt < 3 && ! ual ; attempt ++ ) {
464- try {
465- const hist = await client . asset ( kaName , contextGraphId ) ;
466- ual = hist ?. publishedUal || hist ?. ual || null ;
467- const rescuedAddr = hist ?. publisherAddress || hist ?. authorAddress ;
468- if ( rescuedAddr ) publisherAddresses . add ( rescuedAddr ) ;
469- } catch { /* descriptor read failed — remote read falls back to FALLBACK_UAL */ }
470- if ( ! ual ) await sleep ( 5000 ) ;
471- }
472- // NOT pushed into publishDurations: the latency is dominated by
473- // our own response-wait, so the avg stays clean-response-only.
474- console . log ( `✅ Published KA #${ i + 1 } — CONFIRMED via VM read-back after response loss (node response-hang bug) in ${ formatDuration ( Date . now ( ) - pubStart ) } ${ ual ? ` | UAL: ${ ual } ` : ' | UAL unrecovered' } ` ) ;
475- } else {
476- await logError ( error , name , step , errorStats , i + 1 , { baseUrl : client . baseUrl } ) ;
477- console . log ( `❌ Publish failed | No UAL` ) ;
478- failedAssets . push ( `KA #${ i + 1 } (Publish failed)` ) ;
479- publishFail ++ ;
480- }
415+ await logError ( error , name , step , errorStats , i + 1 , { baseUrl : client . baseUrl } ) ;
416+ console . log ( `❌ Publish failed | No UAL` ) ;
417+ failedAssets . push ( `KA #${ i + 1 } (Publish failed)` ) ;
418+ publishFail ++ ;
481419 }
482420
483421 // Reads are RUN-SPECIFIC when the publish succeeded — they target THIS
@@ -493,7 +431,7 @@ export function defineChainPublishSuite(config) {
493431 const queryStart = Date . now ( ) ;
494432 try {
495433 let result ;
496- if ( publishOk ) {
434+ if ( ual ) {
497435 const sparql = `SELECT ?s ?name WHERE { ?s <http://schema.org/isPartOf> <${ rootEntity } > ; <http://schema.org/name> ?name } LIMIT 5` ;
498436 result = await readWithRetry ( 'query' , name , ( ) => client . query ( sparql , contextGraphId , 'verifiable-memory' ) ) ;
499437 } else if ( FALLBACK_UAL ) {
@@ -516,7 +454,7 @@ export function defineChainPublishSuite(config) {
516454 const vmGetStart = Date . now ( ) ;
517455 try {
518456 let result ;
519- if ( publishOk ) {
457+ if ( ual ) {
520458 const entitySparql = `SELECT ?p ?o WHERE { <${ rootEntity } > ?p ?o } LIMIT 5` ;
521459 result = await readWithRetry ( 'VM GET' , name , ( ) => client . query ( entitySparql , contextGraphId , 'verifiable-memory' ) ) ;
522460 } else if ( FALLBACK_UAL ) {
@@ -544,18 +482,12 @@ export function defineChainPublishSuite(config) {
544482 const remoteNode = nodes [ otherIndexes [ Math . floor ( Math . random ( ) * otherIndexes . length ) ] ] ;
545483 const remoteStart = Date . now ( ) ;
546484 try {
547- if ( ! readUal && ! publishOk ) throw new Error ( 'Query Remote (sync): publish failed and no DKG_FALLBACK_UAL configured' ) ;
485+ if ( ! readUal ) throw new Error ( 'Query Remote (sync): publish failed and no DKG_FALLBACK_UAL configured' ) ;
548486 const remotePeerId = await getPeerId ( remoteNode ) ;
549487 if ( ! remotePeerId ) throw new Error ( `Could not resolve peerId for ${ remoteNode . name } (${ remoteNode . hostname } )` ) ;
550- // Run-specific even when the UAL could not be recovered (response
551- // lost, #1572): ask the peer for THIS KA's unique root entity
552- // directly — a pass proves this run's KA propagated cross-node.
553- const lookup = publishOk && ! ual
554- ? { lookupType : 'ENTITY_TRIPLES' , entityUri : rootEntity }
555- : { lookupType : 'ENTITY_BY_UAL' , ual : readUal } ;
556- const result = await readWithRetry ( 'Query Remote (sync)' , remoteNode . name , ( ) => client . queryRemote ( remotePeerId , contextGraphId , lookup ) ) ;
557- assert . ok ( queryHasData ( result ) , `Query Remote (sync) returned no triples for ${ lookup . ual || lookup . entityUri } from ${ remoteNode . name } ` ) ;
558- console . log ( `✅ Query Remote (sync) succeeded — ${ remoteNode . name } has the KA (synced${ publishOk && ! ual ? ', by entity URI' : '' } )` ) ;
488+ const result = await readWithRetry ( 'Query Remote (sync)' , remoteNode . name , ( ) => client . queryRemote ( remotePeerId , contextGraphId , { lookupType : 'ENTITY_BY_UAL' , ual : readUal } ) ) ;
489+ assert . ok ( queryHasData ( result ) , `Query Remote (sync) returned no triples for ${ readUal } from ${ remoteNode . name } ` ) ;
490+ console . log ( `✅ Query Remote (sync) succeeded — ${ remoteNode . name } has the KA (synced)` ) ;
559491 queryRemoteSuccess ++ ;
560492 queryRemoteDurations . push ( Date . now ( ) - remoteStart ) ;
561493 } catch ( error ) {
@@ -584,7 +516,7 @@ export function defineChainPublishSuite(config) {
584516 }
585517
586518 globalStats [ blockchainName ] [ name ] = {
587- publishSuccess, publishFail, publishRescued ,
519+ publishSuccess, publishFail,
588520 querySuccess, queryFail,
589521 vmGetSuccess, vmGetFail,
590522 queryRemoteSuccess, queryRemoteFail,
@@ -597,9 +529,6 @@ export function defineChainPublishSuite(config) {
597529 node_wallet : nodeWalletAddrs . join ( ', ' ) ,
598530 node_publisher_wallets : [ ...publisherAddresses ] . join ( ', ' ) ,
599531 publish_success_rate : safeRate ( publishSuccess , publishFail ) ,
600- // publishes whose HTTP response was lost to the 10.0.5 response-hang
601- // but were CONFIRMED via VM read-back (counted in the success rate)
602- publish_response_lost : publishRescued ,
603532 query_success_rate : safeRate ( querySuccess , queryFail ) ,
604533 publisher_get_success_rate : safeRate ( vmGetSuccess , vmGetFail ) , // VM GET
605534 non_publisher_get_success_rate : safeRate ( queryRemoteSuccess , queryRemoteFail ) , // Query Remote (sync)
@@ -622,36 +551,16 @@ export function defineChainPublishSuite(config) {
622551 const errorsFileName = `errors_${ name . replace ( / \s + / g, '_' ) } .json` ;
623552 fs . writeFileSync ( errorsFileName , JSON . stringify ( errorData , null , 2 ) ) ;
624553 console . log ( `✅ Saved errors to ${ errorsFileName } ` ) ;
625-
626- // Response-loss flag: consumed by the Jenkins pipeline to mark the
627- // build UNSTABLE while node bug OriginTrail/dkg#1572 is active.
628- // Appended (not truncated) — parallel node stages share the workspace.
629- if ( publishRescued > 0 ) {
630- fs . appendFileSync ( 'response_lost.flag' , `${ name } : ${ publishRescued } \n` ) ;
631- }
632554 }
633555 } ) ;
634556
635- // Runs AFTER the main test (summaries already written, Grafana import
636- // unaffected — the pipeline wraps stages in catchError(buildResult:
637- // 'SUCCESS')). Fails the STAGE whenever any publish response was lost,
638- // so node bug OriginTrail/dkg#1572 is impossible to overlook while the
639- // publish stats above still tell the true success story.
640- it ( 'publish responses arrived in time (node bug OriginTrail/dkg#1572 inactive)' , function ( ) {
641- const lost = Object . values ( globalStats [ blockchainName ] || { } )
642- . reduce ( ( s , st ) => s + ( st . publishRescued || 0 ) , 0 ) ;
643- assert . equal ( lost , 0 ,
644- `${ lost } publish response(s) never arrived and were confirmed via VM read-back instead — ` +
645- `sync publish response hang (OriginTrail/dkg#1572) is active on this chain's cores` ) ;
646- } ) ;
647-
648557 after ( ( ) => {
649558 console . log ( `\n\nGlobal Publish Summary:` ) ;
650559 Object . entries ( globalStats ) . forEach ( ( [ blockchain , nodeStats ] ) => {
651560 console . log ( `\n🔗 Blockchain: ${ blockchain } ` ) ;
652561 Object . entries ( nodeStats ) . forEach ( ( [ nodeName , stats ] ) => {
653562 console . log ( ` • ${ nodeName } :` ) ;
654- console . log ( ` Publish: ✅ ${ stats . publishSuccess } / ❌ ${ stats . publishFail } -> ${ safeRate ( stats . publishSuccess , stats . publishFail ) } %${ stats . publishRescued ? ` ( ${ stats . publishRescued } confirmed via VM read-back after response loss — node response-hang bug)` : '' } ` ) ;
563+ console . log ( ` Publish: ✅ ${ stats . publishSuccess } / ❌ ${ stats . publishFail } -> ${ safeRate ( stats . publishSuccess , stats . publishFail ) } %` ) ;
655564 console . log ( ` Query: ✅ ${ stats . querySuccess } / ❌ ${ stats . queryFail } -> ${ safeRate ( stats . querySuccess , stats . queryFail ) } %` ) ;
656565 console . log ( ` VM GET: ✅ ${ stats . vmGetSuccess } / ❌ ${ stats . vmGetFail } -> ${ safeRate ( stats . vmGetSuccess , stats . vmGetFail ) } %` ) ;
657566 console . log ( ` Query Remote (sync): ✅ ${ stats . queryRemoteSuccess } / ❌ ${ stats . queryRemoteFail } -> ${ safeRate ( stats . queryRemoteSuccess , stats . queryRemoteFail ) } %` ) ;
0 commit comments