Repository: opossum-tool/OpossumUI
Pull request #2438 (2023-12-20 13:55:08)

feat: fine-tune autocomplete suggestions
### Summary of changes

1. ensure that the autocomplete suggestion "+" button overwrites all previously entered data so that there cannot be confusion about what was and what wasn't replaced by the user's action
2. make autocomplete suggestion "+" button more visually distinct from the primary suggestion action of just adding the single attribute so that it becomes clearer that there really are two separate actions on the suggestion card
![image](https://github.com/opossum-tool/OpossumUI/assets/46091730/a5b03ee6-9e7f-4438-bdbb-eb6a730ec83e)
3. ensure click on entire autocomplete input field opens option popper and not just a click on the underlying HTML input element
4. catch unhandled errors produced by the PackageURL constructor and instead display "INVALID PURL"
5. do not set a default version when user adopts a suggestion via autocomplete "+" button because the default (latest) version will almost always be the wrong one

### Context and reason for change

Finalizing #2333.

### How can the changes be tested

See summary of changes.

For 3: click on the margin area of the input element (highlighted in green here): 
![image](https://github.com/opossum-tool/OpossumUI/assets/46091730/75bc6f9c-49cc-4353-aa85-a6c8ffd5bb94)

For 4: chose "pub" as package type and type a package name which includes an extended symbol. See [here](https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst) for details.
![image](https://github.com/opossum-tool/OpossumUI/assets/46091730/d3acc5db-70b8-40ae-a7b2-5661a46ac040)



Patch:
diff --git a/example-files/opossum_input.json b/example-files/opossum_input.json
index bfcef1ce..5964eb52 100644
--- a/example-files/opossum_input.json
+++ b/example-files/opossum_input.json
@@ -161,7 +161,8 @@
       },
       "attributionConfidence": 100,
       "comment": "TypeScript. \nhttps://www.npmjs.com/package/typescript",
-      "packageName": "TypeScript",
+      "packageName": "typescript",
+      "packageNamespace": "microsoft",
       "preSelected": true,
       "criticality": "medium",
       "packageVersion": "3.9.6",
@@ -178,7 +179,8 @@
       },
       "attributionConfidence": 100,
       "comment": "TypeScript. \nhttps://www.npmjs.com/package/typescript",
-      "packageName": "TypeScript",
+      "packageName": "typescript",
+      "packageNamespace": "microsoft",
       "preSelected": true,
       "criticality": "medium",
       "packageVersion": "3.9.6",
@@ -194,7 +196,8 @@
       },
       "attributionConfidence": 100,
       "comment": "TypeScript. \nhttps://www.npmjs.com/package/typescript",
-      "packageName": "TypeScript",
+      "packageName": "typescript",
+      "packageNamespace": "microsoft",
       "preSelected": true,
       "criticality": "medium",
       "packageVersion": "3.9.6",
@@ -210,7 +213,8 @@
       },
       "attributionConfidence": 100,
       "comment": "TypeScript. \nhttps://www.npmjs.com/package/typescript",
-      "packageName": "TypeScript",
+      "packageName": "typescript",
+      "packageNamespace": "microsoft",
       "packageType": "github",
       "preSelected": true,
       "criticality": "medium",
diff --git a/src/Frontend/Components/AttributionColumn/LicenseSubPanel.tsx b/src/Frontend/Components/AttributionColumn/LicenseSubPanel.tsx
index 319a8982..ea864e05 100644
--- a/src/Frontend/Components/AttributionColumn/LicenseSubPanel.tsx
+++ b/src/Frontend/Components/AttributionColumn/LicenseSubPanel.tsx
@@ -11,7 +11,10 @@ import MuiInputAdornment from '@mui/material/InputAdornment';
 import { sortBy } from 'lodash';
 import { ReactElement, useMemo, useState } from 'react';
 
-import { DisplayPackageInfo, PackageInfo } from '../../../shared/shared-types';
+import {
+  AutocompleteSignal,
+  DisplayPackageInfo,
+} from '../../../shared/shared-types';
 import { text } from '../../../shared/text';
 import { OpossumColors } from '../../shared-styles';
 import { setTemporaryDisplayPackageInfo } from '../../state/actions/resource-actions/all-views-simple-actions';
@@ -81,7 +84,9 @@ export function LicenseSubPanel({
   const defaultLicenses = useMemo(
     () =>
       sortBy(
-        frequentLicensesNames.map<PackageInfo>(({ fullName }) => ({
+        frequentLicensesNames.map<AutocompleteSignal>(({ fullName }) => ({
+          attributionIds: [],
+          default: true,
           licenseName: fullName,
           source: {
             documentConfidence: 100,
diff --git a/src/Frontend/Components/AttributionColumn/PackageAutocomplete.tsx b/src/Frontend/Components/AttributionColumn/PackageAutocomplete.tsx
index 4a978da7..d5e85885 100644
--- a/src/Frontend/Components/AttributionColumn/PackageAutocomplete.tsx
+++ b/src/Frontend/Components/AttributionColumn/PackageAutocomplete.tsx
@@ -14,7 +14,11 @@ import MuiTooltip from '@mui/material/Tooltip';
 import { compact } from 'lodash';
 import { useEffect, useMemo, useState } from 'react';
 
-import { AutocompleteSignal, PackageInfo } from '../../../shared/shared-types';
+import {
+  AutocompleteSignal,
+  DisplayPackageInfo,
+  PackageInfo,
+} from '../../../shared/shared-types';
 import { text } from '../../../shared/text';
 import { baseIcon, clickableIcon, OpossumColors } from '../../shared-styles';
 import { setTemporaryDisplayPackageInfo } from '../../state/actions/resource-actions/all-views-simple-actions';
@@ -26,6 +30,7 @@ import {
 import { isAuditViewSelected } from '../../state/selectors/view-selector';
 import { generatePurl } from '../../util/handle-purl';
 import { isImportantAttributionInformationMissing } from '../../util/is-important-attribution-information-missing';
+import { omit, pick } from '../../util/lodash-extension-utils';
 import { maybePluralize } from '../../util/maybe-pluralize';
 import { openUrl } from '../../util/open-url';
 import { PackageSearchHooks } from '../../util/package-search-hooks';
@@ -63,6 +68,12 @@ const Badge = styled(MuiBadge)({
   },
 });
 
+const AddIconButton = styled(MuiIconButton)({
+  backgroundColor: 'rgba(0, 0, 0, 0.04)',
+  marginLeft: '8px',
+  '&:hover': { backgroundColor: 'rgba(0, 0, 0, 0.1)' },
+});
+
 export function PackageAutocomplete({
   attribute,
   title,
@@ -80,8 +91,7 @@ export function PackageAutocomplete({
   const sources = useAppSelector(getExternalAttributionSources);
   const isAuditView = useAppSelector(isAuditViewSelected);
 
-  const { getPackageUrlAndLicense } =
-    PackageSearchHooks.useGetPackageUrlAndLegal();
+  const { enrichPackageInfo } = PackageSearchHooks.useEnrichPackageInfo();
 
   const autocompleteSignals = useAutocompleteSignals();
   const filteredSignals = useMemo(
@@ -217,11 +227,12 @@ export function PackageAutocomplete({
           showStar
             ? `${baseTitle} (${
                 option.preferred
-                  ? text.auditingOptions.currentlyPreferred
-                  : text.auditingOptions.previouslyPreferred
+                  ? text.auditingOptions.currentlyPreferred.toLowerCase()
+                  : text.auditingOptions.previouslyPreferred.toLowerCase()
               })`
             : baseTitle
         }
+        enterDelay={500}
       >
         <Badge
           badgeContent={
@@ -248,16 +259,7 @@ export function PackageAutocomplete({
   }
 
   function renderOptionEndIcon(
-    {
-      attributionConfidence,
-      count,
-      excludeFromNotice,
-      followUp,
-      needsReview,
-      preSelected,
-      source,
-      ...option
-    }: AutocompleteSignal,
+    option: AutocompleteSignal,
     { closePopper }: { closePopper: () => void },
   ) {
     if (!option.packageName || !option.packageType) {
@@ -267,27 +269,39 @@ export function PackageAutocomplete({
     return (
       <MuiTooltip
         title={text.attributionColumn.useAutocompleteSuggestion}
+        enterDelay={1000}
         disableInteractive
       >
-        <MuiIconButton
+        <AddIconButton
           onClick={async (event) => {
             event.stopPropagation();
+            const merged: DisplayPackageInfo = {
+              ...omit(option, [
+                'count',
+                'default',
+                'preSelected',
+                'source',
+                'suffix',
+              ]),
+              ...pick(temporaryPackageInfo, [
+                'attributionConfidence',
+                'excludeFromNotice',
+                'followUp',
+                'needsReview',
+                'preferred',
+              ]),
+            };
             dispatch(
-              setTemporaryDisplayPackageInfo({
-                ...temporaryPackageInfo,
-                ...option,
-                ...(await getPackageUrlAndLicense({
-                  ...temporaryPackageInfo,
-                  ...option,
-                })),
-              }),
+              setTemporaryDisplayPackageInfo(
+                (option.default && (await enrichPackageInfo(merged))) || merged,
+              ),
             );
             closePopper();
           }}
           size={'small'}
         >
           <AddIcon fontSize={'inherit'} color={'primary'} />
-        </MuiIconButton>
+        </AddIconButton>
       </MuiTooltip>
     );
   }
diff --git a/src/Frontend/Components/AttributionColumn/PackageSubPanel.tsx b/src/Frontend/Components/AttributionColumn/PackageSubPanel.tsx
index 698457e7..57a6d11f 100644
--- a/src/Frontend/Components/AttributionColumn/PackageSubPanel.tsx
+++ b/src/Frontend/Components/AttributionColumn/PackageSubPanel.tsx
@@ -10,7 +10,10 @@ import MuiBox from '@mui/material/Box';
 import { styled } from '@mui/system';
 import { useMemo } from 'react';
 
-import { DisplayPackageInfo, PackageInfo } from '../../../shared/shared-types';
+import {
+  AutocompleteSignal,
+  DisplayPackageInfo,
+} from '../../../shared/shared-types';
 import { text } from '../../../shared/text';
 import { clickableIcon } from '../../shared-styles';
 import { setTemporaryDisplayPackageInfo } from '../../state/actions/resource-actions/all-views-simple-actions';
@@ -73,7 +76,9 @@ export function PackageSubPanel(props: PackageSubPanelProps) {
   const dispatch = useAppDispatch();
   const defaultPackageTypes = useMemo(
     () =>
-      COMMON_PACKAGE_TYPES.map<PackageInfo>((packageType) => ({
+      COMMON_PACKAGE_TYPES.map<AutocompleteSignal>((packageType) => ({
+        attributionIds: [],
+        default: true,
         packageType,
         source: {
           name: text.attributionColumn.commonEcosystems,
diff --git a/src/Frontend/Components/Autocomplete/Autocomplete.tsx b/src/Frontend/Components/Autocomplete/Autocomplete.tsx
index f3b92feb..c1bbcac4 100644
--- a/src/Frontend/Components/Autocomplete/Autocomplete.tsx
+++ b/src/Frontend/Components/Autocomplete/Autocomplete.tsx
@@ -71,7 +71,7 @@ export function Autocomplete<
   groupBy,
   groupProps,
   highlight,
-  inputProps,
+  inputProps: customInputProps,
   multiple,
   optionText,
   renderOptionEndIcon,
@@ -147,10 +147,13 @@ export function Autocomplete<
   const hasClearButton = !disableClearable && !disabled && containsValue;
   const isPopupOpen = !!groupedOptions.length && popupOpen;
 
+  const { ref, color, ...inputProps } = getInputProps();
+
   return (
     <>
       <Container {...getRootProps()} sx={sx} ref={setAnchorEl}>
         <Input
+          {...inputProps}
           disabled={disabled}
           label={title}
           highlight={highlight}
@@ -159,7 +162,8 @@ export function Autocomplete<
           }
           size={'small'}
           InputLabelProps={getInputLabelProps()}
-          inputProps={{ ...getInputProps(), ...inputProps }}
+          inputRef={ref}
+          inputProps={customInputProps}
           InputProps={{
             startAdornment: renderStartAdornment(),
             endAdornment: renderEndAdornment(),
@@ -190,6 +194,7 @@ export function Autocomplete<
           key={key}
           label={label}
           {...tagProps}
+          onMouseDown={(event) => event.stopPropagation()}
           data-testid={`tag-${label}`}
         />
       );
diff --git a/src/Frontend/util/handle-purl.ts b/src/Frontend/util/handle-purl.ts
index 449e5b78..a2461fba 100644
--- a/src/Frontend/util/handle-purl.ts
+++ b/src/Frontend/util/handle-purl.ts
@@ -5,6 +5,7 @@
 import { PackageURL } from 'packageurl-js';
 
 import { PackageInfo } from '../../shared/shared-types';
+import { text } from '../../shared/text';
 
 export function parsePurl(purl: string): PackageURL | undefined {
   try {
@@ -15,14 +16,18 @@ export function parsePurl(purl: string): PackageURL | undefined {
 }
 
 export function generatePurl(packageInfo: PackageInfo): string {
-  return packageInfo.packageType?.trim() && packageInfo.packageName?.trim()
-    ? new PackageURL(
-        packageInfo.packageType.trim(),
-        packageInfo?.packageNamespace?.trim(),
-        packageInfo.packageName.trim(),
-        packageInfo?.packageVersion?.trim(),
-        undefined,
-        undefined,
-      ).toString()
-    : '';
+  try {
+    return packageInfo.packageType?.trim() && packageInfo.packageName?.trim()
+      ? new PackageURL(
+          packageInfo.packageType.trim(),
+          packageInfo?.packageNamespace?.trim(),
+          packageInfo.packageName.trim(),
+          packageInfo?.packageVersion?.trim(),
+          undefined,
+          undefined,
+        ).toString()
+      : '';
+  } catch (error) {
+    return text.attributionColumn.invalidPurl;
+  }
 }
diff --git a/src/Frontend/util/lodash-extension-utils.ts b/src/Frontend/util/lodash-extension-utils.ts
index 917795cb..3aa9d007 100644
--- a/src/Frontend/util/lodash-extension-utils.ts
+++ b/src/Frontend/util/lodash-extension-utils.ts
@@ -2,7 +2,7 @@
 // SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
 //
 // SPDX-License-Identifier: Apache-2.0
-import { remove } from 'lodash';
+import { omit as _omit, pick as _pick, remove } from 'lodash';
 
 import { isIdOfResourceWithChildren } from './can-resource-have-children';
 
@@ -73,3 +73,19 @@ export function isChildOf(parentId: string, possibleChildId: string): boolean {
 
   return possibleChildId.includes(parentId);
 }
+
+/** Improves the lodash pick typing by correctly inferring the type of keys */
+export function pick<T extends object, K extends keyof T>(
+  object: T,
+  keys: Array<K>,
+): Pick<T, K> {
+  return _pick(object, keys);
+}
+
+/** Improves the lodash omit typing by correctly inferring the type of keys */
+export function omit<T extends object, K extends keyof T>(
+  object: T,
+  keys: Array<K>,
+): Omit<T, K> {
+  return _omit(object, keys);
+}
diff --git a/src/Frontend/util/package-search-api.ts b/src/Frontend/util/package-search-api.ts
index 602ba4df..5bc656b0 100644
--- a/src/Frontend/util/package-search-api.ts
+++ b/src/Frontend/util/package-search-api.ts
@@ -3,15 +3,17 @@
 //
 // SPDX-License-Identifier: Apache-2.0
 import { compareVersions, validate } from 'compare-versions';
-import { compact, mapValues, partition, pick } from 'lodash';
+import { compact, mapValues, partition } from 'lodash';
 
 import {
   AutocompleteSignal,
+  DisplayPackageInfo,
   PackageInfo,
   Source,
 } from '../../shared/shared-types';
 import { text } from '../../shared/text';
 import { HttpClient } from './http-client';
+import { pick } from './lodash-extension-utils';
 
 const DEPS_DEV_NON_STANDARD_LICENSE_MARKER = 'non-standard';
 const COPYRIGHT_REGEX = /^Copyright \(c\).*$/gm;
@@ -119,11 +121,6 @@ export interface GitLabLicenseResponse {
   content: string;
 }
 
-export type UrlAndLegal = Pick<
-  PackageInfo,
-  'url' | 'licenseName' | 'copyright' | 'packageVersion'
->;
-
 export class PackageSearchApi {
   private readonly baseUrls = {
     GITHUB: 'https://api.github.com/',
@@ -203,7 +200,7 @@ export class PackageSearchApi {
     version?: string;
     system?: PackageSystem;
     projectType?: ProjectType;
-  }): PackageInfo {
+  }): AutocompleteSignal {
     const type = system || projectType;
     const source: Source = {
       name: text.attributionColumn.openSourceInsights,
@@ -213,6 +210,8 @@ export class PackageSearchApi {
       case 'MAVEN': {
         const [packageNamespace, packageName] = name.split(':');
         return {
+          default: true,
+          attributionIds: [],
           packageName,
           packageNamespace,
           packageType: 'maven',
@@ -225,6 +224,8 @@ export class PackageSearchApi {
       case 'BITBUCKET': {
         const [packageNamespace, packageName] = name.split('/');
         return {
+          default: true,
+          attributionIds: [],
           packageName,
           packageNamespace,
           packageType: type.toLowerCase(),
@@ -235,6 +236,8 @@ export class PackageSearchApi {
       }
       case 'GO': {
         return {
+          default: true,
+          attributionIds: [],
           packageName: name,
           packageNamespace: undefined,
           packageType: 'golang',
@@ -244,6 +247,8 @@ export class PackageSearchApi {
       }
       default: {
         return {
+          default: true,
+          attributionIds: [],
           packageName: name,
           packageNamespace: undefined,
           packageType: type?.toLowerCase(),
@@ -313,11 +318,11 @@ export class PackageSearchApi {
       this.deserialize(props);
 
     if (isComplete && kind && system && name) {
-      return this.getPackageVersions({ kind, name, projectType, system });
+      return this.getSystemPackageVersions({ kind, name, projectType, system });
     }
 
     if (isComplete && kind && projectType && name) {
-      return this.getProjectVersions({
+      return this.getProjectPackageVersions({
         kind,
         name,
         projectType,
@@ -329,7 +334,7 @@ export class PackageSearchApi {
     return Promise.resolve([]);
   }
 
-  private async getPackageVersions({
+  private async getSystemPackageVersions({
     name,
     system,
   }: PackageSuggestion): Promise<Array<AutocompleteSignal>> {
@@ -364,7 +369,7 @@ export class PackageSearchApi {
     ];
   }
 
-  private async getProjectVersions({
+  private async getProjectPackageVersions({
     name,
     projectType,
     system,
@@ -380,10 +385,6 @@ export class PackageSearchApi {
           params: { per_page: 100 },
         });
 
-        if (!response.ok) {
-          return [];
-        }
-
         const tags: Array<TagResponse> = await response.json();
 
         return tags.map((tag) =>
@@ -397,10 +398,6 @@ export class PackageSearchApi {
           params: { search: packageVersion, per_page: 100 },
         });
 
-        if (!response.ok) {
-          return [];
-        }
-
         const tags: Array<TagResponse> = await response.json();
 
         return tags.map((tag) =>
@@ -412,64 +409,53 @@ export class PackageSearchApi {
     }
   }
 
-  public getUrlAndLegal({
-    url,
-    licenseName,
-    packageVersion,
-    copyright,
-    ...props
-  }: PackageInfo): Promise<UrlAndLegal> {
+  public async enrichPackageInfo(
+    packageInfo: DisplayPackageInfo,
+  ): Promise<DisplayPackageInfo> {
+    if (packageInfo.url && packageInfo.copyright && packageInfo.licenseName) {
+      return packageInfo;
+    }
+
     const { isComplete, name, projectType, system, kind } =
-      this.deserialize(props);
+      this.deserialize(packageInfo);
 
     if (isComplete && kind && name && system) {
-      return this.getPackageUrlAndLegal({
-        copyright,
+      return this.enrichSystemPackageInfo({
+        ...packageInfo,
         kind,
-        licenseName,
         name,
-        packageVersion,
         system,
-        url,
       });
     }
 
     if (isComplete && kind && name && projectType) {
-      return this.getProjectUrlAndLegal({
-        copyright,
+      return this.enrichProjectPackageInfo({
+        ...packageInfo,
         kind,
-        licenseName,
         name,
-        packageVersion,
         projectType,
-        url,
       });
     }
 
-    return Promise.resolve({ url, licenseName, copyright, packageVersion });
+    return packageInfo;
   }
 
-  private async getPackageUrlAndLegal({
-    copyright,
+  private async enrichSystemPackageInfo({
     kind,
-    licenseName,
     name,
-    packageVersion,
-    projectType,
     system,
-    url,
-  }: PackageSuggestion & UrlAndLegal): Promise<UrlAndLegal> {
+    ...packageInfo
+  }: PackageSuggestion & DisplayPackageInfo): Promise<DisplayPackageInfo> {
     const effectiveVersion =
-      packageVersion ||
-      (await this.getPackageDefaultVersion({
+      packageInfo.packageVersion ||
+      (await this.getSystemPackageDefaultVersion({
         kind,
         name,
-        projectType,
         system,
       }));
 
     if (!effectiveVersion) {
-      return { url, licenseName, copyright, packageVersion };
+      return packageInfo;
     }
 
     const response = await this.httpClient.request({
@@ -480,25 +466,26 @@ export class PackageSearchApi {
     });
 
     if (!response.ok) {
-      return { url, licenseName, copyright, packageVersion: effectiveVersion };
+      return packageInfo;
     }
 
     const {
       version: { links, licenses },
     }: WebVersionResponse = await response.json();
-    const newUrl = url || links.repo || links.homepage || links.origins?.[0];
-
-    return this.getLegalFromUrl({
-      copyright,
-      licenseName: licenses
-        .filter((license) => license !== DEPS_DEV_NON_STANDARD_LICENSE_MARKER)
-        .join(' AND '),
-      packageVersion: effectiveVersion,
-      url: newUrl,
+
+    return this.enrichPackageInfoViaRepoUrl({
+      ...packageInfo,
+      licenseName:
+        packageInfo.licenseName ||
+        licenses
+          .filter((license) => license !== DEPS_DEV_NON_STANDARD_LICENSE_MARKER)
+          .join(' AND '),
+      url:
+        packageInfo.url || links.repo || links.homepage || links.origins?.[0],
     });
   }
 
-  private async getPackageDefaultVersion({
+  private async getSystemPackageDefaultVersion({
     name,
     system,
   }: PackageSuggestion): Promise<string | undefined> {
@@ -516,85 +503,27 @@ export class PackageSearchApi {
     return defaultVersion;
   }
 
-  private async getProjectUrlAndLegal({
-    copyright,
-    kind,
-    licenseName,
+  private enrichProjectPackageInfo({
     name,
-    packageVersion,
     projectType,
-    system,
-    url,
-  }: ProjectSuggestion & UrlAndLegal): Promise<UrlAndLegal> {
-    const [effectiveVersion, urlAndLegal] = await Promise.all([
-      packageVersion ||
-        this.getProjectDefaultVersion({
-          kind,
-          name,
-          projectType,
-          system,
-        }),
-      this.getLegalFromUrl({
-        copyright,
-        licenseName,
-        packageVersion,
-        url: url || `https://${this.projectTypeDomains[projectType]}/${name}`,
-      }),
-    ]);
-
-    return {
-      ...urlAndLegal,
-      packageVersion: effectiveVersion,
-    };
+    ...packageInfo
+  }: ProjectSuggestion & DisplayPackageInfo): Promise<DisplayPackageInfo> {
+    return this.enrichPackageInfoViaRepoUrl({
+      ...packageInfo,
+      url:
+        packageInfo.url ||
+        `https://${this.projectTypeDomains[projectType]}/${name}`,
+    });
   }
 
-  private async getProjectDefaultVersion({
-    name,
-    projectType,
-  }: ProjectSuggestion): Promise<string | undefined> {
-    switch (projectType) {
-      case 'GITHUB': {
-        const response = await this.httpClient.request({
-          baseUrl: this.baseUrls.GITHUB,
-          path: `repos/${name}/releases/latest`,
-        });
-
-        if (!response.ok) {
-          return undefined;
-        }
-
-        const { tag_name }: LatestReleaseResponse = await response.json();
-
-        return tag_name;
-      }
-      case 'GITLAB': {
-        const response = await this.httpClient.request({
-          baseUrl: this.baseUrls.GITLAB,
-          path: `projects/${encodeURIComponent(
-            name,
-          )}/releases/permalink/latest`,
-        });
-
-        if (!response.ok) {
-          return undefined;
-        }
-
-        const { tag_name }: LatestReleaseResponse = await response.json();
-
-        return tag_name;
-      }
-      default:
-        return undefined;
+  private async enrichPackageInfoViaRepoUrl(
+    packageInfo: DisplayPackageInfo,
+  ): Promise<DisplayPackageInfo> {
+    if (packageInfo.copyright && packageInfo.licenseName) {
+      return packageInfo;
     }
-  }
 
-  private async getLegalFromUrl({
-    copyright,
-    licenseName,
-    packageVersion,
-    url,
-  }: UrlAndLegal): Promise<UrlAndLegal> {
-    const githubMatch = url?.match(GITHUB_REGEX);
+    const githubMatch = packageInfo.url?.match(GITHUB_REGEX);
 
     if (githubMatch) {
       const response = await this.httpClient.request({
@@ -603,20 +532,20 @@ export class PackageSearchApi {
       });
 
       if (!response.ok) {
-        return { url, licenseName, copyright, packageVersion };
+        return packageInfo;
       }
 
       const { license, content }: GitHubLicenseResponse = await response.json();
 
       return {
-        copyright: copyright || this.getCopyrightFromFileContent(content),
-        licenseName: licenseName || license?.name,
-        packageVersion,
-        url,
+        ...packageInfo,
+        copyright:
+          packageInfo.copyright || this.getCopyrightFromFileContent(content),
+        licenseName: packageInfo.licenseName || license?.name,
       };
     }
 
-    const gitlabMatch = url?.match(GITLAB_REGEX);
+    const gitlabMatch = packageInfo.url?.match(GITLAB_REGEX);
 
     if (gitlabMatch) {
       const response = await this.httpClient.request({
@@ -626,7 +555,7 @@ export class PackageSearchApi {
       });
 
       if (!response.ok) {
-        return { url, licenseName, copyright, packageVersion };
+        return packageInfo;
       }
 
       const { license_url: licenseUrl, license }: GitLabProjectResponse =
@@ -643,32 +572,28 @@ export class PackageSearchApi {
 
         if (!licenseResponse.ok) {
           return {
-            url,
-            licenseName: licenseName || license?.name,
-            copyright,
-            packageVersion,
+            ...packageInfo,
+            licenseName: packageInfo.licenseName || license?.name,
           };
         }
 
         const { content }: GitLabLicenseResponse = await licenseResponse.json();
 
         return {
-          copyright: copyright || this.getCopyrightFromFileContent(content),
-          licenseName: licenseName || license?.name,
-          packageVersion,
-          url,
+          ...packageInfo,
+          copyright:
+            packageInfo.copyright || this.getCopyrightFromFileContent(content),
+          licenseName: packageInfo.licenseName || license?.name,
         };
       }
 
       return {
-        url,
-        licenseName: licenseName || license?.name,
-        copyright,
-        packageVersion,
+        ...packageInfo,
+        licenseName: packageInfo.licenseName || license?.name,
       };
     }
 
-    return { url, licenseName, copyright, packageVersion };
+    return packageInfo;
   }
 
   private getCopyrightFromFileContent(content: string) {
diff --git a/src/Frontend/util/package-search-hooks.ts b/src/Frontend/util/package-search-hooks.ts
index 17e8d61b..d6d725f4 100644
--- a/src/Frontend/util/package-search-hooks.ts
+++ b/src/Frontend/util/package-search-hooks.ts
@@ -4,7 +4,7 @@
 // SPDX-License-Identifier: Apache-2.0
 import { useMutation, useQuery } from '@tanstack/react-query';
 
-import { PackageInfo } from '../../shared/shared-types';
+import { DisplayPackageInfo } from '../../shared/shared-types';
 import PackageSearchApi from './package-search-api';
 import { tryit } from './tryit';
 
@@ -12,7 +12,7 @@ function usePackageNames({
   packageName,
   packageNamespace,
   packageType,
-}: PackageInfo) {
+}: DisplayPackageInfo) {
   const { data, error, isLoading } = useQuery({
     queryKey: [
       'package-name-suggestions',
@@ -35,7 +35,7 @@ function usePackageNamespaces({
   packageName,
   packageNamespace,
   packageType,
-}: PackageInfo) {
+}: DisplayPackageInfo) {
   const { data, error, isLoading } = useQuery({
     queryKey: [
       'package-namespace-suggestions',
@@ -63,7 +63,7 @@ function usePackageVersions({
   packageNamespace,
   packageType,
   packageVersion,
-}: PackageInfo) {
+}: DisplayPackageInfo) {
   const { data, error, isLoading } = useQuery({
     queryKey: [
       'package-version-suggestions',
@@ -88,20 +88,21 @@ function usePackageVersions({
   };
 }
 
-function useGetPackageUrlAndLegal() {
+function useEnrichPackageInfo() {
   const { mutateAsync, error, isPending } = useMutation({
-    mutationFn: (props: PackageInfo) => PackageSearchApi.getUrlAndLegal(props),
+    mutationFn: (props: DisplayPackageInfo) =>
+      PackageSearchApi.enrichPackageInfo(props),
   });
 
   return {
-    getPackageUrlAndLicense: tryit(mutateAsync),
-    getPackageUrlAndLicenseError: error,
-    getPackageUrlAndLicenseLoading: isPending,
+    enrichPackageInfo: tryit(mutateAsync),
+    enrichPackageInfoError: error,
+    enrichPackageInfoLoading: isPending,
   };
 }
 
 export const PackageSearchHooks = {
-  useGetPackageUrlAndLegal,
+  useEnrichPackageInfo,
   usePackageNames,
   usePackageNamespaces,
   usePackageVersions,
diff --git a/src/Frontend/web-workers/scripts/get-autocomplete-signals.ts b/src/Frontend/web-workers/scripts/get-autocomplete-signals.ts
index 28860b34..4f7508ef 100644
--- a/src/Frontend/web-workers/scripts/get-autocomplete-signals.ts
+++ b/src/Frontend/web-workers/scripts/get-autocomplete-signals.ts
@@ -57,7 +57,7 @@ export function getAutocompleteSignals({
     ...signalsOnResource,
     ...signalsOnChildren,
     ...attributionsOnChildren,
-  ].reduce<Array<AutocompleteSignal>>((acc, signal) => {
+  ].reduce<Array<AutocompleteSignal>>((acc, { comment, ...signal }) => {
     if (!generatePurl(signal) || signal.preferred) {
       return acc;
     }
@@ -67,7 +67,9 @@ export function getAutocompleteSignals({
 
     if (dupeIndex === -1) {
       acc.push({
+        attributionIds: [],
         count: 1,
+        comments: comment ? [comment] : undefined,
         ...signal,
       });
     } else {
diff --git a/src/shared/shared-types.ts b/src/shared/shared-types.ts
index b271cd4b..92a56bdf 100644
--- a/src/shared/shared-types.ts
+++ b/src/shared/shared-types.ts
@@ -278,7 +278,8 @@ export interface UserSettings {
   qaMode: boolean;
 }
 
-export type AutocompleteSignal = PackageInfo & {
+export type AutocompleteSignal = DisplayPackageInfo & {
   count?: number;
   suffix?: string;
+  default?: boolean;
 };
diff --git a/src/shared/text.ts b/src/shared/text.ts
index c44a92d9..2aedb610 100644
--- a/src/shared/text.ts
+++ b/src/shared/text.ts
@@ -32,6 +32,7 @@ export const text = {
     useAutocompleteSuggestion:
       'Adopt all coordinates and legal information from suggestion',
     originallyFrom: 'Originally from ',
+    invalidPurl: 'INVALID PURL',
   },
   changePreferredStatusGloballyPopup: {
     markAsPreferred: 'Do you really want to prefer the attribution globally?',


Test patch:
diff --git a/src/Frontend/util/__tests__/package-search-api.test.ts b/src/Frontend/util/__tests__/package-search-api.test.ts
index 3720eee3..17ab966d 100644
--- a/src/Frontend/util/__tests__/package-search-api.test.ts
+++ b/src/Frontend/util/__tests__/package-search-api.test.ts
@@ -5,14 +5,16 @@
 import { difference } from 'lodash';
 
 import { faker } from '../../../shared/Faker';
-import { AutocompleteSignal } from '../../../shared/shared-types';
+import {
+  AutocompleteSignal,
+  DisplayPackageInfo,
+} from '../../../shared/shared-types';
 import { text } from '../../../shared/text';
 import { RequestProps } from '../http-client';
 import {
   PackageSearchApi,
   packageSystems,
   packageSystemsRequiringNamespace,
-  UrlAndLegal,
 } from '../package-search-api';
 
 describe('PackageSearchApi', () => {
@@ -36,7 +38,7 @@ describe('PackageSearchApi', () => {
 
       expect(httpClient.request).toHaveBeenCalledTimes(1);
       expect(searchResults).toEqual([
-        expect.objectContaining<AutocompleteSignal>({
+        expect.objectContaining<Partial<AutocompleteSignal>>({
           packageName: name,
           packageNamespace: namespace,
           packageType: 'github',
@@ -64,7 +66,7 @@ describe('PackageSearchApi', () => {
 
       expect(httpClient.request).toHaveBeenCalledTimes(1);
       expect(searchResults).toEqual([
-        expect.objectContaining<AutocompleteSignal>({
+        expect.objectContaining<Partial<AutocompleteSignal>>({
           packageName: name,
           packageNamespace: namespace,
           packageType: 'maven',
@@ -88,7 +90,7 @@ describe('PackageSearchApi', () => {
 
       expect(httpClient.request).toHaveBeenCalledTimes(1);
       expect(searchResults).toEqual([
-        expect.objectContaining<AutocompleteSignal>({
+        expect.objectContaining<Partial<AutocompleteSignal>>({
           packageName: projectSuggestion.name,
           packageNamespace: undefined,
           packageType: 'golang',
@@ -112,7 +114,7 @@ describe('PackageSearchApi', () => {
 
       expect(httpClient.request).toHaveBeenCalledTimes(1);
       expect(searchResults).toEqual([
-        expect.objectContaining<AutocompleteSignal>({
+        expect.objectContaining<Partial<AutocompleteSignal>>({
           packageName: projectSuggestion.name,
           packageNamespace: undefined,
           packageType: 'npm',
@@ -276,8 +278,8 @@ describe('PackageSearchApi', () => {
       const versions = await packageSearchApi.getVersions(packageInfo);
 
       expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(versions).toEqual<Array<AutocompleteSignal>>([
-        {
+      expect(versions).toEqual([
+        expect.objectContaining<Partial<AutocompleteSignal>>({
           packageName: defaultVersion.versionKey.name,
           packageType: defaultVersion.versionKey.system.toLowerCase(),
           packageVersion: defaultVersion.versionKey.version,
@@ -286,8 +288,8 @@ describe('PackageSearchApi', () => {
             documentConfidence: 100,
           },
           suffix: '(default)',
-        },
-        {
+        }),
+        expect.objectContaining<Partial<AutocompleteSignal>>({
           packageName: nonDefaultVersion2.versionKey.name,
           packageType: nonDefaultVersion2.versionKey.system.toLowerCase(),
           packageVersion: nonDefaultVersion2.versionKey.version,
@@ -295,8 +297,8 @@ describe('PackageSearchApi', () => {
             name: text.attributionColumn.openSourceInsights,
             documentConfidence: 100,
           },
-        },
-        {
+        }),
+        expect.objectContaining<Partial<AutocompleteSignal>>({
           packageName: nonDefaultVersion1.versionKey.name,
           packageType: nonDefaultVersion1.versionKey.system.toLowerCase(),
           packageVersion: nonDefaultVersion1.versionKey.version,
@@ -304,7 +306,7 @@ describe('PackageSearchApi', () => {
             name: text.attributionColumn.openSourceInsights,
             documentConfidence: 100,
           },
-        },
+        }),
       ]);
     });
 
@@ -325,8 +327,8 @@ describe('PackageSearchApi', () => {
       const versions = await packageSearchApi.getVersions(packageInfo);
 
       expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(versions).toEqual<Array<AutocompleteSignal>>([
-        {
+      expect(versions).toEqual([
+        expect.objectContaining<Partial<AutocompleteSignal>>({
           packageName,
           packageNamespace,
           packageType: 'github',
@@ -336,7 +338,7 @@ describe('PackageSearchApi', () => {
             documentConfidence: 100,
           },
           url: `https://github.com/${packageNamespace}/${packageName}`,
-        },
+        }),
       ]);
     });
 
@@ -357,8 +359,8 @@ describe('PackageSearchApi', () => {
       const versions = await packageSearchApi.getVersions(packageInfo);
 
       expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(versions).toEqual<Array<AutocompleteSignal>>([
-        {
+      expect(versions).toEqual([
+        expect.objectContaining<Partial<AutocompleteSignal>>({
           packageName,
           packageNamespace,
           packageType: 'gitlab',
@@ -368,7 +370,7 @@ describe('PackageSearchApi', () => {
             documentConfidence: 100,
           },
           url: `https://gitlab.com/${packageNamespace}/${packageName}`,
-        },
+        }),
       ]);
     });
 
@@ -431,9 +433,9 @@ describe('PackageSearchApi', () => {
     });
   });
 
-  describe('getUrlAndLicense', () => {
+  describe('enrichPackageInfo', () => {
     it('provides repo URL and license for a given package version', async () => {
-      const packageInfo = faker.opossum.externalPackageInfo({
+      const packageInfo = faker.opossum.displayPackageInfo({
         packageType: faker.packageSearch.packageSystem(),
         packageNamespace: faker.internet.domainWord(),
         url: undefined,
@@ -452,11 +454,11 @@ describe('PackageSearchApi', () => {
       );
       const packageSearchApi = new PackageSearchApi(httpClient);
 
-      const urlAndLegal = await packageSearchApi.getUrlAndLegal(packageInfo);
+      const enriched = await packageSearchApi.enrichPackageInfo(packageInfo);
 
       expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(urlAndLegal).toEqual(
-        expect.objectContaining<UrlAndLegal>({
+      expect(enriched).toEqual(
+        expect.objectContaining<Partial<DisplayPackageInfo>>({
           url: repoUrl,
           licenseName: `${license1} AND ${license2}`,
         }),
@@ -464,7 +466,7 @@ describe('PackageSearchApi', () => {
     });
 
     it('falls back to homepage URL when repo URL not available', async () => {
-      const packageInfo = faker.opossum.externalPackageInfo({
+      const packageInfo = faker.opossum.displayPackageInfo({
         packageType: faker.packageSearch.packageSystem(),
         packageNamespace: faker.internet.domainWord(),
         url: undefined,
@@ -481,11 +483,11 @@ describe('PackageSearchApi', () => {
       );
       const packageSearchApi = new PackageSearchApi(httpClient);
 
-      const urlAndLegal = await packageSearchApi.getUrlAndLegal(packageInfo);
+      const enriched = await packageSearchApi.enrichPackageInfo(packageInfo);
 
       expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(urlAndLegal).toEqual(
-        expect.objectContaining<UrlAndLegal>({
+      expect(enriched).toEqual(
+        expect.objectContaining<Partial<DisplayPackageInfo>>({
           url: homepageUrl,
           licenseName: '',
         }),
@@ -493,7 +495,7 @@ describe('PackageSearchApi', () => {
     });
 
     it('falls back to first origin URL when neither repo URL nor homepage URL are available', async () => {
-      const packageInfo = faker.opossum.externalPackageInfo({
+      const packageInfo = faker.opossum.displayPackageInfo({
         packageType: faker.packageSearch.packageSystem(),
         packageNamespace: faker.internet.domainWord(),
         url: undefined,
@@ -512,11 +514,11 @@ describe('PackageSearchApi', () => {
       );
       const packageSearchApi = new PackageSearchApi(httpClient);
 
-      const urlAndLegal = await packageSearchApi.getUrlAndLegal(packageInfo);
+      const enriched = await packageSearchApi.enrichPackageInfo(packageInfo);
 
       expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(urlAndLegal).toEqual(
-        expect.objectContaining<UrlAndLegal>({
+      expect(enriched).toEqual(
+        expect.objectContaining<Partial<DisplayPackageInfo>>({
           url: originUrl,
           licenseName: '',
         }),
@@ -524,10 +526,26 @@ describe('PackageSearchApi', () => {
     });
   });
 
+  it('does not enrich package info if URL, copyright, and license name are already known', async () => {
+    const packageInfo = faker.opossum.displayPackageInfo({
+      packageType: faker.packageSearch.packageSystem(),
+      packageNamespace: faker.internet.domainWord(),
+    });
+    const httpClient = faker.httpClient();
+    const packageSearchApi = new PackageSearchApi(httpClient);
+
+    await packageSearchApi.enrichPackageInfo(packageInfo);
+
+    expect(httpClient.request).not.toHaveBeenCalled();
+  });
+
   it('gets license based on package default version if no package version provided', async () => {
-    const packageInfo = faker.opossum.externalPackageInfo({
+    const packageInfo = faker.opossum.displayPackageInfo({
       packageType: faker.packageSearch.packageSystem(),
       packageNamespace: faker.internet.domainWord(),
+      url: undefined,
+      licenseName: undefined,
+      copyright: undefined,
       packageVersion: undefined,
     });
     const defaultVersion = faker.system.semver();
@@ -537,7 +555,7 @@ describe('PackageSearchApi', () => {
     );
     const packageSearchApi = new PackageSearchApi(httpClient);
 
-    await packageSearchApi.getUrlAndLegal(packageInfo);
+    await packageSearchApi.enrichPackageInfo(packageInfo);
 
     expect(httpClient.request).toHaveBeenCalledTimes(2);
     expect(httpClient.request).toHaveBeenNthCalledWith(
@@ -548,50 +566,10 @@ describe('PackageSearchApi', () => {
     );
   });
 
-  it("gets GitHub project's latest tag as default version if no package version provided", async () => {
-    const packageInfo = faker.opossum.externalPackageInfo({
-      packageType: 'github',
-      packageNamespace: faker.internet.domainWord(),
-      packageVersion: undefined,
-    });
-    const tagName = faker.system.semver();
-    const httpClient = faker.httpClient(
-      faker.packageSearch.latestReleaseResponse({ tag_name: tagName }),
-      faker.packageSearch.webVersionResponse(),
-    );
-    const packageSearchApi = new PackageSearchApi(httpClient);
-
-    const { packageVersion } =
-      await packageSearchApi.getUrlAndLegal(packageInfo);
-
-    expect(httpClient.request).toHaveBeenCalledTimes(1);
-    expect(packageVersion).toBe(tagName);
-  });
-
-  it("gets GitLab project's latest tag as default version if no package version provided", async () => {
-    const packageInfo = faker.opossum.externalPackageInfo({
-      packageType: 'gitlab',
-      packageNamespace: faker.internet.domainWord(),
-      packageVersion: undefined,
-    });
-    const tagName = faker.system.semver();
-    const httpClient = faker.httpClient(
-      faker.packageSearch.latestReleaseResponse({ tag_name: tagName }),
-      faker.packageSearch.webVersionResponse(),
-    );
-    const packageSearchApi = new PackageSearchApi(httpClient);
-
-    const { packageVersion } =
-      await packageSearchApi.getUrlAndLegal(packageInfo);
-
-    expect(httpClient.request).toHaveBeenCalledTimes(1);
-    expect(packageVersion).toBe(tagName);
-  });
-
   it('provides repo URL, license, and copyright for a GitHub project', async () => {
     const name = faker.internet.domainWord();
     const namespace = faker.internet.domainWord();
-    const packageInfo = faker.opossum.externalPackageInfo({
+    const packageInfo = faker.opossum.displayPackageInfo({
       packageType: 'github',
       packageName: name,
       packageNamespace: namespace,
@@ -609,21 +587,23 @@ describe('PackageSearchApi', () => {
     );
     const packageSearchApi = new PackageSearchApi(httpClient);
 
-    const urlAndLegal = await packageSearchApi.getUrlAndLegal(packageInfo);
+    const enriched = await packageSearchApi.enrichPackageInfo(packageInfo);
 
     expect(httpClient.request).toHaveBeenCalledTimes(1);
-    expect(urlAndLegal).toEqual<UrlAndLegal>({
-      url: `https://github.com/${namespace}/${name}`,
-      licenseName: license,
-      packageVersion: packageInfo.packageVersion,
-      copyright,
-    });
+    expect(enriched).toEqual(
+      expect.objectContaining<Partial<DisplayPackageInfo>>({
+        url: `https://github.com/${namespace}/${name}`,
+        licenseName: license,
+        packageVersion: packageInfo.packageVersion,
+        copyright,
+      }),
+    );
   });
 
   it('provides repo URL, license, and copyright for a GitLab project', async () => {
     const name = faker.internet.domainWord();
     const namespace = faker.internet.domainWord();
-    const packageInfo = faker.opossum.externalPackageInfo({
+    const packageInfo = faker.opossum.displayPackageInfo({
       packageType: 'gitlab',
       packageName: name,
       packageNamespace: namespace,
@@ -643,19 +623,21 @@ describe('PackageSearchApi', () => {
     );
     const packageSearchApi = new PackageSearchApi(httpClient);
 
-    const urlAndLegal = await packageSearchApi.getUrlAndLegal(packageInfo);
+    const enriched = await packageSearchApi.enrichPackageInfo(packageInfo);
 
     expect(httpClient.request).toHaveBeenCalledTimes(2);
-    expect(urlAndLegal).toEqual<UrlAndLegal>({
-      url: `https://gitlab.com/${namespace}/${name}`,
-      licenseName: license,
-      packageVersion: packageInfo.packageVersion,
-      copyright,
-    });
+    expect(enriched).toEqual(
+      expect.objectContaining<Partial<DisplayPackageInfo>>({
+        url: `https://gitlab.com/${namespace}/${name}`,
+        licenseName: license,
+        packageVersion: packageInfo.packageVersion,
+        copyright,
+      }),
+    );
   });
 
   it('does not make any request if URL, license name, and copyright of a project are already known', async () => {
-    const packageInfo = faker.opossum.externalPackageInfo({
+    const packageInfo = faker.opossum.displayPackageInfo({
       packageType: faker.packageSearch.projectType(),
       packageNamespace: faker.internet.domainWord(),
       url: faker.internet.url(),
@@ -665,7 +647,7 @@ describe('PackageSearchApi', () => {
     const httpClient = faker.httpClient();
     const packageSearchApi = new PackageSearchApi(httpClient);
 
-    await packageSearchApi.getUrlAndLegal(packageInfo);
+    await packageSearchApi.enrichPackageInfo(packageInfo);
 
     expect(httpClient.request).not.toHaveBeenCalled();
   });
diff --git a/src/Frontend/web-workers/scripts/__tests__/get-autocomplete-signals.test.ts b/src/Frontend/web-workers/scripts/__tests__/get-autocomplete-signals.test.ts
index dd6d0861..f7afc83e 100644
--- a/src/Frontend/web-workers/scripts/__tests__/get-autocomplete-signals.test.ts
+++ b/src/Frontend/web-workers/scripts/__tests__/get-autocomplete-signals.test.ts
@@ -4,6 +4,7 @@
 // SPDX-License-Identifier: Apache-2.0
 import { faker } from '../../../../shared/Faker';
 import { AutocompleteSignal } from '../../../../shared/shared-types';
+import { EMPTY_DISPLAY_PACKAGE_INFO } from '../../../shared-constants';
 import { getAutocompleteSignals } from '../get-autocomplete-signals';
 
 describe('getAutocompleteSignals', () => {
@@ -60,7 +61,7 @@ describe('getAutocompleteSignals', () => {
 
     expect(signals).toHaveLength(1);
     expect(signals).toEqual<Array<AutocompleteSignal>>([
-      { ...attribution1, count: 2 },
+      { ...EMPTY_DISPLAY_PACKAGE_INFO, ...attribution1, count: 2 },
     ]);
   });
 
@@ -155,8 +156,8 @@ describe('getAutocompleteSignals', () => {
 
     expect(signals).toHaveLength(2);
     expect(signals).toEqual<Array<AutocompleteSignal>>([
-      { ...attribution1, count: 2 },
-      { ...attribution2, count: 1 },
+      { ...EMPTY_DISPLAY_PACKAGE_INFO, ...attribution1, count: 2 },
+      { ...EMPTY_DISPLAY_PACKAGE_INFO, ...attribution2, count: 1 },
     ]);
   });
 
@@ -206,8 +207,8 @@ describe('getAutocompleteSignals', () => {
 
     expect(signals).toHaveLength(2);
     expect(signals).toEqual<Array<AutocompleteSignal>>([
-      { ...attribution2, count: 1 },
-      { ...attribution1, count: 1 },
+      { ...EMPTY_DISPLAY_PACKAGE_INFO, ...attribution2, count: 1 },
+      { ...EMPTY_DISPLAY_PACKAGE_INFO, ...attribution1, count: 1 },
     ]);
   });
 
@@ -262,8 +263,8 @@ describe('getAutocompleteSignals', () => {
 
     expect(signals).toHaveLength(2);
     expect(signals).toEqual<Array<AutocompleteSignal>>([
-      { ...attribution2, count: 1 },
-      { ...attribution1, count: 1 },
+      { ...EMPTY_DISPLAY_PACKAGE_INFO, ...attribution2, count: 1 },
+      { ...EMPTY_DISPLAY_PACKAGE_INFO, ...attribution1, count: 1 },
     ]);
   });
 });