Repository: ansible-collections/community.general

Issue:
pacman: Can't install from local file
### Summary

When trying to install a package from a local file, pacman fails with `target not found`. This happens because the `--sync` operations is used, whereas for local files, the `--upgrade` operation is necessary instead. Introduced in https://github.com/ansible-collections/community.general/pull/3907.

### Issue Type

Bug Report

### Component Name

pacman

### Ansible Version

```console (paste below)
$ ansible --version
ansible [core 2.12.2]
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/home/fotis/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.10/site-packages/ansible
  ansible collection location = /home/fotis/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/bin/ansible
  python version = 3.10.2 (main, Jan 15 2022, 19:56:27) [GCC 11.1.0]
  jinja version = 3.0.3
  libyaml = True
```


### Community.general Version

```console (paste below)
$ ansible-galaxy collection list community.general
# /usr/lib/python3.10/site-packages/ansible_collections
Collection        Version
----------------- -------
community.general 4.5.0
```


### Configuration

_No response_

### OS / Environment

_No response_

### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)
# From the module examples
- name: Install package bar from file
  community.general.pacman:
    name: ~/bar-1.0-1-any.pkg.tar.xz
    state: present
```


### Expected Results

I expected to install package bar contained in `~/bar-1.0-1-any.pkg.tar.xz`, but it fails.

### Actual Results

```console (paste below)

```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/modules/packaging/os/pacman.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/packaging/os/pacman.py)

If these files are incorrect, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @indrajitr @tchernomax
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
CC @jraby
I'll have a look.
@foxeng  could you give this a spin?  https://github.com/ansible-collections/community.general/pull/4286

I do not have time to finish the PR right now, but I think it is in working shape.
It needs tests and I'm not super happy with how the code looks afterwards, but hey at least it works.

https://github.com/ansible-collections/community.general/pull/4286 should fix this and it is now in a better shape than yesterday :-)

If you can give it a try and confirm it works for your usecase, it would be great.

Thanks and sorry for the breakage...
Sorry for the late response, the second iteration worked like a charm! (ftr, the first one still had trouble, seemed to treat all packages as local, didn't get to the bottom of it though). Thank you very much for the prompt fix!

Feel free to (auto)close the issue when the PR is merged.

Resolving patch:
diff --git a/plugins/modules/packaging/os/pacman.py b/plugins/modules/packaging/os/pacman.py
--- a/plugins/modules/packaging/os/pacman.py
+++ b/plugins/modules/packaging/os/pacman.py
@@ -190,7 +190,22 @@
 from collections import defaultdict, namedtuple
 
 
-Package = namedtuple("Package", ["name", "source"])
+class Package(object):
+    def __init__(self, name, source, source_is_URL=False):
+        self.name = name
+        self.source = source
+        self.source_is_URL = source_is_URL
+
+    def __eq__(self, o):
+        return self.name == o.name and self.source == o.source and self.source_is_URL == o.source_is_URL
+
+    def __lt__(self, o):
+        return self.name < o.name
+
+    def __repr__(self):
+        return 'Package("%s", "%s", %s)' % (self.name, self.source, self.source_is_URL)
+
+
 VersionTuple = namedtuple("VersionTuple", ["current", "latest"])
 
 
@@ -273,7 +288,13 @@ def run(self):
 
     def install_packages(self, pkgs):
         pkgs_to_install = []
+        pkgs_to_install_from_url = []
         for p in pkgs:
+            if p.source_is_URL:
+                # URL packages bypass the latest / upgradable_pkgs test
+                # They go through the dry-run to let pacman decide if they will be installed
+                pkgs_to_install_from_url.append(p)
+                continue
             if (
                 p.name not in self.inventory["installed_pkgs"]
                 or self.target_state == "latest"
@@ -281,14 +302,12 @@ def install_packages(self, pkgs):
             ):
                 pkgs_to_install.append(p)
 
-        if len(pkgs_to_install) == 0:
+        if len(pkgs_to_install) == 0 and len(pkgs_to_install_from_url) == 0:
             self.add_exit_infos("package(s) already installed")
             return
 
-        self.changed = True
         cmd_base = [
             self.pacman_path,
-            "--sync",
             "--noconfirm",
             "--noprogressbar",
             "--needed",
@@ -296,45 +315,78 @@ def install_packages(self, pkgs):
         if self.m.params["extra_args"]:
             cmd_base.extend(self.m.params["extra_args"])
 
-        # Dry run first to gather what will be done
-        cmd = cmd_base + ["--print-format", "%n %v"] + [p.source for p in pkgs_to_install]
-        rc, stdout, stderr = self.m.run_command(cmd, check_rc=False)
-        if rc != 0:
-            self.fail("Failed to list package(s) to install", stdout=stdout, stderr=stderr)
+        def _build_install_diff(pacman_verb, pkglist):
+            # Dry run to build the installation diff
+
+            cmd = cmd_base + [pacman_verb, "--print-format", "%n %v"] + [p.source for p in pkglist]
+            rc, stdout, stderr = self.m.run_command(cmd, check_rc=False)
+            if rc != 0:
+                self.fail("Failed to list package(s) to install", cmd=cmd, stdout=stdout, stderr=stderr)
+
+            name_ver = [l.strip() for l in stdout.splitlines()]
+            before = []
+            after = []
+            to_be_installed = []
+            for p in name_ver:
+                # With Pacman v6.0.1 - libalpm v13.0.1, --upgrade outputs "loading packages..." on stdout. strip that.
+                # When installing from URLs, pacman can also output a 'nothing to do' message. strip that too.
+                if "loading packages" in p or 'there is nothing to do' in p:
+                    continue
+                name, version = p.split()
+                if name in self.inventory["installed_pkgs"]:
+                    before.append("%s-%s" % (name, self.inventory["installed_pkgs"][name]))
+                after.append("%s-%s" % (name, version))
+                to_be_installed.append(name)
+
+            return (to_be_installed, before, after)
 
-        name_ver = [l.strip() for l in stdout.splitlines()]
         before = []
         after = []
         installed_pkgs = []
-        self.exit_params["packages"] = []
-        for p in name_ver:
-            name, version = p.split()
-            if name in self.inventory["installed_pkgs"]:
-                before.append("%s-%s" % (name, self.inventory["installed_pkgs"][name]))
-            after.append("%s-%s" % (name, version))
-            installed_pkgs.append(name)
+
+        if pkgs_to_install:
+            p, b, a = _build_install_diff("--sync", pkgs_to_install)
+            installed_pkgs.extend(p)
+            before.extend(b)
+            after.extend(a)
+        if pkgs_to_install_from_url:
+            p, b, a = _build_install_diff("--upgrade", pkgs_to_install_from_url)
+            installed_pkgs.extend(p)
+            before.extend(b)
+            after.extend(a)
+
+        if len(installed_pkgs) == 0:
+            # This can happen with URL packages if pacman decides there's nothing to do
+            self.add_exit_infos("package(s) already installed")
+            return
+
+        self.changed = True
 
         self.exit_params["diff"] = {
-            "before": "\n".join(before) + "\n" if before else "",
-            "after": "\n".join(after) + "\n" if after else "",
+            "before": "\n".join(sorted(before)) + "\n" if before else "",
+            "after": "\n".join(sorted(after)) + "\n" if after else "",
         }
 
         if self.m.check_mode:
             self.add_exit_infos("Would have installed %d packages" % len(installed_pkgs))
-            self.exit_params["packages"] = installed_pkgs
+            self.exit_params["packages"] = sorted(installed_pkgs)
             return
 
         # actually do it
-        cmd = cmd_base + [p.source for p in pkgs_to_install]
+        def _install_packages_for_real(pacman_verb, pkglist):
+            cmd = cmd_base + [pacman_verb] + [p.source for p in pkglist]
+            rc, stdout, stderr = self.m.run_command(cmd, check_rc=False)
+            if rc != 0:
+                self.fail("Failed to install package(s)", cmd=cmd, stdout=stdout, stderr=stderr)
+            self.add_exit_infos(stdout=stdout, stderr=stderr)
 
-        rc, stdout, stderr = self.m.run_command(cmd, check_rc=False)
-        if rc != 0:
-            self.fail("Failed to install package(s)", stdout=stdout, stderr=stderr)
+        if pkgs_to_install:
+            _install_packages_for_real("--sync", pkgs_to_install)
+        if pkgs_to_install_from_url:
+            _install_packages_for_real("--upgrade", pkgs_to_install_from_url)
 
         self.exit_params["packages"] = installed_pkgs
-        self.add_exit_infos(
-            "Installed %d package(s)" % len(installed_pkgs), stdout=stdout, stderr=stderr
-        )
+        self.add_exit_infos("Installed %d package(s)" % len(installed_pkgs))
 
     def remove_packages(self, pkgs):
         force_args = ["--nodeps", "--nodeps"] if self.m.params["force"] else []
@@ -362,7 +414,7 @@ def remove_packages(self, pkgs):
 
         rc, stdout, stderr = self.m.run_command(cmd, check_rc=False)
         if rc != 0:
-            self.fail("failed to list package(s) to remove", stdout=stdout, stderr=stderr)
+            self.fail("failed to list package(s) to remove", cmd=cmd, stdout=stdout, stderr=stderr)
 
         removed_pkgs = stdout.split()
         self.exit_params["packages"] = removed_pkgs
@@ -381,7 +433,7 @@ def remove_packages(self, pkgs):
 
         rc, stdout, stderr = self.m.run_command(cmd, check_rc=False)
         if rc != 0:
-            self.fail("failed to remove package(s)", stdout=stdout, stderr=stderr)
+            self.fail("failed to remove package(s)", cmd=cmd, stdout=stdout, stderr=stderr)
         self.exit_params["packages"] = removed_pkgs
         self.add_exit_infos("Removed %d package(s)" % len(removed_pkgs), stdout=stdout, stderr=stderr)
 
@@ -420,7 +472,7 @@ def upgrade(self):
             if rc == 0:
                 self.add_exit_infos("System upgraded", stdout=stdout, stderr=stderr)
             else:
-                self.fail("Could not upgrade", stdout=stdout, stderr=stderr)
+                self.fail("Could not upgrade", cmd=cmd, stdout=stdout, stderr=stderr)
 
     def update_package_db(self):
         """runs pacman --sync --refresh"""
@@ -446,7 +498,7 @@ def update_package_db(self):
         if rc == 0:
             self.add_exit_infos("Updated package db", stdout=stdout, stderr=stderr)
         else:
-            self.fail("could not update package db", stdout=stdout, stderr=stderr)
+            self.fail("could not update package db", cmd=cmd, stdout=stdout, stderr=stderr)
 
     def package_list(self):
         """Takes the input package list and resolves packages groups to their package list using the inventory,
@@ -459,6 +511,7 @@ def package_list(self):
             if not pkg:
                 continue
 
+            is_URL = False
             if pkg in self.inventory["available_groups"]:
                 # Expand group members
                 for group_member in self.inventory["available_groups"][pkg]:
@@ -488,8 +541,11 @@ def package_list(self):
                                 stderr=stderr,
                                 rc=rc,
                             )
+                    # With Pacman v6.0.1 - libalpm v13.0.1, --upgrade outputs "loading packages..." on stdout. strip that
+                    stdout = stdout.replace("loading packages...\n", "")
+                    is_URL = True
                 pkg_name = stdout.strip()
-                pkg_list.append(Package(name=pkg_name, source=pkg))
+                pkg_list.append(Package(name=pkg_name, source=pkg, source_is_URL=is_URL))
 
         return pkg_list
 


Test patch:
diff --git a/tests/integration/targets/pacman/tasks/main.yml b/tests/integration/targets/pacman/tasks/main.yml
--- a/tests/integration/targets/pacman/tasks/main.yml
+++ b/tests/integration/targets/pacman/tasks/main.yml
@@ -8,3 +8,4 @@
   block:
     # Add more tests here by including more task files:
     - include: 'basic.yml'
+    - include: 'package_urls.yml'
diff --git a/tests/integration/targets/pacman/tasks/package_urls.yml b/tests/integration/targets/pacman/tasks/package_urls.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/pacman/tasks/package_urls.yml
@@ -0,0 +1,125 @@
+---
+- vars:
+    reg_pkg: ed
+    url_pkg: lemon
+    file_pkg: hdparm
+    file_pkg_path: /tmp/pkg.zst
+    extra_pkg: core/sdparm
+    extra_pkg_outfmt: sdparm
+  block:
+    - name: Make sure that test packages are not installed
+      pacman:
+        name:
+          - '{{reg_pkg}}'
+          - '{{url_pkg}}'
+          - '{{file_pkg}}'
+          - '{{extra_pkg}}'
+        state: absent
+
+    - name: Get URL for {{url_pkg}}
+      command:
+        cmd: pacman --sync --print-format "%l" {{url_pkg}}
+      register: url_pkg_url
+
+    - name: Get URL for {{file_pkg}}
+      command:
+        cmd: pacman --sync --print-format "%l" {{file_pkg}}
+      register: file_pkg_url
+    - name: Download {{file_pkg}} pkg
+      get_url:
+        url: '{{file_pkg_url.stdout}}'
+        dest: '{{file_pkg_path}}'
+
+    - name: Install packages from mixed sources (check mode)
+      pacman:
+        name:
+          - '{{reg_pkg}}'
+          - '{{url_pkg_url.stdout}}'
+          - '{{file_pkg_path}}'
+      check_mode: True
+      register: install_1
+
+    - name: Install packages from mixed sources
+      pacman:
+        name:
+          - '{{reg_pkg}}'
+          - '{{url_pkg_url.stdout}}'
+          - '{{file_pkg_path}}'
+      register: install_2
+
+    - name: Install packages from mixed sources - (idempotency)
+      pacman:
+        name:
+          - '{{reg_pkg}}'
+          - '{{url_pkg_url.stdout}}'
+          - '{{file_pkg_path}}'
+      register: install_3
+
+    - name: Install packages with their regular names (idempotency)
+      pacman:
+        name:
+          - '{{reg_pkg}}'
+          - '{{url_pkg}}'
+          - '{{file_pkg}}'
+      register: install_4
+
+    - name: Install new package with already installed packages from mixed sources
+      pacman:
+        name:
+          - '{{reg_pkg}}'
+          - '{{url_pkg_url.stdout}}'
+          - '{{file_pkg_path}}'
+          - '{{extra_pkg}}'
+      register: install_5
+
+    - name: Uninstall packages - mixed sources (check mode)
+      pacman:
+        state: absent
+        name:
+          - '{{reg_pkg}}'
+          - '{{url_pkg_url.stdout}}'
+          - '{{file_pkg_path}}'
+      check_mode: True
+      register: uninstall_1
+
+    - name: Uninstall packages - mixed sources
+      pacman:
+        state: absent
+        name:
+          - '{{reg_pkg}}'
+          - '{{url_pkg_url.stdout}}'
+          - '{{file_pkg_path}}'
+      register: uninstall_2
+
+    - name: Uninstall packages - mixed sources (idempotency)
+      pacman:
+        state: absent
+        name:
+          - '{{reg_pkg}}'
+          - '{{url_pkg_url.stdout}}'
+          - '{{file_pkg_path}}'
+      register: uninstall_3
+
+    - assert:
+        that:
+          - install_1 is changed
+          - install_1.msg == 'Would have installed 3 packages'
+          - install_1.packages|sort() == [reg_pkg, url_pkg, file_pkg]|sort()
+          - install_2 is changed
+          - install_2.msg == 'Installed 3 package(s)'
+          - install_1.packages|sort() == [reg_pkg, url_pkg, file_pkg]|sort()
+          - install_3 is not changed
+          - install_3.msg == 'package(s) already installed'
+          - install_4 is not changed
+          - install_4.msg == 'package(s) already installed'
+          - install_5 is changed
+          - install_5.msg == 'Installed 1 package(s)'
+          - install_5.packages == [extra_pkg_outfmt]
+          - uninstall_1 is changed
+          - uninstall_1.msg == 'Would have removed 3 packages'
+          - uninstall_1.packages | length() == 3  # pkgs have versions here
+          - uninstall_2 is changed
+          - uninstall_2.msg == 'Removed 3 package(s)'
+          - uninstall_2.packages | length() == 3
+          - uninstall_3 is not changed
+          - uninstall_3.msg == 'package(s) already absent'
diff --git a/tests/unit/plugins/modules/packaging/os/test_pacman.py b/tests/unit/plugins/modules/packaging/os/test_pacman.py
--- a/tests/unit/plugins/modules/packaging/os/test_pacman.py
+++ b/tests/unit/plugins/modules/packaging/os/test_pacman.py
@@ -475,7 +475,13 @@ def test_upgrade_fail(self, mock_valid_inventory):
                 # catch all -> call to pacman to resolve (--sync and --upgrade)
                 "present",
                 ["somepackage-12.3-x86_64.pkg.tar.zst"],
-                [Package(name="somepackage", source="somepackage-12.3-x86_64.pkg.tar.zst")],
+                [
+                    Package(
+                        name="somepackage",
+                        source="somepackage-12.3-x86_64.pkg.tar.zst",
+                        source_is_URL=True,
+                    )
+                ],
                 {
                     "calls": [
                         mock.call(
@@ -582,12 +588,13 @@ def test_op_packages_nothing_to_do(
         self.mock_run_command.call_count == 0
 
     @pytest.mark.parametrize(
-        "module_args, expected_packages, run_command_data, raises",
+        "module_args, expected_packages, package_list_out, run_command_data, raises",
         [
             (
                 # remove pkg: Check mode -- call to print format but that's it
                 {"_ansible_check_mode": True, "name": ["grep"], "state": "absent"},
                 ["grep-version"],
+                [Package("grep", "grep")],
                 {
                     "calls": [
                         mock.call(
@@ -612,6 +619,7 @@ def test_op_packages_nothing_to_do(
                 # remove pkg for real now -- with 2 packages
                 {"name": ["grep", "gawk"], "state": "absent"},
                 ["grep-version", "gawk-anotherversion"],
+                [Package("grep", "grep"), Package("gawk", "gawk")],
                 {
                     "calls": [
                         mock.call(
@@ -650,6 +658,7 @@ def test_op_packages_nothing_to_do(
                     "extra_args": "--some --extra arg",
                 },
                 ["grep-version"],
+                [Package("grep", "grep")],
                 {
                     "calls": [
                         mock.call(
@@ -698,6 +707,7 @@ def test_op_packages_nothing_to_do(
                 # remove pkg -- Failure to list
                 {"name": ["grep"], "state": "absent"},
                 ["grep-3.7-1"],
+                [Package("grep", "grep")],
                 {
                     "calls": [
                         mock.call(
@@ -724,6 +734,7 @@ def test_op_packages_nothing_to_do(
                 # remove pkg -- Failure to remove
                 {"name": ["grep"], "state": "absent"},
                 ["grep-3.7-1"],
+                [Package("grep", "grep")],
                 {
                     "calls": [
                         mock.call(
@@ -756,16 +767,17 @@ def test_op_packages_nothing_to_do(
                 # install pkg: Check mode
                 {"_ansible_check_mode": True, "name": ["sudo"], "state": "present"},
                 ["sudo"],
+                [Package("sudo", "sudo")],
                 {
                     "calls": [
                         mock.call(
                             mock.ANY,
                             [
                                 "pacman",
-                                "--sync",
                                 "--noconfirm",
                                 "--noprogressbar",
                                 "--needed",
+                                "--sync",
                                 "--print-format",
                                 "%n %v",
                                 "sudo",
@@ -778,19 +790,37 @@ def test_op_packages_nothing_to_do(
                 AnsibleExitJson,
             ),
             (
-                # install 2 pkgs, one already present
-                {"name": ["sudo", "grep"], "state": "present"},
-                ["sudo"],
+                # Install pkgs: one regular, one already installed, one file URL and one https URL
+                {
+                    "name": [
+                        "sudo",
+                        "grep",
+                        "./somepackage-12.3-x86_64.pkg.tar.zst",
+                        "http://example.com/otherpkg-1.2-x86_64.pkg.tar.zst",
+                    ],
+                    "state": "present",
+                },
+                ["sudo", "somepackage", "otherpkg"],
+                [
+                    Package("sudo", "sudo"),
+                    Package("grep", "grep"),
+                    Package("somepackage", "./somepackage-12.3-x86_64.pkg.tar.zst", source_is_URL=True),
+                    Package(
+                        "otherpkg",
+                        "http://example.com/otherpkg-1.2-x86_64.pkg.tar.zst",
+                        source_is_URL=True,
+                    ),
+                ],
                 {
                     "calls": [
                         mock.call(
                             mock.ANY,
                             [
                                 "pacman",
-                                "--sync",
                                 "--noconfirm",
                                 "--noprogressbar",
                                 "--needed",
+                                "--sync",
                                 "--print-format",
                                 "%n %v",
                                 "sudo",
@@ -801,16 +831,49 @@ def test_op_packages_nothing_to_do(
                             mock.ANY,
                             [
                                 "pacman",
-                                "--sync",
                                 "--noconfirm",
                                 "--noprogressbar",
                                 "--needed",
+                                "--upgrade",
+                                "--print-format",
+                                "%n %v",
+                                "./somepackage-12.3-x86_64.pkg.tar.zst",
+                                "http://example.com/otherpkg-1.2-x86_64.pkg.tar.zst",
+                            ],
+                            check_rc=False,
+                        ),
+                        mock.call(
+                            mock.ANY,
+                            [
+                                "pacman",
+                                "--noconfirm",
+                                "--noprogressbar",
+                                "--needed",
+                                "--sync",
                                 "sudo",
                             ],
                             check_rc=False,
                         ),
+                        mock.call(
+                            mock.ANY,
+                            [
+                                "pacman",
+                                "--noconfirm",
+                                "--noprogressbar",
+                                "--needed",
+                                "--upgrade",
+                                "./somepackage-12.3-x86_64.pkg.tar.zst",
+                                "http://example.com/otherpkg-1.2-x86_64.pkg.tar.zst",
+                            ],
+                            check_rc=False,
+                        ),
+                    ],
+                    "side_effect": [
+                        (0, "sudo version", ""),
+                        (0, "somepackage 12.3\notherpkg 1.2", ""),
+                        (0, "", ""),
+                        (0, "", ""),
                     ],
-                    "side_effect": [(0, "sudo version", ""), (0, "", "")],
                 },
                 AnsibleExitJson,
             ),
@@ -818,19 +881,20 @@ def test_op_packages_nothing_to_do(
                 # install pkg, extra_args
                 {"name": ["sudo"], "state": "present", "extra_args": "--some --thing else"},
                 ["sudo"],
+                [Package("sudo", "sudo")],
                 {
                     "calls": [
                         mock.call(
                             mock.ANY,
                             [
                                 "pacman",
-                                "--sync",
                                 "--noconfirm",
                                 "--noprogressbar",
                                 "--needed",
                                 "--some",
                                 "--thing",
                                 "else",
+                                "--sync",
                                 "--print-format",
                                 "%n %v",
                                 "sudo",
@@ -841,13 +905,13 @@ def test_op_packages_nothing_to_do(
                             mock.ANY,
                             [
                                 "pacman",
-                                "--sync",
                                 "--noconfirm",
                                 "--noprogressbar",
                                 "--needed",
                                 "--some",
                                 "--thing",
                                 "else",
+                                "--sync",
                                 "sudo",
                             ],
                             check_rc=False,
@@ -861,16 +925,17 @@ def test_op_packages_nothing_to_do(
                 # latest pkg: Check mode
                 {"_ansible_check_mode": True, "name": ["sqlite"], "state": "latest"},
                 ["sqlite"],
+                [Package("sqlite", "sqlite")],
                 {
                     "calls": [
                         mock.call(
                             mock.ANY,
                             [
                                 "pacman",
-                                "--sync",
                                 "--noconfirm",
                                 "--noprogressbar",
                                 "--needed",
+                                "--sync",
                                 "--print-format",
                                 "%n %v",
                                 "sqlite",
@@ -886,16 +951,17 @@ def test_op_packages_nothing_to_do(
                 # latest pkg -- one already latest
                 {"name": ["sqlite", "grep"], "state": "latest"},
                 ["sqlite"],
+                [Package("sqlite", "sqlite")],
                 {
                     "calls": [
                         mock.call(
                             mock.ANY,
                             [
                                 "pacman",
-                                "--sync",
                                 "--noconfirm",
                                 "--noprogressbar",
                                 "--needed",
+                                "--sync",
                                 "--print-format",
                                 "%n %v",
                                 "sqlite",
@@ -906,10 +972,10 @@ def test_op_packages_nothing_to_do(
                             mock.ANY,
                             [
                                 "pacman",
-                                "--sync",
                                 "--noconfirm",
                                 "--noprogressbar",
                                 "--needed",
+                                "--sync",
                                 "sqlite",
                             ],
                             check_rc=False,
@@ -924,13 +990,16 @@ def test_op_packages_nothing_to_do(
     def test_op_packages(
         self,
         mock_valid_inventory,
+        mock_package_list,
         module_args,
         expected_packages,
+        package_list_out,
         run_command_data,
         raises,
     ):
         set_module_args(module_args)
         self.mock_run_command.side_effect = run_command_data["side_effect"]
+        mock_package_list.return_value = package_list_out
 
         P = pacman.Pacman(pacman.setup_module())
         with pytest.raises(raises) as e: