@@ -510,6 +510,73 @@ describe("stackflow agent", () => {
510510 store . close ( ) ;
511511 } ) ;
512512
513+ it ( "skips overlapping readonly watcher runs" , async ( ) => {
514+ const dbFile = tempDbFile ( "agent-readonly-overlap" ) ;
515+ const store = new AgentStateStore ( { dbFile } ) ;
516+
517+ const contractId = "ST1TESTABC.contract" ;
518+ const pipeKey = {
519+ "principal-1" : "ST1LOCAL" ,
520+ "principal-2" : "ST1OTHER" ,
521+ token : null ,
522+ } ;
523+ const pipeId = buildPipeId ( { contractId, pipeKey } ) ;
524+
525+ store . upsertTrackedPipe ( {
526+ pipeId,
527+ contractId,
528+ pipeKey,
529+ localPrincipal : "ST1LOCAL" ,
530+ counterpartyPrincipal : "ST1OTHER" ,
531+ token : null ,
532+ } ) ;
533+
534+ let resolveFetch : ( ( value : unknown ) => void ) | null = null ;
535+
536+ const agent = new StackflowAgentService ( {
537+ stateStore : store ,
538+ signer : {
539+ async submitDispute ( ) {
540+ return { txid : "0xnoop" } ;
541+ } ,
542+ async sip018Sign ( ) {
543+ return "0x" + "44" . repeat ( 65 ) ;
544+ } ,
545+ async callContract ( ) {
546+ return { ok : true } ;
547+ } ,
548+ } ,
549+ network : "devnet" ,
550+ } ) ;
551+
552+ const watcher = new HourlyClosureWatcher ( {
553+ agentService : agent ,
554+ getPipeState : ( ) =>
555+ new Promise ( ( resolve ) => {
556+ resolveFetch = resolve ;
557+ } ) ,
558+ } ) ;
559+
560+ const firstRunPromise = watcher . runOnce ( ) ;
561+ const overlapping = await watcher . runOnce ( ) ;
562+
563+ expect ( overlapping . ok ) . toBe ( true ) ;
564+ expect ( overlapping . skipped ) . toBe ( true ) ;
565+ expect ( overlapping . reason ) . toBe ( "already-running" ) ;
566+
567+ resolveFetch ?.( {
568+ "balance-1" : "20" ,
569+ "balance-2" : "80" ,
570+ "expires-at" : "200" ,
571+ nonce : "5" ,
572+ closer : "ST1OTHER" ,
573+ } ) ;
574+
575+ await firstRunPromise ;
576+ watcher . stop ( ) ;
577+ store . close ( ) ;
578+ } ) ;
579+
513580 it ( "validates and signs incoming transfer requests" , async ( ) => {
514581 const dbFile = tempDbFile ( "agent-sign" ) ;
515582 const store = new AgentStateStore ( { dbFile } ) ;
@@ -568,6 +635,62 @@ describe("stackflow agent", () => {
568635 store . close ( ) ;
569636 } ) ;
570637
638+ it ( "rejects incoming transfer requests with token mismatch" , ( ) => {
639+ const dbFile = tempDbFile ( "agent-sign-token-mismatch" ) ;
640+ const store = new AgentStateStore ( { dbFile } ) ;
641+ const contractId = "ST1TESTABC.contract" ;
642+ const pipeKey = {
643+ "principal-1" : "ST1LOCAL" ,
644+ "principal-2" : "ST1OTHER" ,
645+ token : "ST1TOKEN.token-1" ,
646+ } ;
647+ const pipeId = buildPipeId ( { contractId, pipeKey } ) ;
648+ store . upsertTrackedPipe ( {
649+ pipeId,
650+ contractId,
651+ pipeKey,
652+ localPrincipal : "ST1LOCAL" ,
653+ counterpartyPrincipal : "ST1OTHER" ,
654+ token : "ST1TOKEN.token-1" ,
655+ } ) ;
656+
657+ const agent = new StackflowAgentService ( {
658+ stateStore : store ,
659+ signer : {
660+ async sip018Sign ( ) {
661+ return "0x" + "44" . repeat ( 65 ) ;
662+ } ,
663+ async submitDispute ( ) {
664+ return { txid : "0x1" } ;
665+ } ,
666+ async callContract ( ) {
667+ return { ok : true } ;
668+ } ,
669+ } ,
670+ network : "devnet" ,
671+ } ) ;
672+
673+ const validation = agent . validateIncomingTransfer ( {
674+ pipeId,
675+ payload : {
676+ contractId,
677+ forPrincipal : "ST1LOCAL" ,
678+ withPrincipal : "ST1OTHER" ,
679+ token : "ST1TOKEN.token-2" ,
680+ myBalance : "90" ,
681+ theirBalance : "10" ,
682+ nonce : "1" ,
683+ action : "1" ,
684+ actor : "ST1OTHER" ,
685+ theirSignature : "0x" + "22" . repeat ( 65 ) ,
686+ } ,
687+ } ) ;
688+
689+ expect ( validation . valid ) . toBe ( false ) ;
690+ expect ( validation . reason ) . toBe ( "token-mismatch" ) ;
691+ store . close ( ) ;
692+ } ) ;
693+
571694 it ( "opens a pipe via signer adapter with expected contract call" , async ( ) => {
572695 const dbFile = tempDbFile ( "agent-open" ) ;
573696 const store = new AgentStateStore ( { dbFile } ) ;
0 commit comments