Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package os-autoinst for openSUSE:Factory 
checked in at 2026-08-19 18:00:24
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/os-autoinst (Old)
 and      /work/SRC/openSUSE:Factory/.os-autoinst.new.1258 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "os-autoinst"

Wed Aug 19 18:00:24 2026 rev:617 rq:1372000 version:5.1787055635.69d1218

Changes:
--------
--- /work/SRC/openSUSE:Factory/os-autoinst/os-autoinst.changes  2026-08-05 
17:49:09.551431473 +0200
+++ /work/SRC/openSUSE:Factory/.os-autoinst.new.1258/os-autoinst.changes        
2026-08-19 18:02:47.635629264 +0200
@@ -1,0 +2,12 @@
+Tue Aug 18 12:20:46 UTC 2026 - [email protected]
+
+- Update to version 5.1787055635.69d1218:
+  * feat: validate domain is gone after undefine in svirt
+  * fix: report detailed stderr for failed virsh define
+  * build(deps): bump codecov/codecov-action in the all-actions group (#3082)
+  * docs: Document rootless container example in README
+  * feat: Add custom dev container Dockerfile
+  * feat(testapi): support become_user helper
+  * docs: Improve architecture documentation
+
+-------------------------------------------------------------------

Old:
----
  os-autoinst-5.1785768930.71d56f4.obscpio

New:
----
  os-autoinst-5.1787055635.69d1218.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ os-autoinst-devel-test.spec ++++++
--- /var/tmp/diff_new_pack.Hpp0Ix/_old  2026-08-19 18:02:49.133682994 +0200
+++ /var/tmp/diff_new_pack.Hpp0Ix/_new  2026-08-19 18:02:49.136683102 +0200
@@ -18,7 +18,7 @@
 
 %define         short_name os-autoinst-devel
 Name:           %{short_name}-test
-Version:        5.1785768930.71d56f4
+Version:        5.1787055635.69d1218
 Release:        0
 Summary:        Test package for %{short_name}
 License:        GPL-2.0-or-later

++++++ os-autoinst-openvswitch-test.spec ++++++
--- /var/tmp/diff_new_pack.Hpp0Ix/_old  2026-08-19 18:02:49.173684429 +0200
+++ /var/tmp/diff_new_pack.Hpp0Ix/_new  2026-08-19 18:02:49.175684500 +0200
@@ -19,7 +19,7 @@
 %define name_ext -test
 %define         short_name os-autoinst-openvswitch
 Name:           %{short_name}%{?name_ext}
-Version:        5.1785768930.71d56f4
+Version:        5.1787055635.69d1218
 Release:        0
 Summary:        test package for %{short_name}
 License:        GPL-2.0-or-later

++++++ os-autoinst-test.spec ++++++
--- /var/tmp/diff_new_pack.Hpp0Ix/_old  2026-08-19 18:02:49.208685684 +0200
+++ /var/tmp/diff_new_pack.Hpp0Ix/_new  2026-08-19 18:02:49.210685756 +0200
@@ -19,7 +19,7 @@
 %define name_ext -test
 %define         short_name os-autoinst
 Name:           %{short_name}%{?name_ext}
-Version:        5.1785768930.71d56f4
+Version:        5.1787055635.69d1218
 Release:        0
 Summary:        test package for os-autoinst
 License:        GPL-2.0-or-later

++++++ os-autoinst.spec ++++++
--- /var/tmp/diff_new_pack.Hpp0Ix/_old  2026-08-19 18:02:49.263687657 +0200
+++ /var/tmp/diff_new_pack.Hpp0Ix/_new  2026-08-19 18:02:49.270687908 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           os-autoinst
-Version:        5.1785768930.71d56f4
+Version:        5.1787055635.69d1218
 Release:        0
 Summary:        OS-level test automation
 License:        GPL-2.0-or-later

++++++ os-autoinst-5.1785768930.71d56f4.obscpio -> 
os-autoinst-5.1787055635.69d1218.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1785768930.71d56f4/.github/workflows/author-tests.yaml 
new/os-autoinst-5.1787055635.69d1218/.github/workflows/author-tests.yaml
--- old/os-autoinst-5.1785768930.71d56f4/.github/workflows/author-tests.yaml    
2026-08-03 16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/.github/workflows/author-tests.yaml    
2026-08-18 14:20:35.000000000 +0200
@@ -12,7 +12,7 @@
     container:
       image: registry.opensuse.org/devel/openqa/containers/os-autoinst_dev
     steps:
-      - uses: actions/checkout@v7
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7
       - name: make test-local
         run: |
           git config --global --add safe.directory '*'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1785768930.71d56f4/.github/workflows/checklist.yml 
new/os-autoinst-5.1787055635.69d1218/.github/workflows/checklist.yml
--- old/os-autoinst-5.1785768930.71d56f4/.github/workflows/checklist.yml        
2026-08-03 16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/.github/workflows/checklist.yml        
2026-08-18 14:20:35.000000000 +0200
@@ -10,9 +10,9 @@
     name: Checklist job
     steps:
       - name: Checkout
-        uses: actions/checkout@v7
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7
       - name: Checklist
-        uses: wyozi/contextual-qa-checklist-action@master
+        uses: 
wyozi/contextual-qa-checklist-action@dd4b6b51d85a59f537b7ca9e2f162164627ec811  
# master
         with:
           gh-token: ${{ secrets.GITHUB_TOKEN }}
           input-file: .github/checklist.yml
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1785768930.71d56f4/.github/workflows/ci.yml 
new/os-autoinst-5.1787055635.69d1218/.github/workflows/ci.yml
--- old/os-autoinst-5.1785768930.71d56f4/.github/workflows/ci.yml       
2026-08-03 16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/.github/workflows/ci.yml       
2026-08-18 14:20:35.000000000 +0200
@@ -11,11 +11,11 @@
       contents: read
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v7
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7
       - name: Static checks, unit tests and integration tests
         run: tools/container_run_ci
       - name: Upload coverage to Codecov
-        uses: codecov/codecov-action@v7
+        uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f  
# v7
         with:
           # should not be necessary for public repos, but might help avoid 
sporadic upload token errors
           token: ${{ secrets.CODECOV_TOKEN }}
@@ -24,7 +24,7 @@
           verbose: true
       - run: |
           tar cvf coverage.tar build/cover_db
-      - uses: actions/upload-artifact@v7
+      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a 
 # v7
         with:
           name: coverage
           path: coverage.tar
@@ -35,6 +35,6 @@
       contents: read
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v7
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7
       - name: Test our container definitions
         run: tools/static_check_containers
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1785768930.71d56f4/.github/workflows/ci_extended.yml 
new/os-autoinst-5.1787055635.69d1218/.github/workflows/ci_extended.yml
--- old/os-autoinst-5.1785768930.71d56f4/.github/workflows/ci_extended.yml      
2026-08-03 16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/.github/workflows/ci_extended.yml      
2026-08-18 14:20:35.000000000 +0200
@@ -14,6 +14,6 @@
       contents: read
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v7
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7
       - name: Test our container definitions
         run: tools/test_containers
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1785768930.71d56f4/.github/workflows/commit-message-checker.yml
 
new/os-autoinst-5.1787055635.69d1218/.github/workflows/commit-message-checker.yml
--- 
old/os-autoinst-5.1785768930.71d56f4/.github/workflows/commit-message-checker.yml
   2026-08-03 16:55:30.000000000 +0200
+++ 
new/os-autoinst-5.1787055635.69d1218/.github/workflows/commit-message-checker.yml
   2026-08-18 14:20:35.000000000 +0200
@@ -12,4 +12,4 @@
   check-commit-message:
     permissions:
       contents: read
-    uses: 
os-autoinst/os-autoinst-common/.github/workflows/base-commit-message-checker.yml@master
+    uses: 
os-autoinst/os-autoinst-common/.github/workflows/base-commit-message-checker.yml@e76072d144ca3313002006ff4ee8d4e5a7529aa9
  # master
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1785768930.71d56f4/.github/workflows/obs-helper.yaml 
new/os-autoinst-5.1787055635.69d1218/.github/workflows/obs-helper.yaml
--- old/os-autoinst-5.1785768930.71d56f4/.github/workflows/obs-helper.yaml      
2026-08-03 16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/.github/workflows/obs-helper.yaml      
2026-08-18 14:20:35.000000000 +0200
@@ -12,7 +12,7 @@
       contents: read
       issues: write
     steps:
-      - uses: actions/checkout@v7
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7
       - name: debug
         run: |
           env | sort
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1785768930.71d56f4/.github/workflows/openqa_fullstack.yml 
new/os-autoinst-5.1787055635.69d1218/.github/workflows/openqa_fullstack.yml
--- old/os-autoinst-5.1785768930.71d56f4/.github/workflows/openqa_fullstack.yml 
2026-08-03 16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/.github/workflows/openqa_fullstack.yml 
2026-08-18 14:20:35.000000000 +0200
@@ -12,6 +12,7 @@
 env:
   # Set to 1 to temporarily ignore warnings
   PERL_TEST_WARNINGS_ONLY_REPORT_WARNINGS: 0
+  OPENQA_FULLSTACK_TEMP_DIR: /tmp/test-results/fullstack
   TEST_USER: testuser
 
 jobs:
@@ -22,7 +23,7 @@
     container:
       image: 
registry.opensuse.org/devel/openqa/containers/opensuse/openqa_ci:latest
     steps:
-      - uses: actions/checkout@v7
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7
       - name: Create additional user to avoid initdb error "cannot be run as 
root"
         run: useradd "$TEST_USER"
       - name: Make project folder user-writable
@@ -34,6 +35,16 @@
       - name: Build os-autoinst
         run: sudo -u "$TEST_USER" make symlinks
       - name: Run unit tests
-        run: sudo -u "$TEST_USER" make -C ../openQA test-fullstack
+        run: |
+          sudo -u "$TEST_USER" mkdir -p "$OPENQA_FULLSTACK_TEMP_DIR"
+          sudo -u "$TEST_USER" make -C ../openQA test-fullstack 
OPENQA_FULLSTACK_TEMP_DIR="$OPENQA_FULLSTACK_TEMP_DIR"
         env:
           STABILITY_TEST: ${{ github.event.inputs.stability_test }}
+      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a 
 # v7
+        if: ${{ always() }}
+        with:
+          name: Test results
+          path: |
+            /tmp/test-results/
+            !/tmp/test-results/**/*.iso
+          include-hidden-files: true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1785768930.71d56f4/OpenQA/Exceptions.pm 
new/os-autoinst-5.1787055635.69d1218/OpenQA/Exceptions.pm
--- old/os-autoinst-5.1785768930.71d56f4/OpenQA/Exceptions.pm   2026-08-03 
16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/OpenQA/Exceptions.pm   2026-08-18 
14:20:35.000000000 +0200
@@ -28,6 +28,9 @@
     'OpenQA::Exception::TestapiError' => {
         description => 'A testapi function failed'
     },
+    'OpenQA::Exception::TestapiUsageError' => {
+        description => 'A testapi function failed because of wrong usage'
+    },
 );
 
 1;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1785768930.71d56f4/OpenQA/Isotovideo/Interface.pm 
new/os-autoinst-5.1787055635.69d1218/OpenQA/Isotovideo/Interface.pm
--- old/os-autoinst-5.1785768930.71d56f4/OpenQA/Isotovideo/Interface.pm 
2026-08-03 16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/OpenQA/Isotovideo/Interface.pm 
2026-08-18 14:20:35.000000000 +0200
@@ -9,7 +9,7 @@
 # -> increment on every change of such APIs
 # -> never move that variable to another place (when refactoring)
 #    because it may be accessed by the tests itself
-our $version = 58;    ## no critic (Variables::ProhibitPackageVars)
+our $version = 59;    ## no critic (Variables::ProhibitPackageVars)
 
 # major version of the (web socket) API relevant to the developer mode
 # -> increment when making non-backward compatible changes to that API
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1785768930.71d56f4/OpenQA/Isotovideo/Runner.pm 
new/os-autoinst-5.1787055635.69d1218/OpenQA/Isotovideo/Runner.pm
--- old/os-autoinst-5.1785768930.71d56f4/OpenQA/Isotovideo/Runner.pm    
2026-08-03 16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/OpenQA/Isotovideo/Runner.pm    
2026-08-18 14:20:35.000000000 +0200
@@ -50,6 +50,9 @@
     $io_select->add($ch->cmd_srv_fd);
     $io_select->add($ch->backend_out_fd);
 
+    my @interleaved_cmds;
+    myjsonrpc::set_interleaved_command_handler(\@interleaved_cmds);
+
     while ($self->loop) {
         my ($ready_for_read, $ready_for_write, $exceptions) = 
IO::Select::select($io_select, undef, $io_select, $ch->timeout);
         for my $readable (@$ready_for_read) {
@@ -57,6 +60,7 @@
             $self->_read_response($rsp, $readable);
             last unless defined $rsp;
         }
+        $self->_read_response(@$_) for splice @interleaved_cmds;
         $ch->check_asserted_screen if defined($ch->tags);
     }
     $ch->stop_command_processing;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/README.md 
new/os-autoinst-5.1787055635.69d1218/README.md
--- old/os-autoinst-5.1785768930.71d56f4/README.md      2026-08-03 
16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/README.md      2026-08-18 
14:20:35.000000000 +0200
@@ -365,6 +365,24 @@
 (e.g. `os-autoinst-distri-opensuse-deps`) must be provided by that container 
image rather
 than the worker host.
 
+For example, you can run an openQA test using your custom `os-autoinst` branch 
and the
+pre-built `osado-dev-container` image (which already bundles 
`os-autoinst-distri-opensuse-deps`).
+
+Note: You must point `CASEDIR` and `NEEDLES_DIR` to remote Git repositories 
because the
+rootless container starts with a clean environment and does not mount local 
worker directories:
+
+```sh
+openqa-clone-job --skip-chained-deps --within-instance <target_job_url> \
+  _GROUP=0 \
+  BUILD+=-custom_suffix \
+  TEST+=-custom_suffix \
+  CASEDIR=https://github.com/os-autoinst/os-autoinst-distri-opensuse.git \
+  NEEDLES_DIR=https://github.com/os-autoinst/os-autoinst-needles-opensuse.git \
+  OS_AUTOINST_GIT_REPO=https://github.com/<your_username>/os-autoinst.git \
+  OS_AUTOINST_GIT_BRANCH=<your_test_branch> \
+  
OS_AUTOINST_CONTAINER_IMAGE=registry.opensuse.org/devel/openqa/containers/osado-dev-container:latest
+```
+
 ### Advanced testing with custom test-distribution dependencies
 
 If you are testing changes against tests that require custom dependencies not
@@ -384,7 +402,7 @@
   TEST+=-custom_suffix \
   
CASEDIR=https://github.com/<your_username>/os-autoinst-distri-opensuse.git#<your_test_branch>
 \
   WORKER_CLASS=<suitable_worker_class> \
-  ISOTOVIDEO="p=/var/lib/openqa/cache/podman && mkdir -p \$p/run \$p/config 
\$p/data && env HOME=\$p XDG_CONFIG_HOME=\$p/config XDG_DATA_HOME=\$p/data 
XDG_RUNTIME_DIR=\$p/run podman --root \$p/data/containers/storage --runroot 
\$p/run/containers --storage-opt ignore_chown_errors=true 
--cgroup-manager=cgroupfs --events-backend=file run --init --rm --entrypoint 
\"\" --device /dev/kvm -v \$(pwd):/pool -w /pool -v 
/var/lib/openqa/cache:/var/lib/openqa/cache 
registry.opensuse.org/devel/openqa/containers/os-autoinst_dev:latest sh -c 
'zypper -n in os-autoinst-distri-opensuse-deps && rm -rf os-autoinst && git 
clone --branch=<your_engine_branch> --depth=1 
https://github.com/<your_username>/os-autoinst.git && make -C os-autoinst 
symlinks && os-autoinst/isotovideo -d'"
+  ISOTOVIDEO="c=/var/lib/openqa/cache/podman_storage && p=\$(pwd)/podman_tmp 
&& mkdir -p \$c \$p/run && env HOME=\$p XDG_RUNTIME_DIR=\$p/run podman --root 
\$c --runroot \$p/run/containers --storage-opt ignore_chown_errors=true 
--cgroup-manager=cgroupfs --events-backend=file run --init --rm --entrypoint 
\"\" --device /dev/kvm -v \$(pwd):/pool -w /pool -v 
/var/lib/openqa/cache:/var/lib/openqa/cache 
registry.opensuse.org/devel/openqa/containers/os-autoinst_dev:latest sh -c 
'zypper -n in os-autoinst-distri-opensuse-deps && rm -rf os-autoinst && git 
clone --branch=<your_engine_branch> --depth=1 
https://github.com/<your_username>/os-autoinst.git && make -C os-autoinst 
symlinks && os-autoinst/isotovideo -d'"
 ```
 
 # Running isotovideo as CI check
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1785768930.71d56f4/backend/baseclass.pm 
new/os-autoinst-5.1787055635.69d1218/backend/baseclass.pm
--- old/os-autoinst-5.1785768930.71d56f4/backend/baseclass.pm   2026-08-03 
16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/backend/baseclass.pm   2026-08-18 
14:20:35.000000000 +0200
@@ -139,6 +139,7 @@
         $console->backend($self);
     }
 
+    myjsonrpc::set_interleaved_command_handler($self->{interleaved_cmds} = []);
     $self->run_capture_loop;
 
     bmwqemu::diag('management process exit at ' . POSIX::strftime('%F %T', 
gmtime));    # uncoverable statement
@@ -302,6 +303,10 @@
         # next time
         last;
     }
+
+    # handle interleaved commands (commands received while waiting for the 
reply to another command)
+    $self->_handle_cmd($_->[0]) for splice @{$self->{interleaved_cmds}};
+
     return 1;
 }
 
@@ -633,24 +638,23 @@
     _exit(0);    # uncoverable statement
 }
 
+sub _handle_cmd ($self, $cmd) {
+    die 'no command in ' . Dumper($cmd) unless $cmd->{cmd};
+    my $rsp = ($self->handle_command($cmd) // 0);
+    my $response = {rsp => $rsp};
+    if (ref $rsp eq 'HASH' && $rsp->{postponed}) {
+        $self->{_postponed_cmd_token} = $cmd->{json_cmd_token};
+    } elsif ($self->{rsppipe}) {    # the command might have closed it
+        $response->{json_cmd_token} = $cmd->{json_cmd_token};
+        myjsonrpc::send_json($self->{rsppipe}, $response);
+    }
+}
+
 # this is called for all sockets ready to read from
 sub check_socket ($self, $fh, $write = undef) {
     return 0 unless $self->{cmdpipe} && $fh == $self->{cmdpipe};
     return 1 if $write;
-    my $cmd = myjsonrpc::read_json($self->{cmdpipe});
-    if ($cmd->{cmd}) {
-        my $rsp = ($self->handle_command($cmd) // 0);
-        my $response = {rsp => $rsp};
-        if (ref $rsp eq 'HASH' && $rsp->{postponed}) {
-            $self->{_postponed_cmd_token} = $cmd->{json_cmd_token};
-        } elsif ($self->{rsppipe}) {    # the command might have closed it
-            $response->{json_cmd_token} = $cmd->{json_cmd_token};
-            myjsonrpc::send_json($self->{rsppipe}, $response);
-        }
-    }
-    else {
-        die 'no command in ' . Dumper($cmd);
-    }
+    $self->_handle_cmd($_) for myjsonrpc::read_json($self->{cmdpipe}, undef, 
1);
     return 1;
 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/backend/svirt.pm 
new/os-autoinst-5.1787055635.69d1218/backend/svirt.pm
--- old/os-autoinst-5.1785768930.71d56f4/backend/svirt.pm       2026-08-03 
16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/backend/svirt.pm       2026-08-18 
14:20:35.000000000 +0200
@@ -78,8 +78,17 @@
 
 sub do_stop_vm_svirt ($self) {
     my $vmname = $self->vmname;
-    $self->run_ssh_cmd(virsh() . " destroy $vmname");
-    $self->run_ssh_cmd(virsh() . " undefine --snapshots-metadata $vmname");
+
+    # Only attempt cleanup if the domain actually exists on the host
+    if ($self->run_ssh_cmd(virsh() . " dominfo $vmname 2>/dev/null") == 0) {
+        $self->run_ssh_cmd(virsh() . " destroy $vmname");
+        $self->run_ssh_cmd(virsh() . " undefine --snapshots-metadata $vmname");
+
+        # Verify the domain is actually gone to prevent cryptic UUID clashes 
later
+        if ($self->run_ssh_cmd(virsh() . " dominfo $vmname 2>/dev/null") == 0) 
{
+            bmwqemu::fctwarn("Domain '$vmname' still exists after undefine 
attempt. The QEMU process is likely stuck (D-state) on the host.");
+        }
+    }
 }
 
 sub do_stop_vm ($self, @) {
@@ -121,7 +130,7 @@
 sub is_shutdown_cmd_hyperv ($vmname) { qq{powershell -Command "if (\$(Get-VM 
-VMName $vmname \| Where-Object {\$_.state -eq 'Off'})) { exit 1 } else { exit 
0 }"} }
 
 sub is_shutdown_cmd_svirt ($vmname) {
-    return '! ' . virsh() . " dominfo $vmname | grep -w 'shut off'";
+    return '! ' . virsh() . " dominfo $vmname 2>/dev/null | grep -w 'shut 
off'";
 }
 
 sub is_shutdown ($self, @) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1785768930.71d56f4/consoles/sshVirtsh.pm 
new/os-autoinst-5.1787055635.69d1218/consoles/sshVirtsh.pm
--- old/os-autoinst-5.1785768930.71d56f4/consoles/sshVirtsh.pm  2026-08-03 
16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/consoles/sshVirtsh.pm  2026-08-18 
14:20:35.000000000 +0200
@@ -300,8 +300,15 @@
     return;
 }
 
+sub _free_lock_forcefully ($self) {
+    bmwqemu::diag('Lock is still held. Attempting active recovery by 
force-killing lingering processes for ' . $self->name);
+    $self->run_cmd(sprintf "%s destroy '%s'", backend::svirt::virsh, 
$self->name);
+    $self->run_cmd(sprintf "pkill -9 -f '%s'", $self->name);
+}
+
 sub _do_create_disk ($self, $file, $size, $args = undef) {
     my $bucket = 5;
+    my $active_recovery_attempted = 0;
     my @cmd = "qemu-img create '$file' -f qcow2";
     push @cmd, $args->{additional_args} if $args->{additional_args};
     push @cmd, $size;
@@ -311,7 +318,16 @@
         my ($ret, $stdout, $stderr) = $self->run_cmd((join ' ', @cmd), 
wantarray => 1);
         if (($stderr // '') =~ /lock/i) {
             $bucket--;
-            die 'Too many attempts to create disk' unless $bucket;
+            if (!$bucket) {
+                if (!$active_recovery_attempted) {
+                    $self->_free_lock_forcefully;
+                    $active_recovery_attempted = 1;
+                    $bucket = 2;    # allow 2 more attempts after active 
recovery
+                }
+                else {
+                    die 'Too many attempts to create disk';
+                }
+            }
             bmwqemu::diag("Resource is still not free, waiting a bit more. 
$bucket attempts left");
             sleep 5;
             next;
@@ -476,7 +492,7 @@
         # $args->{size} is expected to be e.g. '20G' but internally we need it 
as integer
         my $size = ($args->{size} // 0) =~ tr/G//dr;
         # expected value in Bytes
-        my (undef, $json) = $self->run_cmd("qemu-img info --output=json 
$args->{file}", wantarray => 1);
+        my (undef, $json) = $self->run_cmd("qemu-img info --force-share 
--output=json $args->{file}", wantarray => 1);
         my $image_vsize = decode_json($json)->{'virtual-size'};
         $size = (($size * 1024 * 1024 * 1024) <= $image_vsize) ? $image_vsize 
: $size . 'G';
         $self->_do_create_disk($file, $size, {additional_args => "-F qcow2 -b 
'$basedir/$file_basename'"});
@@ -615,7 +631,12 @@
     $self->backend->do_stop_vm_svirt() if $args{pre_cleanup};
 
     # define the new domain
-    $self->run_cmd(backend::svirt::virsh() . " define $xmlfilename") && die 
'virsh define failed';
+    my ($stdout, $stderr);
+    ($ret, $stdout, $stderr) = $self->run_cmd(backend::svirt::virsh() . " 
define $xmlfilename", wantarray => 1);
+    if ($ret) {
+        $stderr =~ s/\s+$// if $stderr;
+        die 'virsh define failed: ' . ($stderr || "exit code $ret");
+    }
     if ($self->vmm_family eq 'vmware') {
         my $vmx = sprintf '/vmfs/volumes/%s/openQA/%s.vmx', 
$bmwqemu::vars{VMWARE_DATASTORE} // 'datastore1', $self->name;
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1785768930.71d56f4/container/osado-dev-container/Containerfile
 
new/os-autoinst-5.1787055635.69d1218/container/osado-dev-container/Containerfile
--- 
old/os-autoinst-5.1785768930.71d56f4/container/osado-dev-container/Containerfile
    1970-01-01 01:00:00.000000000 +0100
+++ 
new/os-autoinst-5.1787055635.69d1218/container/osado-dev-container/Containerfile
    2026-08-18 14:20:35.000000000 +0200
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: MIT
+#!BuildTag: osado-dev-container:latest opensuse/osado-dev-container:latest 
opensuse/osado-dev-container:%PKG_VERSION% 
opensuse/osado-dev-container:%PKG_VERSION%.%RELEASE%
+# hadolint ignore=DL3007
+FROM opensuse/os-autoinst-dev:latest
+RUN zypper in -y -C os-autoinst-distri-opensuse-deps && zypper clean -a
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1785768930.71d56f4/container/osado-dev-container/Dockerfile 
new/os-autoinst-5.1787055635.69d1218/container/osado-dev-container/Dockerfile
--- 
old/os-autoinst-5.1785768930.71d56f4/container/osado-dev-container/Dockerfile   
    1970-01-01 01:00:00.000000000 +0100
+++ 
new/os-autoinst-5.1787055635.69d1218/container/osado-dev-container/Dockerfile   
    2026-08-19 18:02:50.877745547 +0200
@@ -0,0 +1 @@
+symbolic link to Containerfile
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1785768930.71d56f4/container/osado-dev-container/_service 
new/os-autoinst-5.1787055635.69d1218/container/osado-dev-container/_service
--- old/os-autoinst-5.1785768930.71d56f4/container/osado-dev-container/_service 
1970-01-01 01:00:00.000000000 +0100
+++ new/os-autoinst-5.1787055635.69d1218/container/osado-dev-container/_service 
2026-08-18 14:20:35.000000000 +0200
@@ -0,0 +1,22 @@
+<services>
+  <service name="obs_scm">
+    <param name="url">https://github.com/os-autoinst/os-autoinst</param>
+    <param name="scm">git</param>
+    <param name="revision">master</param>
+    <param name="versionprefix">5</param>
+    <param name="versionformat">%ct.%h</param>
+    <param name="subdir">container/osado-dev-container</param>
+    <param name="extract">Dockerfile</param>
+    <param name="changesgenerate">enable</param>
+    <param name="changesauthor">[email protected]</param>
+  </service>
+  <service mode="buildtime" name="kiwi_metainfo_helper"/>
+  <service mode="buildtime" name="docker_label_helper"/>
+  <service mode="buildtime" name="replace_using_package_version">
+    <param name="file">Dockerfile</param>
+    <param name="regex">%PKG_VERSION%</param>
+    <param name="parse-version">patch</param>
+    <param name="package">os-autoinst-distri-opensuse-deps</param>
+  </service>
+</services>
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/distribution.pm 
new/os-autoinst-5.1787055635.69d1218/distribution.pm
--- old/os-autoinst-5.1785768930.71d56f4/distribution.pm        2026-08-03 
16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/distribution.pm        2026-08-18 
14:20:35.000000000 +0200
@@ -98,6 +98,15 @@
     $self->script_run('cd /tmp');
 }
 
+sub become_user ($self, $user) {
+    die "Invalid username: $user" unless $user =~ /^[a-zA-Z0-9_.-]+$/;
+    $self->script_run("su - $user", 0);
+    $self->invalidate_serial_marker_hook();
+    my $console = testapi::current_console() // 'sut';
+    my $level = $self->{_serial_marker_level}->{$console} // 1;
+    $self->install_serial_marker_hook($level);
+}
+
 =head2 disable_key_repeat
 
   disable_key_repeat()
@@ -118,6 +127,10 @@
 
 sub _handle_cmd_typing_error ($cmd, $args) { ($args->{check_typing_cmd} // 1 ? 
\&croak : \&fctwarn)->("typing command '$cmd' timed out") }
 
+# _OANM: openQA No Marker, skips the shell hook's done marker. Applied on
+# serial terminals or when C<$force> is set (e.g. manual serial redirection).
+sub _no_marker_prefix ($cmd, $force = 0) { ($force || 
testapi::is_serial_terminal) ? "_OANM=1; $cmd" : $cmd }
+
 =head2 script_run
 
   script_run($cmd [, timeout => $timeout] [, output => $output] [,quiet => 
$quiet] [,max_interval => $max_interval])
@@ -177,9 +190,11 @@
             testapi::query_isotovideo('backend_clear_serial_buffer', {});
             testapi::type_string "$cmd\n", max_interval => $args{max_interval};
             $self->_check_sudo_password(\%args);
-            my $res = testapi::wait_serial(qr/OA:DONE-[0-9a-f]{4}-(\d+)-/, 
timeout => $args{timeout}, quiet => $args{quiet}, record_command => $cmd, 
internal_marker => 1, capture_name => 'Exit code');
+            my $fp = $self->sut_marker($cmd);
+            my $regex = qr/OA:DONE-[0-9a-f]{4}-(\d+)-\Q$fp\E/;
+            my $res = testapi::wait_serial($regex, timeout => $args{timeout}, 
quiet => $args{quiet}, record_command => $cmd, internal_marker => 1, 
capture_name => 'Exit code');
             return undef unless $res;
-            return ($res =~ /OA:DONE-[0-9a-f]{4}-(\d+)-/)[0];
+            return ($res =~ $regex)[0];
         }
         $str = testapi::hashed_string('SR' . $cmd . $args{timeout});
         $wait_pattern = qr/$str-(\d+)-/;
@@ -189,8 +204,7 @@
         }
         else {
             my $marker = "; echo $str-\$?-" . ($args{output} ? "Comment: 
$args{output}" : '');
-            # _OANM: openQA No Marker, skips printing the hook's done marker
-            my $final_cmd = $skip_pretty ? "_OANM=1; $cmd" : $cmd;
+            my $final_cmd = _no_marker_prefix($cmd, $skip_pretty);
             if (testapi::is_serial_terminal) {
                 testapi::type_string "$final_cmd$marker", max_interval => 
$args{max_interval};
                 testapi::wait_serial($final_cmd . $marker, no_regex => 1, 
quiet => 1, buffer_size => (length $final_cmd) + 128, internal_marker => 1)
@@ -235,7 +249,7 @@
         testapi::wait_serial($self->{serial_term_prompt}, no_regex => 1, quiet 
=> 1);
     }
 
-    $cmd = "( $cmd )";
+    $cmd = _no_marker_prefix("( $cmd )");
     testapi::type_string $cmd;
     my $str = testapi::hashed_string('SR' . $cmd);
     my $marker = "& echo $str-\$!-" . ($args{output} ? "Comment: 
$args{output}" : '');
@@ -350,7 +364,7 @@
     # might encounter on the serial device depending on how it is used in the
     # SUT
     my $shell_cmd = testapi::is_serial_terminal() ? 'bash -oe pipefail' : 
'bash -eox pipefail';
-    my $run_script = "echo $marker; $shell_cmd $script_path ; echo 
SCRIPT_FINISHED$marker-\$?-";
+    my $run_script = _no_marker_prefix("echo $marker; $shell_cmd $script_path 
; echo SCRIPT_FINISHED$marker-\$?-");
     if (testapi::is_serial_terminal) {
         testapi::wait_serial($self->{serial_term_prompt}, no_regex => 1, quiet 
=> 1);
         testapi::type_string "$run_script\n";
@@ -432,7 +446,8 @@
     sut_marker($cmd)
 
 Generate a unique marker string for a command to be used for synchronization
-with the SUT. Used primarily for internal testing.
+with the SUT. Canonical command fingerprint used for level-3 marker
+correlation.
 
 =cut
 
@@ -452,27 +467,42 @@
 Install shell hooks (like PROMPT_COMMAND) into the SUT to emit synchronization
 markers to serial.
 
+B<Persistence implications:> the hook is appended to the SUT's C<~/.bashrc> and
+C<~/.profile> and therefore persists across shells and across jobs that boot 
the
+same qcow image. Once installed, every prompt of every shell sourcing those
+files emits C<OA:DONE>/C<OA:START> to C</dev/$testapi::serialdev> (i.e.
+C<serial0.txt>), including login/C<exec $SHELL>/C<su> shells and non-serial
+terminal (VNC/tty) consoles. This is by design (C<serial0.txt> is the control
+channel for these markers) but means C<serial0.txt> cannot be kept completely
+clean by patching individual command invocations. To fully suppress the hook,
+disable the feature via C<PRETTY_SERIAL_MARKER=0> for the whole cluster,
+including the parent job that creates the qcow so no hook is ever baked in. See
+also L</detect_serial_marker_capability> and C<doc/backend_vars.md>.
+
 =cut
 
 sub install_serial_marker_hook ($self, $level) {
     return undef if $level < 2;
     my $dev = "/dev/$testapi::serialdev";
-    my $func;
-    # _oap: openQA prompt hook function
-    # _OANM: openQA No Marker (skip hook done marker)
-    # _OAM: openQA Marker (custom marker string)
-    if ($level == 3) {
-        $func = qq{_oap(){ r=\$?;if [ -n "\$_OANM" ];then unset _OANM;else 
c=\$(fc -ln -1 2>/dev/null);printf "OA:DONE-%04x-%d-%s\\nOA:START\\n" \$RANDOM 
\$r "\${c#\${c%%[![:space:]]*}}">$dev;fi;}};
-    }
-    else {
-        $func = qq{_oap(){ r=\$?;if [ -n "\$_OANM" ];then unset _OANM;elif [ 
-n "\$_OAM" ];then echo "\$_OAM-\$r-">$dev;unset _OAM;fi;echo 
"OA:START">$dev;}};
-    }
+    # Shell hook tokens: _oap=prompt hook, _OANM=skip-done-marker, _OAM=custom
+    # marker (grepped in ~/.bashrc below to detect an already-installed hook).
+    my $func = {
+        3 => qq{_oap(){ r=\$?;if [ -n "\$_OANM" ];then unset _OANM;else 
c=\$(HISTTIMEFORMAT= history 1);c=\${c#*[0-9]  
};c=\${c#\${c%%[![:space:]]*}};c=\${c%\${c##*[![:space:]]}};l=\${#c};t=\$c;[ 
\$l -ge 4 ]&&t=\${c: -4};printf "OA:DONE-%04x-%d-OA:%s%d%s\\nOA:START\\n" 
\$RANDOM \$r "\${c:0:4}" \$l "\$t">$dev;fi;}},
+        2 => qq{_oap(){ r=\$?;if [ -n "\$_OANM" ];then unset _OANM;elif [ -n 
"\$_OAM" ];then echo "\$_OAM-\$r-">$dev;unset _OAM;fi;echo "OA:START">$dev;}},
+    }->{$level};
     my $pc = 'PROMPT_COMMAND=_oap';
 
+    # Version tag: bump whenever the emitted marker format changes so a stale
+    # hook persisted in ~/.bashrc by an older os-autoinst is replaced instead 
of
+    # kept (a presence-only `grep _oap` guard would re-source the old format 
and
+    # emit markers the current wait_serial regex never matches).
+    my $tag = '_OAPV=3';
+
     # Consolidate installation and persistence into a single typed line to 
minimize VNC overhead.
     # We append to both ~/.bashrc and ~/.profile to cover both interactive and 
login shells.
-    # Sourcing ~/.bashrc then activates the hook in the current session.
-    testapi::type_string "grep -q _oap ~/.bashrc 2>/dev/null||{ echo 
'$func;$pc'|tee -a ~/.bashrc ~/.profile>/dev/null;};. ~/.bashrc\n";
+    # If the current version tag is absent, strip any stale hook lines first, 
then
+    # (re)install. Sourcing ~/.bashrc then activates the hook in the current 
session.
+    testapi::type_string "grep -q '$tag' ~/.bashrc 2>/dev/null||{ sed -i 
'/_oap/d;/$tag/d' ~/.bashrc ~/.profile 2>/dev/null;echo '$tag;$func;$pc'|tee -a 
~/.bashrc ~/.profile>/dev/null;};. ~/.bashrc\n";
 
     my $console = testapi::current_console() // 'sut';
     $self->{_serial_marker_hook_installed}->{$console} = 1;
@@ -518,6 +548,14 @@
     delete $self->{_serial_marker_hook_installed}->{$console};
 }
 
+# Disable any pre-existing PROMPT_COMMAND hook (e.g. inherited from ~/.bashrc)
+# so it cannot pollute serial0.txt with markers. On a serial terminal we wait
+# for the prompt to consume the command echo.
+sub _disable_inherited_prompt_hook ($self, $wait_prompt = 0) {
+    testapi::type_string "unset PROMPT_COMMAND\n";
+    testapi::wait_serial($self->{serial_term_prompt}, no_regex => 1, quiet => 
1) if $wait_prompt && $self->{serial_term_prompt};
+}
+
 =head2 get_pretty_serial_marker
 
     get_pretty_serial_marker()
@@ -547,7 +585,7 @@
 
     # If we are turning it OFF, we MUST tell the SUT to stop sending markers
     # to avoid polluting the fallback mode.
-    testapi::type_string "unset PROMPT_COMMAND\n" if !$value;
+    $self->_disable_inherited_prompt_hook if !$value;
 
     $self->reset_serial_marker();
 }
@@ -587,6 +625,16 @@
 - 2: Basic bash (PROMPT_COMMAND support)
 - 3: Advanced bash (PROMPT_COMMAND + history/fc support)
 
+Level 1 is returned without installing any hook when C<PRETTY_SERIAL_MARKER> is
+disabled or when the active console is a serial terminal 
(C<is_serial_terminal>).
+On a serial terminal command markers already appear on that terminal's log
+(C<serial_terminal.txt>), so no hook is baked in and nothing is written to
+C<serial0.txt> from that console. Consequently, residual C<OA:DONE>/C<OA:START>
+markers seen in C<serial0.txt> for serial-terminal-centric tests originate from
+the VNC/tty consoles those tests still use (where the hook I<is> installed), 
not
+from the serial terminal itself. To eliminate them entirely, disable the 
feature
+cluster-wide with C<PRETTY_SERIAL_MARKER=0>; see 
L</install_serial_marker_hook>.
+
 =cut
 
 sub detect_serial_marker_capability ($self) {
@@ -601,7 +649,10 @@
     my $level = 1;
     my $pretty = $self->get_pretty_serial_marker();
     my $serial_term = testapi::is_serial_terminal();
-    return $self->{_serial_marker_level}->{$console} = $level if !$pretty || 
$serial_term;
+    if (!$pretty || $serial_term) {
+        $self->_disable_inherited_prompt_hook(1) if $serial_term;
+        return $self->{_serial_marker_level}->{$console} = $level;
+    }
 
     testapi::type_string "echo \"BASH:\$BASH_VERSION:\" > 
/dev/$testapi::serialdev\n";
     my $out = testapi::wait_serial(qr/BASH:([^:]*):/, 10);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/doc/architecture.md 
new/os-autoinst-5.1787055635.69d1218/doc/architecture.md
--- old/os-autoinst-5.1785768930.71d56f4/doc/architecture.md    2026-08-03 
16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/doc/architecture.md    2026-08-18 
14:20:35.000000000 +0200
@@ -5,11 +5,13 @@
 Once everything is running, the process tree looks like this:
 
 * **isotovideo**: spawns further processes, IO-loop for passing commands (main 
occupation), cleanup  
-  relevant files: `isotovideo`, `needle.pm` (initial needle scan)
+  relevant files: `isotovideo`, `driver.pm`, `OpenQA/Isotovideo/Runner.pm`,
+                  `OpenQA/Isotovideo/CommandHandler.pm`, `needle.pm` (initial 
needle scan)
 
     * **backend**: spawns and handles backend (eg. qemu), receives commands 
from isotovideo IO-loop,
                    handles the VNC connections, makes regular screenshots  
-      relevant files: `baseclass.pm` and derived, `console.pm` and derived, 
`needle.pm` (reloading, matching), `cv.pm`, `ppmclibs/*`
+      relevant files: `baseclass.pm` and derived, `console.pm` and derived, 
`needle.pm` (reloading,
+                      matching), `cv.pm`, `ppmclibs/*`
 
         * **qemu** (for instance)
 
@@ -30,4 +32,29 @@
 * **isotovideo** starts everything and passes commands between the other 
processes.
 * All processes have an IO loop except **autotest**. The latter mainly 
executes the test code and
   everything else reacts to it.
-* The command server is accessed by the openQA worker and livehandler.
+* The command server is accessed by the openQA worker and livehandler and the 
SUT via
+  `autoinst_url`.
+
+### Details about IPC between main processes
+* **isotovideo**
+    * in `OpenQA::Isotovideo::Runner`: tokenless `read_json` from 
**autotest**, **backend**,
+      **command server**
+    * in `driver`: `send_json` and `read_json` pair *with* tokens to 
send/receive to/from
+      **backend**
+    * in `OpenQA::Isotovideo::CommandHandler`: `send_json` to send messages to 
**command server**
+    * in `OpenQA::Isotovideo::CommandHandler`: `send_json` to send messages to 
**backend**
+* **autotest**
+    * in `autotest`: `send_json` and `read_json` pair *with* tokens to 
send/receive to/from
+      **isotovideo**
+* **backend**
+    * in `baseclass:::check_socket`: tokenless `read_json` and optional 
`send_json` pair to handle
+      commands from **isotovideo** and send back result
+        * uses the multi-flag of `read_json` to handle multiple commands at 
once
+    * in `baseclass`: `send_json` in various places to send messages 
unrequested to **isotovideo**
+* **command server**
+    * in `commands`: tokenless `read_json` to handle commands from 
**isotovideo**
+        * uses the multi-flag of `read_json` to handle multiple commands at 
once
+    * in `commands::isotovideo_command`: `send_json` and `read_json` pair 
*without* tokens to
+      send messages to **isotovideo** and wait for a response
+    * in `OpenQA::Commands`: `send_json` to send messages from ws clients to 
**isotovideo** (which
+      might handle them directly or forward them to the backend)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/doc/backend_vars.md 
new/os-autoinst-5.1787055635.69d1218/doc/backend_vars.md
--- old/os-autoinst-5.1785768930.71d56f4/doc/backend_vars.md    2026-08-03 
16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/doc/backend_vars.md    2026-08-18 
14:20:35.000000000 +0200
@@ -41,7 +41,7 @@
 | PAUSE_ON_NEXT_COMMAND | boolean | 0 | Pause test execution on the next test 
API command. Same notes as for `PAUSE_AT` apply. |
 | PAUSE_ON_FAILURE | boolean | 0 | Pause test execution on a test failure 
(instead of invoking the post-fail hook and terminating). Same notes as for 
`PAUSE_AT` apply. |
 | _QUIET_SCRIPT_CALLS | boolean | 0 | Add quiet flag to all the calls to 
script_run, script_output and validate_script_output. It will omit all the 
squares "wait_serial expected" on the Details view of the test case. This 
option might be useful for serial terminal tests. |
-| PRETTY_SERIAL_MARKER | boolean | 1 | Enable "pretty" serial markers. When 
enabled, os-autoinst attempts to automatically detect SUT shell capabilities 
(like bash PROMPT_COMMAND and fc history) to forward command exit codes to 
serial without typing them visibly over VNC. Falls back to regular serial 
markers if no advanced shell features are detected. |
+| PRETTY_SERIAL_MARKER | boolean | 1 | Enable "pretty" serial markers. When 
enabled, os-autoinst attempts to automatically detect SUT shell capabilities 
(like bash PROMPT_COMMAND and fc history) to forward command exit codes to 
serial without typing them visibly over VNC. Falls back to regular serial 
markers if no advanced shell features are detected. Implication: the detection 
installs a persistent PROMPT_COMMAND hook into the SUT's `~/.bashrc` and 
`~/.profile`, which then emits `OA:DONE`/`OA:START` markers to 
`/dev/$SERIALDEV` (i.e. `serial0.txt`) on every prompt of every shell that 
sources those files (including login/`exec $SHELL`/`su` and non-serial-terminal 
VNC consoles). Because the hook is persisted, it survives into later jobs 
booting the same qcow. To keep `serial0.txt` completely clean (e.g. for tests 
that parse it as a data channel or that run mostly via the virtio serial 
terminal where markers already appear in `serial_terminal.txt`), set 
`PRETTY_SERIAL_MARKER=0` for th
 e whole cluster, i.e. on both the parent job that creates the qcow image (so 
no hook is ever baked in) and all downstream jobs. Note that on the virtio 
serial terminal itself the hook is never installed; the residual `serial0.txt` 
markers in such tests originate from the VNC/tty consoles they still use. |
 | _WAIT_STILL_SCREEN_ON_HERE_DOC_INPUT | float | 0 | If this value is greater 
then 0, it is used by `wait_still_screen` before starting to write the script 
into the here document used in `testapi::script_output()` function (see: 
poo#60566). By default this depends on the backend. |
 | AUTOINST_URL_HOSTNAME | string |  | hostname or IP address of host running 
the autoinst webserver endpoint, defaults to the local IP address within the 
qemu network for the qemu backend or the `WORKER_HOSTNAME` otherwise. |
 | UPLOAD_METER | boolean | 0 | Display curl progress meter in `upload_logs()` 
and `upload_assets()` test API functions. |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/myjsonrpc.pm 
new/os-autoinst-5.1787055635.69d1218/myjsonrpc.pm
--- old/os-autoinst-5.1785768930.71d56f4/myjsonrpc.pm   2026-08-03 
16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/myjsonrpc.pm   2026-08-18 
14:20:35.000000000 +0200
@@ -14,8 +14,11 @@
 use constant DEBUG_JSON => $ENV{PERL_MYJSONRPC_DEBUG} || 0;
 use constant READ_BUFFER => $ENV{PERL_MYJSONRPC_BYTES} || 8_000_000;
 
-# hash for keeping state
-my $sockets;
+my $interleaved_command_handler;    # handler to deal with commands received 
while waiting for another reply
+
+sub set_interleaved_command_handler ($handler_or_array) {
+    $interleaved_command_handler = ref $handler_or_array eq 'ARRAY' ? sub 
(@args) { push @$handler_or_array, \@args } : $handler_or_array;
+}
 
 sub _syswrite ($to_fd, $json, $length = undef, $offset = undef) { syswrite 
$to_fd, $json, $length, $offset }
 
@@ -60,49 +63,65 @@
 }
 
 # utility function
-sub read_json ($socket, $cmd_token = undef, $multi = undef) {
-    my $cjx = Cpanel::JSON::XS->new->utf8;
+my %RESULTS;
+my %CJX;
+
+sub _extract_result_for_cmd_token ($results, $cmd_token, $multi) {
+    return undef if $multi;
+    my $i = 0;
+    for my $hash (@$results) {
+        if ($cmd_token && ($hash->{json_cmd_token} || '') ne $cmd_token) {
+            ++$i;
+            next;
+        }
+        return splice @$results, $i, 1;
+    }
+    return undef;
+}
 
+sub read_json ($socket, $cmd_token = undef, $multi = undef) {
     my $fd = fileno $socket;
     bmwqemu::diag("read_json($fd)") if is_debug();
-    if (exists $sockets->{$fd}) {
-        # start with the trailing text from previous call
-        my $buffer = delete $sockets->{$fd};
-        $cjx->incr_parse($buffer);
-    }
 
+    # return excess result from previous invocation
+    my $results = $RESULTS{$fd} //= [];
+    my $single_result = _extract_result_for_cmd_token($results, $cmd_token, 
$multi);
+    return $single_result if defined $single_result;
+    return splice @$results if $multi && @$results;
+
+    my $cjx = $CJX{$fd} //= Cpanel::JSON::XS->new->utf8;
     my $s = IO::Select->new();
     $s->add($socket);
 
-    my @results;
-
     # the goal here is to find the end of the next valid JSON - and don't
     # add more data to it. As the backend sends things unasked, we might
     # run into the next message otherwise
     while (1) {
         my $hash = $cjx->incr_parse();
-        # remember the trailing text
         if ($hash) {
-            $sockets->{$fd} = $cjx->incr_text();
             bmwqemu::diag(sprintf 'read_json(%d) json_cmd_token=%s', $fd, 
$hash->{json_cmd_token} // 'no-token') if is_debug();
             if ($hash->{QUIT}) {
                 bmwqemu::diag('received magic close');
-                push @results, undef;
+                push @$results, undef;
+                last;
+            }
+            if ($cmd_token && ($hash->{json_cmd_token} || '') ne $cmd_token) {
+                $interleaved_command_handler ? 
$interleaved_command_handler->($hash, $socket) : (push @$results, $hash);
+                next;
+            }
+            else {
+                push @$results, $hash;
+                # parse all lines from buffer
+                next if $multi;
                 last;
             }
-            confess 'ERROR: the token does not match - questions and answers 
not in the right order' if $cmd_token && ($hash->{json_cmd_token} || '') ne 
$cmd_token; # uncoverable statement
-            push @results, $hash;
-            # parse all lines from buffer
-            next if $multi;
-            last;
         }
-        elsif ($multi and @results) {
+        elsif ($multi and @$results) {
             # read at least one item in list context
             last;
         }
 
         # wait for next read
-
         handle_read_error($fd) until (my @res = $s->can_read);
 
         my $qbuffer;
@@ -110,7 +129,8 @@
         $cjx->incr_parse($qbuffer);
     }
 
-    return $multi ? @results : $results[0];
+    return splice @$results if $multi;
+    return _extract_result_for_cmd_token($results, $cmd_token, $multi);
 }
 
 ###################################################################
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/t/03-testapi.t 
new/os-autoinst-5.1787055635.69d1218/t/03-testapi.t
--- old/os-autoinst-5.1785768930.71d56f4/t/03-testapi.t 2026-08-03 
16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/t/03-testapi.t 2026-08-18 
14:20:35.000000000 +0200
@@ -309,6 +309,9 @@
     });
     testapi::assert_screen_change(sub { }, 42);
     is $received_timeout, 42, 'timeout forwarded to wait_screen_change';
+
+    $mock_wsc->redefine(wait_screen_change => 0);
+    throws_ok { testapi::assert_screen_change(sub { }, 42) } 
qr/assert_screen_change failed to detect a screen change/, 'expected error 
message for no screen change';
 };
 
 is $autotest::current_test->{dents}, 0, 'no soft failures so far';
@@ -323,15 +326,21 @@
 always_explain $details unless $details_ok;
 $mock_bmwqemu->noop('log_call');
 
-require distribution;
-testapi::set_distribution(distribution->new());
-$autotest::last_milestone = {};
-select_console('a-console');
-is console('a-console')->{console}, 'a-console';
-is_deeply $autotest::activated_consoles, ['a-console'], 'Current console is 
activated';
-is is_serial_terminal, 0, 'Not a serial terminal';
-is current_console, 'a-console', 'Current console is the a-console';
-is console('b-console')->{console}, 'b-console', 'new console created on the 
fly';
+subtest console => sub {
+    require distribution;
+    testapi::set_distribution(distribution->new());
+    $autotest::last_milestone = {};
+    select_console('a-console');
+    is console('a-console')->{console}, 'a-console';
+    is_deeply $autotest::activated_consoles, ['a-console'], 'Current console 
is activated';
+    is is_serial_terminal, 0, 'Not a serial terminal';
+    is current_console, 'a-console', 'Current console is the a-console';
+    is console('b-console')->{console}, 'b-console', 'new console created on 
the fly';
+
+    $mod2->redefine(query_isotovideo => {error => 'on purpose'});
+    throws_ok { select_console('dummy') } qr/on purpose/, 'select_console dies 
as expected';
+    $mod2->unmock('query_isotovideo');
+};
 
 subtest 'script_run' => sub {
     local $bmwqemu::vars{PRETTY_SERIAL_MARKER} = 0;
@@ -406,7 +415,7 @@
     is assert_script_run('true', max_interval => 1), undef, 'nothing happens 
on success (slow typing)';
     is_deeply $cmds, [
         {
-            text => 'true; echo XXX-$?-',
+            text => '_OANM=1; true; echo XXX-$?-',
             cmd => 'backend_type_string',
             max_interval => 1
         },
@@ -659,7 +668,7 @@
     upload_logs '/var/log/messages';
     is_deeply $cmds, [
         {
-            text => 'curl --form upload=@/var/log/messages --form 
upname=basetest-messages 
http://localhost:4243/LookAtMeImAToken/uploadlog/messages; echo XXX-$?-',
+            text => '_OANM=1; curl --form upload=@/var/log/messages --form 
upname=basetest-messages 
http://localhost:4243/LookAtMeImAToken/uploadlog/messages; echo XXX-$?-',
             cmd => 'backend_type_string',
             max_interval => 250
         },
@@ -673,7 +682,7 @@
     upload_logs '/var/log/messages', failok => 1;
     is_deeply $cmds, [
         {
-            text => 'curl --form upload=@/var/log/messages --form 
upname=basetest-messages --max-time 90 
http://localhost:4243/LookAtMeImAToken/uploadlog/messages; echo XXX-$?-',
+            text => '_OANM=1; curl --form upload=@/var/log/messages --form 
upname=basetest-messages --max-time 90 
http://localhost:4243/LookAtMeImAToken/uploadlog/messages; echo XXX-$?-',
             cmd => 'backend_type_string',
             max_interval => 250
         },
@@ -696,7 +705,7 @@
     is_deeply $cmds, [
         {
             cmd => 'backend_type_string',
-            text => 'sudo rm /boot/grub/menu.lst; echo XXX-$?-',
+            text => '_OANM=1; sudo rm /boot/grub/menu.lst; echo XXX-$?-',
             max_interval => 250
         },
         {
@@ -921,7 +930,7 @@
     } qr/output not validating/, 'Die on output not match for regex';
     throws_ok {
         validate_script_output('script', ['Invalid parameter'])
-    } qr/coderef or regexp/, 'Die on invalid parameter';
+    } qr/Invalid use.*coderef or regexp/, 'Die on invalid parameter';
     throws_ok {
         validate_script_output('script', qr/error/, fail_message => 'foo bar')
     } qr/foo bar/, 'Die on output not match';
@@ -979,6 +988,10 @@
       'assert_still_screen forwards arguments to wait_still_screen';
     $fake_timeout = 1;
     ok !wait_still_screen, 'falsy return value on timeout';
+
+    $testapi->redefine(wait_still_screen => 0);
+    throws_ok { assert_still_screen; } qr/assert_still_screen failed to detect 
a still screen/,
+      'expeected error message for no still screen';
 };
 
 subtest 'test console::console argument settings' => sub {
@@ -1294,6 +1307,10 @@
 
 lives_ok { force_soft_failure('boo#42') } 'can call force_soft_failure';
 
+subtest 'get_var' => sub {
+    throws_ok { get_required_var 'NOT HERE' } qr/Could not retrieve required 
variable NOT HERE/, 'get_required_var dies with expected message';
+};
+
 subtest 'set_var' => sub {
     $cmds = [];
     lives_ok { set_var('FOO', 'BAR', reload_needles => 1) } 'can call set_var 
with reload_needles';
@@ -1366,6 +1383,7 @@
 like get_test_data('console.ref.json'), qr/area/, 'get_test_data can be 
called';
 $fake_exit = 0;
 lives_ok { become_root } 'become_root can be called';
+lives_ok { become_user('geeko') } 'become_user can be called';
 lives_ok { hold_key('ret') } 'hold_key can be called';
 lives_ok { release_key('ret') } 'release_key can be called';
 lives_ok { reset_consoles } 'reset_consoles can be called';
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/t/05-distribution.t 
new/os-autoinst-5.1787055635.69d1218/t/05-distribution.t
--- old/os-autoinst-5.1785768930.71d56f4/t/05-distribution.t    2026-08-03 
16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/t/05-distribution.t    2026-08-18 
14:20:35.000000000 +0200
@@ -53,24 +53,27 @@
             return $wait_serial_res;
     });
     $mock_testapi->redefine(is_serial_terminal => 1);
+    my $oanm_len = length '_OANM=1; ';
     $d->script_run('short_command');
-    # script_run calls wait_serial three times when on a serial
-    # console, the call we want to check - which actually types the
-    # command - is the second
-    my $cmdcall = $wait_serial_calls[1];
-    is $cmdcall->{buffer_size}, 141, 'appropriate buffer size used for short 
command';
+    # First script_run also detects marker capability, which types an extra
+    # 'unset PROMPT_COMMAND' and waits for the prompt, so the call that types
+    # the command is the third
+    my $cmdcall = $wait_serial_calls[2];
+    is $cmdcall->{buffer_size}, 141 + $oanm_len, 'appropriate buffer size used 
for short command (base + _OANM prefix)';
+    # marker capability is now cached, so no 'unset PROMPT_COMMAND' wait
+    # precedes this call: the command is at index 1
     @wait_serial_calls = ();
     $d->script_run('long_command' x 512);
     $cmdcall = $wait_serial_calls[1];
-    is $cmdcall->{buffer_size}, 6272, 'appropriate buffer size used for long 
command';
+    is $cmdcall->{buffer_size}, 6272 + $oanm_len, 'appropriate buffer size 
used for long command (base + _OANM prefix)';
 
     $wait_serial_res = 0;
     @wait_serial_calls = ();
-    throws_ok { $d->script_run('foo') } qr/typing command 'foo' timed out/, 
'timeout while typing command handled';
+    throws_ok { $d->script_run('foo') } qr/typing command '_OANM=1; foo' timed 
out/, 'timeout while typing command handled';
 
     @wait_serial_calls = ();
     combined_like { $d->script_run('foo', check_typing_cmd => 0) }
-    qr/typing command 'foo' timed out/, 'timeout while typing command just 
logged when opted-out';
+    qr/typing command '_OANM=1; foo' timed out/, 'timeout while typing command 
just logged when opted-out';
 };
 
 subtest 'pretty_serial_marker' => sub {
@@ -78,6 +81,7 @@
     my $mock_testapi = Test::MockModule->new('testapi');
     my $mock_bmwqemu = Test::MockModule->new('bmwqemu');
     $mock_bmwqemu->noop('log_call');
+    $mock_bmwqemu->noop('diag');
     my $typed_string = '';
     $mock_testapi->redefine(query_isotovideo => sub { });
     $mock_testapi->redefine(type_string => sub { $typed_string .= $_[0] });
@@ -101,7 +105,7 @@
     $mock_testapi->redefine(wait_serial => sub ($regexp, @) {
             return 'BASH:4.4:' if ref($regexp) eq 'Regexp' && 'BASH:4.4:' =~ 
$regexp;
             return 'FC:OK:' if ref($regexp) eq 'Regexp' && 'FC:OK:' =~ $regexp;
-            return 'OA:DONE-abcd-0-foo';
+            return 'OA:DONE-abcd-0-OA:foo3foo';
     });
 
     $d->{_serial_marker_level} = {};
@@ -130,13 +134,13 @@
     $mock_testapi->redefine(is_serial_terminal => sub { 1 });
     $typed_string = '';
     $d->script_run('foo');
-    like $typed_string, qr/foo; echo SR.*-.*-\n/, 'Level 1 uses classic marker 
on serial terminal';
+    like $typed_string, qr/_OANM=1; foo; echo SR.*-.*-\n/, 'Level 1 uses 
classic marker on serial terminal';
 
     $mock_testapi->redefine(wait_serial => sub ($pat, %) {
             return 0 if $pat =~ /foo; echo SR.*-\$\?-/;
             return 'SRfoo-0-';
     });
-    throws_ok { $d->script_run('foo') } qr/typing command 'foo' timed out/, 
'typing error handled in Level 1';
+    throws_ok { $d->script_run('foo') } qr/typing command '_OANM=1; foo' timed 
out/, 'typing error handled in Level 1';
 };
 
 subtest 'serial_marker_reinstall_cached_level' => sub {
@@ -150,7 +154,7 @@
     $d->invalidate_serial_marker_hook('test-console');
 
     is $d->detect_serial_marker_capability(), 2, 'Returns cached level 2';
-    like $typed, qr/grep -q _oap.*\. ~\/\.bashrc/, 'Calls 
install_serial_marker_hook (types consolidated setup with sourcing)';
+    like $typed, qr/grep -q '_OAPV=\d+'.*\. ~\/\.bashrc/, 'Calls 
install_serial_marker_hook (types consolidated setup with sourcing)';
     ok $d->{_serial_marker_hook_installed}->{'test-console'}, 'Hook marked as 
installed';
 };
 
@@ -171,11 +175,14 @@
     $mock_testapi->redefine(wait_serial => sub ($regexp, @) {
             return 'BASH:4.4:' if ref($regexp) eq 'Regexp' && 'BASH:4.4:' =~ 
$regexp;
             return 'FC:OK:' if ref($regexp) eq 'Regexp' && 'FC:OK:' =~ $regexp;
-            return 'OA:DONE-abcd-0-';
+            my $regexp_str = "$regexp";
+            my ($fp) = grep { (my $c = $regexp_str) =~ s/\\//g; $c =~ /\Q$_\E/ 
}
+              map { $d->sut_marker($_) } qw(foo bar baz qux);
+            return "OA:DONE-abcd-0-$fp";
     });
 
     $d->script_run('foo');
-    like $typed_string, qr/grep -q _oap.*_oap\(\).*OA:DONE.*\. ~\/\.bashrc/s, 
'Initial install';
+    like $typed_string, qr/grep -q '_OAPV=\d+'.*_oap\(\).*OA:DONE.*\. 
~\/\.bashrc/s, 'Initial install';
     $typed_string = '';
 
     # Simulate console selection (e.g. after reboot/login)
@@ -191,7 +198,7 @@
     $d->reset_serial_marker('test-console');
     $typed_string = '';
     $d->script_run('baz');
-    like $typed_string, qr/grep -q _oap.*_oap\(\).*OA:DONE.*\. ~\/\.bashrc/s, 
'Re-detect and re-install after resetting the serial marker';
+    like $typed_string, qr/grep -q '_OAPV=\d+'.*_oap\(\).*OA:DONE.*\. 
~\/\.bashrc/s, 'Re-detect and re-install after resetting the serial marker';
     like $typed_string, qr/baz\n/, 'Command typed after re-installation';
 
     # Case 3: select_console triggers reset
@@ -217,6 +224,61 @@
     }
 };
 
+subtest 'level3_marker_correlation' => sub {
+    my $d = distribution->new;
+    my $mock_testapi = Test::MockModule->new('testapi');
+    my $mock_bmwqemu = Test::MockModule->new('bmwqemu');
+    $mock_bmwqemu->noop('log_call');
+    $testapi::serialdev = 'ttyS0';
+
+    my $typed = '';
+    $mock_testapi->redefine(type_string => sub { $typed .= $_[0] });
+    $d->install_serial_marker_hook(3);
+    like $typed, qr'c=\$\(HISTTIMEFORMAT= history 1\);c=\$\{c#\*\[0-9\]  \}',
+      'level-3 shell hook captures the just-run command via in-memory history 
(no fc off-by-one)';
+    like $typed, qr'l=\$\{#c\};t=\$c;\[ \$l -ge 4 \]&&t=\$\{c: 
-4\};printf.*OA:DONE',
+      'level-3 shell hook emits the compact head4+len+tail4 command 
fingerprinting script';
+    unlike $typed, qr'fc -ln -1',
+      'level-3 shell hook no longer uses fc -ln -1 (off-by-one lag emitting 
the previous command marker one prompt late)';
+
+    $d->{_serial_marker_level}->{'test-console'} = 3;
+    $d->{_serial_marker_hook_installed}->{'test-console'} = 1;
+    $mock_testapi->redefine(current_console => sub { 'test-console' });
+    $mock_testapi->redefine(query_isotovideo => sub { });
+
+    my @regexes_seen;
+    $mock_testapi->redefine(wait_serial => sub ($regexp, @) {
+            push @regexes_seen, $regexp;
+            return 'OA:DONE-1234-0-OA:curl11logs';
+    });
+
+    my $exit_code = $d->script_run('curl --logs');
+    is $exit_code, 0, 'Level 3 script_run returns correct exit code on 
successful command match';
+    is scalar(@regexes_seen), 1, 'Only wait_serial for the anchored 
fingerprint is called when match succeeds';
+    like $regexes_seen[0], 
qr/OA:DONE-\[0-9a-f\]\{4\}-\(\\d\+\)-OA(?:\\:|:)curl11logs/,
+      'wait_serial matches the exact head4+len+tail4 command fingerprint of 
curl --logs';
+
+    my $fp = $d->sut_marker('curl --logs');
+    my $regex = qr/OA:DONE-[0-9a-f]{4}-(\d+)-\Q$fp\E/;
+    my $stale_systemctl = "OA:DONE-aaaa-1-OA:syst15_ctl\n";
+    my $stale_tar = "OA:DONE-bbbb-2-OA:tar_7_tar\n";
+    my $correct_curl = "OA:DONE-cccc-0-OA:curl11logs\n";
+
+    unlike $stale_systemctl, $regex, 'Stale unconsumed systemctl markers from 
other commands are ignored by the curl regex';
+    unlike $stale_tar, $regex, 'Stale unconsumed tar markers from other 
commands are ignored by the curl regex';
+    like $correct_curl, $regex, 'The target curl marker matches the anchored 
regex perfectly';
+
+    my ($extracted_exit) = ($correct_curl =~ $regex);
+    is $extracted_exit, 0, 'Exit code is correctly extracted from the 
fingerprinted marker';
+
+    @regexes_seen = ();
+    $mock_testapi->redefine(wait_serial => sub ($regexp, @) { push 
@regexes_seen, $regexp; return undef });
+    $exit_code = $d->script_run('curl --logs');
+    is $exit_code, undef, 'Anchored match miss fails closed with undef instead 
of an unreliable generic fallback';
+    is scalar(@regexes_seen), 1, 'No second generic wait_serial is issued, 
preventing stale-marker mismatch and doubled timeout';
+    like $regexes_seen[0], 
qr/OA:DONE-\[0-9a-f\]\{4\}-\(\\d\+\)-OA(?:\\:|:)curl11logs/, 'The single 
wait_serial call uses the anchored fingerprint';
+};
+
 subtest 'set expected serial and autoinst failures' => sub {
     my $d = distribution->new;
     my @failures = (
@@ -296,7 +358,7 @@
 
     # First install
     $d->install_serial_marker_hook(3);
-    like $typed, qr/grep -q _oap.*\. ~\/\.bashrc/, 'Types consolidated setup 
with persistence and sourcing';
+    like $typed, qr/grep -q '_OAPV=\d+'.*\. ~\/\.bashrc/, 'Types consolidated 
setup with persistence and sourcing';
     ok $d->{_serial_marker_hook_persistent}->{'test-console'}, 'Persistence 
marked';
 
     # Invalidate hook but keep persistence
@@ -306,6 +368,19 @@
     like $typed, qr/\. ~\/\.bashrc/, 'Types setup again (with sourcing) when 
invalidated';
 };
 
+subtest 'serial_marker_hook_version_migration' => sub {
+    my $d = distribution->new;
+    my $mock_testapi = Test::MockModule->new('testapi');
+    $mock_testapi->redefine(current_console => sub { 'test-console' });
+    my $typed = '';
+    $mock_testapi->redefine(type_string => sub { $typed .= $_[0] });
+    $d->install_serial_marker_hook(3);
+    like $typed, qr/grep -q '_OAPV=\d+'/, 'Guard keys on a version tag, not 
mere _oap presence';
+    like $typed, qr/sed -i '[^']*_oap[^']*' ~\/\.bashrc ~\/\.profile/,
+      'Stale hook lines are stripped before reinstall so an old-format _oap 
persisted by a previous os-autoinst is replaced';
+    like $typed, qr/echo '_OAPV=\d+;_oap\(\)/, 'The version tag is persisted 
together with the hook definition';
+};
+
 subtest 'serial_terminal_redirection_guard' => sub {
     my $d = distribution->new;
     my $mock_testapi = Test::MockModule->new('testapi');
@@ -391,6 +466,26 @@
     like $typed, qr/_oap/, 'hook is successfully re-installed on the next 
command after cached status is explicitly invalidated';
 };
 
+subtest 'become_user' => sub {
+    my $d = distribution->new;
+    my $mock_testapi = Test::MockModule->new('testapi');
+    my $mock_bmwqemu = Test::MockModule->new('bmwqemu');
+    $mock_bmwqemu->noop('log_call');
+    my $typed = '';
+    $mock_testapi->redefine(query_isotovideo => sub { });
+    $mock_testapi->redefine(type_string => sub { $typed .= $_[0] });
+    $mock_testapi->redefine(is_serial_terminal => sub { 0 });
+    $mock_testapi->redefine(current_console => sub { 'test-console' });
+    $mock_testapi->redefine(get_var => sub { $_[0] eq 'PRETTY_SERIAL_MARKER' ? 
1 : undef });
+    $testapi::serialdev = 'ttyS0';
+    $mock_testapi->redefine(wait_serial => undef);
+    $d->{_serial_marker_level}->{'test-console'} = 3;
+    $d->become_user('geeko');
+    like $typed, qr/su - geeko/, 'become_user types su command to switch 
session user';
+    like $typed, qr/_oap/, 'become_user automatically reinstalls the shell 
synchronization hook after invalidation';
+    throws_ok { $d->become_user('geeko; rm -rf /') } qr/Invalid username/, 
'invalid username is rejected';
+};
+
 done_testing;
 
 1;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/t/22-svirt.t 
new/os-autoinst-5.1787055635.69d1218/t/22-svirt.t
--- old/os-autoinst-5.1785768930.71d56f4/t/22-svirt.t   2026-08-03 
16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/t/22-svirt.t   2026-08-18 
14:20:35.000000000 +0200
@@ -117,8 +117,10 @@
     like shift @$cmds, qr/cat > \/t <<.*username=u.*password=p.*auth-esx-h/s, 
'config written';
     my $s = 'virsh -c esx://u@h/?no_verify=1\\&authfile=/t ';
     my @expected = (
+        $s . ' dominfo openQA-SUT-1 2>/dev/null',
         $s . ' destroy openQA-SUT-1',
         $s . ' undefine --snapshots-metadata openQA-SUT-1',
+        $s . ' dominfo openQA-SUT-1 2>/dev/null',
         $s . ' define /var/lib/libvirt/images/openQA-SUT-1.xml',
         'echo \'bios.bootDelay = "10000"\' >> 
/vmfs/volumes/datastore1/openQA/openQA-SUT-1.vmx',
         'test -e /vmfs/volumes/datastore1/openQA/openQA-SUT-1.nvram',
@@ -321,6 +323,22 @@
     ]);
 };
 
+subtest 'starting VMware console with define failure' => sub {
+    for my $tc (
+        {ret => 1, stderr => "some define error\n", expected => qr/virsh 
define failed: some define error/},
+        {ret => 2, stderr => undef, expected => qr/virsh define failed: exit 
code 2/}
+    ) {
+        my $mocks = _mock_svirt_vmware([], []);
+        $mocks->{console}->redefine(
+            run_cmd => sub ($self, $cmd, %args) {
+                return ($tc->{ret}, undef, $tc->{stderr}) if $cmd =~ /define/ 
&& $args{wantarray};
+                return 0;
+            }
+        );
+        throws_ok { $svirt_console->define_and_start } $tc->{expected};
+    }
+};
+
 subtest 'SSH credentials' => sub {
 
     set_var('VIRSH_GUEST', 'foo321');
@@ -668,15 +686,43 @@
         };
 
         subtest 'family svirt-xen-hvm create=1 error handling' => sub {
-            @ssh_cmd_return = ([1, '', 'lock'], [1, '', 'lock'], [1, '', 
'lock'], [1, '', 'lock'], [1, '', 'lock']);
+            # 5 failures, active recovery (destroy + pkill), 2 more failures 
-> dies
+            @ssh_cmd_return = (
+                [1, '', 'lock'],
+                [1, '', 'lock'],
+                [1, '', 'lock'],
+                [1, '', 'lock'],
+                [1, '', 'lock'],
+                [0, '', ''],    # virsh destroy
+                [0, '', ''],    # pkill
+                [1, '', 'lock'],
+                [1, '', 'lock'],
+            );
 
             my $dev_id = 'dev_id_005';
             my $exp_file = $svirt->name . $dev_id . '.img';
-            throws_ok { $svirt->add_disk({create => 1, size => '88G', dev_id 
=> $dev_id}) } qr/Too many attempts to create disk/, 'Died after 5 retry 
attempts';
+            throws_ok { $svirt->add_disk({create => 1, size => '88G', dev_id 
=> $dev_id}) } qr/Too many attempts to create disk/, 'died after 7 total 
attempts with active recovery';
+
+            # 5 failures, active recovery, then succeeds on 6th attempt
+            @ssh_cmd_return = (
+                [1, '', 'lock'],
+                [1, '', 'lock'],
+                [1, '', 'lock'],
+                [1, '', 'lock'],
+                [1, '', 'lock'],
+                [0, '', ''],    # virsh destroy
+                [0, '', ''],    # pkill
+                [0, '', ''],    # succeeds
+            );
+            @last_ssh_commands = ();
+            $svirt->add_disk({create => 1, size => '88G', dev_id => $dev_id});
+            is $last_ssh_commands[-2], "pkill -9 -f 'openQA-SUT-1'", 'active 
recovery ran pkill';
+            like $last_ssh_commands[-3], qr/destroy 'openQA-SUT-1'/, 'active 
recovery ran virsh destroy';
+            is $last_ssh_commands[-1], "qemu-img create '$basedir$exp_file' -f 
qcow2 88G", 'triggered img creation, after active recovery';
 
             @ssh_cmd_return = ([1, '', 'lock'], [1, '', 'lock'], [1, '', 
'lock'], [1, '', 'lock'], [0, '', '']);
             $svirt->add_disk({create => 1, size => '88G', dev_id => $dev_id});
-            is $last_ssh_commands[-1], "qemu-img create '$basedir$exp_file' -f 
qcow2 88G", 'Triggered img creation, after 4 errors';
+            is $last_ssh_commands[-1], "qemu-img create '$basedir$exp_file' -f 
qcow2 88G", 'triggered img creation, after 4 errors';
 
             @ssh_cmd_return = ([0, '', ''], [0, '', ''], [0, '', ''], [0, '', 
''], [0, '', '']);
 
@@ -729,6 +775,7 @@
                     size => 12
             });
             like $last_ssh_commands[0], 
qr%^rsync.*--partial.*/my/path/to/this/file/$file.*$basedir/$file%, 'Use rsync 
to copy file';
+            is $last_ssh_commands[1], "qemu-img info --force-share 
--output=json /my/path/to/this/file/$file", 'qemu-img info uses --force-share';
             is $last_ssh_commands[-1], "qemu-img create 
'${basedir}openQA-SUT-1$dev_id.img' -f qcow2 -F qcow2 -b '$basedir/$file' 12G", 
'Used image size > backingfile size';
         };
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/t/24-myjsonrpc.t 
new/os-autoinst-5.1787055635.69d1218/t/24-myjsonrpc.t
--- old/os-autoinst-5.1785768930.71d56f4/t/24-myjsonrpc.t       2026-08-03 
16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/t/24-myjsonrpc.t       2026-08-18 
14:20:35.000000000 +0200
@@ -66,6 +66,44 @@
     dies_ok { myjsonrpc::send_json($child, $send1) } 'myjsonrpc: remote end 
terminated connection, stopping';
 };
 
+subtest 'handling interleaved commands' => sub {
+    my ($sub_child, $sub_isotovideo);
+    socketpair $sub_child, $sub_isotovideo, AF_UNIX, SOCK_STREAM, PF_UNSPEC;
+    $sub_child->autoflush(1);
+    $sub_isotovideo->autoflush(1);
+
+    my $msg_a = {a => 1, json_cmd_token => 'token-A'};
+    my $msg_b = {b => 2, json_cmd_token => 'token-B'};
+    my $msg_c = {c => 3, json_cmd_token => 'token-C'};
+    my $msg_d = {d => 4, json_cmd_token => 'token-D'};
+
+    subtest 'interleaved_command_handler is not set' => sub {
+        myjsonrpc::send_json($sub_child, $msg_c);
+        myjsonrpc::send_json($sub_child, $msg_d);
+        myjsonrpc::send_json($sub_child, $msg_b);
+        myjsonrpc::send_json($sub_child, $msg_a);
+        my $read_a = myjsonrpc::read_json($sub_isotovideo, 'token-A');
+        is_deeply $read_a, $msg_a, 'read_json with token-A returns msg_a';
+        my $read_b = myjsonrpc::read_json($sub_isotovideo, 'token-B');
+        is_deeply $read_b, $msg_b, 'subsequent read_json call returns msg_b 
from cached results';
+        my @read_c_d = myjsonrpc::read_json($sub_isotovideo, undef, 1);
+        is_deeply \@read_c_d, [$msg_c, $msg_d], 'subsequent multi read_json 
call returns msg_c and msg_d from cached results';
+    };
+    subtest 'interleaved_command_handler is set' => sub {
+        my @interleaved;
+        myjsonrpc::set_interleaved_command_handler(\@interleaved);
+        myjsonrpc::send_json($sub_child, $msg_b);
+        myjsonrpc::send_json($sub_child, $msg_a);
+        my $read_a = myjsonrpc::read_json($sub_isotovideo, 'token-A');
+        is_deeply $read_a, $msg_a, 'read_json with token-A returns msg_a';
+        is_deeply \@interleaved, [[$msg_b, $sub_isotovideo]], 'message msg_b 
captured';
+    };
+
+    myjsonrpc::set_interleaved_command_handler(undef);
+    close $sub_child;
+    close $sub_isotovideo;
+};
+
 my $io_select_mock = Test::MockModule->new('IO::Select');
 $io_select_mock->redefine(can_read => undef);
 throws_ok { myjsonrpc::read_json($isotovideo) } qr/Illegal seek/, 'error 
exception raised when reading is aborted';
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1785768930.71d56f4/t/29-backend-svirt.t 
new/os-autoinst-5.1787055635.69d1218/t/29-backend-svirt.t
--- old/os-autoinst-5.1785768930.71d56f4/t/29-backend-svirt.t   2026-08-03 
16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/t/29-backend-svirt.t   2026-08-18 
14:20:35.000000000 +0200
@@ -54,6 +54,7 @@
     # silence some log output for cleaner tests
     $bmwqemu_mock->noop('diag');
     $bmwqemu_mock->noop('log_call');
+    $bmwqemu_mock->noop('fctwarn');
     redefine_ssh;
     $backend->{need_delete_log} = 1;
     ok $backend->do_start_vm, 'can start vm';
@@ -80,6 +81,7 @@
     # silence some log output for cleaner tests
     $bmwqemu_mock->noop('diag');
     $bmwqemu_mock->noop('log_call');
+    $bmwqemu_mock->noop('fctwarn');
     redefine_ssh;
     $backend->{need_delete_log} = 1;
     ok $backend->do_start_vm, 'can start vm';
@@ -104,6 +106,7 @@
     # silence some log output for cleaner tests
     $bmwqemu_mock->noop('diag');
     $bmwqemu_mock->noop('log_call');
+    $bmwqemu_mock->noop('fctwarn');
     redefine_ssh;
     ok $backend->do_start_vm, 'can start vm';
     is $backend->can_handle({function => 'snapshots'})->{ret}, 1, 'can handle 
snapshots';
@@ -126,4 +129,29 @@
     is $virsh, 'virsh my_vmm', 'correct output from virsh()';
 };
 
+subtest 'do_stop_vm_svirt validation' => sub {
+    my $backend = backend::svirt->new;
+    my $bmwqemu_mock = Test::MockModule->new('bmwqemu');
+    $bmwqemu_mock->noop('diag');
+    $bmwqemu_mock->noop('log_call');
+
+    subtest 'triggers fctwarn when the domain still exists after undefining' 
=> sub {
+        my @warn_log;
+        $bmwqemu_mock->redefine(fctwarn => sub { push @warn_log, @_ });
+        $run_ssh_cmd_mock->redefine(run_ssh_cmd => 0);
+        $backend->do_stop_vm_svirt;
+        like "@warn_log", qr/still exists/, 'logged fctwarn contains still 
exists warning';
+    };
+
+    subtest 'does not trigger fctwarn when the domain is successfully gone' => 
sub {
+        my @warn_log;
+        $bmwqemu_mock->redefine(fctwarn => sub { push @warn_log, @_ });
+        $run_ssh_cmd_mock->redefine(run_ssh_cmd => sub ($self, $cmd, %args) {
+                return $cmd =~ /dominfo/ ? 1 : 0;
+        });
+        $backend->do_stop_vm_svirt;
+        is scalar(@warn_log), 0, 'no warning was logged';
+    };
+};
+
 done_testing;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/os-autoinst-5.1785768930.71d56f4/testapi.pm 
new/os-autoinst-5.1787055635.69d1218/testapi.pm
--- old/os-autoinst-5.1785768930.71d56f4/testapi.pm     2026-08-03 
16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/testapi.pm     2026-08-18 
14:20:35.000000000 +0200
@@ -63,7 +63,7 @@
 
   wait_screen_change assert_screen_change wait_still_screen 
assert_still_screen wait_serial
   record_soft_failure record_info force_soft_failure
-  become_root x11_start_program ensure_installed eject_cd disconnect_usb power
+  become_root become_user x11_start_program ensure_installed eject_cd 
disconnect_usb power
 
   switch_network
   save_memory_dump freeze_vm resume_vm save_storage
@@ -244,7 +244,7 @@
 
 sub record_info ($title, $output = undef, %nargs) {
     $nargs{result} //= 'ok';
-    die 'unsupported $result \'' . $nargs{result} . '\'' unless 
_is_valid_result($nargs{result});
+    OpenQA::Exception::TestapiUsageError->throw(error => "unsupported \$result 
'$nargs{result}'") unless _is_valid_result($nargs{result});
     $output //= '';
     bmwqemu::log_call(title => $title, output => $output, %nargs);
     $autotest::current_test->record_resultfile($title, $output, %nargs);
@@ -375,8 +375,8 @@
 }
 
 sub _check_or_assert ($mustmatch, $check, %args) {
-    die 'no tags specified' if (!$mustmatch || (ref $mustmatch eq 'ARRAY' && 
scalar @$mustmatch == 0));
-    die 'current_test undefined' unless $autotest::current_test;
+    OpenQA::Exception::TestapiUsageError->throw(error => 'no tags specified') 
if (!$mustmatch || (ref $mustmatch eq 'ARRAY' && scalar @$mustmatch == 0));
+    OpenQA::Exception::TestapiUsageError->throw(error => 'current_test 
undefined') unless $autotest::current_test;
 
     $args{timeout} = bmwqemu::scale_timeout($args{timeout});
 
@@ -645,7 +645,8 @@
     # wait_screen_change uses prototype which expects code block as an argument
     # This resolves compile time issues
     my ($coderef, @args) = @_;
-    wait_screen_change(\&{$coderef}, @args) or die 'assert_screen_change 
failed to detect a screen change';
+    wait_screen_change(\&{$coderef}, @args) or
+      OpenQA::Exception::TestapiError->throw(error => 'assert_screen_change 
failed to detect a screen change');
 }
 
 
@@ -700,7 +701,7 @@
 =cut
 
 sub assert_still_screen (@args) {
-    wait_still_screen(@args) or die 'assert_still_screen failed to detect a 
still screen';
+    wait_still_screen(@args) or OpenQA::Exception::TestapiError->throw(error 
=> 'assert_still_screen failed to detect a still screen');
 }
 
 =head1 test variable access
@@ -727,7 +728,7 @@
 =cut
 
 sub get_required_var ($var) {
-    return $bmwqemu::vars{$var} // croak "Could not retrieve required variable 
$var";
+    return $bmwqemu::vars{$var} // 
OpenQA::Exception::TestapiError->throw(error => "Could not retrieve required 
variable $var");
 }
 
 =head2 set_var
@@ -1008,7 +1009,7 @@
 
     bmwqemu::log_call(cmd => $cmd, %args);
     my $ret = $distri->script_run($cmd, %args);
-    croak("command '$cmd' timed out") if $args{timeout} > 0 && !defined $ret;
+    OpenQA::Exception::TestapiError->throw(error => "command '$cmd' timed 
out") if $args{timeout} > 0 && !defined $ret;
     return $ret;
 }
 
@@ -1238,13 +1239,13 @@
           $script, $check, $output;
     }
     else {
-        croak 'Invalid use of validate_script_output(), second arg must be a 
coderef or regexp';
+        OpenQA::Exception::TestapiError->throw(error => 'Invalid use of 
validate_script_output(), second arg must be a coderef or regexp');
     }
     $autotest::current_test->record_resultfile(
         $title, $message,
         result => $res,
     );
-    croak $fail_message if $res eq 'fail';
+    OpenQA::Exception::TestapiError->throw(error => $fail_message) if $res eq 
'fail';
     return 0;
 }
 
@@ -1260,6 +1261,20 @@
 
 sub become_root () { $distri->become_root }
 
+=head2 become_user
+
+  become_user($user);
+
+Switch the current shell session's user to C<$user> (e.g. via C<su - $user>) 
and
+properly handle openQA serial marker synchronization hook invalidation so 
subsequent
+commands run cleanly under the new user.
+
+I<The implementation is distribution specific and not always available.>
+
+=cut
+
+sub become_user ($user) { $distri->become_user($user) }
+
 =head2 ensure_installed
 
   ensure_installed $package;
@@ -1439,7 +1454,7 @@
         }
         if ($wait_still && !wait_still_screen(stilltime => $wait_still,
                 timeout => $wait_timeout, similarity_level => 
$wait_sim_level)) {
-            die "wait_still_screen timed out after ${wait_timeout}s!";
+            OpenQA::Exception::TestapiError->throw(error => "wait_still_screen 
timed out after ${wait_timeout}s!");
         }
     }
 }
@@ -1606,7 +1621,7 @@
     }
     # If neither coordinates nor a needle is provided, report an error and 
quit.
     else {
-        die "The starting point of the drag was not correctly provided. Either 
provide the 'startx' and 'starty' coordinates, or a needle marking the starting 
point.";
+        OpenQA::Exception::TestapiUsageError->throw(error => "The starting 
point of the drag was not correctly provided. Either provide the 'startx' and 
'starty' coordinates, or a needle marking the starting point.");
     }
 
     # Repeat the same for endpoint coordinates or needles.
@@ -1620,7 +1635,7 @@
         ($endx, $endy) = _calculate_clickpoint($end_matched_needle);
     }
     else {
-        die "The ending point of the drag was not correctly provided. Either 
provide the 'endx' and 'endy' coordinates, or a needle marking the end point.";
+        OpenQA::Exception::TestapiUsageError->throw(error => "The ending point 
of the drag was not correctly provided. Either provide the 'endx' and 'endy' 
coordinates, or a needle marking the end point.");
     }
     # Get the button variable. If no button has been provided, assume the 
"left" button.
     my $button = $args{button} // 'left';
@@ -1704,7 +1719,7 @@
         $testapi_console_proxies{$testapi_console} = 
backend::console_proxy->new($testapi_console);
     }
     my $ret = query_isotovideo('backend_select_console', {testapi_console => 
$testapi_console});
-    die $ret->{error} if $ret->{error};
+    OpenQA::Exception::TestapiError->throw(error => $ret->{error}) if 
$ret->{error};
 
     $autotest::selected_console = $testapi_console;
     if ($ret->{activated}) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/os-autoinst-5.1785768930.71d56f4/tools/test_containers 
new/os-autoinst-5.1787055635.69d1218/tools/test_containers
--- old/os-autoinst-5.1785768930.71d56f4/tools/test_containers  2026-08-03 
16:55:30.000000000 +0200
+++ new/os-autoinst-5.1787055635.69d1218/tools/test_containers  2026-08-18 
14:20:35.000000000 +0200
@@ -7,5 +7,12 @@
     echo "$0 => Testing container $i"
     tag=${i,,}
     $cre build -t "$tag" -f "$i" "$(dirname "$i")"/
+    if grep -q "BuildTag:" "$i"; then
+        # shellcheck disable=SC2013
+        for t in $(sed -n 's/.*BuildTag: //p' "$i"); do
+            [[ "$t" == *%* ]] && continue
+            $cre tag "$tag" "$t"
+        done
+    fi
     $cre run --rm "$tag" --help
 done

++++++ os-autoinst.obsinfo ++++++
--- /var/tmp/diff_new_pack.Hpp0Ix/_old  2026-08-19 18:02:51.334761939 +0200
+++ /var/tmp/diff_new_pack.Hpp0Ix/_new  2026-08-19 18:02:51.339762118 +0200
@@ -1,5 +1,5 @@
 name: os-autoinst
-version: 5.1785768930.71d56f4
-mtime: 1785768930
-commit: 71d56f4caac7a7cbb21f22654bce7a7dbe4cc779
+version: 5.1787055635.69d1218
+mtime: 1787055635
+commit: 69d12181d071203c9ce43249351c5f5ac6760a3e
 

Reply via email to