mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
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
227 lines
10 KiB
Go
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())
|
|
})
|
|
})
|
|
})
|