@@ -20,6 +20,7 @@ const mockHost = vi.hoisted(() => ({
2020 getEnvironment : vi . fn ( ) ,
2121 detectRepo : vi . fn ( ) ,
2222 getCloudPromptTransport : vi . fn ( ) ,
23+ resolveLocalSkillCommandPrompt : vi . fn ( async ( prompt : string ) => prompt ) ,
2324 uploadRunAttachments : vi . fn ( ) ,
2425 setProvisioningActive : vi . fn ( ) ,
2526 clearProvisioning : vi . fn ( ) ,
@@ -414,6 +415,106 @@ describe("TaskCreationSaga", () => {
414415 ) ;
415416 } ) ;
416417
418+ it ( "resolves a typed local-skill slash command before building the cloud transport" , async ( ) => {
419+ const createdTask = createTask ( ) ;
420+ const startedTask = createTask ( { latest_run : createRun ( ) } ) ;
421+ const createTaskMock = vi . fn ( ) . mockResolvedValue ( createdTask ) ;
422+ const createTaskRunMock = vi . fn ( ) . mockResolvedValue ( createRun ( ) ) ;
423+ const startTaskRunMock = vi . fn ( ) . mockResolvedValue ( startedTask ) ;
424+
425+ const skillTag =
426+ '<skill name="my-skill" source="user" path="/skills/my-skill" /> do it' ;
427+ mockHost . resolveLocalSkillCommandPrompt . mockResolvedValue ( skillTag ) ;
428+ mockHost . getCloudPromptTransport . mockReturnValue ( {
429+ filePaths : [ ] ,
430+ skillBundles : [
431+ { name : "my-skill" , source : "user" , path : "/skills/my-skill" } ,
432+ ] ,
433+ messageText : "/my-skill do it" ,
434+ promptText : "/my-skill do it" ,
435+ } ) ;
436+ mockHost . uploadRunAttachments . mockResolvedValue ( [ "skill-artifact-1" ] ) ;
437+
438+ const saga = makeSaga ( {
439+ createTask : createTaskMock ,
440+ createTaskRun : createTaskRunMock ,
441+ startTaskRun : startTaskRunMock ,
442+ } ) ;
443+
444+ const result = await saga . run ( {
445+ content : "/my-skill do it" ,
446+ repository : "posthog/posthog" ,
447+ workspaceMode : "cloud" ,
448+ branch : "main" ,
449+ } ) ;
450+
451+ expect ( result . success ) . toBe ( true ) ;
452+ expect ( mockHost . resolveLocalSkillCommandPrompt ) . toHaveBeenCalledWith (
453+ "/my-skill do it" ,
454+ ) ;
455+ // The resolved tag (not the raw slash command) must reach the transport so
456+ // the bundle is collected and uploaded on the first message.
457+ expect ( mockHost . getCloudPromptTransport ) . toHaveBeenCalledWith (
458+ skillTag ,
459+ undefined ,
460+ ) ;
461+ expect ( startTaskRunMock ) . toHaveBeenCalledWith ( "task-123" , "run-123" , {
462+ pendingUserMessage : "/my-skill do it" ,
463+ pendingUserArtifactIds : [ "skill-artifact-1" ] ,
464+ } ) ;
465+ } ) ;
466+
467+ it . each ( [
468+ [ "a plain-text prompt that isn't a slash command" , "just do the thing" ] ,
469+ [ "a slash command that isn't a local skill" , "/good keep going" ] ,
470+ ] ) (
471+ "passes %s through to the transport unchanged" ,
472+ async ( _label , content ) => {
473+ const createdTask = createTask ( ) ;
474+ const startedTask = createTask ( { latest_run : createRun ( ) } ) ;
475+ const createTaskRunMock = vi . fn ( ) . mockResolvedValue ( createRun ( ) ) ;
476+ const startTaskRunMock = vi . fn ( ) . mockResolvedValue ( startedTask ) ;
477+
478+ // The host resolver is a no-op for non-local-skill prompts — it returns the
479+ // original string unchanged (mirrors `resolveLocalSkillPrompt` yielding null
480+ // and the host falling back to the prompt).
481+ mockHost . resolveLocalSkillCommandPrompt . mockImplementation (
482+ async ( prompt : string ) => prompt ,
483+ ) ;
484+ mockHost . getCloudPromptTransport . mockReturnValue ( {
485+ filePaths : [ ] ,
486+ skillBundles : [ ] ,
487+ messageText : content ,
488+ promptText : content ,
489+ } ) ;
490+ mockHost . uploadRunAttachments . mockResolvedValue ( [ ] ) ;
491+
492+ const saga = makeSaga ( {
493+ createTask : vi . fn ( ) . mockResolvedValue ( createdTask ) ,
494+ createTaskRun : createTaskRunMock ,
495+ startTaskRun : startTaskRunMock ,
496+ } ) ;
497+
498+ const result = await saga . run ( {
499+ content,
500+ repository : "posthog/posthog" ,
501+ workspaceMode : "cloud" ,
502+ branch : "main" ,
503+ } ) ;
504+
505+ expect ( result . success ) . toBe ( true ) ;
506+ expect ( mockHost . resolveLocalSkillCommandPrompt ) . toHaveBeenCalledWith (
507+ content ,
508+ ) ;
509+ // Resolution is a no-op, so the original content (not a rewritten skill tag)
510+ // reaches the transport and no bundle is collected.
511+ expect ( mockHost . getCloudPromptTransport ) . toHaveBeenCalledWith (
512+ content ,
513+ undefined ,
514+ ) ;
515+ } ,
516+ ) ;
517+
417518 it ( "uses the selected user GitHub integration for cloud task creation" , async ( ) => {
418519 const createdTask = createTask ( {
419520 github_user_integration : "user-integration-123" ,
0 commit comments