add (schedule validated by systemd-analyze, units + script printed), list (next elapse + last result), run, logs, rm. Command stored as a script so quoting never meets ExecStart; PATH includes ~/.local/bin so agent tools resolve. cronr-* namespace guards foreign units. Smoked live: add -> run -> journal -> rm. Spec: doc/tool-parity.md 3.1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011KikHkfCiC3yELbsMN8fT9
85 lines
2.3 KiB
Go
85 lines
2.3 KiB
Go
package unit
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestValidName(t *testing.T) {
|
|
for _, ok := range []string{"nightly-ci-check", "a", "Job_2", "x1-y2"} {
|
|
if !ValidName(ok) {
|
|
t.Errorf("%q should be valid", ok)
|
|
}
|
|
}
|
|
for _, bad := range []string{"", "-leading", "has space", "slash/y", "ä", strings.Repeat("x", 65), "dot.name"} {
|
|
if ValidName(bad) {
|
|
t.Errorf("%q should be invalid", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScriptKeepsCommandVerbatim(t *testing.T) {
|
|
cmd := `agy -p "check CI, say 'hi' & notify" | tee /tmp/x`
|
|
s := Script(cmd)
|
|
if !strings.HasPrefix(s, "#!/bin/sh\n") {
|
|
t.Error("missing shebang")
|
|
}
|
|
if !strings.Contains(s, cmd) {
|
|
t.Error("command was mangled")
|
|
}
|
|
}
|
|
|
|
func TestServiceUnit(t *testing.T) {
|
|
s := Service("nightly", "/home/x/.local/share/cronr/nightly.sh")
|
|
for _, want := range []string{
|
|
"Type=oneshot",
|
|
"ExecStart=/bin/sh /home/x/.local/share/cronr/nightly.sh",
|
|
"Environment=PATH=%h/.local/bin",
|
|
} {
|
|
if !strings.Contains(s, want) {
|
|
t.Errorf("service missing %q:\n%s", want, s)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTimerUnit(t *testing.T) {
|
|
rec := Timer("nightly", "*-*-* 07:00", false)
|
|
if !strings.Contains(rec, "OnCalendar=*-*-* 07:00") || !strings.Contains(rec, "Persistent=true") {
|
|
t.Errorf("recurring timer wrong:\n%s", rec)
|
|
}
|
|
once := Timer("boot", "2026-08-06 03:00", true)
|
|
if !strings.Contains(once, "Persistent=false") {
|
|
t.Errorf("one-shot timer should not be persistent:\n%s", once)
|
|
}
|
|
if !strings.Contains(once, "WantedBy=timers.target") {
|
|
t.Error("timer missing install section")
|
|
}
|
|
}
|
|
|
|
func TestUnitAndJobNames(t *testing.T) {
|
|
svc, tmr := UnitNames("nightly")
|
|
if svc != "cronr-nightly.service" || tmr != "cronr-nightly.timer" {
|
|
t.Errorf("unit names: %s %s", svc, tmr)
|
|
}
|
|
if JobName("/home/x/.config/systemd/user/cronr-nightly.timer") != "nightly" {
|
|
t.Error("JobName failed on full path")
|
|
}
|
|
if JobName("helmd.service") != "" {
|
|
t.Error("foreign unit must not map to a job")
|
|
}
|
|
}
|
|
|
|
func TestAtToCalendar(t *testing.T) {
|
|
if _, err := AtToCalendar("2026-08-06 03:00"); err != nil {
|
|
t.Errorf("valid --at rejected: %v", err)
|
|
}
|
|
if _, err := AtToCalendar("2026-08-06 03:00:30"); err != nil {
|
|
t.Errorf("valid --at with seconds rejected: %v", err)
|
|
}
|
|
for _, bad := range []string{"imorgon", "03:00", "2026-8-6 03:00", "2026-08-06T03:00"} {
|
|
if _, err := AtToCalendar(bad); err == nil {
|
|
t.Errorf("bad --at %q accepted", bad)
|
|
}
|
|
}
|
|
}
|