feat: modelos y CRUD para unit_tests y e2e_tests
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.
This commit is contained in:
@@ -167,6 +167,33 @@ type Log struct {
|
|||||||
CreatedAt time.Time `json:"created_at"`
|
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.
|
// TypeSnapshot is an immutable copy of a registry type at point of use.
|
||||||
type TypeSnapshot struct {
|
type TypeSnapshot struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
|||||||
@@ -903,3 +903,122 @@ func (db *DB) ListLogs(level LogLevel, source, entityID, executionID string, lim
|
|||||||
}
|
}
|
||||||
return result, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -180,6 +180,18 @@ type Proposal struct {
|
|||||||
UpdatedAt time.Time `json:"updated_at"`
|
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}
|
// GenerateID builds the canonical ID: {name}_{lang}_{domain}
|
||||||
func GenerateID(name, lang, domain string) string {
|
func GenerateID(name, lang, domain string) string {
|
||||||
return name + "_" + lang + "_" + domain
|
return name + "_" + lang + "_" + domain
|
||||||
|
|||||||
@@ -614,6 +614,91 @@ func scanTypes(rows interface{ Next() bool; Scan(...any) error }) ([]Type, error
|
|||||||
return result, nil
|
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 ---
|
// --- Proposal CRUD ---
|
||||||
|
|
||||||
// InsertProposal inserts or replaces a proposal.
|
// InsertProposal inserts or replaces a proposal.
|
||||||
|
|||||||
Reference in New Issue
Block a user