@@ -16,7 +16,7 @@ import { DEFAULT_SENTRY_URL } from "../../../src/lib/constants.js";
1616import { setAuthToken } from "../../../src/lib/db/auth.js" ;
1717import { setCachedProject } from "../../../src/lib/db/project-cache.js" ;
1818import { setOrgRegion } from "../../../src/lib/db/regions.js" ;
19- import { ResolutionError } from "../../../src/lib/errors.js" ;
19+ import { ApiError , ResolutionError } from "../../../src/lib/errors.js" ;
2020import { useTestConfigDir } from "../../helpers.js" ;
2121
2222describe ( "buildCommandHint" , ( ) => {
@@ -707,6 +707,201 @@ describe("resolveOrgAndIssueId", () => {
707707 } )
708708 ) . rejects . toThrow ( "500" ) ;
709709 } ) ;
710+
711+ test ( "fast path: ambiguous when shortid resolves in multiple orgs" , async ( ) => {
712+ const { clearProjectAliases } = await import (
713+ "../../../src/lib/db/project-aliases.js"
714+ ) ;
715+ await clearProjectAliases ( ) ;
716+
717+ await setOrgRegion ( "org2" , DEFAULT_SENTRY_URL ) ;
718+
719+ const makeShortIdResponse = ( orgSlug : string , groupId : string ) =>
720+ new Response (
721+ JSON . stringify ( {
722+ organizationSlug : orgSlug ,
723+ projectSlug : "shared" ,
724+ groupId,
725+ group : {
726+ id : groupId ,
727+ shortId : "SHARED-G" ,
728+ title : "Test Issue" ,
729+ status : "unresolved" ,
730+ platform : "javascript" ,
731+ type : "error" ,
732+ count : "1" ,
733+ userCount : 1 ,
734+ } ,
735+ } ) ,
736+ {
737+ status : 200 ,
738+ headers : { "Content-Type" : "application/json" } ,
739+ }
740+ ) ;
741+
742+ // @ts -expect-error - partial mock
743+ globalThis . fetch = async ( input : RequestInfo | URL , init ?: RequestInit ) => {
744+ const req = new Request ( input , init ) ;
745+ const url = req . url ;
746+
747+ if ( url . includes ( "/users/me/regions/" ) ) {
748+ return new Response ( JSON . stringify ( { regions : [ ] } ) , {
749+ status : 200 ,
750+ headers : { "Content-Type" : "application/json" } ,
751+ } ) ;
752+ }
753+
754+ if (
755+ url . includes ( "/organizations/" ) &&
756+ ! url . includes ( "/projects/" ) &&
757+ ! url . includes ( "/issues/" ) &&
758+ ! url . includes ( "/shortids/" )
759+ ) {
760+ return new Response (
761+ JSON . stringify ( [
762+ { id : "1" , slug : "org1" , name : "Org 1" } ,
763+ { id : "2" , slug : "org2" , name : "Org 2" } ,
764+ ] ) ,
765+ {
766+ status : 200 ,
767+ headers : { "Content-Type" : "application/json" } ,
768+ }
769+ ) ;
770+ }
771+
772+ // Both orgs resolve the shortid — triggers fast-path ambiguity
773+ if ( url . includes ( "organizations/org1/shortids/SHARED-G" ) ) {
774+ return makeShortIdResponse ( "org1" , "111" ) ;
775+ }
776+ if ( url . includes ( "organizations/org2/shortids/SHARED-G" ) ) {
777+ return makeShortIdResponse ( "org2" , "222" ) ;
778+ }
779+
780+ return new Response ( JSON . stringify ( { detail : "Not found" } ) , {
781+ status : 404 ,
782+ } ) ;
783+ } ;
784+
785+ await expect (
786+ resolveOrgAndIssueId ( {
787+ issueArg : "shared-g" ,
788+ cwd : getConfigDir ( ) ,
789+ command : "explain" ,
790+ } )
791+ ) . rejects . toThrow ( "is ambiguous" ) ;
792+ } ) ;
793+
794+ test ( "fast path: surfaces 403 when all orgs return forbidden" , async ( ) => {
795+ const { clearProjectAliases } = await import (
796+ "../../../src/lib/db/project-aliases.js"
797+ ) ;
798+ await clearProjectAliases ( ) ;
799+
800+ // @ts -expect-error - partial mock
801+ globalThis . fetch = async ( input : RequestInfo | URL , init ?: RequestInit ) => {
802+ const req = new Request ( input , init ) ;
803+ const url = req . url ;
804+
805+ if ( url . includes ( "/users/me/regions/" ) ) {
806+ return new Response ( JSON . stringify ( { regions : [ ] } ) , {
807+ status : 200 ,
808+ headers : { "Content-Type" : "application/json" } ,
809+ } ) ;
810+ }
811+
812+ if (
813+ url . includes ( "/organizations/" ) &&
814+ ! url . includes ( "/projects/" ) &&
815+ ! url . includes ( "/issues/" ) &&
816+ ! url . includes ( "/shortids/" )
817+ ) {
818+ return new Response (
819+ JSON . stringify ( [ { id : "1" , slug : "my-org" , name : "My Org" } ] ) ,
820+ {
821+ status : 200 ,
822+ headers : { "Content-Type" : "application/json" } ,
823+ }
824+ ) ;
825+ }
826+
827+ // Shortid endpoint returns 403 for all orgs
828+ if ( url . includes ( "/shortids/" ) ) {
829+ return new Response (
830+ JSON . stringify ( { detail : "You do not have permission" } ) ,
831+ { status : 403 }
832+ ) ;
833+ }
834+
835+ return new Response ( JSON . stringify ( { detail : "Not found" } ) , {
836+ status : 404 ,
837+ } ) ;
838+ } ;
839+
840+ const err = await resolveOrgAndIssueId ( {
841+ issueArg : "restricted-g" ,
842+ cwd : getConfigDir ( ) ,
843+ command : "explain" ,
844+ } ) . catch ( ( e : unknown ) => e ) ;
845+
846+ expect ( err ) . toBeInstanceOf ( ApiError ) ;
847+ expect ( ( err as ApiError ) . status ) . toBe ( 403 ) ;
848+ } ) ;
849+
850+ test ( "fast path: surfaces 500 when all orgs return server error" , async ( ) => {
851+ const { clearProjectAliases } = await import (
852+ "../../../src/lib/db/project-aliases.js"
853+ ) ;
854+ await clearProjectAliases ( ) ;
855+
856+ // @ts -expect-error - partial mock
857+ globalThis . fetch = async ( input : RequestInfo | URL , init ?: RequestInit ) => {
858+ const req = new Request ( input , init ) ;
859+ const url = req . url ;
860+
861+ if ( url . includes ( "/users/me/regions/" ) ) {
862+ return new Response ( JSON . stringify ( { regions : [ ] } ) , {
863+ status : 200 ,
864+ headers : { "Content-Type" : "application/json" } ,
865+ } ) ;
866+ }
867+
868+ if (
869+ url . includes ( "/organizations/" ) &&
870+ ! url . includes ( "/projects/" ) &&
871+ ! url . includes ( "/issues/" ) &&
872+ ! url . includes ( "/shortids/" )
873+ ) {
874+ return new Response (
875+ JSON . stringify ( [ { id : "1" , slug : "my-org" , name : "My Org" } ] ) ,
876+ {
877+ status : 200 ,
878+ headers : { "Content-Type" : "application/json" } ,
879+ }
880+ ) ;
881+ }
882+
883+ // Shortid endpoint returns 500 for all orgs
884+ if ( url . includes ( "/shortids/" ) ) {
885+ return new Response (
886+ JSON . stringify ( { detail : "Internal Server Error" } ) ,
887+ { status : 500 }
888+ ) ;
889+ }
890+
891+ return new Response ( JSON . stringify ( { detail : "Not found" } ) , {
892+ status : 404 ,
893+ } ) ;
894+ } ;
895+
896+ const err = await resolveOrgAndIssueId ( {
897+ issueArg : "broken-g" ,
898+ cwd : getConfigDir ( ) ,
899+ command : "explain" ,
900+ } ) . catch ( ( e : unknown ) => e ) ;
901+
902+ expect ( err ) . toBeInstanceOf ( ApiError ) ;
903+ expect ( ( err as ApiError ) . status ) . toBe ( 500 ) ;
904+ } ) ;
710905} ) ;
711906
712907describe ( "pollAutofixState" , ( ) => {
0 commit comments