From 9660a1c432f721d6769e8cfdb94eded6fec310cf Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sun, 5 Apr 2026 18:19:10 +0200 Subject: [PATCH] feat: modelos y CRUD para unit_tests y e2e_tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UnitTest en registry con Insert, GetByFunction, Search FTS5, Purge. E2ETest en fn_operations con Insert, Get, List, UpdateResult, Delete. Ambos con scan helpers y serialización JSON. --- fn_operations/models.go | 27 +++++++++ fn_operations/store.go | 119 ++++++++++++++++++++++++++++++++++++++++ registry/models.go | 12 ++++ registry/store.go | 85 ++++++++++++++++++++++++++++ 4 files changed, 243 insertions(+) diff --git a/fn_operations/models.go b/fn_operations/models.go index a951505d..430ebc48 100644 --- a/fn_operations/models.go +++ b/fn_operations/models.go @@ -167,6 +167,33 @@ type Log struct { CreatedAt time.Time `json:"created_at"` } +// E2ETestStatus represents the result of an e2e test run. +type E2ETestStatus string + +const ( + E2EPass E2ETestStatus = "pass" + E2EFail E2ETestStatus = "fail" + E2ESkip E2ETestStatus = "skip" + E2EPending E2ETestStatus = "" +) + +// E2ETest is an integration test that verifies function composition within an app. +type E2ETest struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + RelationID string `json:"relation_id"` + Steps []string `json:"steps"` + InputFixture map[string]any `json:"input_fixture"` + Expected map[string]any `json:"expected"` + LastStatus E2ETestStatus `json:"last_status"` + LastRunAt string `json:"last_run_at"` + ExecutionID string `json:"execution_id"` + DurationMs int64 `json:"duration_ms"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + // TypeSnapshot is an immutable copy of a registry type at point of use. type TypeSnapshot struct { ID string `json:"id"` diff --git a/fn_operations/store.go b/fn_operations/store.go index a2942c9f..a6a0c49c 100644 --- a/fn_operations/store.go +++ b/fn_operations/store.go @@ -903,3 +903,122 @@ func (db *DB) ListLogs(level LogLevel, source, entityID, executionID string, lim } return result, nil } + +// --- E2E Tests CRUD --- + +// InsertE2ETest inserts or replaces an e2e test. +func (db *DB) InsertE2ETest(t *E2ETest) error { + now := time.Now().UTC() + if t.CreatedAt.IsZero() { + t.CreatedAt = now + } + t.UpdatedAt = now + + _, err := db.conn.Exec(` + INSERT OR REPLACE INTO e2e_tests ( + id, name, description, relation_id, steps, input_fixture, + expected, last_status, last_run_at, execution_id, duration_ms, + created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + t.ID, t.Name, t.Description, t.RelationID, + marshalStrings(t.Steps), marshalJSON(t.InputFixture), marshalJSON(t.Expected), + string(t.LastStatus), t.LastRunAt, t.ExecutionID, t.DurationMs, + t.CreatedAt.Format(time.RFC3339), t.UpdatedAt.Format(time.RFC3339), + ) + return err +} + +// GetE2ETest returns an e2e test by ID. +func (db *DB) GetE2ETest(id string) (*E2ETest, error) { + row := db.conn.QueryRow(` + SELECT id, name, description, relation_id, steps, input_fixture, + expected, last_status, last_run_at, execution_id, duration_ms, + created_at, updated_at + FROM e2e_tests WHERE id = ?`, id) + + t, err := scanE2ETest(row) + if err != nil { + return nil, fmt.Errorf("e2e test %q not found: %w", id, err) + } + return t, nil +} + +// ListE2ETests returns e2e tests with optional status filter. +func (db *DB) ListE2ETests(status E2ETestStatus) ([]E2ETest, error) { + where := []string{} + args := []any{} + if status != "" { + where = append(where, "last_status = ?") + args = append(args, string(status)) + } + + q := `SELECT id, name, description, relation_id, steps, input_fixture, + expected, last_status, last_run_at, execution_id, duration_ms, + created_at, updated_at + FROM e2e_tests` + if len(where) > 0 { + q += " WHERE " + strings.Join(where, " AND ") + } + q += " ORDER BY name" + + rows, err := db.conn.Query(q, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var result []E2ETest + for rows.Next() { + var t E2ETest + var stepsJSON, fixtureJSON, expectedJSON, createdAt, updatedAt string + if err := rows.Scan(&t.ID, &t.Name, &t.Description, &t.RelationID, + &stepsJSON, &fixtureJSON, &expectedJSON, + &t.LastStatus, &t.LastRunAt, &t.ExecutionID, &t.DurationMs, + &createdAt, &updatedAt); err != nil { + return nil, fmt.Errorf("scanning e2e test: %w", err) + } + t.Steps = unmarshalStrings(stepsJSON) + t.InputFixture = unmarshalJSON(fixtureJSON) + t.Expected = unmarshalJSON(expectedJSON) + t.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) + t.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) + result = append(result, t) + } + return result, nil +} + +// UpdateE2ETestResult updates the result fields after running an e2e test. +func (db *DB) UpdateE2ETestResult(id string, status E2ETestStatus, executionID string, durationMs int64) error { + now := time.Now().UTC() + _, err := db.conn.Exec(` + UPDATE e2e_tests SET last_status=?, last_run_at=?, execution_id=?, duration_ms=?, updated_at=? + WHERE id=?`, + string(status), now.Format(time.RFC3339), executionID, durationMs, + now.Format(time.RFC3339), id, + ) + return err +} + +// DeleteE2ETest removes an e2e test by ID. +func (db *DB) DeleteE2ETest(id string) error { + _, err := db.conn.Exec("DELETE FROM e2e_tests WHERE id = ?", id) + return err +} + +func scanE2ETest(row *sql.Row) (*E2ETest, error) { + var t E2ETest + var stepsJSON, fixtureJSON, expectedJSON, createdAt, updatedAt string + err := row.Scan(&t.ID, &t.Name, &t.Description, &t.RelationID, + &stepsJSON, &fixtureJSON, &expectedJSON, + &t.LastStatus, &t.LastRunAt, &t.ExecutionID, &t.DurationMs, + &createdAt, &updatedAt) + if err != nil { + return nil, err + } + t.Steps = unmarshalStrings(stepsJSON) + t.InputFixture = unmarshalJSON(fixtureJSON) + t.Expected = unmarshalJSON(expectedJSON) + t.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) + t.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) + return &t, nil +} diff --git a/registry/models.go b/registry/models.go index 031d3cb2..0f1eae68 100644 --- a/registry/models.go +++ b/registry/models.go @@ -180,6 +180,18 @@ type Proposal struct { UpdatedAt time.Time `json:"updated_at"` } +// UnitTest represents an individual test case extracted from a test file. +type UnitTest struct { + ID string `json:"id"` + FunctionID string `json:"function_id"` + Name string `json:"name"` + Code string `json:"code"` + FilePath string `json:"file_path"` + Lang string `json:"lang"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + // GenerateID builds the canonical ID: {name}_{lang}_{domain} func GenerateID(name, lang, domain string) string { return name + "_" + lang + "_" + domain diff --git a/registry/store.go b/registry/store.go index d874c17a..b2d8d102 100644 --- a/registry/store.go +++ b/registry/store.go @@ -614,6 +614,91 @@ func scanTypes(rows interface{ Next() bool; Scan(...any) error }) ([]Type, error return result, nil } +// --- Unit Tests CRUD --- + +// InsertUnitTest inserts or replaces a unit test entry. +func (db *DB) InsertUnitTest(ut *UnitTest) error { + now := time.Now().UTC() + if ut.CreatedAt.IsZero() { + ut.CreatedAt = now + } + if ut.UpdatedAt.IsZero() { + ut.UpdatedAt = now + } + + _, err := db.conn.Exec(` + INSERT OR REPLACE INTO unit_tests ( + id, function_id, name, code, file_path, lang, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + ut.ID, ut.FunctionID, ut.Name, ut.Code, ut.FilePath, ut.Lang, + ut.CreatedAt.Format(time.RFC3339), ut.UpdatedAt.Format(time.RFC3339), + ) + return err +} + +// GetUnitTestsByFunction returns all unit tests for a given function ID. +func (db *DB) GetUnitTestsByFunction(functionID string) ([]UnitTest, error) { + rows, err := db.conn.Query( + "SELECT id, function_id, name, code, file_path, lang, created_at, updated_at FROM unit_tests WHERE function_id = ? ORDER BY name", + functionID, + ) + if err != nil { + return nil, err + } + defer rows.Close() + return scanUnitTests(rows) +} + +// SearchUnitTests performs FTS search on unit tests. +func (db *DB) SearchUnitTests(query string, lang string) ([]UnitTest, error) { + where := []string{} + args := []any{} + + if query != "" { + where = append(where, "ut.id IN (SELECT id FROM unit_tests_fts WHERE unit_tests_fts MATCH ?)") + args = append(args, query) + } + if lang != "" { + where = append(where, "ut.lang = ?") + args = append(args, lang) + } + + sql := "SELECT id, function_id, name, code, file_path, lang, created_at, updated_at FROM unit_tests ut" + if len(where) > 0 { + sql += " WHERE " + strings.Join(where, " AND ") + } + sql += " ORDER BY ut.function_id, ut.name" + + rows, err := db.conn.Query(sql, args...) + if err != nil { + return nil, fmt.Errorf("search unit tests: %w", err) + } + defer rows.Close() + return scanUnitTests(rows) +} + +func scanUnitTests(rows interface{ Next() bool; Scan(...any) error }) ([]UnitTest, error) { + var result []UnitTest + for rows.Next() { + var ut UnitTest + var createdAt, updatedAt string + err := rows.Scan(&ut.ID, &ut.FunctionID, &ut.Name, &ut.Code, &ut.FilePath, &ut.Lang, &createdAt, &updatedAt) + if err != nil { + return nil, fmt.Errorf("scanning unit test: %w", err) + } + ut.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) + ut.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt) + result = append(result, ut) + } + return result, nil +} + +// PurgeUnitTests deletes all unit test entries. Used before re-indexing. +func (db *DB) PurgeUnitTests() error { + _, err := db.conn.Exec("DELETE FROM unit_tests") + return err +} + // --- Proposal CRUD --- // InsertProposal inserts or replaces a proposal.