llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、 builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、 编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。 产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt) 留在 AutoGo,宿主接入方式见 MIGRATION.md。
89 lines
3.1 KiB
Go
89 lines
3.1 KiB
Go
package diag
|
||
|
||
import (
|
||
"go/ast"
|
||
"go/parser"
|
||
"go/token"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// TestNoDirectTerminalWrites:TUI 跑在 alt-screen 下,**任何**直写 os.Stdout/os.Stderr
|
||
// 都会打穿界面。工具可达的包(工具实现、协议客户端、调度器……)都拿不到 agent 的
|
||
// Opt.Stderr,所以很容易顺手 fmt.Fprintf(os.Stderr, ...) 或 log.Printf——这条约定以前
|
||
// 只写在注释里,靠人记得,而实际就漏过好几处:
|
||
//
|
||
// - webui 的 11 处 log.Printf(默认 logger 直写 os.Stderr;而 WebUI 就在 TUI 里跑,
|
||
// SetLogOutput 只接管了 gin 的 writer);
|
||
// - MCP 子进程的 stderr 直连终端(而 MCP 的惯例恰恰是往 stderr 打日志);
|
||
// - computer 工具的三处区域截图警告与 Windows 子进程 stderr。
|
||
//
|
||
// 所以做成静态检查:工具可达包里出现 os.Stdout / os.Stderr / fmt.Print(log|f)(
|
||
// 就失败。诊断请走 diag;子进程的 stderr 用 diag.Indirect()。
|
||
//
|
||
// 用 go/ast 而不是 grep:注释里也会出现 "os.Stderr" 字样(本来就是在讲这件事),
|
||
// 文本匹配会误伤。
|
||
func TestNoDirectTerminalWrites(t *testing.T) {
|
||
// 工具调用可达的包(能跑在 TUI 里的路径)。列出当前与规划中的框架包;
|
||
// 尚未迁入的包 ReadDir 失败自动跳过。
|
||
pkgs := []string{
|
||
"tools", "agent", "cdp", "mcp", "session", "llm",
|
||
"execx", "jsonl", "textx", "scratch",
|
||
}
|
||
// 允许清单:file → 理由。目前为空——确有必要时再加,并写清为什么。
|
||
allowed := map[string]string{}
|
||
|
||
fset := token.NewFileSet()
|
||
var offenders []string
|
||
|
||
for _, pkg := range pkgs {
|
||
dir := filepath.Join("..", pkg)
|
||
entries, err := os.ReadDir(dir)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
for _, e := range entries {
|
||
name := e.Name()
|
||
if e.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
|
||
continue
|
||
}
|
||
path := filepath.Join(dir, name)
|
||
if _, ok := allowed[path]; ok {
|
||
continue
|
||
}
|
||
f, err := parser.ParseFile(fset, path, nil, 0)
|
||
if err != nil {
|
||
t.Fatalf("解析 %s 失败: %v", path, err)
|
||
}
|
||
ast.Inspect(f, func(n ast.Node) bool {
|
||
switch v := n.(type) {
|
||
case *ast.SelectorExpr:
|
||
// os.Stdout / os.Stderr
|
||
if id, ok := v.X.(*ast.Ident); ok && id.Name == "os" &&
|
||
(v.Sel.Name == "Stdout" || v.Sel.Name == "Stderr") {
|
||
pos := fset.Position(v.Pos())
|
||
offenders = append(offenders, pos.String()+" 直写 os."+v.Sel.Name)
|
||
}
|
||
// fmt.Print / Println / Printf(默认写 stdout)
|
||
if id, ok := v.X.(*ast.Ident); ok && id.Name == "fmt" {
|
||
switch v.Sel.Name {
|
||
case "Print", "Println", "Printf":
|
||
pos := fset.Position(v.Pos())
|
||
offenders = append(offenders, pos.String()+" fmt."+v.Sel.Name+" 写 stdout")
|
||
}
|
||
}
|
||
}
|
||
return true
|
||
})
|
||
}
|
||
}
|
||
|
||
if len(offenders) > 0 {
|
||
t.Errorf("工具可达的包里有 %d 处直接写终端——TUI 的 alt-screen 会被打穿。\n"+
|
||
"诊断请走 internal/diag(Printf/Println),子进程 stderr 用 diag.Indirect():\n %s",
|
||
len(offenders), strings.Join(offenders, "\n "))
|
||
}
|
||
}
|