diff --git a/internal/api/docs/openapi.yaml b/internal/api/docs/openapi.yaml index f32c9a76..77721c82 100644 --- a/internal/api/docs/openapi.yaml +++ b/internal/api/docs/openapi.yaml @@ -1192,7 +1192,7 @@ components: properties: bricks: items: - $ref: '#/components/schemas/BrickInstance' + $ref: '#/components/schemas/BrickInstanceListItem' nullable: true type: array type: object @@ -1380,6 +1380,33 @@ components: for backward compatibility.' type: object type: object + BrickInstanceListItem: + properties: + author: + type: string + category: + type: string + config_variables: + items: + $ref: '#/components/schemas/BrickConfigVariable' + type: array + id: + type: string + model: + type: string + name: + type: string + require_model: + type: boolean + status: + type: string + variables: + additionalProperties: + type: string + description: 'Deprecated: use config_variables instead. This field is kept + for backward compatibility.' + type: object + type: object BrickListItem: properties: author: diff --git a/internal/e2e/client/client.gen.go b/internal/e2e/client/client.gen.go index 496680c9..25472727 100644 --- a/internal/e2e/client/client.gen.go +++ b/internal/e2e/client/client.gen.go @@ -66,7 +66,7 @@ type AIModelsListResult struct { // AppBrickInstancesResult defines model for AppBrickInstancesResult. type AppBrickInstancesResult struct { - Bricks *[]BrickInstance `json:"bricks"` + Bricks *[]BrickInstanceListItem `json:"bricks"` } // AppDetailedBrick defines model for AppDetailedBrick. @@ -174,6 +174,21 @@ type BrickInstance struct { Variables *map[string]string `json:"variables,omitempty"` } +// BrickInstanceListItem defines model for BrickInstanceListItem. +type BrickInstanceListItem struct { + Author *string `json:"author,omitempty"` + Category *string `json:"category,omitempty"` + ConfigVariables *[]BrickConfigVariable `json:"config_variables,omitempty"` + Id *string `json:"id,omitempty"` + Model *string `json:"model,omitempty"` + Name *string `json:"name,omitempty"` + RequireModel *bool `json:"require_model,omitempty"` + Status *string `json:"status,omitempty"` + + // Variables Deprecated: use config_variables instead. This field is kept for backward compatibility. + Variables *map[string]string `json:"variables,omitempty"` +} + // BrickListItem defines model for BrickListItem. type BrickListItem struct { Author *string `json:"author,omitempty"` diff --git a/internal/e2e/daemon/bricks_instance_test.go b/internal/e2e/daemon/bricks_instance_test.go index 0a6375bc..1b8ee7c5 100644 --- a/internal/e2e/daemon/bricks_instance_test.go +++ b/internal/e2e/daemon/bricks_instance_test.go @@ -97,11 +97,20 @@ func TestGetAppBrickInstances(t *testing.T) { var actualBody models.ErrorResponse createResp, httpClient := setupTestApp(t) t.Run("GetAppBrickInstances_Success", func(t *testing.T) { + expectedVariables := map[string]string{ + "CUSTOM_MODEL_PATH": "/home/arduino/.arduino-bricks/ei-models", + "EI_CLASSIFICATION_MODEL": "/models/ootb/ei/mobilenet-v2-224px.eim", + } + brickInstances, err := httpClient.GetAppBrickInstancesWithResponse(t.Context(), *createResp.JSON201.Id, func(ctx context.Context, req *http.Request) error { return nil }) require.NoError(t, err) require.Len(t, *brickInstances.JSON200.Bricks, 1) require.Equal(t, ImageClassifactionBrickID, *(*brickInstances.JSON200.Bricks)[0].Id) require.Equal(t, expectedConfigVariables, *(*brickInstances.JSON200.Bricks)[0].ConfigVariables) + require.Equal(t, "Arduino", *(*brickInstances.JSON200.Bricks)[0].Author) + require.Equal(t, "video", *(*brickInstances.JSON200.Bricks)[0].Category) + require.True(t, *(*brickInstances.JSON200.Bricks)[0].RequireModel) + require.Equal(t, expectedVariables, *(*brickInstances.JSON200.Bricks)[0].Variables) }) diff --git a/internal/orchestrator/bricks/bricks.go b/internal/orchestrator/bricks/bricks.go index df49c1ec..85e6e601 100644 --- a/internal/orchestrator/bricks/bricks.go +++ b/internal/orchestrator/bricks/bricks.go @@ -71,7 +71,7 @@ func (s *Service) List() (BrickListResult, error) { } func (s *Service) AppBrickInstancesList(a *app.ArduinoApp) (AppBrickInstancesResult, error) { - res := AppBrickInstancesResult{BrickInstances: make([]BrickInstance, len(a.Descriptor.Bricks))} + res := AppBrickInstancesResult{BrickInstances: make([]BrickInstanceListItem, len(a.Descriptor.Bricks))} for i, brickInstance := range a.Descriptor.Bricks { brick, found := s.bricksIndex.FindBrickByID(brickInstance.ID) if !found { @@ -80,7 +80,7 @@ func (s *Service) AppBrickInstancesList(a *app.ArduinoApp) (AppBrickInstancesRes variablesMap, configVariables := getBrickConfigDetails(brick, brickInstance.Variables) - res.BrickInstances[i] = BrickInstance{ + res.BrickInstances[i] = BrickInstanceListItem{ ID: brick.ID, Name: brick.Name, Author: "Arduino", // TODO: for now we only support our bricks diff --git a/internal/orchestrator/bricks/bricks_test.go b/internal/orchestrator/bricks/bricks_test.go index 0077a5b2..266d41ef 100644 --- a/internal/orchestrator/bricks/bricks_test.go +++ b/internal/orchestrator/bricks/bricks_test.go @@ -644,3 +644,191 @@ func TestAppBrickInstanceModelsDetails(t *testing.T) { }) } } + +func TestAppBrickInstancesList(t *testing.T) { + + bIndex := &bricksindex.BricksIndex{ + Bricks: []bricksindex.Brick{ + { + ID: "arduino:weather_forecast", + Name: "Weather Forecast", + Category: "miscellaneous", + RequireModel: false, + Variables: []bricksindex.BrickVariable{}, + }, + { + ID: "arduino:object_detection", + Name: "Object Detection", + Category: "video", + ModelName: "yolox-object-detection", + RequireModel: true, + Variables: []bricksindex.BrickVariable{ + {Name: "CUSTOM_MODEL_PATH", DefaultValue: "/home/arduino/.arduino-bricks/ei-models", Description: "path to the custom model directory"}, + {Name: "EI_OBJ_DETECTION_MODEL", DefaultValue: "/models/ootb/ei/yolo-x-nano.eim", Description: "path to the model file"}, + }, + }, + { + ID: "arduino:audio_classification", + Name: "Audio Classification", + Category: "audio", + ModelName: "glass-breaking", + RequireModel: true, + Variables: []bricksindex.BrickVariable{ + {Name: "CUSTOM_MODEL_PATH", DefaultValue: "/home/arduino/.arduino-bricks/ei-models"}, + {Name: "EI_AUDIO_CLASSIFICATION_MODEL", DefaultValue: "/models/ootb/ei/glass-breaking.eim"}, + }, + }, + { + ID: "arduino:streamlit_ui", + Name: "WebUI - Streamlit", + Category: "ui", + RequireModel: false, + Ports: []string{"7000", "8000"}, + }, + }, + } + + svc := &Service{ + bricksIndex: bIndex, + modelsIndex: &modelsindex.ModelsIndex{}, + } + + tests := []struct { + name string + app *app.ArduinoApp + expectedError string + validate func(*testing.T, AppBrickInstancesResult) + }{ + { + name: "Error - Brick not found in Index", + app: &app.ArduinoApp{ + Descriptor: app.AppDescriptor{ + Bricks: []app.Brick{ + {ID: "arduino:non_existent_brick"}, + }, + }, + }, + expectedError: "brick not found with id arduino:non_existent_brick", + }, + { + name: "Success - Empty App", + app: &app.ArduinoApp{ + Descriptor: app.AppDescriptor{ + Bricks: []app.Brick{}, + }, + }, + validate: func(t *testing.T, res AppBrickInstancesResult) { + require.Empty(t, res.BrickInstances) + }, + }, + { + name: "Success - Simple Brick", + app: &app.ArduinoApp{ + Descriptor: app.AppDescriptor{ + Bricks: []app.Brick{ + {ID: "arduino:weather_forecast"}, + }, + }, + }, + validate: func(t *testing.T, res AppBrickInstancesResult) { + require.Len(t, res.BrickInstances, 1) + brick := res.BrickInstances[0] + + require.Equal(t, "arduino:weather_forecast", brick.ID) + require.Equal(t, "Weather Forecast", brick.Name) + require.Equal(t, "miscellaneous", brick.Category) + require.Equal(t, "installed", brick.Status) + require.Equal(t, "Arduino", brick.Author) + require.False(t, brick.RequireModel) + require.Empty(t, brick.ModelID) + }, + }, + { + name: "Success - Brick with Model Configured", + app: &app.ArduinoApp{ + Descriptor: app.AppDescriptor{ + Bricks: []app.Brick{ + { + ID: "arduino:object_detection", + Model: "face-detection", // default model overridden + Variables: map[string]string{ + "CUSTOM_MODEL_PATH": "/custom/path", + }, + }, + }, + }, + }, + validate: func(t *testing.T, res AppBrickInstancesResult) { + require.Len(t, res.BrickInstances, 1) + brick := res.BrickInstances[0] + + require.Equal(t, "arduino:object_detection", brick.ID) + require.Equal(t, "video", brick.Category) + require.True(t, brick.RequireModel) + require.Equal(t, "face-detection", brick.ModelID) + + foundCustom := false + for _, v := range brick.ConfigVariables { + if v.Name == "CUSTOM_MODEL_PATH" { + require.Equal(t, "/custom/path", v.Value) + foundCustom = true + } + } + require.True(t, foundCustom, "Variable CUSTOM_MODEL_PATH should be present and overridden") + }, + }, + { + name: "Success - Multiple Bricks", + app: &app.ArduinoApp{ + Descriptor: app.AppDescriptor{ + Bricks: []app.Brick{ + {ID: "arduino:streamlit_ui"}, + {ID: "arduino:audio_classification", Model: "glass-breaking"}, + }, + }, + }, + validate: func(t *testing.T, res AppBrickInstancesResult) { + require.Len(t, res.BrickInstances, 2) + + // Brick 1: Streamlit UI + b1 := res.BrickInstances[0] + require.Equal(t, "arduino:streamlit_ui", b1.ID) + require.Equal(t, "WebUI - Streamlit", b1.Name) + require.Equal(t, "Arduino", b1.Author) + require.Equal(t, "ui", b1.Category) + require.Equal(t, "installed", b1.Status) + require.Equal(t, "", b1.ModelID) + require.Empty(t, b1.Variables) + require.Empty(t, b1.ConfigVariables) + require.False(t, b1.RequireModel) + + // Brick 2: Audio Classification + b2 := res.BrickInstances[1] + require.Equal(t, "arduino:audio_classification", b2.ID) + require.Equal(t, "audio", b2.Category) + require.True(t, b2.RequireModel) + require.Equal(t, "glass-breaking", b2.ModelID) + require.Equal(t, 2, len(b2.ConfigVariables)) + require.Equal(t, "/home/arduino/.arduino-bricks/ei-models", b2.ConfigVariables[0].Value) + require.Equal(t, "/models/ootb/ei/glass-breaking.eim", b2.ConfigVariables[1].Value) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := svc.AppBrickInstancesList(tt.app) + + if tt.expectedError != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedError) + return + } + + require.NoError(t, err) + if tt.validate != nil { + tt.validate(t, result) + } + }) + } +} diff --git a/internal/orchestrator/bricks/types.go b/internal/orchestrator/bricks/types.go index 1fca898f..bd63bd57 100644 --- a/internal/orchestrator/bricks/types.go +++ b/internal/orchestrator/bricks/types.go @@ -30,9 +30,19 @@ type BrickListItem struct { } type AppBrickInstancesResult struct { - BrickInstances []BrickInstance `json:"bricks"` + BrickInstances []BrickInstanceListItem `json:"bricks"` +} +type BrickInstanceListItem struct { + ID string `json:"id"` + Name string `json:"name"` + Author string `json:"author"` + Category string `json:"category"` + Status string `json:"status"` + Variables map[string]string `json:"variables,omitempty" description:"Deprecated: use config_variables instead. This field is kept for backward compatibility."` + ConfigVariables []BrickConfigVariable `json:"config_variables,omitempty"` + RequireModel bool `json:"require_model"` + ModelID string `json:"model,omitempty"` } - type BrickInstance struct { ID string `json:"id"` Name string `json:"name"`