11import * as fs from "fs/promises" ;
22import * as path from "path" ;
3- import {
4- setupWorkspace ,
5- setupWorkspaceWithoutProvider ,
6- shouldRunIntegrationTests ,
7- validateApiKeys ,
8- } from "./setup" ;
3+ import { shouldRunIntegrationTests , validateApiKeys } from "./setup" ;
94import {
105 sendMessageWithModel ,
116 sendMessage ,
@@ -20,7 +15,12 @@ import {
2015 modelString ,
2116 configureTestRetries ,
2217} from "./helpers" ;
23- import { createSharedRepo , cleanupSharedRepo , withSharedWorkspace } from "./sendMessageTestHelpers" ;
18+ import {
19+ createSharedRepo ,
20+ cleanupSharedRepo ,
21+ withSharedWorkspace ,
22+ withSharedWorkspaceNoProvider ,
23+ } from "./sendMessageTestHelpers" ;
2424import type { StreamDeltaEvent } from "../../src/common/types/stream" ;
2525import { IPC_CHANNELS } from "../../src/common/constants/ipc-constants" ;
2626
@@ -415,10 +415,7 @@ These are general instructions that apply to all modes.
415415 test . each ( PROVIDER_CONFIGS ) (
416416 "%s should return api_key_not_found error when API key is missing" ,
417417 async ( provider , model ) => {
418- const { env, workspaceId, cleanup } = await setupWorkspaceWithoutProvider (
419- `noapi-${ provider } `
420- ) ;
421- try {
418+ await withSharedWorkspaceNoProvider ( async ( { env, workspaceId } ) => {
422419 // Try to send message without API key configured
423420 const result = await sendMessageWithModel (
424421 env . mockIpcRenderer ,
@@ -432,9 +429,7 @@ These are general instructions that apply to all modes.
432429 if ( ! result . success && result . error . type === "api_key_not_found" ) {
433430 expect ( result . error . provider ) . toBe ( provider ) ;
434431 }
435- } finally {
436- await cleanup ( ) ;
437- }
432+ } ) ;
438433 }
439434 ) ;
440435 } ) ;
@@ -487,8 +482,7 @@ These are general instructions that apply to all modes.
487482 test . each ( PROVIDER_CONFIGS ) (
488483 "%s should include full file_edit diff in UI/history but redact it from the next provider request" ,
489484 async ( provider , model ) => {
490- const { env, workspaceId, workspacePath, cleanup } = await setupWorkspace ( provider ) ;
491- try {
485+ await withSharedWorkspace ( provider , async ( { env, workspaceId, workspacePath } ) => {
492486 // 1) Create a file and ask the model to edit it to ensure a file_edit tool runs
493487 const testFilePath = path . join ( workspacePath , "redaction-edit-test.txt" ) ;
494488 await fs . writeFile ( testFilePath , "line1\nline2\nline3\n" , "utf-8" ) ;
@@ -553,131 +547,64 @@ These are general instructions that apply to all modes.
553547
554548 // Note: We don't assert on the exact provider payload (black box), but the fact that
555549 // the second request succeeds proves the redaction path produced valid provider messages
556- } finally {
557- await cleanup ( ) ;
558- }
550+ } ) ;
559551 } ,
560552 90000
561553 ) ;
562554 } ) ;
563555
564- // Test frontend metadata round-trip (no provider needed - just verifies storage)
565- test . concurrent (
566- "should preserve arbitrary frontend metadata through IPC round-trip" ,
567- async ( ) => {
568- const { env, workspaceId, cleanup } = await setupWorkspaceWithoutProvider ( ) ;
569- try {
570- // Create structured metadata
571- const testMetadata = {
572- type : "compaction-request" as const ,
573- rawCommand : "/compact -c continue working" ,
574- parsed : {
575- maxOutputTokens : 5000 ,
576- continueMessage : "continue working" ,
577- } ,
578- } ;
579-
580- // Send a message with frontend metadata
581- // Use invalid model to fail fast - we only care about metadata storage
582- const result = await env . mockIpcRenderer . invoke (
583- IPC_CHANNELS . WORKSPACE_SEND_MESSAGE ,
584- workspaceId ,
585- "Test message with metadata" ,
586- {
587- model : "openai:gpt-4" , // Valid format but provider not configured - will fail after storing message
588- muxMetadata : testMetadata ,
589- }
590- ) ;
591-
592- // Note: IPC call will fail due to missing provider config, but that's okay
593- // We only care that the user message was written to history with metadata
594- // (sendMessage writes user message before attempting to stream)
595-
596- // Use event collector to get messages sent to frontend
597- const collector = createEventCollector ( env . sentEvents , workspaceId ) ;
598-
599- // Wait for the user message to appear in the chat channel
600- await waitFor ( ( ) => {
601- const messages = collector . collect ( ) ;
602- return messages . some ( ( m ) => "role" in m && m . role === "user" ) ;
603- } , 2000 ) ;
604-
605- // Get all messages for this workspace
606- const allMessages = collector . collect ( ) ;
607-
608- // Find the user message we just sent
609- const userMessage = allMessages . find ( ( msg ) => "role" in msg && msg . role === "user" ) ;
610- expect ( userMessage ) . toBeDefined ( ) ;
611-
612- // Verify metadata was preserved exactly as sent (black-box)
613- expect ( userMessage ) . toHaveProperty ( "metadata" ) ;
614- const metadata = ( userMessage as any ) . metadata ;
615- expect ( metadata ) . toHaveProperty ( "muxMetadata" ) ;
616- expect ( metadata . muxMetadata ) . toEqual ( testMetadata ) ;
617-
618- // Verify structured fields are accessible
619- expect ( metadata . muxMetadata . type ) . toBe ( "compaction-request" ) ;
620- expect ( metadata . muxMetadata . rawCommand ) . toBe ( "/compact -c continue working" ) ;
621- expect ( metadata . muxMetadata . parsed . continueMessage ) . toBe ( "continue working" ) ;
622- expect ( metadata . muxMetadata . parsed . maxOutputTokens ) . toBe ( 5000 ) ;
623- } finally {
624- await cleanup ( ) ;
625- }
626- } ,
627- 5000
628- ) ;
629- } ) ;
556+ // Test multi-turn conversation with response ID persistence
557+ describe . each ( PROVIDER_CONFIGS ) ( "%s:%s response ID persistence" , ( provider , model ) => {
558+ test . concurrent (
559+ "should handle multi-turn conversation with response ID persistence" ,
560+ async ( ) => {
561+ await withSharedWorkspace ( provider , async ( { env, workspaceId } ) => {
562+ // First message
563+ const result1 = await sendMessageWithModel (
564+ env . mockIpcRenderer ,
565+ workspaceId ,
566+ "What is 2+2?" ,
567+ modelString ( provider , model )
568+ ) ;
569+ expect ( result1 . success ) . toBe ( true ) ;
630570
631- // Test image support across providers
632- describe . each ( PROVIDER_CONFIGS ) ( "%s:%s image support" , ( provider , model ) => {
633- test . concurrent (
634- "should handle multi-turn conversation with response ID persistence (openai reasoning models)" ,
635- async ( ) => {
636- const { env, workspaceId, cleanup } = await setupWorkspace ( "openai" ) ;
637- try {
638- // First message
639- const result1 = await sendMessageWithModel (
640- env . mockIpcRenderer ,
641- workspaceId ,
642- "What is 2+2?" ,
643- modelString ( "openai" , KNOWN_MODELS . GPT_MINI . providerModelId )
644- ) ;
645- expect ( result1 . success ) . toBe ( true ) ;
646-
647- const collector1 = createEventCollector ( env . sentEvents , workspaceId ) ;
648- await collector1 . waitForEvent ( "stream-end" , 30000 ) ;
649- assertStreamSuccess ( collector1 ) ;
650- env . sentEvents . length = 0 ; // Clear events
651-
652- // Second message - should use previousResponseId from first
653- const result2 = await sendMessageWithModel (
654- env . mockIpcRenderer ,
655- workspaceId ,
656- "Now add 3 to that" ,
657- modelString ( "openai" , KNOWN_MODELS . GPT_MINI . providerModelId )
658- ) ;
659- expect ( result2 . success ) . toBe ( true ) ;
660-
661- const collector2 = createEventCollector ( env . sentEvents , workspaceId ) ;
662- await collector2 . waitForEvent ( "stream-end" , 30000 ) ;
663- assertStreamSuccess ( collector2 ) ;
664-
665- // Verify history contains both messages
666- const history = await readChatHistory ( env . tempDir , workspaceId ) ;
667- expect ( history . length ) . toBeGreaterThanOrEqual ( 4 ) ; // 2 user + 2 assistant
668-
669- // Verify assistant messages have responseId
670- const assistantMessages = history . filter ( ( m ) => m . role === "assistant" ) ;
671- expect ( assistantMessages . length ) . toBeGreaterThanOrEqual ( 2 ) ;
672- // Check that responseId exists (type is unknown from JSONL parsing)
673- const firstAssistant = assistantMessages [ 0 ] as any ;
674- const secondAssistant = assistantMessages [ 1 ] as any ;
675- expect ( firstAssistant . metadata ?. providerMetadata ?. openai ?. responseId ) . toBeDefined ( ) ;
676- expect ( secondAssistant . metadata ?. providerMetadata ?. openai ?. responseId ) . toBeDefined ( ) ;
677- } finally {
678- await cleanup ( ) ;
679- }
680- } ,
681- 60000
682- ) ;
571+ const collector1 = createEventCollector ( env . sentEvents , workspaceId ) ;
572+ await collector1 . waitForEvent ( "stream-end" , 30000 ) ;
573+ assertStreamSuccess ( collector1 ) ;
574+ env . sentEvents . length = 0 ; // Clear events
575+
576+ // Second message - should use previousResponseId from first
577+ const result2 = await sendMessageWithModel (
578+ env . mockIpcRenderer ,
579+ workspaceId ,
580+ "Now add 3 to that" ,
581+ modelString ( provider , model )
582+ ) ;
583+ expect ( result2 . success ) . toBe ( true ) ;
584+
585+ const collector2 = createEventCollector ( env . sentEvents , workspaceId ) ;
586+ await collector2 . waitForEvent ( "stream-end" , 30000 ) ;
587+ assertStreamSuccess ( collector2 ) ;
588+
589+ // Verify history contains both messages
590+ // Note: readChatHistory needs the temp directory (root of config).
591+ const history = await readChatHistory ( env . tempDir , workspaceId ) ;
592+ expect ( history . length ) . toBeGreaterThanOrEqual ( 4 ) ; // 2 user + 2 assistant
593+
594+ // Verify assistant messages have responseId
595+ const assistantMessages = history . filter ( ( m ) => m . role === "assistant" ) ;
596+ expect ( assistantMessages . length ) . toBeGreaterThanOrEqual ( 2 ) ;
597+
598+ // Check that responseId exists (if provider supports it)
599+ if ( provider === "openai" ) {
600+ const firstAssistant = assistantMessages [ 0 ] as any ;
601+ const secondAssistant = assistantMessages [ 1 ] as any ;
602+ expect ( firstAssistant . metadata ?. providerMetadata ?. openai ?. responseId ) . toBeDefined ( ) ;
603+ expect ( secondAssistant . metadata ?. providerMetadata ?. openai ?. responseId ) . toBeDefined ( ) ;
604+ }
605+ } ) ;
606+ } ,
607+ 60000
608+ ) ;
609+ } ) ;
683610} ) ;
0 commit comments