Files
jarvis-home/cmd/skillworker/main_test.go
2026-08-29 17:02:35 +02:00

102 lines
3.6 KiB
Go

package main
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)
func TestWorkerReloadCopiesReadOnlySkillAndExecutes(t *testing.T) {
skills := t.TempDir()
work := t.TempDir()
dir := filepath.Join(skills, "demo")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
manifestJSON := `{
"protocol":"jarvis.skill.v1",
"id":"test.remote",
"name":"Remote test",
"version":"1.0.0",
"runtime":{"type":"process","command":"./run","timeout_ms":5000},
"actions":[{
"name":"echo",
"description":"echo",
"input_schema":{"type":"object","properties":{"text":{"type":"string"}},"required":["text"],"additionalProperties":false},
"output_schema":{"type":"object","properties":{"text":{"type":"string"}},"required":["text"],"additionalProperties":false}
}]
}`
if err := os.WriteFile(filepath.Join(dir, "skill.json"), []byte(manifestJSON), 0o644); err != nil {
t.Fatal(err)
}
run := `#!/bin/sh
python3 -c 'import json,sys; r=json.load(sys.stdin); json.dump({"protocol":"jarvis.skill.invoke.v1","success":True,"data":{"text":r["input"]["text"]}},sys.stdout)'
`
if err := os.WriteFile(filepath.Join(dir, "run"), []byte(run), 0o755); err != nil {
t.Fatal(err)
}
w := &worker{skillsDir: skills, workDir: work, maxOutput: 1 << 20, runtime: "test", skills: map[string]manifest{}, actions: map[string]skillRef{}}
if err := w.reload(); err != nil {
t.Fatal(err)
}
ref, ok := w.actions["test.remote\x00echo"]
if !ok {
t.Fatalf("action not loaded")
}
if filepath.Dir(ref.Dir) != work {
t.Fatalf("skill did not execute from isolated work copy: %s", ref.Dir)
}
res := w.execute(context.Background(), ref, invokeRequest{Protocol: invokeProtocol, RequestID: "x", SkillID: "test.remote", Action: "echo", Input: map[string]any{"text": "hello"}, Context: invokeContext{Timezone: "UTC"}})
if !res.Success {
t.Fatalf("execution failed: %+v", res)
}
b, _ := json.Marshal(res.Data)
if string(b) != `{"text":"hello"}` {
t.Fatalf("data=%s", b)
}
}
func TestWorkerEnvironmentExplicitEnvFrom(t *testing.T) {
t.Setenv("HUE_APP_KEY", "secret-key")
t.Setenv("UNRELATED_SECRET", "must-not-leak")
w := &worker{runtime: "python"}
ref := skillRef{Manifest: manifest{ID: "test.env", Runtime: runtimeSpec{EnvFrom: []string{"HUE_APP_KEY", "JARVIS_MESH_ENROLLMENT_TOKEN"}}}, Action: actionManifest{Name: "run"}}
env := w.environment(ref, invokeRequest{RequestID: "req", Context: invokeContext{Timezone: "Europe/Berlin"}})
got := map[string]string{}
for _, item := range env {
parts := strings.SplitN(item, "=", 2)
if len(parts) == 2 {
got[parts[0]] = parts[1]
}
}
if got["HUE_APP_KEY"] != "secret-key" {
t.Fatalf("explicit env_from was not passed: %#v", got)
}
if _, ok := got["UNRELATED_SECRET"]; ok {
t.Fatalf("unrelated parent secret leaked into skill env")
}
if _, ok := got["JARVIS_MESH_ENROLLMENT_TOKEN"]; ok {
t.Fatalf("JARVIS control env must never be passed to skills")
}
}
func TestHomeControlSkillPackManifestsLoad(t *testing.T) {
skillsDir := filepath.Clean(filepath.Join("..", "..", "skills", "python"))
w := &worker{skillsDir: skillsDir, maxOutput: 1 << 20, runtime: "python", skills: map[string]manifest{}, actions: map[string]skillRef{}}
if err := w.reload(); err != nil {
t.Fatalf("home-control skill pack did not load: %v", err)
}
for _, id := range []string{"philips.hue", "unifi.network", "unifi.protect", "proxmox.ve", "dockge.compose", "home.network-tools", "notify.ntfy"} {
if _, ok := w.skills[id]; !ok {
t.Fatalf("expected skill %s to be loaded", id)
}
}
if len(w.actions) < 42 {
t.Fatalf("expected home-control actions, got %d", len(w.actions))
}
}