detached 42b3f98

test(obs): validate canonical local evidence

commit
42b3f98bd5ac93dff62932d526bccf02bb1efb5c
author
brice · · 2026-07-13 00:59 UTC
committer
brice · · 2026-07-13 01:11 UTC
parent
0021186
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +198 -10
M cmd/vev-perf-harness/main.go +117 -3
@@ -5,6 +5,7 @@ package main
55 import (
66 "bufio"
77 "bytes"
8+ "context"
89 "encoding/json"
910 "errors"
1011 "flag"
@@ -13,6 +14,7 @@ import (
1314 "os"
1415 "os/exec"
1516 "path/filepath"
17+ "runtime/debug"
1618 "sort"
1719 "strconv"
1820 "strings"
@@ -33,6 +35,7 @@ const (
3335
3436 type options struct {
3537 vevBin, manifest, out string
38+ scenario string
3639 warmup, duration time.Duration
3740 repetitions int
3841 }
@@ -247,7 +250,9 @@ type cliProcess struct {
247250 waitTimeout func() <-chan time.Time
248251 forceCleanup func()
249252 forceKill func()
253+ shutdown func() error
250254 terminalReady bool
255+ readyPath string
251256 }
252257
253258 func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProcess, error) {
@@ -285,7 +290,7 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
285290 if err := safedir.EnsurePrivate(filepath.Join(runDir, "state")); err != nil {
286291 return nil, err
287292 }
288- p := &cliProcess{cmd: cmd, waitErr: make(chan error, 1)}
293+ p := &cliProcess{cmd: cmd, waitErr: make(chan error, 1), readyPath: filepath.Join(runDir, "runtime", "vev", "daemon.sock")}
289294 if m.Role == "client" {
290295 master, slave, err := openPTY()
291296 if err != nil {
@@ -324,6 +329,18 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
324329 }
325330 _ = devNull.Close()
326331 }
332+ if m.Role == "daemon" {
333+ // Stop the foreground daemon through its public CLI before its bounded
334+ // process-group fallback. This lets blocked carriage operations close and
335+ // serialize their failed end marks instead of being cut off by SIGKILL.
336+ p.shutdown = func() error {
337+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
338+ defer cancel()
339+ shutdown := exec.CommandContext(ctx, l.bin, "kill", "--daemon")
340+ shutdown.Env = withoutEnv(cmd.Env, "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_PERF_SCENARIO", "VEV_PERF_RUN")
341+ return shutdown.Run()
342+ }
343+ }
327344 go func() { p.waitErr <- cmd.Wait() }()
328345 return p, nil
329346 }
@@ -482,6 +499,38 @@ func (p *cliProcess) copyTerminal() {
482499 }
483500 }
484501
502+// WaitReady confirms the explicitly launched public daemon owns its unique
503+// socket before a client is launched. Without this barrier, the normal client
504+// auto-spawn behavior can create an unmanifested daemon process during startup.
505+func (p *cliProcess) WaitReady() error {
506+ if p.readyPath == "" {
507+ return errors.New("daemon readiness path is empty")
508+ }
509+ ticker := time.NewTicker(10 * time.Millisecond)
510+ defer ticker.Stop()
511+ timer := time.NewTimer(10 * time.Second)
512+ defer timer.Stop()
513+ for {
514+ if info, err := os.Stat(p.readyPath); err == nil {
515+ if info.Mode()&os.ModeSocket == 0 {
516+ return fmt.Errorf("daemon readiness path %s is not a socket", p.readyPath)
517+ }
518+ return nil
519+ }
520+ select {
521+ case err := <-p.waitErr:
522+ p.waitErr <- err
523+ if err == nil {
524+ return errors.New("daemon exited before readiness")
525+ }
526+ return fmt.Errorf("daemon exited before readiness: %w", err)
527+ case <-ticker.C:
528+ case <-timer.C:
529+ return fmt.Errorf("timed out waiting for daemon socket %s", p.readyPath)
530+ }
531+ }
532+}
533+
485534 func (p *cliProcess) Warmup(input []byte) error {
486535 if p.pty == nil || len(input) == 0 {
487536 return nil
@@ -649,6 +698,17 @@ func (p *cliProcess) Close() error {
649698 // before closing the PTY so its transport can finish its receive span;
650699 // this avoids turning ordinary harness teardown into an aborted trace.
651700 exited := false
701+ if p.shutdown != nil && p.waitErr != nil {
702+ select {
703+ case <-p.waitErr:
704+ exited = true
705+ default:
706+ // The client may have just ended the last session and raced this
707+ // best-effort public shutdown. The normal bounded reaping path below
708+ // remains authoritative in that case.
709+ _ = p.shutdown()
710+ }
711+ }
652712 if p.pty != nil && p.waitErr != nil {
653713 _, _ = p.pty.Write([]byte("exit\n"))
654714 select {
@@ -692,6 +752,12 @@ func (p *cliProcess) closeDeadline() <-chan time.Time {
692752 if p.waitTimeout != nil {
693753 return p.waitTimeout()
694754 }
755+ if p.shutdown != nil {
756+ // A public kill asks the daemon to finish bounded detached notices and
757+ // close blocked carriage before it exits; do not turn that graceful
758+ // protocol into a SIGKILL at the two-second client deadline.
759+ return time.After(5 * time.Second)
760+ }
695761 return time.After(2 * time.Second)
696762 }
697763
@@ -768,7 +834,25 @@ func main() {
768834 }
769835 }
770836 func defaultHarness() *harness {
771- return &harness{clock: systemClock{time.Now()}, mkdir: safedir.EnsurePrivate, create: func(p string) (*os.File, error) { return os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) }, removeAll: os.RemoveAll, gitSHA: func() string { return "unknown" }}
837+ return &harness{clock: systemClock{time.Now()}, mkdir: safedir.EnsurePrivate, create: func(p string) (*os.File, error) { return os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) }, removeAll: os.RemoveAll, gitSHA: recordedGitSHA}
838+}
839+
840+// recordedGitSHA prefers build provenance so an installed public harness does
841+// not depend on its launch directory being a checkout. The git fallback keeps
842+// go run evidence attributable as well.
843+func recordedGitSHA() string {
844+ if info, ok := debug.ReadBuildInfo(); ok {
845+ for _, setting := range info.Settings {
846+ if setting.Key == "vcs.revision" && setting.Value != "" {
847+ return setting.Value
848+ }
849+ }
850+ }
851+ out, err := exec.Command("git", "rev-parse", "HEAD").Output()
852+ if err == nil && strings.TrimSpace(string(out)) != "" {
853+ return strings.TrimSpace(string(out))
854+ }
855+ return "unknown"
772856 }
773857 func run(args []string, h *harness) error {
774858 opt, err := parseOptions(args)
@@ -793,6 +877,12 @@ func run(args []string, h *harness) error {
793877 results := make([]runResult, 0, len(m.Scenarios)*opt.repetitions)
794878 var localSpans []span
795879 scenarios := m.Scenarios
880+ if opt.scenario != "" {
881+ scenarios, err = selectScenario(m, opt.scenario)
882+ if err != nil {
883+ return err
884+ }
885+ }
796886 if h.selectScenarios != nil {
797887 scenarios = h.selectScenarios(m)
798888 }
@@ -840,6 +930,7 @@ func parseOptions(args []string) (options, error) {
840930 fs.StringVar(&o.vevBin, "vev-bin", "", "public vev binary")
841931 fs.StringVar(&o.manifest, "manifest", "", "manifest")
842932 fs.StringVar(&o.out, "out", "", "output directory")
933+ fs.StringVar(&o.scenario, "scenario", "", "one canonical scenario ID (after full manifest validation)")
843934 fs.DurationVar(&o.warmup, "warmup", 0, "warmup")
844935 fs.DurationVar(&o.duration, "duration", 0, "measurement duration")
845936 fs.IntVar(&o.repetitions, "repetitions", 0, "repetitions")
@@ -948,6 +1039,22 @@ func validateManifest(m manifest) error {
9481039 }
9491040 return nil
9501041 }
1042+
1043+// selectScenario is intentionally called only after validateManifest. A bounded
1044+// local evidence run therefore cannot mask an incomplete canonical matrix.
1045+func selectScenario(m manifest, id string) ([]scenario, error) {
1046+ for _, s := range m.Scenarios {
1047+ if s.ID != id {
1048+ continue
1049+ }
1050+ if s.InapplicableReason != "" {
1051+ return nil, fmt.Errorf("scenario %s is inapplicable: %s", id, s.InapplicableReason)
1052+ }
1053+ return []scenario{s}, nil
1054+ }
1055+ return nil, fmt.Errorf("unknown canonical scenario %s", id)
1056+}
1057+
9511058 func requiredRoles(transportID string) []string {
9521059 switch transportID {
9531060 case "local":
@@ -1063,6 +1170,13 @@ func (h *harness) runOne(o options, mat manifest, s scenario, run int, raw io.Wr
10631170 return res, e
10641171 }
10651172 processes = append(processes, launchedRole{mapping: pm, process: p})
1173+ if pm.Role == "daemon" {
1174+ if ready, ok := p.(interface{ WaitReady() error }); ok {
1175+ if err = ready.WaitReady(); err != nil {
1176+ return res, err
1177+ }
1178+ }
1179+ }
10661180 }
10671181 for _, p := range processes {
10681182 if p.mapping.Role != "client" {
@@ -1382,7 +1496,7 @@ type traceRecord struct {
13821496 }
13831497 type spanPair struct{ start, end, name string }
13841498
1385-var spanPairs = []spanPair{{"diff_start", "diff_end", "diff_duration"}, {"queue_enqueued", "queue_dequeued", "queue_wait"}, {"ack_blocked_start", "ack_blocked_end", "ack_blocked_interval"}, {"adapter_send_start", "adapter_send_end", "adapter_send_duration"}, {"adapter_receive_start", "adapter_receive_end", "adapter_receive_duration"}}
1499+var spanPairs = []spanPair{{"capture_start", "capture_end", "capture_duration"}, {"compose_start", "compose_end", "compose_duration"}, {"diff_start", "diff_end", "diff_duration"}, {"queue_enqueued", "queue_dequeued", "queue_wait"}, {"ack_blocked_start", "ack_blocked_end", "ack_blocked_interval"}, {"emit_start", "emit_end", "emit_duration"}, {"adapter_send_start", "adapter_send_end", "adapter_send_duration"}, {"adapter_receive_start", "adapter_receive_end", "adapter_receive_duration"}}
13861500
13871501 // mergeProcessTraces permits only records from one manifest process in a span.
13881502 // In particular, the correlation key includes process_id before ticks are read.
@@ -5,6 +5,7 @@ package main
5 import (5 import (
6 "bufio"6 "bufio"
7 "bytes"7 "bytes"
8+ "context"
8 "encoding/json"9 "encoding/json"
9 "errors"10 "errors"
10 "flag"11 "flag"
@@ -13,6 +14,7 @@ import (
13 "os"14 "os"
14 "os/exec"15 "os/exec"
15 "path/filepath"16 "path/filepath"
17+ "runtime/debug"
16 "sort"18 "sort"
17 "strconv"19 "strconv"
18 "strings"20 "strings"
@@ -33,6 +35,7 @@ const (
33 35
34 type options struct {36 type options struct {
35 vevBin, manifest, out string37 vevBin, manifest, out string
38+ scenario string
36 warmup, duration time.Duration39 warmup, duration time.Duration
37 repetitions int40 repetitions int
38 }41 }
@@ -247,7 +250,9 @@ type cliProcess struct {
247 waitTimeout func() <-chan time.Time250 waitTimeout func() <-chan time.Time
248 forceCleanup func()251 forceCleanup func()
249 forceKill func()252 forceKill func()
253+ shutdown func() error
250 terminalReady bool254 terminalReady bool
255+ readyPath string
251 }256 }
252 257
253 func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProcess, error) {258 func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProcess, error) {
@@ -285,7 +290,7 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
285 if err := safedir.EnsurePrivate(filepath.Join(runDir, "state")); err != nil {290 if err := safedir.EnsurePrivate(filepath.Join(runDir, "state")); err != nil {
286 return nil, err291 return nil, err
287 }292 }
288- p := &cliProcess{cmd: cmd, waitErr: make(chan error, 1)}293+ p := &cliProcess{cmd: cmd, waitErr: make(chan error, 1), readyPath: filepath.Join(runDir, "runtime", "vev", "daemon.sock")}
289 if m.Role == "client" {294 if m.Role == "client" {
290 master, slave, err := openPTY()295 master, slave, err := openPTY()
291 if err != nil {296 if err != nil {
@@ -324,6 +329,18 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
324 }329 }
325 _ = devNull.Close()330 _ = devNull.Close()
326 }331 }
332+ if m.Role == "daemon" {
333+ // Stop the foreground daemon through its public CLI before its bounded
334+ // process-group fallback. This lets blocked carriage operations close and
335+ // serialize their failed end marks instead of being cut off by SIGKILL.
336+ p.shutdown = func() error {
337+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
338+ defer cancel()
339+ shutdown := exec.CommandContext(ctx, l.bin, "kill", "--daemon")
340+ shutdown.Env = withoutEnv(cmd.Env, "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_PERF_SCENARIO", "VEV_PERF_RUN")
341+ return shutdown.Run()
342+ }
343+ }
327 go func() { p.waitErr <- cmd.Wait() }()344 go func() { p.waitErr <- cmd.Wait() }()
328 return p, nil345 return p, nil
329 }346 }
@@ -482,6 +499,38 @@ func (p *cliProcess) copyTerminal() {
482 }499 }
483 }500 }
484 501
502+// WaitReady confirms the explicitly launched public daemon owns its unique
503+// socket before a client is launched. Without this barrier, the normal client
504+// auto-spawn behavior can create an unmanifested daemon process during startup.
505+func (p *cliProcess) WaitReady() error {
506+ if p.readyPath == "" {
507+ return errors.New("daemon readiness path is empty")
508+ }
509+ ticker := time.NewTicker(10 * time.Millisecond)
510+ defer ticker.Stop()
511+ timer := time.NewTimer(10 * time.Second)
512+ defer timer.Stop()
513+ for {
514+ if info, err := os.Stat(p.readyPath); err == nil {
515+ if info.Mode()&os.ModeSocket == 0 {
516+ return fmt.Errorf("daemon readiness path %s is not a socket", p.readyPath)
517+ }
518+ return nil
519+ }
520+ select {
521+ case err := <-p.waitErr:
522+ p.waitErr <- err
523+ if err == nil {
524+ return errors.New("daemon exited before readiness")
525+ }
526+ return fmt.Errorf("daemon exited before readiness: %w", err)
527+ case <-ticker.C:
528+ case <-timer.C:
529+ return fmt.Errorf("timed out waiting for daemon socket %s", p.readyPath)
530+ }
531+ }
532+}
533+
485 func (p *cliProcess) Warmup(input []byte) error {534 func (p *cliProcess) Warmup(input []byte) error {
486 if p.pty == nil || len(input) == 0 {535 if p.pty == nil || len(input) == 0 {
487 return nil536 return nil
@@ -649,6 +698,17 @@ func (p *cliProcess) Close() error {
649 // before closing the PTY so its transport can finish its receive span;698 // before closing the PTY so its transport can finish its receive span;
650 // this avoids turning ordinary harness teardown into an aborted trace.699 // this avoids turning ordinary harness teardown into an aborted trace.
651 exited := false700 exited := false
701+ if p.shutdown != nil && p.waitErr != nil {
702+ select {
703+ case <-p.waitErr:
704+ exited = true
705+ default:
706+ // The client may have just ended the last session and raced this
707+ // best-effort public shutdown. The normal bounded reaping path below
708+ // remains authoritative in that case.
709+ _ = p.shutdown()
710+ }
711+ }
652 if p.pty != nil && p.waitErr != nil {712 if p.pty != nil && p.waitErr != nil {
653 _, _ = p.pty.Write([]byte("exit\n"))713 _, _ = p.pty.Write([]byte("exit\n"))
654 select {714 select {
@@ -692,6 +752,12 @@ func (p *cliProcess) closeDeadline() <-chan time.Time {
692 if p.waitTimeout != nil {752 if p.waitTimeout != nil {
693 return p.waitTimeout()753 return p.waitTimeout()
694 }754 }
755+ if p.shutdown != nil {
756+ // A public kill asks the daemon to finish bounded detached notices and
757+ // close blocked carriage before it exits; do not turn that graceful
758+ // protocol into a SIGKILL at the two-second client deadline.
759+ return time.After(5 * time.Second)
760+ }
695 return time.After(2 * time.Second)761 return time.After(2 * time.Second)
696 }762 }
697 763
@@ -768,7 +834,25 @@ func main() {
768 }834 }
769 }835 }
770 func defaultHarness() *harness {836 func defaultHarness() *harness {
771- return &harness{clock: systemClock{time.Now()}, mkdir: safedir.EnsurePrivate, create: func(p string) (*os.File, error) { return os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) }, removeAll: os.RemoveAll, gitSHA: func() string { return "unknown" }}837+ return &harness{clock: systemClock{time.Now()}, mkdir: safedir.EnsurePrivate, create: func(p string) (*os.File, error) { return os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) }, removeAll: os.RemoveAll, gitSHA: recordedGitSHA}
838+}
839+
840+// recordedGitSHA prefers build provenance so an installed public harness does
841+// not depend on its launch directory being a checkout. The git fallback keeps
842+// go run evidence attributable as well.
843+func recordedGitSHA() string {
844+ if info, ok := debug.ReadBuildInfo(); ok {
845+ for _, setting := range info.Settings {
846+ if setting.Key == "vcs.revision" && setting.Value != "" {
847+ return setting.Value
848+ }
849+ }
850+ }
851+ out, err := exec.Command("git", "rev-parse", "HEAD").Output()
852+ if err == nil && strings.TrimSpace(string(out)) != "" {
853+ return strings.TrimSpace(string(out))
854+ }
855+ return "unknown"
772 }856 }
773 func run(args []string, h *harness) error {857 func run(args []string, h *harness) error {
774 opt, err := parseOptions(args)858 opt, err := parseOptions(args)
@@ -793,6 +877,12 @@ func run(args []string, h *harness) error {
793 results := make([]runResult, 0, len(m.Scenarios)*opt.repetitions)877 results := make([]runResult, 0, len(m.Scenarios)*opt.repetitions)
794 var localSpans []span878 var localSpans []span
795 scenarios := m.Scenarios879 scenarios := m.Scenarios
880+ if opt.scenario != "" {
881+ scenarios, err = selectScenario(m, opt.scenario)
882+ if err != nil {
883+ return err
884+ }
885+ }
796 if h.selectScenarios != nil {886 if h.selectScenarios != nil {
797 scenarios = h.selectScenarios(m)887 scenarios = h.selectScenarios(m)
798 }888 }
@@ -840,6 +930,7 @@ func parseOptions(args []string) (options, error) {
840 fs.StringVar(&o.vevBin, "vev-bin", "", "public vev binary")930 fs.StringVar(&o.vevBin, "vev-bin", "", "public vev binary")
841 fs.StringVar(&o.manifest, "manifest", "", "manifest")931 fs.StringVar(&o.manifest, "manifest", "", "manifest")
842 fs.StringVar(&o.out, "out", "", "output directory")932 fs.StringVar(&o.out, "out", "", "output directory")
933+ fs.StringVar(&o.scenario, "scenario", "", "one canonical scenario ID (after full manifest validation)")
843 fs.DurationVar(&o.warmup, "warmup", 0, "warmup")934 fs.DurationVar(&o.warmup, "warmup", 0, "warmup")
844 fs.DurationVar(&o.duration, "duration", 0, "measurement duration")935 fs.DurationVar(&o.duration, "duration", 0, "measurement duration")
845 fs.IntVar(&o.repetitions, "repetitions", 0, "repetitions")936 fs.IntVar(&o.repetitions, "repetitions", 0, "repetitions")
@@ -948,6 +1039,22 @@ func validateManifest(m manifest) error {
948 }1039 }
949 return nil1040 return nil
950 }1041 }
1042+
1043+// selectScenario is intentionally called only after validateManifest. A bounded
1044+// local evidence run therefore cannot mask an incomplete canonical matrix.
1045+func selectScenario(m manifest, id string) ([]scenario, error) {
1046+ for _, s := range m.Scenarios {
1047+ if s.ID != id {
1048+ continue
1049+ }
1050+ if s.InapplicableReason != "" {
1051+ return nil, fmt.Errorf("scenario %s is inapplicable: %s", id, s.InapplicableReason)
1052+ }
1053+ return []scenario{s}, nil
1054+ }
1055+ return nil, fmt.Errorf("unknown canonical scenario %s", id)
1056+}
1057+
951 func requiredRoles(transportID string) []string {1058 func requiredRoles(transportID string) []string {
952 switch transportID {1059 switch transportID {
953 case "local":1060 case "local":
@@ -1063,6 +1170,13 @@ func (h *harness) runOne(o options, mat manifest, s scenario, run int, raw io.Wr
1063 return res, e1170 return res, e
1064 }1171 }
1065 processes = append(processes, launchedRole{mapping: pm, process: p})1172 processes = append(processes, launchedRole{mapping: pm, process: p})
1173+ if pm.Role == "daemon" {
1174+ if ready, ok := p.(interface{ WaitReady() error }); ok {
1175+ if err = ready.WaitReady(); err != nil {
1176+ return res, err
1177+ }
1178+ }
1179+ }
1066 }1180 }
1067 for _, p := range processes {1181 for _, p := range processes {
1068 if p.mapping.Role != "client" {1182 if p.mapping.Role != "client" {
@@ -1382,7 +1496,7 @@ type traceRecord struct {
1382 }1496 }
1383 type spanPair struct{ start, end, name string }1497 type spanPair struct{ start, end, name string }
1384 1498
1385-var spanPairs = []spanPair{{"diff_start", "diff_end", "diff_duration"}, {"queue_enqueued", "queue_dequeued", "queue_wait"}, {"ack_blocked_start", "ack_blocked_end", "ack_blocked_interval"}, {"adapter_send_start", "adapter_send_end", "adapter_send_duration"}, {"adapter_receive_start", "adapter_receive_end", "adapter_receive_duration"}}1499+var spanPairs = []spanPair{{"capture_start", "capture_end", "capture_duration"}, {"compose_start", "compose_end", "compose_duration"}, {"diff_start", "diff_end", "diff_duration"}, {"queue_enqueued", "queue_dequeued", "queue_wait"}, {"ack_blocked_start", "ack_blocked_end", "ack_blocked_interval"}, {"emit_start", "emit_end", "emit_duration"}, {"adapter_send_start", "adapter_send_end", "adapter_send_duration"}, {"adapter_receive_start", "adapter_receive_end", "adapter_receive_duration"}}
1386 1500
1387 // mergeProcessTraces permits only records from one manifest process in a span.1501 // mergeProcessTraces permits only records from one manifest process in a span.
1388 // In particular, the correlation key includes process_id before ticks are read.1502 // In particular, the correlation key includes process_id before ticks are read.
M cmd/vev-perf-harness/main_test.go +80 -6
@@ -117,6 +117,40 @@ func TestCLIProcessCloseIsBoundedWhenPublicRoleDoesNotExit(t *testing.T) {
117117 }
118118 }
119119
120+func TestCLIProcessWaitReadyRequiresDaemonSocket(t *testing.T) {
121+ path := filepath.Join(t.TempDir(), "daemon.sock")
122+ listener, err := net.Listen("unix", path)
123+ if err != nil {
124+ t.Fatal(err)
125+ }
126+ defer listener.Close()
127+ if err := (&cliProcess{readyPath: path, waitErr: make(chan error, 1)}).WaitReady(); err != nil {
128+ t.Fatal(err)
129+ }
130+}
131+
132+func TestCLIProcessCloseUsesPublicDaemonShutdown(t *testing.T) {
133+ wait := make(chan error, 1)
134+ wait <- nil
135+ var shutdown int
136+ p := &cliProcess{waitErr: wait, shutdown: func() error { shutdown++; return nil }}
137+ if err := p.Close(); err != nil {
138+ t.Fatal(err)
139+ }
140+ if shutdown != 0 {
141+ t.Fatalf("shutdown calls = %d, want 0 for an exited daemon", shutdown)
142+ }
143+
144+ wait = make(chan error, 1)
145+ p = &cliProcess{waitErr: wait, shutdown: func() error { shutdown++; wait <- nil; return nil }}
146+ if err := p.Close(); err != nil {
147+ t.Fatal(err)
148+ }
149+ if shutdown != 1 {
150+ t.Fatalf("shutdown calls = %d, want 1", shutdown)
151+ }
152+}
153+
120154 func TestHarnessCanonicalLocalSmokeRealRoles(t *testing.T) {
121155 if testing.Short() {
122156 t.Skip("public CLI smoke")
@@ -166,7 +200,7 @@ func TestHarnessCanonicalLocalSmokeRealRoles(t *testing.T) {
166200 }
167201 }
168202
169-func TestHarnessTestSelectionKeepsCanonicalManifestValidation(t *testing.T) {
203+func TestHarnessScenarioSelectionKeepsCanonicalManifestValidation(t *testing.T) {
170204 root, err := filepath.Abs(filepath.Join("..", ".."))
171205 if err != nil {
172206 t.Fatal(err)
@@ -176,15 +210,11 @@ func TestHarnessTestSelectionKeepsCanonicalManifestValidation(t *testing.T) {
176210 if err != nil {
177211 t.Fatal(err)
178212 }
179- if err := validateManifest(m); err != nil {
180- t.Fatal(err)
181- }
182213 selected := m.Scenarios[0]
183214 h := defaultHarness()
184215 h.clock, h.launcher = &fakeClock{}, &fakeLauncher{}
185- h.selectScenarios = func(manifest) []scenario { return []scenario{selected} }
186216 out := filepath.Join(t.TempDir(), "out")
187- if err := run([]string{"--vev-bin", "ignored", "--manifest", manifestPath, "--out", out, "--warmup", "0s", "--duration", "30s", "--repetitions", "10"}, h); err != nil {
217+ if err := run([]string{"--vev-bin", "ignored", "--manifest", manifestPath, "--out", out, "--scenario", selected.ID, "--warmup", "0s", "--duration", "30s", "--repetitions", "10"}, h); err != nil {
188218 t.Fatal(err)
189219 }
190220 var runs []runResult
@@ -198,6 +228,39 @@ func TestHarnessTestSelectionKeepsCanonicalManifestValidation(t *testing.T) {
198228 if len(runs) != 10 {
199229 t.Fatalf("selected canonical runs=%d, want 10", len(runs))
200230 }
231+ for _, result := range runs {
232+ if result.Scenario != selected.ID {
233+ t.Fatalf("selected scenario = %q, want %q", result.Scenario, selected.ID)
234+ }
235+ }
236+}
237+
238+func TestHarnessScenarioSelectionRejectsIncompleteCanonicalManifest(t *testing.T) {
239+ m, err := readManifest(filepath.Join("..", "..", "testdata", "perf", "manifest.json"))
240+ if err != nil {
241+ t.Fatal(err)
242+ }
243+ selected := m.Scenarios[0]
244+ m.Scenarios = m.Scenarios[:len(m.Scenarios)-1]
245+ dir := t.TempDir()
246+ manifestPath := filepath.Join(dir, "manifest.json")
247+ if err := os.WriteFile(manifestPath, []byte(mustJSON(m)), 0o600); err != nil {
248+ t.Fatal(err)
249+ }
250+ launcher := &fakeLauncher{}
251+ h := defaultHarness()
252+ h.clock, h.launcher = &fakeClock{}, launcher
253+ out := filepath.Join(dir, "out")
254+ err = run([]string{"--vev-bin", "ignored", "--manifest", manifestPath, "--out", out, "--scenario", selected.ID, "--warmup", "0s", "--duration", "30s", "--repetitions", "10"}, h)
255+ if err == nil || !strings.Contains(err.Error(), "missing scenario or inapplicable reason") {
256+ t.Fatalf("incomplete manifest selection error = %v", err)
257+ }
258+ if len(launcher.mappings) != 0 {
259+ t.Fatalf("launched with incomplete canonical manifest: %+v", launcher.mappings)
260+ }
261+ if _, statErr := os.Stat(out); !errors.Is(statErr, os.ErrNotExist) {
262+ t.Fatalf("output created before manifest validation: %v", statErr)
263+ }
201264 }
202265
203266 func TestTraceEnvironmentMatchesProcessMapping(t *testing.T) {
@@ -883,6 +946,17 @@ func equalStrings(a, b []string) bool {
883946 return true
884947 }
885948
949+func TestHarnessIncludesAllRequiredProcessLocalSpans(t *testing.T) {
950+ got := make([]string, 0, len(spanPairs))
951+ for _, pair := range spanPairs {
952+ got = append(got, pair.name)
953+ }
954+ want := []string{"capture_duration", "compose_duration", "diff_duration", "queue_wait", "ack_blocked_interval", "emit_duration", "adapter_send_duration", "adapter_receive_duration"}
955+ if !equalStrings(got, want) {
956+ t.Fatalf("span pairs = %v, want %v", got, want)
957+ }
958+}
959+
886960 func TestHarnessRawRecordsAreDeterministicJSONL(t *testing.T) {
887961 var b bytes.Buffer
888962 for _, m := range []harnessMark{{Scenario: "s", Run: 1, Sequence: 1, Kind: "input_injected", Tick: 1, Valid: true}, {Scenario: "s", Run: 1, Sequence: 1, Kind: "terminal_flushed", Tick: 2, Valid: true}} {
@@ -117,6 +117,40 @@ func TestCLIProcessCloseIsBoundedWhenPublicRoleDoesNotExit(t *testing.T) {
117 }117 }
118 }118 }
119 119
120+func TestCLIProcessWaitReadyRequiresDaemonSocket(t *testing.T) {
121+ path := filepath.Join(t.TempDir(), "daemon.sock")
122+ listener, err := net.Listen("unix", path)
123+ if err != nil {
124+ t.Fatal(err)
125+ }
126+ defer listener.Close()
127+ if err := (&cliProcess{readyPath: path, waitErr: make(chan error, 1)}).WaitReady(); err != nil {
128+ t.Fatal(err)
129+ }
130+}
131+
132+func TestCLIProcessCloseUsesPublicDaemonShutdown(t *testing.T) {
133+ wait := make(chan error, 1)
134+ wait <- nil
135+ var shutdown int
136+ p := &cliProcess{waitErr: wait, shutdown: func() error { shutdown++; return nil }}
137+ if err := p.Close(); err != nil {
138+ t.Fatal(err)
139+ }
140+ if shutdown != 0 {
141+ t.Fatalf("shutdown calls = %d, want 0 for an exited daemon", shutdown)
142+ }
143+
144+ wait = make(chan error, 1)
145+ p = &cliProcess{waitErr: wait, shutdown: func() error { shutdown++; wait <- nil; return nil }}
146+ if err := p.Close(); err != nil {
147+ t.Fatal(err)
148+ }
149+ if shutdown != 1 {
150+ t.Fatalf("shutdown calls = %d, want 1", shutdown)
151+ }
152+}
153+
120 func TestHarnessCanonicalLocalSmokeRealRoles(t *testing.T) {154 func TestHarnessCanonicalLocalSmokeRealRoles(t *testing.T) {
121 if testing.Short() {155 if testing.Short() {
122 t.Skip("public CLI smoke")156 t.Skip("public CLI smoke")
@@ -166,7 +200,7 @@ func TestHarnessCanonicalLocalSmokeRealRoles(t *testing.T) {
166 }200 }
167 }201 }
168 202
169-func TestHarnessTestSelectionKeepsCanonicalManifestValidation(t *testing.T) {203+func TestHarnessScenarioSelectionKeepsCanonicalManifestValidation(t *testing.T) {
170 root, err := filepath.Abs(filepath.Join("..", ".."))204 root, err := filepath.Abs(filepath.Join("..", ".."))
171 if err != nil {205 if err != nil {
172 t.Fatal(err)206 t.Fatal(err)
@@ -176,15 +210,11 @@ func TestHarnessTestSelectionKeepsCanonicalManifestValidation(t *testing.T) {
176 if err != nil {210 if err != nil {
177 t.Fatal(err)211 t.Fatal(err)
178 }212 }
179- if err := validateManifest(m); err != nil {
180- t.Fatal(err)
181- }
182 selected := m.Scenarios[0]213 selected := m.Scenarios[0]
183 h := defaultHarness()214 h := defaultHarness()
184 h.clock, h.launcher = &fakeClock{}, &fakeLauncher{}215 h.clock, h.launcher = &fakeClock{}, &fakeLauncher{}
185- h.selectScenarios = func(manifest) []scenario { return []scenario{selected} }
186 out := filepath.Join(t.TempDir(), "out")216 out := filepath.Join(t.TempDir(), "out")
187- if err := run([]string{"--vev-bin", "ignored", "--manifest", manifestPath, "--out", out, "--warmup", "0s", "--duration", "30s", "--repetitions", "10"}, h); err != nil {217+ if err := run([]string{"--vev-bin", "ignored", "--manifest", manifestPath, "--out", out, "--scenario", selected.ID, "--warmup", "0s", "--duration", "30s", "--repetitions", "10"}, h); err != nil {
188 t.Fatal(err)218 t.Fatal(err)
189 }219 }
190 var runs []runResult220 var runs []runResult
@@ -198,6 +228,39 @@ func TestHarnessTestSelectionKeepsCanonicalManifestValidation(t *testing.T) {
198 if len(runs) != 10 {228 if len(runs) != 10 {
199 t.Fatalf("selected canonical runs=%d, want 10", len(runs))229 t.Fatalf("selected canonical runs=%d, want 10", len(runs))
200 }230 }
231+ for _, result := range runs {
232+ if result.Scenario != selected.ID {
233+ t.Fatalf("selected scenario = %q, want %q", result.Scenario, selected.ID)
234+ }
235+ }
236+}
237+
238+func TestHarnessScenarioSelectionRejectsIncompleteCanonicalManifest(t *testing.T) {
239+ m, err := readManifest(filepath.Join("..", "..", "testdata", "perf", "manifest.json"))
240+ if err != nil {
241+ t.Fatal(err)
242+ }
243+ selected := m.Scenarios[0]
244+ m.Scenarios = m.Scenarios[:len(m.Scenarios)-1]
245+ dir := t.TempDir()
246+ manifestPath := filepath.Join(dir, "manifest.json")
247+ if err := os.WriteFile(manifestPath, []byte(mustJSON(m)), 0o600); err != nil {
248+ t.Fatal(err)
249+ }
250+ launcher := &fakeLauncher{}
251+ h := defaultHarness()
252+ h.clock, h.launcher = &fakeClock{}, launcher
253+ out := filepath.Join(dir, "out")
254+ err = run([]string{"--vev-bin", "ignored", "--manifest", manifestPath, "--out", out, "--scenario", selected.ID, "--warmup", "0s", "--duration", "30s", "--repetitions", "10"}, h)
255+ if err == nil || !strings.Contains(err.Error(), "missing scenario or inapplicable reason") {
256+ t.Fatalf("incomplete manifest selection error = %v", err)
257+ }
258+ if len(launcher.mappings) != 0 {
259+ t.Fatalf("launched with incomplete canonical manifest: %+v", launcher.mappings)
260+ }
261+ if _, statErr := os.Stat(out); !errors.Is(statErr, os.ErrNotExist) {
262+ t.Fatalf("output created before manifest validation: %v", statErr)
263+ }
201 }264 }
202 265
203 func TestTraceEnvironmentMatchesProcessMapping(t *testing.T) {266 func TestTraceEnvironmentMatchesProcessMapping(t *testing.T) {
@@ -883,6 +946,17 @@ func equalStrings(a, b []string) bool {
883 return true946 return true
884 }947 }
885 948
949+func TestHarnessIncludesAllRequiredProcessLocalSpans(t *testing.T) {
950+ got := make([]string, 0, len(spanPairs))
951+ for _, pair := range spanPairs {
952+ got = append(got, pair.name)
953+ }
954+ want := []string{"capture_duration", "compose_duration", "diff_duration", "queue_wait", "ack_blocked_interval", "emit_duration", "adapter_send_duration", "adapter_receive_duration"}
955+ if !equalStrings(got, want) {
956+ t.Fatalf("span pairs = %v, want %v", got, want)
957+ }
958+}
959+
886 func TestHarnessRawRecordsAreDeterministicJSONL(t *testing.T) {960 func TestHarnessRawRecordsAreDeterministicJSONL(t *testing.T) {
887 var b bytes.Buffer961 var b bytes.Buffer
888 for _, m := range []harnessMark{{Scenario: "s", Run: 1, Sequence: 1, Kind: "input_injected", Tick: 1, Valid: true}, {Scenario: "s", Run: 1, Sequence: 1, Kind: "terminal_flushed", Tick: 2, Valid: true}} {962 for _, m := range []harnessMark{{Scenario: "s", Run: 1, Sequence: 1, Kind: "input_injected", Tick: 1, Valid: true}, {Scenario: "s", Run: 1, Sequence: 1, Kind: "terminal_flushed", Tick: 2, Valid: true}} {
M testdata/perf/README.md +1 -1
@@ -66,4 +66,4 @@ jq -e '(.topologies|length)==4 and (.workloads|length)==9 and (.transports|lengt
6666 go test ./cmd/vev-perf-harness
6767 ```
6868
69-The full command above is the measurement procedure; its 10-second warmup, 30-second measured interval, and 10 repetitions are the minimum accepted settings.
69+The full command above is the measurement procedure; its 10-second warmup, 30-second measured interval, and 10 repetitions are the minimum accepted settings. For a bounded local evidence run, add `--scenario 1x4-idle-local`; the harness validates the complete canonical manifest before it applies that selection, so a single-scenario run cannot hide missing matrix coverage.
@@ -66,4 +66,4 @@ jq -e '(.topologies|length)==4 and (.workloads|length)==9 and (.transports|lengt
66 go test ./cmd/vev-perf-harness66 go test ./cmd/vev-perf-harness
67 ```67 ```
68 68
69-The full command above is the measurement procedure; its 10-second warmup, 30-second measured interval, and 10 repetitions are the minimum accepted settings.69+The full command above is the measurement procedure; its 10-second warmup, 30-second measured interval, and 10 repetitions are the minimum accepted settings. For a bounded local evidence run, add `--scenario 1x4-idle-local`; the harness validates the complete canonical manifest before it applies that selection, so a single-scenario run cannot hide missing matrix coverage.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close