@@ -21,6 +21,9 @@ const mockHost = vi.hoisted(() => ({
2121 detectRepo : vi . fn ( ) ,
2222 getCloudPromptTransport : vi . fn ( ) ,
2323 resolveLocalSkillCommandPrompt : vi . fn ( async ( prompt : string ) => prompt ) ,
24+ takeWarmTaskLease : vi . fn (
25+ ( ) : { taskId : string ; runId : string } | null => null ,
26+ ) ,
2427 uploadRunAttachments : vi . fn ( ) ,
2528 setProvisioningActive : vi . fn ( ) ,
2629 clearProvisioning : vi . fn ( ) ,
@@ -516,6 +519,167 @@ describe("TaskCreationSaga", () => {
516519 } ,
517520 ) ;
518521
522+ it ( "uploads skill bundles to the warm run and passes pending fields through createTask" , async ( ) => {
523+ const skillTag =
524+ '<skill name="my-skill" source="user" path="/skills/my-skill" /> do it' ;
525+ mockHost . resolveLocalSkillCommandPrompt . mockResolvedValue ( skillTag ) ;
526+ mockHost . getCloudPromptTransport . mockReturnValue ( {
527+ filePaths : [ ] ,
528+ skillBundles : [
529+ { name : "my-skill" , source : "user" , path : "/skills/my-skill" } ,
530+ ] ,
531+ messageText : "/my-skill do it" ,
532+ promptText : "/my-skill do it" ,
533+ } ) ;
534+ mockHost . takeWarmTaskLease . mockReturnValue ( {
535+ taskId : "warm-task" ,
536+ runId : "warm-run" ,
537+ } ) ;
538+ mockHost . uploadRunAttachments . mockResolvedValue ( [ "skill-artifact-1" ] ) ;
539+
540+ const warmActivatedTask = createTask ( {
541+ id : "warm-task" ,
542+ latest_run : createRun ( { id : "warm-run" , task : "warm-task" } ) ,
543+ } ) ;
544+ const createTaskMock = vi . fn ( ) . mockResolvedValue ( warmActivatedTask ) ;
545+ const createTaskRunMock = vi . fn ( ) ;
546+ const startTaskRunMock = vi . fn ( ) ;
547+ const saga = makeSaga ( {
548+ createTask : createTaskMock ,
549+ createTaskRun : createTaskRunMock ,
550+ startTaskRun : startTaskRunMock ,
551+ } ) ;
552+
553+ const result = await saga . run ( {
554+ content : "/my-skill do it" ,
555+ repository : "posthog/posthog" ,
556+ workspaceMode : "cloud" ,
557+ branch : "main" ,
558+ } ) ;
559+
560+ expect ( result . success ) . toBe ( true ) ;
561+ expect ( mockHost . takeWarmTaskLease ) . toHaveBeenCalledWith ( {
562+ repository : "posthog/posthog" ,
563+ branch : "main" ,
564+ runtimeAdapter : null ,
565+ model : null ,
566+ reasoningEffort : null ,
567+ } ) ;
568+ // The bundle must land on the warm run before createTask triggers activation.
569+ expect ( mockHost . uploadRunAttachments ) . toHaveBeenCalledWith (
570+ expect . anything ( ) ,
571+ "warm-task" ,
572+ "warm-run" ,
573+ [ ] ,
574+ [ { name : "my-skill" , source : "user" , path : "/skills/my-skill" } ] ,
575+ ) ;
576+ expect ( createTaskMock ) . toHaveBeenCalledWith (
577+ expect . objectContaining ( {
578+ branch : "main" ,
579+ pending_user_message : "/my-skill do it" ,
580+ pending_user_artifact_ids : [ "skill-artifact-1" ] ,
581+ } ) ,
582+ ) ;
583+ // Warm-activated at create time: no fresh run is created or started.
584+ expect ( createTaskRunMock ) . not . toHaveBeenCalled ( ) ;
585+ expect ( startTaskRunMock ) . not . toHaveBeenCalled ( ) ;
586+ } ) ;
587+
588+ it ( "suppresses warm reuse when attachments exist but no warm lease is known" , async ( ) => {
589+ const skillTag =
590+ '<skill name="my-skill" source="user" path="/skills/my-skill" /> do it' ;
591+ mockHost . resolveLocalSkillCommandPrompt . mockResolvedValue ( skillTag ) ;
592+ mockHost . getCloudPromptTransport . mockReturnValue ( {
593+ filePaths : [ ] ,
594+ skillBundles : [
595+ { name : "my-skill" , source : "user" , path : "/skills/my-skill" } ,
596+ ] ,
597+ messageText : "/my-skill do it" ,
598+ promptText : "/my-skill do it" ,
599+ } ) ;
600+ mockHost . takeWarmTaskLease . mockReturnValue ( null ) ;
601+ mockHost . uploadRunAttachments . mockResolvedValue ( [ "skill-artifact-1" ] ) ;
602+
603+ const createdTask = createTask ( ) ;
604+ const startedTask = createTask ( { latest_run : createRun ( ) } ) ;
605+ const createTaskMock = vi . fn ( ) . mockResolvedValue ( createdTask ) ;
606+ const createTaskRunMock = vi . fn ( ) . mockResolvedValue ( createRun ( ) ) ;
607+ const startTaskRunMock = vi . fn ( ) . mockResolvedValue ( startedTask ) ;
608+ const saga = makeSaga ( {
609+ createTask : createTaskMock ,
610+ createTaskRun : createTaskRunMock ,
611+ startTaskRun : startTaskRunMock ,
612+ } ) ;
613+
614+ const result = await saga . run ( {
615+ content : "/my-skill do it" ,
616+ repository : "posthog/posthog" ,
617+ workspaceMode : "cloud" ,
618+ branch : "main" ,
619+ } ) ;
620+
621+ expect ( result . success ) . toBe ( true ) ;
622+ // No lease to upload to: omit the warm-reuse branch hint so the backend
623+ // cannot activate a warm run this client can't attach the bundle to.
624+ expect ( createTaskMock . mock . calls [ 0 ] [ 0 ] . branch ) . toBeUndefined ( ) ;
625+ // Cold path proceeds and delivers the bundle through the run start.
626+ expect ( startTaskRunMock ) . toHaveBeenCalledWith ( "task-123" , "run-123" , {
627+ pendingUserMessage : "/my-skill do it" ,
628+ pendingUserArtifactIds : [ "skill-artifact-1" ] ,
629+ } ) ;
630+ } ) ;
631+
632+ it ( "falls back to cold creation when the warm-run upload fails" , async ( ) => {
633+ const skillTag =
634+ '<skill name="my-skill" source="user" path="/skills/my-skill" /> do it' ;
635+ mockHost . resolveLocalSkillCommandPrompt . mockResolvedValue ( skillTag ) ;
636+ mockHost . getCloudPromptTransport . mockReturnValue ( {
637+ filePaths : [ ] ,
638+ skillBundles : [
639+ { name : "my-skill" , source : "user" , path : "/skills/my-skill" } ,
640+ ] ,
641+ messageText : "/my-skill do it" ,
642+ promptText : "/my-skill do it" ,
643+ } ) ;
644+ mockHost . takeWarmTaskLease . mockReturnValue ( {
645+ taskId : "warm-task" ,
646+ runId : "warm-run" ,
647+ } ) ;
648+ mockHost . uploadRunAttachments
649+ . mockRejectedValueOnce ( new Error ( "warm upload failed" ) )
650+ . mockResolvedValueOnce ( [ "skill-artifact-1" ] ) ;
651+
652+ const createdTask = createTask ( ) ;
653+ const startedTask = createTask ( { latest_run : createRun ( ) } ) ;
654+ const createTaskMock = vi . fn ( ) . mockResolvedValue ( createdTask ) ;
655+ const createTaskRunMock = vi . fn ( ) . mockResolvedValue ( createRun ( ) ) ;
656+ const startTaskRunMock = vi . fn ( ) . mockResolvedValue ( startedTask ) ;
657+ const saga = makeSaga ( {
658+ createTask : createTaskMock ,
659+ createTaskRun : createTaskRunMock ,
660+ startTaskRun : startTaskRunMock ,
661+ } ) ;
662+
663+ const result = await saga . run ( {
664+ content : "/my-skill do it" ,
665+ repository : "posthog/posthog" ,
666+ workspaceMode : "cloud" ,
667+ branch : "main" ,
668+ } ) ;
669+
670+ // The failed pre-upload must not fail creation or activate warm without
671+ // the bundle: warm reuse is suppressed and the cold path re-uploads.
672+ expect ( result . success ) . toBe ( true ) ;
673+ expect ( createTaskMock . mock . calls [ 0 ] [ 0 ] . branch ) . toBeUndefined ( ) ;
674+ expect (
675+ createTaskMock . mock . calls [ 0 ] [ 0 ] . pending_user_artifact_ids ,
676+ ) . toBeUndefined ( ) ;
677+ expect ( startTaskRunMock ) . toHaveBeenCalledWith ( "task-123" , "run-123" , {
678+ pendingUserMessage : "/my-skill do it" ,
679+ pendingUserArtifactIds : [ "skill-artifact-1" ] ,
680+ } ) ;
681+ } ) ;
682+
519683 it ( "uses the selected user GitHub integration for cloud task creation" , async ( ) => {
520684 const createdTask = createTask ( {
521685 github_user_integration : "user-integration-123" ,
0 commit comments