项目文件夹

文件
Claude 1c8d8f0ff6 feat(cli): add micro loop to scaffold a self-improving repo loop
`micro loop init` writes an autonomous improvement loop into any repository —
the same planner/builder/triage loop that maintains go-micro, generalized:

- planner (loop-planner.yml): keeps a ranked queue in .github/loop/PRIORITIES.md
- builder (loop-builder.yml): builds the top open item as a single-concern PR,
  auto-merged on green CI
- triage (loop-triage.yml): turns CI failures into scoped fix issues

plus .github/loop/NORTH_STAR.md (direction) and PRIORITIES.md (queue).

The agent adapter is mention-based, not hardcoded to Codex: `--agent @codex`
(or any @mention agent that responds on an issue and can run gh), `--token-secret`,
`--branch`, `--ci-workflow`, and cron flags are the whole config-vs-core boundary.
Templates use << >> delimiters so GitHub Actions' own ${{ }} expressions pass
through untouched. `micro loop verify` checks the wiring and flags the two things
the CLI can't: the token secret and branch protection (the green-CI gate).

Built inside go-micro with the config/core split already drawn, so the workflows
can later be extracted to a standalone reusable-workflows repo without a rewrite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CmdEY7pYmV5zzwCjNJ4ykL
2026-07-02 09:07:19 +00:00

108 行
3.1 KiB
Go

package loop
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestRenderProducesValidPlaceholderFreeYAML(t *testing.T) {
cfg := config{
DefaultBranch: "main",
AgentMention: "@codex",
TokenSecret: "LOOP_TOKEN",
CIWorkflow: "CI",
PlannerCron: "0 * * * *",
BuilderCron: "30 * * * *",
}
for tmplName := range workflows {
rendered, err := render(tmplName, cfg)
if err != nil {
t.Fatalf("render %s: %v", tmplName, err)
}
s := string(rendered)
// No unresolved substitution delimiters should remain...
if strings.Contains(s, "<<") || strings.Contains(s, ">>") {
t.Errorf("%s still contains << >> placeholders after render", tmplName)
}
// ...but GitHub Actions' own ${{ }} expressions must survive verbatim.
if !strings.Contains(s, "${{ secrets.LOOP_TOKEN") {
t.Errorf("%s lost its ${{ secrets.LOOP_TOKEN }} expression", tmplName)
}
// The configured values must be substituted in.
if !strings.Contains(s, "@codex") {
t.Errorf("%s missing agent mention", tmplName)
}
// Structural sanity: a workflow needs these top-level keys.
for _, key := range []string{"name:", "on:", "jobs:"} {
if !strings.Contains(s, key) {
t.Errorf("%s missing top-level %q", tmplName, key)
}
}
}
}
func TestInitThenVerify(t *testing.T) {
dir := t.TempDir()
// A non-loop workflow so verify's CI-gate check passes.
mustWrite(t, filepath.Join(dir, ".github/workflows/ci.yml"), "name: CI\n")
if err := scaffold(dir, config{
DefaultBranch: "main",
AgentMention: "@codex",
TokenSecret: "LOOP_TOKEN",
CIWorkflow: "CI",
PlannerCron: "0 * * * *",
BuilderCron: "30 * * * *",
}, false); err != nil {
t.Fatalf("scaffold: %v", err)
}
for _, dest := range workflows {
if !fileExists(filepath.Join(dir, dest)) {
t.Errorf("expected %s to be written", dest)
}
}
for _, dest := range docs {
if !fileExists(filepath.Join(dir, dest)) {
t.Errorf("expected %s to be written", dest)
}
}
// A second scaffold without --force must fail on an existing workflow.
if err := scaffold(dir, config{DefaultBranch: "main", AgentMention: "@codex", TokenSecret: "LOOP_TOKEN", CIWorkflow: "CI", PlannerCron: "0 * * * *", BuilderCron: "30 * * * *"}, false); err == nil {
t.Error("expected second scaffold without --force to fail")
}
}
func TestVerifyMissingFilesFails(t *testing.T) {
dir := t.TempDir()
if _, missing := verifyState(dir); len(missing) == 0 {
t.Error("expected missing files in an empty dir")
}
}
func TestVerifyWarnsWithoutCIGate(t *testing.T) {
dir := t.TempDir()
if err := scaffold(dir, config{DefaultBranch: "main", AgentMention: "@codex", TokenSecret: "LOOP_TOKEN", CIWorkflow: "CI", PlannerCron: "0 * * * *", BuilderCron: "30 * * * *"}, false); err != nil {
t.Fatalf("scaffold: %v", err)
}
// Only loop-* workflows exist → no CI gate.
if hasCIWorkflow(dir) {
t.Error("expected no CI gate when only loop-* workflows are present")
}
}
func mustWrite(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}