Feat v0.7.11 query & HTTP ergonomics
Some checks failed
CI/CD / e2e-smoke (pull_request) Has been skipped
CI/CD / test-go-pg (pull_request) Has been cancelled
CI/CD / detect-changes (pull_request) Successful in 4s
CI/CD / test-frontend (pull_request) Has been skipped
CI/CD / build-and-deploy (pull_request) Has been cancelled
CI/CD / test-sqlite (pull_request) Has been cancelled
CI/CD / test-runners (pull_request) Has been skipped

Add four new Starlark sandbox builtins to reduce extension friction
around SQL decomposition and synchronous HTTP fan-out:

- db.count(table, filters) — integer row count
- db.aggregate(table, column, op, filters) — single-value aggregation
- db.query_batch(queries) — up to 10 query specs in one call
- http.batch(requests) — concurrent dispatch of up to 10 HTTP requests

Extract buildSelectQuery helper from dbQuery for reuse by query_batch.
21 new tests (14 db, 7 http). No new permissions or schema changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 23:31:52 +00:00
parent f06c6c954b
commit f036995d5c
8 changed files with 924 additions and 77 deletions

View File

@@ -508,3 +508,178 @@ result = test()
t.Errorf("binding signature broken: %v", err)
}
}
// ─── http.batch ─────────────────────────────
func TestHTTPBatch_Starlark_BlockedDomains(t *testing.T) {
mod := BuildHTTPModule(context.Background(), HTTPModuleConfig{
AllowDomains: []string{"allowed.example.com"},
})
sb := New(DefaultConfig())
modules := map[string]starlark.Value{"http": mod}
// Both requests target a blocked domain — should return error dicts, not throw.
script := `
results = http.batch([
{"method": "GET", "url": "https://blocked.example.com/a"},
{"method": "POST", "url": "https://blocked.example.com/b", "body": "{}", "headers": {"Content-Type": "application/json"}},
])
count = len(results)
`
result, err := sb.Exec(context.Background(), "test.star", script, modules)
if err != nil {
t.Fatalf("script error: %v", err)
}
countVal, ok := result.Globals["count"].(starlark.Int)
if !ok {
t.Fatalf("count is %T, want starlark.Int", result.Globals["count"])
}
v, _ := countVal.Int64()
if v != 2 {
t.Errorf("got %d results, want 2", v)
}
// Verify each result is an error dict with status=0
results := result.Globals["results"].(*starlark.List)
for i := 0; i < results.Len(); i++ {
d, ok := results.Index(i).(*starlark.Dict)
if !ok {
t.Fatalf("result[%d] is %T, want *starlark.Dict", i, results.Index(i))
}
statusVal, found, _ := d.Get(starlark.String("status"))
if !found {
t.Fatalf("result[%d] missing 'status' key", i)
}
statusInt, _ := starlark.AsInt32(statusVal)
if statusInt != 0 {
t.Errorf("result[%d] status = %d, want 0 (error)", i, statusInt)
}
bodyVal, _, _ := d.Get(starlark.String("body"))
bodyStr, _ := starlark.AsString(bodyVal)
if !strings.Contains(bodyStr, "error:") {
t.Errorf("result[%d] body should contain 'error:', got %q", i, bodyStr)
}
}
}
func TestHTTPBatch_EmptyList(t *testing.T) {
mod := BuildHTTPModule(context.Background(), HTTPModuleConfig{})
sb := New(DefaultConfig())
modules := map[string]starlark.Value{"http": mod}
_, err := sb.Exec(context.Background(), "test.star", `results = http.batch([])`, modules)
if err == nil {
t.Fatal("expected error for empty list")
}
if !strings.Contains(err.Error(), "empty") {
t.Errorf("unexpected error: %v", err)
}
}
func TestHTTPBatch_TooMany(t *testing.T) {
mod := BuildHTTPModule(context.Background(), HTTPModuleConfig{})
sb := New(DefaultConfig())
modules := map[string]starlark.Value{"http": mod}
script := `
def run():
specs = []
for i in range(11):
specs.append({"method": "GET", "url": "https://x.com/"})
return http.batch(specs)
results = run()
`
_, err := sb.Exec(context.Background(), "test.star", script, modules)
if err == nil {
t.Fatal("expected error for >10 requests")
}
if !strings.Contains(err.Error(), "max 10") {
t.Errorf("unexpected error: %v", err)
}
}
func TestHTTPBatch_MissingMethod(t *testing.T) {
mod := BuildHTTPModule(context.Background(), HTTPModuleConfig{})
sb := New(DefaultConfig())
modules := map[string]starlark.Value{"http": mod}
_, err := sb.Exec(context.Background(), "test.star",
`results = http.batch([{"url": "https://x.com/"}])`, modules)
if err == nil {
t.Fatal("expected error for missing method")
}
if !strings.Contains(err.Error(), "method") {
t.Errorf("unexpected error: %v", err)
}
}
func TestHTTPBatch_MissingURL(t *testing.T) {
mod := BuildHTTPModule(context.Background(), HTTPModuleConfig{})
sb := New(DefaultConfig())
modules := map[string]starlark.Value{"http": mod}
_, err := sb.Exec(context.Background(), "test.star",
`results = http.batch([{"method": "GET"}])`, modules)
if err == nil {
t.Fatal("expected error for missing url")
}
if !strings.Contains(err.Error(), "url") {
t.Errorf("unexpected error: %v", err)
}
}
func TestHTTPBatch_NotDict(t *testing.T) {
mod := BuildHTTPModule(context.Background(), HTTPModuleConfig{})
sb := New(DefaultConfig())
modules := map[string]starlark.Value{"http": mod}
_, err := sb.Exec(context.Background(), "test.star",
`results = http.batch(["not a dict"])`, modules)
if err == nil {
t.Fatal("expected error for non-dict element")
}
if !strings.Contains(err.Error(), "dict") {
t.Errorf("unexpected error: %v", err)
}
}
func TestHTTPBatch_MixedResults(t *testing.T) {
// One allowed domain (will fail at DNS but not at allowlist), one blocked.
mod := BuildHTTPModule(context.Background(), HTTPModuleConfig{
AllowDomains: []string{"allowed.invalid"},
})
sb := New(DefaultConfig())
modules := map[string]starlark.Value{"http": mod}
script := `
results = http.batch([
{"method": "GET", "url": "https://allowed.invalid/a"},
{"method": "GET", "url": "https://blocked.invalid/b"},
])
`
result, err := sb.Exec(context.Background(), "test.star", script, modules)
if err != nil {
t.Fatalf("script error: %v", err)
}
results := result.Globals["results"].(*starlark.List)
if results.Len() != 2 {
t.Fatalf("got %d results, want 2", results.Len())
}
// Both should be error dicts (first = DNS failure, second = allowlist block)
// but neither should have caused the whole batch to fail.
for i := 0; i < 2; i++ {
d, ok := results.Index(i).(*starlark.Dict)
if !ok {
t.Fatalf("result[%d] is %T, want *starlark.Dict", i, results.Index(i))
}
statusVal, _, _ := d.Get(starlark.String("status"))
statusInt, _ := starlark.AsInt32(statusVal)
if statusInt != 0 {
t.Errorf("result[%d] expected error (status 0), got %d", i, statusInt)
}
}
}