One kind for the module's own code, with three modes
The first cut of this added a `daemon` for the long-running case alone. That would have meant a new vocabulary entry for each of the others — a scheduled task, a run-once migration, a health check — when they are one thing run at different cadences. That is a field, not four entries in a vocabulary where every entry widens what a compromised control plane can express. So it mirrors a container exactly, because it IS a container's twin: the same intent, hosted by the machine's own supervisor instead of a runtime. Stays up, runs once, or runs on a schedule. Tools, hooks and event consumers are not further modes. They are loaded by a tool host, which is itself a process that stays up — so the generic case already covers them, which is the test of whether it is generic. A scheduled process gets a timer and a unit that finishes; a long-running one gets a unit that is restarted when it exits. Getting that wrong either way is a second copy running continuously between fires, or a schedule that never fires. The modes are exclusive and validation says so near the author: something that runs once does not run on a schedule, and something not running between fires cannot be restarted when a file changes. A missed fire happens when the machine comes back rather than being skipped, which is the difference between a machine that was down and a schedule that quietly stopped. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package declaration
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func aProcess() *Process {
|
||||
return &Process{
|
||||
ID: "server", Type: TypeProcess, Name: "greeter",
|
||||
Source: "https://store.invalid/greeter/daemon",
|
||||
Digest: "sha256:" + strings.Repeat("a", 64),
|
||||
Run: []string{"node", "index.js"},
|
||||
}
|
||||
}
|
||||
|
||||
// A process is part of the vocabulary, or a declaration carrying one is refused whole.
|
||||
func TestAProcessIsSomethingTheHostSpeaks(t *testing.T) {
|
||||
var found bool
|
||||
for _, kind := range Vocabulary() {
|
||||
if kind == TypeProcess {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("a process cannot be declared, so a module that declares one is refused")
|
||||
}
|
||||
if newOf(TypeProcess) == nil {
|
||||
t.Fatal("the decoder has no daemon, so one would be refused as an unknown kind")
|
||||
}
|
||||
}
|
||||
|
||||
// **Pinned by digest, like everything else that crosses a network.** A bundle fetched by a
|
||||
// reference somebody can repoint is not pinned, and it is the one thing on a machine that would
|
||||
// then be running code nobody reviewed.
|
||||
func TestAProcesssBundleMustBePinned(t *testing.T) {
|
||||
for _, bad := range []string{"", "latest", "sha256:short", strings.Repeat("a", 64)} {
|
||||
d := aProcess()
|
||||
d.Digest = bad
|
||||
if problems := d.validate("a process", false); len(problems) == 0 {
|
||||
t.Fatalf("a process pinned by %q was accepted", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// What to run is named, never inferred. Guessing an entrypoint from which files are present makes
|
||||
// a process change what it runs when somebody adds a file.
|
||||
func TestAProcessMustSayWhatToRun(t *testing.T) {
|
||||
d := aProcess()
|
||||
d.Run = nil
|
||||
if problems := d.validate("a process", false); len(problems) == 0 {
|
||||
t.Fatal("a process with no command was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
// Its name becomes a unit name and a path, so a separator in it would write somewhere nobody meant.
|
||||
func TestAProcesssNameCannotEscapeItsUnit(t *testing.T) {
|
||||
for _, bad := range []string{"", "../escape", "two words", "a/b"} {
|
||||
d := aProcess()
|
||||
d.Name = bad
|
||||
if problems := d.validate("a process", false); len(problems) == 0 {
|
||||
t.Fatalf("a process called %q was accepted", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// And a well-formed one is accepted, or the tests above prove only that everything is refused.
|
||||
func TestAWellFormedDaemonIsAccepted(t *testing.T) {
|
||||
if problems := aProcess().validate("a process", false); len(problems) != 0 {
|
||||
t.Fatalf("a well-formed daemon was refused: %v", problems)
|
||||
}
|
||||
}
|
||||
|
||||
// **The modes are exclusive, and saying so is the point of having one kind.** Something that runs
|
||||
// once does not run on a schedule; something not running between fires cannot be restarted when a
|
||||
// file changes. A container's modes carry the same rule, and this is the same rule because it is
|
||||
// the same thing hosted differently.
|
||||
func TestTheModesAreExclusive(t *testing.T) {
|
||||
both := aProcess()
|
||||
both.RunOnce = true
|
||||
both.Schedule = "0 3 * * *"
|
||||
if problems := both.validate("a process", false); len(problems) == 0 {
|
||||
t.Fatal("a process that runs once and on a schedule was accepted")
|
||||
}
|
||||
|
||||
watching := aProcess()
|
||||
watching.Schedule = "0 3 * * *"
|
||||
watching.RestartOn = []string{"some-file"}
|
||||
if problems := watching.validate("a process", false); len(problems) == 0 {
|
||||
t.Fatal("a scheduled process was given something to restart on, and it is never running")
|
||||
}
|
||||
}
|
||||
|
||||
// A cadence that is not a cadence is refused near its author, rather than by a machine at the far
|
||||
// end of a declaration.
|
||||
func TestAScheduleMustBeACadence(t *testing.T) {
|
||||
for _, bad := range []string{"often", "0 3 * *", "99 3 * * *"} {
|
||||
p := aProcess()
|
||||
p.Schedule = bad
|
||||
if problems := p.validate("a process", false); len(problems) == 0 {
|
||||
t.Fatalf("a process scheduled %q was accepted", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// And each mode on its own is accepted, or the tests above prove only that everything is refused.
|
||||
func TestEachModeOnItsOwnIsAccepted(t *testing.T) {
|
||||
once := aProcess()
|
||||
once.RunOnce = true
|
||||
if problems := once.validate("a process", false); len(problems) != 0 {
|
||||
t.Fatalf("a step was refused: %v", problems)
|
||||
}
|
||||
every := aProcess()
|
||||
every.Schedule = "0 3 * * *"
|
||||
if problems := every.validate("a process", false); len(problems) != 0 {
|
||||
t.Fatalf("a scheduled process was refused: %v", problems)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user