102 lines
3.6 KiB
Go
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))
|
|
}
|
|
}
|