@@ -6,7 +6,10 @@ import * as NodeChildProcess from "node:child_process";
66import * as NodeServices from "@effect/platform-node/NodeServices" ;
77import { it } from "@effect/vitest" ;
88import * as Duration from "effect/Duration" ;
9+ import * as Context from "effect/Context" ;
910import * as Effect from "effect/Effect" ;
11+ import * as Deferred from "effect/Deferred" ;
12+ import * as Exit from "effect/Exit" ;
1013import * as FileSystem from "effect/FileSystem" ;
1114import * as Layer from "effect/Layer" ;
1215import * as Logger from "effect/Logger" ;
@@ -16,11 +19,13 @@ import * as References from "effect/References";
1619import * as Result from "effect/Result" ;
1720import * as Schema from "effect/Schema" ;
1821import * as Scope from "effect/Scope" ;
22+ import * as Stream from "effect/Stream" ;
1923import { TestClock } from "effect/testing" ;
2024import { ChildProcessSpawner } from "effect/process" ;
2125import { expect } from "vite-plus/test" ;
2226import type {
2327 GitActionProgressEvent ,
28+ GitManagerServiceError ,
2429 GitPreparePullRequestThreadInput ,
2530 ModelSelection ,
2631} from "@t3tools/contracts" ;
@@ -43,6 +48,11 @@ import * as GitLabCli from "../sourceControl/GitLabCli.ts";
4348import * as TextGeneration from "../textGeneration/TextGeneration.ts" ;
4449import * as GitVcsDriver from "../vcs/GitVcsDriver.ts" ;
4550import * as VcsProcess from "../vcs/VcsProcess.ts" ;
51+ import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts" ;
52+ import * as VcsProjectConfig from "../vcs/VcsProjectConfig.ts" ;
53+ import * as VcsStatusBroadcaster from "../vcs/VcsStatusBroadcaster.ts" ;
54+ import * as BackgroundPolicy from "../background/BackgroundPolicy.ts" ;
55+ import * as GitWorkflowService from "./GitWorkflowService.ts" ;
4656import * as GitHubSourceControlProvider from "../sourceControl/GitHubSourceControlProvider.ts" ;
4757import * as GitLabSourceControlProvider from "../sourceControl/GitLabSourceControlProvider.ts" ;
4858import {
@@ -778,6 +788,120 @@ const layerGitManagerTest = GitVcsDriver.layer.pipe(
778788) ;
779789
780790it . layer ( layerGitManagerTest ) ( "GitManager" , ( it ) => {
791+ it . effect ( "passive worktree status streams do not start remote refreshes" , ( ) =>
792+ Effect . gen ( function * ( ) {
793+ const repoDir = yield * makeTempDir ( "t3code-passive-vcs-" ) ;
794+ yield * initRepo ( repoDir ) ;
795+ const remoteDir = yield * createBareRemote ( ) ;
796+ yield * runGit ( repoDir , [ "remote" , "add" , "origin" , remoteDir ] ) ;
797+ yield * runGit ( repoDir , [ "push" , "-u" , "origin" , "main" ] ) ;
798+ const worktreeDir = NodePath . join ( repoDir , "worktree" ) ;
799+ yield * runGit ( repoDir , [ "worktree" , "add" , "-b" , "feature/passive" , worktreeDir ] ) ;
800+ yield * runGit ( worktreeDir , [ "push" , "-u" , "origin" , "feature/passive" ] ) ;
801+
802+ const { manager } = yield * makeManager ( ) ;
803+ let remoteReads = 0 ;
804+ const workflowContext = yield * Layer . build (
805+ GitWorkflowService . layer . pipe (
806+ Layer . provide (
807+ Layer . succeed ( GitManager . GitManager , {
808+ ...manager ,
809+ remoteStatus : ( input , options ) =>
810+ manager . remoteStatus ( input , options ) . pipe (
811+ Effect . tap ( ( ) =>
812+ Effect . sync ( ( ) => {
813+ remoteReads += 1 ;
814+ } ) ,
815+ ) ,
816+ ) ,
817+ } ) ,
818+ ) ,
819+ Layer . provide (
820+ VcsDriverRegistry . layer . pipe (
821+ Layer . provide ( VcsProjectConfig . layer ) ,
822+ Layer . provide ( VcsProcess . layer ) ,
823+ ) ,
824+ ) ,
825+ ) ,
826+ ) ;
827+ const broadcasterContext = yield * Layer . build (
828+ VcsStatusBroadcaster . layer . pipe (
829+ Layer . provide (
830+ Layer . succeed (
831+ GitWorkflowService . GitWorkflowService ,
832+ Context . get ( workflowContext , GitWorkflowService . GitWorkflowService ) ,
833+ ) ,
834+ ) ,
835+ Layer . provide (
836+ Layer . mock ( BackgroundPolicy . BackgroundPolicy ) ( {
837+ hasDemand : ( ) => Effect . succeed ( true ) ,
838+ shouldRunScopeWork : ( ) => Effect . succeed ( true ) ,
839+ } ) ,
840+ ) ,
841+ ) ,
842+ ) ;
843+ const broadcaster = Context . get (
844+ broadcasterContext ,
845+ VcsStatusBroadcaster . VcsStatusBroadcaster ,
846+ ) ;
847+ const passiveScope = yield * Scope . make ( ) ;
848+ const snapshots = yield * Deferred . make < void , GitManagerServiceError > ( ) ;
849+ const localUpdated = yield * Deferred . make < void > ( ) ;
850+ const remoteUpdated = yield * Deferred . make < void > ( ) ;
851+ let snapshotCount = 0 ;
852+ for ( const cwd of [ repoDir , worktreeDir ] ) {
853+ yield * Stream . runForEach (
854+ broadcaster . streamStatus (
855+ { cwd, includeRemote : false } ,
856+ { automaticRemoteRefreshInterval : Effect . succeed ( Duration . seconds ( 1 ) ) } ,
857+ ) ,
858+ ( event ) => {
859+ if (
860+ cwd === worktreeDir &&
861+ event . _tag === "localUpdated" &&
862+ event . local . hasWorkingTreeChanges
863+ ) {
864+ return Deferred . succeed ( localUpdated , undefined ) ;
865+ }
866+ if ( cwd === worktreeDir && event . _tag === "remoteUpdated" ) {
867+ expect ( event . remote ?. hasUpstream ) . toBe ( true ) ;
868+ return Deferred . succeed ( remoteUpdated , undefined ) ;
869+ }
870+ if ( event . _tag !== "snapshot" ) return Effect . void ;
871+ expect ( event . local . isRepo ) . toBe ( true ) ;
872+ expect ( event . local . refName ) . toBe ( cwd === repoDir ? "main" : "feature/passive" ) ;
873+ snapshotCount += 1 ;
874+ return snapshotCount === 2 ? Deferred . succeed ( snapshots , undefined ) : Effect . void ;
875+ } ,
876+ ) . pipe (
877+ Effect . catchCause ( ( cause ) => Deferred . failCause ( snapshots , cause ) ) ,
878+ Effect . forkIn ( passiveScope ) ,
879+ ) ;
880+ }
881+ yield * Deferred . await ( snapshots ) ;
882+ expect ( remoteReads ) . toBe ( 0 ) ;
883+ yield * TestClock . adjust ( "1 minute" ) ;
884+ expect ( remoteReads ) . toBe ( 0 ) ;
885+
886+ const fs = yield * FileSystem . FileSystem ;
887+ yield * fs . writeFileString ( NodePath . join ( worktreeDir , "README.md" ) , "changed\n" ) ;
888+ yield * broadcaster . refreshLocalStatus ( worktreeDir ) ;
889+ yield * Deferred . await ( localUpdated ) ;
890+ expect ( remoteReads ) . toBe ( 0 ) ;
891+
892+ const activeScope = yield * Scope . make ( ) ;
893+ yield * Stream . runDrain ( broadcaster . streamStatus ( { cwd : worktreeDir } ) ) . pipe (
894+ Effect . forkIn ( activeScope ) ,
895+ ) ;
896+ yield * Deferred . await ( remoteUpdated ) ;
897+ expect ( remoteReads ) . toBe ( 1 ) ;
898+ yield * Scope . close ( activeScope , Exit . void ) ;
899+ yield * TestClock . adjust ( "1 minute" ) ;
900+ expect ( remoteReads ) . toBe ( 1 ) ;
901+ yield * Scope . close ( passiveScope , Exit . void ) ;
902+ } ) ,
903+ ) ;
904+
781905 it . effect ( "status includes draft PR metadata when branch already has a draft PR" , ( ) =>
782906 Effect . gen ( function * ( ) {
783907 const repoDir = yield * makeTempDir ( "t3code-git-manager-" ) ;
0 commit comments