Files
LocalAI/core/services/studio/agentfolder_test.go
T
Ettore Di Giacinto b15905e90f fix: restore the files that the previous commit removed by mistake
The previous commit was made on a stale base and so reverted work that
landed on master before it. This commit puts every file back to the
state of its parent's parent. The permission enforcement of the
previous commit stays.

Assisted-by: Claude:claude-sonnet-5-5 go
2026-11-03 14:33:05 +00:00

227 lines
10 KiB
Go

package studio_test
import (
"context"
"strings"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/mudler/LocalAI/core/services/studio"
"github.com/mudler/LocalAI/core/services/studio/agents"
"github.com/mudler/LocalAI/core/services/studio/files"
"github.com/mudler/LocalAI/core/services/studio/provider"
"github.com/mudler/LocalAI/core/services/studio/session"
"github.com/mudler/LocalAI/core/services/studio/store"
"github.com/mudler/LocalAI/core/services/studio/studiotest"
"github.com/mudler/LocalAI/core/services/studio/workspace"
)
// newAgentFolderService is a service with files and saved agents.
func newAgentFolderService(f *files.Service) *studio.Service {
st, err := store.NewJSON("")
Expect(err).NotTo(HaveOccurred())
ag, err := agents.NewJSON("")
Expect(err).NotTo(HaveOccurred())
svc, err := studio.New(studio.Deps{Store: st, Provider: provider.New(), StateDir: GinkgoT().TempDir(), Harness: studiotest.NewGateHarness(), Files: f, Agents: ag})
Expect(err).NotTo(HaveOccurred())
DeferCleanup(func() { _ = svc.Close(context.Background()) })
return svc
}
var _ = Describe("agent folders", func() {
var (
ctx context.Context
f *files.Service
svc *studio.Service
ws workspace.Store
user workspace.Actor
)
home := session.HomePath(alice)
mkdir := func(rel string) workspace.Node {
n, err := ws.Mkdir(ctx, user, home+rel)
Expect(err).NotTo(HaveOccurred())
return n
}
BeforeEach(func() {
ctx = context.Background()
f = newFiles()
svc = newAgentFolderService(f)
Expect(f.EnsureHome(ctx, alice)).To(Succeed())
ws, user = f.Store(), f.Actor(alice)
})
It("stores the folder by node and answers its current path after a rename and a move", func() {
acme := mkdir("/Projects/Acme")
a, err := svc.SaveAgentWithFolder(ctx, alice, agents.Agent{Name: "Scoped"}, &studio.AgentFolderWrite{Path: "/Projects/Acme", Mode: agents.FolderRead})
Expect(err).NotTo(HaveOccurred())
Expect(a.FolderID).To(Equal(acme.ID))
Expect(a.FolderMode).To(Equal(agents.FolderRead))
st, ok := svc.AgentFolder(ctx, alice, a)
Expect(ok).To(BeTrue())
Expect(st).To(Equal(studio.AgentFolderState{Path: "/Projects/Acme", Name: "Acme", Mode: "read", Effective: "read", State: studio.AgentFolderOK}))
_, err = ws.Move(ctx, user, home+"/Projects/Acme", home+"/Projects/Acme Corp")
Expect(err).NotTo(HaveOccurred())
st, _ = svc.AgentFolder(ctx, alice, a)
Expect(st.Path).To(Equal("/Projects/Acme Corp"), "after a rename")
Expect(st.Name).To(Equal("Acme Corp"))
// A move of the parent leaves no journal row on the folder itself.
_, err = ws.Move(ctx, user, home+"/Projects", home+"/Archive")
Expect(err).NotTo(HaveOccurred())
st, _ = svc.AgentFolder(ctx, alice, a)
Expect(st.Path).To(Equal("/Archive/Acme Corp"), "after a move of the parent")
Expect(st.State).To(Equal(studio.AgentFolderOK))
})
It("keeps an unnamed readable root without exposing another home's workspace path", func() {
mkdir("/Notes")
a, err := svc.SaveAgentWithFolder(ctx, alice, agents.Agent{Name: "Scoped"}, &studio.AgentFolderWrite{Path: "/Notes", Mode: agents.FolderRead})
Expect(err).NotTo(HaveOccurred())
admin := workspace.Actor{Principal: files.AdminPrincipal}
otherHome := session.HomePath("other")
_, err = ws.Mkdir(ctx, admin, otherHome)
Expect(err).NotTo(HaveOccurred())
Expect(ws.Grant(ctx, admin, alice, otherHome, workspace.ModeRead)).To(Succeed())
_, err = ws.Move(ctx, admin, home+"/Notes", otherHome+"/Notes")
Expect(err).NotTo(HaveOccurred())
st, ok := svc.AgentFolder(ctx, alice, a)
Expect(ok).To(BeTrue())
Expect(st).To(Equal(studio.AgentFolderState{Name: "Notes", Mode: "read", Effective: "read", State: studio.AgentFolderOK}))
abs, live := svc.FolderResolver().PathOf(ctx, alice, a.FolderID)
Expect(live).To(BeTrue())
Expect(abs).To(Equal(otherHome + "/Notes"))
Expect(ws.Revoke(ctx, admin, alice, otherHome)).To(Succeed())
st, _ = svc.AgentFolder(ctx, alice, a)
Expect(st).To(Equal(studio.AgentFolderState{Mode: "read", State: studio.AgentFolderMissing}))
})
It("keeps the folder when a save has no folder, and clears it with an empty path", func() {
mkdir("/Notes")
a, err := svc.SaveAgentWithFolder(ctx, alice, agents.Agent{Name: "Scoped"}, &studio.AgentFolderWrite{Path: "/Notes", Mode: agents.FolderWrite})
Expect(err).NotTo(HaveOccurred())
node := a.FolderID
// A caller that knows nothing of folders replaces the agent.
a2, err := svc.SaveAgent(ctx, alice, agents.Agent{ID: a.ID, Name: "Renamed"})
Expect(err).NotTo(HaveOccurred())
Expect(a2.Name).To(Equal("Renamed"))
Expect(a2.FolderID).To(Equal(node), "a save without the folder widened the agent")
Expect(a2.FolderMode).To(Equal(agents.FolderWrite))
a3, err := svc.SaveAgentWithFolder(ctx, alice, agents.Agent{ID: a.ID, Name: "Renamed"}, &studio.AgentFolderWrite{Path: ""})
Expect(err).NotTo(HaveOccurred())
Expect(a3.FolderID).To(BeZero())
Expect(a3.FolderMode).To(BeEmpty())
_, ok := svc.AgentFolder(ctx, alice, a3)
Expect(ok).To(BeFalse(), "an agent without a folder has no folder state")
})
It("takes the whole home as the folder", func() {
a, err := svc.SaveAgentWithFolder(ctx, alice, agents.Agent{Name: "Reader"}, &studio.AgentFolderWrite{Path: "/", Mode: agents.FolderRead})
Expect(err).NotTo(HaveOccurred())
root, err := ws.Stat(ctx, user, home)
Expect(err).NotTo(HaveOccurred())
Expect(a.FolderID).To(Equal(root.ID))
st, _ := svc.AgentFolder(ctx, alice, a)
Expect(st).To(Equal(studio.AgentFolderState{Path: "/", Mode: "read", Effective: "read", State: studio.AgentFolderOK}))
})
It("refuses a file, a missing path, Chats, LocalAI, a bad path and a bad mode as the folder", func() {
mkdir("/Docs")
_, err := ws.Put(ctx, user, home+"/Docs/a.txt", strings.NewReader("x"), workspace.PutOptions{})
Expect(err).NotTo(HaveOccurred())
mkdir("/" + session.SettingsDir + "/inner")
for _, w := range []studio.AgentFolderWrite{
{Path: "/Docs/a.txt", Mode: "read"},
{Path: "/Nope", Mode: "read"},
{Path: "/" + session.ChatsDir, Mode: "read"},
{Path: "/" + session.SettingsDir, Mode: "read"},
{Path: "/" + session.SettingsDir + "/inner", Mode: "write"},
{Path: "/../bob", Mode: "read"},
{Path: "/Docs", Mode: ""},
{Path: "/Docs", Mode: "admin"},
} {
_, err := svc.SaveAgentWithFolder(ctx, alice, agents.Agent{Name: "x"}, &w)
Expect(err).To(MatchError(studio.ErrAgentFolder), "%+v", w)
}
list, err := svc.Agents(ctx, alice)
Expect(err).NotTo(HaveOccurred())
Expect(list).To(BeEmpty(), "a refused folder saved an agent")
})
It("cannot name a folder of another user", func() {
Expect(f.EnsureHome(ctx, "bob")).To(Succeed())
_, err := ws.Mkdir(ctx, f.Actor("bob"), session.HomePath("bob")+"/Secret")
Expect(err).NotTo(HaveOccurred())
_, err = svc.SaveAgentWithFolder(ctx, alice, agents.Agent{Name: "x"}, &studio.AgentFolderWrite{Path: "/users/bob/Secret", Mode: "read"})
Expect(err).To(MatchError(studio.ErrAgentFolder), "the path is read inside alice's home")
bobs, err := ws.Stat(ctx, f.Actor("bob"), session.HomePath("bob")+"/Secret")
Expect(err).NotTo(HaveOccurred())
_, ok := svc.FolderResolver().PathOf(ctx, alice, bobs.ID)
Expect(ok).To(BeFalse(), "alice resolved bob's folder")
})
It("answers missing for a deleted folder and ok again after undo", func() {
mkdir("/Gone")
a, err := svc.SaveAgentWithFolder(ctx, alice, agents.Agent{Name: "Scoped"}, &studio.AgentFolderWrite{Path: "/Gone", Mode: agents.FolderRead})
Expect(err).NotTo(HaveOccurred())
Expect(ws.Delete(ctx, user, home+"/Gone")).To(Succeed())
st, ok := svc.AgentFolder(ctx, alice, a)
Expect(ok).To(BeTrue())
Expect(st).To(Equal(studio.AgentFolderState{Mode: "read", State: studio.AgentFolderMissing}))
hist, err := ws.History(ctx, user, workspace.HistoryQuery{Path: home, Limit: 1})
Expect(err).NotTo(HaveOccurred())
_, _, err = ws.Undo(ctx, user, hist[0].Rev, workspace.UndoOptions{})
Expect(err).NotTo(HaveOccurred())
st, _ = svc.AgentFolder(ctx, alice, a)
Expect(st.State).To(Equal(studio.AgentFolderOK), "after undo")
Expect(st.Path).To(Equal("/Gone"))
})
It("resolves several agents with one call", func() {
mkdir("/A")
mkdir("/B")
a, _ := svc.SaveAgentWithFolder(ctx, alice, agents.Agent{Name: "a"}, &studio.AgentFolderWrite{Path: "/A", Mode: "read"})
b, _ := svc.SaveAgentWithFolder(ctx, alice, agents.Agent{Name: "b"}, &studio.AgentFolderWrite{Path: "/B", Mode: "write"})
plain, _ := svc.SaveAgent(ctx, alice, agents.Agent{Name: "plain"})
unset, err := svc.SaveAgent(ctx, alice, agents.Agent{Name: "unset", FolderMode: agents.FolderRead})
Expect(err).NotTo(HaveOccurred())
got := svc.AgentFolders(ctx, alice, []agents.Agent{a, b, plain, unset})
Expect(got).To(HaveLen(3))
Expect(got[a.ID].Path).To(Equal("/A"))
Expect(got[b.ID].Path).To(Equal("/B"))
Expect(got[unset.ID]).To(Equal(studio.AgentFolderState{Mode: "read", State: studio.AgentFolderUnset}))
})
Describe("import", func() {
It("resolves the folder of the file in the importer's home", func() {
acme := mkdir("/Projects/Acme")
a, err := svc.ImportAgent(ctx, alice, agents.Agent{Name: "Imported", FolderID: 999, FolderMode: "write"}, &studio.AgentFolderWrite{Path: "/Projects/Acme", Mode: "read"})
Expect(err).NotTo(HaveOccurred())
Expect(a.FolderID).To(Equal(acme.ID))
Expect(a.FolderMode).To(Equal("read"))
})
It("saves a folder that is not there as unset, never as no folder", func() {
for i, w := range []studio.AgentFolderWrite{{Path: "/Projects/Acme", Mode: "read"}, {Path: "", Mode: "write"}, {Path: "/" + session.ChatsDir, Mode: "read"}} {
a, err := svc.ImportAgent(ctx, alice, agents.Agent{Name: "Imported " + string(rune('a'+i))}, &w)
Expect(err).NotTo(HaveOccurred(), "%+v", w)
Expect(a.FolderID).To(BeZero())
Expect(a.FolderMode).To(Equal(w.Mode), "%+v", w)
}
})
It("refuses a folder without a valid mode and ignores the node of the file", func() {
_, err := svc.ImportAgent(ctx, alice, agents.Agent{Name: "x"}, &studio.AgentFolderWrite{Path: "/A"})
Expect(err).To(MatchError(studio.ErrAgentFolder))
a, err := svc.ImportAgent(ctx, alice, agents.Agent{Name: "y", FolderID: 5, FolderMode: "read"}, nil)
Expect(err).NotTo(HaveOccurred())
Expect(a.FolderID).To(BeZero())
Expect(a.FolderMode).To(BeEmpty())
})
})
})