at_exit() {
set +e
+ systemctl stop test-sysupdate-notify-recorder.socket
+ rm -f /run/systemd/system/test-sysupdate-notify-recorder.socket \
+ /run/systemd/system/test-sysupdate-notify-recorder@.service
+
losetup -n --output NAME --associated "$BACKING_FILE" | while read -r loop_dev; do
losetup --detach "$loop_dev"
done
rm -rf "$CONFIGDIR" "$INSTALLDB" "$CLEANUP"
+# Verify the notification callout: after a successful update, sysupdate must connect to every socket in
+# /run/systemd/sysupdate/notify/ and invoke io.systemd.SysUpdate.Notify.OnCompletedUpdate(). We hook a tiny
+# recorder socket into that directory that captures the request and replies with success.
+NOTIFY_LOG="$WORKDIR/notify.log"
+rm -f "$NOTIFY_LOG"
+
+cat >"$WORKDIR/notify-recorder.py" <<EOF
+#!/usr/bin/env python3
+# Minimal Varlink server: read one NUL-terminated request, record it, reply with empty parameters.
+import sys
+buf = b""
+while True:
+ c = sys.stdin.buffer.read(1)
+ if not c or c == b"\x00":
+ break
+ buf += c
+with open("$NOTIFY_LOG", "ab") as f:
+ f.write(buf + b"\n")
+sys.stdout.buffer.write(b'{"parameters":{}}\x00')
+sys.stdout.buffer.flush()
+EOF
+chmod +x "$WORKDIR/notify-recorder.py"
+
+cat >/run/systemd/system/test-sysupdate-notify-recorder.socket <<EOF
+[Socket]
+ListenStream=/run/systemd/sysupdate/notify/io.test.SysUpdateRecorder
+Accept=yes
+EOF
+
+cat >"/run/systemd/system/test-sysupdate-notify-recorder@.service" <<EOF
+[Service]
+ExecStart=$WORKDIR/notify-recorder.py
+StandardInput=socket
+StandardOutput=socket
+EOF
+
+systemctl daemon-reload
+systemctl start test-sysupdate-notify-recorder.socket
+
+rm -rf "$CONFIGDIR" "$WORKDIR/blobs"
+mkdir -p "$CONFIGDIR" "$WORKDIR/blobs"
+echo "hello" >"$WORKDIR/source/notifytest-v1.bin"
+(cd "$WORKDIR/source" && sha256sum notifytest-v1.bin >SHA256SUMS)
+cat >"$CONFIGDIR/01-notifytest.transfer" <<EOF
+[Source]
+Type=url-file
+Path=file://$WORKDIR/source
+MatchPattern=notifytest-@v.bin
+
+[Target]
+Type=regular-file
+Path=$WORKDIR/blobs
+MatchPattern=notifytest-@v.bin
+InstancesMax=1
+EOF
+
+# A real update must trigger exactly one notification carrying the version and the updated resources.
+# The callout is synchronous (sysupdate blocks until the subscriber replied, which happens after the
+# request was recorded), so the log is fully written by the time the update returns.
+"$SYSUPDATE" --verify=no update
+test -s "$NOTIFY_LOG" # the notification must have been recorded
+notify_line="$(tail -n1 "$NOTIFY_LOG")"
+echo "Recorded notification: $notify_line"
+jq -e '.method == "io.systemd.SysUpdate.Notify.OnCompletedUpdate"' <<<"$notify_line" >/dev/null
+jq -e '.parameters.version == "v1"' <<<"$notify_line" >/dev/null
+jq -e '.parameters.resources | length >= 1' <<<"$notify_line" >/dev/null
+jq -e '.parameters.resources | all(has("transfer"))' <<<"$notify_line" >/dev/null
+
+# A no-op update ("No update needed") must NOT emit a notification.
+rm -f "$NOTIFY_LOG"
+"$SYSUPDATE" --verify=no update
+test ! -s "$NOTIFY_LOG"
+
+systemctl stop test-sysupdate-notify-recorder.socket
+rm -f /run/systemd/system/test-sysupdate-notify-recorder.socket \
+ /run/systemd/system/test-sysupdate-notify-recorder@.service
+systemctl daemon-reload
+rm -rf "$CONFIGDIR" "$WORKDIR/blobs"
+rm -f "$WORKDIR/source/notifytest-v1.bin" "$WORKDIR/source/SHA256SUMS" \
+ "$WORKDIR/notify-recorder.py" "$NOTIFY_LOG"
+
touch /testok