detached 1ffeb55

fix(obs): validate harness terminal evidence

commit
1ffeb5584c39ba44ff282bae80a3ed00ab2a5bec
author
brice · · 2026-07-12 23:35 UTC
parent
72845c9
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +177 -24
M cmd/vev-perf-harness/main.go +104 -21
@@ -210,15 +210,16 @@ type terminalOutput interface {
210210 }
211211
212212 type cliProcess struct {
213- cmd *exec.Cmd
214- pty io.ReadWriteCloser
215- output terminalOutput
216- chunks chan []byte
217- done chan struct{}
218- closed sync.Once
219- waitErr chan error
220- waitTimeout func() <-chan time.Time
221- forceCleanup func()
213+ cmd *exec.Cmd
214+ pty io.ReadWriteCloser
215+ output terminalOutput
216+ chunks chan []byte
217+ done chan struct{}
218+ closed sync.Once
219+ waitErr chan error
220+ waitTimeout func() <-chan time.Time
221+ forceCleanup func()
222+ terminalReady bool
222223 }
223224
224225 func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProcess, error) {
@@ -413,17 +414,27 @@ func (p *cliProcess) Warmup(input []byte) error {
413414 if p.pty == nil || len(input) == 0 {
414415 return nil
415416 }
417+ // A client cannot write application output until it has received Welcome and
418+ // entered raw mode. Require that observable output before submitting any
419+ // warmup input, so the PTY's line discipline cannot echo it locally.
420+ if !p.terminalReady {
421+ if err := p.waitForTerminalReady(); err != nil {
422+ return err
423+ }
424+ p.terminalReady = true
425+ }
416426 if _, err := p.pty.Write(input); err != nil {
417427 return err
418428 }
419429 // Do not begin measured input until the public client has rendered a real
420430 // shell response to warmup input. This is intentionally not a measurement.
421- return p.waitForTerminalMarker(inputMarker(input))
431+ return p.waitForTerminalMarker(inputMarker(input), input)
422432 }
423433
424434 // Measure records a boundary only after the harness-owned terminal output file
425-// has accepted data and Sync has reported a successful flush. The marker is
426-// injected through the real client PTY, never fabricated by the harness.
435+// has accepted application output and Sync has reported a successful flush.
436+// The marker is injected through the real client PTY, never fabricated by the
437+// harness. PTY local echo is discarded and cannot satisfy this boundary.
427438 func (p *cliProcess) Measure(input []byte, injected, flushed func() error) error {
428439 if p.pty == nil {
429440 return errors.New("measured process has no terminal PTY")
@@ -435,7 +446,7 @@ func (p *cliProcess) Measure(input []byte, injected, flushed func() error) error
435446 if err := injected(); err != nil {
436447 return err
437448 }
438- if err := p.waitForTerminalMarker(inputMarker(input)); err != nil {
449+ if err := p.waitForTerminalMarker(inputMarker(input), input); err != nil {
439450 return err
440451 }
441452 if err := p.output.Sync(); err != nil {
@@ -444,26 +455,98 @@ func (p *cliProcess) Measure(input []byte, injected, flushed func() error) error
444455 return flushed()
445456 }
446457
447-func (p *cliProcess) waitForTerminalMarker(marker []byte) error {
458+// waitForTerminalReady waits for public client output produced before any
459+// harness input. The client enters raw mode before its output loop can render
460+// this initial prompt/state, so this is an observable readiness boundary.
461+func (p *cliProcess) waitForTerminalReady() error {
462+ timer := time.NewTimer(10 * time.Second)
463+ defer timer.Stop()
464+ for {
465+ select {
466+ case chunk, ok := <-p.chunks:
467+ if !ok {
468+ return errors.New("client terminal closed before readiness output")
469+ }
470+ if len(chunk) == 0 {
471+ continue
472+ }
473+ if err := p.output.Sync(); err != nil {
474+ return fmt.Errorf("flush terminal readiness output: %w", err)
475+ }
476+ return nil
477+ case err := <-p.waitErr:
478+ // Keep the exit result available to Close, which owns reaping.
479+ p.waitErr <- err
480+ if err == nil {
481+ return errors.New("client exited before readiness output")
482+ }
483+ return fmt.Errorf("client exited before readiness output: %w", err)
484+ case <-timer.C:
485+ return errors.New("timed out waiting for client readiness output")
486+ }
487+ }
488+}
489+
490+func (p *cliProcess) waitForTerminalMarker(marker, input []byte) error {
448491 var received []byte
492+ echo := newPTYLocalEcho(input)
449493 timer := time.NewTimer(10 * time.Second)
450494 defer timer.Stop()
451495 for !bytes.Contains(received, marker) {
452496 select {
453497 case chunk, ok := <-p.chunks:
454498 if !ok {
455- return errors.New("client terminal closed before terminal output")
499+ return errors.New("client terminal closed before application output")
456500 }
457- received = append(received, chunk...)
501+ received = append(received, echo.applicationOutput(chunk)...)
458502 case err := <-p.waitErr:
459503 // Keep the exit result available to Close, which owns reaping.
460504 p.waitErr <- err
461505 if err == nil {
462- return errors.New("client exited before terminal output")
506+ return errors.New("client exited before application output")
463507 }
464- return fmt.Errorf("client exited before terminal output: %w", err)
508+ return fmt.Errorf("client exited before application output: %w", err)
465509 case <-timer.C:
466- return errors.New("timed out waiting for terminal output")
510+ return errors.New("timed out waiting for application terminal output")
511+ }
512+ }
513+ return nil
514+}
515+
516+// ptyLocalEcho removes the one ordered input echo that a not-yet-raw slave PTY
517+// can produce. It normalizes NL to the conventional ONLCR echo spelling; any
518+// divergence is retained as application output. The line discipline emits the
519+// echo before the child can read and respond to the same write.
520+type ptyLocalEcho struct {
521+ expected []byte
522+ matched int
523+ done bool
524+}
525+
526+func newPTYLocalEcho(input []byte) *ptyLocalEcho {
527+ expected := make([]byte, 0, len(input))
528+ for _, b := range input {
529+ if b == '\n' {
530+ expected = append(expected, '\r')
531+ }
532+ expected = append(expected, b)
533+ }
534+ return &ptyLocalEcho{expected: expected}
535+}
536+
537+func (e *ptyLocalEcho) applicationOutput(chunk []byte) []byte {
538+ if e.done || len(e.expected) == 0 {
539+ return chunk
540+ }
541+ for i, b := range chunk {
542+ if b != e.expected[e.matched] {
543+ e.done = true
544+ return append([]byte(nil), chunk[i:]...)
545+ }
546+ e.matched++
547+ if e.matched == len(e.expected) {
548+ e.done = true
549+ return append([]byte(nil), chunk[i+1:]...)
467550 }
468551 }
469552 return nil
@@ -1073,9 +1156,9 @@ func mergeProcessTraces(mappings []processMapping) ([]span, error) {
10731156 _ = f.Close()
10741157 return nil, err
10751158 }
1076- if r.ProcessID != m.ProcessID {
1159+ if r.ProcessID != m.ProcessID || r.Scenario != m.Scenario || r.Run != uint64(m.Run) {
10771160 _ = f.Close()
1078- return nil, errors.New("trace record process_id does not match manifest")
1161+ return nil, errors.New("trace record identity does not match manifest")
10791162 }
10801163 if r.Scenario == "" || r.Run == 0 || r.Component == "" || r.Sequence == 0 || r.RequestID == 0 || r.Epoch == 0 {
10811164 _ = f.Close()
@@ -210,15 +210,16 @@ type terminalOutput interface {
210 }210 }
211 211
212 type cliProcess struct {212 type cliProcess struct {
213- cmd *exec.Cmd213+ cmd *exec.Cmd
214- pty io.ReadWriteCloser214+ pty io.ReadWriteCloser
215- output terminalOutput215+ output terminalOutput
216- chunks chan []byte216+ chunks chan []byte
217- done chan struct{}217+ done chan struct{}
218- closed sync.Once218+ closed sync.Once
219- waitErr chan error219+ waitErr chan error
220- waitTimeout func() <-chan time.Time220+ waitTimeout func() <-chan time.Time
221- forceCleanup func()221+ forceCleanup func()
222+ terminalReady bool
222 }223 }
223 224
224 func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProcess, error) {225 func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProcess, error) {
@@ -413,17 +414,27 @@ func (p *cliProcess) Warmup(input []byte) error {
413 if p.pty == nil || len(input) == 0 {414 if p.pty == nil || len(input) == 0 {
414 return nil415 return nil
415 }416 }
417+ // A client cannot write application output until it has received Welcome and
418+ // entered raw mode. Require that observable output before submitting any
419+ // warmup input, so the PTY's line discipline cannot echo it locally.
420+ if !p.terminalReady {
421+ if err := p.waitForTerminalReady(); err != nil {
422+ return err
423+ }
424+ p.terminalReady = true
425+ }
416 if _, err := p.pty.Write(input); err != nil {426 if _, err := p.pty.Write(input); err != nil {
417 return err427 return err
418 }428 }
419 // Do not begin measured input until the public client has rendered a real429 // Do not begin measured input until the public client has rendered a real
420 // shell response to warmup input. This is intentionally not a measurement.430 // shell response to warmup input. This is intentionally not a measurement.
421- return p.waitForTerminalMarker(inputMarker(input))431+ return p.waitForTerminalMarker(inputMarker(input), input)
422 }432 }
423 433
424 // Measure records a boundary only after the harness-owned terminal output file434 // Measure records a boundary only after the harness-owned terminal output file
425-// has accepted data and Sync has reported a successful flush. The marker is435+// has accepted application output and Sync has reported a successful flush.
426-// injected through the real client PTY, never fabricated by the harness.436+// The marker is injected through the real client PTY, never fabricated by the
437+// harness. PTY local echo is discarded and cannot satisfy this boundary.
427 func (p *cliProcess) Measure(input []byte, injected, flushed func() error) error {438 func (p *cliProcess) Measure(input []byte, injected, flushed func() error) error {
428 if p.pty == nil {439 if p.pty == nil {
429 return errors.New("measured process has no terminal PTY")440 return errors.New("measured process has no terminal PTY")
@@ -435,7 +446,7 @@ func (p *cliProcess) Measure(input []byte, injected, flushed func() error) error
435 if err := injected(); err != nil {446 if err := injected(); err != nil {
436 return err447 return err
437 }448 }
438- if err := p.waitForTerminalMarker(inputMarker(input)); err != nil {449+ if err := p.waitForTerminalMarker(inputMarker(input), input); err != nil {
439 return err450 return err
440 }451 }
441 if err := p.output.Sync(); err != nil {452 if err := p.output.Sync(); err != nil {
@@ -444,26 +455,98 @@ func (p *cliProcess) Measure(input []byte, injected, flushed func() error) error
444 return flushed()455 return flushed()
445 }456 }
446 457
447-func (p *cliProcess) waitForTerminalMarker(marker []byte) error {458+// waitForTerminalReady waits for public client output produced before any
459+// harness input. The client enters raw mode before its output loop can render
460+// this initial prompt/state, so this is an observable readiness boundary.
461+func (p *cliProcess) waitForTerminalReady() error {
462+ timer := time.NewTimer(10 * time.Second)
463+ defer timer.Stop()
464+ for {
465+ select {
466+ case chunk, ok := <-p.chunks:
467+ if !ok {
468+ return errors.New("client terminal closed before readiness output")
469+ }
470+ if len(chunk) == 0 {
471+ continue
472+ }
473+ if err := p.output.Sync(); err != nil {
474+ return fmt.Errorf("flush terminal readiness output: %w", err)
475+ }
476+ return nil
477+ case err := <-p.waitErr:
478+ // Keep the exit result available to Close, which owns reaping.
479+ p.waitErr <- err
480+ if err == nil {
481+ return errors.New("client exited before readiness output")
482+ }
483+ return fmt.Errorf("client exited before readiness output: %w", err)
484+ case <-timer.C:
485+ return errors.New("timed out waiting for client readiness output")
486+ }
487+ }
488+}
489+
490+func (p *cliProcess) waitForTerminalMarker(marker, input []byte) error {
448 var received []byte491 var received []byte
492+ echo := newPTYLocalEcho(input)
449 timer := time.NewTimer(10 * time.Second)493 timer := time.NewTimer(10 * time.Second)
450 defer timer.Stop()494 defer timer.Stop()
451 for !bytes.Contains(received, marker) {495 for !bytes.Contains(received, marker) {
452 select {496 select {
453 case chunk, ok := <-p.chunks:497 case chunk, ok := <-p.chunks:
454 if !ok {498 if !ok {
455- return errors.New("client terminal closed before terminal output")499+ return errors.New("client terminal closed before application output")
456 }500 }
457- received = append(received, chunk...)501+ received = append(received, echo.applicationOutput(chunk)...)
458 case err := <-p.waitErr:502 case err := <-p.waitErr:
459 // Keep the exit result available to Close, which owns reaping.503 // Keep the exit result available to Close, which owns reaping.
460 p.waitErr <- err504 p.waitErr <- err
461 if err == nil {505 if err == nil {
462- return errors.New("client exited before terminal output")506+ return errors.New("client exited before application output")
463 }507 }
464- return fmt.Errorf("client exited before terminal output: %w", err)508+ return fmt.Errorf("client exited before application output: %w", err)
465 case <-timer.C:509 case <-timer.C:
466- return errors.New("timed out waiting for terminal output")510+ return errors.New("timed out waiting for application terminal output")
511+ }
512+ }
513+ return nil
514+}
515+
516+// ptyLocalEcho removes the one ordered input echo that a not-yet-raw slave PTY
517+// can produce. It normalizes NL to the conventional ONLCR echo spelling; any
518+// divergence is retained as application output. The line discipline emits the
519+// echo before the child can read and respond to the same write.
520+type ptyLocalEcho struct {
521+ expected []byte
522+ matched int
523+ done bool
524+}
525+
526+func newPTYLocalEcho(input []byte) *ptyLocalEcho {
527+ expected := make([]byte, 0, len(input))
528+ for _, b := range input {
529+ if b == '\n' {
530+ expected = append(expected, '\r')
531+ }
532+ expected = append(expected, b)
533+ }
534+ return &ptyLocalEcho{expected: expected}
535+}
536+
537+func (e *ptyLocalEcho) applicationOutput(chunk []byte) []byte {
538+ if e.done || len(e.expected) == 0 {
539+ return chunk
540+ }
541+ for i, b := range chunk {
542+ if b != e.expected[e.matched] {
543+ e.done = true
544+ return append([]byte(nil), chunk[i:]...)
545+ }
546+ e.matched++
547+ if e.matched == len(e.expected) {
548+ e.done = true
549+ return append([]byte(nil), chunk[i+1:]...)
467 }550 }
468 }551 }
469 return nil552 return nil
@@ -1073,9 +1156,9 @@ func mergeProcessTraces(mappings []processMapping) ([]span, error) {
1073 _ = f.Close()1156 _ = f.Close()
1074 return nil, err1157 return nil, err
1075 }1158 }
1076- if r.ProcessID != m.ProcessID {1159+ if r.ProcessID != m.ProcessID || r.Scenario != m.Scenario || r.Run != uint64(m.Run) {
1077 _ = f.Close()1160 _ = f.Close()
1078- return nil, errors.New("trace record process_id does not match manifest")1161+ return nil, errors.New("trace record identity does not match manifest")
1079 }1162 }
1080 if r.Scenario == "" || r.Run == 0 || r.Component == "" || r.Sequence == 0 || r.RequestID == 0 || r.Epoch == 0 {1163 if r.Scenario == "" || r.Run == 0 || r.Component == "" || r.Sequence == 0 || r.RequestID == 0 || r.Epoch == 0 {
1081 _ = f.Close()1164 _ = f.Close()
M cmd/vev-perf-harness/main_test.go +73 -3
@@ -157,6 +157,21 @@ func (p *fakePTY) Write(b []byte) (int, error) {
157157 }
158158 func (*fakePTY) Close() error { return nil }
159159
160+type stagedPTY struct {
161+ allow <-chan struct{}
162+ writes chan []byte
163+}
164+
165+func (*stagedPTY) Read([]byte) (int, error) { return 0, os.ErrClosed }
166+func (p *stagedPTY) Write(b []byte) (int, error) {
167+ if p.allow != nil {
168+ <-p.allow
169+ }
170+ p.writes <- append([]byte(nil), b...)
171+ return len(b), nil
172+}
173+func (*stagedPTY) Close() error { return nil }
174+
160175 type fakeOutput struct {
161176 bytes.Buffer
162177 syncs int
@@ -218,11 +233,59 @@ func TestHarnessDoesNotInheritNestedSession(t *testing.T) {
218233 }
219234 }
220235
221-func TestCLIProcessOwnsPTYInjectionAndSuccessfulFlushBoundary(t *testing.T) {
236+func TestCLIProcessWarmupWaitsForDelayedTerminalReadiness(t *testing.T) {
237+ allowWrite := make(chan struct{})
238+ pty, output := &stagedPTY{allow: allowWrite, writes: make(chan []byte, 1)}, &fakeOutput{}
239+ p := &cliProcess{pty: pty, output: output, chunks: make(chan []byte)}
240+ input := workloadInput(scenario{ID: "s"}, 1, "warmup")
241+ done := make(chan error, 1)
242+ go func() { done <- p.Warmup(input) }()
243+
244+ // This is public client output from the initial application prompt/state,
245+ // emitted only after the client has entered raw mode. A premature PTY write
246+ // blocks on allowWrite and therefore cannot consume this delayed readiness.
247+ go func() {
248+ p.chunks <- []byte("shell prompt$ ")
249+ close(allowWrite)
250+ }()
251+ select {
252+ case got := <-pty.writes:
253+ if !bytes.Equal(got, input) {
254+ t.Fatalf("warmup write=%q want %q", got, input)
255+ }
256+ case <-time.After(time.Second):
257+ t.Fatal("warmup did not wait for and then proceed from client readiness")
258+ }
259+ go func() { p.chunks <- append([]byte("application "), inputMarker(input)...); close(p.chunks) }()
260+ if err := <-done; err != nil {
261+ t.Fatal(err)
262+ }
263+ if output.syncs != 1 {
264+ t.Fatalf("readiness output was not flushed: syncs=%d", output.syncs)
265+ }
266+}
267+
268+func TestCLIProcessRejectsPTYLocalEchoAsFlushEvidence(t *testing.T) {
269+ pty, output := &fakePTY{}, &fakeOutput{}
270+ p := &cliProcess{pty: pty, output: output, chunks: make(chan []byte, 1)}
271+ input := workloadInput(scenario{ID: "s"}, 1, "measured-1")
272+ p.chunks <- newPTYLocalEcho(input).expected
273+ close(p.chunks)
274+ flushed := false
275+ if err := p.Measure(input, func() error { return nil }, func() error { flushed = true; return nil }); err == nil {
276+ t.Fatal("local PTY echo satisfied terminal flush boundary")
277+ }
278+ if flushed || output.syncs != 0 {
279+ t.Fatalf("local echo stamped a flush: flushed=%t syncs=%d", flushed, output.syncs)
280+ }
281+}
282+
283+func TestCLIProcessPairsApplicationOutputWithSuccessfulFlush(t *testing.T) {
222284 pty, output := &fakePTY{}, &fakeOutput{}
223285 p := &cliProcess{pty: pty, output: output, chunks: make(chan []byte, 1)}
224286 input := workloadInput(scenario{ID: "s"}, 1, "measured-1")
225- p.chunks <- append([]byte("terminal "), inputMarker(input)...)
287+ echo := newPTYLocalEcho(input).expected
288+ p.chunks <- append(append([]byte(nil), echo...), append([]byte("application "), inputMarker(input)...)...)
226289 order := []string{}
227290 if err := p.Measure(input, func() error { order = append(order, "injected"); return nil }, func() error { order = append(order, "flushed"); return nil }); err != nil {
228291 t.Fatal(err)
@@ -630,7 +693,14 @@ func TestHarnessRejectsCrossProcessAndBadTraceSpans(t *testing.T) {
630693 base := func(kind string, tick int64) traceRecord {
631694 return traceRecord{ProcessID: "one", Component: "daemon", Scenario: "s", Run: 1, Sequence: 1, RequestID: 1, Epoch: 1, Kind: kind, Tick: tick}
632695 }
633- for _, records := range [][]traceRecord{{base("diff_end", 1)}, {base("diff_start", 2), base("diff_end", 1)}, {base("diff_start", 1)}, {func() traceRecord { r := base("diff_start", 1); r.ProcessID = "other"; return r }()}} {
696+ for _, records := range [][]traceRecord{
697+ {base("diff_end", 1)},
698+ {base("diff_start", 2), base("diff_end", 1)},
699+ {base("diff_start", 1)},
700+ {func() traceRecord { r := base("diff_start", 1); r.ProcessID = "other"; return r }()},
701+ {func() traceRecord { r := base("diff_start", 1); r.Scenario = "other-scenario"; return r }()},
702+ {func() traceRecord { r := base("diff_start", 1); r.Run = 2; return r }()},
703+ } {
634704 if _, err := mergeProcessTraces([]processMapping{write(t, records...)}); err == nil {
635705 t.Fatalf("invalid trace accepted: %+v", records)
636706 }
@@ -157,6 +157,21 @@ func (p *fakePTY) Write(b []byte) (int, error) {
157 }157 }
158 func (*fakePTY) Close() error { return nil }158 func (*fakePTY) Close() error { return nil }
159 159
160+type stagedPTY struct {
161+ allow <-chan struct{}
162+ writes chan []byte
163+}
164+
165+func (*stagedPTY) Read([]byte) (int, error) { return 0, os.ErrClosed }
166+func (p *stagedPTY) Write(b []byte) (int, error) {
167+ if p.allow != nil {
168+ <-p.allow
169+ }
170+ p.writes <- append([]byte(nil), b...)
171+ return len(b), nil
172+}
173+func (*stagedPTY) Close() error { return nil }
174+
160 type fakeOutput struct {175 type fakeOutput struct {
161 bytes.Buffer176 bytes.Buffer
162 syncs int177 syncs int
@@ -218,11 +233,59 @@ func TestHarnessDoesNotInheritNestedSession(t *testing.T) {
218 }233 }
219 }234 }
220 235
221-func TestCLIProcessOwnsPTYInjectionAndSuccessfulFlushBoundary(t *testing.T) {236+func TestCLIProcessWarmupWaitsForDelayedTerminalReadiness(t *testing.T) {
237+ allowWrite := make(chan struct{})
238+ pty, output := &stagedPTY{allow: allowWrite, writes: make(chan []byte, 1)}, &fakeOutput{}
239+ p := &cliProcess{pty: pty, output: output, chunks: make(chan []byte)}
240+ input := workloadInput(scenario{ID: "s"}, 1, "warmup")
241+ done := make(chan error, 1)
242+ go func() { done <- p.Warmup(input) }()
243+
244+ // This is public client output from the initial application prompt/state,
245+ // emitted only after the client has entered raw mode. A premature PTY write
246+ // blocks on allowWrite and therefore cannot consume this delayed readiness.
247+ go func() {
248+ p.chunks <- []byte("shell prompt$ ")
249+ close(allowWrite)
250+ }()
251+ select {
252+ case got := <-pty.writes:
253+ if !bytes.Equal(got, input) {
254+ t.Fatalf("warmup write=%q want %q", got, input)
255+ }
256+ case <-time.After(time.Second):
257+ t.Fatal("warmup did not wait for and then proceed from client readiness")
258+ }
259+ go func() { p.chunks <- append([]byte("application "), inputMarker(input)...); close(p.chunks) }()
260+ if err := <-done; err != nil {
261+ t.Fatal(err)
262+ }
263+ if output.syncs != 1 {
264+ t.Fatalf("readiness output was not flushed: syncs=%d", output.syncs)
265+ }
266+}
267+
268+func TestCLIProcessRejectsPTYLocalEchoAsFlushEvidence(t *testing.T) {
269+ pty, output := &fakePTY{}, &fakeOutput{}
270+ p := &cliProcess{pty: pty, output: output, chunks: make(chan []byte, 1)}
271+ input := workloadInput(scenario{ID: "s"}, 1, "measured-1")
272+ p.chunks <- newPTYLocalEcho(input).expected
273+ close(p.chunks)
274+ flushed := false
275+ if err := p.Measure(input, func() error { return nil }, func() error { flushed = true; return nil }); err == nil {
276+ t.Fatal("local PTY echo satisfied terminal flush boundary")
277+ }
278+ if flushed || output.syncs != 0 {
279+ t.Fatalf("local echo stamped a flush: flushed=%t syncs=%d", flushed, output.syncs)
280+ }
281+}
282+
283+func TestCLIProcessPairsApplicationOutputWithSuccessfulFlush(t *testing.T) {
222 pty, output := &fakePTY{}, &fakeOutput{}284 pty, output := &fakePTY{}, &fakeOutput{}
223 p := &cliProcess{pty: pty, output: output, chunks: make(chan []byte, 1)}285 p := &cliProcess{pty: pty, output: output, chunks: make(chan []byte, 1)}
224 input := workloadInput(scenario{ID: "s"}, 1, "measured-1")286 input := workloadInput(scenario{ID: "s"}, 1, "measured-1")
225- p.chunks <- append([]byte("terminal "), inputMarker(input)...)287+ echo := newPTYLocalEcho(input).expected
288+ p.chunks <- append(append([]byte(nil), echo...), append([]byte("application "), inputMarker(input)...)...)
226 order := []string{}289 order := []string{}
227 if err := p.Measure(input, func() error { order = append(order, "injected"); return nil }, func() error { order = append(order, "flushed"); return nil }); err != nil {290 if err := p.Measure(input, func() error { order = append(order, "injected"); return nil }, func() error { order = append(order, "flushed"); return nil }); err != nil {
228 t.Fatal(err)291 t.Fatal(err)
@@ -630,7 +693,14 @@ func TestHarnessRejectsCrossProcessAndBadTraceSpans(t *testing.T) {
630 base := func(kind string, tick int64) traceRecord {693 base := func(kind string, tick int64) traceRecord {
631 return traceRecord{ProcessID: "one", Component: "daemon", Scenario: "s", Run: 1, Sequence: 1, RequestID: 1, Epoch: 1, Kind: kind, Tick: tick}694 return traceRecord{ProcessID: "one", Component: "daemon", Scenario: "s", Run: 1, Sequence: 1, RequestID: 1, Epoch: 1, Kind: kind, Tick: tick}
632 }695 }
633- for _, records := range [][]traceRecord{{base("diff_end", 1)}, {base("diff_start", 2), base("diff_end", 1)}, {base("diff_start", 1)}, {func() traceRecord { r := base("diff_start", 1); r.ProcessID = "other"; return r }()}} {696+ for _, records := range [][]traceRecord{
697+ {base("diff_end", 1)},
698+ {base("diff_start", 2), base("diff_end", 1)},
699+ {base("diff_start", 1)},
700+ {func() traceRecord { r := base("diff_start", 1); r.ProcessID = "other"; return r }()},
701+ {func() traceRecord { r := base("diff_start", 1); r.Scenario = "other-scenario"; return r }()},
702+ {func() traceRecord { r := base("diff_start", 1); r.Run = 2; return r }()},
703+ } {
634 if _, err := mergeProcessTraces([]processMapping{write(t, records...)}); err == nil {704 if _, err := mergeProcessTraces([]processMapping{write(t, records...)}); err == nil {
635 t.Fatalf("invalid trace accepted: %+v", records)705 t.Fatalf("invalid trace accepted: %+v", records)
636 }706 }

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