Why: Real transcripts (dev instance, 62 assistant turns) showed the artifact gate silently off and self-deadlocking. lintFile ran python3 -m housestyle.lint without the skills PYTHONPATH, so the import failed and nil was treated as "no issues" (same docx: 9 lint errors with PYTHONPATH, 0 without). stop() re-scored every artifact registered in the run, including superseded versions, so a fixed document could never pass and the composed reply reported the old verdict. ctxasm rendered history artifacts as "[产出工件 …]" - the exact pattern ClaimGate treats as a forged tool marker, so models copying the format were rejected. scoreReadable flagged table cells like "项目概述撰写" as column headings. Prompts and recipes said "python -m" (no python on PATH; exit 127) and housestyle.summarize was invoked as a module that does not exist. Evidence: SELF - new tests TestLintUnavailableFailsVisibleWhenSkillsRootSet, TestLintCommandEnvCarriesSkillsRoot, TestDocxHeadingMarkedAndTableCellNotFlagged, TestLatestDeliverablesKeepsLastPerName, TestRecipesUseRunnablePythonCommands, TestLibraryUsesRunnablePythonCommands; go test ./internal/... passes; manual: python3 -m housestyle.outline_cli reads a real artifact docx cleanly, lint without PYTHONPATH exits 1 with ModuleNotFoundError and is now surfaced as 自检未运行 when the skills root is configured. Impact: compose root injects quality.SetLintEnvRoot(<data>/skills). Final-turn checks score only the newest artifact per name; deliverable lists and reply cards follow the same dedupe. History artifact placeholders no longer collide with the forged-marker regex. docx text extraction marks Title/HeadingN styles so readability only judges real headings, with line numbers in the reason. Check reminders no longer invite the model to ignore failed checks. Oversized tool output is truncated by runes, not bytes. Refs: Dev task feature/agent-quality-phase01 (quality gap diagnosis 2026-10-09).
61 lines
1.7 KiB
Go
61 lines
1.7 KiB
Go
package skills
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
var (
|
|
barePythonRe = regexp.MustCompile(`(?:^|[^0-9A-Za-z_.-])python -(?:m|c)\b`)
|
|
housestyleRe = regexp.MustCompile(`python3 -m housestyle\.([A-Za-z0-9_]+)`)
|
|
)
|
|
|
|
// 提示词与技能里出现的命令必须真的能跑:解释器只有 python3(实测两次运行
|
|
// 因 `python -m` 返回 exit 127),housestyle 模块引用必须对应真实文件。
|
|
// 真实执行环境没有 `python` 这个名字,写 `python -m …` 的指引必然失败。
|
|
func TestRecipesUseRunnablePythonCommands(t *testing.T) {
|
|
recipes := "recipes"
|
|
var problems []string
|
|
err := filepath.WalkDir(recipes, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil || d.IsDir() || !strings.HasSuffix(path, ".md") {
|
|
return err
|
|
}
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body := string(raw)
|
|
for _, loc := range barePythonRe.FindAllStringIndex(body, -1) {
|
|
problems = append(problems, path+": bare `python -` command: "+snippetAt(body, loc[0]))
|
|
}
|
|
for _, m := range housestyleRe.FindAllStringSubmatch(body, -1) {
|
|
mod := m[1]
|
|
if _, err := os.Stat(filepath.Join(recipes, "house-style", "housestyle", mod+".py")); err != nil {
|
|
problems = append(problems, path+": references missing module housestyle/"+mod)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("walk recipes: %v", err)
|
|
}
|
|
if len(problems) > 0 {
|
|
t.Fatalf("unrunnable python commands in recipes:\n%s", strings.Join(problems, "\n"))
|
|
}
|
|
}
|
|
|
|
func snippetAt(body string, at int) string {
|
|
start := at
|
|
end := at + 60
|
|
if start > 0 && start > len(body) {
|
|
start = len(body)
|
|
}
|
|
if end > len(body) {
|
|
end = len(body)
|
|
}
|
|
return strings.ReplaceAll(body[start:end], "\n", " ")
|
|
}
|