@@ -14,6 +14,7 @@ import { homedir } from "node:os";
1414import { fileURLToPath } from "node:url" ;
1515import { afterAll , beforeAll , describe , expect , it } from "vitest" ;
1616import { LinearAgentApi } from "../api/linear-api.js" ;
17+ import { createLinearIssuesTool } from "../tools/linear-issues-tool.js" ;
1718
1819const __dirname = dirname ( fileURLToPath ( import . meta. url ) ) ;
1920
@@ -684,6 +685,147 @@ describe("Linear API smoke tests", () => {
684685 } ) ;
685686 } ) ;
686687
688+ // ---------------------------------------------------------------------------
689+ // Tool-level sub-issue creation (linear_issues tool)
690+ // ---------------------------------------------------------------------------
691+
692+ describe ( "tool-level sub-issue creation (linear_issues tool)" , ( ) => {
693+ let toolParentIdentifier : string | null = null ;
694+ let toolSubIdentifier : string | null = null ;
695+ let toolParentId : string | null = null ;
696+ let toolSubId : string | null = null ;
697+ let tool : any ;
698+
699+ function parseToolResult ( result : any ) : any {
700+ if ( result ?. content && Array . isArray ( result . content ) ) {
701+ const textBlock = result . content . find ( ( r : any ) => r . type === "text" ) ;
702+ if ( textBlock ) return JSON . parse ( textBlock . text ) ;
703+ }
704+ if ( result ?. details ) return result . details ;
705+ return typeof result === "string" ? JSON . parse ( result ) : result ;
706+ }
707+
708+ it ( "instantiates linear_issues tool with real credentials" , ( ) => {
709+ const apiKey = loadApiKey ( ) ;
710+ const pluginApi = {
711+ logger : {
712+ info : ( ...args : any [ ] ) => console . log ( "[tool-smoke]" , ...args ) ,
713+ warn : ( ...args : any [ ] ) => console . warn ( "[tool-smoke]" , ...args ) ,
714+ error : ( ...args : any [ ] ) => console . error ( "[tool-smoke]" , ...args ) ,
715+ debug : ( ) => { } ,
716+ } ,
717+ pluginConfig : { accessToken : apiKey } ,
718+ } ;
719+ tool = createLinearIssuesTool ( pluginApi as any ) ;
720+ expect ( tool ) . toBeTruthy ( ) ;
721+ expect ( tool . name ) . toBe ( "linear_issues" ) ;
722+ } ) ;
723+
724+ it ( "creates a parent issue via tool action=create" , async ( ) => {
725+ const result = parseToolResult (
726+ await tool . execute ( "smoke-call-1" , {
727+ action : "create" ,
728+ title : "[SMOKE TEST] Tool Sub-Issue Parent" ,
729+ description :
730+ "Auto-generated by tool-level smoke test.\n" +
731+ "Tests linear_issues tool can create parent + sub-issues.\n\n" +
732+ `Created: ${ new Date ( ) . toISOString ( ) } ` ,
733+ teamId : TEAM_ID ,
734+ priority : 4 ,
735+ } ) ,
736+ ) ;
737+
738+ expect ( result . error ) . toBeUndefined ( ) ;
739+ expect ( result . success ) . toBe ( true ) ;
740+ expect ( result . identifier ) . toBeTruthy ( ) ;
741+ expect ( result . id ) . toBeTruthy ( ) ;
742+
743+ toolParentIdentifier = result . identifier ;
744+ toolParentId = result . id ;
745+ console . log ( `Tool created parent: ${ result . identifier } (${ result . id } )` ) ;
746+ } ) ;
747+
748+ it ( "reads parent issue via tool action=read (by identifier)" , async ( ) => {
749+ expect ( toolParentIdentifier ) . toBeTruthy ( ) ;
750+
751+ const result = parseToolResult (
752+ await tool . execute ( "smoke-call-2" , {
753+ action : "read" ,
754+ issueId : toolParentIdentifier ! ,
755+ } ) ,
756+ ) ;
757+
758+ expect ( result . error ) . toBeUndefined ( ) ;
759+ expect ( result . identifier ) . toBe ( toolParentIdentifier ) ;
760+ expect ( result . title ) . toContain ( "[SMOKE TEST]" ) ;
761+ expect ( result . team . id ) . toBe ( TEAM_ID ) ;
762+ expect ( result . parent ) . toBeNull ( ) ;
763+ console . log ( `Tool read parent: ${ result . identifier } (status=${ result . status } )` ) ;
764+ } ) ;
765+
766+ it ( "creates a sub-issue via tool action=create with parentIssueId (identifier)" , async ( ) => {
767+ expect ( toolParentIdentifier ) . toBeTruthy ( ) ;
768+
769+ const result = parseToolResult (
770+ await tool . execute ( "smoke-call-3" , {
771+ action : "create" ,
772+ title : "[SMOKE TEST] Tool Sub-Issue: Backend work" ,
773+ description :
774+ "Sub-issue created via linear_issues tool with parentIssueId.\n" +
775+ "Verifies identifier → UUID resolution and teamId inheritance." ,
776+ parentIssueId : toolParentIdentifier ! ,
777+ priority : 3 ,
778+ estimate : 2 ,
779+ } ) ,
780+ ) ;
781+
782+ expect ( result . error ) . toBeUndefined ( ) ;
783+ expect ( result . success ) . toBe ( true ) ;
784+ expect ( result . identifier ) . toBeTruthy ( ) ;
785+ expect ( result . id ) . toBeTruthy ( ) ;
786+ expect ( result . parentIssueId ) . toBe ( toolParentIdentifier ) ;
787+
788+ toolSubIdentifier = result . identifier ;
789+ toolSubId = result . id ;
790+ console . log ( `Tool created sub-issue: ${ result . identifier } (parent=${ toolParentIdentifier } )` ) ;
791+ } ) ;
792+
793+ it ( "verifies sub-issue has correct parent via tool action=read" , async ( ) => {
794+ expect ( toolSubIdentifier ) . toBeTruthy ( ) ;
795+
796+ const result = parseToolResult (
797+ await tool . execute ( "smoke-call-4" , {
798+ action : "read" ,
799+ issueId : toolSubIdentifier ! ,
800+ } ) ,
801+ ) ;
802+
803+ expect ( result . error ) . toBeUndefined ( ) ;
804+ expect ( result . identifier ) . toBe ( toolSubIdentifier ) ;
805+ expect ( result . parent ) . not . toBeNull ( ) ;
806+ expect ( result . parent . identifier ) . toBe ( toolParentIdentifier ) ;
807+ // teamId was inherited from parent (not provided explicitly in create)
808+ expect ( result . team . id ) . toBe ( TEAM_ID ) ;
809+ console . log ( `Tool sub-issue parent confirmed: ${ result . parent . identifier } ` ) ;
810+ } ) ;
811+
812+ it ( "cleans up: cancels tool-created issues" , async ( ) => {
813+ const states = await api . getTeamStates ( TEAM_ID ) ;
814+ const canceledState = states . find (
815+ ( s ) => s . type === "canceled" || s . name . toLowerCase ( ) . includes ( "cancel" ) ,
816+ ) ;
817+
818+ for ( const id of [ toolSubId , toolParentId ] ) {
819+ if ( ! id || ! canceledState ) continue ;
820+ try {
821+ await api . updateIssue ( id , { stateId : canceledState . id } ) ;
822+ } catch {
823+ // Best effort
824+ }
825+ }
826+ } ) ;
827+ } ) ;
828+
687829 describe ( "cleanup" , ( ) => {
688830 it ( "cancels the smoke test issue" , async ( ) => {
689831 if ( ! smokeIssueId ) return ;
0 commit comments