helmd v2: Go-server som styr agent i tmux — REST+SSE, frågedetektor, fildelning, ntfy, CI-release
Some checks failed
build-and-push / build (push) Successful in 42s
release-client / build-release (push) Has been cancelled
helmd-release / build-release (push) Successful in 1m6s

- tmux som sanningskälla (agentoberoende, överlever omstart via adoption)
- API: sessions, prompt, question/answer, keys, mode, config, shares, notify, events
- frågedetektor testad mot riktiga agy 1.1.9-dumpar + mock
- integrationstest: 26 tester i isolerad debian-container (scripts/run-integration.sh)
- e2e-verifierad mot riktig agy (trust-fråga -> svar -> prompt)
- .gitea/workflows/helmd-release.yaml -> rullande helmd-latest (x64+arm64)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 20:54:54 +02:00
parent 3e7152a884
commit f9c79ac84e
17 changed files with 2075 additions and 0 deletions

126
helmd/question_test.go Normal file
View File

@@ -0,0 +1,126 @@
package main
import "testing"
// Real captures from a live agy 1.1.9 session (2026-08-05).
const agyApproval = ` python3 -c '
import json
with open("analys.ipynb", "r", encoding="utf-8") as f:
⋯ (10 lines hidden)
Do you want to proceed?
> 1. Yes
2. Yes, and always allow in this conversation for commands that start with 'python3 -c '
import json
with open("analys.ipynb", "r", encoding="utf-8") as f:...'
3. Yes, and always allow for commands that start with 'python3 -c '
import json
with open("analys.ipynb", "r", encoding="utf-8") as f:...' (Persist to settings.json)
4. No
↑/↓ Navigate · tab Amend · ctrl+g edit/expand command
esc to cancel Gemini 3.6 Flash · high`
const agyTrust = `Accessing workspace:
/tmp/scratch/gemini-arena
Do you trust the contents of this project?
Antigravity CLI requires permission to read, edit, and execute files here.
> Yes, I trust this folder
No, exit
↑/↓ Navigate · enter Confirm
Gemini 3.6 Flash · high`
const agyIdle = ` ▄▀▀▄ Antigravity CLI 1.1.9
▀▀▀▀▀▀ brasse.bb@gmail.com (Google AI Pro)
────────────────────────────────────────────────────────────
>
────────────────────────────────────────────────────────────
? for shortcuts Gemini 3.6 Flash · high`
// Agent output containing a markdown numbered list must NOT trigger.
const agyMarkdownList = ` Here are 5 targeted CLI tools:
1. ntfy: sends push notifications to your phone.
2. tea: manages Gitea repositories from the command line.
3. skopeo: inspects registry images.
────────────────────────────────────────────────────────────
>
────────────────────────────────────────────────────────────
? for shortcuts Gemini 3.6 Flash · high`
const geminiAuthPicker = ` ╭──────────────────────────────────────────────╮
│ ? Get started │
│ │
│ How would you like to authenticate? │
│ │
│ ● 1. Sign in with Google │
│ 2. Use Gemini API Key │
│ 3. Vertex AI │
│ │
│ (Use Enter to select) │
╰──────────────────────────────────────────────╯`
func TestApprovalPrompt(t *testing.T) {
q := DetectQuestion(agyApproval)
if q == nil {
t.Fatal("expected question, got nil")
}
if !q.Numbered || len(q.Options) != 4 {
t.Fatalf("want 4 numbered options, got %+v", q.Options)
}
if q.Options[0].Label != "Yes" || q.Options[3].Label != "No" {
t.Errorf("bad labels: %+v", q.Options)
}
if q.Selected != 0 {
t.Errorf("want selected=0, got %d", q.Selected)
}
if q.Text != "Do you want to proceed?" {
t.Errorf("bad text: %q", q.Text)
}
}
func TestTrustPrompt(t *testing.T) {
q := DetectQuestion(agyTrust)
if q == nil {
t.Fatal("expected question, got nil")
}
if q.Numbered || len(q.Options) != 2 {
t.Fatalf("want 2 unnumbered options, got %+v", q.Options)
}
if q.Selected != 0 {
t.Errorf("want selected=0, got %d", q.Selected)
}
if q.Options[1].Label != "No, exit" {
t.Errorf("bad option: %+v", q.Options[1])
}
}
func TestIdleScreenNoQuestion(t *testing.T) {
if q := DetectQuestion(agyIdle); q != nil {
t.Fatalf("idle screen misdetected as question: %+v", q)
}
}
func TestMarkdownListNoQuestion(t *testing.T) {
if q := DetectQuestion(agyMarkdownList); q != nil {
t.Fatalf("markdown list misdetected as question: %+v", q)
}
}
func TestBoxedNumberedPicker(t *testing.T) {
q := DetectQuestion(geminiAuthPicker)
if q == nil {
t.Fatal("expected question, got nil")
}
if len(q.Options) != 3 || !q.Numbered {
t.Fatalf("want 3 numbered options, got %+v", q.Options)
}
}
func TestSameQuestionSameHash(t *testing.T) {
a, b := DetectQuestion(agyApproval), DetectQuestion(agyApproval)
if a.Hash != b.Hash {
t.Error("hash not stable")
}
if DetectQuestion(agyTrust).Hash == a.Hash {
t.Error("different questions share hash")
}
}