Declare scanner.scan's apart argument, so the console lets it through to the tool (issue 1528)
mesh/merge-gate head c4913dbbcb35 merged into trunk 9daeab209ba4 — pass: builds brother → shanks; no bus step; every machine composes with the change as it…
mesh/repo-check head c4913dbbcb35 merged into trunk 9daeab209ba4 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered

This commit is contained in:
2026-10-21 09:26:11 +02:00
parent 9daeab209b
commit c4913dbbcb
2 changed files with 96 additions and 0 deletions
@@ -0,0 +1,95 @@
package main
import (
"go/ast"
"go/parser"
"go/token"
"sort"
"strconv"
"testing"
)
// 1528: a tool reads only arguments its input schema declares; the console refuses an undeclared
// one before it reaches the tool (scanner.scan read apart, which its schema never named).
func TestEveryArgumentAToolReadsIsDeclared(t *testing.T) {
fs := token.NewFileSet()
f, err := parser.ParseFile(fs, "main.go", nil, 0)
if err != nil {
t.Fatal(err)
}
checked := 0
ast.Inspect(f, func(n ast.Node) bool {
lit, ok := n.(*ast.CompositeLit)
if !ok {
return true
}
var name string
var input, run ast.Expr
for _, e := range lit.Elts {
kv, ok := e.(*ast.KeyValueExpr)
if !ok {
continue
}
switch k, _ := kv.Key.(*ast.Ident); {
case k == nil:
case k.Name == "Name":
if b, ok := kv.Value.(*ast.BasicLit); ok {
name, _ = strconv.Unquote(b.Value)
}
case k.Name == "Input":
input = kv.Value
case k.Name == "Run":
run = kv.Value
}
}
if name == "" || input == nil || run == nil {
return true
}
declared := map[string]bool{}
ast.Inspect(input, func(m ast.Node) bool {
kv, ok := m.(*ast.KeyValueExpr)
if !ok {
return true
}
if k, ok := kv.Key.(*ast.BasicLit); ok && k.Value == `"properties"` {
if props, ok := kv.Value.(*ast.CompositeLit); ok {
for _, p := range props.Elts {
if pkv, ok := p.(*ast.KeyValueExpr); ok {
if b, ok := pkv.Key.(*ast.BasicLit); ok {
s, _ := strconv.Unquote(b.Value)
declared[s] = true
}
}
}
}
return false
}
return true
})
var missing []string
ast.Inspect(run, func(m ast.Node) bool {
ix, ok := m.(*ast.IndexExpr)
if !ok {
return true
}
if id, ok := ix.X.(*ast.Ident); ok && id.Name == "args" {
if b, ok := ix.Index.(*ast.BasicLit); ok && b.Kind == token.STRING {
s, _ := strconv.Unquote(b.Value)
if !declared[s] {
missing = append(missing, s)
}
}
}
return true
})
checked++
if len(missing) > 0 {
sort.Strings(missing)
t.Errorf("%s reads %v, which its input schema does not declare", name, missing)
}
return false
})
if checked < 5 {
t.Fatalf("checked %d tools; the test no longer finds the tool literals", checked)
}
}
+1
View File
@@ -277,6 +277,7 @@ func tools(settingsFile string, now func() time.Time) []stdio.Tool {
"document": map[string]any{"type": "string", "description": "glass only, a document of several pages in one PDF: start opens one and scans its first page; the id of an open document scans the next page into it"},
"finish": map[string]any{"type": "boolean", "description": "with document: the id of an open document: close it into one PDF, filed once (nothing is scanned)"},
"duplex": map[string]any{"type": "boolean", "description": "feeder only: scan both sides of each sheet (default false)"},
"apart": map[string]any{"type": "boolean", "description": "glass only: several documents lying apart on the glass are each filed as their own PDF (default false: one scan of the glass)"},
}},
Run: func(args map[string]any) (any, error) {
s, err := service()