- Update agent contract documentation for openai-compatible API - Update automation-runtime-bridge milestone tracking - Add edge smoke tests for openai CLI workspace - Add node CLI adapters (codex, opencode, oneshot, persistent) - Add e2e openai-cli-workspace script - Add agent task tracking for execution contract
1266 lines
37 KiB
Go
1266 lines
37 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
"iop/packages/go/config"
|
|
"iop/packages/go/version"
|
|
)
|
|
|
|
func TestRootCmdIncludesOperationalCommands(t *testing.T) {
|
|
root := rootCmd()
|
|
want := map[string]bool{
|
|
"serve": false,
|
|
"console": false,
|
|
"version": false,
|
|
"config": false,
|
|
"env": false,
|
|
"setup": false,
|
|
"bootstrap": false,
|
|
"node": false,
|
|
"nodes": false,
|
|
"smoke": false,
|
|
}
|
|
for _, c := range root.Commands() {
|
|
if _, ok := want[c.Name()]; ok {
|
|
want[c.Name()] = true
|
|
}
|
|
}
|
|
for name, ok := range want {
|
|
if !ok {
|
|
t.Errorf("root command missing %q", name)
|
|
}
|
|
}
|
|
|
|
cfg, _, err := root.Find([]string{"config"})
|
|
if err != nil {
|
|
t.Fatalf("find config: %v", err)
|
|
}
|
|
subs := map[string]bool{"print": false, "check": false}
|
|
subs["init"] = false
|
|
for _, c := range cfg.Commands() {
|
|
if _, ok := subs[c.Name()]; ok {
|
|
subs[c.Name()] = true
|
|
}
|
|
}
|
|
for name, ok := range subs {
|
|
if !ok {
|
|
t.Errorf("config subcommand missing %q", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVersionCmdPrintsVersion(t *testing.T) {
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"version"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute: %v", err)
|
|
}
|
|
got := strings.TrimSpace(out.String())
|
|
if got != version.Version {
|
|
t.Fatalf("version output = %q, want %q", got, version.Version)
|
|
}
|
|
}
|
|
|
|
func TestConfigCheckCmdLoadsEdgeConfig(t *testing.T) {
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"config", "check", "--config", "../../../../configs/edge.yaml"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute: %v\n%s", err, out.String())
|
|
}
|
|
if !strings.Contains(out.String(), "OK") {
|
|
t.Fatalf("expected OK in output, got %q", out.String())
|
|
}
|
|
}
|
|
|
|
func TestConfigInitWritesTemplateThatChecks(t *testing.T) {
|
|
dir := t.TempDir()
|
|
outPath := filepath.Join(dir, "edge.yaml")
|
|
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"config", "init", "--output", outPath})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute init: %v\n%s", err, out.String())
|
|
}
|
|
if !strings.Contains(out.String(), "wrote "+outPath) {
|
|
t.Fatalf("expected written path in output, got %q", out.String())
|
|
}
|
|
|
|
root = rootCmd()
|
|
out.Reset()
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"config", "check", "--config", outPath})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute check: %v\n%s", err, out.String())
|
|
}
|
|
if !strings.Contains(out.String(), "OK "+outPath) {
|
|
t.Fatalf("expected config check OK for generated file, got %q", out.String())
|
|
}
|
|
}
|
|
|
|
func TestConfigInitPrintsTemplateToStdout(t *testing.T) {
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"config", "init", "--stdout"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute: %v\n%s", err, out.String())
|
|
}
|
|
for _, want := range []string{"edge:", "server:", "nodes: []"} {
|
|
if !strings.Contains(out.String(), want) {
|
|
t.Fatalf("template output missing %q:\n%s", want, out.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConfigInitPrintsBundleTemplateWithEmptyLogPath(t *testing.T) {
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"config", "init", "--stdout"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute: %v\n%s", err, out.String())
|
|
}
|
|
got := out.String()
|
|
for _, want := range []string{
|
|
"advertise_host:",
|
|
"bootstrap:",
|
|
"artifact_base_url:",
|
|
"path: \"\"",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("bundle init output missing %q\n---\n%s", want, got)
|
|
}
|
|
}
|
|
if strings.Contains(got, "/var/lib/iop/edge/logs/edge.log") {
|
|
t.Errorf("bundle init leaked service log path:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestConfigInitRefusesOverwriteWithoutForce(t *testing.T) {
|
|
outPath := filepath.Join(t.TempDir(), "edge.yaml")
|
|
if err := os.WriteFile(outPath, []byte("preserved: true\n"), 0o600); err != nil {
|
|
t.Fatalf("write existing: %v", err)
|
|
}
|
|
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"config", "init", "--output", outPath})
|
|
if err := root.Execute(); err == nil {
|
|
t.Fatalf("expected overwrite refusal, output:\n%s", out.String())
|
|
}
|
|
got, err := os.ReadFile(outPath)
|
|
if err != nil {
|
|
t.Fatalf("read existing: %v", err)
|
|
}
|
|
if string(got) != "preserved: true\n" {
|
|
t.Fatalf("existing file was overwritten: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestSetupDryRunUsesEdgeDefaults(t *testing.T) {
|
|
cfgFile = "configs/edge.yaml" // reset to root default
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"setup", "--dry-run", "--binary", "/usr/local/bin/iop-edge"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute: %v\n%s", err, out.String())
|
|
}
|
|
s := out.String()
|
|
for _, want := range []string{
|
|
"/etc/iop/edge.yaml",
|
|
"/var/lib/iop/edge",
|
|
"/etc/systemd/system/iop-edge.service",
|
|
"/usr/local/bin/iop-edge serve --config /etc/iop/edge.yaml",
|
|
"--- unit preview ---",
|
|
"--- config preview ---",
|
|
"path: \"/var/lib/iop/edge/logs/edge.log\"",
|
|
} {
|
|
if !strings.Contains(s, want) {
|
|
t.Errorf("dry-run output missing %q\n---\n%s", want, s)
|
|
}
|
|
}
|
|
if strings.Contains(s, "configs/edge.yaml") {
|
|
t.Errorf("setup default leaked root dev config path: %s", s)
|
|
}
|
|
}
|
|
|
|
func TestSetupDryRunAcceptsExplicitConfig(t *testing.T) {
|
|
cfgFile = "configs/edge.yaml" // reset
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
custom := filepath.Join(t.TempDir(), "edge.yaml")
|
|
root.SetArgs([]string{"--config", custom, "setup", "--dry-run", "--binary", "/usr/local/bin/iop-edge"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute: %v\n%s", err, out.String())
|
|
}
|
|
if !strings.Contains(out.String(), custom) {
|
|
t.Errorf("explicit config not used; output:\n%s", out.String())
|
|
}
|
|
}
|
|
|
|
const minimalEdgeYAML = "server:\n listen: \"0.0.0.0:9090\"\n"
|
|
|
|
func writeMinimalEdgeYAML(t *testing.T, path string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, []byte(minimalEdgeYAML), 0o600); err != nil {
|
|
t.Fatalf("write %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
func TestConfigCheckUsesBundleEdgeYAMLByDefault(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeMinimalEdgeYAML(t, filepath.Join(dir, "edge.yaml"))
|
|
t.Chdir(dir)
|
|
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"config", "check"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute: %v\n%s", err, out.String())
|
|
}
|
|
if !strings.Contains(out.String(), "OK edge.yaml") {
|
|
t.Fatalf("expected OK edge.yaml (cwd bundle), got %q", out.String())
|
|
}
|
|
}
|
|
|
|
func TestExplicitConfigWinsOverBundleDefault(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeMinimalEdgeYAML(t, filepath.Join(dir, "edge.yaml"))
|
|
t.Chdir(dir)
|
|
|
|
otherDir := t.TempDir()
|
|
other := filepath.Join(otherDir, "edge.yaml")
|
|
writeMinimalEdgeYAML(t, other)
|
|
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"config", "check", "--config", other})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute: %v\n%s", err, out.String())
|
|
}
|
|
if !strings.Contains(out.String(), "OK "+other) {
|
|
t.Fatalf("expected OK %s, got %q", other, out.String())
|
|
}
|
|
}
|
|
|
|
func TestConfigDiscoveryReportsMissingWhenNoBundleFound(t *testing.T) {
|
|
dir := t.TempDir()
|
|
t.Chdir(dir)
|
|
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"config", "check"})
|
|
err := root.Execute()
|
|
if err == nil {
|
|
t.Fatalf("expected error when no edge.yaml found, got output: %s", out.String())
|
|
}
|
|
if !strings.Contains(err.Error(), "no edge.yaml found") {
|
|
t.Fatalf("expected discovery error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEnvCmdPrintsResolvedReportWithExplicitAdvertiseHost(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfgPath := filepath.Join(dir, "edge.yaml")
|
|
yaml := `server:
|
|
listen: "0.0.0.0:9090"
|
|
advertise_host: "edge.example.test"
|
|
bootstrap:
|
|
artifact_base_url: "http://edge.example.test:18080"
|
|
openai:
|
|
enabled: true
|
|
listen: "0.0.0.0:8080"
|
|
nodes:
|
|
- id: "node-a"
|
|
alias: "a"
|
|
token: "t-a"
|
|
- id: "node-b"
|
|
alias: "b"
|
|
token: "t-b"
|
|
`
|
|
if err := os.WriteFile(cfgPath, []byte(yaml), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"env", "--config", cfgPath})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute: %v\n%s", err, out.String())
|
|
}
|
|
|
|
got := out.String()
|
|
wants := []string{
|
|
"config: " + cfgPath,
|
|
"binary_dir: ",
|
|
"log_path: ",
|
|
"advertise_host: edge.example.test (explicit)",
|
|
"node_transport: edge.example.test:9090",
|
|
"artifact_base_url: http://edge.example.test:18080",
|
|
"openai_url: http://edge.example.test:8080/v1",
|
|
"configured_nodes: 2 (node-a, node-b)",
|
|
"connected_nodes: offline",
|
|
}
|
|
for _, w := range wants {
|
|
if !strings.Contains(got, w) {
|
|
t.Errorf("env output missing %q\n---\n%s", w, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBootstrapPackCreatesNodeArtifacts(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfgPath := filepath.Join(dir, "edge.yaml")
|
|
yamlStr := `server:
|
|
listen: "127.0.0.1:39090"
|
|
advertise_host: "edge.example.test"
|
|
bootstrap:
|
|
listen: "127.0.0.1:38080"
|
|
artifact_dir: "artifacts"
|
|
`
|
|
if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
|
|
t.Fatalf("write config: %v", err)
|
|
}
|
|
nodeBinary := filepath.Join(dir, "iop-node")
|
|
if err := os.WriteFile(nodeBinary, []byte("fake-node-binary"), 0o755); err != nil {
|
|
t.Fatalf("write node binary: %v", err)
|
|
}
|
|
outDir := filepath.Join(dir, "out")
|
|
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{
|
|
"--config", cfgPath,
|
|
"bootstrap", "pack",
|
|
"--node-binary", nodeBinary,
|
|
"--output", outDir,
|
|
"--target", "linux-arm64",
|
|
})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute pack: %v\n%s", err, out.String())
|
|
}
|
|
|
|
for _, path := range []string{
|
|
filepath.Join(outDir, "linux-arm64", "iop-node"),
|
|
filepath.Join(outDir, "linux-arm64", "SHA256SUMS"),
|
|
filepath.Join(outDir, "bootstrap", "node.sh"),
|
|
filepath.Join(outDir, "bootstrap", "node-linux-arm64.sh"),
|
|
} {
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Fatalf("expected artifact %s: %v", path, err)
|
|
}
|
|
}
|
|
script, err := os.ReadFile(filepath.Join(outDir, "bootstrap", "node-linux-arm64.sh"))
|
|
if err != nil {
|
|
t.Fatalf("read script: %v", err)
|
|
}
|
|
if !strings.Contains(string(script), `DEFAULT_ARTIFACT_BASE_URL="http://edge.example.test:38080"`) {
|
|
t.Fatalf("script missing derived artifact URL:\n%s", script)
|
|
}
|
|
if !strings.Contains(string(script), `DEFAULT_EDGE_ADDR="edge.example.test:39090"`) {
|
|
t.Fatalf("script missing derived edge addr:\n%s", script)
|
|
}
|
|
universalScript, err := os.ReadFile(filepath.Join(outDir, "bootstrap", "node.sh"))
|
|
if err != nil {
|
|
t.Fatalf("read universal script: %v", err)
|
|
}
|
|
if !strings.Contains(string(universalScript), `DEFAULT_TARGET=""`) {
|
|
t.Fatalf("universal script should detect target dynamically:\n%s", universalScript)
|
|
}
|
|
if !strings.Contains(string(universalScript), `detect_target()`) {
|
|
t.Fatalf("universal script missing target detection:\n%s", universalScript)
|
|
}
|
|
}
|
|
|
|
func TestNodeRegisterRefreshesExistingBootstrapArtifact(t *testing.T) {
|
|
dir := t.TempDir()
|
|
artifactDir := filepath.Join(dir, "artifacts")
|
|
target := "darwin-arm64"
|
|
targetDir := filepath.Join(artifactDir, target)
|
|
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
|
t.Fatalf("mkdir target dir: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(targetDir, "iop-node"), []byte("fake-node-binary"), 0o755); err != nil {
|
|
t.Fatalf("write node artifact: %v", err)
|
|
}
|
|
|
|
cfgPath := filepath.Join(dir, "edge.yaml")
|
|
yamlStr := `server:
|
|
listen: "127.0.0.1:39090"
|
|
advertise_host: "edge.deploy.test"
|
|
bootstrap:
|
|
listen: "127.0.0.1:38080"
|
|
artifact_dir: "` + artifactDir + `"
|
|
nodes: []
|
|
`
|
|
if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
|
|
t.Fatalf("write config: %v", err)
|
|
}
|
|
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{
|
|
"--config", cfgPath,
|
|
"node", "register", "mac-node",
|
|
"--target", target,
|
|
"--token", "test-token",
|
|
})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute register: %v\n%s", err, out.String())
|
|
}
|
|
|
|
script, err := os.ReadFile(filepath.Join(artifactDir, "bootstrap", "node-"+target+".sh"))
|
|
if err != nil {
|
|
t.Fatalf("read refreshed script: %v", err)
|
|
}
|
|
universalScript, err := os.ReadFile(filepath.Join(artifactDir, "bootstrap", "node.sh"))
|
|
if err != nil {
|
|
t.Fatalf("read refreshed universal script: %v", err)
|
|
}
|
|
if !strings.Contains(string(script), `DEFAULT_ARTIFACT_BASE_URL="http://edge.deploy.test:38080"`) {
|
|
t.Fatalf("script missing refreshed artifact URL:\n%s", script)
|
|
}
|
|
if !strings.Contains(string(universalScript), `DEFAULT_ARTIFACT_BASE_URL="http://edge.deploy.test:38080"`) {
|
|
t.Fatalf("universal script missing refreshed artifact URL:\n%s", universalScript)
|
|
}
|
|
if !strings.Contains(string(script), `DEFAULT_EDGE_ADDR="edge.deploy.test:39090"`) {
|
|
t.Fatalf("script missing refreshed edge addr:\n%s", script)
|
|
}
|
|
if !strings.Contains(string(universalScript), `DEFAULT_EDGE_ADDR="edge.deploy.test:39090"`) {
|
|
t.Fatalf("universal script missing refreshed edge addr:\n%s", universalScript)
|
|
}
|
|
if !strings.Contains(out.String(), `curl -fsSL http://edge.deploy.test:38080/bootstrap/node.sh | bash -s test-token`) {
|
|
t.Fatalf("unexpected register output:\n%s", out.String())
|
|
}
|
|
}
|
|
|
|
func TestConfigCheckRejectsDuplicateNodeTokenAndInvalidConfigs(t *testing.T) {
|
|
// Duplicate token test
|
|
dir := t.TempDir()
|
|
cfgPath := filepath.Join(dir, "edge.yaml")
|
|
yamlStr := `server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- id: "node-a"
|
|
alias: "a"
|
|
token: "same-token"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
- id: "node-b"
|
|
alias: "b"
|
|
token: "same-token"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
`
|
|
if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"config", "check", "--config", cfgPath})
|
|
if err := root.Execute(); err == nil {
|
|
t.Fatalf("expected error due to duplicate token, got nil")
|
|
}
|
|
|
|
// Invalid runtime concurrency test
|
|
yamlStr2 := `server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- id: "node-a"
|
|
alias: "a"
|
|
token: "token-a"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
runtime:
|
|
concurrency: -1
|
|
`
|
|
if err := os.WriteFile(cfgPath, []byte(yamlStr2), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
root = rootCmd()
|
|
out.Reset()
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"config", "check", "--config", cfgPath})
|
|
if err := root.Execute(); err == nil {
|
|
t.Fatalf("expected error due to negative concurrency, got nil")
|
|
}
|
|
|
|
// Invalid Ollama BaseURL test
|
|
yamlStr3 := `server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- id: "node-a"
|
|
alias: "a"
|
|
token: "token-a"
|
|
adapters:
|
|
ollama:
|
|
enabled: true
|
|
base_url: ""
|
|
`
|
|
if err := os.WriteFile(cfgPath, []byte(yamlStr3), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
root = rootCmd()
|
|
out.Reset()
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"config", "check", "--config", cfgPath})
|
|
if err := root.Execute(); err == nil {
|
|
t.Fatalf("expected error due to empty ollama base_url, got nil")
|
|
}
|
|
}
|
|
|
|
func TestConfigCheckRejectsAdapterlessNode(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfgPath := filepath.Join(dir, "edge.yaml")
|
|
|
|
yamlStr := `server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- id: "node-a"
|
|
alias: "node-a"
|
|
token: "token-a"
|
|
agent_kind: "generic-node"
|
|
`
|
|
if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"config", "check", "--config", cfgPath})
|
|
err := root.Execute()
|
|
if err == nil {
|
|
t.Fatalf("expected adapterless generic node config to fail, got OK output %q", out.String())
|
|
}
|
|
if !strings.Contains(err.Error(), "at least one adapter must be enabled") {
|
|
t.Fatalf("expected adapterless error, got %v\n%s", err, out.String())
|
|
}
|
|
}
|
|
|
|
func TestNodeRegisterSwitchesCLIToOllama(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfgPath := filepath.Join(dir, "edge.yaml")
|
|
|
|
yamlStr := `server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- id: "node-a"
|
|
alias: "a"
|
|
token: "token-a"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
`
|
|
if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-a", "--adapter", "ollama"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute switch to ollama: %v\n%s", err, out.String())
|
|
}
|
|
|
|
data, err := os.ReadFile(cfgPath)
|
|
if err != nil {
|
|
t.Fatalf("read config: %v", err)
|
|
}
|
|
var cfg config.EdgeConfig
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
|
|
if !cfg.Nodes[0].Adapters.Ollama.Enabled {
|
|
t.Fatal("expected ollama to be enabled")
|
|
}
|
|
if cfg.Nodes[0].Adapters.CLI.Enabled {
|
|
t.Fatal("expected cli to be disabled")
|
|
}
|
|
// Preferred path: base_url was set to default
|
|
if cfg.Nodes[0].Adapters.Ollama.BaseURL != "http://127.0.0.1:11434" {
|
|
t.Fatalf("expected preferred fallback base url, got %q", cfg.Nodes[0].Adapters.Ollama.BaseURL)
|
|
}
|
|
}
|
|
|
|
func TestNodeRegisterRejectsDuplicateAliasAndDuplicateToken(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfgPath := filepath.Join(dir, "edge.yaml")
|
|
|
|
yamlStr := `server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- id: "node-a"
|
|
alias: "a"
|
|
token: "token-a"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
`
|
|
if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
originalBytes, err := os.ReadFile(cfgPath)
|
|
if err != nil {
|
|
t.Fatalf("read original: %v", err)
|
|
}
|
|
|
|
// 1. Try to register node-b with DUPLICATE alias "a"
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-b", "--alias", "a", "--token", "token-b"})
|
|
if err := root.Execute(); err == nil {
|
|
t.Fatalf("expected error due to duplicate alias, got nil")
|
|
}
|
|
|
|
output := out.String()
|
|
if strings.Contains(output, "Registered node") || strings.Contains(output, "Updated node") || strings.Contains(output, "curl -fsSL") {
|
|
t.Errorf("stdout should not contain success clues on failure, got:\n%s", output)
|
|
}
|
|
|
|
// Assert YAML file is byte-for-byte unchanged
|
|
currentBytes, err := os.ReadFile(cfgPath)
|
|
if err != nil {
|
|
t.Fatalf("read current: %v", err)
|
|
}
|
|
if !bytes.Equal(originalBytes, currentBytes) {
|
|
t.Fatal("expected edge.yaml to remain unchanged on validation failure")
|
|
}
|
|
|
|
// 2. Try to register node-b with DUPLICATE token "token-a"
|
|
root = rootCmd()
|
|
out.Reset()
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-b", "--alias", "b", "--token", "token-a"})
|
|
if err := root.Execute(); err == nil {
|
|
t.Fatalf("expected error due to duplicate token, got nil")
|
|
}
|
|
|
|
currentBytes2, err := os.ReadFile(cfgPath)
|
|
if err != nil {
|
|
t.Fatalf("read current2: %v", err)
|
|
}
|
|
if !bytes.Equal(originalBytes, currentBytes2) {
|
|
t.Fatal("expected edge.yaml to remain unchanged on validation failure")
|
|
}
|
|
|
|
// 3. Confirm config check still passes on original file
|
|
root = rootCmd()
|
|
out.Reset()
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"config", "check", "--config", cfgPath})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("config check failed on original valid file: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNodeRegisterPrintsOneLineBootstrapCommandAndUsesConfiguredArtifactBaseURL(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfgPath := filepath.Join(dir, "edge.yaml")
|
|
|
|
// Create minimal config but with configured artifact base url
|
|
yamlStr := `server:
|
|
listen: "0.0.0.0:9090"
|
|
bootstrap:
|
|
artifact_base_url: "http://configured-artifact-server:8080"
|
|
`
|
|
if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-test", "--target", "darwin-arm64", "--token", "test-token"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute register: %v\n%s", err, out.String())
|
|
}
|
|
|
|
output := out.String()
|
|
expectedCmd := "curl -fsSL http://configured-artifact-server:8080/bootstrap/node.sh | bash -s test-token"
|
|
if !strings.Contains(output, expectedCmd) {
|
|
t.Errorf("expected bootstrap command %q, got %q", expectedCmd, output)
|
|
}
|
|
|
|
// Now override artifact base URL with flag
|
|
root = rootCmd()
|
|
out.Reset()
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-test", "--target", "linux-amd64", "--artifact-base-url", "http://flag-artifact-server:9000", "--token", "test-token-2"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute register override: %v\n%s", err, out.String())
|
|
}
|
|
|
|
output = out.String()
|
|
expectedCmdOverride := "curl -fsSL http://flag-artifact-server:9000/bootstrap/node.sh | bash -s test-token-2"
|
|
if !strings.Contains(output, expectedCmdOverride) {
|
|
t.Errorf("expected bootstrap command %q, got %q", expectedCmdOverride, output)
|
|
}
|
|
}
|
|
|
|
func TestNodeRegisterUsesAdvertiseHostForDerivedArtifactBaseURL(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfgPath := filepath.Join(dir, "edge.yaml")
|
|
|
|
yamlStr := `server:
|
|
listen: "0.0.0.0:9090"
|
|
advertise_host: "edge.example.test"
|
|
bootstrap:
|
|
artifact_base_url: ""
|
|
`
|
|
if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-test", "--target", "linux-amd64", "--token", "test-token"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute register: %v\n%s", err, out.String())
|
|
}
|
|
|
|
expectedCmd := "curl -fsSL http://edge.example.test:18080/bootstrap/node.sh | bash -s test-token"
|
|
if !strings.Contains(out.String(), expectedCmd) {
|
|
t.Errorf("expected bootstrap command %q, got %q", expectedCmd, out.String())
|
|
}
|
|
}
|
|
|
|
func TestTempFileCLIWorkflowConfigInitNodeRegisterConfigCheck(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfgPath := filepath.Join(dir, "edge.yaml")
|
|
t.Chdir(dir)
|
|
|
|
// 1. config init
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"config", "init", "--output", cfgPath})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute init: %v\n%s", err, out.String())
|
|
}
|
|
|
|
// 2. node register
|
|
root = rootCmd()
|
|
out.Reset()
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"node", "register", "node-silicon-ollama", "--adapter", "ollama", "--ollama-base-url", "http://127.0.0.1:11434", "--target", "darwin-arm64", "--artifact-base-url", "http://toki-labs.com:18080", "--token", "test-token"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute register: %v\n%s", err, out.String())
|
|
}
|
|
|
|
output := out.String()
|
|
expectedCmd := "curl -fsSL http://toki-labs.com:18080/bootstrap/node.sh | bash -s test-token"
|
|
if !strings.Contains(output, expectedCmd) {
|
|
t.Errorf("expected bootstrap command %q, got %q", expectedCmd, output)
|
|
}
|
|
|
|
// 3. config check
|
|
root = rootCmd()
|
|
out.Reset()
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"config", "check"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute check: %v\n%s", err, out.String())
|
|
}
|
|
|
|
if !strings.Contains(out.String(), "OK edge.yaml") {
|
|
t.Fatalf("expected check OK, got %q", out.String())
|
|
}
|
|
}
|
|
|
|
func TestNodeRegisterPreservesDefaultedNonNodeConfig(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfgPath := filepath.Join(dir, "edge.yaml")
|
|
t.Chdir(dir)
|
|
|
|
// Write minimal config with server.listen and openai.enabled, omitting other defaults
|
|
yamlStr := `server:
|
|
listen: "0.0.0.0:9090"
|
|
openai:
|
|
enabled: true
|
|
`
|
|
if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
|
|
t.Fatalf("write yaml: %v", err)
|
|
}
|
|
|
|
// Run node register node-a
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"--config", cfgPath, "node", "register", "node-a", "--token", "token-a"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("execute register: %v\n%s", err, out.String())
|
|
}
|
|
|
|
// 1. Assert original YAML structure still omits the defaulted fields (preserves raw layout)
|
|
rawBytes, err := os.ReadFile(cfgPath)
|
|
if err != nil {
|
|
t.Fatalf("read raw config: %v", err)
|
|
}
|
|
rawStr := string(rawBytes)
|
|
badSubstrings := []string{
|
|
"openai:\n listen:",
|
|
"openai:\n listen:",
|
|
"openai:\n adapter:",
|
|
"openai:\n adapter:",
|
|
"openai:\n session_id:",
|
|
"openai:\n session_id:",
|
|
"openai:\n timeout_sec:",
|
|
"openai:\n timeout_sec:",
|
|
}
|
|
for _, sub := range badSubstrings {
|
|
// Replace standard newlines with system newlines just in case, but standard is fine.
|
|
if strings.Contains(rawStr, sub) || strings.Contains(rawStr, strings.ReplaceAll(sub, "\n", "\r\n")) {
|
|
t.Errorf("expected raw edge.yaml to still omit default field, but found explicit %q in YAML:\n%s", sub, rawStr)
|
|
}
|
|
}
|
|
|
|
// 2. Assert effective OpenAI defaults are resolved correctly (not zeroed out)
|
|
cfgPtr, err := config.LoadEdge(cfgPath)
|
|
if err != nil {
|
|
t.Fatalf("load effective config: %v", err)
|
|
}
|
|
cfg := *cfgPtr
|
|
|
|
if cfg.OpenAI.Listen != "0.0.0.0:18081" {
|
|
t.Errorf("expected effective openai.listen to remain 0.0.0.0:18081, got %q", cfg.OpenAI.Listen)
|
|
}
|
|
if cfg.OpenAI.Adapter != "ollama" {
|
|
t.Errorf("expected effective openai.adapter to remain ollama, got %q", cfg.OpenAI.Adapter)
|
|
}
|
|
if cfg.OpenAI.SessionID != "openai" {
|
|
t.Errorf("expected effective openai.session_id to remain openai, got %q", cfg.OpenAI.SessionID)
|
|
}
|
|
if cfg.OpenAI.TimeoutSec != 120 {
|
|
t.Errorf("expected effective openai.timeout_sec to remain 120, got %d", cfg.OpenAI.TimeoutSec)
|
|
}
|
|
if !cfg.OpenAI.StrictOutput {
|
|
t.Errorf("expected effective openai.strict_output to remain true")
|
|
}
|
|
|
|
// 3. Assert nodes[] has the new node and config check passes
|
|
if len(cfg.Nodes) != 1 {
|
|
t.Fatalf("expected exactly 1 node record, got %d", len(cfg.Nodes))
|
|
}
|
|
if cfg.Nodes[0].ID != "node-a" || cfg.Nodes[0].Token != "token-a" {
|
|
t.Fatalf("unexpected registered node: %+v", cfg.Nodes[0])
|
|
}
|
|
|
|
root = rootCmd()
|
|
out.Reset()
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"config", "check", "--config", cfgPath})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("config check failed: %v\n%s", err, out.String())
|
|
}
|
|
}
|
|
|
|
func TestHelpDiscoversEdgeLocalDevFlow(t *testing.T) {
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"--help"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("help execute failed: %v", err)
|
|
}
|
|
got := out.String()
|
|
|
|
// 1. Assert official flow-level help
|
|
wants := []string{
|
|
"1. config init",
|
|
"2. env",
|
|
"3. node register",
|
|
"4. config check",
|
|
"5. serve",
|
|
"6. nodes list",
|
|
"7. smoke openai",
|
|
}
|
|
for _, w := range wants {
|
|
if !strings.Contains(got, w) {
|
|
t.Errorf("root help output missing official flow step: %q", w)
|
|
}
|
|
}
|
|
|
|
// 2. Assert absence of non-official/legacy paths (verify they aren't recommended)
|
|
for _, unwanted := range []string{"scripts/dev/edge.sh", "scripts/dev/node.sh", "dev-deploy", "agent register"} {
|
|
if strings.Contains(got, unwanted) {
|
|
t.Errorf("root help contains legacy path reference: %q", unwanted)
|
|
}
|
|
}
|
|
|
|
helpCases := []struct {
|
|
name string
|
|
args []string
|
|
want []string
|
|
}{
|
|
{
|
|
name: "config",
|
|
args: []string{"config", "--help"},
|
|
want: []string{"config init", "iop-edge env", "node register", "config check", "serve"},
|
|
},
|
|
{
|
|
name: "node",
|
|
args: []string{"node", "--help"},
|
|
want: []string{"node register", "edge.yaml", "one-line bootstrap command", "config check"},
|
|
},
|
|
{
|
|
name: "node register",
|
|
args: []string{"node", "register", "--help"},
|
|
want: []string{"one-line bootstrap command", "node.yaml", "iop-node serve", "--adapter", "--target"},
|
|
},
|
|
{
|
|
name: "smoke",
|
|
args: []string{"smoke", "--help"},
|
|
want: []string{"smoke openai", "config init", "node register", "serve"},
|
|
},
|
|
}
|
|
for _, tc := range helpCases {
|
|
root := rootCmd()
|
|
out.Reset()
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs(tc.args)
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("%s help execute failed: %v", tc.name, err)
|
|
}
|
|
got := out.String()
|
|
for _, w := range tc.want {
|
|
if !strings.Contains(got, w) {
|
|
t.Errorf("%s help output missing %q\n---\n%s", tc.name, w, got)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNodesListCommand(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfgPath := filepath.Join(dir, "edge.yaml")
|
|
yamlStr := `server:
|
|
listen: "0.0.0.0:9090"
|
|
nodes:
|
|
- id: "node-x"
|
|
alias: "alias-x"
|
|
token: "token-x"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
- id: "node-y"
|
|
alias: "alias-y"
|
|
token: "token-y"
|
|
adapters:
|
|
cli:
|
|
enabled: true
|
|
`
|
|
if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
|
|
t.Fatalf("write temp edge.yaml: %v", err)
|
|
}
|
|
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"--config", cfgPath, "nodes", "list"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("nodes list execute failed: %v\n%s", err, out.String())
|
|
}
|
|
got := out.String()
|
|
|
|
if !strings.Contains(got, "node-x") || !strings.Contains(got, "node-y") {
|
|
t.Errorf("nodes list output missing configured nodes: %s", got)
|
|
}
|
|
if !strings.Contains(got, "offline (configured)") {
|
|
t.Errorf("nodes list output should explicitly mark nodes as offline, got: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestSmokeOpenAICommandSuccess(t *testing.T) {
|
|
// Start an httptest server to mock the OpenAI-compatible endpoint
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/healthz":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
case "/v1/models":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"object":"list","data":[{"id":"test-model","object":"model","created":123456,"owned_by":"iop"}]}`))
|
|
case "/v1/responses":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"id":"resp-123","object":"response","created_at":123456,"model":"test-model","output_text":"responses pong","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"responses pong"}]}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"smoke", "openai", "--model", "test-model", "--base-url", server.URL, "--prompt", "ping", "--timeout", "5s"})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("smoke openai command failed: %v\n%s", err, out.String())
|
|
}
|
|
|
|
got := out.String()
|
|
wants := []string{
|
|
"Step 1: Checking /healthz ... [OK]",
|
|
"Step 2: Checking /v1/models ... [OK]",
|
|
"Step 3: Checking /v1/responses ... [OK]",
|
|
"Responses Output Text: \"responses pong\"",
|
|
"IOP Edge OpenAI Smoke Test SUCCESS!",
|
|
}
|
|
for _, w := range wants {
|
|
if !strings.Contains(got, w) {
|
|
t.Errorf("smoke openai success output missing: %q\nFull output:\n%s", w, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSmokeOpenAICommandWorkspace(t *testing.T) {
|
|
// Create a temporary directory for workspace
|
|
tmpDir, err := os.MkdirTemp("", "iop-smoke-workspace-*")
|
|
if err != nil {
|
|
t.Fatalf("failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
// Create an expect file inside the workspace
|
|
expectFile := "test_marker.txt"
|
|
expectContent := "hello smoke test"
|
|
if err := os.WriteFile(filepath.Join(tmpDir, expectFile), []byte(expectContent), 0644); err != nil {
|
|
t.Fatalf("failed to write expect file: %v", err)
|
|
}
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/healthz":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
case "/v1/models":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"object":"list","data":[{"id":"test-model","object":"model","created":123456,"owned_by":"iop"}]}`))
|
|
case "/v1/responses":
|
|
// Assert request body
|
|
bodyBytes, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
t.Errorf("failed to read request body: %v", err)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
var reqPayload map[string]interface{}
|
|
if err := json.Unmarshal(bodyBytes, &reqPayload); err != nil {
|
|
t.Errorf("failed to unmarshal request body: %v", err)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Validate metadata.workspace
|
|
metadata, ok := reqPayload["metadata"].(map[string]interface{})
|
|
if !ok {
|
|
t.Errorf("metadata missing in request body")
|
|
} else {
|
|
gotWS := metadata["workspace"]
|
|
if gotWS != tmpDir {
|
|
t.Errorf("expected workspace %q, got %q", tmpDir, gotWS)
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"id":"resp-123","object":"response","created_at":123456,"model":"test-model","output_text":"responses pong","output":[{"type":"message","role":"assistant","content":[{"type":"output_text","text":"responses pong"}]}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{
|
|
"smoke", "openai",
|
|
"--model", "test-model",
|
|
"--base-url", server.URL,
|
|
"--prompt", "ping",
|
|
"--timeout", "5s",
|
|
"--workspace", tmpDir,
|
|
"--expect-file", expectFile,
|
|
"--expect-contains", "smoke test",
|
|
})
|
|
if err := root.Execute(); err != nil {
|
|
t.Fatalf("smoke openai command failed: %v\n%s", err, out.String())
|
|
}
|
|
|
|
got := out.String()
|
|
wants := []string{
|
|
"Step 1: Checking /healthz ... [OK]",
|
|
"Step 2: Checking /v1/models ... [OK]",
|
|
"Step 3: Checking /v1/responses ... [OK]",
|
|
"Responses Output Text: \"responses pong\"",
|
|
"IOP Edge OpenAI Smoke Test SUCCESS!",
|
|
}
|
|
for _, w := range wants {
|
|
if !strings.Contains(got, w) {
|
|
t.Errorf("smoke openai success output missing: %q\nFull output:\n%s", w, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSmokeOpenAICommandFailure(t *testing.T) {
|
|
// 1. Non-200 /healthz error
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/healthz" {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("internal server error"))
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer server.Close()
|
|
|
|
root := rootCmd()
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"smoke", "openai", "--model", "test-model", "--base-url", server.URL})
|
|
err := root.Execute()
|
|
if err == nil {
|
|
t.Fatalf("expected error from non-200 healthz, got nil. Output:\n%s", out.String())
|
|
}
|
|
if !strings.Contains(err.Error(), "returned non-200 status 500") {
|
|
t.Errorf("unexpected error message: %v", err)
|
|
}
|
|
|
|
// 2. Responses non-200 error
|
|
server4 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/healthz":
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
case "/v1/models":
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"object":"list","data":[{"id":"test-model","object":"model"}]}`))
|
|
case "/v1/responses":
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
w.Write([]byte("node offline"))
|
|
}
|
|
}))
|
|
defer server4.Close()
|
|
|
|
root = rootCmd()
|
|
out.Reset()
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"smoke", "openai", "--model", "test-model", "--base-url", server4.URL})
|
|
err4 := root.Execute()
|
|
if err4 == nil {
|
|
t.Fatalf("expected error from non-200 responses, got nil")
|
|
}
|
|
if !strings.Contains(err4.Error(), "returned non-200 status 503") {
|
|
t.Errorf("unexpected error message: %v", err4)
|
|
}
|
|
|
|
// 3. Responses empty text error
|
|
server5 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/healthz":
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"status":"ok"}`))
|
|
case "/v1/models":
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"object":"list","data":[{"id":"test-model","object":"model"}]}`))
|
|
case "/v1/responses":
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"id":"resp-123","object":"response","created_at":123456,"model":"test-model","output_text":"","output":[]}`))
|
|
}
|
|
}))
|
|
defer server5.Close()
|
|
|
|
root = rootCmd()
|
|
out.Reset()
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs([]string{"smoke", "openai", "--model", "test-model", "--base-url", server5.URL})
|
|
err5 := root.Execute()
|
|
if err5 == nil {
|
|
t.Fatalf("expected error from empty responses text, got nil")
|
|
}
|
|
if !strings.Contains(err5.Error(), "returned empty response text") {
|
|
t.Errorf("unexpected error message: %v", err5)
|
|
}
|
|
}
|