WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,468 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/application/repository"
|
||||
apperrors "github.com/Tencent/WeKnora/internal/errors"
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestListCatalogGroupsInstallsByDefinition(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open("file:catalog-list?mode=memory&cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(
|
||||
&types.TenantSkillEntity{}, &types.TenantSkillCatalogEntity{},
|
||||
&types.TenantSkillSnapshotEntity{}, &types.TenantUserEnvVar{},
|
||||
))
|
||||
repo := repository.NewTenantSkillRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
require.NoError(t, repo.CreateCatalog(ctx, &types.TenantSkillCatalogEntity{
|
||||
ID: "cat-pdf", TenantID: 7, Name: "pdf", Description: "extract",
|
||||
}))
|
||||
require.NoError(t, repo.CreateSkill(ctx, &types.TenantSkillEntity{
|
||||
ID: "sk-1", TenantID: 7, SandboxConfigID: "cfg-a", CatalogID: "cat-pdf",
|
||||
Name: "pdf", Status: types.SkillStatusReady, Enabled: true,
|
||||
}))
|
||||
require.NoError(t, repo.CreateSkill(ctx, &types.TenantSkillEntity{
|
||||
ID: "sk-2", TenantID: 7, SandboxConfigID: "cfg-b", CatalogID: "cat-pdf",
|
||||
Name: "pdf", Status: types.SkillStatusInstalling, Enabled: true,
|
||||
}))
|
||||
|
||||
svc := NewTenantSkillService(repo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
list, err := svc.ListCatalog(ctx, 7)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list, 1)
|
||||
require.Equal(t, "pdf", list[0].Name)
|
||||
require.Len(t, list[0].Installations, 2)
|
||||
}
|
||||
|
||||
func TestResolveCatalogFindsLegacySkillID(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open("file:catalog-legacy?mode=memory&cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(
|
||||
&types.TenantSkillEntity{}, &types.TenantSkillCatalogEntity{},
|
||||
&types.TenantSkillSnapshotEntity{}, &types.TenantUserEnvVar{},
|
||||
))
|
||||
repo := repository.NewTenantSkillRepository(db)
|
||||
ctx := context.Background()
|
||||
require.NoError(t, repo.CreateSkill(ctx, &types.TenantSkillEntity{
|
||||
ID: "sk-old", TenantID: 7, SandboxConfigID: "cfg-a",
|
||||
Name: "pdf", BundleRef: "local://7/tenant-skills/sk-old.zip",
|
||||
Status: types.SkillStatusReady, Enabled: true,
|
||||
}))
|
||||
|
||||
svc := NewTenantSkillService(repo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
cat, err := svc.resolveCatalog(ctx, 7, "sk-old")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cat)
|
||||
require.Equal(t, "pdf", cat.Name)
|
||||
require.Equal(t, "local://7/tenant-skills/sk-old.zip", cat.BundleRef)
|
||||
}
|
||||
|
||||
func catalogTestRepo(t *testing.T, dsn string) repository.TenantSkillRepository {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(
|
||||
&types.TenantSkillEntity{}, &types.TenantSkillCatalogEntity{},
|
||||
&types.TenantSkillSnapshotEntity{}, &types.TenantUserEnvVar{},
|
||||
))
|
||||
return repository.NewTenantSkillRepository(db)
|
||||
}
|
||||
|
||||
func TestListCatalogShowsInstallsWhoseCatalogWasDeleted(t *testing.T) {
|
||||
repo := catalogTestRepo(t, "file:catalog-orphan?mode=memory&cache=shared")
|
||||
ctx := context.Background()
|
||||
require.NoError(t, repo.CreateCatalog(ctx, &types.TenantSkillCatalogEntity{
|
||||
ID: "cat-gone", TenantID: 7, Name: "pdf",
|
||||
}))
|
||||
require.NoError(t, repo.CreateSkill(ctx, &types.TenantSkillEntity{
|
||||
ID: "sk-1", TenantID: 7, SandboxConfigID: "cfg-a", CatalogID: "cat-gone",
|
||||
Name: "pdf", Status: types.SkillStatusReady, Enabled: true,
|
||||
}))
|
||||
require.NoError(t, repo.DeleteCatalog(ctx, 7, "cat-gone"))
|
||||
|
||||
svc := NewTenantSkillService(repo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
list, err := svc.ListCatalog(ctx, 7)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, list, 1)
|
||||
require.Equal(t, "pdf", list[0].Name)
|
||||
require.Len(t, list[0].Installations, 1)
|
||||
require.Equal(t, "sk-1", list[0].Installations[0].SkillID)
|
||||
}
|
||||
|
||||
func TestDeleteCatalogRefusesWhileARemovalIsInFlight(t *testing.T) {
|
||||
repo := catalogTestRepo(t, "file:catalog-removing?mode=memory&cache=shared")
|
||||
ctx := context.Background()
|
||||
require.NoError(t, repo.CreateCatalog(ctx, &types.TenantSkillCatalogEntity{
|
||||
ID: "cat-pdf", TenantID: 7, Name: "pdf",
|
||||
}))
|
||||
require.NoError(t, repo.CreateSkill(ctx, &types.TenantSkillEntity{
|
||||
ID: "sk-1", TenantID: 7, SandboxConfigID: "cfg-a", CatalogID: "cat-pdf",
|
||||
Name: "pdf", Status: types.SkillStatusRemoving, Enabled: true,
|
||||
}))
|
||||
|
||||
svc := NewTenantSkillService(repo, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
err := svc.DeleteCatalog(ctx, 7, "cat-pdf")
|
||||
require.Error(t, err)
|
||||
appErr, ok := apperrors.IsAppError(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 409, appErr.HTTPCode)
|
||||
}
|
||||
|
||||
func TestInstallSkillStoresTheArchiveOnTheCatalog(t *testing.T) {
|
||||
fx := newInstallFixture(t)
|
||||
ctx := context.Background()
|
||||
archive := zipBundle(t, map[string]string{
|
||||
"SKILL.md": validSkillMD,
|
||||
"scripts/extract.py": "print('hi')\n",
|
||||
})
|
||||
|
||||
id, err := fx.svc.InstallSkill(ctx, 7, "cfg-1", archive)
|
||||
require.NoError(t, err)
|
||||
|
||||
cat, err := fx.skillRepo.GetCatalogByName(ctx, 7, "pdf-tools")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cat)
|
||||
require.NotEmpty(t, cat.BundleRef)
|
||||
require.Equal(t, 1, fx.savedBundles)
|
||||
|
||||
skill, err := fx.skillRepo.GetSkill(ctx, 7, "cfg-1", id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, cat.ID, skill.CatalogID)
|
||||
require.Empty(t, skill.BundleRef,
|
||||
"the install row must not copy the catalog object; uninstall must not be able to delete it")
|
||||
files, err := fx.svc.ListSkillFiles(ctx, 7, "cfg-1", id)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, files)
|
||||
}
|
||||
|
||||
func TestRemovingLastSandboxInstallKeepsTheCatalogArchive(t *testing.T) {
|
||||
fx := newInstallFixture(t)
|
||||
ctx := context.Background()
|
||||
archive := zipBundle(t, map[string]string{
|
||||
"SKILL.md": validSkillMD,
|
||||
"scripts/extract.py": "print('hi')\n",
|
||||
})
|
||||
id, err := fx.svc.InstallSkill(ctx, 7, "cfg-1", archive)
|
||||
require.NoError(t, err)
|
||||
skill, err := fx.skillRepo.GetSkill(ctx, 7, "cfg-1", id)
|
||||
require.NoError(t, err)
|
||||
skill.Status = types.SkillStatusRemoving
|
||||
require.NoError(t, fx.skillRepo.UpdateSkill(ctx, skill))
|
||||
|
||||
require.NoError(t, fx.svc.runRemove(ctx, 7, "cfg-1", id))
|
||||
|
||||
cat, err := fx.skillRepo.GetCatalogByName(ctx, 7, "pdf-tools")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cat)
|
||||
files, err := fx.svc.ListCatalogFiles(ctx, 7, cat.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, files)
|
||||
require.Empty(t, fx.deletedBundles,
|
||||
"uninstalling from the last sandbox must not delete the definition zip")
|
||||
gone, err := fx.skillRepo.GetSkill(ctx, 7, "cfg-1", id)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, gone)
|
||||
|
||||
require.NoError(t, fx.svc.DeleteCatalog(ctx, 7, cat.ID))
|
||||
require.Equal(t, []string{cat.BundleRef}, fx.deletedBundles,
|
||||
"only deleting the skill from the catalog drops the stored zip")
|
||||
}
|
||||
|
||||
func TestUpsertCatalogDoesNotStampSHAWhenStoreFails(t *testing.T) {
|
||||
fx := newInstallFixture(t)
|
||||
fx.saveErr = errors.New("disk full")
|
||||
ctx := context.Background()
|
||||
oldSHA := strings.Repeat("c", 64)
|
||||
require.NoError(t, fx.skillRepo.CreateCatalog(ctx, &types.TenantSkillCatalogEntity{
|
||||
ID: "cat-1", TenantID: 7, Name: "pdf-tools",
|
||||
BundleRef: "file://old.zip", BundleSHA256: oldSHA,
|
||||
}))
|
||||
archive := zipBundle(t, map[string]string{"SKILL.md": validSkillMD})
|
||||
bundle, err := ParseSkillBundle(archive)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, oldSHA, bundle.SHA256)
|
||||
|
||||
got, err := fx.svc.upsertCatalogFromBundle(ctx, 7, bundle, archive, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, oldSHA, got.BundleSHA256)
|
||||
require.Equal(t, "file://old.zip", got.BundleRef)
|
||||
}
|
||||
|
||||
func TestInstallCatalogToConfigsReportsPartialFailure(t *testing.T) {
|
||||
fx := newInstallFixture(t)
|
||||
ctx := context.Background()
|
||||
archive := zipBundle(t, map[string]string{
|
||||
"SKILL.md": validSkillMD,
|
||||
"scripts/extract.py": "print('hi')\n",
|
||||
})
|
||||
cat, err := fx.svc.RegisterCatalogFromArchive(ctx, 7, archive)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cat)
|
||||
|
||||
result, err := fx.svc.InstallCatalogToConfigs(ctx, 7, cat.ID, []string{"cfg-1", "missing"})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Contains(t, result.Installs, "cfg-1")
|
||||
require.Equal(t, "sandbox config not found", result.Errors["missing"])
|
||||
require.Equal(t, 1, fx.savedBundles,
|
||||
"installing onto a sandbox must reuse the zip already stored on the catalog")
|
||||
}
|
||||
|
||||
func TestRegisterCatalogReplacesThePreviousZip(t *testing.T) {
|
||||
fx := newInstallFixture(t)
|
||||
ctx := context.Background()
|
||||
first := zipBundle(t, map[string]string{
|
||||
"SKILL.md": validSkillMD,
|
||||
"scripts/extract.py": "print('v1')\n",
|
||||
})
|
||||
cat, err := fx.svc.RegisterCatalogFromArchive(ctx, 7, first)
|
||||
require.NoError(t, err)
|
||||
firstRef := cat.BundleRef
|
||||
require.Equal(t, 1, fx.savedBundles)
|
||||
|
||||
second := zipBundle(t, map[string]string{
|
||||
"SKILL.md": validSkillMD,
|
||||
"scripts/extract.py": "print('v2')\n",
|
||||
})
|
||||
cat, err = fx.svc.RegisterCatalogFromArchive(ctx, 7, second)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, fx.savedBundles)
|
||||
require.NotEqual(t, firstRef, cat.BundleRef)
|
||||
require.Equal(t, []string{firstRef}, fx.deletedBundles,
|
||||
"replacing the definition zip must drop the previous object")
|
||||
}
|
||||
|
||||
// A re-register that cannot commit the row has to be a no-op. Retiring the
|
||||
// previous object before the write would leave the stored definition naming a
|
||||
// ref that no longer resolves, which takes its files down for good — far worse
|
||||
// than the failed upload the caller asked about.
|
||||
func TestRegisterCatalogKeepsThePreviousZipWhenTheRowFailsToCommit(t *testing.T) {
|
||||
fx := newInstallFixture(t)
|
||||
ctx := context.Background()
|
||||
first := zipBundle(t, map[string]string{
|
||||
"SKILL.md": validSkillMD,
|
||||
"scripts/extract.py": "print('v1')\n",
|
||||
})
|
||||
cat, err := fx.svc.RegisterCatalogFromArchive(ctx, 7, first)
|
||||
require.NoError(t, err)
|
||||
firstRef := cat.BundleRef
|
||||
|
||||
fx.skillRepo.updateCatalogErr = errors.New("database unavailable")
|
||||
second := zipBundle(t, map[string]string{
|
||||
"SKILL.md": validSkillMD,
|
||||
"scripts/extract.py": "print('v2')\n",
|
||||
})
|
||||
_, err = fx.svc.RegisterCatalogFromArchive(ctx, 7, second)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Empty(t, fx.deletedBundles,
|
||||
"the stored definition still names the previous archive, so it must survive")
|
||||
|
||||
stored, err := fx.skillRepo.GetCatalog(ctx, 7, cat.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, firstRef, stored.BundleRef)
|
||||
require.NotEmpty(t, fx.storedBundles[firstRef],
|
||||
"the ref the row still names has to resolve to bytes")
|
||||
}
|
||||
|
||||
// Two first-time registrations of one name race the unique index. The loser's
|
||||
// object is keyed by the row ID that was never written, so nothing will ever
|
||||
// name it again and the retry stores its own copy under the winner's key.
|
||||
func TestRegisterCatalogDropsTheObjectOfTheRowThatLostTheNameRace(t *testing.T) {
|
||||
fx := newInstallFixture(t)
|
||||
ctx := context.Background()
|
||||
archive := zipBundle(t, map[string]string{"SKILL.md": validSkillMD})
|
||||
|
||||
fx.skillRepo.createCatalogHook = func(e *types.TenantSkillCatalogEntity) error {
|
||||
// The winner appears between this caller's name lookup and its insert.
|
||||
fx.skillRepo.catalogs["cat-winner"] = &types.TenantSkillCatalogEntity{
|
||||
ID: "cat-winner", TenantID: e.TenantID, Name: e.Name,
|
||||
}
|
||||
fx.skillRepo.createCatalogHook = nil
|
||||
return errors.New("duplicate key value violates unique constraint")
|
||||
}
|
||||
|
||||
cat, err := fx.svc.RegisterCatalogFromArchive(ctx, 7, archive)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "cat-winner", cat.ID)
|
||||
require.Equal(t, 2, fx.savedBundles, "the retry stores the archive under the winner's key")
|
||||
require.Equal(t, []string{"file://bundle-1.zip"}, fx.deletedBundles,
|
||||
"the object of the row that was never written is unreachable and must go")
|
||||
require.NotEqual(t, "file://bundle-1.zip", cat.BundleRef)
|
||||
}
|
||||
|
||||
// The sandbox that installed v1 goes on running v1 no matter what the
|
||||
// definition says next, so the archive it was built from has to outlive the
|
||||
// re-registration that supersedes it. Deleting it here is what would take
|
||||
// read_skill and the admin file browser down for an install that works.
|
||||
func TestRegisterCatalogKeepsTheReplacedZipForInstallsStillOnIt(t *testing.T) {
|
||||
fx := newInstallFixture(t)
|
||||
ctx := context.Background()
|
||||
first := zipBundle(t, map[string]string{
|
||||
"SKILL.md": validSkillMD,
|
||||
"scripts/extract.py": "print('v1')\n",
|
||||
})
|
||||
firstBundle, err := ParseSkillBundle(first)
|
||||
require.NoError(t, err)
|
||||
skillID, err := fx.svc.InstallSkill(ctx, 7, "cfg-1", first)
|
||||
require.NoError(t, err)
|
||||
installed, err := fx.skillRepo.GetSkill(ctx, 7, "cfg-1", skillID)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, installed.BundleRef, "a fresh install reads the definition's copy")
|
||||
firstRef := fx.catalogRefFor(t, installed.CatalogID)
|
||||
|
||||
second := zipBundle(t, map[string]string{
|
||||
"SKILL.md": validSkillMD,
|
||||
"scripts/extract.py": "print('v2')\n",
|
||||
})
|
||||
_, err = fx.svc.RegisterCatalogFromArchive(ctx, 7, second)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Empty(t, fx.deletedBundles,
|
||||
"an archive a live install was built from must survive the definition moving on")
|
||||
installed, err = fx.skillRepo.GetSkill(ctx, 7, "cfg-1", skillID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, firstRef, installed.BundleRef,
|
||||
"the install now names the archive itself, so nothing has to remember its version")
|
||||
require.Equal(t, firstBundle.SHA256, installed.BundleSHA256)
|
||||
|
||||
// The point of keeping the object: the file browser still answers with the
|
||||
// tree this sandbox actually has.
|
||||
files, err := fx.svc.ListSkillFiles(ctx, 7, "cfg-1", skillID)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, files)
|
||||
content, err := fx.svc.ReadSkillFile(ctx, 7, "cfg-1", skillID, "scripts/extract.py")
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, content.Content, "v1")
|
||||
}
|
||||
|
||||
// A stamp that fails must not move the definition: the install still has an
|
||||
// empty BundleRef, so committing the new catalog object would make read_skill
|
||||
// and the file browser follow v2 while this sandbox is still running v1.
|
||||
func TestRegisterCatalogDoesNotMoveTheDefinitionWhenPinFails(t *testing.T) {
|
||||
fx := newInstallFixture(t)
|
||||
ctx := context.Background()
|
||||
first := zipBundle(t, map[string]string{
|
||||
"SKILL.md": validSkillMD,
|
||||
"scripts/extract.py": "print('v1')\n",
|
||||
})
|
||||
skillID, err := fx.svc.InstallSkill(ctx, 7, "cfg-1", first)
|
||||
require.NoError(t, err)
|
||||
installed, err := fx.skillRepo.GetSkill(ctx, 7, "cfg-1", skillID)
|
||||
require.NoError(t, err)
|
||||
catalogID := installed.CatalogID
|
||||
firstRef := fx.catalogRefFor(t, catalogID)
|
||||
|
||||
fx.skillRepo.updateFailsWhen = func(e *types.TenantSkillEntity) bool {
|
||||
return strings.TrimSpace(e.BundleRef) == firstRef
|
||||
}
|
||||
|
||||
second := zipBundle(t, map[string]string{
|
||||
"SKILL.md": validSkillMD,
|
||||
"scripts/extract.py": "print('v2')\n",
|
||||
})
|
||||
_, err = fx.svc.RegisterCatalogFromArchive(ctx, 7, second)
|
||||
require.Error(t, err)
|
||||
|
||||
stored, err := fx.skillRepo.GetCatalog(ctx, 7, catalogID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, firstRef, stored.BundleRef,
|
||||
"the definition must keep naming v1 when the install could not be pinned")
|
||||
installed, err = fx.skillRepo.GetSkill(ctx, 7, "cfg-1", skillID)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, installed.BundleRef)
|
||||
|
||||
content, err := fx.svc.ReadSkillFile(ctx, 7, "cfg-1", skillID, "scripts/extract.py")
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, content.Content, "v1")
|
||||
}
|
||||
|
||||
func TestRegisterCatalogSkipsSaveWhenTheSameZipIsStillStored(t *testing.T) {
|
||||
fx := newInstallFixture(t)
|
||||
ctx := context.Background()
|
||||
archive := zipBundle(t, map[string]string{"SKILL.md": validSkillMD})
|
||||
cat, err := fx.svc.RegisterCatalogFromArchive(ctx, 7, archive)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, fx.savedBundles)
|
||||
firstRef := cat.BundleRef
|
||||
|
||||
cat, err = fx.svc.RegisterCatalogFromArchive(ctx, 7, archive)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, fx.savedBundles, "the same bytes must not mint a second object")
|
||||
require.Equal(t, firstRef, cat.BundleRef)
|
||||
}
|
||||
|
||||
func TestRegisterCatalogRewritesAMissingObjectWithTheSameSHA(t *testing.T) {
|
||||
fx := newInstallFixture(t)
|
||||
ctx := context.Background()
|
||||
archive := zipBundle(t, map[string]string{"SKILL.md": validSkillMD})
|
||||
cat, err := fx.svc.RegisterCatalogFromArchive(ctx, 7, archive)
|
||||
require.NoError(t, err)
|
||||
firstRef := cat.BundleRef
|
||||
delete(fx.storedBundles, firstRef)
|
||||
|
||||
cat, err = fx.svc.RegisterCatalogFromArchive(ctx, 7, archive)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, fx.savedBundles)
|
||||
require.NotEqual(t, firstRef, cat.BundleRef)
|
||||
|
||||
files, err := fx.svc.ListCatalogFiles(ctx, 7, cat.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, files)
|
||||
}
|
||||
|
||||
// The mirror of the test above: once the last install of a pinned archive is
|
||||
// gone, nothing can reach those bytes again, so removal is what reclaims them.
|
||||
func TestRemovingThePinnedInstallReclaimsTheReplacedZip(t *testing.T) {
|
||||
fx := newInstallFixture(t)
|
||||
ctx := context.Background()
|
||||
first := zipBundle(t, map[string]string{
|
||||
"SKILL.md": validSkillMD,
|
||||
"scripts/extract.py": "print('v1')\n",
|
||||
})
|
||||
skillID, err := fx.svc.InstallSkill(ctx, 7, "cfg-1", first)
|
||||
require.NoError(t, err)
|
||||
installed, err := fx.skillRepo.GetSkill(ctx, 7, "cfg-1", skillID)
|
||||
require.NoError(t, err)
|
||||
catalogID := installed.CatalogID
|
||||
firstRef := fx.catalogRefFor(t, catalogID)
|
||||
|
||||
second := zipBundle(t, map[string]string{
|
||||
"SKILL.md": validSkillMD,
|
||||
"scripts/extract.py": "print('v2')\n",
|
||||
})
|
||||
_, err = fx.svc.RegisterCatalogFromArchive(ctx, 7, second)
|
||||
require.NoError(t, err)
|
||||
secondRef := fx.catalogRefFor(t, catalogID)
|
||||
require.NotEqual(t, firstRef, secondRef)
|
||||
|
||||
pinned, err := fx.skillRepo.GetSkill(ctx, 7, "cfg-1", skillID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, firstRef, pinned.BundleRef)
|
||||
pinned.Status = types.SkillStatusRemoving
|
||||
require.NoError(t, fx.skillRepo.UpdateSkill(ctx, pinned))
|
||||
|
||||
require.NoError(t, fx.svc.runRemove(ctx, 7, "cfg-1", skillID))
|
||||
|
||||
require.Equal(t, []string{firstRef}, fx.deletedBundles,
|
||||
"the pinned archive is reclaimed with its last reader, the definition's is not")
|
||||
}
|
||||
|
||||
func (f *installFixture) catalogRefFor(t *testing.T, catalogID string) string {
|
||||
t.Helper()
|
||||
cat, err := f.skillRepo.GetCatalog(context.Background(), 7, catalogID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, cat)
|
||||
return cat.BundleRef
|
||||
}
|
||||
Reference in New Issue
Block a user