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:
parent
3286555321
commit
a76db05a09
15 changed files with 283 additions and 130 deletions
|
|
@ -13,7 +13,7 @@ import (
|
|||
"github.com/ZanzyTHEbar/dragonscale/pkg"
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg/bus"
|
||||
"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/logger"
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg/memory"
|
||||
|
|
@ -93,7 +93,7 @@ func (al *AgentLoop) assembleContext(ctx context.Context, opts processOptions) (
|
|||
"history": formatMessagesForLog(historyMsgs),
|
||||
})
|
||||
|
||||
fantasyHistory := picofantasy.MessagesToFantasy(historyMsgs)
|
||||
fantasyHistory := dragonfantasy.MessagesToFantasy(historyMsgs)
|
||||
adaptedTools, prepareStep := al.prepareToolset(ctx, opts)
|
||||
agent, err := al.createFantasyAgent(ctx, opts, systemPrompt, adaptedTools, prepareStep)
|
||||
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)) {
|
||||
adaptCfg := picofantasy.AdaptedToolsConfig{
|
||||
adaptCfg := dragonfantasy.AdaptedToolsConfig{
|
||||
MemStore: al.memoryStore,
|
||||
AgentID: pkg.NAME,
|
||||
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 {
|
||||
adaptedTools = append(adaptedTools, al.toolResultSearch)
|
||||
}
|
||||
|
|
@ -217,7 +217,7 @@ func (al *AgentLoop) prepareToolset(ctx context.Context, opts processOptions) ([
|
|||
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...)
|
||||
adaptedTools = expanded
|
||||
|
||||
|
|
@ -419,7 +419,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, opts processOptions) (str
|
|||
}
|
||||
|
||||
for _, step := range result.Steps {
|
||||
stepMsgs := picofantasy.StepToMessages(step)
|
||||
stepMsgs := dragonfantasy.StepToMessages(step)
|
||||
for _, m := range stepMsgs {
|
||||
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 {
|
||||
stepMsgs := picofantasy.StepToMessages(step)
|
||||
stepMsgs := dragonfantasy.StepToMessages(step)
|
||||
for _, m := range stepMsgs {
|
||||
al.sessions.AddFullMessage(opts.SessionKey, m)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// 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
|
||||
//
|
||||
// Copyright (c) 2026 DragonScale contributors
|
||||
|
|
@ -32,7 +32,7 @@ import (
|
|||
"github.com/ZanzyTHEbar/dragonscale/pkg/security/securebus"
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg/session"
|
||||
"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"
|
||||
)
|
||||
|
||||
|
|
@ -47,21 +47,21 @@ type AgentLoop struct {
|
|||
state *state.Manager
|
||||
contextBuilder *ContextBuilder
|
||||
tools *tools.ToolRegistry
|
||||
memoryStore *memstore.MemoryStore // 3-tier MemGPT memory (always initialized)
|
||||
memDelegate memory.MemoryDelegate // DB delegate (always initialized)
|
||||
obsManager *observation.Manager // Observational memory (always initialized)
|
||||
secureBus *securebus.Bus // ITR SecureBus (always initialized)
|
||||
queries *memsqlc.Queries // SQL query surface for runtime persistence
|
||||
kvDelegate KVDelegate // KV adapter for offloaded tool results
|
||||
stateStore *StateStore // Agent run state persistence
|
||||
conversationIDs sync.Map // Owner: agent_run.go — wrote by prepareRuntimeState, read in prepareRuntimeState/load path
|
||||
conversationMu sync.Mutex // serializes conversation creation path
|
||||
identitySync *picosync.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
|
||||
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
|
||||
summarizeFailures sync.Map // Owner: summarizer.go — write/read in forceCompression + summarizeSession error paths
|
||||
cfg *config.Config // Stored for subagent factory access
|
||||
memoryStore *memstore.MemoryStore // 3-tier MemGPT memory (always initialized)
|
||||
memDelegate memory.MemoryDelegate // DB delegate (always initialized)
|
||||
obsManager *observation.Manager // Observational memory (always initialized)
|
||||
secureBus *securebus.Bus // ITR SecureBus (always initialized)
|
||||
queries *memsqlc.Queries // SQL query surface for runtime persistence
|
||||
kvDelegate KVDelegate // KV adapter for offloaded tool results
|
||||
stateStore *StateStore // Agent run state persistence
|
||||
conversationIDs sync.Map // Owner: agent_run.go — wrote by prepareRuntimeState, read in prepareRuntimeState/load path
|
||||
conversationMu sync.Mutex // serializes conversation creation path
|
||||
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
|
||||
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
|
||||
summarizeFailures sync.Map // Owner: summarizer.go — write/read in forceCompression + summarizeSession error paths
|
||||
cfg *config.Config // Stored for subagent factory access
|
||||
channelManager *channels.Manager
|
||||
commandRegistry []SlashCommand
|
||||
outputOverride atomic.Value // Owner: command_handler.go — CLI output redirection target for internal messages
|
||||
|
|
@ -217,13 +217,13 @@ func NewAgentLoop(ctx context.Context, cfg *config.Config, msgBus *bus.MessageBu
|
|||
}
|
||||
|
||||
// Identity file sync (disk → DB)
|
||||
var idSync *picosync.IdentitySync
|
||||
var idSync *dragonsync.IdentitySync
|
||||
identityDir, idErr := config.IdentityDir()
|
||||
if idErr != nil {
|
||||
logger.WarnCF("agent", "Could not resolve identity dir, identity sync disabled",
|
||||
map[string]interface{}{"error": idErr.Error()})
|
||||
} else {
|
||||
idSync = picosync.New(identityDir, pkg.NAME, memDelegate)
|
||||
idSync = dragonsync.New(identityDir, pkg.NAME, memDelegate)
|
||||
if syncErr := idSync.SyncAll(ctx); syncErr != nil {
|
||||
logger.WarnCF("agent", "Initial identity sync failed (non-fatal)",
|
||||
map[string]interface{}{"error": syncErr.Error()})
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
fantasy "charm.land/fantasy"
|
||||
"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"
|
||||
memstore "github.com/ZanzyTHEbar/dragonscale/pkg/memory/store"
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg/tools"
|
||||
|
|
@ -90,12 +90,12 @@ func runToolLoopWithRuntime(
|
|||
return nil, fmt.Errorf("tool runtime is required")
|
||||
}
|
||||
|
||||
adaptCfg := picofantasy.AdaptedToolsConfig{
|
||||
adaptCfg := dragonfantasy.AdaptedToolsConfig{
|
||||
MemStore: ms,
|
||||
AgentID: pkg.NAME,
|
||||
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 {
|
||||
adaptedTools = append(adaptedTools, extraTools...)
|
||||
}
|
||||
|
|
@ -126,7 +126,7 @@ func runToolLoopWithRuntime(
|
|||
if len(newTools) == 0 {
|
||||
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...)
|
||||
return ctx, fantasy.PrepareStepResult{Tools: adaptedTools}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// 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
|
||||
//
|
||||
// Copyright (c) 2026 DragonScale contributors
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ import (
|
|||
"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
|
||||
// simple ToolResponse by publishing ForUser content to the bus as a side effect
|
||||
// and returning only ForLLM content to Fantasy.
|
||||
type PicoToolAdapter struct {
|
||||
type DragonToolAdapter struct {
|
||||
inner tools.Tool
|
||||
bus *bus.MessageBus
|
||||
channel string
|
||||
|
|
@ -32,8 +32,8 @@ type PicoToolAdapter struct {
|
|||
sessionKey string
|
||||
}
|
||||
|
||||
// Compile-time check that PicoToolAdapter implements fantasy.AgentTool.
|
||||
var _ fantasy.AgentTool = (*PicoToolAdapter)(nil)
|
||||
// Compile-time check that DragonToolAdapter implements fantasy.AgentTool.
|
||||
var _ fantasy.AgentTool = (*DragonToolAdapter)(nil)
|
||||
|
||||
// Info returns Fantasy-compatible tool metadata from the DragonScale tool.
|
||||
// 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"
|
||||
// keys), so we must unwrap it here to avoid double-wrapping in
|
||||
// agent.prepareTools() and agent.validateToolCall().
|
||||
func (a *PicoToolAdapter) Info() fantasy.ToolInfo {
|
||||
func (a *DragonToolAdapter) Info() fantasy.ToolInfo {
|
||||
params := a.inner.Parameters()
|
||||
properties, required := unwrapSchema(params)
|
||||
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 is a ContextualTool, sets channel/chatID context before execution.
|
||||
// - 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.
|
||||
args, err := parseToolArgs(call.Input)
|
||||
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.
|
||||
func (a *PicoToolAdapter) ProviderOptions() fantasy.ProviderOptions {
|
||||
func (a *DragonToolAdapter) ProviderOptions() fantasy.ProviderOptions {
|
||||
return fantasy.ProviderOptions{}
|
||||
}
|
||||
|
||||
// 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.
|
||||
type AdaptedToolsConfig struct {
|
||||
|
|
@ -182,7 +182,7 @@ func BuildAdaptedTools(registry *tools.ToolRegistry, msgBus *bus.MessageBus, cha
|
|||
if !ok {
|
||||
continue
|
||||
}
|
||||
adapted = append(adapted, &PicoToolAdapter{
|
||||
adapted = append(adapted, &DragonToolAdapter{
|
||||
inner: tool,
|
||||
bus: msgBus,
|
||||
channel: channel,
|
||||
|
|
@ -198,10 +198,10 @@ func BuildAdaptedTools(registry *tools.ToolRegistry, msgBus *bus.MessageBus, cha
|
|||
|
||||
// AdaptTools wraps specific Tool instances as Fantasy AgentTools.
|
||||
// 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 {
|
||||
adapted := make([]fantasy.AgentTool, 0, len(picoTools))
|
||||
for _, tool := range picoTools {
|
||||
adapted = append(adapted, &PicoToolAdapter{
|
||||
func AdaptTools(dragonTools []tools.Tool, msgBus *bus.MessageBus, channel, chatID string, cfg AdaptedToolsConfig) []fantasy.AgentTool {
|
||||
adapted := make([]fantasy.AgentTool, 0, len(dragonTools))
|
||||
for _, tool := range dragonTools {
|
||||
adapted = append(adapted, &DragonToolAdapter{
|
||||
inner: tool,
|
||||
bus: msgBus,
|
||||
channel: channel,
|
||||
|
|
|
|||
|
|
@ -100,11 +100,11 @@ func (t *mockNilResultTool) Execute(_ context.Context, _ map[string]interface{})
|
|||
return nil
|
||||
}
|
||||
|
||||
// --- PicoToolAdapter.Info() Tests ---
|
||||
// --- DragonToolAdapter.Info() Tests ---
|
||||
|
||||
func TestAdapter_Info(t *testing.T) {
|
||||
t.Parallel()
|
||||
adapter := &PicoToolAdapter{inner: &mockDualChannelTool{}}
|
||||
adapter := &DragonToolAdapter{inner: &mockDualChannelTool{}}
|
||||
info := adapter.Info()
|
||||
|
||||
if info.Name != "dual_tool" {
|
||||
|
|
@ -130,7 +130,7 @@ func TestAdapter_Info(t *testing.T) {
|
|||
func TestAdapter_Info_UnwrapsSchemaWithRequired(t *testing.T) {
|
||||
t.Parallel()
|
||||
mock := &mockToolWithRequired{}
|
||||
adapter := &PicoToolAdapter{inner: mock}
|
||||
adapter := &DragonToolAdapter{inner: mock}
|
||||
info := adapter.Info()
|
||||
|
||||
if _, ok := info.Parameters["path"]; !ok {
|
||||
|
|
@ -158,12 +158,12 @@ func (t *mockToolWithRequired) Execute(_ context.Context, _ map[string]interface
|
|||
return &tools.ToolResult{ForLLM: "ok"}
|
||||
}
|
||||
|
||||
// --- PicoToolAdapter.Run() Tests ---
|
||||
// --- DragonToolAdapter.Run() Tests ---
|
||||
|
||||
func TestAdapter_Run_SilentTool_NoPublish(t *testing.T) {
|
||||
t.Parallel()
|
||||
msgBus := bus.NewMessageBus()
|
||||
adapter := &PicoToolAdapter{
|
||||
adapter := &DragonToolAdapter{
|
||||
inner: &mockSilentTool{},
|
||||
bus: msgBus,
|
||||
channel: "test",
|
||||
|
|
@ -192,7 +192,7 @@ func TestAdapter_Run_SilentTool_NoPublish(t *testing.T) {
|
|||
func TestAdapter_Run_DualChannel_PublishesForUser(t *testing.T) {
|
||||
t.Parallel()
|
||||
msgBus := bus.NewMessageBus()
|
||||
adapter := &PicoToolAdapter{
|
||||
adapter := &DragonToolAdapter{
|
||||
inner: &mockDualChannelTool{},
|
||||
bus: msgBus,
|
||||
channel: "telegram",
|
||||
|
|
@ -221,7 +221,7 @@ func TestAdapter_Run_DualChannel_PublishesForUser(t *testing.T) {
|
|||
|
||||
func TestAdapter_Run_ErrorTool_ReturnsErrorResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
adapter := &PicoToolAdapter{
|
||||
adapter := &DragonToolAdapter{
|
||||
inner: &mockErrorTool{},
|
||||
}
|
||||
|
||||
|
|
@ -246,7 +246,7 @@ func TestAdapter_Run_ErrorTool_ReturnsErrorResponse(t *testing.T) {
|
|||
|
||||
func TestAdapter_Run_NilResult_ReturnsError(t *testing.T) {
|
||||
t.Parallel()
|
||||
adapter := &PicoToolAdapter{
|
||||
adapter := &DragonToolAdapter{
|
||||
inner: &mockNilResultTool{},
|
||||
}
|
||||
|
||||
|
|
@ -272,7 +272,7 @@ func TestAdapter_Run_NilResult_ReturnsError(t *testing.T) {
|
|||
func TestAdapter_Run_ContextualTool_SetsContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctxTool := &mockContextualTool{}
|
||||
adapter := &PicoToolAdapter{
|
||||
adapter := &DragonToolAdapter{
|
||||
inner: ctxTool,
|
||||
channel: "discord",
|
||||
chatID: "guild-1",
|
||||
|
|
@ -297,7 +297,7 @@ func TestAdapter_Run_ContextualTool_SetsContext(t *testing.T) {
|
|||
|
||||
func TestAdapter_Run_InvalidJSON_ReturnsError(t *testing.T) {
|
||||
t.Parallel()
|
||||
adapter := &PicoToolAdapter{
|
||||
adapter := &DragonToolAdapter{
|
||||
inner: &mockSilentTool{},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -428,7 +428,7 @@ func TestAgentResultToMessages_MultipleSteps(t *testing.T) {
|
|||
|
||||
// --- Round-trip fidelity test ---
|
||||
|
||||
func TestRoundTrip_PicoClawToFantasyAndBack(t *testing.T) {
|
||||
func TestRoundTrip_DragonScaleToFantasyAndBack(t *testing.T) {
|
||||
t.Parallel(
|
||||
// Start with DragonScale messages representing a typical conversation
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// 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
|
||||
//
|
||||
// Copyright (c) 2026 DragonScale contributors
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// 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
|
||||
//
|
||||
// Copyright (c) 2026 DragonScale contributors
|
||||
|
|
|
|||
|
|
@ -22,13 +22,13 @@ const (
|
|||
)
|
||||
|
||||
type Options struct {
|
||||
DryRun bool
|
||||
ConfigOnly bool
|
||||
WorkspaceOnly bool
|
||||
Force bool
|
||||
Refresh bool
|
||||
OpenClawHome string
|
||||
PicoClawHome string
|
||||
DryRun bool
|
||||
ConfigOnly bool
|
||||
WorkspaceOnly bool
|
||||
Force bool
|
||||
Refresh bool
|
||||
OpenClawHome string
|
||||
DragonScaleHome string
|
||||
}
|
||||
|
||||
type Action struct {
|
||||
|
|
@ -62,7 +62,7 @@ func Run(opts Options) (*Result, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
picoClawHome, err := resolvePicoClawHome(opts.PicoClawHome)
|
||||
dragonscaleHome, err := resolveDragonScaleHome(opts.DragonScaleHome)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -71,14 +71,14 @@ func Run(opts Options) (*Result, error) {
|
|||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Println("Migrating from OpenClaw to DragonScale")
|
||||
fmt.Printf(" Source: %s\n", openclawHome)
|
||||
fmt.Printf(" Destination: %s\n", picoClawHome)
|
||||
fmt.Printf(" Destination: %s\n", dragonscaleHome)
|
||||
fmt.Println()
|
||||
|
||||
if opts.DryRun {
|
||||
|
|
@ -95,12 +95,12 @@ func Run(opts Options) (*Result, error) {
|
|||
fmt.Println()
|
||||
}
|
||||
|
||||
result := Execute(actions, openclawHome, picoClawHome)
|
||||
result := Execute(actions, openclawHome, dragonscaleHome)
|
||||
result.Warnings = warnings
|
||||
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 warnings []string
|
||||
|
||||
|
|
@ -117,7 +117,7 @@ func Plan(opts Options, openclawHome, picoClawHome string) ([]Action, []string,
|
|||
actions = append(actions, Action{
|
||||
Type: ActionConvertConfig,
|
||||
Source: configPath,
|
||||
Destination: filepath.Join(picoClawHome, "config.json"),
|
||||
Destination: filepath.Join(dragonscaleHome, "config.json"),
|
||||
Description: "convert OpenClaw config to DragonScale format",
|
||||
})
|
||||
|
||||
|
|
@ -131,7 +131,7 @@ func Plan(opts Options, openclawHome, picoClawHome string) ([]Action, []string,
|
|||
|
||||
if !opts.ConfigOnly {
|
||||
srcWorkspace := resolveWorkspace(openclawHome)
|
||||
dstWorkspace := resolveWorkspace(picoClawHome)
|
||||
dstWorkspace := resolveWorkspace(dragonscaleHome)
|
||||
|
||||
if _, err := os.Stat(srcWorkspace); err == nil {
|
||||
wsActions, err := PlanWorkspaceMigration(srcWorkspace, dstWorkspace, force)
|
||||
|
|
@ -147,13 +147,13 @@ func Plan(opts Options, openclawHome, picoClawHome string) ([]Action, []string,
|
|||
return actions, warnings, nil
|
||||
}
|
||||
|
||||
func Execute(actions []Action, openclawHome, picoClawHome string) *Result {
|
||||
func Execute(actions []Action, openclawHome, dragonscaleHome string) *Result {
|
||||
result := &Result{}
|
||||
|
||||
for _, action := range actions {
|
||||
switch action.Type {
|
||||
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))
|
||||
fmt.Printf(" ✗ Config migration failed: %v\n", err)
|
||||
} else {
|
||||
|
|
@ -207,7 +207,7 @@ func Execute(actions []Action, openclawHome, picoClawHome string) *Result {
|
|||
return result
|
||||
}
|
||||
|
||||
func executeConfigMigration(srcConfigPath, dstConfigPath, picoClawHome string) error {
|
||||
func executeConfigMigration(srcConfigPath, dstConfigPath, dragonscaleHome string) error {
|
||||
data, err := LoadOpenClawConfig(srcConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -326,7 +326,7 @@ func resolveOpenClawHome(override string) (string, error) {
|
|||
return filepath.Join(home, ".openclaw"), nil
|
||||
}
|
||||
|
||||
func resolvePicoClawHome(override string) (string, error) {
|
||||
func resolveDragonScaleHome(override string) (string, error) {
|
||||
if override != "" {
|
||||
return expandHome(override), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -579,7 +579,7 @@ func TestRewriteWorkspacePath(t *testing.T) {
|
|||
func TestRunDryRun(t *testing.T) {
|
||||
t.Parallel()
|
||||
openclawHome := t.TempDir()
|
||||
picoClawHome := t.TempDir()
|
||||
dragonscaleHome := t.TempDir()
|
||||
|
||||
wsDir := filepath.Join(openclawHome, "workspace")
|
||||
os.MkdirAll(wsDir, 0755)
|
||||
|
|
@ -597,9 +597,9 @@ func TestRunDryRun(t *testing.T) {
|
|||
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
|
||||
|
||||
opts := Options{
|
||||
DryRun: true,
|
||||
OpenClawHome: openclawHome,
|
||||
PicoClawHome: picoClawHome,
|
||||
DryRun: true,
|
||||
OpenClawHome: openclawHome,
|
||||
DragonScaleHome: dragonscaleHome,
|
||||
}
|
||||
|
||||
result, err := Run(opts)
|
||||
|
|
@ -607,11 +607,11 @@ func TestRunDryRun(t *testing.T) {
|
|||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
|
||||
picoWs := filepath.Join(picoClawHome, "workspace")
|
||||
if _, err := os.Stat(filepath.Join(picoWs, "SOUL.md")); !os.IsNotExist(err) {
|
||||
dragonWs := filepath.Join(dragonscaleHome, "workspace")
|
||||
if _, err := os.Stat(filepath.Join(dragonWs, "SOUL.md")); !os.IsNotExist(err) {
|
||||
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")
|
||||
}
|
||||
|
||||
|
|
@ -621,7 +621,7 @@ func TestRunDryRun(t *testing.T) {
|
|||
func TestRunFullMigration(t *testing.T) {
|
||||
t.Parallel()
|
||||
openclawHome := t.TempDir()
|
||||
picoClawHome := t.TempDir()
|
||||
dragonscaleHome := t.TempDir()
|
||||
|
||||
wsDir := filepath.Join(openclawHome, "workspace")
|
||||
os.MkdirAll(wsDir, 0755)
|
||||
|
|
@ -653,9 +653,9 @@ func TestRunFullMigration(t *testing.T) {
|
|||
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
|
||||
|
||||
opts := Options{
|
||||
Force: true,
|
||||
OpenClawHome: openclawHome,
|
||||
PicoClawHome: picoClawHome,
|
||||
Force: true,
|
||||
OpenClawHome: openclawHome,
|
||||
DragonScaleHome: dragonscaleHome,
|
||||
}
|
||||
|
||||
result, err := Run(opts)
|
||||
|
|
@ -663,9 +663,9 @@ func TestRunFullMigration(t *testing.T) {
|
|||
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 {
|
||||
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")
|
||||
}
|
||||
|
||||
agentsData, err := os.ReadFile(filepath.Join(picoWs, "AGENTS.md"))
|
||||
agentsData, err := os.ReadFile(filepath.Join(dragonWs, "AGENTS.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("reading AGENTS.md: %v", err)
|
||||
}
|
||||
|
|
@ -681,7 +681,7 @@ func TestRunFullMigration(t *testing.T) {
|
|||
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 {
|
||||
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))
|
||||
}
|
||||
|
||||
picoConfig, err := config.LoadConfig(filepath.Join(picoClawHome, "config.json"))
|
||||
dragonConfig, err := config.LoadConfig(filepath.Join(dragonscaleHome, "config.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("loading DragonScale config: %v", err)
|
||||
}
|
||||
if picoConfig.Providers.Anthropic.APIKey != "sk-ant-migrate-test" {
|
||||
t.Errorf("Anthropic.APIKey = %q, want %q", picoConfig.Providers.Anthropic.APIKey, "sk-ant-migrate-test")
|
||||
if dragonConfig.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" {
|
||||
t.Errorf("OpenRouter.APIKey = %q, want %q", picoConfig.Providers.OpenRouter.APIKey, "sk-or-migrate-test")
|
||||
if dragonConfig.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")
|
||||
}
|
||||
if picoConfig.Channels.Telegram.Token != "tg-migrate-test" {
|
||||
t.Errorf("Telegram.Token = %q, want %q", picoConfig.Channels.Telegram.Token, "tg-migrate-test")
|
||||
if dragonConfig.Channels.Telegram.Token != "tg-migrate-test" {
|
||||
t.Errorf("Telegram.Token = %q, want %q", dragonConfig.Channels.Telegram.Token, "tg-migrate-test")
|
||||
}
|
||||
|
||||
if result.FilesCopied < 3 {
|
||||
|
|
@ -720,8 +720,8 @@ func TestRunFullMigration(t *testing.T) {
|
|||
func TestRunOpenClawNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
opts := Options{
|
||||
OpenClawHome: "/nonexistent/path/to/openclaw",
|
||||
PicoClawHome: t.TempDir(),
|
||||
OpenClawHome: "/nonexistent/path/to/openclaw",
|
||||
DragonScaleHome: t.TempDir(),
|
||||
}
|
||||
|
||||
_, err := Run(opts)
|
||||
|
|
@ -787,7 +787,7 @@ func TestCopyFile(t *testing.T) {
|
|||
func TestRunConfigOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
openclawHome := t.TempDir()
|
||||
picoClawHome := t.TempDir()
|
||||
dragonscaleHome := t.TempDir()
|
||||
|
||||
wsDir := filepath.Join(openclawHome, "workspace")
|
||||
os.MkdirAll(wsDir, 0755)
|
||||
|
|
@ -804,10 +804,10 @@ func TestRunConfigOnly(t *testing.T) {
|
|||
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
|
||||
|
||||
opts := Options{
|
||||
Force: true,
|
||||
ConfigOnly: true,
|
||||
OpenClawHome: openclawHome,
|
||||
PicoClawHome: picoClawHome,
|
||||
Force: true,
|
||||
ConfigOnly: true,
|
||||
OpenClawHome: openclawHome,
|
||||
DragonScaleHome: dragonscaleHome,
|
||||
}
|
||||
|
||||
result, err := Run(opts)
|
||||
|
|
@ -819,8 +819,8 @@ func TestRunConfigOnly(t *testing.T) {
|
|||
t.Error("config should have been migrated")
|
||||
}
|
||||
|
||||
picoWs := filepath.Join(picoClawHome, "workspace")
|
||||
if _, err := os.Stat(filepath.Join(picoWs, "SOUL.md")); !os.IsNotExist(err) {
|
||||
dragonWs := filepath.Join(dragonscaleHome, "workspace")
|
||||
if _, err := os.Stat(filepath.Join(dragonWs, "SOUL.md")); !os.IsNotExist(err) {
|
||||
t.Error("config-only should not copy workspace files")
|
||||
}
|
||||
}
|
||||
|
|
@ -828,7 +828,7 @@ func TestRunConfigOnly(t *testing.T) {
|
|||
func TestRunWorkspaceOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
openclawHome := t.TempDir()
|
||||
picoClawHome := t.TempDir()
|
||||
dragonscaleHome := t.TempDir()
|
||||
|
||||
wsDir := filepath.Join(openclawHome, "workspace")
|
||||
os.MkdirAll(wsDir, 0755)
|
||||
|
|
@ -845,10 +845,10 @@ func TestRunWorkspaceOnly(t *testing.T) {
|
|||
os.WriteFile(filepath.Join(openclawHome, "openclaw.json"), data, 0644)
|
||||
|
||||
opts := Options{
|
||||
Force: true,
|
||||
WorkspaceOnly: true,
|
||||
OpenClawHome: openclawHome,
|
||||
PicoClawHome: picoClawHome,
|
||||
Force: true,
|
||||
WorkspaceOnly: true,
|
||||
OpenClawHome: openclawHome,
|
||||
DragonScaleHome: dragonscaleHome,
|
||||
}
|
||||
|
||||
result, err := Run(opts)
|
||||
|
|
@ -860,8 +860,8 @@ func TestRunWorkspaceOnly(t *testing.T) {
|
|||
t.Error("workspace-only should not migrate config")
|
||||
}
|
||||
|
||||
picoWs := filepath.Join(picoClawHome, "workspace")
|
||||
soulData, err := os.ReadFile(filepath.Join(picoWs, "SOUL.md"))
|
||||
dragonWs := filepath.Join(dragonscaleHome, "workspace")
|
||||
soulData, err := os.ReadFile(filepath.Join(dragonWs, "SOUL.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("reading SOUL.md: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/ZanzyTHEbar/dragonscale/pkg/agent"
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg/bus"
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg/config"
|
||||
picofantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy"
|
||||
dragonfantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy"
|
||||
)
|
||||
|
||||
type OutboundMode string
|
||||
|
|
@ -72,13 +72,13 @@ func Bootstrap(parent context.Context, cfg *config.Config, opts BootstrapOptions
|
|||
|
||||
ctx, cancel := withExecutionContext(parent, opts.Timeout)
|
||||
|
||||
provider, err := picofantasy.CreateProvider(cfg)
|
||||
provider, err := dragonfantasy.CreateProvider(cfg)
|
||||
if err != nil {
|
||||
cancel()
|
||||
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 {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("model error: %w", err)
|
||||
|
|
|
|||
|
|
@ -6,10 +6,15 @@ import (
|
|||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/ZanzyTHEbar/dragonscale/pkg"
|
||||
"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 {
|
||||
BaseConfigPath string
|
||||
|
|
@ -18,24 +23,51 @@ type LoadConfigOptions struct {
|
|||
}
|
||||
|
||||
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.
|
||||
if xdgPath, err := config.DefaultConfigPath(); err == nil {
|
||||
xdgPath, err := config.DefaultConfigPath()
|
||||
if err == nil {
|
||||
if _, statErr := os.Stat(xdgPath); statErr == nil {
|
||||
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.
|
||||
if xdgPath, err := config.DefaultConfigPath(); err == nil {
|
||||
if err == nil {
|
||||
return xdgPath
|
||||
}
|
||||
return legacy
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func LoadResolvedConfig(opts LoadConfigOptions) (*config.Config, error) {
|
||||
|
|
|
|||
|
|
@ -44,18 +44,139 @@ func TestResolveBaseConfigPath_PrefersXDGOverLegacy(t *testing.T) {
|
|||
assert.Empty(t, cmp.Diff(xdgPath, got))
|
||||
}
|
||||
|
||||
func TestResolveBaseConfigPath_FallsBackToLegacyWhenXDGMissing(t *testing.T) {
|
||||
func TestResolveBaseConfigPath_FallsBackToXDGWhenLegacyOnlyExists(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
xdg := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("XDG_CONFIG_HOME", xdg)
|
||||
|
||||
xdgPath := filepath.Join(xdg, pkg.NAME, "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.WriteFile(xdgPath, []byte(`{}`), 0o644))
|
||||
require.NoError(t, os.WriteFile(legacyPath, []byte(`{}`), 0o644))
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ func TestSyncAll_InsertsNewFiles(t *testing.T) {
|
|||
"AGENT.md": "# Agent\nYou are helpful.",
|
||||
"SOUL.md": "# Soul\nCurious and kind.",
|
||||
"USER.md": "# User\nName: Alice",
|
||||
"IDENTITY.md": "# Identity\nPicoClaw v1",
|
||||
"IDENTITY.md": "# Identity\nDragonScale v1",
|
||||
})
|
||||
|
||||
store := newMockStore()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue