Some echo tests added.
This commit is contained in:
1 parent
8a32895457
commit
eb5b2038d6
38 files changed
+1739
-31
No files matched your search
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// Package messaging owns bounded, session-only conversation history. Application
|
||||
// adapters supply copied messages; the reader owns input, output and security.
|
||||
package messaging
|
||||
|
||||
import "strings"
|
||||
|
||||
const HistorySize = 9
|
||||
const maxProviders = 32
|
||||
|
||||
// Message contains no native objects. Provider is the unique accessibility bus
|
||||
// name, so restarting an application cannot expose its previous instance's chat.
|
||||
type Message struct {
|
||||
Provider, Conversation, Window, RoomName, Text string
|
||||
Typing bool
|
||||
}
|
||||
|
||||
type providerHistory struct {
|
||||
provider string
|
||||
messages []Message
|
||||
}
|
||||
|
||||
// History is owned by the reader event loop. Focus changes retain it; window
|
||||
// destruction retires that window's messages, and screen lock clears it all.
|
||||
type History struct{ providers []providerHistory }
|
||||
|
||||
func (history *History) Add(message Message) bool {
|
||||
message.Text = strings.Trim(message.Text, "\n")
|
||||
if message.Typing || strings.TrimSpace(message.Text) == "" || message.Provider == "" || message.Conversation == "" || message.Window == "" {
|
||||
return false
|
||||
}
|
||||
characters := []rune(message.Text)
|
||||
message.Text = string(characters[:min(len(characters), 4096)])
|
||||
index := -1
|
||||
for i := range history.providers {
|
||||
if history.providers[i].provider == message.Provider {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index < 0 {
|
||||
if len(history.providers) == maxProviders {
|
||||
history.providers = history.providers[1:]
|
||||
}
|
||||
history.providers = append(history.providers, providerHistory{provider: message.Provider})
|
||||
index = len(history.providers) - 1
|
||||
}
|
||||
provider := &history.providers[index]
|
||||
if len(provider.messages) == HistorySize {
|
||||
provider.messages = provider.messages[1:]
|
||||
}
|
||||
provider.messages = append(provider.messages, message)
|
||||
return true
|
||||
}
|
||||
|
||||
// Previous uses zero for the newest message, matching Cthulhu+F1.
|
||||
func (history *History) Previous(provider string, index int) (Message, bool) {
|
||||
if index < 0 || index >= HistorySize {
|
||||
return Message{}, false
|
||||
}
|
||||
for _, entry := range history.providers {
|
||||
if entry.provider == provider && index < len(entry.messages) {
|
||||
return entry.messages[len(entry.messages)-1-index], true
|
||||
}
|
||||
}
|
||||
return Message{}, false
|
||||
}
|
||||
|
||||
func (history *History) Retire(provider, path string) {
|
||||
for i := len(history.providers) - 1; i >= 0; i-- {
|
||||
entry := &history.providers[i]
|
||||
if entry.provider != provider {
|
||||
continue
|
||||
}
|
||||
messages := entry.messages[:0]
|
||||
for _, message := range entry.messages {
|
||||
if message.Window != path && message.Conversation != path {
|
||||
messages = append(messages, message)
|
||||
}
|
||||
}
|
||||
clear(entry.messages[len(messages):])
|
||||
entry.messages = messages
|
||||
if len(messages) == 0 {
|
||||
history.providers = append(history.providers[:i], history.providers[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (history *History) Reset() { history.providers = nil }
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package messaging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHistoryOrdersConversationsAndIsolatesApplicationInstances(t *testing.T) {
|
||||
var history History
|
||||
for i := 0; i < 12; i++ {
|
||||
history.Add(Message{Provider: ":1.42", Conversation: fmt.Sprintf("/room%d", i%2), Window: "/window", Text: fmt.Sprintf("Message %d", i)})
|
||||
}
|
||||
history.Add(Message{Provider: ":1.43", Conversation: "/room0", Window: "/window", Text: "Other instance"})
|
||||
for i := 0; i < HistorySize; i++ {
|
||||
message, ok := history.Previous(":1.42", i)
|
||||
if !ok || message.Text != fmt.Sprintf("Message %d", 11-i) {
|
||||
t.Fatalf("slot %d = %+v, %t", i, message, ok)
|
||||
}
|
||||
}
|
||||
for _, index := range []int{-1, 9} {
|
||||
if _, ok := history.Previous(":1.42", index); ok {
|
||||
t.Fatal("out-of-range slot exists")
|
||||
}
|
||||
}
|
||||
if _, ok := history.Previous(":1.44", 0); ok {
|
||||
t.Fatal("restarted provider inherited old history")
|
||||
}
|
||||
// Identical messages from real insertions are legitimate separate entries.
|
||||
for range 2 {
|
||||
history.Add(Message{Provider: ":1.42", Conversation: "/room0", Window: "/window", Text: "Repeated"})
|
||||
}
|
||||
for i := range 2 {
|
||||
message, _ := history.Previous(":1.42", i)
|
||||
if message.Text != "Repeated" {
|
||||
t.Fatal("identical messages collapsed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHistoryRetirementTypingAndBounds(t *testing.T) {
|
||||
var history History
|
||||
message := Message{Provider: ":1.42", Conversation: "/one", Window: "/window1", Text: "First"}
|
||||
history.Add(message)
|
||||
message.Conversation, message.Window, message.Text = "/two", "/window2", "Second"
|
||||
history.Add(message)
|
||||
message.Typing = true
|
||||
if history.Add(message) {
|
||||
t.Fatal("typing entered history")
|
||||
}
|
||||
history.Retire(":1.42", "/window1")
|
||||
if got, _ := history.Previous(":1.42", 0); got.Text != "Second" {
|
||||
t.Fatal("unrelated window was retired")
|
||||
}
|
||||
if _, ok := history.Previous(":1.42", 1); ok {
|
||||
t.Fatal("destroyed window remains")
|
||||
}
|
||||
history.Retire(":1.42", "/two")
|
||||
if _, ok := history.Previous(":1.42", 0); ok {
|
||||
t.Fatal("destroyed transcript remains")
|
||||
}
|
||||
message.Typing = false
|
||||
message.Text = "\n" + strings.Repeat("界", 5000) + "\n"
|
||||
history.Add(message)
|
||||
if got, _ := history.Previous(":1.42", 0); len([]rune(got.Text)) != 4096 {
|
||||
t.Fatal("Unicode message is not bounded")
|
||||
}
|
||||
for i := 0; i < 64; i++ {
|
||||
message.Provider = fmt.Sprintf(":1.%d", i+100)
|
||||
history.Add(message)
|
||||
}
|
||||
if len(history.providers) != maxProviders {
|
||||
t.Fatal("provider count is not bounded")
|
||||
}
|
||||
history.Reset()
|
||||
if _, ok := history.Previous(message.Provider, 0); ok {
|
||||
t.Fatal("lock retained messages")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user