diff --git a/packages/cli/src/commands/event/view.ts b/packages/cli/src/commands/event/view.ts index a1a85bbaa..d54b5f6a0 100644 --- a/packages/cli/src/commands/event/view.ts +++ b/packages/cli/src/commands/event/view.ts @@ -453,6 +453,19 @@ export function parsePositionalArgs(args: string[]): ParsedPositionalArgs { }; } + // Detect issue short ID as second arg (e.g., "my-org/my-project BRUNCHIE-APP-29"). + // Auto-redirect to that issue's latest event instead of treating the short + // ID as an event hex ID (which would fail validation). + if (looksLikeIssueShortId(second)) { + const extraEventIds = args.length > 2 ? args.slice(2) : undefined; + return { + eventId: LATEST_EVENT_SENTINEL, + targetArg: first, + issueShortId: second, + extraEventIds, + }; + } + // Two or more args - first is target, second is event ID. // Any additional args are extra event IDs (from newline-separated input). const extraEventIds = args.length > 2 ? args.slice(2) : undefined; @@ -888,9 +901,12 @@ async function resolveIssueShortcut( // alongside a hex event ID. Resolve the issue to get org/project. if (issueShortId) { // Use the explicit org from the parsed target if available (e.g., - // "figma/" → org-all with org "figma"), otherwise fall back to - // auto-detection via DSN/env/config. - const explicitOrg = parsed.type === "org-all" ? parsed.org : undefined; + // "figma/" → org-all, or "figma/project" → explicit, both carry the + // org), otherwise fall back to auto-detection via DSN/env/config. + const explicitOrg = + parsed.type === "org-all" || parsed.type === "explicit" + ? parsed.org + : undefined; const resolved = await resolveOrg({ org: explicitOrg, cwd }); if (!resolved) { throw new ContextError( diff --git a/packages/cli/test/commands/event/view.test.ts b/packages/cli/test/commands/event/view.test.ts index 4fe0b55d5..6a8aa2bf3 100644 --- a/packages/cli/test/commands/event/view.test.ts +++ b/packages/cli/test/commands/event/view.test.ts @@ -1138,6 +1138,45 @@ describe("viewCommand.func", () => { getLatestEventSpy.mockRestore(); }); + test("org/project + short-ID second arg passes explicit org through to resolveOrg", async () => { + // Regression: with an explicit "org/project" target and an issue short ID + // as the second arg, the org must be forwarded to resolveOrg instead of + // being dropped (which would fall back to auto-detection and miss/mishit). + const resolveOrgSpy = vi + .spyOn(resolveTarget, "resolveOrg") + .mockResolvedValue({ org: "my-org" }); + const getIssueByShortIdSpy = vi + .spyOn(apiClient, "getIssueByShortId") + .mockResolvedValue({ id: "999", shortId: "CAM-82X" } as never); + const getLatestEventSpy = vi + .spyOn(apiClient, "getLatestEvent") + .mockResolvedValue(sampleEvent); + getSpanTreeLinesSpy.mockResolvedValue({ + lines: [], + spans: null, + traceId: null, + success: false, + }); + + const { context } = createMockContext(); + const func = await viewCommand.loader(); + await func.call( + context, + { json: true, web: false, spans: 0 }, + "my-org/my-project", + "CAM-82X" + ); + + expect(resolveOrgSpy).toHaveBeenCalledWith( + expect.objectContaining({ org: "my-org" }) + ); + expect(getLatestEventSpy).toHaveBeenCalled(); + + resolveOrgSpy.mockRestore(); + getIssueByShortIdSpy.mockRestore(); + getLatestEventSpy.mockRestore(); + }); + test("logs normalized slug warning when underscores present", async () => { getEventSpy.mockResolvedValue(sampleEvent); getSpanTreeLinesSpy.mockResolvedValue({