@@ -4,6 +4,8 @@ import type { ReactNode } from "react";
44import { afterEach , beforeEach , describe , expect , mock , spyOn , test } from "bun:test" ;
55import { cleanup , fireEvent , render , waitFor } from "@testing-library/react" ;
66import { installDom } from "../../../../tests/ui/dom" ;
7+ import * as APIModule from "@/browser/contexts/API" ;
8+ import type { APIClient , UseAPIResult } from "@/browser/contexts/API" ;
79import * as WorkspaceHeartbeatHookModule from "@/browser/hooks/useWorkspaceHeartbeat" ;
810import type { HeartbeatFormSettings } from "@/browser/hooks/useWorkspaceHeartbeat" ;
911import {
@@ -33,6 +35,27 @@ let saveResult = true;
3335let hookError : string | null = null ;
3436let hookIsLoading = false ;
3537let hookIsSaving = false ;
38+ let useWorkspaceHeartbeatSpy : ReturnType <
39+ typeof spyOn < typeof WorkspaceHeartbeatHookModule , "useWorkspaceHeartbeat" >
40+ > ;
41+
42+ type ConnectedUseAPIResult = Extract < UseAPIResult , { status : "connected" } > ;
43+ interface WorkspaceHeartbeatTestAPI {
44+ workspace : {
45+ heartbeat : {
46+ get : ( input : { workspaceId : string } ) => Promise < HeartbeatFormSettings | null > ;
47+ set : (
48+ _input : unknown
49+ ) => Promise < { success : true ; data : void } | { success : false ; error : string } > ;
50+ } ;
51+ } ;
52+ config : {
53+ getConfig : ( ) => Promise < {
54+ heartbeatDefaultIntervalMs ?: number ;
55+ heartbeatDefaultPrompt ?: string ;
56+ } > ;
57+ } ;
58+ }
3659
3760function createHeartbeatSettings (
3861 overrides : Partial < HeartbeatFormSettings > = { }
@@ -45,6 +68,16 @@ function createHeartbeatSettings(
4568 } ;
4669}
4770
71+ function createConnectedUseAPIResult ( api : WorkspaceHeartbeatTestAPI ) : ConnectedUseAPIResult {
72+ return {
73+ api : api as APIClient ,
74+ status : "connected" ,
75+ error : null ,
76+ authenticate : ( ) => undefined ,
77+ retry : ( ) => undefined ,
78+ } ;
79+ }
80+
4881const LONG_HEARTBEAT_MESSAGE = "Review pending work and summarize next steps. " . repeat ( 30 ) . trim ( ) ;
4982
5083describe ( "WorkspaceHeartbeatModal" , ( ) => {
@@ -57,7 +90,10 @@ describe("WorkspaceHeartbeatModal", () => {
5790 hookIsLoading = false ;
5891 hookIsSaving = false ;
5992
60- spyOn ( WorkspaceHeartbeatHookModule , "useWorkspaceHeartbeat" ) . mockImplementation ( ( params ) => {
93+ useWorkspaceHeartbeatSpy = spyOn (
94+ WorkspaceHeartbeatHookModule ,
95+ "useWorkspaceHeartbeat"
96+ ) . mockImplementation ( ( params ) => {
6197 const workspaceId = params . workspaceId ;
6298 return {
6399 settings :
@@ -136,6 +172,57 @@ describe("WorkspaceHeartbeatModal", () => {
136172 expect ( onOpenChange ) . toHaveBeenCalledWith ( false ) ;
137173 } ) ;
138174
175+ test ( "loads global heartbeat defaults when the workspace has no saved heartbeat config" , async ( ) => {
176+ useWorkspaceHeartbeatSpy . mockRestore ( ) ;
177+
178+ const globalIntervalMs = 6 * 60_000 ;
179+ const globalPrompt = "test" ;
180+ const workspaceHeartbeatGetMock = mock ( ( ) => Promise . resolve ( null ) ) ;
181+ const workspaceHeartbeatSetMock = mock ( ( ) =>
182+ Promise . resolve ( { success : true as const , data : undefined } )
183+ ) ;
184+ const getConfigMock = mock ( ( ) =>
185+ Promise . resolve ( {
186+ heartbeatDefaultIntervalMs : globalIntervalMs ,
187+ heartbeatDefaultPrompt : globalPrompt ,
188+ } )
189+ ) ;
190+ const mockApi : WorkspaceHeartbeatTestAPI = {
191+ workspace : {
192+ heartbeat : {
193+ get : workspaceHeartbeatGetMock ,
194+ set : workspaceHeartbeatSetMock ,
195+ } ,
196+ } ,
197+ config : {
198+ getConfig : getConfigMock ,
199+ } ,
200+ } ;
201+ spyOn ( APIModule , "useAPI" ) . mockImplementation ( ( ) => createConnectedUseAPIResult ( mockApi ) ) ;
202+
203+ const view = render (
204+ < WorkspaceHeartbeatModal
205+ workspaceId = "ws-1"
206+ open = { true }
207+ onOpenChange = { mock ( ( _open : boolean ) => undefined ) }
208+ />
209+ ) ;
210+
211+ const intervalField = ( await waitFor ( ( ) =>
212+ view . getByLabelText ( "Heartbeat interval in minutes" )
213+ ) ) as HTMLInputElement ;
214+ expect ( intervalField . value ) . toBe ( "6" ) ;
215+ expect ( workspaceHeartbeatGetMock ) . toHaveBeenCalledWith ( { workspaceId : "ws-1" } ) ;
216+ expect ( getConfigMock ) . toHaveBeenCalled ( ) ;
217+
218+ fireEvent . click ( view . getByRole ( "switch" , { name : "Enable workspace heartbeats" } ) ) ;
219+
220+ const messageField = ( await waitFor ( ( ) =>
221+ view . getByLabelText ( "Heartbeat message" )
222+ ) ) as HTMLTextAreaElement ;
223+ expect ( messageField . value ) . toBe ( globalPrompt ) ;
224+ } ) ;
225+
139226 test ( "saves the selected heartbeat context mode and updates helper copy" , async ( ) => {
140227 settingsByWorkspaceId . set (
141228 "ws-1" ,
0 commit comments