Files

370 lines
13 KiB
Go

package automation
import (
"os"
"path/filepath"
"testing"
)
func TestScriptStoreSaveListAndDelete(t *testing.T) {
store := NewScriptStore(filepath.Join(t.TempDir(), "data", "automation", "scripts"))
saved, err := store.Save(ScriptRecord{
ID: "buyer-script",
Name: "买家脚本",
Description: "用于接管页面并截图",
Type: "playwright-cdp",
Status: "ready",
EntryFile: "index.cjs",
Tags: []string{"Playwright", "CDP", "Playwright"},
SelectorText: `{"code":"BUYER_001"}`,
ParamsText: `{"url":"https://example.com"}`,
ScriptText: "module.exports.run = async () => ({ ok: true })\r\n",
Notes: "stable",
})
if err != nil {
t.Fatalf("Save returned error: %v", err)
}
if saved.ID != "buyer-script" {
t.Fatalf("expected saved id buyer-script, got %q", saved.ID)
}
if saved.PackageFormat != defaultScriptPackageFormat {
t.Fatalf("expected package format %q, got %q", defaultScriptPackageFormat, saved.PackageFormat)
}
if saved.ManifestVersion != defaultScriptManifestVersion {
t.Fatalf("expected manifest version %d, got %d", defaultScriptManifestVersion, saved.ManifestVersion)
}
if len(saved.Tags) != 2 {
t.Fatalf("expected deduped tags, got %#v", saved.Tags)
}
if saved.CreatedAt == "" || saved.UpdatedAt == "" {
t.Fatalf("expected timestamps to be populated, got %+v", saved)
}
if saved.ScriptText != "module.exports.run = async () => ({ ok: true })\n" {
t.Fatalf("expected normalized script line endings, got %q", saved.ScriptText)
}
configPath := filepath.Join(store.rootDir, "buyer-script", scriptStoreConfigFileName)
if _, err := os.Stat(configPath); err != nil {
t.Fatalf("expected config to exist: %v", err)
}
if _, err := os.Stat(filepath.Join(store.rootDir, "buyer-script", scriptStoreLegacyConfigName)); !os.IsNotExist(err) {
t.Fatalf("expected legacy manifest to be absent, got %v", err)
}
scriptPath := filepath.Join(store.rootDir, "buyer-script", "index.cjs")
if _, err := os.Stat(scriptPath); err != nil {
t.Fatalf("expected script file to exist: %v", err)
}
items, err := store.List()
if err != nil {
t.Fatalf("List returned error: %v", err)
}
if len(items) != 1 {
t.Fatalf("expected one item, got %d", len(items))
}
if items[0].ScriptText == "" {
t.Fatalf("expected script text to be loaded from file")
}
if items[0].PackageFormat != defaultScriptPackageFormat {
t.Fatalf("expected loaded package format %q, got %q", defaultScriptPackageFormat, items[0].PackageFormat)
}
if err := store.Delete("buyer-script"); err != nil {
t.Fatalf("Delete returned error: %v", err)
}
if _, err := os.Stat(filepath.Join(store.rootDir, "buyer-script")); !os.IsNotExist(err) {
t.Fatalf("expected script dir to be removed, got %v", err)
}
}
func TestScriptStoreSaveRenamedEntryRemovesOldFile(t *testing.T) {
store := NewScriptStore(filepath.Join(t.TempDir(), "data", "automation", "scripts"))
original, err := store.Save(ScriptRecord{
ID: "rename-script",
Name: "重命名入口",
EntryFile: "index.cjs",
ScriptText: "module.exports.run = async () => ({ ok: true })",
})
if err != nil {
t.Fatalf("initial Save returned error: %v", err)
}
updated, err := store.Save(ScriptRecord{
ID: original.ID,
Name: original.Name,
Type: original.Type,
Status: original.Status,
EntryFile: "runner.cjs",
ScriptText: "module.exports.run = async () => ({ ok: false })",
CreatedAt: original.CreatedAt,
})
if err != nil {
t.Fatalf("second Save returned error: %v", err)
}
if updated.EntryFile != "runner.cjs" {
t.Fatalf("expected entry file runner.cjs, got %q", updated.EntryFile)
}
if updated.PackageFormat != defaultScriptPackageFormat {
t.Fatalf("expected package format %q, got %q", defaultScriptPackageFormat, updated.PackageFormat)
}
if updated.CreatedAt != original.CreatedAt {
t.Fatalf("expected createdAt to be preserved, got %q want %q", updated.CreatedAt, original.CreatedAt)
}
if _, err := os.Stat(filepath.Join(store.rootDir, original.ID, "runner.cjs")); err != nil {
t.Fatalf("expected new entry file to exist: %v", err)
}
if _, err := os.Stat(filepath.Join(store.rootDir, original.ID, "index.cjs")); !os.IsNotExist(err) {
t.Fatalf("expected old entry file to be removed, got %v", err)
}
}
func TestScriptStoreSaveGeneratesIDForNewScript(t *testing.T) {
store := NewScriptStore(filepath.Join(t.TempDir(), "data", "automation", "scripts"))
saved, err := store.Save(ScriptRecord{
Name: "自动生成 ID",
ScriptText: "module.exports.run = async () => ({ ok: true })",
})
if err != nil {
t.Fatalf("Save returned error: %v", err)
}
if saved.ID == "" {
t.Fatalf("expected generated id to be populated")
}
if _, err := os.Stat(filepath.Join(store.rootDir, saved.ID)); err != nil {
t.Fatalf("expected generated script dir to exist: %v", err)
}
}
func TestScriptStorePersistsTargetConfig(t *testing.T) {
store := NewScriptStore(filepath.Join(t.TempDir(), "data", "automation", "scripts"))
saved, err := store.Save(ScriptRecord{
ID: "target-config-script",
Name: "实例策略脚本",
EntryFile: "index.cjs",
ScriptText: "module.exports.run = async () => ({ ok: true })",
TargetConfig: ScriptTargetConfig{
Mode: "rotate",
Selector: ScriptTargetSelector{
Tags: []string{"pool", "pool"},
Keywords: []string{"buyer"},
},
},
})
if err != nil {
t.Fatalf("Save returned error: %v", err)
}
if saved.TargetConfig.Mode != "rotate" {
t.Fatalf("expected target mode rotate, got %+v", saved.TargetConfig)
}
if len(saved.TargetConfig.Selector.Tags) != 1 {
t.Fatalf("expected normalized target tags, got %+v", saved.TargetConfig.Selector.Tags)
}
loaded, err := store.Get(saved.ID)
if err != nil {
t.Fatalf("Get returned error: %v", err)
}
if loaded.TargetConfig.Mode != "rotate" {
t.Fatalf("expected persisted target mode rotate, got %+v", loaded.TargetConfig)
}
if len(loaded.TargetConfig.Selector.Tags) != 1 || loaded.TargetConfig.Selector.Tags[0] != "pool" {
t.Fatalf("unexpected persisted target tags: %+v", loaded.TargetConfig.Selector.Tags)
}
}
func TestScriptStorePersistsPublicAPIConfig(t *testing.T) {
store := NewScriptStore(filepath.Join(t.TempDir(), "data", "automation", "scripts"))
saved, err := store.Save(ScriptRecord{
ID: "public-api-script",
Name: "对外接口脚本",
EntryFile: "index.cjs",
ScriptText: "module.exports.run = async () => ({ ok: true })",
PublicAPI: ScriptPublicAPIConfig{
Enabled: true,
Method: "post",
Path: "/API/Automation/Hooks/Mail/Proton First Message",
RequestMode: "params-only",
ResponseMode: "result-only",
TimeoutMs: 999,
RequestBodyText: "{\n \"recipientQuery\": \"target@example.com\"\n}",
ResponseBodyText: "{\n \"verificationCode\": \"429792\"\n}",
Variables: []ScriptPublicAPIVariable{
{Name: "recipientQuery", DefaultValue: "target@example.com", Description: "收件人", Required: true},
{Name: "recipientQuery", DefaultValue: "duplicate"},
{Name: "senderEmail", DefaultValue: "otp@tm1.openai.com, noreply@tm.openai.com"},
},
},
})
if err != nil {
t.Fatalf("Save returned error: %v", err)
}
if !saved.PublicAPI.Enabled {
t.Fatalf("expected public api to be enabled")
}
if saved.PublicAPI.Method != "POST" {
t.Fatalf("expected public api method POST, got %+v", saved.PublicAPI)
}
if saved.PublicAPI.Path != "mail/proton-first-message" {
t.Fatalf("expected normalized public api path, got %+v", saved.PublicAPI)
}
if saved.PublicAPI.TimeoutMs != 1000 {
t.Fatalf("expected normalized public api timeout 1000, got %+v", saved.PublicAPI)
}
if len(saved.PublicAPI.Variables) != 2 || saved.PublicAPI.Variables[0].Name != "recipientQuery" || !saved.PublicAPI.Variables[0].Required {
t.Fatalf("expected normalized public api variables, got %+v", saved.PublicAPI.Variables)
}
loaded, err := store.Get(saved.ID)
if err != nil {
t.Fatalf("Get returned error: %v", err)
}
if loaded.PublicAPI.Path != "mail/proton-first-message" {
t.Fatalf("unexpected persisted public api path: %+v", loaded.PublicAPI)
}
if loaded.PublicAPI.RequestMode != "params-only" || loaded.PublicAPI.ResponseMode != "result-only" {
t.Fatalf("unexpected persisted public api modes: %+v", loaded.PublicAPI)
}
if len(loaded.PublicAPI.Variables) != 2 || loaded.PublicAPI.Variables[1].Name != "senderEmail" {
t.Fatalf("unexpected persisted public api variables: %+v", loaded.PublicAPI.Variables)
}
}
func TestScriptStoreRejectsDuplicatePublicAPIPath(t *testing.T) {
store := NewScriptStore(filepath.Join(t.TempDir(), "data", "automation", "scripts"))
if _, err := store.Save(ScriptRecord{
ID: "hook-a",
Name: "Hook A",
EntryFile: "index.cjs",
ScriptText: "module.exports.run = async () => ({ ok: true })",
PublicAPI: ScriptPublicAPIConfig{
Enabled: true,
Path: "mail/proton-first-message",
},
}); err != nil {
t.Fatalf("initial Save returned error: %v", err)
}
if _, err := store.Save(ScriptRecord{
ID: "hook-b",
Name: "Hook B",
EntryFile: "index.cjs",
ScriptText: "module.exports.run = async () => ({ ok: true })",
PublicAPI: ScriptPublicAPIConfig{
Enabled: false,
Path: "/api/automation/hooks/mail/proton-first-message",
},
}); err == nil {
t.Fatalf("expected duplicate public api path to fail")
}
}
func TestScriptStoreReadsLegacyManifestAndMigratesOnSave(t *testing.T) {
store := NewScriptStore(filepath.Join(t.TempDir(), "data", "automation", "scripts"))
scriptDir := filepath.Join(store.rootDir, "legacy-script")
if err := os.MkdirAll(scriptDir, 0o755); err != nil {
t.Fatalf("create script dir failed: %v", err)
}
legacyConfig := `{
"packageFormat": "ant-automation-script",
"manifestVersion": 1,
"id": "legacy-script",
"name": "旧结构脚本",
"type": "playwright-cdp",
"status": "ready",
"entryFile": "index.cjs",
"createdAt": "2026-04-03T00:00:00Z",
"updatedAt": "2026-04-03T00:00:00Z"
}`
if err := os.WriteFile(filepath.Join(scriptDir, scriptStoreLegacyConfigName), []byte(legacyConfig), 0o644); err != nil {
t.Fatalf("write legacy manifest failed: %v", err)
}
if err := os.WriteFile(filepath.Join(scriptDir, "index.cjs"), []byte("module.exports.run = async () => ({ ok: true })"), 0o644); err != nil {
t.Fatalf("write script failed: %v", err)
}
loaded, err := store.Get("legacy-script")
if err != nil {
t.Fatalf("Get returned error: %v", err)
}
if loaded.Name != "旧结构脚本" {
t.Fatalf("unexpected loaded record: %+v", loaded)
}
loaded.ScriptText = "module.exports.run = async () => ({ ok: false })"
saved, err := store.Save(loaded)
if err != nil {
t.Fatalf("Save returned error: %v", err)
}
if saved.ID != "legacy-script" {
t.Fatalf("unexpected saved record: %+v", saved)
}
if _, err := os.Stat(filepath.Join(scriptDir, scriptStoreConfigFileName)); err != nil {
t.Fatalf("expected new config to exist: %v", err)
}
if _, err := os.Stat(filepath.Join(scriptDir, scriptStoreLegacyConfigName)); !os.IsNotExist(err) {
t.Fatalf("expected legacy manifest to be removed, got %v", err)
}
}
func TestScriptStoreKeepsManifestAsRegularFile(t *testing.T) {
store := NewScriptStore(filepath.Join(t.TempDir(), "data", "automation", "scripts"))
record, err := store.ImportBundle(ImportedBundle{
Record: ScriptRecord{
ID: "regular-manifest",
Name: "普通 manifest 文件",
Type: "playwright-cdp",
Status: "ready",
EntryFile: "index.cjs",
ScriptText: "module.exports.run = async () => ({ ok: true })",
},
Files: []ImportedBundleFile{
{
Path: "index.cjs",
Content: []byte("module.exports.run = async () => ({ ok: true })"),
},
{
Path: "manifest.json",
Content: []byte(`{"keep":true}`),
},
},
})
if err != nil {
t.Fatalf("ImportBundle returned error: %v", err)
}
if _, err := os.Stat(filepath.Join(store.rootDir, record.ID, scriptStoreConfigFileName)); err != nil {
t.Fatalf("expected system config to exist: %v", err)
}
if _, err := os.Stat(filepath.Join(store.rootDir, record.ID, "manifest.json")); err != nil {
t.Fatalf("expected manifest.json to be kept as a regular file: %v", err)
}
record.ScriptText = "module.exports.run = async () => ({ ok: false })"
if _, err := store.Save(record); err != nil {
t.Fatalf("Save returned error: %v", err)
}
if _, err := os.Stat(filepath.Join(store.rootDir, record.ID, "manifest.json")); err != nil {
t.Fatalf("expected manifest.json to survive Save: %v", err)
}
exported, err := store.ExportBundle(record.ID)
if err != nil {
t.Fatalf("ExportBundle returned error: %v", err)
}
if !hasBundleFile(exported.Files, "manifest.json", []byte(`{"keep":true}`)) {
t.Fatalf("expected manifest.json in exported files, got %+v", exported.Files)
}
}