yuan-lab-llm--clawmanager
71c7672545
* feat(skill-hub): add Skill Hub catalog, publish flow, and lite materialize pipeline Introduce Skill Hub for browsing, importing, publishing, and installing skills, with lite instance package materialization and runtime sync support. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(skill-hub): remove token-governance hooks from Skill Hub PR Strip validateManagedRuntimeEnvironmentOverrides, network lock policy sync, and egress proxy audit wiring that belong to the upcoming token-usage work, so the Skill Hub branch compiles independently. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(skill-hub): update migration number in materialize docs Co-authored-by: Cursor <cursoragent@cursor.com> * fix(skill-hub): make RuntimeAgentClient test stub and hub tests compile-safe Add ResyncInstanceSkills to the runtime pool handler fake client, and harden skill hub payload helpers/tests against nil storage/instance repos so go test passes. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: add Skill Hub hardening with session usage tracking and egress governance Unify Skill Hub runtime sync improvements with session-token observability, egress network policy, and admin/instance usage reporting for reopenable PR. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(skill-hub): repair CI tests and nested skill install * fix(ci): restore release deployment configuration --------- Co-authored-by: heshengran <heshengran@ieisystem.com> Co-authored-by: Cursor <cursoragent@cursor.com>
473 行
16 KiB
Go
473 行
16 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"clawreef/internal/models"
|
|
)
|
|
|
|
func TestParseRuntimeAgentSkillsReport(t *testing.T) {
|
|
payload := map[string]any{
|
|
"mode": "full",
|
|
"instances": []map[string]any{
|
|
{
|
|
"instance_id": 12,
|
|
"skills": []map[string]any{
|
|
{
|
|
"identifier": "weather",
|
|
"content_md5": "abc123",
|
|
"source": "runtime",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
reports, mode, _, err := parseRuntimeAgentSkillsReport(payload)
|
|
if err != nil {
|
|
t.Fatalf("parseRuntimeAgentSkillsReport() error = %v", err)
|
|
}
|
|
if mode != "full" {
|
|
t.Fatalf("mode = %q, want full", mode)
|
|
}
|
|
if len(reports) != 1 || reports[0].InstanceID != 12 {
|
|
t.Fatalf("unexpected reports: %#v", reports)
|
|
}
|
|
if len(reports[0].Skills) != 1 || reports[0].Skills[0].Identifier != "weather" {
|
|
t.Fatalf("unexpected skills: %#v", reports[0].Skills)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeRuntimeSkillSource(t *testing.T) {
|
|
if got := normalizeRuntimeSkillSource("runtime"); got != "discovered_in_instance" {
|
|
t.Fatalf("normalizeRuntimeSkillSource(runtime) = %q", got)
|
|
}
|
|
if got := normalizeRuntimeSkillSource("injected_by_clawmanager"); got != "injected_by_clawmanager" {
|
|
t.Fatalf("normalizeRuntimeSkillSource(injected) = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestCollectLiteSkillDirectoryFiles(t *testing.T) {
|
|
root := t.TempDir()
|
|
skillRoot := filepath.Join(root, "weather")
|
|
if err := os.MkdirAll(filepath.Join(skillRoot, "src"), 0o750); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(skillRoot, "SKILL.md"), []byte("# weather\n"), 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(skillRoot, "src", "main.py"), []byte("print('ok')\n"), 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
files, err := collectLiteSkillDirectoryFiles(skillRoot)
|
|
if err != nil {
|
|
t.Fatalf("collectLiteSkillDirectoryFiles() error = %v", err)
|
|
}
|
|
if len(files) != 2 {
|
|
t.Fatalf("files = %#v, want 2 entries", files)
|
|
}
|
|
if got := hashDirectory(files); got == "" {
|
|
t.Fatal("expected non-empty content md5")
|
|
}
|
|
}
|
|
|
|
func TestCollectLiteSkillDirectoryFilesSkipsWithoutManifest(t *testing.T) {
|
|
root := t.TempDir()
|
|
skillRoot := filepath.Join(root, "orphan")
|
|
if err := os.MkdirAll(skillRoot, 0o750); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
files, err := collectLiteSkillDirectoryFiles(skillRoot)
|
|
if err != nil {
|
|
t.Fatalf("collectLiteSkillDirectoryFiles() error = %v", err)
|
|
}
|
|
if files != nil {
|
|
t.Fatalf("files = %#v, want nil", files)
|
|
}
|
|
}
|
|
|
|
func TestDiscoverRuntimeSkillDirectoriesNestedCategory(t *testing.T) {
|
|
root := t.TempDir()
|
|
categoryRoot := filepath.Join(root, "productivity")
|
|
skillRoot := filepath.Join(categoryRoot, "my-skill")
|
|
for _, dir := range []string{skillRoot, filepath.Join(skillRoot, "src")} {
|
|
if err := os.MkdirAll(dir, 0o750); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := os.WriteFile(filepath.Join(skillRoot, "SKILL.md"), []byte("# my skill\n"), 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(skillRoot, "src", "main.py"), []byte("print('ok')\n"), 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
discoveries, err := discoverRuntimeSkillDirectories(root, runtimeSkillDiscoveryMaxDepth)
|
|
if err != nil {
|
|
t.Fatalf("discoverRuntimeSkillDirectories() error = %v", err)
|
|
}
|
|
if len(discoveries) != 1 {
|
|
t.Fatalf("discoveries = %#v, want 1 nested skill", discoveries)
|
|
}
|
|
if discoveries[0].RelativePath != "productivity/my-skill" {
|
|
t.Fatalf("RelativePath = %q, want productivity/my-skill", discoveries[0].RelativePath)
|
|
}
|
|
}
|
|
|
|
func TestDiscoverRuntimeSkillDirectoriesFlatAndNested(t *testing.T) {
|
|
root := t.TempDir()
|
|
flatRoot := filepath.Join(root, "weather")
|
|
if err := os.MkdirAll(flatRoot, 0o750); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(flatRoot, "SKILL.md"), []byte("# weather\n"), 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
discoveries, err := discoverRuntimeSkillDirectories(root, runtimeSkillDiscoveryMaxDepth)
|
|
if err != nil {
|
|
t.Fatalf("discoverRuntimeSkillDirectories() error = %v", err)
|
|
}
|
|
if len(discoveries) != 1 || discoveries[0].RelativePath != "weather" {
|
|
t.Fatalf("discoveries = %#v, want flat weather skill", discoveries)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeSkillInstallRootOpenClawAndHermes(t *testing.T) {
|
|
workspace := "/workspaces/demo/instance-1"
|
|
hermes := &models.Instance{Type: RuntimeTypeHermes, WorkspacePath: &workspace}
|
|
openclaw := &models.Instance{Type: RuntimeTypeOpenClaw, WorkspacePath: &workspace}
|
|
|
|
hermesRoot := runtimeSkillInstallRoot(hermes)
|
|
openclawRoot := runtimeSkillInstallRoot(openclaw)
|
|
if hermesRoot != filepath.Join(workspace, ".hermes", "skills") {
|
|
t.Fatalf("hermes root = %q", hermesRoot)
|
|
}
|
|
if openclawRoot != filepath.Join(workspace, "home", ".openclaw", "workspace", "skills") {
|
|
t.Fatalf("openclaw root = %q", openclawRoot)
|
|
}
|
|
}
|
|
|
|
func TestResolveInstanceSkillSourceTypePreservesInjected(t *testing.T) {
|
|
existing := &models.InstanceSkill{SourceType: "injected_by_clawmanager"}
|
|
skill := &models.Skill{SourceType: skillSourceUploaded}
|
|
got := resolveInstanceSkillSourceType(existing, "discovered_in_instance", skill)
|
|
if got != "injected_by_clawmanager" {
|
|
t.Fatalf("resolveInstanceSkillSourceType() = %q, want injected_by_clawmanager", got)
|
|
}
|
|
}
|
|
|
|
func TestSanitizeWorkspaceRelativePath(t *testing.T) {
|
|
if got := sanitizeWorkspaceRelativePath("productivity/my-skill"); got != "productivity/my-skill" {
|
|
t.Fatalf("sanitizeWorkspaceRelativePath() = %q", got)
|
|
}
|
|
if got := sanitizeWorkspaceRelativePath("../escape"); got != "" {
|
|
t.Fatalf("sanitizeWorkspaceRelativePath(../escape) = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestIsLiteRuntimeInstanceOpenClawVariants(t *testing.T) {
|
|
liteGateway := &models.Instance{InstanceMode: InstanceModeLite, RuntimeType: RuntimeBackendGateway, Type: RuntimeTypeOpenClaw}
|
|
if !IsLiteRuntimeInstance(liteGateway) {
|
|
t.Fatal("expected gateway lite instance")
|
|
}
|
|
proDesktop := &models.Instance{InstanceMode: InstanceModePro, RuntimeType: RuntimeBackendDesktop, Type: RuntimeTypeOpenClaw}
|
|
if IsLiteRuntimeInstance(proDesktop) {
|
|
t.Fatal("expected pro desktop instance to be non-lite")
|
|
}
|
|
shellPod := &models.Instance{InstanceMode: InstanceModePro, RuntimeType: RuntimeBackendShell, Type: RuntimeTypeOpenClaw}
|
|
if IsLiteRuntimeInstance(shellPod) {
|
|
t.Fatal("expected shell pod instance to use pro agent path")
|
|
}
|
|
}
|
|
|
|
type provenanceCaptureRepoStub struct {
|
|
capturingSkillRepoStub
|
|
upserted []*models.InstanceSkill
|
|
}
|
|
|
|
func (s *provenanceCaptureRepoStub) GetBlobByContentHash(hash string) (*models.SkillBlob, error) {
|
|
for _, blob := range s.blobs {
|
|
if strings.EqualFold(strings.TrimSpace(blob.ContentHash), strings.TrimSpace(hash)) {
|
|
copy := *blob
|
|
return ©, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *provenanceCaptureRepoStub) GetVersionBySkillAndBlob(skillID, blobID int) (*models.SkillVersion, error) {
|
|
for _, version := range s.versions {
|
|
if version.SkillID == skillID && version.BlobID == blobID {
|
|
copy := *version
|
|
return ©, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *provenanceCaptureRepoStub) UpsertInstanceSkill(item *models.InstanceSkill) error {
|
|
copy := *item
|
|
s.upserted = append(s.upserted, ©)
|
|
updated := false
|
|
for i, existing := range s.instanceSkills {
|
|
if existing.InstanceID == item.InstanceID && existing.SkillID == item.SkillID {
|
|
s.instanceSkills[i] = copy
|
|
updated = true
|
|
break
|
|
}
|
|
}
|
|
if !updated {
|
|
s.instanceSkills = append(s.instanceSkills, copy)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestSyncAgentSkillsPreservesInjectedProvenanceAfterWorkspaceScan(t *testing.T) {
|
|
contentHash := "abc123def456789012345678901234"
|
|
versionID := 1
|
|
stub := &provenanceCaptureRepoStub{
|
|
capturingSkillRepoStub: capturingSkillRepoStub{
|
|
skillRepoStub: skillRepoStub{
|
|
skills: map[int]*models.Skill{
|
|
10: {
|
|
ID: 10, UserID: 1, SkillKey: "ppt-1-0-0", Name: "ppt-1.0.0",
|
|
SourceType: skillSourceUploaded, Status: skillStatusActive,
|
|
Visibility: skillVisibilityPublic, CurrentVersionID: &versionID,
|
|
},
|
|
},
|
|
blobs: map[int]*models.SkillBlob{
|
|
1: {ID: 1, ContentHash: contentHash, ObjectKey: "hub/ppt.zip", ScanStatus: "completed"},
|
|
},
|
|
versions: map[int]*models.SkillVersion{
|
|
1: {ID: 1, SkillID: 10, BlobID: 1, VersionNo: 1},
|
|
},
|
|
instanceSkills: []models.InstanceSkill{
|
|
{InstanceID: 1, SkillID: 10, SourceType: "injected_by_clawmanager", Status: "active"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
instRepo := &importTestInstanceRepo{instances: map[int]*models.Instance{
|
|
1: {ID: 1, UserID: 1, Type: RuntimeTypeHermes, InstanceMode: InstanceModePro, RuntimeType: RuntimeBackendDesktop},
|
|
}}
|
|
svc := &skillService{repo: stub, instanceRepo: instRepo, commandService: &noopInstanceCommandService{}}
|
|
err := svc.SyncAgentSkills(1, AgentSkillInventoryReportRequest{
|
|
Mode: "full",
|
|
Skills: []AgentSkillRecord{{
|
|
Identifier: "ppt-1-0-0",
|
|
ContentMD5: contentHash,
|
|
Source: "discovered_in_instance",
|
|
InstallPath: "home/.hermes/skills/ppt-1-0-0",
|
|
}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SyncAgentSkills() error = %v", err)
|
|
}
|
|
if len(stub.upserted) != 1 {
|
|
t.Fatalf("upserted %d instance skills, want 1", len(stub.upserted))
|
|
}
|
|
if stub.upserted[0].SourceType != "injected_by_clawmanager" {
|
|
t.Fatalf("SourceType = %q, want injected_by_clawmanager", stub.upserted[0].SourceType)
|
|
}
|
|
}
|
|
|
|
func TestSyncAgentSkillsReusesUploadedSkillOnWorkspaceScan(t *testing.T) {
|
|
contentHash := "abc123def456789012345678901234"
|
|
versionID := 1
|
|
stub := &provenanceCaptureRepoStub{
|
|
capturingSkillRepoStub: capturingSkillRepoStub{
|
|
skillRepoStub: skillRepoStub{
|
|
skills: map[int]*models.Skill{
|
|
10: {
|
|
ID: 10, UserID: 1, SkillKey: "ppt-1-0-0", Name: "ppt-1.0.0",
|
|
SourceType: skillSourceUploaded, Status: skillStatusActive,
|
|
Visibility: skillVisibilityPublic, CurrentVersionID: &versionID,
|
|
},
|
|
},
|
|
blobs: map[int]*models.SkillBlob{
|
|
1: {ID: 1, ContentHash: contentHash, ObjectKey: "hub/ppt.zip", ScanStatus: "completed"},
|
|
},
|
|
versions: map[int]*models.SkillVersion{
|
|
1: {ID: 1, SkillID: 10, BlobID: 1, VersionNo: 1},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
instRepo := &importTestInstanceRepo{instances: map[int]*models.Instance{
|
|
1: {ID: 1, UserID: 1, Type: RuntimeTypeHermes, InstanceMode: InstanceModeLite, RuntimeType: RuntimeBackendGateway},
|
|
}}
|
|
svc := &skillService{repo: stub, instanceRepo: instRepo, commandService: &noopInstanceCommandService{}}
|
|
err := svc.SyncAgentSkills(1, AgentSkillInventoryReportRequest{
|
|
Mode: "full",
|
|
Skills: []AgentSkillRecord{{
|
|
Identifier: "ppt-1-0-0",
|
|
ContentMD5: contentHash,
|
|
Source: "discovered_in_instance",
|
|
InstallPath: "home/.hermes/skills/ppt-1-0-0",
|
|
}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SyncAgentSkills() error = %v", err)
|
|
}
|
|
if len(stub.createdSkills) != 0 {
|
|
t.Fatalf("created %d discovered skills, want 0 reuse of uploaded skill", len(stub.createdSkills))
|
|
}
|
|
if len(stub.upserted) != 1 || stub.upserted[0].SkillID != 10 {
|
|
t.Fatalf("upserted = %#v, want instance skill for uploaded skill id 10", stub.upserted)
|
|
}
|
|
}
|
|
|
|
type recordingSkillResyncAgentClient struct {
|
|
fakeRuntimeAgentClient
|
|
calls []struct {
|
|
instanceID int
|
|
mode string
|
|
}
|
|
err error
|
|
}
|
|
|
|
func (c *recordingSkillResyncAgentClient) ResyncInstanceSkills(_ context.Context, _ string, instanceID int, mode string) error {
|
|
c.calls = append(c.calls, struct {
|
|
instanceID int
|
|
mode string
|
|
}{instanceID: instanceID, mode: mode})
|
|
return c.err
|
|
}
|
|
|
|
type inventorySyncCommandRepo struct {
|
|
commands []models.InstanceCommand
|
|
}
|
|
|
|
func (r *inventorySyncCommandRepo) Create(*models.InstanceCommand) error { return nil }
|
|
func (r *inventorySyncCommandRepo) Update(command *models.InstanceCommand) error {
|
|
for i := range r.commands {
|
|
if r.commands[i].ID == command.ID {
|
|
r.commands[i] = *command
|
|
return nil
|
|
}
|
|
}
|
|
r.commands = append(r.commands, *command)
|
|
return nil
|
|
}
|
|
func (r *inventorySyncCommandRepo) GetByID(int) (*models.InstanceCommand, error) { return nil, nil }
|
|
func (r *inventorySyncCommandRepo) GetByInstanceIdempotencyKey(int, string) (*models.InstanceCommand, error) {
|
|
return nil, nil
|
|
}
|
|
func (r *inventorySyncCommandRepo) GetNextPendingByInstance(int) (*models.InstanceCommand, error) {
|
|
return nil, nil
|
|
}
|
|
func (r *inventorySyncCommandRepo) ListByInstanceID(int, int) ([]models.InstanceCommand, error) {
|
|
return append([]models.InstanceCommand(nil), r.commands...), nil
|
|
}
|
|
func (r *inventorySyncCommandRepo) FindLatestFailedCollectSkillPackage(string) (*models.InstanceCommand, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func TestRequestLiteSkillInventorySyncUsesIncrementalWhenAgentResyncFollows(t *testing.T) {
|
|
workspace := t.TempDir()
|
|
skillsRoot := filepath.Join(workspace, "home", ".hermes", "skills")
|
|
if err := os.MkdirAll(skillsRoot, 0o750); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stub := &capturingSkillRepoStub{
|
|
skillRepoStub: skillRepoStub{
|
|
skills: map[int]*models.Skill{
|
|
10: {ID: 10, UserID: 1, SkillKey: "weather", Name: "weather", SourceType: skillSourceDiscovered, Status: skillStatusActive},
|
|
},
|
|
blobs: map[int]*models.SkillBlob{},
|
|
versions: map[int]*models.SkillVersion{},
|
|
instanceSkills: []models.InstanceSkill{
|
|
{InstanceID: 1, SkillID: 10, Status: "active", SourceType: "discovered_in_instance"},
|
|
},
|
|
},
|
|
}
|
|
instRepo := &importTestInstanceRepo{instances: map[int]*models.Instance{
|
|
1: {
|
|
ID: 1, UserID: 1, Type: RuntimeTypeHermes, InstanceMode: InstanceModeLite,
|
|
RuntimeType: RuntimeBackendGateway, WorkspacePath: &workspace, RuntimeGeneration: 1,
|
|
},
|
|
}}
|
|
cmdRepo := &inventorySyncCommandRepo{commands: []models.InstanceCommand{{
|
|
ID: 44, InstanceID: 1, CommandType: InstanceCommandTypeSyncSkillInventory, Status: instanceCommandStatusPending,
|
|
}}}
|
|
agent := &recordingSkillResyncAgentClient{}
|
|
endpoint := "http://runtime-agent"
|
|
bindingRepo := newFakeRuntimeBindingRepo()
|
|
bindingRepo.bindings[1] = &models.InstanceRuntimeBinding{
|
|
InstanceID: 1, RuntimePodID: 9, State: "running", Generation: 1,
|
|
}
|
|
podRepo := &fakeRuntimePodRepo{pods: map[int64]*models.RuntimePod{
|
|
9: {ID: 9, AgentEndpoint: &endpoint},
|
|
}}
|
|
|
|
svc := &skillService{
|
|
repo: stub,
|
|
instanceRepo: instRepo,
|
|
commandRepo: cmdRepo,
|
|
commandService: &noopInstanceCommandService{},
|
|
}
|
|
svc.ConfigureRuntimeSkillSync(bindingRepo, podRepo, agent)
|
|
|
|
if err := svc.RequestLiteSkillInventorySync(1); err != nil {
|
|
t.Fatalf("RequestLiteSkillInventorySync() error = %v", err)
|
|
}
|
|
if stub.markMissingCalls != 0 {
|
|
t.Fatalf("markMissingCalls = %d, want 0 when workspace sync is incremental", stub.markMissingCalls)
|
|
}
|
|
if stub.instanceSkills[0].Status != "active" {
|
|
t.Fatalf("status = %q, want active", stub.instanceSkills[0].Status)
|
|
}
|
|
if len(agent.calls) != 1 || agent.calls[0].mode != "full" {
|
|
t.Fatalf("resync calls = %#v, want one full resync", agent.calls)
|
|
}
|
|
if cmdRepo.commands[0].Status != instanceCommandStatusPending {
|
|
t.Fatalf("command status = %q, want pending until agent inventory arrives", cmdRepo.commands[0].Status)
|
|
}
|
|
}
|
|
|
|
func TestRequestLiteSkillInventorySyncPropagatesResyncError(t *testing.T) {
|
|
workspace := t.TempDir()
|
|
skillsRoot := filepath.Join(workspace, "home", ".hermes", "skills")
|
|
if err := os.MkdirAll(skillsRoot, 0o750); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stub := &capturingSkillRepoStub{
|
|
skillRepoStub: skillRepoStub{
|
|
skills: map[int]*models.Skill{},
|
|
blobs: map[int]*models.SkillBlob{},
|
|
versions: map[int]*models.SkillVersion{},
|
|
instanceSkills: nil,
|
|
},
|
|
}
|
|
instRepo := &importTestInstanceRepo{instances: map[int]*models.Instance{
|
|
1: {
|
|
ID: 1, UserID: 1, Type: RuntimeTypeHermes, InstanceMode: InstanceModeLite,
|
|
RuntimeType: RuntimeBackendGateway, WorkspacePath: &workspace, RuntimeGeneration: 1,
|
|
},
|
|
}}
|
|
agent := &recordingSkillResyncAgentClient{err: errors.New("resync failed")}
|
|
endpoint := "http://runtime-agent"
|
|
bindingRepo := newFakeRuntimeBindingRepo()
|
|
bindingRepo.bindings[1] = &models.InstanceRuntimeBinding{
|
|
InstanceID: 1, RuntimePodID: 9, State: "running", Generation: 1,
|
|
}
|
|
podRepo := &fakeRuntimePodRepo{pods: map[int64]*models.RuntimePod{
|
|
9: {ID: 9, AgentEndpoint: &endpoint},
|
|
}}
|
|
svc := &skillService{repo: stub, instanceRepo: instRepo, commandService: &noopInstanceCommandService{}}
|
|
svc.ConfigureRuntimeSkillSync(bindingRepo, podRepo, agent)
|
|
|
|
err := svc.RequestLiteSkillInventorySync(1)
|
|
if err == nil || !strings.Contains(err.Error(), "resync") {
|
|
t.Fatalf("error = %v, want resync failure", err)
|
|
}
|
|
}
|