refactor(pkg): adapt agent, runtime, migrate, fantasy for SDK/CLI structure

Supporting changes for the extracted CLI and dragonscale SDK:
- pkg/agent: agent_run, loop, toolloop adjustments for service wiring
- pkg/runtime: bootstrap, config updates for new invocation paths
- pkg/migrate: migrate logic updates
- pkg/fantasy: adapter changes for tool wiring
- pkg/channels: manager tweak
- pkg/heartbeat: service adjustment
- pkg/messages: types update
- pkg/sync: identity_test update

Remove t.Parallel from TestLoadResolvedConfig_FallsBackWhenExplicitBaseConfigInvalid
(t.Setenv incompatible with t.Parallel).
This commit is contained in:
ZanzyTHEbar 2026-02-22 15:31:40 +00:00
parent 3286555321
commit a76db05a09
15 changed files with 283 additions and 130 deletions

View file

@ -13,7 +13,7 @@ import (
"github.com/ZanzyTHEbar/dragonscale/pkg" "github.com/ZanzyTHEbar/dragonscale/pkg"
"github.com/ZanzyTHEbar/dragonscale/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/ZanzyTHEbar/dragonscale/pkg/constants" "github.com/ZanzyTHEbar/dragonscale/pkg/constants"
picofantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy" dragonfantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy"
"github.com/ZanzyTHEbar/dragonscale/pkg/ids" "github.com/ZanzyTHEbar/dragonscale/pkg/ids"
"github.com/ZanzyTHEbar/dragonscale/pkg/logger" "github.com/ZanzyTHEbar/dragonscale/pkg/logger"
"github.com/ZanzyTHEbar/dragonscale/pkg/memory" "github.com/ZanzyTHEbar/dragonscale/pkg/memory"
@ -93,7 +93,7 @@ func (al *AgentLoop) assembleContext(ctx context.Context, opts processOptions) (
"history": formatMessagesForLog(historyMsgs), "history": formatMessagesForLog(historyMsgs),
}) })
fantasyHistory := picofantasy.MessagesToFantasy(historyMsgs) fantasyHistory := dragonfantasy.MessagesToFantasy(historyMsgs)
adaptedTools, prepareStep := al.prepareToolset(ctx, opts) adaptedTools, prepareStep := al.prepareToolset(ctx, opts)
agent, err := al.createFantasyAgent(ctx, opts, systemPrompt, adaptedTools, prepareStep) agent, err := al.createFantasyAgent(ctx, opts, systemPrompt, adaptedTools, prepareStep)
if err != nil { if err != nil {
@ -177,13 +177,13 @@ func (al *AgentLoop) splitMessages(opts processOptions, builtMsgs []messages.Mes
} }
func (al *AgentLoop) prepareToolset(ctx context.Context, opts processOptions) ([]fantasy.AgentTool, func(context.Context, fantasy.PrepareStepFunctionOptions) (context.Context, fantasy.PrepareStepResult, error)) { func (al *AgentLoop) prepareToolset(ctx context.Context, opts processOptions) ([]fantasy.AgentTool, func(context.Context, fantasy.PrepareStepFunctionOptions) (context.Context, fantasy.PrepareStepResult, error)) {
adaptCfg := picofantasy.AdaptedToolsConfig{ adaptCfg := dragonfantasy.AdaptedToolsConfig{
MemStore: al.memoryStore, MemStore: al.memoryStore,
AgentID: pkg.NAME, AgentID: pkg.NAME,
SessionKey: opts.SessionKey, SessionKey: opts.SessionKey,
} }
adaptedTools := picofantasy.BuildAdaptedTools(al.tools, al.bus, opts.Channel, opts.ChatID, adaptCfg) adaptedTools := dragonfantasy.BuildAdaptedTools(al.tools, al.bus, opts.Channel, opts.ChatID, adaptCfg)
if al.toolResultSearch != nil { if al.toolResultSearch != nil {
adaptedTools = append(adaptedTools, al.toolResultSearch) adaptedTools = append(adaptedTools, al.toolResultSearch)
} }
@ -217,7 +217,7 @@ func (al *AgentLoop) prepareToolset(ctx context.Context, opts processOptions) ([
return ctx, fantasy.PrepareStepResult{}, nil return ctx, fantasy.PrepareStepResult{}, nil
} }
newAdapted := picofantasy.AdaptTools(newTools, msgBus, channel, chatID, adaptCfg) newAdapted := dragonfantasy.AdaptTools(newTools, msgBus, channel, chatID, adaptCfg)
expanded := append(adaptedTools, newAdapted...) expanded := append(adaptedTools, newAdapted...)
adaptedTools = expanded adaptedTools = expanded
@ -419,7 +419,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, opts processOptions) (str
} }
for _, step := range result.Steps { for _, step := range result.Steps {
stepMsgs := picofantasy.StepToMessages(step) stepMsgs := dragonfantasy.StepToMessages(step)
for _, m := range stepMsgs { for _, m := range stepMsgs {
al.sessions.AddFullMessage(opts.SessionKey, m) al.sessions.AddFullMessage(opts.SessionKey, m)
} }
@ -458,7 +458,7 @@ func (al *AgentLoop) runStreaming(ctx context.Context, opts processOptions, ac a
}, },
OnStepFinish: func(step fantasy.StepResult) error { OnStepFinish: func(step fantasy.StepResult) error {
stepMsgs := picofantasy.StepToMessages(step) stepMsgs := dragonfantasy.StepToMessages(step)
for _, m := range stepMsgs { for _, m := range stepMsgs {
al.sessions.AddFullMessage(opts.SessionKey, m) al.sessions.AddFullMessage(opts.SessionKey, m)
} }

View file

@ -1,5 +1,5 @@
// DragonScale - Ultra-lightweight personal AI agent // DragonScale - Ultra-lightweight personal AI agent
// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot // Inspired by and based on picoclaw: https://github.com/sipeed/picoclaw
// License: MIT // License: MIT
// //
// Copyright (c) 2026 DragonScale contributors // Copyright (c) 2026 DragonScale contributors
@ -32,7 +32,7 @@ import (
"github.com/ZanzyTHEbar/dragonscale/pkg/security/securebus" "github.com/ZanzyTHEbar/dragonscale/pkg/security/securebus"
"github.com/ZanzyTHEbar/dragonscale/pkg/session" "github.com/ZanzyTHEbar/dragonscale/pkg/session"
"github.com/ZanzyTHEbar/dragonscale/pkg/state" "github.com/ZanzyTHEbar/dragonscale/pkg/state"
picosync "github.com/ZanzyTHEbar/dragonscale/pkg/sync" dragonsync "github.com/ZanzyTHEbar/dragonscale/pkg/sync"
"github.com/ZanzyTHEbar/dragonscale/pkg/tools" "github.com/ZanzyTHEbar/dragonscale/pkg/tools"
) )
@ -56,7 +56,7 @@ type AgentLoop struct {
stateStore *StateStore // Agent run state persistence stateStore *StateStore // Agent run state persistence
conversationIDs sync.Map // Owner: agent_run.go — wrote by prepareRuntimeState, read in prepareRuntimeState/load path conversationIDs sync.Map // Owner: agent_run.go — wrote by prepareRuntimeState, read in prepareRuntimeState/load path
conversationMu sync.Mutex // serializes conversation creation path conversationMu sync.Mutex // serializes conversation creation path
identitySync *picosync.IdentitySync // File→DB sync for identity docs (nil if memory disabled) identitySync *dragonsync.IdentitySync // File→DB sync for identity docs (nil if memory disabled)
activeSessionKey atomic.Value // Owner: agent_run.go — written in runAgentLoop, read by router/toolloop for context routing activeSessionKey atomic.Value // Owner: agent_run.go — written in runAgentLoop, read by router/toolloop for context routing
running atomic.Bool // Owner: loop.go — lifecycle gate controlled by Run/Stop only running atomic.Bool // Owner: loop.go — lifecycle gate controlled by Run/Stop only
summarizing sync.Map // Owner: summarizer.go — intended for async summarization lockout, currently gated by TODO path summarizing sync.Map // Owner: summarizer.go — intended for async summarization lockout, currently gated by TODO path
@ -217,13 +217,13 @@ func NewAgentLoop(ctx context.Context, cfg *config.Config, msgBus *bus.MessageBu
} }
// Identity file sync (disk → DB) // Identity file sync (disk → DB)
var idSync *picosync.IdentitySync var idSync *dragonsync.IdentitySync
identityDir, idErr := config.IdentityDir() identityDir, idErr := config.IdentityDir()
if idErr != nil { if idErr != nil {
logger.WarnCF("agent", "Could not resolve identity dir, identity sync disabled", logger.WarnCF("agent", "Could not resolve identity dir, identity sync disabled",
map[string]interface{}{"error": idErr.Error()}) map[string]interface{}{"error": idErr.Error()})
} else { } else {
idSync = picosync.New(identityDir, pkg.NAME, memDelegate) idSync = dragonsync.New(identityDir, pkg.NAME, memDelegate)
if syncErr := idSync.SyncAll(ctx); syncErr != nil { if syncErr := idSync.SyncAll(ctx); syncErr != nil {
logger.WarnCF("agent", "Initial identity sync failed (non-fatal)", logger.WarnCF("agent", "Initial identity sync failed (non-fatal)",
map[string]interface{}{"error": syncErr.Error()}) map[string]interface{}{"error": syncErr.Error()})

View file

@ -12,7 +12,7 @@ import (
fantasy "charm.land/fantasy" fantasy "charm.land/fantasy"
"github.com/ZanzyTHEbar/dragonscale/pkg" "github.com/ZanzyTHEbar/dragonscale/pkg"
picofantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy" dragonfantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy"
"github.com/ZanzyTHEbar/dragonscale/pkg/logger" "github.com/ZanzyTHEbar/dragonscale/pkg/logger"
memstore "github.com/ZanzyTHEbar/dragonscale/pkg/memory/store" memstore "github.com/ZanzyTHEbar/dragonscale/pkg/memory/store"
"github.com/ZanzyTHEbar/dragonscale/pkg/tools" "github.com/ZanzyTHEbar/dragonscale/pkg/tools"
@ -90,12 +90,12 @@ func runToolLoopWithRuntime(
return nil, fmt.Errorf("tool runtime is required") return nil, fmt.Errorf("tool runtime is required")
} }
adaptCfg := picofantasy.AdaptedToolsConfig{ adaptCfg := dragonfantasy.AdaptedToolsConfig{
MemStore: ms, MemStore: ms,
AgentID: pkg.NAME, AgentID: pkg.NAME,
SessionKey: sessionKey, SessionKey: sessionKey,
} }
adaptedTools := picofantasy.BuildAdaptedTools(config.Tools, config.Bus, channel, chatID, adaptCfg) adaptedTools := dragonfantasy.BuildAdaptedTools(config.Tools, config.Bus, channel, chatID, adaptCfg)
if len(extraTools) > 0 { if len(extraTools) > 0 {
adaptedTools = append(adaptedTools, extraTools...) adaptedTools = append(adaptedTools, extraTools...)
} }
@ -126,7 +126,7 @@ func runToolLoopWithRuntime(
if len(newTools) == 0 { if len(newTools) == 0 {
return ctx, fantasy.PrepareStepResult{}, nil return ctx, fantasy.PrepareStepResult{}, nil
} }
newAdapted := picofantasy.AdaptTools(newTools, config.Bus, channel, chatID, adaptCfg) newAdapted := dragonfantasy.AdaptTools(newTools, config.Bus, channel, chatID, adaptCfg)
adaptedTools = append(adaptedTools, newAdapted...) adaptedTools = append(adaptedTools, newAdapted...)
return ctx, fantasy.PrepareStepResult{Tools: adaptedTools}, nil return ctx, fantasy.PrepareStepResult{Tools: adaptedTools}, nil
} }

View file

@ -1,5 +1,5 @@
// DragonScale - Ultra-lightweight personal AI agent // DragonScale - Ultra-lightweight personal AI agent
// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot // Inspired by and based on picoclaw: https://github.com/sipeed/picoclaw
// License: MIT // License: MIT
// //
// Copyright (c) 2026 DragonScale contributors // Copyright (c) 2026 DragonScale contributors

View file

@ -18,11 +18,11 @@ import (
"github.com/ZanzyTHEbar/dragonscale/pkg/tools" "github.com/ZanzyTHEbar/dragonscale/pkg/tools"
) )
// PicoToolAdapter wraps a DragonScale tool as a Fantasy AgentTool. // DragonToolAdapter wraps a DragonScale tool as a Fantasy AgentTool.
// It bridges DragonScale's dual-channel ToolResult semantics with Fantasy's // It bridges DragonScale's dual-channel ToolResult semantics with Fantasy's
// simple ToolResponse by publishing ForUser content to the bus as a side effect // simple ToolResponse by publishing ForUser content to the bus as a side effect
// and returning only ForLLM content to Fantasy. // and returning only ForLLM content to Fantasy.
type PicoToolAdapter struct { type DragonToolAdapter struct {
inner tools.Tool inner tools.Tool
bus *bus.MessageBus bus *bus.MessageBus
channel string channel string
@ -32,8 +32,8 @@ type PicoToolAdapter struct {
sessionKey string sessionKey string
} }
// Compile-time check that PicoToolAdapter implements fantasy.AgentTool. // Compile-time check that DragonToolAdapter implements fantasy.AgentTool.
var _ fantasy.AgentTool = (*PicoToolAdapter)(nil) var _ fantasy.AgentTool = (*DragonToolAdapter)(nil)
// Info returns Fantasy-compatible tool metadata from the DragonScale tool. // Info returns Fantasy-compatible tool metadata from the DragonScale tool.
// Fantasy's ToolInfo expects Parameters to be just the properties map and // Fantasy's ToolInfo expects Parameters to be just the properties map and
@ -41,7 +41,7 @@ var _ fantasy.AgentTool = (*PicoToolAdapter)(nil)
// Schema object from Parameters() (with "type", "properties", "required" // Schema object from Parameters() (with "type", "properties", "required"
// keys), so we must unwrap it here to avoid double-wrapping in // keys), so we must unwrap it here to avoid double-wrapping in
// agent.prepareTools() and agent.validateToolCall(). // agent.prepareTools() and agent.validateToolCall().
func (a *PicoToolAdapter) Info() fantasy.ToolInfo { func (a *DragonToolAdapter) Info() fantasy.ToolInfo {
params := a.inner.Parameters() params := a.inner.Parameters()
properties, required := unwrapSchema(params) properties, required := unwrapSchema(params)
return fantasy.ToolInfo{ return fantasy.ToolInfo{
@ -83,7 +83,7 @@ func unwrapSchema(params map[string]interface{}) (map[string]interface{}, []stri
// - If the tool result has ForUser content and is not Silent, publishes to the bus. // - If the tool result has ForUser content and is not Silent, publishes to the bus.
// - If the tool is a ContextualTool, sets channel/chatID context before execution. // - If the tool is a ContextualTool, sets channel/chatID context before execution.
// - If the tool is an AsyncTool, wires a callback that publishes results to the bus. // - If the tool is an AsyncTool, wires a callback that publishes results to the bus.
func (a *PicoToolAdapter) Run(ctx context.Context, call fantasy.ToolCall) (fantasy.ToolResponse, error) { func (a *DragonToolAdapter) Run(ctx context.Context, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
// 1. Deserialize Fantasy's JSON string input into DragonScale's map format. // 1. Deserialize Fantasy's JSON string input into DragonScale's map format.
args, err := parseToolArgs(call.Input) args, err := parseToolArgs(call.Input)
if err != nil { if err != nil {
@ -147,12 +147,12 @@ func (a *PicoToolAdapter) Run(ctx context.Context, call fantasy.ToolCall) (fanta
} }
// ProviderOptions returns nil — DragonScale tools have no provider-specific options. // ProviderOptions returns nil — DragonScale tools have no provider-specific options.
func (a *PicoToolAdapter) ProviderOptions() fantasy.ProviderOptions { func (a *DragonToolAdapter) ProviderOptions() fantasy.ProviderOptions {
return fantasy.ProviderOptions{} return fantasy.ProviderOptions{}
} }
// SetProviderOptions is a no-op for DragonScale tools. // SetProviderOptions is a no-op for DragonScale tools.
func (a *PicoToolAdapter) SetProviderOptions(_ fantasy.ProviderOptions) {} func (a *DragonToolAdapter) SetProviderOptions(_ fantasy.ProviderOptions) {}
// AdaptedToolsConfig configures how tools are adapted for the Fantasy agent. // AdaptedToolsConfig configures how tools are adapted for the Fantasy agent.
type AdaptedToolsConfig struct { type AdaptedToolsConfig struct {
@ -182,7 +182,7 @@ func BuildAdaptedTools(registry *tools.ToolRegistry, msgBus *bus.MessageBus, cha
if !ok { if !ok {
continue continue
} }
adapted = append(adapted, &PicoToolAdapter{ adapted = append(adapted, &DragonToolAdapter{
inner: tool, inner: tool,
bus: msgBus, bus: msgBus,
channel: channel, channel: channel,
@ -198,10 +198,10 @@ func BuildAdaptedTools(registry *tools.ToolRegistry, msgBus *bus.MessageBus, cha
// AdaptTools wraps specific Tool instances as Fantasy AgentTools. // AdaptTools wraps specific Tool instances as Fantasy AgentTools.
// Used by PrepareStep to promote discovered tools to native callables. // Used by PrepareStep to promote discovered tools to native callables.
func AdaptTools(picoTools []tools.Tool, msgBus *bus.MessageBus, channel, chatID string, cfg AdaptedToolsConfig) []fantasy.AgentTool { func AdaptTools(dragonTools []tools.Tool, msgBus *bus.MessageBus, channel, chatID string, cfg AdaptedToolsConfig) []fantasy.AgentTool {
adapted := make([]fantasy.AgentTool, 0, len(picoTools)) adapted := make([]fantasy.AgentTool, 0, len(dragonTools))
for _, tool := range picoTools { for _, tool := range dragonTools {
adapted = append(adapted, &PicoToolAdapter{ adapted = append(adapted, &DragonToolAdapter{
inner: tool, inner: tool,
bus: msgBus, bus: msgBus,
channel: channel, channel: channel,

View file

@ -100,11 +100,11 @@ func (t *mockNilResultTool) Execute(_ context.Context, _ map[string]interface{})
return nil return nil
} }
// --- PicoToolAdapter.Info() Tests --- // --- DragonToolAdapter.Info() Tests ---
func TestAdapter_Info(t *testing.T) { func TestAdapter_Info(t *testing.T) {
t.Parallel() t.Parallel()
adapter := &PicoToolAdapter{inner: &mockDualChannelTool{}} adapter := &DragonToolAdapter{inner: &mockDualChannelTool{}}
info := adapter.Info() info := adapter.Info()
if info.Name != "dual_tool" { if info.Name != "dual_tool" {
@ -130,7 +130,7 @@ func TestAdapter_Info(t *testing.T) {
func TestAdapter_Info_UnwrapsSchemaWithRequired(t *testing.T) { func TestAdapter_Info_UnwrapsSchemaWithRequired(t *testing.T) {
t.Parallel() t.Parallel()
mock := &mockToolWithRequired{} mock := &mockToolWithRequired{}
adapter := &PicoToolAdapter{inner: mock} adapter := &DragonToolAdapter{inner: mock}
info := adapter.Info() info := adapter.Info()
if _, ok := info.Parameters["path"]; !ok { if _, ok := info.Parameters["path"]; !ok {
@ -158,12 +158,12 @@ func (t *mockToolWithRequired) Execute(_ context.Context, _ map[string]interface
return &tools.ToolResult{ForLLM: "ok"} return &tools.ToolResult{ForLLM: "ok"}
} }
// --- PicoToolAdapter.Run() Tests --- // --- DragonToolAdapter.Run() Tests ---
func TestAdapter_Run_SilentTool_NoPublish(t *testing.T) { func TestAdapter_Run_SilentTool_NoPublish(t *testing.T) {
t.Parallel() t.Parallel()
msgBus := bus.NewMessageBus() msgBus := bus.NewMessageBus()
adapter := &PicoToolAdapter{ adapter := &DragonToolAdapter{
inner: &mockSilentTool{}, inner: &mockSilentTool{},
bus: msgBus, bus: msgBus,
channel: "test", channel: "test",
@ -192,7 +192,7 @@ func TestAdapter_Run_SilentTool_NoPublish(t *testing.T) {
func TestAdapter_Run_DualChannel_PublishesForUser(t *testing.T) { func TestAdapter_Run_DualChannel_PublishesForUser(t *testing.T) {
t.Parallel() t.Parallel()
msgBus := bus.NewMessageBus() msgBus := bus.NewMessageBus()
adapter := &PicoToolAdapter{ adapter := &DragonToolAdapter{
inner: &mockDualChannelTool{}, inner: &mockDualChannelTool{},
bus: msgBus, bus: msgBus,
channel: "telegram", channel: "telegram",
@ -221,7 +221,7 @@ func TestAdapter_Run_DualChannel_PublishesForUser(t *testing.T) {
func TestAdapter_Run_ErrorTool_ReturnsErrorResponse(t *testing.T) { func TestAdapter_Run_ErrorTool_ReturnsErrorResponse(t *testing.T) {
t.Parallel() t.Parallel()
adapter := &PicoToolAdapter{ adapter := &DragonToolAdapter{
inner: &mockErrorTool{}, inner: &mockErrorTool{},
} }
@ -246,7 +246,7 @@ func TestAdapter_Run_ErrorTool_ReturnsErrorResponse(t *testing.T) {
func TestAdapter_Run_NilResult_ReturnsError(t *testing.T) { func TestAdapter_Run_NilResult_ReturnsError(t *testing.T) {
t.Parallel() t.Parallel()
adapter := &PicoToolAdapter{ adapter := &DragonToolAdapter{
inner: &mockNilResultTool{}, inner: &mockNilResultTool{},
} }
@ -272,7 +272,7 @@ func TestAdapter_Run_NilResult_ReturnsError(t *testing.T) {
func TestAdapter_Run_ContextualTool_SetsContext(t *testing.T) { func TestAdapter_Run_ContextualTool_SetsContext(t *testing.T) {
t.Parallel() t.Parallel()
ctxTool := &mockContextualTool{} ctxTool := &mockContextualTool{}
adapter := &PicoToolAdapter{ adapter := &DragonToolAdapter{
inner: ctxTool, inner: ctxTool,
channel: "discord", channel: "discord",
chatID: "guild-1", chatID: "guild-1",
@ -297,7 +297,7 @@ func TestAdapter_Run_ContextualTool_SetsContext(t *testing.T) {
func TestAdapter_Run_InvalidJSON_ReturnsError(t *testing.T) { func TestAdapter_Run_InvalidJSON_ReturnsError(t *testing.T) {
t.Parallel() t.Parallel()
adapter := &PicoToolAdapter{ adapter := &DragonToolAdapter{
inner: &mockSilentTool{}, inner: &mockSilentTool{},
} }

View file

@ -428,7 +428,7 @@ func TestAgentResultToMessages_MultipleSteps(t *testing.T) {
// --- Round-trip fidelity test --- // --- Round-trip fidelity test ---
func TestRoundTrip_PicoClawToFantasyAndBack(t *testing.T) { func TestRoundTrip_DragonScaleToFantasyAndBack(t *testing.T) {
t.Parallel( t.Parallel(
// Start with DragonScale messages representing a typical conversation // Start with DragonScale messages representing a typical conversation
) )

View file

@ -1,5 +1,5 @@
// DragonScale - Ultra-lightweight personal AI agent // DragonScale - Ultra-lightweight personal AI agent
// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot // Inspired by and based on picoclaw: https://github.com/sipeed/picoclaw
// License: MIT // License: MIT
// //
// Copyright (c) 2026 DragonScale contributors // Copyright (c) 2026 DragonScale contributors

View file

@ -1,5 +1,5 @@
// DragonScale - Ultra-lightweight personal AI agent // DragonScale - Ultra-lightweight personal AI agent
// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot // Inspired by and based on picoclaw: https://github.com/sipeed/picoclaw
// License: MIT // License: MIT
// //
// Copyright (c) 2026 DragonScale contributors // Copyright (c) 2026 DragonScale contributors

View file

@ -28,7 +28,7 @@ type Options struct {
Force bool Force bool
Refresh bool Refresh bool
OpenClawHome string OpenClawHome string
PicoClawHome string DragonScaleHome string
} }
type Action struct { type Action struct {
@ -62,7 +62,7 @@ func Run(opts Options) (*Result, error) {
return nil, err return nil, err
} }
picoClawHome, err := resolvePicoClawHome(opts.PicoClawHome) dragonscaleHome, err := resolveDragonScaleHome(opts.DragonScaleHome)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -71,14 +71,14 @@ func Run(opts Options) (*Result, error) {
return nil, fmt.Errorf("OpenClaw installation not found at %s", openclawHome) return nil, fmt.Errorf("OpenClaw installation not found at %s", openclawHome)
} }
actions, warnings, err := Plan(opts, openclawHome, picoClawHome) actions, warnings, err := Plan(opts, openclawHome, dragonscaleHome)
if err != nil { if err != nil {
return nil, err return nil, err
} }
fmt.Println("Migrating from OpenClaw to DragonScale") fmt.Println("Migrating from OpenClaw to DragonScale")
fmt.Printf(" Source: %s\n", openclawHome) fmt.Printf(" Source: %s\n", openclawHome)
fmt.Printf(" Destination: %s\n", picoClawHome) fmt.Printf(" Destination: %s\n", dragonscaleHome)
fmt.Println() fmt.Println()
if opts.DryRun { if opts.DryRun {
@ -95,12 +95,12 @@ func Run(opts Options) (*Result, error) {
fmt.Println() fmt.Println()
} }
result := Execute(actions, openclawHome, picoClawHome) result := Execute(actions, openclawHome, dragonscaleHome)
result.Warnings = warnings result.Warnings = warnings
return result, nil return result, nil
} }
func Plan(opts Options, openclawHome, picoClawHome string) ([]Action, []string, error) { func Plan(opts Options, openclawHome, dragonscaleHome string) ([]Action, []string, error) {
var actions []Action var actions []Action
var warnings []string var warnings []string
@ -117,7 +117,7 @@ func Plan(opts Options, openclawHome, picoClawHome string) ([]Action, []string,
actions = append(actions, Action{ actions = append(actions, Action{
Type: ActionConvertConfig, Type: ActionConvertConfig,
Source: configPath, Source: configPath,
Destination: filepath.Join(picoClawHome, "config.json"), Destination: filepath.Join(dragonscaleHome, "config.json"),
Description: "convert OpenClaw config to DragonScale format", Description: "convert OpenClaw config to DragonScale format",
}) })
@ -131,7 +131,7 @@ func Plan(opts Options, openclawHome, picoClawHome string) ([]Action, []string,
if !opts.ConfigOnly { if !opts.ConfigOnly {
srcWorkspace := resolveWorkspace(openclawHome) srcWorkspace := resolveWorkspace(openclawHome)
dstWorkspace := resolveWorkspace(picoClawHome) dstWorkspace := resolveWorkspace(dragonscaleHome)
if _, err := os.Stat(srcWorkspace); err == nil { if _, err := os.Stat(srcWorkspace); err == nil {
wsActions, err := PlanWorkspaceMigration(srcWorkspace, dstWorkspace, force) wsActions, err := PlanWorkspaceMigration(srcWorkspace, dstWorkspace, force)
@ -147,13 +147,13 @@ func Plan(opts Options, openclawHome, picoClawHome string) ([]Action, []string,
return actions, warnings, nil return actions, warnings, nil
} }
func Execute(actions []Action, openclawHome, picoClawHome string) *Result { func Execute(actions []Action, openclawHome, dragonscaleHome string) *Result {
result := &Result{} result := &Result{}
for _, action := range actions { for _, action := range actions {
switch action.Type { switch action.Type {
case ActionConvertConfig: case ActionConvertConfig:
if err := executeConfigMigration(action.Source, action.Destination, picoClawHome); err != nil { if err := executeConfigMigration(action.Source, action.Destination, dragonscaleHome); err != nil {
result.Errors = append(result.Errors, fmt.Errorf("config migration: %w", err)) result.Errors = append(result.Errors, fmt.Errorf("config migration: %w", err))
fmt.Printf(" ✗ Config migration failed: %v\n", err) fmt.Printf(" ✗ Config migration failed: %v\n", err)
} else { } else {
@ -207,7 +207,7 @@ func Execute(actions []Action, openclawHome, picoClawHome string) *Result {
return result return result
} }
func executeConfigMigration(srcConfigPath, dstConfigPath, picoClawHome string) error { func executeConfigMigration(srcConfigPath, dstConfigPath, dragonscaleHome string) error {
data, err := LoadOpenClawConfig(srcConfigPath) data, err := LoadOpenClawConfig(srcConfigPath)
if err != nil { if err != nil {
return err return err
@ -326,7 +326,7 @@ func resolveOpenClawHome(override string) (string, error) {
return filepath.Join(home, ".openclaw"), nil return filepath.Join(home, ".openclaw"), nil
} }
func resolvePicoClawHome(override string) (string, error) { func resolveDragonScaleHome(override string) (string, error) {
if override != "" { if override != "" {
return expandHome(override), nil return expandHome(override), nil
} }

View file

@ -579,7 +579,7 @@ func TestRewriteWorkspacePath(t *testing.T) {
func TestRunDryRun(t *testing.T) { func TestRunDryRun(t *testing.T) {
t.Parallel() t.Parallel()
openclawHome := t.TempDir() openclawHome := t.TempDir()
picoClawHome := t.TempDir() dragonscaleHome := t.TempDir()
wsDir := filepath.Join(openclawHome, "workspace") wsDir := filepath.Join(openclawHome, "workspace")
os.MkdirAll(wsDir, 0755) os.MkdirAll(wsDir, 0755)
@ -599,7 +599,7 @@ func TestRunDryRun(t *testing.T) {
opts := Options{ opts := Options{
DryRun: true, DryRun: true,
OpenClawHome: openclawHome, OpenClawHome: openclawHome,
PicoClawHome: picoClawHome, DragonScaleHome: dragonscaleHome,
} }
result, err := Run(opts) result, err := Run(opts)
@ -607,11 +607,11 @@ func TestRunDryRun(t *testing.T) {
t.Fatalf("Run: %v", err) t.Fatalf("Run: %v", err)
} }
picoWs := filepath.Join(picoClawHome, "workspace") dragonWs := filepath.Join(dragonscaleHome, "workspace")
if _, err := os.Stat(filepath.Join(picoWs, "SOUL.md")); !os.IsNotExist(err) { if _, err := os.Stat(filepath.Join(dragonWs, "SOUL.md")); !os.IsNotExist(err) {
t.Error("dry run should not create files") t.Error("dry run should not create files")
} }
if _, err := os.Stat(filepath.Join(picoClawHome, "config.json")); !os.IsNotExist(err) { if _, err := os.Stat(filepath.Join(dragonscaleHome, "config.json")); !os.IsNotExist(err) {
t.Error("dry run should not create config") t.Error("dry run should not create config")
} }
@ -621,7 +621,7 @@ func TestRunDryRun(t *testing.T) {
func TestRunFullMigration(t *testing.T) { func TestRunFullMigration(t *testing.T) {
t.Parallel() t.Parallel()
openclawHome := t.TempDir() openclawHome := t.TempDir()
picoClawHome := t.TempDir() dragonscaleHome := t.TempDir()
wsDir := filepath.Join(openclawHome, "workspace") wsDir := filepath.Join(openclawHome, "workspace")
os.MkdirAll(wsDir, 0755) os.MkdirAll(wsDir, 0755)
@ -655,7 +655,7 @@ func TestRunFullMigration(t *testing.T) {
opts := Options{ opts := Options{
Force: true, Force: true,
OpenClawHome: openclawHome, OpenClawHome: openclawHome,
PicoClawHome: picoClawHome, DragonScaleHome: dragonscaleHome,
} }
result, err := Run(opts) result, err := Run(opts)
@ -663,9 +663,9 @@ func TestRunFullMigration(t *testing.T) {
t.Fatalf("Run: %v", err) t.Fatalf("Run: %v", err)
} }
picoWs := filepath.Join(picoClawHome, "workspace") dragonWs := filepath.Join(dragonscaleHome, "workspace")
soulData, err := os.ReadFile(filepath.Join(picoWs, "SOUL.md")) soulData, err := os.ReadFile(filepath.Join(dragonWs, "SOUL.md"))
if err != nil { if err != nil {
t.Fatalf("reading SOUL.md: %v", err) t.Fatalf("reading SOUL.md: %v", err)
} }
@ -673,7 +673,7 @@ func TestRunFullMigration(t *testing.T) {
t.Errorf("SOUL.md content = %q, want %q", string(soulData), "# Soul from OpenClaw") t.Errorf("SOUL.md content = %q, want %q", string(soulData), "# Soul from OpenClaw")
} }
agentsData, err := os.ReadFile(filepath.Join(picoWs, "AGENTS.md")) agentsData, err := os.ReadFile(filepath.Join(dragonWs, "AGENTS.md"))
if err != nil { if err != nil {
t.Fatalf("reading AGENTS.md: %v", err) t.Fatalf("reading AGENTS.md: %v", err)
} }
@ -681,7 +681,7 @@ func TestRunFullMigration(t *testing.T) {
t.Errorf("AGENTS.md content = %q", string(agentsData)) t.Errorf("AGENTS.md content = %q", string(agentsData))
} }
memData, err := os.ReadFile(filepath.Join(picoWs, "memory", "MEMORY.md")) memData, err := os.ReadFile(filepath.Join(dragonWs, "memory", "MEMORY.md"))
if err != nil { if err != nil {
t.Fatalf("reading memory/MEMORY.md: %v", err) t.Fatalf("reading memory/MEMORY.md: %v", err)
} }
@ -689,21 +689,21 @@ func TestRunFullMigration(t *testing.T) {
t.Errorf("MEMORY.md content = %q", string(memData)) t.Errorf("MEMORY.md content = %q", string(memData))
} }
picoConfig, err := config.LoadConfig(filepath.Join(picoClawHome, "config.json")) dragonConfig, err := config.LoadConfig(filepath.Join(dragonscaleHome, "config.json"))
if err != nil { if err != nil {
t.Fatalf("loading DragonScale config: %v", err) t.Fatalf("loading DragonScale config: %v", err)
} }
if picoConfig.Providers.Anthropic.APIKey != "sk-ant-migrate-test" { if dragonConfig.Providers.Anthropic.APIKey != "sk-ant-migrate-test" {
t.Errorf("Anthropic.APIKey = %q, want %q", picoConfig.Providers.Anthropic.APIKey, "sk-ant-migrate-test") t.Errorf("Anthropic.APIKey = %q, want %q", dragonConfig.Providers.Anthropic.APIKey, "sk-ant-migrate-test")
} }
if picoConfig.Providers.OpenRouter.APIKey != "sk-or-migrate-test" { if dragonConfig.Providers.OpenRouter.APIKey != "sk-or-migrate-test" {
t.Errorf("OpenRouter.APIKey = %q, want %q", picoConfig.Providers.OpenRouter.APIKey, "sk-or-migrate-test") t.Errorf("OpenRouter.APIKey = %q, want %q", dragonConfig.Providers.OpenRouter.APIKey, "sk-or-migrate-test")
} }
if !picoConfig.Channels.Telegram.Enabled { if !dragonConfig.Channels.Telegram.Enabled {
t.Error("Telegram should be enabled") t.Error("Telegram should be enabled")
} }
if picoConfig.Channels.Telegram.Token != "tg-migrate-test" { if dragonConfig.Channels.Telegram.Token != "tg-migrate-test" {
t.Errorf("Telegram.Token = %q, want %q", picoConfig.Channels.Telegram.Token, "tg-migrate-test") t.Errorf("Telegram.Token = %q, want %q", dragonConfig.Channels.Telegram.Token, "tg-migrate-test")
} }
if result.FilesCopied < 3 { if result.FilesCopied < 3 {
@ -721,7 +721,7 @@ func TestRunOpenClawNotFound(t *testing.T) {
t.Parallel() t.Parallel()
opts := Options{ opts := Options{
OpenClawHome: "/nonexistent/path/to/openclaw", OpenClawHome: "/nonexistent/path/to/openclaw",
PicoClawHome: t.TempDir(), DragonScaleHome: t.TempDir(),
} }
_, err := Run(opts) _, err := Run(opts)
@ -787,7 +787,7 @@ func TestCopyFile(t *testing.T) {
func TestRunConfigOnly(t *testing.T) { func TestRunConfigOnly(t *testing.T) {
t.Parallel() t.Parallel()
openclawHome := t.TempDir() openclawHome := t.TempDir()
picoClawHome := t.TempDir() dragonscaleHome := t.TempDir()
wsDir := filepath.Join(openclawHome, "workspace") wsDir := filepath.Join(openclawHome, "workspace")
os.MkdirAll(wsDir, 0755) os.MkdirAll(wsDir, 0755)
@ -807,7 +807,7 @@ func TestRunConfigOnly(t *testing.T) {
Force: true, Force: true,
ConfigOnly: true, ConfigOnly: true,
OpenClawHome: openclawHome, OpenClawHome: openclawHome,
PicoClawHome: picoClawHome, DragonScaleHome: dragonscaleHome,
} }
result, err := Run(opts) result, err := Run(opts)
@ -819,8 +819,8 @@ func TestRunConfigOnly(t *testing.T) {
t.Error("config should have been migrated") t.Error("config should have been migrated")
} }
picoWs := filepath.Join(picoClawHome, "workspace") dragonWs := filepath.Join(dragonscaleHome, "workspace")
if _, err := os.Stat(filepath.Join(picoWs, "SOUL.md")); !os.IsNotExist(err) { if _, err := os.Stat(filepath.Join(dragonWs, "SOUL.md")); !os.IsNotExist(err) {
t.Error("config-only should not copy workspace files") t.Error("config-only should not copy workspace files")
} }
} }
@ -828,7 +828,7 @@ func TestRunConfigOnly(t *testing.T) {
func TestRunWorkspaceOnly(t *testing.T) { func TestRunWorkspaceOnly(t *testing.T) {
t.Parallel() t.Parallel()
openclawHome := t.TempDir() openclawHome := t.TempDir()
picoClawHome := t.TempDir() dragonscaleHome := t.TempDir()
wsDir := filepath.Join(openclawHome, "workspace") wsDir := filepath.Join(openclawHome, "workspace")
os.MkdirAll(wsDir, 0755) os.MkdirAll(wsDir, 0755)
@ -848,7 +848,7 @@ func TestRunWorkspaceOnly(t *testing.T) {
Force: true, Force: true,
WorkspaceOnly: true, WorkspaceOnly: true,
OpenClawHome: openclawHome, OpenClawHome: openclawHome,
PicoClawHome: picoClawHome, DragonScaleHome: dragonscaleHome,
} }
result, err := Run(opts) result, err := Run(opts)
@ -860,8 +860,8 @@ func TestRunWorkspaceOnly(t *testing.T) {
t.Error("workspace-only should not migrate config") t.Error("workspace-only should not migrate config")
} }
picoWs := filepath.Join(picoClawHome, "workspace") dragonWs := filepath.Join(dragonscaleHome, "workspace")
soulData, err := os.ReadFile(filepath.Join(picoWs, "SOUL.md")) soulData, err := os.ReadFile(filepath.Join(dragonWs, "SOUL.md"))
if err != nil { if err != nil {
t.Fatalf("reading SOUL.md: %v", err) t.Fatalf("reading SOUL.md: %v", err)
} }

View file

@ -9,7 +9,7 @@ import (
"github.com/ZanzyTHEbar/dragonscale/pkg/agent" "github.com/ZanzyTHEbar/dragonscale/pkg/agent"
"github.com/ZanzyTHEbar/dragonscale/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/ZanzyTHEbar/dragonscale/pkg/config" "github.com/ZanzyTHEbar/dragonscale/pkg/config"
picofantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy" dragonfantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy"
) )
type OutboundMode string type OutboundMode string
@ -72,13 +72,13 @@ func Bootstrap(parent context.Context, cfg *config.Config, opts BootstrapOptions
ctx, cancel := withExecutionContext(parent, opts.Timeout) ctx, cancel := withExecutionContext(parent, opts.Timeout)
provider, err := picofantasy.CreateProvider(cfg) provider, err := dragonfantasy.CreateProvider(cfg)
if err != nil { if err != nil {
cancel() cancel()
return nil, fmt.Errorf("provider error: %w", err) return nil, fmt.Errorf("provider error: %w", err)
} }
model, err := provider.LanguageModel(ctx, picofantasy.ModelID(cfg)) model, err := provider.LanguageModel(ctx, dragonfantasy.ModelID(cfg))
if err != nil { if err != nil {
cancel() cancel()
return nil, fmt.Errorf("model error: %w", err) return nil, fmt.Errorf("model error: %w", err)

View file

@ -6,10 +6,15 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"github.com/ZanzyTHEbar/dragonscale/pkg"
"github.com/ZanzyTHEbar/dragonscale/pkg/config" "github.com/ZanzyTHEbar/dragonscale/pkg/config"
) )
const EvalConfigEnvVar = "DRAGONSCALE_EVAL_CONFIG" const (
EvalConfigEnvVar = "DRAGONSCALE_EVAL_CONFIG"
EvalBaseConfigEnvVar = "DRAGONSCALE_EVAL_BASE_CONFIG"
EvalHostHomeEnvVar = "DRAGONSCALE_EVAL_HOST_HOME"
)
type LoadConfigOptions struct { type LoadConfigOptions struct {
BaseConfigPath string BaseConfigPath string
@ -18,24 +23,51 @@ type LoadConfigOptions struct {
} }
func ResolveBaseConfigPath() string { func ResolveBaseConfigPath() string {
if explicit := os.Getenv(EvalBaseConfigEnvVar); explicit != "" {
if _, err := os.Stat(explicit); err == nil {
return explicit
}
}
checkHostConfig := func(hostHome string) string {
if hostHome == "" {
return ""
}
// XDG standard path.
hostXDG := filepath.Join(hostHome, ".config", pkg.NAME, "config.json")
if _, err := os.Stat(hostXDG); err == nil {
return hostXDG
}
return ""
}
// Prefer host-mounted config when running in containerized eval.
if hostHome := os.Getenv(EvalHostHomeEnvVar); hostHome != "" {
if path := checkHostConfig(hostHome); path != "" {
return path
}
}
// Fall back to the default devcontainer host mount location.
if path := checkHostConfig("/host_home"); path != "" {
return path
}
// Prefer XDG standard path (~/.config/dragonscale/config.json) when present. // Prefer XDG standard path (~/.config/dragonscale/config.json) when present.
if xdgPath, err := config.DefaultConfigPath(); err == nil { xdgPath, err := config.DefaultConfigPath()
if err == nil {
if _, statErr := os.Stat(xdgPath); statErr == nil { if _, statErr := os.Stat(xdgPath); statErr == nil {
return xdgPath return xdgPath
} }
} }
home, _ := os.UserHomeDir()
legacy := filepath.Join(home, ".dragonscale", "config.json")
if _, err := os.Stat(legacy); err == nil {
return legacy
}
// Neither exists; return XDG path if resolvable so defaults still load. // Neither exists; return XDG path if resolvable so defaults still load.
if xdgPath, err := config.DefaultConfigPath(); err == nil { if err == nil {
return xdgPath return xdgPath
} }
return legacy
return ""
} }
func LoadResolvedConfig(opts LoadConfigOptions) (*config.Config, error) { func LoadResolvedConfig(opts LoadConfigOptions) (*config.Config, error) {

View file

@ -44,18 +44,139 @@ func TestResolveBaseConfigPath_PrefersXDGOverLegacy(t *testing.T) {
assert.Empty(t, cmp.Diff(xdgPath, got)) assert.Empty(t, cmp.Diff(xdgPath, got))
} }
func TestResolveBaseConfigPath_FallsBackToLegacyWhenXDGMissing(t *testing.T) { func TestResolveBaseConfigPath_FallsBackToXDGWhenLegacyOnlyExists(t *testing.T) {
home := t.TempDir() home := t.TempDir()
xdg := t.TempDir() xdg := t.TempDir()
t.Setenv("HOME", home) t.Setenv("HOME", home)
t.Setenv("XDG_CONFIG_HOME", xdg) t.Setenv("XDG_CONFIG_HOME", xdg)
xdgPath := filepath.Join(xdg, pkg.NAME, "config.json")
legacyPath := filepath.Join(home, ".dragonscale", "config.json") legacyPath := filepath.Join(home, ".dragonscale", "config.json")
require.NoError(t, os.MkdirAll(filepath.Dir(xdgPath), 0o755))
require.NoError(t, os.MkdirAll(filepath.Dir(legacyPath), 0o755)) require.NoError(t, os.MkdirAll(filepath.Dir(legacyPath), 0o755))
require.NoError(t, os.WriteFile(xdgPath, []byte(`{}`), 0o644))
require.NoError(t, os.WriteFile(legacyPath, []byte(`{}`), 0o644)) require.NoError(t, os.WriteFile(legacyPath, []byte(`{}`), 0o644))
got := ResolveBaseConfigPath() got := ResolveBaseConfigPath()
assert.Empty(t, cmp.Diff(legacyPath, got)) assert.Empty(t, cmp.Diff(xdgPath, got))
assert.NotEmpty(t, legacyPath)
}
func TestResolveBaseConfigPath_UsesEvalHostHomeWhenContainerConfigMissing(t *testing.T) {
home := t.TempDir()
xdg := t.TempDir()
hostHome := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("XDG_CONFIG_HOME", xdg)
t.Setenv(EvalHostHomeEnvVar, hostHome)
hostXDGConfig := filepath.Join(hostHome, ".config", pkg.NAME, "config.json")
require.NoError(t, os.MkdirAll(filepath.Dir(hostXDGConfig), 0o755))
require.NoError(t, os.WriteFile(hostXDGConfig, []byte(`{}`), 0o644))
got := ResolveBaseConfigPath()
assert.Empty(t, cmp.Diff(hostXDGConfig, got))
}
func TestResolveBaseConfigPath_PrefersEvalHostHomeOverContainerConfig(t *testing.T) {
home := t.TempDir()
xdg := t.TempDir()
hostHome := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("XDG_CONFIG_HOME", xdg)
t.Setenv(EvalHostHomeEnvVar, hostHome)
containerXDG := filepath.Join(xdg, pkg.NAME, "config.json")
hostXDG := filepath.Join(hostHome, ".config", pkg.NAME, "config.json")
require.NoError(t, os.MkdirAll(filepath.Dir(containerXDG), 0o755))
require.NoError(t, os.MkdirAll(filepath.Dir(hostXDG), 0o755))
require.NoError(t, os.WriteFile(containerXDG, []byte(`{}`), 0o644))
require.NoError(t, os.WriteFile(hostXDG, []byte(`{}`), 0o644))
got := ResolveBaseConfigPath()
assert.Empty(t, cmp.Diff(hostXDG, got))
}
func TestResolveBaseConfigPath_DoesNotUseLegacyHostPathForHostHome(t *testing.T) {
home := t.TempDir()
xdg := t.TempDir()
hostHome := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("XDG_CONFIG_HOME", xdg)
t.Setenv(EvalHostHomeEnvVar, hostHome)
containerXDG := filepath.Join(xdg, pkg.NAME, "config.json")
hostLegacy := filepath.Join(hostHome, ".dragonscale", "config.json")
require.NoError(t, os.MkdirAll(filepath.Dir(containerXDG), 0o755))
require.NoError(t, os.MkdirAll(filepath.Dir(hostLegacy), 0o755))
require.NoError(t, os.WriteFile(containerXDG, []byte(`{}`), 0o644))
require.NoError(t, os.WriteFile(hostLegacy, []byte(`{}`), 0o644))
got := ResolveBaseConfigPath()
assert.Empty(t, cmp.Diff(containerXDG, got))
}
func TestResolveBaseConfigPath_UsesEvalBaseConfigOverride(t *testing.T) {
override := filepath.Join(t.TempDir(), "explicit-config.json")
require.NoError(t, os.WriteFile(override, []byte(`{}`), 0o644))
t.Setenv(EvalBaseConfigEnvVar, override)
got := ResolveBaseConfigPath()
assert.Empty(t, cmp.Diff(override, got))
}
func TestResolveBaseConfigPath_UsesEvalBaseConfigUnsetAsFallback(t *testing.T) {
home := t.TempDir()
xdg := t.TempDir()
hostHome := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("XDG_CONFIG_HOME", xdg)
t.Setenv(EvalHostHomeEnvVar, hostHome)
hostXDG := filepath.Join(hostHome, ".config", pkg.NAME, "config.json")
require.NoError(t, os.MkdirAll(filepath.Dir(hostXDG), 0o755))
require.NoError(t, os.WriteFile(hostXDG, []byte(`{}`), 0o644))
t.Setenv(EvalBaseConfigEnvVar, "")
got := ResolveBaseConfigPath()
assert.Empty(t, cmp.Diff(hostXDG, got))
}
func TestResolveBaseConfigPath_InvalidEvalBaseConfigFallsBackToHostConfig(t *testing.T) {
home := t.TempDir()
xdg := t.TempDir()
hostHome := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("XDG_CONFIG_HOME", xdg)
t.Setenv(EvalHostHomeEnvVar, hostHome)
hostXDG := filepath.Join(hostHome, ".config", pkg.NAME, "config.json")
require.NoError(t, os.MkdirAll(filepath.Dir(hostXDG), 0o755))
require.NoError(t, os.WriteFile(hostXDG, []byte(`{}`), 0o644))
t.Setenv(EvalBaseConfigEnvVar, filepath.Join(t.TempDir(), "no-such-file.json"))
got := ResolveBaseConfigPath()
assert.Empty(t, cmp.Diff(hostXDG, got))
}
func TestLoadResolvedConfig_FallsBackWhenExplicitBaseConfigInvalid(t *testing.T) {
workDir := t.TempDir()
hostDir := t.TempDir()
home := t.TempDir()
xdg := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("XDG_CONFIG_HOME", xdg)
t.Setenv(EvalHostHomeEnvVar, hostDir)
basePath := filepath.Join(hostDir, ".config", pkg.NAME, "config.json")
require.NoError(t, os.MkdirAll(filepath.Dir(basePath), 0o755))
require.NoError(t, os.WriteFile(basePath, []byte(`{"agents":{"defaults":{"restrict_to_sandbox":true,"max_tool_iterations":20}}}`), 0o644))
t.Setenv(EvalBaseConfigEnvVar, filepath.Join(workDir, "does-not-exist.json"))
cfg, err := LoadResolvedConfig(LoadConfigOptions{MinProviderTimeout: 2 * time.Second})
require.NoError(t, err)
assert.Empty(t, cmp.Diff(true, cfg.Agents.Defaults.RestrictToSandbox))
} }
func TestLoadResolvedConfig_AppliesOverlayAndKeepsBaseValues(t *testing.T) { func TestLoadResolvedConfig_AppliesOverlayAndKeepsBaseValues(t *testing.T) {

View file

@ -91,7 +91,7 @@ func TestSyncAll_InsertsNewFiles(t *testing.T) {
"AGENT.md": "# Agent\nYou are helpful.", "AGENT.md": "# Agent\nYou are helpful.",
"SOUL.md": "# Soul\nCurious and kind.", "SOUL.md": "# Soul\nCurious and kind.",
"USER.md": "# User\nName: Alice", "USER.md": "# User\nName: Alice",
"IDENTITY.md": "# Identity\nPicoClaw v1", "IDENTITY.md": "# Identity\nDragonScale v1",
}) })
store := newMockStore() store := newMockStore()