@@ -28,6 +28,7 @@ const mocks = vi.hoisted(() => ({
2828 noteLegacyCodexProviderOverride : vi . fn ( ) ,
2929 noteMemorySearchHealth : vi . fn ( ) . mockResolvedValue ( undefined ) ,
3030 buildGatewayConnectionDetails : vi . fn ( ( ) => ( { message : "gateway details" } ) ) ,
31+ callGateway : vi . fn ( ) ,
3132 resolveSecretInputRef : vi . fn ( ( params : { value ?: unknown } ) => ( {
3233 ref :
3334 params . value === "exec-token"
@@ -100,6 +101,8 @@ const mocks = vi.hoisted(() => ({
100101 collectDiskSpaceHealthFindings : vi . fn ( ( ) : readonly HealthFinding [ ] => [ ] ) ,
101102 collectHeartbeatTemplateHealthFindings : vi . fn ( async ( ) => [ ] as unknown [ ] ) ,
102103 maybeRepairHeartbeatTemplate : vi . fn ( ) . mockResolvedValue ( undefined ) ,
104+ collectWhatsappResponsivenessHealthFindings : vi . fn ( ( ) : readonly HealthFinding [ ] => [ ] ) ,
105+ noteWhatsappResponsivenessHealth : vi . fn ( ) . mockResolvedValue ( undefined ) ,
103106 collectDevicePairingHealthFindings : vi . fn ( async ( ) => [ ] ) ,
104107 scanConfiguredChannelPluginBlockers : vi . fn (
105108 ( ) : Array < { channelId : string ; pluginId : string ; reason : string } > => [ ] ,
@@ -213,6 +216,7 @@ vi.mock("../commands/doctor-memory-search.js", () => ({
213216
214217vi . mock ( "../gateway/call.js" , ( ) => ( {
215218 buildGatewayConnectionDetails : mocks . buildGatewayConnectionDetails ,
219+ callGateway : mocks . callGateway ,
216220} ) ) ;
217221
218222vi . mock ( "../commands/doctor-platform-notes.js" , ( ) => ( {
@@ -338,6 +342,11 @@ vi.mock("../commands/doctor-heartbeat-template-repair.js", () => ({
338342 maybeRepairHeartbeatTemplate : mocks . maybeRepairHeartbeatTemplate ,
339343} ) ) ;
340344
345+ vi . mock ( "../commands/doctor-whatsapp-responsiveness.js" , ( ) => ( {
346+ collectWhatsappResponsivenessHealthFindings : mocks . collectWhatsappResponsivenessHealthFindings ,
347+ noteWhatsappResponsivenessHealth : mocks . noteWhatsappResponsivenessHealth ,
348+ } ) ) ;
349+
341350vi . mock ( "../commands/doctor-device-pairing.js" , ( ) => ( {
342351 collectDevicePairingHealthFindings : mocks . collectDevicePairingHealthFindings ,
343352 noteDevicePairingHealth : vi . fn ( ) . mockResolvedValue ( undefined ) ,
@@ -428,6 +437,8 @@ describe("doctor health contributions", () => {
428437 mocks . noteMemorySearchHealth . mockResolvedValue ( undefined ) ;
429438 mocks . buildGatewayConnectionDetails . mockClear ( ) ;
430439 mocks . buildGatewayConnectionDetails . mockReturnValue ( { message : "gateway details" } ) ;
440+ mocks . callGateway . mockReset ( ) ;
441+ mocks . callGateway . mockResolvedValue ( { } ) ;
431442 mocks . resolveSecretInputRef . mockClear ( ) ;
432443 mocks . resolveGatewayAuth . mockClear ( ) ;
433444 mocks . resolveGatewayAuth . mockReturnValue ( { mode : "token" , token : undefined } ) ;
@@ -542,6 +553,10 @@ describe("doctor health contributions", () => {
542553 mocks . collectHeartbeatTemplateHealthFindings . mockResolvedValue ( [ ] ) ;
543554 mocks . maybeRepairHeartbeatTemplate . mockReset ( ) ;
544555 mocks . maybeRepairHeartbeatTemplate . mockResolvedValue ( undefined ) ;
556+ mocks . collectWhatsappResponsivenessHealthFindings . mockReset ( ) ;
557+ mocks . collectWhatsappResponsivenessHealthFindings . mockReturnValue ( [ ] ) ;
558+ mocks . noteWhatsappResponsivenessHealth . mockReset ( ) ;
559+ mocks . noteWhatsappResponsivenessHealth . mockResolvedValue ( undefined ) ;
545560 mocks . collectDevicePairingHealthFindings . mockReset ( ) ;
546561 mocks . collectDevicePairingHealthFindings . mockResolvedValue ( [ ] ) ;
547562 mocks . scanConfiguredChannelPluginBlockers . mockReset ( ) ;
@@ -1362,6 +1377,7 @@ describe("doctor health contributions", () => {
13621377 expect ( contributionIds ) . toContain ( "core/doctor/stale-plugin-runtime-symlinks" ) ;
13631378 expect ( contributionIds ) . toContain ( "core/doctor/disk-space" ) ;
13641379 expect ( contributionIds ) . toContain ( "core/doctor/heartbeat-template" ) ;
1380+ expect ( contributionIds ) . toContain ( "core/doctor/whatsapp-responsiveness" ) ;
13651381 expect ( contributionIds ) . toContain ( "core/doctor/device-pairing" ) ;
13661382 expect ( contributionIds ) . toContain ( "core/doctor/channel-plugin-blockers" ) ;
13671383 expect ( contributionIds ) . toContain ( "core/doctor/tool-result-cap" ) ;
@@ -1694,6 +1710,125 @@ describe("doctor health contributions", () => {
16941710 expect ( mocks . collectDiskSpaceHealthFindings ) . toHaveBeenCalledWith ( ctx . cfg ) ;
16951711 } ) ;
16961712
1713+ it ( "keeps WhatsApp responsiveness opt-in for default lint selection" , async ( ) => {
1714+ const contributionChecks = await resolveDoctorContributionHealthChecks ( ) ;
1715+ const whatsappCheck = contributionChecks . find (
1716+ ( check ) => check . id === "core/doctor/whatsapp-responsiveness" ,
1717+ ) ;
1718+ expect ( whatsappCheck ) . toMatchObject ( { defaultEnabled : false } ) ;
1719+ expect ( whatsappCheck ) . toBeDefined ( ) ;
1720+
1721+ const ctx = {
1722+ cfg : { channels : { whatsapp : { enabled : true } } } ,
1723+ mode : "lint" ,
1724+ allowExecSecretRefs : true ,
1725+ runtime : { log : vi . fn ( ) , error : vi . fn ( ) , exit : vi . fn ( ) } ,
1726+ } as const ;
1727+ const checks = [ whatsappCheck ! ] ;
1728+
1729+ await expect ( runDoctorLintChecks ( ctx , { checks } ) ) . resolves . toMatchObject ( {
1730+ checksRun : 0 ,
1731+ checksSkipped : 1 ,
1732+ } ) ;
1733+ expect ( mocks . checkGatewayHealth ) . not . toHaveBeenCalled ( ) ;
1734+ expect ( mocks . callGateway ) . not . toHaveBeenCalled ( ) ;
1735+ expect ( mocks . collectWhatsappResponsivenessHealthFindings ) . not . toHaveBeenCalled ( ) ;
1736+
1737+ const status = {
1738+ eventLoop : {
1739+ degraded : true ,
1740+ reasons : [ "event_loop_delay" ] ,
1741+ intervalMs : 30_000 ,
1742+ delayP99Ms : 42 ,
1743+ delayMaxMs : 12_000 ,
1744+ utilization : 0.3 ,
1745+ cpuCoreRatio : 0.4 ,
1746+ } ,
1747+ } ;
1748+ mocks . callGateway . mockResolvedValueOnce ( status ) ;
1749+ mocks . collectWhatsappResponsivenessHealthFindings . mockReturnValueOnce ( [
1750+ {
1751+ checkId : "core/doctor/whatsapp-responsiveness" ,
1752+ severity : "warning" ,
1753+ message : "Gateway event loop is degraded while local TUI clients are running." ,
1754+ path : "channels.whatsapp" ,
1755+ requirement : "local-tui-event-loop-pressure" ,
1756+ } ,
1757+ ] ) ;
1758+
1759+ await expect (
1760+ runDoctorLintChecks ( ctx , { checks, onlyIds : [ "core/doctor/whatsapp-responsiveness" ] } ) ,
1761+ ) . resolves . toMatchObject ( {
1762+ checksRun : 1 ,
1763+ checksSkipped : 0 ,
1764+ findings : [ expect . objectContaining ( { checkId : "core/doctor/whatsapp-responsiveness" } ) ] ,
1765+ } ) ;
1766+ expect ( mocks . checkGatewayHealth ) . not . toHaveBeenCalled ( ) ;
1767+ expect ( mocks . callGateway ) . toHaveBeenCalledWith ( {
1768+ method : "status" ,
1769+ params : { includeChannelSummary : false } ,
1770+ timeoutMs : 3000 ,
1771+ config : ctx . cfg ,
1772+ deviceIdentity : null ,
1773+ } ) ;
1774+ expect ( mocks . collectWhatsappResponsivenessHealthFindings ) . toHaveBeenCalledWith ( {
1775+ cfg : ctx . cfg ,
1776+ status,
1777+ } ) ;
1778+
1779+ mocks . callGateway . mockRejectedValueOnce ( new Error ( "gateway unavailable" ) ) ;
1780+ mocks . collectWhatsappResponsivenessHealthFindings . mockReturnValueOnce ( [ ] ) ;
1781+ const error = vi . fn ( ) ;
1782+ await expect (
1783+ runDoctorLintChecks (
1784+ {
1785+ ...ctx ,
1786+ runtime : { log : vi . fn ( ) , error, exit : vi . fn ( ) } ,
1787+ } ,
1788+ { checks, onlyIds : [ "core/doctor/whatsapp-responsiveness" ] } ,
1789+ ) ,
1790+ ) . resolves . toMatchObject ( {
1791+ checksRun : 1 ,
1792+ checksSkipped : 0 ,
1793+ findings : [ ] ,
1794+ } ) ;
1795+ expect ( error ) . not . toHaveBeenCalled ( ) ;
1796+ expect ( mocks . collectWhatsappResponsivenessHealthFindings ) . toHaveBeenLastCalledWith ( {
1797+ cfg : ctx . cfg ,
1798+ status : undefined ,
1799+ } ) ;
1800+ } ) ;
1801+
1802+ it ( "skips WhatsApp responsiveness Gateway status probes for exec SecretRefs without allow-exec" , async ( ) => {
1803+ const contributionChecks = await resolveDoctorContributionHealthChecks ( ) ;
1804+ const whatsappCheck = contributionChecks . find (
1805+ ( check ) => check . id === "core/doctor/whatsapp-responsiveness" ,
1806+ ) ;
1807+ expect ( whatsappCheck ) . toBeDefined ( ) ;
1808+ mocks . gatewaySecretInputPathCanWin . mockReturnValue ( true ) ;
1809+ mocks . readGatewaySecretInputValue . mockReturnValue ( "exec-token" ) ;
1810+
1811+ const ctx = {
1812+ cfg : { channels : { whatsapp : { enabled : true } } } ,
1813+ mode : "lint" ,
1814+ runtime : { log : vi . fn ( ) , error : vi . fn ( ) , exit : vi . fn ( ) } ,
1815+ } as const ;
1816+ const checks = [ whatsappCheck ! ] ;
1817+
1818+ await expect (
1819+ runDoctorLintChecks ( ctx , { checks, onlyIds : [ "core/doctor/whatsapp-responsiveness" ] } ) ,
1820+ ) . resolves . toMatchObject ( {
1821+ checksRun : 1 ,
1822+ checksSkipped : 0 ,
1823+ findings : [ ] ,
1824+ } ) ;
1825+ expect ( mocks . callGateway ) . not . toHaveBeenCalled ( ) ;
1826+ expect ( mocks . collectWhatsappResponsivenessHealthFindings ) . toHaveBeenCalledWith ( {
1827+ cfg : ctx . cfg ,
1828+ status : undefined ,
1829+ } ) ;
1830+ } ) ;
1831+
16971832 it ( "keeps device pairing opt-in for default lint selection" , async ( ) => {
16981833 const contributionChecks = await resolveDoctorContributionHealthChecks ( ) ;
16991834 const devicePairingCheck = contributionChecks . find (
0 commit comments