diff --git a/src/__tests__/commands/developer.test.ts b/src/__tests__/commands/developer.test.ts index 63ca0ba364..b41dbb1d4e 100644 --- a/src/__tests__/commands/developer.test.ts +++ b/src/__tests__/commands/developer.test.ts @@ -143,6 +143,60 @@ describe('handleDeveloperSearchCommand', () => { expect(content).toContain('rust: not indexed'); }); + it('prints a result URL only when its id does not contain it', async () => { + const duplicateUrl = 'https://docs.rs/tokio/latest/tokio'; + mockHttpGet.mockResolvedValue( + mockDeveloperResponse([ + sampleResult, + { + id: `web:${duplicateUrl}`, + url: duplicateUrl, + title: 'Tokio docs', + passages: [{ text: 'Runtime documentation.' }], + }, + { + id: `web:${duplicateUrl}/search`, + url: duplicateUrl, + title: 'Tokio search', + passages: [{ text: 'Search documentation.' }], + }, + ]) + ); + + await handleDeveloperSearchCommand({ query: 'tokio runtime' }); + + const [content] = vi.mocked(writeOutput).mock.calls[0] as [string]; + expect(content.split(duplicateUrl).length - 1).toBe(3); + expect(content).toContain( + `## [web:${duplicateUrl}/search] Tokio search\n${duplicateUrl}\n` + ); + expect(content).toContain( + '\nhttps://github.com/tokio-rs/tokio/issues/2309\n' + ); + }); + + it('does not render separators for empty passages', async () => { + mockHttpGet.mockResolvedValue( + mockDeveloperResponse([ + { + ...sampleResult, + passages: [ + { text: '' }, + { text: ' ' }, + { text: 'The answer.' }, + {}, + ], + }, + ]) + ); + + await handleDeveloperSearchCommand({ query: 'tokio runtime' }); + + const [content] = vi.mocked(writeOutput).mock.calls[0] as [string]; + expect(content).toContain('\nThe answer.'); + expect(content).not.toContain('---'); + }); + it('renders citation URLs and object license disclosures', async () => { mockHttpGet.mockResolvedValue(mockDeveloperResponse([sampleResult])); diff --git a/src/commands/developer.ts b/src/commands/developer.ts index 5fbc3a32b9..5d2f525e39 100644 --- a/src/commands/developer.ts +++ b/src/commands/developer.ts @@ -38,6 +38,10 @@ function fmtLicense(license: DeveloperLicense | string): string { return `License: ${license.state.replace('_', ' ')}`; } +function idEncodesUrl(id: string | undefined, url: string): boolean { + return id === url || id?.endsWith(`:${url}`) === true; +} + function fmtResult(item: DeveloperItem): string { // The wire carries no type field; the artifact kind is the id prefix // (doc:, issue:, pull_request:, readme:). @@ -46,7 +50,7 @@ function fmtResult(item: DeveloperItem): string { ? ` (${prefix})` : ''; const lines = [`## [${item.id ?? '?'}]${kind} ${item.title ?? '(untitled)'}`]; - if (item.url) lines.push(item.url); + if (item.url && !idEncodesUrl(item.id, item.url)) lines.push(item.url); if (item.license) lines.push(fmtLicense(item.license)); const body = (item.passages ?? []) .map((passage) => @@ -57,6 +61,7 @@ function fmtResult(item: DeveloperItem): string { .filter(Boolean) .join('\n') ) + .filter((passage) => passage.trim().length > 0) .join('\n---\n') .trim(); lines.push(body || '(no content)');