Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4df8f58
fix: improve error logging message in HandleBrickCreate function
dido18 Nov 12, 2025
650906d
test: rename TestBrickCreate to TestBrickCreateFromAppExample and upd…
dido18 Nov 12, 2025
59a73d2
fix: log a warning for missing mandatory variables in BrickCreate and…
dido18 Nov 12, 2025
18a0030
revert some changes
dido18 Nov 12, 2025
a8c8314
fix: update test cases for BrickCreate to improve clarity and logging
dido18 Nov 12, 2025
289e6af
fix: correct typo in comment for BrickCreate function to enhance clarity
dido18 Nov 12, 2025
ad54d74
fix: improve warning message for missing mandatory variables in Brick…
dido18 Nov 12, 2025
1e8e541
feat: add validation for app descriptors and corresponding test cases
dido18 Nov 13, 2025
58a4227
fix: correct expected error message for missing required variable in …
dido18 Nov 14, 2025
7878a2c
refactor: rename validation functions and update test cases for clari…
dido18 Nov 14, 2025
ef6c2de
fix: rename variable for clarity and validate app descriptor in Handl…
dido18 Nov 14, 2025
84f1256
fix: update error code to BadRequestErr in HandleAppStart and add cor…
dido18 Nov 14, 2025
9bc05ac
refactor: consolidate brick validation into AppDescriptor and update …
dido18 Nov 14, 2025
05f12bf
fix: ensure bricks index is not nil and capture error from ValidateBr…
dido18 Nov 14, 2025
70e1dbb
test: add YAML test cases for app descriptor validation with empty an…
dido18 Nov 14, 2025
38b49bc
test: add test case for validation of non-existing variable in app de…
dido18 Nov 14, 2025
3708460
fix: enhance brick validation to collect all errors and add new test …
dido18 Nov 14, 2025
d8028b6
fix: update ValidateBricks to return all validation errors as a slice…
dido18 Nov 14, 2025
6a17436
fix: update error handling in ValidateBricks to yield all validation …
dido18 Nov 14, 2025
c7a2e55
fix: refactor ValidateBricks to return a single error and update rela…
dido18 Nov 17, 2025
5742d45
test: add dummy app configuration and main.py for brick creation tests
dido18 Nov 17, 2025
1b18b36
fix: add logging for missing required variables in BrickCreate function
dido18 Nov 17, 2025
36319aa
chore: remove unused app.golden.yaml test data file
dido18 Nov 17, 2025
d9992fe
fix: clean up app.yaml by removing unused variables section
dido18 Nov 17, 2025
89bb0b9
refactor: move ValidateBricks function to a new file and update refer…
dido18 Nov 17, 2025
8acb1cc
refactor: update app type references from appspecification to app
dido18 Nov 17, 2025
4b525b3
FIX
dido18 Nov 17, 2025
7a9bdf2
refactor: log a warning for undeclared variables in brick configuration
dido18 Nov 17, 2025
7f935b9
Update internal/orchestrator/app/validator.go
dido18 Nov 17, 2025
1661442
refactor: rename variable for clarity in ValidateBricks function
dido18 Nov 17, 2025
2ccabd8
refactor: improve error handling and logging in validation functions
dido18 Nov 19, 2025
def6889
refactor: update test cases to use YAML content instead of filenames …
dido18 Nov 19, 2025
7a8ad44
refactor: remove obsolete YAML test files and clean up validator tests
dido18 Nov 19, 2025
ee2db2d
refactor: remove redundant brick validation from app restart process
dido18 Nov 19, 2025
c264066
refactor: improve error messages for brick update and validation proc…
dido18 Nov 19, 2025
98a2ae1
fix: correct typo in error message for required brick variable
dido18 Nov 19, 2025
18b6daf
refactor: improve error messages and update brick handling in service…
dido18 Nov 19, 2025
95728f2
refactor: update brick handling and improve test coverage for variabl…
dido18 Nov 19, 2025
d183dae
fix: correct typo in TODO comment for variable handling in tests
dido18 Nov 20, 2025
e1da66c
fix: remove hardcoded VERSION variable from build task in Taskfile
dido18 Nov 20, 2025
02df117
Update internal/orchestrator/bricks/bricks_test.go
dido18 Nov 20, 2025
37a4cf4
Add comprehensive AI model support documentation for AppLab
dido18 Nov 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions internal/api/handlers/bricks.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func HandleBrickCreate(
err = brickService.BrickCreate(req, app)
if err != nil {
// TODO: handle specific errors
slog.Error("Unable to parse the app.yaml", slog.String("error", err.Error()))
slog.Error("Unable to create brick", slog.String("error", err.Error()))
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "error while creating or updating brick"})
return
}
Expand Down Expand Up @@ -213,7 +213,7 @@ func HandleBrickUpdates(
req.ID = id
err = brickService.BrickUpdate(req, app)
if err != nil {
slog.Error("Unable to parse the app.yaml", slog.String("error", err.Error()))
slog.Error("Unable to update the brick", slog.String("error", err.Error()))
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "unable to update the brick"})

return
Expand Down
45 changes: 45 additions & 0 deletions internal/orchestrator/app/validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package app

import (
"errors"
"fmt"
"log/slog"

"github.com/arduino/arduino-app-cli/internal/orchestrator/bricksindex"
)

// ValidateBricks checks that all bricks referenced in the given AppDescriptor exist in the provided BricksIndex,
// It collects and returns all validation errors as a single joined error, allowing the caller to see all issues at once rather than stopping at the first error.
func ValidateBricks(a AppDescriptor, index *bricksindex.BricksIndex) error {
if index == nil {
return fmt.Errorf("bricks index cannot be nil")
}

var allErrors error

for _, appBrick := range a.Bricks {
indexBrick, found := index.FindBrickByID(appBrick.ID)
if !found {
allErrors = errors.Join(allErrors, fmt.Errorf("brick %q not found", appBrick.ID))
continue // Skip further validation for this brick since it doesn't exist
}

for appBrickVariableName := range appBrick.Variables {
_, exist := indexBrick.GetVariable(appBrickVariableName)
if !exist {
// TODO: we should return warnings
slog.Warn("[skip] variable does not exist into the brick definition", "variable", appBrickVariableName, "brick", indexBrick.ID)
}
}

// Check that all required brick variables are provided by app
for _, indexBrickVariable := range indexBrick.Variables {
if indexBrickVariable.IsRequired() {
if _, exist := appBrick.Variables[indexBrickVariable.Name]; !exist {
allErrors = errors.Join(allErrors, fmt.Errorf("variable %q is required by brick %q", indexBrickVariable.Name, indexBrick.ID))
}
}
}
}
return allErrors
}
163 changes: 163 additions & 0 deletions internal/orchestrator/app/validator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package app

import (
"errors"
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/arduino/go-paths-helper"

"github.com/arduino/arduino-app-cli/internal/orchestrator/bricksindex"
)

func TestValidateAppDescriptorBricks(t *testing.T) {
bricksIndex := &bricksindex.BricksIndex{
Bricks: []bricksindex.Brick{
{
ID: "arduino:arduino_cloud",
Name: "Arduino Cloud",
Description: "Connects to Arduino Cloud",
Variables: []bricksindex.BrickVariable{
{
Name: "ARDUINO_DEVICE_ID",
Description: "Arduino Cloud Device ID",
DefaultValue: "", // Required (no default value)
},
{
Name: "ARDUINO_SECRET",
Description: "Arduino Cloud Secret",
DefaultValue: "", // Required (no default value)
},
},
},
},
}

testCases := []struct {
name string
yamlContent string
expectedError error
}{
{
name: "valid with all required filled",
yamlContent: `
name: App ok
description: App ok
bricks:
- arduino:arduino_cloud:
variables:
ARDUINO_DEVICE_ID: "my-device-id"
ARDUINO_SECRET: "my-secret"
`,
expectedError: nil,
},
{
name: "valid with missing bricks",
yamlContent: `
name: App with no bricks
description: App with no bricks description
`,
expectedError: nil,
},
{
name: "valid with empty list of bricks",
yamlContent: `
name: App with empty bricks
description: App with empty bricks

bricks: []
`,
expectedError: nil,
},
{
name: "valid if required variable is empty string",
yamlContent: `
name: App with an empty variable
description: App with an empty variable
bricks:
- arduino:arduino_cloud:
variables:
ARDUINO_DEVICE_ID: "my-device-id"
ARDUINO_SECRET:
`,
expectedError: nil,
},
{
name: "invalid if required variable is omitted",
yamlContent: `
name: App with no required variables
description: App with no required variables
bricks:
- arduino:arduino_cloud
`,
expectedError: errors.Join(
errors.New("variable \"ARDUINO_DEVICE_ID\" is required by brick \"arduino:arduino_cloud\""),
errors.New("variable \"ARDUINO_SECRET\" is required by brick \"arduino:arduino_cloud\""),
),
},
{
name: "invalid if a required variable among two is omitted",
yamlContent: `
name: App only one required variable filled
description: App only one required variable filled
bricks:
- arduino:arduino_cloud:
variables:
ARDUINO_DEVICE_ID: "my-device-id"
`,
expectedError: errors.New("variable \"ARDUINO_SECRET\" is required by brick \"arduino:arduino_cloud\""),
},
{
name: "invalid if brick id not found",
yamlContent: `
name: App no existing brick
description: App no existing brick
bricks:
- arduino:not_existing_brick:
variables:
ARDUINO_DEVICE_ID: "my-device-id"
ARDUINO_SECRET: "LAKDJ"
`,
expectedError: errors.New("brick \"arduino:not_existing_brick\" not found"),
},
{
name: "log a warning if variable does not exist in the brick",
yamlContent: `
name: App with non existing variable
description: App with non existing variable
bricks:
- arduino:arduino_cloud:
variables:
NOT_EXISTING_VARIABLE: "this-is-a-not-existing-variable-for-the-brick"
ARDUINO_DEVICE_ID: "my-device-id"
ARDUINO_SECRET: "my-secret"
`,
expectedError: nil,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tempDir := t.TempDir()
err := paths.New(tempDir).MkdirAll()
require.NoError(t, err)
appYaml := paths.New(tempDir, "app.yaml")
err = os.WriteFile(appYaml.String(), []byte(tc.yamlContent), 0600)
require.NoError(t, err)

appDescriptor, err := ParseDescriptorFile(appYaml)
require.NoError(t, err)

err = ValidateBricks(appDescriptor, bricksIndex)
if tc.expectedError == nil {
assert.NoError(t, err, "Expected no validation errors")
} else {
require.Error(t, err, "Expected validation error")
assert.Equal(t, tc.expectedError.Error(), err.Error(), "Error message should match")
}
})
}
}
43 changes: 22 additions & 21 deletions internal/orchestrator/bricks/bricks.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,15 +282,15 @@ func (s *Service) BrickCreate(
if !exist {
return fmt.Errorf("variable %q does not exist on brick %q", name, brick.ID)
}
if value.DefaultValue == "" && reqValue == "" {
return fmt.Errorf("variable %q cannot be empty", name)
if value.IsRequired() && reqValue == "" {
return fmt.Errorf("required variable %q cannot be empty", name)
}
}

for _, brickVar := range brick.Variables {
if brickVar.DefaultValue == "" {
if brickVar.IsRequired() {
if _, exist := req.Variables[brickVar.Name]; !exist {
return fmt.Errorf("required variable %q is mandatory", brickVar.Name)
slog.Warn("[Skip] a required variable is not set by user", "variable", brickVar.Name, "brick", brickVar.Name)
}
}
}
Expand Down Expand Up @@ -335,16 +335,21 @@ func (s *Service) BrickUpdate(
req BrickCreateUpdateRequest,
appCurrent app.ArduinoApp,
) error {
index := slices.IndexFunc(appCurrent.Descriptor.Bricks, func(b app.Brick) bool { return b.ID == req.ID })
if index == -1 {
return fmt.Errorf("brick not found with id %s", req.ID)
brickFromIndex, present := s.bricksIndex.FindBrickByID(req.ID)
if !present {
return fmt.Errorf("brick %q not found into the brick index", req.ID)
}

brickPosition := slices.IndexFunc(appCurrent.Descriptor.Bricks, func(b app.Brick) bool { return b.ID == req.ID })
if brickPosition == -1 {
return fmt.Errorf("brick %q not found into the bricks of the app", req.ID)
}
brickID := appCurrent.Descriptor.Bricks[index].ID
brickVariables := appCurrent.Descriptor.Bricks[index].Variables

brickVariables := appCurrent.Descriptor.Bricks[brickPosition].Variables
if len(brickVariables) == 0 {
brickVariables = make(map[string]string)
}
brickModel := appCurrent.Descriptor.Bricks[index].Model
brickModel := appCurrent.Descriptor.Bricks[brickPosition].Model

if req.Model != nil && *req.Model != brickModel {
models := s.modelsIndex.GetModelsByBrick(req.ID)
Expand All @@ -354,17 +359,14 @@ func (s *Service) BrickUpdate(
}
brickModel = *req.Model
}
brick, present := s.bricksIndex.FindBrickByID(brickID)
if !present {
return fmt.Errorf("brick not found with id %s", brickID)
}

for name, updateValue := range req.Variables {
value, exist := brick.GetVariable(name)
value, exist := brickFromIndex.GetVariable(name)
if !exist {
return errors.New("variable does not exist")
return fmt.Errorf("variable %q does not exist on brick %q", name, brickFromIndex.ID)
}
if value.DefaultValue == "" && updateValue == "" {
return errors.New("variable default value cannot be empty")
if value.IsRequired() && updateValue == "" {
return fmt.Errorf("required variable %q cannot be empty", name)
}
updated := false
for _, v := range brickVariables {
Expand All @@ -374,14 +376,13 @@ func (s *Service) BrickUpdate(
break
}
}

if !updated {
brickVariables[name] = updateValue
}
}

appCurrent.Descriptor.Bricks[index].Model = brickModel
appCurrent.Descriptor.Bricks[index].Variables = brickVariables
appCurrent.Descriptor.Bricks[brickPosition].Model = brickModel
appCurrent.Descriptor.Bricks[brickPosition].Variables = brickVariables

err := appCurrent.Save()
if err != nil {
Expand Down
Loading