]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
core: move StartTransient varlink tests to the right place 42161/head
authorMichael Vogt <michael@amutable.com>
Wed, 20 May 2026 09:47:00 +0000 (11:47 +0200)
committerMichael Vogt <michael@amutable.com>
Wed, 20 May 2026 14:11:45 +0000 (16:11 +0200)
This commit moves the io.systemd.Unit.StartTransient tests into
the right place in TEST-74-AUX-UTILS.varlinkctl-unit.sh.

Thanks to Ivan Kruglov for suggesting this.

test/units/TEST-26-SYSTEMCTL.sh
test/units/TEST-74-AUX-UTILS.varlinkctl-unit.sh

index d50ae68ef83b37e21bfca1ce7667a4cee5bb0d0a..75feedd3b097e65e64d2fd01582ff5fadee93764 100755 (executable)
@@ -519,239 +519,6 @@ systemctl show -P Markers "$UNIT_NAME" | grep needs-stop
 (! systemctl show -P Markers "$UNIT_NAME" | grep needs-reload)
 varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": []}}"
 
-# Test io.systemd.Unit.StartTransient
-MANAGER_SOCKET="/run/systemd/io.systemd.Manager"
-
-TRANSIENT_UNITS=()
-defer_transient_cleanup() {
-    TRANSIENT_UNITS+=("$1")
-}
-transient_cleanup() {
-    for u in "${TRANSIENT_UNITS[@]}"; do
-        systemctl stop "$u" 2>/dev/null || true
-        systemctl reset-failed "$u" 2>/dev/null || true
-    done
-}
-trap transient_cleanup EXIT
-
-# Basic oneshot transient service
-defer_transient_cleanup varlink-transient-test.service
-result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    '{"context":{"ID":"varlink-transient-test.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}')
-echo "$result" | grep '"context"' >/dev/null
-echo "$result" | grep '"runtime"' >/dev/null
-
-# Wait for completion
-timeout 30 bash -c 'until systemctl show -P ActiveState varlink-transient-test.service | grep inactive >/dev/null; do sleep 0.5; done'
-systemctl show -P Result varlink-transient-test.service | grep success >/dev/null
-
-# With explicit mode
-defer_transient_cleanup varlink-transient-test2.service
-result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    '{"context":{"ID":"varlink-transient-test2.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}},"mode":"fail"}')
-echo "$result" | grep '"context"' >/dev/null
-
-# Streaming with notifyJobChanges: should get intermediate state updates and a final result
-# Note: use --slurp + any() rather than 'select() -e' because in jq 1.6 (shipped on
-# CentOS 9) -e checks only the last input record's output, so a select() that filters
-# out the trailing record makes jq exit non-zero even when earlier records match.
-defer_transient_cleanup varlink-transient-test3.service
-result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    '{"context":{"ID":"varlink-transient-test3.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}},"notifyJobChanges":true}')
-printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.State == "waiting")' >/dev/null
-printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.Result == "done")' >/dev/null
-
-# Fire-and-forget: --more without notify flags should return immediately with context+runtime
-defer_transient_cleanup varlink-transient-fireforget.service
-result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    '{"context":{"ID":"varlink-transient-fireforget.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}')
-printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .context)' >/dev/null
-printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .runtime)' >/dev/null
-
-# Streaming with notifyUnitChanges: should get unit state change notifications
-defer_transient_cleanup varlink-transient-unitnotify.service
-result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    '{"context":{"ID":"varlink-transient-unitnotify.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}},"notifyUnitChanges":true}')
-printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .runtime.ActiveState)' >/dev/null
-
-# Streaming with both notifyJobChanges and notifyUnitChanges
-defer_transient_cleanup varlink-transient-both.service
-result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    '{"context":{"ID":"varlink-transient-both.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}},"notifyJobChanges":true,"notifyUnitChanges":true}')
-printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.State)' >/dev/null
-printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .runtime.ActiveState)' >/dev/null
-printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.Result == "done")' >/dev/null
-
-# prepare for the error case below: create a long-running service, then try to create it again while it's active
-defer_transient_cleanup varlink-transient-exists.service
-varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    '{"context":{"ID":"varlink-transient-exists.service","Service":{"ExecStart":[{"path":"/usr/bin/sleep","arguments":["/usr/bin/sleep","infinity"]}]}}}'
-timeout 10 bash -c 'until systemctl is-active varlink-transient-exists.service; do sleep 0.5; done'
-
-# Multiple ExecStart commands (oneshot allows multiple)
-defer_transient_cleanup varlink-transient-multi.service
-result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    '{"context":{"ID":"varlink-transient-multi.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"},{"path":"/bin/true"}]}},"notifyJobChanges":true}')
-printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.Result == "done")' >/dev/null
-
-# Transient service with Description and RemainAfterExit
-defer_transient_cleanup varlink-transient-desc.service
-result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    '{"context":{"ID":"varlink-transient-desc.service","Description":"Test description property","Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}')
-echo "$result" | jq -e '.context.Description == "Test description property"'
-echo "$result" | jq -e '.context.Service.Type == "oneshot"'
-echo "$result" | jq -e '.context.Service.RemainAfterExit == true'
-echo "$result" | jq -e '.context.Service.ExecStart[0].path == "/bin/true"'
-echo "$result" | jq -e '.runtime'
-
-# Transient service with explicit arguments
-defer_transient_cleanup varlink-transient-args.service
-result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    '{"context":{"ID":"varlink-transient-args.service","Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/echo","arguments":["/bin/echo","hello"]}]}}}')
-echo "$result" | jq -e '.context'
-echo "$result" | jq -e '.runtime'
-echo "$result" | jq -e '.context.Service.ExecStart[0].path == "/bin/echo"'
-echo "$result" | jq -e '.context.Service.ExecStart[0].arguments == ["/bin/echo", "hello"]'
-timeout 30 bash -c 'until systemctl is-active varlink-transient-args.service; do sleep 0.5; done'
-
-# Verify that omitting arguments defaults argv[0] to the path
-defer_transient_cleanup varlink-transient-noargs.service
-result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    '{"context":{"ID":"varlink-transient-noargs.service","Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}')
-echo "$result" | jq -e '.context.Service.ExecStart[0].arguments == ["/bin/true"]'
-timeout 30 bash -c 'until systemctl is-active varlink-transient-noargs.service; do sleep 0.5; done'
-
-# Exec.WorkingDirectory and Exec.Environment
-defer_transient_cleanup varlink-transient-exec.service
-result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    '{"context":{"ID":"varlink-transient-exec.service","Exec":{"WorkingDirectory":{"path":"/tmp","missingOK":false},"Environment":["FOO=bar","BAZ=qux"]},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}')
-echo "$result" | jq -e '.context.Exec.WorkingDirectory.path == "/tmp"'
-echo "$result" | jq -e '.context.Exec.Environment | index("FOO=bar") != null'
-echo "$result" | jq -e '.context.Exec.Environment | index("BAZ=qux") != null'
-timeout 30 bash -c 'until systemctl is-active varlink-transient-exec.service; do sleep 0.5; done'
-systemctl show -P WorkingDirectory varlink-transient-exec.service | grep '^/tmp$' >/dev/null
-systemctl show -P Environment varlink-transient-exec.service | grep 'FOO=bar' >/dev/null
-systemctl show -P Environment varlink-transient-exec.service | grep 'BAZ=qux' >/dev/null
-
-# WorkingDirectory with missingOK=true (path does not exist but unit still starts)
-defer_transient_cleanup varlink-transient-wd-missing.service
-varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    '{"context":{"ID":"varlink-transient-wd-missing.service","Exec":{"WorkingDirectory":{"path":"/nonexistent/path","missingOK":true}},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}'
-timeout 30 bash -c 'until systemctl is-active varlink-transient-wd-missing.service; do sleep 0.5; done'
-
-# WorkingDirectory with home=true, missingOK omitted (defaults to false)
-defer_transient_cleanup varlink-transient-wd-home.service
-varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    '{"context":{"ID":"varlink-transient-wd-home.service","Exec":{"WorkingDirectory":{"home":true}},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}'
-timeout 30 bash -c 'until systemctl is-active varlink-transient-wd-home.service; do sleep 0.5; done'
-systemctl show -P WorkingDirectory varlink-transient-wd-home.service | grep '^~$' >/dev/null
-
-# Exec.SetCredential: pass a credential and verify the running process can read it
-defer_transient_cleanup varlink-transient-cred.service
-CRED_VALUE_B64=$(printf 'secret-value' | base64 -w0)
-CRED_OUTPUT=$(mktemp)
-varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    "{\"context\":{\"ID\":\"varlink-transient-cred.service\",\"Exec\":{\"SetCredential\":[{\"id\":\"mycred\",\"value\":\"${CRED_VALUE_B64}\"}]},\"Service\":{\"Type\":\"oneshot\",\"RemainAfterExit\":true,\"ExecStart\":[{\"path\":\"/bin/sh\",\"arguments\":[\"/bin/sh\",\"-c\",\"cat \$CREDENTIALS_DIRECTORY/mycred > ${CRED_OUTPUT}\"]}]}}}"
-timeout 30 bash -c "until systemctl is-active varlink-transient-cred.service; do sleep 0.5; done"
-grep '^secret-value$' "$CRED_OUTPUT" >/dev/null
-rm -f "$CRED_OUTPUT"
-
-# Exec.User, Exec.Group, Exec.SupplementaryGroups, Exec.Nice
-# The nobody group is different on different distros so resolve here.
-NOBODY_GROUP=$(id -gn nobody)
-defer_transient_cleanup varlink-transient-ids.service
-ids_payload=$(jq -cn --arg g "$NOBODY_GROUP" \
-    '{context:{ID:"varlink-transient-ids.service",
-               Exec:{User:"nobody",Group:$g,SupplementaryGroups:[$g],Nice:5},
-               Service:{Type:"oneshot",RemainAfterExit:true,
-                        ExecStart:[{path:"/bin/true"}]}}}')
-result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient "$ids_payload")
-echo "$result" | jq -e '.context.Exec.User == "nobody"'
-echo "$result" | jq --arg g "$NOBODY_GROUP" -e '.context.Exec.Group == $g'
-echo "$result" | jq --arg g "$NOBODY_GROUP" -e '.context.Exec.SupplementaryGroups == [$g]'
-echo "$result" | jq -e '.context.Exec.Nice == 5'
-timeout 30 bash -c 'until systemctl is-active varlink-transient-ids.service; do sleep 0.5; done'
-systemctl show -P User varlink-transient-ids.service | grep '^nobody$' >/dev/null
-systemctl show -P Group varlink-transient-ids.service | grep "^${NOBODY_GROUP}$" >/dev/null
-systemctl show -P SupplementaryGroups varlink-transient-ids.service | grep "${NOBODY_GROUP}" >/dev/null
-systemctl show -P Nice varlink-transient-ids.service | grep '^5$' >/dev/null
-
-# Error cases: verify specific varlink error types
-set +o pipefail
-varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    '{"context":{"ID":"varlink-transient-exists.service","Service":{"ExecStart":[{"path":"/usr/bin/sleep","arguments":["/usr/bin/sleep","infinity"]}]}}}' |& grep "io.systemd.Unit.UnitExists"
-varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    '{"context":{"ID":"varlink-transient-test.target","Description":"test"}}' |& grep "io.systemd.Unit.UnitTypeNotSupported"
-# Apply-time and dispatch-time validation errors both surface as
-# org.varlink.service.InvalidParameter, with the offending field name in the
-# response parameters. Use --graceful to treat the expected error as success
-# so jq can assert on the dumped parameters JSON directly.
-expect_invalid_parameter() {
-    local payload="$1" field="$2"
-    varlinkctl call --graceful=org.varlink.service.InvalidParameter \
-                    "$MANAGER_SOCKET" io.systemd.Unit.StartTransient "$payload" \
-        | jq -e --arg f "$field" '.parameter == $f' >/dev/null
-}
-defer_transient_cleanup varlink-transient-bad.service
-expect_invalid_parameter \
-    '{"context":{"ID":"varlink-transient-bad.service","Service":{"Type":"simple"}}}' \
-    "context"
-# Invalid ExecStart path: exercises filename_or_absolute_path_is_valid() in transient_service_apply_properties()
-defer_transient_cleanup varlink-transient-badpath.service
-expect_invalid_parameter \
-    '{"context":{"ID":"varlink-transient-badpath.service","Service":{"Type":"simple","ExecStart":[{"path":""}]}}}' \
-    "Service.ExecStart"
-# Relative WorkingDirectory path is rejected
-defer_transient_cleanup varlink-transient-bad-wd.service
-expect_invalid_parameter \
-    '{"context":{"ID":"varlink-transient-bad-wd.service","Exec":{"WorkingDirectory":{"path":"relative/path","missingOK":false}},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \
-    "Exec.WorkingDirectory"
-# Malformed environment entry (not KEY=VALUE)
-defer_transient_cleanup varlink-transient-bad-env.service
-expect_invalid_parameter \
-    '{"context":{"ID":"varlink-transient-bad-env.service","Exec":{"Environment":["not_an_env_var"]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \
-    "Exec.Environment"
-# Invalid User= name is rejected at JSON dispatch time as a parameter error
-defer_transient_cleanup varlink-transient-bad-user.service
-expect_invalid_parameter \
-    '{"context":{"ID":"varlink-transient-bad-user.service","Exec":{"User":"bad/user"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \
-    "context"
-# Out-of-range Nice= value is rejected
-defer_transient_cleanup varlink-transient-bad-nice.service
-expect_invalid_parameter \
-    '{"context":{"ID":"varlink-transient-bad-nice.service","Exec":{"Nice":100},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \
-    "Exec.Nice"
-# Invalid credential ID
-defer_transient_cleanup varlink-transient-bad-cred-id.service
-expect_invalid_parameter \
-    '{"context":{"ID":"varlink-transient-bad-cred-id.service","Exec":{"SetCredential":[{"id":"bad/id","value":"YWJj"}]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \
-    "Exec.SetCredential"
-# Invalid base64 value for credential (rejected at JSON dispatch time as a parameter error)
-defer_transient_cleanup varlink-transient-bad-cred-value.service
-expect_invalid_parameter \
-    '{"context":{"ID":"varlink-transient-bad-cred-value.service","Exec":{"SetCredential":[{"id":"mycred","value":"!!!not_base64!!!"}]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \
-    "context"
-# Exec on a unit type without an exec context (.slice) is rejected
-varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    '{"context":{"ID":"varlink-transient-exec.slice","Exec":{"WorkingDirectory":{"path":"/tmp","missingOK":false}}}}' |& grep "io.systemd.Unit.UnitTypeNotSupported"
-# Unknown field in Exec is rejected as PropertyNotSupported
-defer_transient_cleanup varlink-transient-unknown-exec.service
-unsupported_exec=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    '{"context":{"ID":"varlink-transient-unknown-exec.service","Exec":{"RootDirectory":"/tmp"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true)
-echo "$unsupported_exec" | grep "io.systemd.Unit.PropertyNotSupported"
-echo "$unsupported_exec" | grep "Exec.RootDirectory"
-# Service field declared in the IDL but not yet settable at creation is rejected as PropertyNotSupported,
-# and the offending sub-property is identified
-defer_transient_cleanup varlink-transient-unknown-service.service
-unsupported_service=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
-    '{"context":{"ID":"varlink-transient-unknown-service.service","Service":{"Type":"oneshot","Restart":"always","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true)
-echo "$unsupported_service" | grep "io.systemd.Unit.PropertyNotSupported"
-echo "$unsupported_service" | grep "Service.Restart"
-set -o pipefail
-
-transient_cleanup
-trap - EXIT
-
 # --dry-run with destructive verbs
 # kexec is skipped intentionally, as it requires a bit more involved setup
 VERBS=(
index 2311e14a1458867b113f04ebac7461de4cbf00aa..c631c6f8896e1a5798ff52a51d8a4cd43069888a 100755 (executable)
@@ -81,3 +81,236 @@ varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$timer_par
 testuser_uid=$(id -u testuser)
 systemd-run --wait --pipe --user --machine testuser@ \
         varlinkctl --more call "/run/user/$testuser_uid/systemd/io.systemd.Manager" io.systemd.Unit.List '{}'
+
+# Test io.systemd.Unit.StartTransient
+MANAGER_SOCKET="/run/systemd/io.systemd.Manager"
+
+TRANSIENT_UNITS=()
+defer_transient_cleanup() {
+    TRANSIENT_UNITS+=("$1")
+}
+transient_cleanup() {
+    for u in "${TRANSIENT_UNITS[@]}"; do
+        systemctl stop "$u" 2>/dev/null || true
+        systemctl reset-failed "$u" 2>/dev/null || true
+    done
+}
+trap transient_cleanup EXIT
+
+# Basic oneshot transient service
+defer_transient_cleanup varlink-transient-test.service
+result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-test.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}')
+echo "$result" | grep '"context"' >/dev/null
+echo "$result" | grep '"runtime"' >/dev/null
+
+# Wait for completion
+timeout 30 bash -c 'until systemctl show -P ActiveState varlink-transient-test.service | grep inactive >/dev/null; do sleep 0.5; done'
+systemctl show -P Result varlink-transient-test.service | grep success >/dev/null
+
+# With explicit mode
+defer_transient_cleanup varlink-transient-test2.service
+result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-test2.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}},"mode":"fail"}')
+echo "$result" | grep '"context"' >/dev/null
+
+# Streaming with notifyJobChanges: should get intermediate state updates and a final result
+# Note: use --slurp + any() rather than 'select() -e' because in jq 1.6 (shipped on
+# CentOS 9) -e checks only the last input record's output, so a select() that filters
+# out the trailing record makes jq exit non-zero even when earlier records match.
+defer_transient_cleanup varlink-transient-test3.service
+result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-test3.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}},"notifyJobChanges":true}')
+printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.State == "waiting")' >/dev/null
+printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.Result == "done")' >/dev/null
+
+# Fire-and-forget: --more without notify flags should return immediately with context+runtime
+defer_transient_cleanup varlink-transient-fireforget.service
+result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-fireforget.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}')
+printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .context)' >/dev/null
+printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .runtime)' >/dev/null
+
+# Streaming with notifyUnitChanges: should get unit state change notifications
+defer_transient_cleanup varlink-transient-unitnotify.service
+result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-unitnotify.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}},"notifyUnitChanges":true}')
+printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .runtime.ActiveState)' >/dev/null
+
+# Streaming with both notifyJobChanges and notifyUnitChanges
+defer_transient_cleanup varlink-transient-both.service
+result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-both.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}},"notifyJobChanges":true,"notifyUnitChanges":true}')
+printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.State)' >/dev/null
+printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .runtime.ActiveState)' >/dev/null
+printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.Result == "done")' >/dev/null
+
+# prepare for the error case below: create a long-running service, then try to create it again while it's active
+defer_transient_cleanup varlink-transient-exists.service
+varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-exists.service","Service":{"ExecStart":[{"path":"/usr/bin/sleep","arguments":["/usr/bin/sleep","infinity"]}]}}}'
+timeout 10 bash -c 'until systemctl is-active varlink-transient-exists.service; do sleep 0.5; done'
+
+# Multiple ExecStart commands (oneshot allows multiple)
+defer_transient_cleanup varlink-transient-multi.service
+result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-multi.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"},{"path":"/bin/true"}]}},"notifyJobChanges":true}')
+printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.Result == "done")' >/dev/null
+
+# Transient service with Description and RemainAfterExit
+defer_transient_cleanup varlink-transient-desc.service
+result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-desc.service","Description":"Test description property","Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}')
+echo "$result" | jq -e '.context.Description == "Test description property"'
+echo "$result" | jq -e '.context.Service.Type == "oneshot"'
+echo "$result" | jq -e '.context.Service.RemainAfterExit == true'
+echo "$result" | jq -e '.context.Service.ExecStart[0].path == "/bin/true"'
+echo "$result" | jq -e '.runtime'
+
+# Transient service with explicit arguments
+defer_transient_cleanup varlink-transient-args.service
+result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-args.service","Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/echo","arguments":["/bin/echo","hello"]}]}}}')
+echo "$result" | jq -e '.context'
+echo "$result" | jq -e '.runtime'
+echo "$result" | jq -e '.context.Service.ExecStart[0].path == "/bin/echo"'
+echo "$result" | jq -e '.context.Service.ExecStart[0].arguments == ["/bin/echo", "hello"]'
+timeout 30 bash -c 'until systemctl is-active varlink-transient-args.service; do sleep 0.5; done'
+
+# Verify that omitting arguments defaults argv[0] to the path
+defer_transient_cleanup varlink-transient-noargs.service
+result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-noargs.service","Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}')
+echo "$result" | jq -e '.context.Service.ExecStart[0].arguments == ["/bin/true"]'
+timeout 30 bash -c 'until systemctl is-active varlink-transient-noargs.service; do sleep 0.5; done'
+
+# Exec.WorkingDirectory and Exec.Environment
+defer_transient_cleanup varlink-transient-exec.service
+result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-exec.service","Exec":{"WorkingDirectory":{"path":"/tmp","missingOK":false},"Environment":["FOO=bar","BAZ=qux"]},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}')
+echo "$result" | jq -e '.context.Exec.WorkingDirectory.path == "/tmp"'
+echo "$result" | jq -e '.context.Exec.Environment | index("FOO=bar") != null'
+echo "$result" | jq -e '.context.Exec.Environment | index("BAZ=qux") != null'
+timeout 30 bash -c 'until systemctl is-active varlink-transient-exec.service; do sleep 0.5; done'
+systemctl show -P WorkingDirectory varlink-transient-exec.service | grep '^/tmp$' >/dev/null
+systemctl show -P Environment varlink-transient-exec.service | grep 'FOO=bar' >/dev/null
+systemctl show -P Environment varlink-transient-exec.service | grep 'BAZ=qux' >/dev/null
+
+# WorkingDirectory with missingOK=true (path does not exist but unit still starts)
+defer_transient_cleanup varlink-transient-wd-missing.service
+varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-wd-missing.service","Exec":{"WorkingDirectory":{"path":"/nonexistent/path","missingOK":true}},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}'
+timeout 30 bash -c 'until systemctl is-active varlink-transient-wd-missing.service; do sleep 0.5; done'
+
+# WorkingDirectory with home=true, missingOK omitted (defaults to false)
+defer_transient_cleanup varlink-transient-wd-home.service
+varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-wd-home.service","Exec":{"WorkingDirectory":{"home":true}},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}'
+timeout 30 bash -c 'until systemctl is-active varlink-transient-wd-home.service; do sleep 0.5; done'
+systemctl show -P WorkingDirectory varlink-transient-wd-home.service | grep '^~$' >/dev/null
+
+# Exec.SetCredential: pass a credential and verify the running process can read it
+defer_transient_cleanup varlink-transient-cred.service
+CRED_VALUE_B64=$(printf 'secret-value' | base64 -w0)
+CRED_OUTPUT=$(mktemp)
+varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    "{\"context\":{\"ID\":\"varlink-transient-cred.service\",\"Exec\":{\"SetCredential\":[{\"id\":\"mycred\",\"value\":\"${CRED_VALUE_B64}\"}]},\"Service\":{\"Type\":\"oneshot\",\"RemainAfterExit\":true,\"ExecStart\":[{\"path\":\"/bin/sh\",\"arguments\":[\"/bin/sh\",\"-c\",\"cat \$CREDENTIALS_DIRECTORY/mycred > ${CRED_OUTPUT}\"]}]}}}"
+timeout 30 bash -c "until systemctl is-active varlink-transient-cred.service; do sleep 0.5; done"
+grep '^secret-value$' "$CRED_OUTPUT" >/dev/null
+rm -f "$CRED_OUTPUT"
+
+# Exec.User, Exec.Group, Exec.SupplementaryGroups, Exec.Nice
+# The nobody group is different on different distros so resolve here.
+NOBODY_GROUP=$(id -gn nobody)
+defer_transient_cleanup varlink-transient-ids.service
+ids_payload=$(jq -cn --arg g "$NOBODY_GROUP" \
+    '{context:{ID:"varlink-transient-ids.service",
+               Exec:{User:"nobody",Group:$g,SupplementaryGroups:[$g],Nice:5},
+               Service:{Type:"oneshot",RemainAfterExit:true,
+                        ExecStart:[{path:"/bin/true"}]}}}')
+result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient "$ids_payload")
+echo "$result" | jq -e '.context.Exec.User == "nobody"'
+echo "$result" | jq --arg g "$NOBODY_GROUP" -e '.context.Exec.Group == $g'
+echo "$result" | jq --arg g "$NOBODY_GROUP" -e '.context.Exec.SupplementaryGroups == [$g]'
+echo "$result" | jq -e '.context.Exec.Nice == 5'
+timeout 30 bash -c 'until systemctl is-active varlink-transient-ids.service; do sleep 0.5; done'
+systemctl show -P User varlink-transient-ids.service | grep '^nobody$' >/dev/null
+systemctl show -P Group varlink-transient-ids.service | grep "^${NOBODY_GROUP}$" >/dev/null
+systemctl show -P SupplementaryGroups varlink-transient-ids.service | grep "${NOBODY_GROUP}" >/dev/null
+systemctl show -P Nice varlink-transient-ids.service | grep '^5$' >/dev/null
+
+# Error cases: verify specific varlink error types
+set +o pipefail
+varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-exists.service","Service":{"ExecStart":[{"path":"/usr/bin/sleep","arguments":["/usr/bin/sleep","infinity"]}]}}}' |& grep "io.systemd.Unit.UnitExists"
+varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-test.target","Description":"test"}}' |& grep "io.systemd.Unit.UnitTypeNotSupported"
+# Apply-time and dispatch-time validation errors both surface as
+# org.varlink.service.InvalidParameter, with the offending field name in the
+# response parameters. Use --graceful to treat the expected error as success
+# so jq can assert on the dumped parameters JSON directly.
+expect_invalid_parameter() {
+    local payload="$1" field="$2"
+    varlinkctl call --graceful=org.varlink.service.InvalidParameter \
+                    "$MANAGER_SOCKET" io.systemd.Unit.StartTransient "$payload" \
+        | jq -e --arg f "$field" '.parameter == $f' >/dev/null
+}
+defer_transient_cleanup varlink-transient-bad.service
+expect_invalid_parameter \
+    '{"context":{"ID":"varlink-transient-bad.service","Service":{"Type":"simple"}}}' \
+    "context"
+# Invalid ExecStart path: exercises filename_or_absolute_path_is_valid() in transient_service_apply_properties()
+defer_transient_cleanup varlink-transient-badpath.service
+expect_invalid_parameter \
+    '{"context":{"ID":"varlink-transient-badpath.service","Service":{"Type":"simple","ExecStart":[{"path":""}]}}}' \
+    "Service.ExecStart"
+# Relative WorkingDirectory path is rejected
+defer_transient_cleanup varlink-transient-bad-wd.service
+expect_invalid_parameter \
+    '{"context":{"ID":"varlink-transient-bad-wd.service","Exec":{"WorkingDirectory":{"path":"relative/path","missingOK":false}},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \
+    "Exec.WorkingDirectory"
+# Malformed environment entry (not KEY=VALUE)
+defer_transient_cleanup varlink-transient-bad-env.service
+expect_invalid_parameter \
+    '{"context":{"ID":"varlink-transient-bad-env.service","Exec":{"Environment":["not_an_env_var"]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \
+    "Exec.Environment"
+# Invalid User= name is rejected at JSON dispatch time as a parameter error
+defer_transient_cleanup varlink-transient-bad-user.service
+expect_invalid_parameter \
+    '{"context":{"ID":"varlink-transient-bad-user.service","Exec":{"User":"bad/user"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \
+    "context"
+# Out-of-range Nice= value is rejected
+defer_transient_cleanup varlink-transient-bad-nice.service
+expect_invalid_parameter \
+    '{"context":{"ID":"varlink-transient-bad-nice.service","Exec":{"Nice":100},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \
+    "Exec.Nice"
+# Invalid credential ID
+defer_transient_cleanup varlink-transient-bad-cred-id.service
+expect_invalid_parameter \
+    '{"context":{"ID":"varlink-transient-bad-cred-id.service","Exec":{"SetCredential":[{"id":"bad/id","value":"YWJj"}]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \
+    "Exec.SetCredential"
+# Invalid base64 value for credential (rejected at JSON dispatch time as a parameter error)
+defer_transient_cleanup varlink-transient-bad-cred-value.service
+expect_invalid_parameter \
+    '{"context":{"ID":"varlink-transient-bad-cred-value.service","Exec":{"SetCredential":[{"id":"mycred","value":"!!!not_base64!!!"}]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \
+    "context"
+# Exec on a unit type without an exec context (.slice) is rejected
+varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-exec.slice","Exec":{"WorkingDirectory":{"path":"/tmp","missingOK":false}}}}' |& grep "io.systemd.Unit.UnitTypeNotSupported"
+# Unknown field in Exec is rejected as PropertyNotSupported
+defer_transient_cleanup varlink-transient-unknown-exec.service
+unsupported_exec=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-unknown-exec.service","Exec":{"RootDirectory":"/tmp"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true)
+echo "$unsupported_exec" | grep "io.systemd.Unit.PropertyNotSupported"
+echo "$unsupported_exec" | grep "Exec.RootDirectory"
+# Service field declared in the IDL but not yet settable at creation is rejected as PropertyNotSupported,
+# and the offending sub-property is identified
+defer_transient_cleanup varlink-transient-unknown-service.service
+unsupported_service=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \
+    '{"context":{"ID":"varlink-transient-unknown-service.service","Service":{"Type":"oneshot","Restart":"always","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true)
+echo "$unsupported_service" | grep "io.systemd.Unit.PropertyNotSupported"
+echo "$unsupported_service" | grep "Service.Restart"
+set -o pipefail
+
+transient_cleanup
+trap - EXIT