hub v0.143.1 (code): Reply-To on household mails (decision 174); R7 told-kernel refusal is temporary (R-898 test)
gates / gates (push) Successful in 3m21s
gates / gates (push) Successful in 3m21s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
go: ring 0 back to select pending-kernel -> TestKernel_Ring0ToldNightStagesThenReboots: --- FAIL: TestKernel_Ring0ToldNightStagesThenReboots (0.00s)
|
||||
hub: no reply_to in the payload -> TestReplyTo_HouseholdMailRepliesReachTheOperator: --- FAIL: TestReplyTo_HouseholdMailRepliesReachTheOperator (0.00s)
|
||||
hub: R7 treated as ended for good -> TestKernel_ToldKernelGoneIsTemporaryAndTheNewerIsOffered: --- FAIL: TestKernel_ToldKernelGoneIsTemporaryAndTheNewerIsOffered (0.04s)
|
||||
controller: no driveReady in the capture sweep -> TestDriveReady_RefreshWaitsForTheBindThenCaptures: --- FAIL: TestDriveReady_RefreshWaitsForTheBindThenCaptures (0.00s)
|
||||
@@ -0,0 +1,25 @@
|
||||
"pve-firmware",
|
||||
"proxmox-kernel-7.0.14-22-pve-signed",
|
||||
"proxmox-kernel-7.0",
|
||||
"proxmox-kernel-helper",
|
||||
"pve-edk2-firmware-aarch64",
|
||||
"pve-edk2-firmware",
|
||||
"zfs-initramfs",
|
||||
"zfsutils-linux",
|
||||
"zfs-zed"
|
||||
],
|
||||
"outcome": "inventory",
|
||||
"pending": 80,
|
||||
"reboot_needed": false,
|
||||
"refused": null,
|
||||
"release_id": "ring0-20261007T163720Z",
|
||||
"restart_needed": [
|
||||
"kvm"
|
||||
],
|
||||
"ring": 0,
|
||||
"run_id": "20261007T163720Z",
|
||||
"upgraded": null,
|
||||
"wrapper_seconds": 14.4
|
||||
}
|
||||
docker step: skipped (see the log line above)
|
||||
pass took 30.8s
|
||||
@@ -1,3 +1,17 @@
|
||||
## v0.143.1 — a household's reply reaches the operator; a told kernel that is gone is retried (`09` §3 decisions 174, 176; R-898) (2026-10-07)
|
||||
|
||||
**Operator action on deploy: none.** Deployed with the operator attending (decision 162).
|
||||
|
||||
- **Reply-To** (decision 174): every mail to a HOUSEHOLD carries `reply_to` = the operator's address
|
||||
(`operator_email`, `admin@felhom.eu`). They are sent from `monitoring@felhom.eu`, which nobody reads, and they invite
|
||||
a reply or contact: the kernel notice ("reply to this e-mail"), the event mails' sign-off ("contact your operator"),
|
||||
the setup and link mails. A mail TO the operator carries none. `notify.replyToFor`; test
|
||||
`TestReplyTo_HouseholdMailRepliesReachTheOperator` (red-proved).
|
||||
- **R-898 (hub half):** a kernel step refused because the told version can no longer be installed (R7) is temporary,
|
||||
never "ended": the box stays due, and once its report offers a newer kernel the household is told again (inside the
|
||||
3-mail limit). Pinned by `TestKernel_ToldKernelGoneIsTemporaryAndTheNewerIsOffered` (red-proved: R7 as "for good"
|
||||
fails it). No code change was needed — the test pins the behaviour agent v0.153.0 relies on.
|
||||
|
||||
## v0.143.0 — the kernel lane: the day-before household mail, the night instruction, the operator's kernel set (R-836; `09` §3 decision 172; `11` §5.11) (2026-10-07)
|
||||
|
||||
**Operator action on deploy: none.** Built and deployed only in a session the operator attends (decision 162). Needs
|
||||
|
||||
@@ -895,6 +895,9 @@ func (d *Dispatcher) sendEmail(to, subject, textBody string, headers map[string]
|
||||
if len(headers) > 0 {
|
||||
payload["headers"] = headers
|
||||
}
|
||||
if rt := d.replyToFor(to); rt != "" {
|
||||
payload["reply_to"] = rt
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
@@ -922,6 +925,18 @@ func (d *Dispatcher) sendEmail(to, subject, textBody string, headers map[string]
|
||||
return nil
|
||||
}
|
||||
|
||||
// replyToFor is the Reply-To of a mail (`09` §3 decision 174): every HOUSEHOLD mail invites a reply or contact — the
|
||||
// kernel notice ("reply to this e-mail"), the event sign-off ("contact your operator"), the setup and link mails — and
|
||||
// it is sent from monitoring@, which no person reads. A reply goes to the operator's address. A mail TO the operator
|
||||
// gets none (a reply to it is the operator's own). "" when no operator address is configured. Pinned by TestReplyTo_*.
|
||||
func (d *Dispatcher) replyToFor(to string) string {
|
||||
op := strings.TrimSpace(d.operatorEmail)
|
||||
if op == "" || strings.EqualFold(strings.TrimSpace(to), op) {
|
||||
return ""
|
||||
}
|
||||
return op
|
||||
}
|
||||
|
||||
func isEventEnabled(enabledEvents []string, eventType string) bool {
|
||||
for _, e := range enabledEvents {
|
||||
if e == eventType {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type captureRT struct{ bodies []map[string]any }
|
||||
|
||||
func (c *captureRT) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
var m map[string]any
|
||||
_ = json.Unmarshal(b, &m)
|
||||
c.bodies = append(c.bodies, m)
|
||||
return &http.Response{StatusCode: 200, Body: io.NopCloser(nil), Header: http.Header{}}, nil
|
||||
}
|
||||
|
||||
// `09` §3 decision 174: a household mail carries Reply-To = the operator's address (it is sent from monitoring@, and the
|
||||
// kernel notice says "reply to this e-mail"); a mail to the operator carries none.
|
||||
// COMPANION RED-PROOF (observed): drop the reply_to line in sendEmail → "household mail has no reply_to".
|
||||
func TestReplyTo_HouseholdMailRepliesReachTheOperator(t *testing.T) {
|
||||
d := NewDispatcher(nil, "k", "monitoring@felhom.eu", "admin@felhom.eu", true, log.New(os.Stderr, "", 0))
|
||||
c := &captureRT{}
|
||||
d.httpClient = &http.Client{Transport: c}
|
||||
if err := d.sendEmail("household@example.com", "s", "b", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := d.sendEmail("admin@felhom.eu", "s", "b", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := c.bodies[0]["reply_to"]; got != "admin@felhom.eu" {
|
||||
t.Fatalf("household mail has no reply_to to the operator: %v", c.bodies[0])
|
||||
}
|
||||
if _, ok := c.bodies[1]["reply_to"]; ok {
|
||||
t.Fatalf("a mail to the operator must carry no reply_to: %v", c.bodies[1])
|
||||
}
|
||||
none := NewDispatcher(nil, "k", "monitoring@felhom.eu", "", true, log.New(os.Stderr, "", 0))
|
||||
if none.replyToFor("x@y") != "" {
|
||||
t.Fatal("no operator address → no reply_to")
|
||||
}
|
||||
}
|
||||
@@ -282,3 +282,30 @@ func countOf(xs []string, x string) int {
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// R-898: a told kernel that can no longer be installed is refused by the box BEFORE any change (R7) — temporary, never
|
||||
// "ended for good": the box stays due, and once its report offers the newer kernel the household is told again (inside
|
||||
// the 3-mail limit). COMPANION RED-PROOF (observed): add "R7" to kernelRefusedForGood → "an R7 refusal ended the step".
|
||||
func TestKernel_ToldKernelGoneIsTemporaryAndTheNewerIsOffered(t *testing.T) {
|
||||
f := newFix(t)
|
||||
f.hostWithPendingKernel(t, "hp") // pending: 7.0.14-22
|
||||
f.laneFacts(t, "hp", "c-hp", kOld, "none", "")
|
||||
r := kreport("refused", "night", false)
|
||||
r.Refused = json.RawMessage(`{"code":"R7","reason":"the simulation failed: E: Version '7.0.14-22' for 'proxmox-kernel-7.0' was not found"}`)
|
||||
f.ingest(t, "hp", r)
|
||||
if k, why := f.s.KernelDue("hp"); k != kNew {
|
||||
t.Fatalf("an R7 refusal ended the step: %q %q", k, why)
|
||||
}
|
||||
// the next night's report offers a newer kernel → that one is due, and its household mail is a new one
|
||||
f.ingest(t, "hp", Report{Layer: LayerHost, Trigger: "night", Mode: "apply", Outcome: "nothing", Healthy: true,
|
||||
Pending: []PendingPkg{{Name: "proxmox-kernel-7.0", From: "7.0.2-6", To: "7.0.14-23", Origin: []string{"Proxmox Debian Repository"}}}})
|
||||
if k, _ := f.s.KernelDue("hp"); k != "7.0.14-23-pve" {
|
||||
t.Fatalf("the newer kernel is not offered: %q", k)
|
||||
}
|
||||
var sent []string
|
||||
f.mail(&sent, nil)
|
||||
f.now = budapest(t, 10)
|
||||
if f.s.KernelNotify(); len(sent) != 1 || sent[0] != "c-hp:7.0.14-23-pve" {
|
||||
t.Fatalf("the household must be told again for the newer kernel: %v", sent)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user