mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-26 21:36:31 +02:00
Single Go module, four top-level packages under packages/. Import
paths become github.com/patriceckhart/zot/packages/<name>; downstream
consumers can depend on individual packages without pulling the rest.
Layout:
packages/provider/ LLM clients + catalog
packages/provider/auth/ credential store + OAuth + login server
packages/core/ agent loop, sessions, cost
packages/tui/ terminal toolkit + chat view
packages/agent/ CLI wiring, system prompt
extensions/ extproto/ modes/ tools/ skills/ swarm/
sdk/ (was pkg/zotcore, package renamed zotcore -> sdk)
ext/ (was pkg/zotext, package renamed zotext -> ext)
internal/ and pkg/ removed. The internal/assets logo moved into
packages/provider/auth/assets.
Public Go SDK identifiers renamed:
pkg/zotcore (package zotcore) -> packages/agent/sdk (package sdk)
pkg/zotext (package zotext) -> packages/agent/ext (package ext)
This breaks Go-based extensions and embedders; the JSON wire protocol
for extensions and RPC is unchanged, so non-Go extensions, already-
built extension binaries, and zot rpc consumers are unaffected.
Docs, examples, and the built-in write-zot-extension skill updated
for the new paths and identifiers. Shadow-bug fixes in code samples
(ext := ext.New -> e := ext.New).
143 lines
5.6 KiB
Go
143 lines
5.6 KiB
Go
package tui
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestWrapLineFirstContinuationHasIndent is a regression test for a
|
|
// bug where wrapLine()'s internal newLine() toggled the firstLine
|
|
// flag and THEN checked it, so the very first wrap continuation
|
|
// flushed without the cont indent. Any subsequent continuation
|
|
// (second wrap onwards) got the indent. That was visible as a
|
|
// misaligned second row and caused the editor's cursor to land in
|
|
// the wrong column after a multi-line paste (locateCursor assumes
|
|
// continuations carry cont, so when they didn't, the cursor drifted
|
|
// one-indent-worth to the right).
|
|
func TestWrapLineFirstContinuationHasIndent(t *testing.T) {
|
|
s := "prefix this is a long line that will wrap around at forty cells"
|
|
out := wrapLine(s, 40, " ")
|
|
|
|
if len(out) < 2 {
|
|
t.Fatalf("want at least 2 wrapped rows, got %d: %v", len(out), out)
|
|
}
|
|
// Row 0 is the first line (no indent; it's the lead).
|
|
// Every row from index 1 onward is a continuation and MUST start
|
|
// with the cont prefix.
|
|
for i := 1; i < len(out); i++ {
|
|
if !strings.HasPrefix(out[i], " ") {
|
|
t.Errorf("row %d missing cont indent: %q", i, out[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestEditorCursorAfterMultilinePaste is the downstream test: the
|
|
// rendered editor cursor must land at the logical end of the paste,
|
|
// with its visual column equal to leadW + runewidth(last-line).
|
|
//
|
|
// Uses Insert directly to bypass the KeyPaste collapse path (which
|
|
// would turn this into a placeholder token); the test's concern is
|
|
// wrap / cursor math, not paste behaviour.
|
|
func TestEditorCursorAfterMultilinePaste(t *testing.T) {
|
|
e := NewEditor("▌ ")
|
|
e.Insert("aaa\nbbb\nccccc")
|
|
|
|
// Logical end: last line "ccccc", cursor past its 5 runes.
|
|
if e.CursorR != 2 || e.CursorC != 5 {
|
|
t.Fatalf("logical cursor: want (2, 5), got (%d, %d)", e.CursorR, e.CursorC)
|
|
}
|
|
|
|
lines, row, col := e.Render(80)
|
|
if len(lines) != 3 {
|
|
t.Fatalf("want 3 rendered rows, got %d: %v", len(lines), lines)
|
|
}
|
|
// Row 0 "▌ aaa", row 1 " bbb", row 2 " ccccc".
|
|
// Cursor lives at row 2; column = 2 (cont indent) + 5 = 7.
|
|
if row != 2 {
|
|
t.Errorf("visual row: want 2, got %d", row)
|
|
}
|
|
if col != 7 {
|
|
t.Errorf("visual col: want 7, got %d", col)
|
|
}
|
|
}
|
|
|
|
// TestEditorCursorAfterLongPasteWithWrap verifies the cursor lands
|
|
// correctly when a pasted line is long enough to wrap at the given
|
|
// render width. This is the scenario that was broken: before the
|
|
// fix, the first wrap continuation missed its cont indent, so the
|
|
// terminal cursor drifted when typed after pasting a wrapped path.
|
|
func TestEditorCursorAfterLongPasteWithWrap(t *testing.T) {
|
|
e := NewEditor("▌ ")
|
|
// Direct Insert bypasses the multi-line collapse path; the
|
|
// concern here is wrap-column math, not the collapse logic.
|
|
e.Insert("this is a very long line that should wrap\nshort")
|
|
|
|
lines, row, col := e.Render(30)
|
|
|
|
// Every continuation row (anything after row 0) must be
|
|
// cont-indented so locateCursor's rune-counting stays honest.
|
|
for i := 1; i < len(lines); i++ {
|
|
if !strings.HasPrefix(lines[i], " ") {
|
|
t.Errorf("continuation row %d missing indent: %q", i, lines[i])
|
|
}
|
|
}
|
|
// Cursor should be at the end of "short" on the last rendered row.
|
|
if row != len(lines)-1 {
|
|
t.Errorf("visual row: want %d (last), got %d", len(lines)-1, row)
|
|
}
|
|
if col != 2+5 { // cont indent + len("short")
|
|
t.Errorf("visual col: want 7, got %d", col)
|
|
}
|
|
}
|
|
|
|
// TestWrapLongTokenKeepsPromptOnFirstLine guards against a bug where
|
|
// pasting a single very long path (no spaces within it) stranded the
|
|
// prompt on its own line and started the path on line 2 with the
|
|
// cont indent. That happened because wrapLine broke to a new line
|
|
// before checking whether the oncoming token would need rune-by-rune
|
|
// splitting anyway. Stranding the prompt offset locateCursor's
|
|
// rune walk and the terminal cursor drew in the wrong column after
|
|
// any typing past the paste.
|
|
func TestWrapLongTokenKeepsPromptOnFirstLine(t *testing.T) {
|
|
// Long single token (a path) + prompt "▌ ". Width 40, the token
|
|
// itself is way longer than (40 - cont).
|
|
s := "▌ /var/folders/xq/hdh5qm6j66nbzd0sh3ljsxyc0000gn/T/TemporaryItems/verylongnamehere"
|
|
out := wrapLine(s, 40, " ")
|
|
|
|
if len(out) < 2 {
|
|
t.Fatalf("expected multiple wrapped rows, got %d: %v", len(out), out)
|
|
}
|
|
// Row 0 must contain BOTH the prompt and the start of the path,
|
|
// not just the prompt alone. A stranded-prompt regression would
|
|
// show up as a short first row and the path starting on row 1.
|
|
if !strings.HasPrefix(out[0], "▌ /") {
|
|
t.Errorf("row 0 should start with the prompt AND path content, got %q", out[0])
|
|
}
|
|
if visibleWidth(out[0]) < 20 {
|
|
t.Errorf("row 0 width %d too small; prompt got stranded", visibleWidth(out[0]))
|
|
}
|
|
}
|
|
|
|
// TestEditorCursorAfterLongPathPaste verifies the on-screen cursor
|
|
// lines up with the logical end of the buffer after a drag-dropped
|
|
// long path is pasted AND the user types additional characters.
|
|
// Screenshot scenario from 20:15 on 2026-04-19.
|
|
func TestEditorCursorAfterLongPathPaste(t *testing.T) {
|
|
e := NewEditor("▌ ")
|
|
path := "/very/long/path/that/exceeds/our/terminal/width/by/a/comfortable/margin/file.png"
|
|
e.HandleKey(Key{Kind: KeyPaste, Paste: path})
|
|
for _, r := range "hello" {
|
|
e.HandleKey(Key{Kind: KeyRune, Rune: r})
|
|
}
|
|
lines, row, col := e.Render(40)
|
|
|
|
// Cursor must be on the last rendered row.
|
|
if row != len(lines)-1 {
|
|
t.Errorf("visual row: want %d (last), got %d (lines: %v)", len(lines)-1, row, lines)
|
|
}
|
|
// And its column should equal the width of the last row (cursor
|
|
// at end-of-line).
|
|
if col != visibleWidth(lines[len(lines)-1]) {
|
|
t.Errorf("visual col %d != last-row width %d", col, visibleWidth(lines[len(lines)-1]))
|
|
}
|
|
}
|