mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
370 lines
13 KiB
Go
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)
|
|
}
|
|
}
|