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
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user