mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 23:40:28 -10:00
* feat(tui): syntax-highlighted code, word-level diffs, and a reduced-motion gate
The remaining three items from the reference-TUI comparison, focused on
"real content rendering" + motion accessibility.
1. Syntax-highlight fenced code blocks. renderMarkdownCodeBlock passed code
through plain; now the fence language is captured and the block is tokenized
with chroma (new dep, pure Go) and colored with Zero's OWN audited palette —
keyword=accent, string=green, number=amber, comment=faint, function=blue,
punctuation=muted — so it stays on-brand and degrades through the same
lipgloss profile path (truecolor→256→16→plain). Wrapping is token-level so a
color never splits across a wrap. Unknown/missing language falls back to the
old plain rendering, so it is never worse than today.
2. Word-level diff highlighting. diffCardBody tinted whole add/del lines; now an
isolated 1:1 replacement (one "-" followed by one "+") highlights only the
changed span on each side via a brighter changed-span bg (new addBgWord/
delBgWord tokens), after trimming the common prefix/suffix. Gated to <=60%
change so a near-rewrite is not confettied, and block changes keep whole-line
tint. Changed-span colors validated by WCAG ratio (fg >=4.6:1, clear
separation from the base band).
3. One reduced-motion gate. ZERO_REDUCED_MOTION (and no-TTY) now swaps the
animated spinner for a steady dot via m.spinnerGlyph() at every render site
(working line, plan, tool/specialist cards) and forces the fade off — one
switch for all motion, where ZERO_NO_FADE only governed the fade. Liveness is
preserved by the advancing elapsed timer, so a static frame never reads as
frozen.
Tests cover the reduced-motion resolver + static glyph, changedSpan/word-diff
pairing + the near-rewrite fallback, and the highlighter's unknown-language
fallback + wrapping. Full internal/tui suite green; gofmt/vet/build clean.
* fix(tui): address render-polish review — highlight committed rows only
Adversarial review found a real interactive-jank regression and three smaller
issues; all fixed:
- PERF (blocker): the live streaming block re-tokenised the whole accumulated
code via chroma on every frame. renderAssistantMarkdownText now takes
allowHighlight; the interim/streaming path (model.interimBlock) and reasoning
bodies pass false (plain, cheap), and only committed/cached rows
(renderAssistantRow, selectable rows) highlight — so chroma runs once per row,
never per frame.
- PERF: cache language->lexer lookups (including nil) and short-circuit an empty
language before chroma's slow registry Match scan.
- Fences with metadata: take the first whitespace token as the language
("```go title=x" -> "go") instead of the whole info string.
- Reduced motion: freeze the compact and doctor braille rings too (their frame
counters no longer advance under ZERO_REDUCED_MOTION), honoring the one-switch
contract.
Tests: streaming code renders verbatim/plain; diff word-span colors AA on both
themes. Full internal/tui suite green; gofmt/vet/build clean.
* test(tui): opt the doctor/compact animation tests into motion
The reduced-motion gate auto-enables under no-TTY (colorprofile.NoTTY ->
reducedMotion), which is exactly the CI environment. That froze the compact and
doctor status rings, so two existing tests asserting "animates on tick" failed on
the Linux/macOS runners (Windows CI detects a color profile, so it passed there —
the platform split). Both tests exercise the animation mechanism, which is only
active when motion is on, so set m.reducedMotion = false in their setup to test it
deterministically regardless of the runner's detected profile.
370 lines
12 KiB
Go
370 lines
12 KiB
Go
package tui
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
|
|
"github.com/Gitlawb/zero/internal/config"
|
|
"github.com/Gitlawb/zero/internal/doctor"
|
|
"github.com/Gitlawb/zero/internal/providerhealth"
|
|
)
|
|
|
|
func TestParseDoctorCommandArgsRejectsFixWithConnectivity(t *testing.T) {
|
|
_, _, _, err := parseDoctorCommandArgs("fix --connectivity")
|
|
if err == nil {
|
|
t.Fatal("parseDoctorCommandArgs accepted both fix and --connectivity")
|
|
}
|
|
if !strings.Contains(err.Error(), "choose either") {
|
|
t.Fatalf("error = %q, want mutually exclusive flag guidance", err)
|
|
}
|
|
}
|
|
|
|
func TestDoctorOptionsIncludeConfigPaths(t *testing.T) {
|
|
m := newModel(context.Background(), Options{
|
|
UserConfigPath: "C:/zero/user.json",
|
|
ProjectConfigPath: "C:/repo/.zero/config.json",
|
|
ProviderProfile: config.ProviderProfile{
|
|
Name: "openai",
|
|
ProviderKind: config.ProviderKindOpenAI,
|
|
Model: "gpt-4.1",
|
|
},
|
|
})
|
|
|
|
options := m.doctorOptions(false)
|
|
|
|
if options.UserConfig != "C:/zero/user.json" {
|
|
t.Fatalf("UserConfig = %q, want %q", options.UserConfig, "C:/zero/user.json")
|
|
}
|
|
if options.ProjectConfig != "C:/repo/.zero/config.json" {
|
|
t.Fatalf("ProjectConfig = %q, want %q", options.ProjectConfig, "C:/repo/.zero/config.json")
|
|
}
|
|
if options.Connectivity {
|
|
t.Fatal("Connectivity = true, want false")
|
|
}
|
|
if options.ProviderHealth != nil {
|
|
t.Fatalf("ProviderHealth = %#v, want nil without connectivity", options.ProviderHealth)
|
|
}
|
|
}
|
|
|
|
func TestDoctorOptionsConnectivityInvokesConfiguredProviderHealthProbe(t *testing.T) {
|
|
profile := config.ProviderProfile{
|
|
Name: "custom",
|
|
ProviderKind: config.ProviderKindOpenAICompatible,
|
|
BaseURL: "https://api.example.com/v1",
|
|
Model: "custom-model",
|
|
}
|
|
var called int
|
|
var gotOptions providerhealth.Options
|
|
m := newModel(context.Background(), Options{
|
|
ProviderProfile: profile,
|
|
UserAgent: "zero-test",
|
|
ProbeProviderHealth: func(_ context.Context, options providerhealth.Options) providerhealth.Result {
|
|
called++
|
|
gotOptions = options
|
|
return providerhealth.Result{
|
|
Status: providerhealth.StatusPass,
|
|
Checks: []providerhealth.Check{{
|
|
ID: "provider.connectivity",
|
|
Status: providerhealth.StatusPass,
|
|
Message: "reachable",
|
|
}},
|
|
}
|
|
},
|
|
})
|
|
|
|
options := m.doctorOptions(true)
|
|
report := doctor.Run(options)
|
|
|
|
if called != 1 {
|
|
t.Fatalf("probe called %d time(s), want 1", called)
|
|
}
|
|
if !gotOptions.Connectivity {
|
|
t.Fatal("probe Connectivity = false, want true")
|
|
}
|
|
if gotOptions.UserAgent != "zero-test" {
|
|
t.Fatalf("probe UserAgent = %q, want %q", gotOptions.UserAgent, "zero-test")
|
|
}
|
|
if !reflect.DeepEqual(gotOptions.Profile, profile) {
|
|
t.Fatalf("probe Profile = %#v, want %#v", gotOptions.Profile, profile)
|
|
}
|
|
check := report.Check("provider.connectivity")
|
|
if options.ProviderHealth == nil || check == nil || check.Status != doctor.StatusPass {
|
|
t.Fatalf("connectivity check = %#v, ProviderHealth = %#v; want passing injected health", check, options.ProviderHealth)
|
|
}
|
|
}
|
|
|
|
func TestDoctorConnectivityCommandRunsProbeAsynchronously(t *testing.T) {
|
|
profile := config.ProviderProfile{
|
|
Name: "custom",
|
|
ProviderKind: config.ProviderKindOpenAICompatible,
|
|
BaseURL: "https://api.example.com/v1",
|
|
Model: "custom-model",
|
|
}
|
|
called := false
|
|
m := newModel(context.Background(), Options{
|
|
ProviderProfile: profile,
|
|
ProbeProviderHealth: func(context.Context, providerhealth.Options) providerhealth.Result {
|
|
called = true
|
|
return providerhealth.Result{
|
|
Status: providerhealth.StatusPass,
|
|
Checks: []providerhealth.Check{{
|
|
ID: "provider.connectivity",
|
|
Status: providerhealth.StatusPass,
|
|
Message: "reachable",
|
|
}},
|
|
}
|
|
},
|
|
})
|
|
m.input.SetValue("/doctor --connectivity")
|
|
|
|
updated, cmd := m.Update(testKey(tea.KeyEnter))
|
|
next := updated.(model)
|
|
if cmd == nil {
|
|
t.Fatal("expected /doctor --connectivity to return an async command")
|
|
}
|
|
if called {
|
|
t.Fatal("provider probe ran synchronously before the returned command executed")
|
|
}
|
|
if !transcriptContains(next.transcript, "checking provider connectivity") {
|
|
t.Fatalf("expected running doctor status, got %#v", next.transcript)
|
|
}
|
|
|
|
msg := execCmd(cmd)
|
|
if !called {
|
|
t.Fatal("provider probe did not run when the async command executed")
|
|
}
|
|
updated, _ = next.Update(msg)
|
|
final := updated.(model)
|
|
for _, want := range []string{"Diagnostics", "[pass] provider.connectivity", "reachable"} {
|
|
if !transcriptContains(final.transcript, want) {
|
|
t.Fatalf("expected final doctor transcript to contain %q, got %#v", want, final.transcript)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDoctorCommandUsesDiagnosticCenterRow(t *testing.T) {
|
|
m := newModel(context.Background(), Options{
|
|
ProviderProfile: config.ProviderProfile{
|
|
Name: "custom",
|
|
ProviderKind: config.ProviderKindOpenAICompatible,
|
|
BaseURL: "https://api.example.com/v1",
|
|
Model: "custom-model",
|
|
},
|
|
})
|
|
m.input.SetValue("/doctor")
|
|
|
|
updated, cmd := m.Update(testKey(tea.KeyEnter))
|
|
next := updated.(model)
|
|
if cmd != nil {
|
|
t.Fatal("expected plain /doctor to render synchronously")
|
|
}
|
|
row := newestDoctorStatusRow(next.transcript)
|
|
if row == nil {
|
|
t.Fatalf("expected /doctor to render a doctor status row, got %#v", next.transcript)
|
|
}
|
|
if row.id != doctorStatusRowID {
|
|
t.Fatalf("expected /doctor row id %q, got %q", doctorStatusRowID, row.id)
|
|
}
|
|
text := row.text
|
|
for _, want := range []string{"Diagnostics", "checks need attention", "Actions"} {
|
|
if !strings.Contains(text, want) {
|
|
t.Fatalf("doctor row missing %q:\n%s", want, text)
|
|
}
|
|
}
|
|
for _, unwanted := range []string{"Generated", "Checks", "[pass]"} {
|
|
if strings.Contains(text, unwanted) {
|
|
t.Fatalf("doctor row should hide %q:\n%s", unwanted, text)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDoctorConnectivityCommandAnimatesAndReplacesStatusRow(t *testing.T) {
|
|
profile := config.ProviderProfile{
|
|
Name: "custom",
|
|
ProviderKind: config.ProviderKindOpenAICompatible,
|
|
BaseURL: "https://api.example.com/v1",
|
|
Model: "custom-model",
|
|
}
|
|
m := newModel(context.Background(), Options{
|
|
ProviderProfile: profile,
|
|
ProbeProviderHealth: func(context.Context, providerhealth.Options) providerhealth.Result {
|
|
return providerhealth.Result{
|
|
Status: providerhealth.StatusPass,
|
|
Checks: []providerhealth.Check{{
|
|
ID: "provider.connectivity",
|
|
Status: providerhealth.StatusPass,
|
|
Message: "reachable",
|
|
}},
|
|
}
|
|
},
|
|
})
|
|
// This test exercises the tick-driven status animation, which is gated off
|
|
// under reduced motion. CI runs with no TTY (reducedMotion auto-on), so force
|
|
// it off to test the animation mechanism deterministically.
|
|
m.reducedMotion = false
|
|
m.input.SetValue("/doctor --connectivity")
|
|
|
|
updated, cmd := m.Update(testKey(tea.KeyEnter))
|
|
next := updated.(model)
|
|
if cmd == nil {
|
|
t.Fatal("expected /doctor --connectivity to start an async command")
|
|
}
|
|
beforeRows := len(next.transcript)
|
|
before := newestDoctorStatusText(next.transcript)
|
|
|
|
updated, _ = next.Update(next.spinner.Tick())
|
|
ticked := updated.(model)
|
|
after := newestDoctorStatusText(ticked.transcript)
|
|
if before == "" || after == "" {
|
|
t.Fatalf("expected doctor status row before=%q after=%q", before, after)
|
|
}
|
|
if before == after {
|
|
t.Fatalf("expected doctor status to animate on tick, still %q", after)
|
|
}
|
|
|
|
updated, _ = ticked.Update(execCmd(cmd))
|
|
final := updated.(model)
|
|
if len(final.transcript) != beforeRows {
|
|
t.Fatalf("expected final doctor report to replace the running row, rows before=%d after=%d: %#v", beforeRows, len(final.transcript), final.transcript)
|
|
}
|
|
if got := countDoctorDiagnosticRows(final.transcript); got != 1 {
|
|
t.Fatalf("expected one doctor diagnostic row after completion, got %d: %#v", got, final.transcript)
|
|
}
|
|
if transcriptContains(final.transcript, "checking provider connectivity") {
|
|
t.Fatalf("expected stale running copy to be replaced, got %#v", final.transcript)
|
|
}
|
|
if !transcriptContains(final.transcript, "[pass] provider.connectivity") {
|
|
t.Fatalf("expected completed diagnostics result, got %#v", final.transcript)
|
|
}
|
|
}
|
|
|
|
func TestDoctorFixOpensProviderWizardWhenProviderMissing(t *testing.T) {
|
|
m := newModel(context.Background(), Options{})
|
|
m.input.SetValue("/doctor fix")
|
|
|
|
updated, cmd := m.Update(testKey(tea.KeyEnter))
|
|
next := updated.(model)
|
|
if cmd != nil {
|
|
t.Fatal("expected /doctor fix provider setup path to be handled synchronously")
|
|
}
|
|
if next.providerWizard == nil {
|
|
t.Fatalf("expected /doctor fix to open provider setup wizard, transcript %#v", next.transcript)
|
|
}
|
|
if !transcriptContains(next.transcript, "Opening provider setup") {
|
|
t.Fatalf("expected doctor fix transcript to explain provider setup, got %#v", next.transcript)
|
|
}
|
|
}
|
|
|
|
func TestDoctorFixLinesShowNoAutomaticFixForUnmappedFailure(t *testing.T) {
|
|
lines := doctorFixLines(doctor.Report{Checks: []doctor.Check{{
|
|
ID: "provider.auth",
|
|
Status: doctor.StatusFail,
|
|
Message: "API key rejected.",
|
|
}}})
|
|
|
|
text := strings.Join(lines, "\n")
|
|
if strings.Contains(text, "already clean") {
|
|
t.Fatalf("doctorFixLines reported clean diagnostics for an unmapped failure:\n%s", text)
|
|
}
|
|
if !strings.Contains(text, "No automatic fixes are available") {
|
|
t.Fatalf("doctorFixLines = %q, want no automatic fix guidance", text)
|
|
}
|
|
}
|
|
|
|
func TestDoctorFixLinesUseSandboxRemedy(t *testing.T) {
|
|
lines := doctorFixLines(doctor.Report{Checks: []doctor.Check{{
|
|
ID: "sandbox.backend",
|
|
Status: doctor.StatusWarn,
|
|
Message: "Native sandbox backend unavailable on windows: Windows sandbox setup helper is not available.",
|
|
Details: map[string]any{
|
|
"remedy": "install the Windows sandbox command runner and setup helper together, then run `zero sandbox setup`",
|
|
},
|
|
}}})
|
|
|
|
text := strings.Join(lines, "\n")
|
|
if !strings.Contains(text, "zero sandbox setup") {
|
|
t.Fatalf("doctorFixLines missing sandbox setup remedy:\n%s", text)
|
|
}
|
|
if strings.Contains(text, "WSL2") || strings.Contains(text, "Linux container") {
|
|
t.Fatalf("doctorFixLines used stale Windows sandbox guidance:\n%s", text)
|
|
}
|
|
}
|
|
|
|
func newestDoctorStatusText(rows []transcriptRow) string {
|
|
if row := newestDoctorStatusRow(rows); row != nil {
|
|
return row.text
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func newestDoctorStatusRow(rows []transcriptRow) *transcriptRow {
|
|
for i := len(rows) - 1; i >= 0; i-- {
|
|
row := rows[i]
|
|
if row.kind == rowSystem && row.id == doctorStatusRowID {
|
|
return &rows[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func countDoctorDiagnosticRows(rows []transcriptRow) int {
|
|
count := 0
|
|
for _, row := range rows {
|
|
if row.kind == rowSystem && row.id == doctorStatusRowID {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func TestDoctorFixRunsConnectivityWhenProviderConfigured(t *testing.T) {
|
|
profile := config.ProviderProfile{
|
|
Name: "custom",
|
|
ProviderKind: config.ProviderKindOpenAICompatible,
|
|
BaseURL: "https://api.example.com/v1",
|
|
Model: "custom-model",
|
|
APIKey: "sk-test", // credentialed so provider.config passes and /doctor fix reaches connectivity
|
|
}
|
|
called := false
|
|
m := newModel(context.Background(), Options{
|
|
ProviderProfile: profile,
|
|
ProbeProviderHealth: func(context.Context, providerhealth.Options) providerhealth.Result {
|
|
called = true
|
|
return providerhealth.Result{
|
|
Status: providerhealth.StatusPass,
|
|
Checks: []providerhealth.Check{{
|
|
ID: "provider.connectivity",
|
|
Status: providerhealth.StatusPass,
|
|
Message: "reachable",
|
|
}},
|
|
}
|
|
},
|
|
})
|
|
m.input.SetValue("/doctor fix")
|
|
|
|
updated, cmd := m.Update(testKey(tea.KeyEnter))
|
|
next := updated.(model)
|
|
if cmd == nil {
|
|
t.Fatal("expected /doctor fix to run provider connectivity asynchronously")
|
|
}
|
|
if called {
|
|
t.Fatal("provider probe ran synchronously before the returned command executed")
|
|
}
|
|
if !transcriptContains(next.transcript, "checking provider connectivity") {
|
|
t.Fatalf("expected running doctor status, got %#v", next.transcript)
|
|
}
|
|
|
|
updated, _ = next.Update(execCmd(cmd))
|
|
final := updated.(model)
|
|
if !called {
|
|
t.Fatal("provider probe did not run when /doctor fix command executed")
|
|
}
|
|
if !transcriptContains(final.transcript, "[pass] provider.connectivity") {
|
|
t.Fatalf("expected connectivity result in transcript, got %#v", final.transcript)
|
|
}
|
|
}
|