Add Lua configuration support (#2845)

* lua opt trial

* go mod tidy
This commit is contained in:
Jo authored and GitHub committed 2026-06-09 00:31:26 +02:00
1 parent 4c5fda7944
commit ecae3e601e
13 files changed
+525 -53

No files matched your search

+4 -1
View File
@@ -31,4 +31,7 @@ qemu-*
node_modules/
xgotext
.devcontainer/
.devcontainer/
init.lua
dyalpm/
!doc/init.lua
+53
View File
@@ -0,0 +1,53 @@
-- Example yay init.lua
--
-- This file is a complete template for yay.opt. Copy entries you need,
-- or keep all of them and tune values. Command-line flags still override
-- these values.
-- Strings
yay.opt.aururl = "https://aur.archlinux.org" -- Base AUR URL.
yay.opt.aurrpcurl = "https://aur.archlinux.org/rpc?" -- AUR RPC endpoint URL.
yay.opt.build_dir = os.getenv("HOME") .. "/.cache/yay" -- Build/cache directory for AUR packages.
yay.opt.editor = os.getenv("EDITOR") or os.getenv("VISUAL") or "vi" -- Editor command used for PKGBUILD edits; empty uses VISUAL/EDITOR.
yay.opt.editor_flags = "" -- Extra flags passed to the editor command.
yay.opt.makepkg_bin = "makepkg" -- makepkg executable (name in PATH or absolute path).
yay.opt.makepkg_conf = "" -- makepkg.conf path; empty uses default makepkg config.
yay.opt.pacman_bin = "pacman" -- pacman executable.
yay.opt.pacman_conf = "/etc/pacman.conf" -- pacman.conf file path.
yay.opt.redownload = "no" -- PKGBUILD download mode: "no" | "yes" | "all".
yay.opt.git_bin = "git" -- git executable.
yay.opt.gpg_bin = "gpg" -- gpg executable.
yay.opt.gpg_flags = "" -- Extra flags passed to gpg.
yay.opt.mflags = "" -- Extra flags passed to makepkg.
yay.opt.sort_by = "" -- AUR search sort field: "votes" | "popularity" | "name" | "base" | "submitted" | "modified" | "".
yay.opt.search_by = "name-desc" -- AUR search field: "name" | "name-desc" | "maintainer" | "depends" | "checkdepends" | "makedepends" | "optdepends" | "provides" | "conflicts" | "replaces" | "groups" | "keywords" | "comaintainers".
yay.opt.git_flags = "" -- Extra flags passed to git.
yay.opt.remove_make = "ask" -- Remove makedepends mode: "no" | "yes" | "ask" | "askyes".
yay.opt.sudo_bin = "sudo" -- Privilege elevation command.
yay.opt.sudo_flags = "" -- Extra flags passed to the sudo command.
yay.opt.rebuild = "no" -- Build mode: "no" | "yes" | "tree" | "all".
-- Integers
yay.opt.request_split_n = 150 -- Max packages per AUR RPC request (use values > 0).
yay.opt.completion_refresh_time = 7 -- Completion cache refresh days: -1 (never), 0 (always), >0 (every N days).
yay.opt.max_concurrent_downloads = 1 -- Parallel PKGBUILD source downloads; 0 uses CPU count.
-- Booleans
yay.opt.bottom_up = true -- Show AUR packages before repo packages in mixed results.
yay.opt.sudo_loop = false -- Keep sudo session alive in the background during long builds.
yay.opt.devel = false -- Check development/VCS packages on sysupgrade.
yay.opt.clean_after = false -- Remove untracked files after install.
yay.opt.keep_src = false -- Keep pkg/ and src/ after successful builds.
yay.opt.provides = true -- Resolve matching providers when dependencies are ambiguous.
yay.opt.pgp_fetch = true -- Prompt to import unknown PGP keys from validpgpkeys.
yay.opt.clean_menu = true -- Show pre-build clean menu.
yay.opt.diff_menu = true -- Show diff menu before building.
yay.opt.edit_menu = false -- Show PKGBUILD edit menu before building.
yay.opt.combined_upgrade = true -- Use combined repo+AUR upgrade flow on sysupgrade.
yay.opt.use_ask = false -- Use pacman's --ask to auto-confirm known conflicts.
yay.opt.batch_install = false -- Queue AUR package installs instead of installing each package immediately.
yay.opt.single_line_results = false -- Use single-line search result format.
yay.opt.separate_sources = true -- Separate query results by source (repo vs AUR).
yay.opt.debug = false -- Enable debug logging and local init.lua lookup convenience.
yay.opt.rpc = true -- Use AUR RPC for dependency/query operations.
yay.opt.double_confirm = true -- Ask for confirmation before and after builds during upgrades.
+49
View File
@@ -0,0 +1,49 @@
# Lua configuration (`init.lua`)
yay can optionally load a Lua configuration file, `init.lua`. `init.lua` overlays whatever is in `config.json`, and any command-line flag you pass still wins over `init.lua`.
## Location
`init.lua` is looked up, in order:
1. `$XDG_CONFIG_HOME/yay/init.lua`
2. `$HOME/.config/yay/init.lua`
## Setting options with `yay.opt`
Assign to keys on the `yay.opt` table using the exact option names shown
below.
```lua
yay.opt.bottom_up = false
yay.opt.clean_after = true
yay.opt.sort_by = "votes"
yay.opt.request_split_n = 150
yay.opt.editor = os.getenv("EDITOR") or "vi"
```
Unknown keys and values of the wrong type are treated as errors. yay aborts
startup and reports the offending keys/values so misconfigurations fail fast.
### Available options
**Strings**
`aururl`, `aurrpcurl`, `build_dir`, `editor`, `editor_flags`, `makepkg_bin`,
`makepkg_conf`, `pacman_bin`, `pacman_conf`, `redownload`, `rebuild`, `git_bin`,
`gpg_bin`, `gpg_flags`, `mflags`, `sort_by`, `search_by`, `git_flags`,
`remove_make`, `sudo_bin`, `sudo_flags`
**Integers**
`request_split_n`, `completion_refresh_time`, `max_concurrent_downloads`
**Booleans**
`bottom_up`, `sudo_loop`, `devel`, `clean_after`, `keep_src`, `provides`,
`pgp_fetch`, `clean_menu`, `diff_menu`, `edit_menu`, `combined_upgrade`,
`use_ask`, `batch_install`, `single_line_results`, `separate_sources`, `debug`,
`rpc`, `double_confirm`
A ready-to-copy example
lives at [`doc/init.lua`](init.lua).
+1
View File
@@ -12,6 +12,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1
github.com/leonelquinteros/gotext v1.7.2
github.com/stretchr/testify v1.11.1
github.com/yuin/gopher-lua v1.1.2
golang.org/x/net v0.55.0
golang.org/x/sys v0.45.0
golang.org/x/term v0.43.0
+2
View File
@@ -57,6 +57,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8=
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
+10
View File
@@ -13,6 +13,7 @@ import (
"github.com/Jguer/yay/v12/pkg/db/ialpm"
"github.com/Jguer/yay/v12/pkg/runtime"
"github.com/Jguer/yay/v12/pkg/settings"
"github.com/Jguer/yay/v12/pkg/settings/lua"
"github.com/Jguer/yay/v12/pkg/settings/parser"
"github.com/Jguer/yay/v12/pkg/text"
)
@@ -85,6 +86,15 @@ func main() {
fallbackLog.Errorln(errS)
}
if luaPath := settings.GetLuaConfigPath(cfg.Debug); luaPath != "" {
if errLua := lua.LoadInto(fallbackLog, luaPath, cfg); errLua != nil {
fallbackLog.Errorln(errLua)
ret = 1
return
}
}
cmdArgs := parser.MakeArguments()
// Parse command line
+51 -51
View File
@@ -23,59 +23,59 @@ var NoConfirm = false
// Configuration stores yay's config.
type Configuration struct {
AURURL string `json:"aururl"`
AURRPCURL string `json:"aurrpcurl"`
BuildDir string `json:"buildDir"`
Editor string `json:"editor"`
EditorFlags string `json:"editorflags"`
MakepkgBin string `json:"makepkgbin"`
MakepkgConf string `json:"makepkgconf"`
PacmanBin string `json:"pacmanbin"`
PacmanConf string `json:"pacmanconf"`
ReDownload string `json:"redownload"`
AnswerClean string `json:"answerclean"`
AnswerDiff string `json:"answerdiff"`
AnswerEdit string `json:"answeredit"`
AnswerUpgrade string `json:"answerupgrade"`
GitBin string `json:"gitbin"`
GpgBin string `json:"gpgbin"`
GpgFlags string `json:"gpgflags"`
MFlags string `json:"mflags"`
SortBy string `json:"sortby"`
SearchBy string `json:"searchby"`
GitFlags string `json:"gitflags"`
RemoveMake string `json:"removemake"`
SudoBin string `json:"sudobin"`
SudoFlags string `json:"sudoflags"`
Version string `json:"version"`
RequestSplitN int `json:"requestsplitn"`
CompletionInterval int `json:"completionrefreshtime"`
MaxConcurrentDownloads int `json:"maxconcurrentdownloads"`
BottomUp bool `json:"bottomup"`
SudoLoop bool `json:"sudoloop"`
Devel bool `json:"devel"`
CleanAfter bool `json:"cleanAfter"`
KeepSrc bool `json:"keepSrc"`
Provides bool `json:"provides"`
PGPFetch bool `json:"pgpfetch"`
CleanMenu bool `json:"cleanmenu"`
DiffMenu bool `json:"diffmenu"`
EditMenu bool `json:"editmenu"`
CombinedUpgrade bool `json:"combinedupgrade"`
UseAsk bool `json:"useask"`
BatchInstall bool `json:"batchinstall"`
SingleLineResults bool `json:"singlelineresults"`
SeparateSources bool `json:"separatesources"`
Debug bool `json:"debug"`
UseRPC bool `json:"rpc"`
DoubleConfirm bool `json:"doubleconfirm"` // confirm install before and after build
AURURL string `json:"aururl" lua:"aururl"`
AURRPCURL string `json:"aurrpcurl" lua:"aurrpcurl"`
BuildDir string `json:"buildDir" lua:"build_dir"`
Editor string `json:"editor" lua:"editor"`
EditorFlags string `json:"editorflags" lua:"editor_flags"`
MakepkgBin string `json:"makepkgbin" lua:"makepkg_bin"`
MakepkgConf string `json:"makepkgconf" lua:"makepkg_conf"`
PacmanBin string `json:"pacmanbin" lua:"pacman_bin"`
PacmanConf string `json:"pacmanconf" lua:"pacman_conf"`
ReDownload string `json:"redownload" lua:"redownload"`
AnswerClean string `json:"answerclean" lua:"-"`
AnswerDiff string `json:"answerdiff" lua:"-"`
AnswerEdit string `json:"answeredit" lua:"-"`
AnswerUpgrade string `json:"answerupgrade" lua:"-"`
GitBin string `json:"gitbin" lua:"git_bin"`
GpgBin string `json:"gpgbin" lua:"gpg_bin"`
GpgFlags string `json:"gpgflags" lua:"gpg_flags"`
MFlags string `json:"mflags" lua:"mflags"`
SortBy string `json:"sortby" lua:"sort_by"`
SearchBy string `json:"searchby" lua:"search_by"`
GitFlags string `json:"gitflags" lua:"git_flags"`
RemoveMake string `json:"removemake" lua:"remove_make"`
SudoBin string `json:"sudobin" lua:"sudo_bin"`
SudoFlags string `json:"sudoflags" lua:"sudo_flags"`
Version string `json:"version" lua:"-"`
RequestSplitN int `json:"requestsplitn" lua:"request_split_n"`
CompletionInterval int `json:"completionrefreshtime" lua:"completion_refresh_time"`
MaxConcurrentDownloads int `json:"maxconcurrentdownloads" lua:"max_concurrent_downloads"`
BottomUp bool `json:"bottomup" lua:"bottom_up"`
SudoLoop bool `json:"sudoloop" lua:"sudo_loop"`
Devel bool `json:"devel" lua:"devel"`
CleanAfter bool `json:"cleanAfter" lua:"clean_after"`
KeepSrc bool `json:"keepSrc" lua:"keep_src"`
Provides bool `json:"provides" lua:"provides"`
PGPFetch bool `json:"pgpfetch" lua:"pgp_fetch"`
CleanMenu bool `json:"cleanmenu" lua:"clean_menu"`
DiffMenu bool `json:"diffmenu" lua:"diff_menu"`
EditMenu bool `json:"editmenu" lua:"edit_menu"`
CombinedUpgrade bool `json:"combinedupgrade" lua:"combined_upgrade"`
UseAsk bool `json:"useask" lua:"use_ask"`
BatchInstall bool `json:"batchinstall" lua:"batch_install"`
SingleLineResults bool `json:"singlelineresults" lua:"single_line_results"`
SeparateSources bool `json:"separatesources" lua:"separate_sources"`
Debug bool `json:"debug" lua:"debug"`
UseRPC bool `json:"rpc" lua:"rpc"`
DoubleConfirm bool `json:"doubleconfirm" lua:"double_confirm"` // confirm install before and after build
CompletionPath string `json:"-"`
VCSFilePath string `json:"-"`
CompletionPath string `json:"-" lua:"-"`
VCSFilePath string `json:"-" lua:"-"`
// ConfigPath string `json:"-"`
SaveConfig bool `json:"-"`
Mode parser.TargetMode `json:"-"`
ReBuild parser.RebuildMode `json:"rebuild"`
SaveConfig bool `json:"-" lua:"-"`
Mode parser.TargetMode `json:"-" lua:"-"`
ReBuild parser.RebuildMode `json:"rebuild" lua:"rebuild"`
}
// SaveConfig writes yay config to file.
+27 -1
View File
@@ -7,7 +7,8 @@ import (
const (
configFileName string = "config.json" // configFileName holds the name of the config file.
vcsFileName string = "vcs.json" // vcsFileName holds the name of the vcs file.
luaConfigFileName string = "init.lua"
vcsFileName string = "vcs.json" // vcsFileName holds the name of the vcs file.
completionFileName string = "completion.cache"
systemdCache string = "/var/cache/yay" // systemd should handle cache creation
)
@@ -30,6 +31,31 @@ func GetConfigPath() string {
return ""
}
// GetLuaConfigPath returns the first configured init.lua path that exists.
func GetLuaConfigPath(debug bool) string {
var candidates []string
if debug {
candidates = append(candidates, luaConfigFileName)
}
if configHome := os.Getenv("XDG_CONFIG_HOME"); configHome != "" {
candidates = append(candidates, filepath.Join(configHome, "yay", luaConfigFileName))
}
if configHome := os.Getenv("HOME"); configHome != "" {
candidates = append(candidates, filepath.Join(configHome, ".config", "yay", luaConfigFileName))
}
for _, candidate := range candidates {
if _, err := os.Stat(candidate); err == nil {
return candidate
}
}
return ""
}
func getCacheHome() (string, error) {
uid := os.Geteuid()
+26
View File
@@ -29,3 +29,29 @@ func Test_getCacheHome(t *testing.T) {
require.NoError(t, os.Unsetenv("TMPDIR"))
require.NoError(t, os.Unsetenv("SUDO_USER"))
}
func TestGetLuaConfigPath(t *testing.T) {
configHome := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", configHome)
require.NoError(t, os.Unsetenv("HOME"))
assert.Empty(t, GetLuaConfigPath(false))
luaDir := filepath.Join(configHome, "yay")
require.NoError(t, os.MkdirAll(luaDir, 0o755))
luaPath := filepath.Join(luaDir, "init.lua")
require.NoError(t, os.WriteFile(luaPath, []byte("-- test"), 0o600))
assert.Equal(t, luaPath, GetLuaConfigPath(false))
}
func TestGetLuaConfigPathDebugCwd(t *testing.T) {
require.NoError(t, os.Unsetenv("XDG_CONFIG_HOME"))
require.NoError(t, os.Unsetenv("HOME"))
t.Chdir(t.TempDir())
require.NoError(t, os.WriteFile("init.lua", []byte("-- test"), 0o600))
assert.Equal(t, "init.lua", GetLuaConfigPath(true))
assert.Empty(t, GetLuaConfigPath(false))
}
+36
View File
@@ -0,0 +1,36 @@
package lua
import (
"fmt"
"github.com/Jguer/yay/v12/pkg/multierror"
"github.com/Jguer/yay/v12/pkg/text"
)
// LoadInto applies the yay.opt values from path onto cfg.
func LoadInto(_ *text.Logger, path string, cfg any) error {
engine := New()
defer engine.Close()
if err := engine.L.DoFile(path); err != nil {
return err
}
unknown, errs := engine.Apply(cfg)
if len(unknown) == 0 && len(errs) == 0 {
return nil
}
merr := &multierror.MultiError{}
for _, key := range unknown {
merr.Add(fmt.Errorf("init.lua: unknown yay.opt key: %s", key))
}
for _, err := range errs {
merr.Add(fmt.Errorf("init.lua: %w", err))
}
return merr.Return()
}
+56
View File
@@ -0,0 +1,56 @@
package lua
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
func writeLuaFile(t *testing.T, body string) string {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, "init.lua")
require.NoError(t, os.WriteFile(path, []byte(body), 0o600))
return path
}
func TestLoadIntoStrictFailsOnUnknownKey(t *testing.T) {
path := writeLuaFile(t, `
yay.opt.unknown_key = true
`)
cfg := &testConfig{}
err := LoadInto(nil, path, cfg)
require.Error(t, err)
require.Contains(t, err.Error(), "unknown yay.opt key: unknown_key")
}
func TestLoadIntoStrictFailsOnTypeMismatch(t *testing.T) {
path := writeLuaFile(t, `
yay.opt.devel = "true"
`)
cfg := &testConfig{}
err := LoadInto(nil, path, cfg)
require.Error(t, err)
require.Contains(t, err.Error(), "yay.opt.devel: expected boolean")
}
func TestLoadIntoAppliesValidValues(t *testing.T) {
path := writeLuaFile(t, `
yay.opt.build_dir = "/tmp/yay"
yay.opt.request_split_n = 123
yay.opt.devel = true
`)
cfg := &testConfig{}
err := LoadInto(nil, path, cfg)
require.NoError(t, err)
require.Equal(t, "/tmp/yay", cfg.BuildDir)
require.Equal(t, 123, cfg.RequestSplitN)
require.True(t, cfg.Devel)
}
+127
View File
@@ -0,0 +1,127 @@
// Package lua loads yay's optional init.lua configuration.
package lua
import (
"fmt"
"reflect"
lua "github.com/yuin/gopher-lua"
)
const (
globalName = "yay"
optTableName = "opt"
)
type Engine struct {
L *lua.LState
}
func New() *Engine {
state := lua.NewState()
yayTbl := state.NewTable()
state.SetGlobal(globalName, yayTbl)
state.SetField(yayTbl, optTableName, state.NewTable())
return &Engine{L: state}
}
func (e *Engine) Close() {
e.L.Close()
}
// Apply writes recognized yay.opt values into cfg.
func (e *Engine) Apply(cfg any) (unknown []string, errs []error) {
v := reflect.ValueOf(cfg)
if v.Kind() != reflect.Pointer || v.Elem().Kind() != reflect.Struct {
return nil, []error{fmt.Errorf("lua: Apply expected pointer to struct, got %T", cfg)}
}
sv := v.Elem()
st := sv.Type()
index := make(map[string]int, st.NumField())
for i := range st.NumField() {
field := st.Field(i)
if name := luaKeyForField(&field); name != "" {
index[name] = i
}
}
optTbl, ok := e.optTable()
if !ok {
return nil, nil
}
optTbl.ForEach(func(k, val lua.LValue) {
key, ok := k.(lua.LString)
if !ok {
return
}
fieldIdx, found := index[string(key)]
if !found {
unknown = append(unknown, string(key))
return
}
if err := assign(sv.Field(fieldIdx), val); err != nil {
errs = append(errs, fmt.Errorf("yay.opt.%s: %w", string(key), err))
}
})
return unknown, errs
}
func (e *Engine) optTable() (*lua.LTable, bool) {
yayTbl, ok := e.L.GetGlobal(globalName).(*lua.LTable)
if !ok {
return nil, false
}
optTbl, ok := e.L.GetField(yayTbl, optTableName).(*lua.LTable)
return optTbl, ok
}
func luaKeyForField(field *reflect.StructField) string {
name := field.Tag.Get("lua")
if name != "" && name != "-" {
return name
}
return ""
}
func assign(field reflect.Value, val lua.LValue) error {
switch field.Kind() {
case reflect.String:
s, ok := val.(lua.LString)
if !ok {
return fmt.Errorf("expected string, got %s", val.Type())
}
field.SetString(string(s))
case reflect.Bool:
b, ok := val.(lua.LBool)
if !ok {
return fmt.Errorf("expected boolean, got %s", val.Type())
}
field.SetBool(bool(b))
case reflect.Int, reflect.Int64:
n, ok := val.(lua.LNumber)
if !ok {
return fmt.Errorf("expected number, got %s", val.Type())
}
field.SetInt(int64(n))
default:
return fmt.Errorf("unsupported field kind %s", field.Kind())
}
return nil
}
+83
View File
@@ -0,0 +1,83 @@
package lua
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type testConfig struct {
BuildDir string `json:"buildDir" lua:"build_dir"`
RequestSplitN int `json:"requestsplitn" lua:"request_split_n"`
Devel bool `json:"devel" lua:"devel"`
AnswerClean string `json:"answerclean"`
Ignored string `json:"-" lua:"-"`
}
func TestApply(t *testing.T) {
e := New()
defer e.Close()
require.NoError(t, e.L.DoString(`
yay.opt.build_dir = "/tmp/yay"
yay.opt.request_split_n = 200
yay.opt.devel = true
`))
cfg := &testConfig{}
unknown, errs := e.Apply(cfg)
assert.Empty(t, unknown)
assert.Empty(t, errs)
assert.Equal(t, "/tmp/yay", cfg.BuildDir)
assert.Equal(t, 200, cfg.RequestSplitN)
assert.True(t, cfg.Devel)
}
func TestApplyUnknownAndTypeMismatch(t *testing.T) {
e := New()
defer e.Close()
require.NoError(t, e.L.DoString(`
yay.opt.does_not_exist = "x"
yay.opt.buildDir = "/tmp/nope"
yay.opt.requestsplitn = 200
yay.opt.Devel = true
yay.opt.devel = "not a bool"
yay.opt.build_dir = "/tmp/ok"
`))
cfg := &testConfig{}
unknown, errs := e.Apply(cfg)
assert.ElementsMatch(t, []string{"does_not_exist", "buildDir", "requestsplitn", "Devel"}, unknown)
assert.Len(t, errs, 1)
assert.Zero(t, cfg.RequestSplitN)
assert.False(t, cfg.Devel)
assert.Equal(t, "/tmp/ok", cfg.BuildDir)
}
func TestApplyIgnoresAnswerOptionsWithoutLuaTags(t *testing.T) {
e := New()
defer e.Close()
require.NoError(t, e.L.DoString(`
yay.opt.answer_clean = "All"
`))
cfg := &testConfig{}
unknown, errs := e.Apply(cfg)
assert.Equal(t, []string{"answer_clean"}, unknown)
assert.Empty(t, errs)
assert.Empty(t, cfg.AnswerClean)
}
func TestApplyRejectsNonPointer(t *testing.T) {
e := New()
defer e.Close()
_, errs := e.Apply(testConfig{})
assert.Len(t, errs, 1)
}