11// npx vitest run core/task/__tests__/Task.run-shadow.spec.ts
22
3+ import * as vscode from "vscode"
34import { providerIdentifiers } from "@roo-code/types/provider-identifiers"
5+ import { TelemetryEventName } from "@roo-code/types"
46import { TelemetryService } from "@roo-code/telemetry"
57
68import { makeExtensionContext , makeUri } from "../../../test-utils/vscode"
@@ -11,10 +13,13 @@ import { initialRunState } from "../run-state/runState"
1113vi . mock ( "../../../i18n" , ( ) => ( { t : ( key : string ) => key } ) )
1214vi . mock ( "../../ignore/RooIgnoreController" )
1315
14- function makeTask ( ) : Task {
16+ function makeTask ( extensionMode = vscode . ExtensionMode . Production ) : Task {
1517 const provider = {
1618 log : vi . fn ( ) ,
17- context : makeExtensionContext ( { globalStorageUri : makeUri ( "/mock/global-storage" ) } ) ,
19+ context : makeExtensionContext ( {
20+ globalStorageUri : makeUri ( "/mock/global-storage" ) ,
21+ extensionMode,
22+ } ) ,
1823 }
1924 // Double cast: the constructor reads only these provider members.
2025 return new Task ( {
@@ -129,3 +134,71 @@ describe("Task run-state write helpers", () => {
129134 expect ( task . runRejections ) . toEqual ( [ ] )
130135 } )
131136} )
137+
138+ describe ( "Task run-state divergence report" , ( ) => {
139+ let warn : ReturnType < typeof vi . spyOn >
140+ let captureEvent : ReturnType < typeof vi . spyOn >
141+
142+ const divergenceMetrics = ( ) =>
143+ captureEvent . mock . calls . filter (
144+ ( [ name ] : unknown [ ] ) =>
145+ name === TelemetryEventName . RUN_STATE_DIVERGENCE || name === TelemetryEventName . RUN_STATE_KNOWN_RACE ,
146+ )
147+
148+ const divergenceWarnings = ( ) =>
149+ warn . mock . calls . filter ( ( [ message ] : unknown [ ] ) => message === "[RunState] divergence" )
150+
151+ beforeEach ( ( ) => {
152+ if ( ! TelemetryService . hasInstance ( ) ) {
153+ TelemetryService . createInstance ( [ ] )
154+ }
155+ warn = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
156+ captureEvent = vi . spyOn ( TelemetryService . instance , "captureEvent" ) . mockImplementation ( ( ) => { } )
157+ } )
158+
159+ afterEach ( ( ) => {
160+ vi . restoreAllMocks ( )
161+ } )
162+
163+ it ( "logs and counts a rejected event once when the check is enabled" , ( ) => {
164+ const task = makeTask ( vscode . ExtensionMode . Test )
165+
166+ task . requestAbandon ( )
167+
168+ expect ( divergenceWarnings ( ) ) . toHaveLength ( 1 )
169+ expect ( divergenceWarnings ( ) [ 0 ] [ 1 ] ) . toMatchObject ( {
170+ taskId : task . taskId ,
171+ kind : "rejection" ,
172+ event : { tag : "abandonRequested" } ,
173+ state : initialRunState ,
174+ mismatched : [ ] ,
175+ } )
176+ expect ( divergenceMetrics ( ) ) . toEqual ( [
177+ [
178+ TelemetryEventName . RUN_STATE_DIVERGENCE ,
179+ expect . objectContaining ( { taskId : task . taskId , kind : "rejection" , eventTag : "abandonRequested" } ) ,
180+ ] ,
181+ ] )
182+ } )
183+
184+ it ( "reports a repeated rejection of the same event once per task" , ( ) => {
185+ const task = makeTask ( vscode . ExtensionMode . Test )
186+
187+ task . requestAbandon ( )
188+ task . requestAbandon ( )
189+
190+ expect ( task . runRejections ) . toHaveLength ( 2 )
191+ expect ( divergenceWarnings ( ) ) . toHaveLength ( 1 )
192+ expect ( divergenceMetrics ( ) ) . toHaveLength ( 1 )
193+ } )
194+
195+ it ( "stays silent for a production build on the stable channel" , ( ) => {
196+ const task = makeTask ( vscode . ExtensionMode . Production )
197+
198+ task . requestAbandon ( )
199+
200+ expect ( task . runRejections ) . toHaveLength ( 1 )
201+ expect ( divergenceWarnings ( ) ) . toHaveLength ( 0 )
202+ expect ( divergenceMetrics ( ) ) . toHaveLength ( 0 )
203+ } )
204+ } )
0 commit comments