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).
107 lines
3.1 KiB
Go
107 lines
3.1 KiB
Go
package core
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/patriceckhart/zot/packages/provider"
|
|
)
|
|
|
|
func TestSessionRoundTrip(t *testing.T) {
|
|
dir := t.TempDir()
|
|
os.Setenv("ZOT_HOME", dir)
|
|
|
|
sess, err := NewSession(dir, "/tmp/project", "anthropic", "claude-sonnet-4-5", "test")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
msg := provider.Message{
|
|
Role: provider.RoleUser,
|
|
Content: []provider.Content{
|
|
provider.TextBlock{Text: "hello"},
|
|
},
|
|
Time: time.Now().UTC(),
|
|
}
|
|
if err := sess.AppendMessage(msg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := sess.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
reopened, msgs, err := OpenSession(sess.Path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// OpenSession returns a live append writer; close it before t.TempDir
|
|
// runs cleanup, otherwise windows refuses to delete the open file.
|
|
t.Cleanup(func() { _ = reopened.Close() })
|
|
if len(msgs) != 1 {
|
|
t.Fatalf("got %d messages", len(msgs))
|
|
}
|
|
tb, ok := msgs[0].Content[0].(provider.TextBlock)
|
|
if !ok || tb.Text != "hello" {
|
|
t.Fatalf("got %+v", msgs[0])
|
|
}
|
|
}
|
|
|
|
// TestNewSessionAtPathCreatesAtExplicitPath proves the swarm child's
|
|
// session-persistence fallback works: NewSessionAtPath creates the
|
|
// file (and parent dirs) at the exact path the caller chose,
|
|
// independent of SessionsDir(root, cwd). Without this helper, the
|
|
// swarm child's --session <path> with a non-existent path would
|
|
// fall through to NewSession and write the session under a
|
|
// different (autogenerated) name, leaving the supervisor-chosen
|
|
// path empty.
|
|
func TestNewSessionAtPathCreatesAtExplicitPath(t *testing.T) {
|
|
dir := t.TempDir()
|
|
want := dir + "/nested/sub/session.json"
|
|
|
|
s, err := NewSessionAtPath(want, "/tmp/proj", "anthropic", "claude", "test")
|
|
if err != nil {
|
|
t.Fatalf("NewSessionAtPath: %v", err)
|
|
}
|
|
if s.Path != want {
|
|
t.Errorf("Path = %q; want %q", s.Path, want)
|
|
}
|
|
if err := s.AppendMessage(provider.Message{
|
|
Role: provider.RoleUser,
|
|
Content: []provider.Content{provider.TextBlock{Text: "hi"}},
|
|
Time: time.Now().UTC(),
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Reopen at the same path and the message must still be there.
|
|
reopened, msgs, err := OpenSession(want)
|
|
if err != nil {
|
|
t.Fatalf("OpenSession at fixed path: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = reopened.Close() })
|
|
if len(msgs) != 1 {
|
|
t.Fatalf("reopen got %d msgs; want 1", len(msgs))
|
|
}
|
|
|
|
// A second call to NewSessionAtPath at the same path must fail
|
|
// (O_EXCL): callers should use OpenSession to reattach to an
|
|
// existing file.
|
|
if _, err := NewSessionAtPath(want, "/tmp/proj", "anthropic", "claude", "test"); err == nil {
|
|
t.Error("NewSessionAtPath on existing path returned nil; want error")
|
|
}
|
|
}
|
|
|
|
func TestCostAdd(t *testing.T) {
|
|
var c CostTracker
|
|
c.Add(provider.Usage{InputTokens: 100, OutputTokens: 50, CostUSD: 0.01})
|
|
c.Add(provider.Usage{InputTokens: 200, OutputTokens: 25, CostUSD: 0.02})
|
|
if c.Total.InputTokens != 300 || c.Total.OutputTokens != 75 {
|
|
t.Fatalf("got %+v", c.Total)
|
|
}
|
|
if c.Total.CostUSD < 0.0299 || c.Total.CostUSD > 0.0301 {
|
|
t.Fatalf("got cost %v", c.Total.CostUSD)
|
|
}
|
|
}
|