diff --git a/docs/roadmap.md b/docs/roadmap.md index a0b1d454b..c8ec48e7d 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -4,7 +4,7 @@ ## 当前总目标 -推进 M2 Edge 本地数据层,让前端、后端、客户端三条线能围绕稳定的 Project / Thread / Run / Item / Event 模型并行开发。当前客户端 PR #30 已提供内存态最小实现,`feat/client-thread-messages-delicious233` 已补 message/item 写入链路,`feat/client-run-lifecycle-delicious233` 已抽出 Runner lifecycle 边界,`feat/client-store-boundary-delicious233` 已抽象 Edge store 接口边界,`feat/client-store-persistence-delicious233` 已提供轻量 JSON 文件持久化实现,`feat/client-edge-store-file-flag-delicious233` 已接入 Edge 启动参数 `--store-file`,`feat/client-runner-process-adapter-delicious233` 已补本地进程 executor 边界,`feat/client-runner-workdir-delicious233` 已补本地进程工作目录边界,`feat/client-runner-adapter-profile-delicious233` 已补 generic adapter profile / 命令模板最小层,`feat/client-runner-profile-preset-delicious233` 已补 `agenthub-runner-mock` preset 入口,`feat/client-runner-context-delicious233` 已让仓库自带 Runner mock 读取 Edge 注入的 Run 上下文,下一步重点是真实 Runner adapter。 +推进 M2 Edge 本地数据层,让前端、后端、客户端三条线能围绕稳定的 Project / Thread / Run / Item / Event 模型并行开发。当前客户端 PR #30 已提供内存态最小实现,`feat/client-thread-messages-delicious233` 已补 message/item 写入链路,`feat/client-run-lifecycle-delicious233` 已抽出 Runner lifecycle 边界,`feat/client-store-boundary-delicious233` 已抽象 Edge store 接口边界,`feat/client-store-persistence-delicious233` 已提供轻量 JSON 文件持久化实现,`feat/client-edge-store-file-flag-delicious233` 已接入 Edge 启动参数 `--store-file`,`feat/client-runner-process-adapter-delicious233` 已补本地进程 executor 边界,`feat/client-runner-workdir-delicious233` 已补本地进程工作目录边界,`feat/client-runner-adapter-profile-delicious233` 已补 generic adapter profile / 命令模板最小层,`feat/client-runner-profile-preset-delicious233` 已补 `agenthub-runner-mock` preset 入口,`feat/client-runner-context-delicious233` 已让仓库自带 Runner mock 读取 Edge 注入的 Run 上下文,`feat/client-runner-edge-smoke-delicious233` 已补 Edge ProcessExecutor 启动仓库 Runner mock CLI 的集成测试,下一步重点是真实 Runner adapter。 ## 路线图分层 @@ -34,7 +34,7 @@ - 前端:从 Mock 数据过渡到真实 REST / WebSocket client,承接 UI 同学设计。 - 后端:实现 Hub Server、Edge-Hub 通信、账号/群聊/同步/中继能力。 -- 客户端:PR #30 推进 Edge 本地数据层,`feat/client-thread-messages-delicious233` 补齐 message/item 写入链路,`feat/client-run-lifecycle-delicious233` 和 `feat/client-store-boundary-delicious233` 分别补齐 lifecycle/store 可替换边界,`feat/client-store-persistence-delicious233` 增加轻量 JSON 文件持久化 store,`feat/client-edge-store-file-flag-delicious233` 将文件 store 接入 Edge 启动入口,`feat/client-runner-process-adapter-delicious233` 增加可测试的本地进程 executor,`feat/client-runner-workdir-delicious233` 增加本地进程工作目录配置,`feat/client-runner-adapter-profile-delicious233` 增加可测试的 generic adapter profile / 命令模板最小层,`feat/client-runner-profile-preset-delicious233` 增加可测试的 `agenthub-runner-mock` preset 入口,`feat/client-runner-context-delicious233` 让 mock Runner stdout 稳定带上 Run / Project / Thread 上下文,后续继续做真实 Runner adapter。 +- 客户端:PR #30 推进 Edge 本地数据层,`feat/client-thread-messages-delicious233` 补齐 message/item 写入链路,`feat/client-run-lifecycle-delicious233` 和 `feat/client-store-boundary-delicious233` 分别补齐 lifecycle/store 可替换边界,`feat/client-store-persistence-delicious233` 增加轻量 JSON 文件持久化 store,`feat/client-edge-store-file-flag-delicious233` 将文件 store 接入 Edge 启动入口,`feat/client-runner-process-adapter-delicious233` 增加可测试的本地进程 executor,`feat/client-runner-workdir-delicious233` 增加本地进程工作目录配置,`feat/client-runner-adapter-profile-delicious233` 增加可测试的 generic adapter profile / 命令模板最小层,`feat/client-runner-profile-preset-delicious233` 增加可测试的 `agenthub-runner-mock` preset 入口,`feat/client-runner-context-delicious233` 让 mock Runner stdout 稳定带上 Run / Project / Thread 上下文,`feat/client-runner-edge-smoke-delicious233` 验证 Edge ProcessExecutor 能启动仓库 Runner mock CLI 并聚合真实上下文输出,后续继续做真实 Runner adapter。 ## 验收门槛 @@ -55,6 +55,7 @@ - [x] 为 Edge 本地进程 executor 增加 generic adapter profile / 命令模板最小层,支持从 Run 上下文展开 args/env。 - [x] 为 Edge 启动入口增加 `--runner-profile agenthub-runner-mock` preset,默认使用仓库自带 Runner mock CLI。 - [x] 让仓库自带 `agenthub-runner --mock` 读取 Edge 注入的 Run / Project / Thread 环境变量,并在 stdout 输出稳定上下文行。 +- [x] 增加 Edge ProcessExecutor 到仓库 Runner mock CLI 的集成测试,覆盖 `run.started`、stdout `run.output.batch`、上下文输出和 `run.finished`。 - [ ] 将 Runner 真正接入 Edge Run lifecycle,替换 handler 内置 mock flow。 - [ ] M2 完成后归档或更新 `docs/client-roadmap.md`,避免路线图重复。 - [ ] 为 Runner 真实 CLI adapter 规划最小测试夹具。 diff --git a/docs/roadmaps/branches/feat-client-runner-edge-smoke-delicious233.md b/docs/roadmaps/branches/feat-client-runner-edge-smoke-delicious233.md new file mode 100644 index 000000000..567e141ca --- /dev/null +++ b/docs/roadmaps/branches/feat-client-runner-edge-smoke-delicious233.md @@ -0,0 +1,32 @@ +# feat/client-runner-edge-smoke-delicious233 路线图 + +最后更新:2026-05-23 + +## 当前目标 + +- [x] 用自动化测试证明 Edge `ProcessExecutor` 能启动仓库自带 Runner mock CLI,并在 Run 事件流中看到真实 Run / Project / Thread 上下文。 + +## 写入范围 + +- `edge-server/internal/lifecycle/` +- `docs/roadmap.md` +- `docs/roadmaps/client.md` +- `docs/roadmaps/branches/feat-client-runner-edge-smoke-delicious233.md` + +## 已完成 + +- [x] 在 `process_executor_test.go` 增加真实 Runner mock CLI 集成测试,通过当前 Go 工具链执行 `go run ./cmd/agenthub-runner --mock`。 +- [x] 测试断言 `run.started`、stdout `run.output.batch`、stdout 中的 `run=` / `project=` / `thread=` 和最终 `run.finished`。 +- [x] 未修改公共 REST / WebSocket 契约。 + +## 下一步 + +- [ ] 后续真实 Runner adapter 接入 Edge Run lifecycle 时,复用本测试作为仓库 mock CLI 的回归保护。 + +## 验收 + +- [x] `cd edge-server; go test -count=1 ./internal/lifecycle -run TestProcessExecutorRunsRepositoryMockRunnerWithInjectedContext -v` +- [x] `git diff --check` +- [x] `python -c "import yaml, pathlib; yaml.safe_load(pathlib.Path('api/openapi.yaml').read_text(encoding='utf-8')); print('yaml ok')"` +- [x] `cd edge-server; go test -count=1 ./...` +- [x] `cd runner; go test -count=1 ./...` diff --git a/docs/roadmaps/client.md b/docs/roadmaps/client.md index 17538a7f8..b4680bb65 100644 --- a/docs/roadmaps/client.md +++ b/docs/roadmaps/client.md @@ -12,7 +12,7 @@ ## 当前目标 -推进 M2 Edge 本地数据层,把 M1 的内存事件流升级为 Project / Thread / Run / Item / Event 模型。当前 PR #30 已完成内存态最小模型,`feat/client-thread-messages-delicious233` 已补 message/item 写入链路,`feat/client-run-lifecycle-delicious233` 已抽出 Edge Run lifecycle executor 边界,`feat/client-store-boundary-delicious233` 已抽象可替换 store 接口,`feat/client-store-persistence-delicious233` 已提供轻量 JSON 文件持久化实现,`feat/client-edge-store-file-flag-delicious233` 已将文件 store 接入 Edge 启动参数,`feat/client-runner-process-adapter-delicious233` 已补本地进程 executor 边界,`feat/client-runner-workdir-delicious233` 已补本地进程工作目录配置边界,`feat/client-runner-adapter-profile-delicious233` 已补 generic adapter profile / 命令模板最小层,`feat/client-runner-profile-preset-delicious233` 已补仓库自带 mock Runner preset 入口,`feat/client-runner-context-delicious233` 已补 Runner mock 读取 Edge 注入上下文的最小契约,后续继续补真实 Runner adapter。 +推进 M2 Edge 本地数据层,把 M1 的内存事件流升级为 Project / Thread / Run / Item / Event 模型。当前 PR #30 已完成内存态最小模型,`feat/client-thread-messages-delicious233` 已补 message/item 写入链路,`feat/client-run-lifecycle-delicious233` 已抽出 Edge Run lifecycle executor 边界,`feat/client-store-boundary-delicious233` 已抽象可替换 store 接口,`feat/client-store-persistence-delicious233` 已提供轻量 JSON 文件持久化实现,`feat/client-edge-store-file-flag-delicious233` 已将文件 store 接入 Edge 启动参数,`feat/client-runner-process-adapter-delicious233` 已补本地进程 executor 边界,`feat/client-runner-workdir-delicious233` 已补本地进程工作目录配置边界,`feat/client-runner-adapter-profile-delicious233` 已补 generic adapter profile / 命令模板最小层,`feat/client-runner-profile-preset-delicious233` 已补仓库自带 mock Runner preset 入口,`feat/client-runner-context-delicious233` 已补 Runner mock 读取 Edge 注入上下文的最小契约,`feat/client-runner-edge-smoke-delicious233` 已补 Edge ProcessExecutor 启动仓库 Runner mock CLI 的集成测试,后续继续补真实 Runner adapter。 ## 近期任务 @@ -33,6 +33,7 @@ - [x] 增加 generic adapter profile / 命令模板最小层,覆盖 args/env 的 Run 占位符展开、未知占位符错误、固定 args 兼容和 workdir 不回退。 - [x] 增加 `--runner-profile agenthub-runner-mock`,覆盖默认 command、command override、用户 args/env 追加和未知 profile 错误。 - [x] 增加 Runner mock 上下文读取边界,覆盖 `AGENTHUB_RUN_ID` 默认值、env 注入 run ID 和 stdout 上下文输出。 +- [x] 增加 Edge ProcessExecutor 到仓库 Runner mock CLI 的集成测试,覆盖真实 env 注入、stdout event 聚合、Run / Project / Thread 上下文输出和完成事件。 - [ ] 将真实 Runner adapter 接入 Edge Run lifecycle。 - [ ] 细化 Project / Thread / Item 的 OpenAPI 响应 schema。 diff --git a/edge-server/internal/lifecycle/process_executor_test.go b/edge-server/internal/lifecycle/process_executor_test.go index a3e0bb0ca..de6752ad5 100644 --- a/edge-server/internal/lifecycle/process_executor_test.go +++ b/edge-server/internal/lifecycle/process_executor_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strings" "testing" "time" @@ -152,6 +153,91 @@ func TestProcessExecutorPublishesOutputAndFinished(t *testing.T) { } } +func TestProcessExecutorRunsRepositoryMockRunnerWithInjectedContext(t *testing.T) { + bus := events.NewBus(100) + s := store.New() + run := newExecutorTestRun(t, s) + _, ch, _ := bus.Subscribe(0) + + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("Getwd returned error: %v", err) + } + runnerDir := filepath.Clean(filepath.Join(cwd, "..", "..", "..", "runner")) + goCommand := filepath.Join(runtime.GOROOT(), "bin", "go") + if runtime.GOOS == "windows" { + goCommand += ".exe" + } + + executor, err := NewProcessExecutor(bus, s, ProcessExecutorConfig{ + Command: goCommand, + Args: []string{"run", "./cmd/agenthub-runner", "--mock"}, + WorkDir: runnerDir, + }) + if err != nil { + t.Fatalf("NewProcessExecutor returned error: %v", err) + } + + if err := executor.Start(run); err != nil { + t.Fatalf("Start returned error: %v", err) + } + + var sawStarted bool + var sawStdoutBatch bool + var stdoutText string + for { + evt := nextEventWithin(t, ch, 20*time.Second) + if evt.Scope["runId"] != run.ID { + t.Fatalf("event scope runId = %#v, want %q", evt.Scope["runId"], run.ID) + } + switch evt.Type { + case "run.started": + sawStarted = true + case "run.output.batch": + payload, ok := evt.Payload.(map[string]any) + if !ok { + t.Fatalf("output payload = %T, want map", evt.Payload) + } + if payload["runId"] != run.ID { + t.Fatalf("output payload runId = %#v, want %q", payload["runId"], run.ID) + } + if payload["stream"] != "stdout" { + continue + } + chunks, ok := payload["chunks"].([]map[string]any) + if !ok || len(chunks) == 0 { + t.Fatalf("output chunks = %#v, want non-empty []map[string]any", payload["chunks"]) + } + sawStdoutBatch = true + text, _ := chunks[0]["text"].(string) + stdoutText += text + case "run.finished": + if !sawStarted { + t.Fatal("run.finished arrived before run.started") + } + if !sawStdoutBatch { + t.Fatal("run.finished arrived without stdout run.output.batch") + } + for _, want := range []string{ + "run=" + run.ID, + "project=" + run.ProjectID, + "thread=" + run.ThreadID, + } { + if !strings.Contains(stdoutText, want) { + t.Fatalf("stdout text = %q, want %q", stdoutText, want) + } + } + return + case "run.failed": + t.Fatalf("repository mock runner failed: %#v", evt.Payload) + case "run.cancelled": + t.Fatalf("repository mock runner was cancelled: %#v", evt.Payload) + default: + t.Fatalf("unexpected event type %q", evt.Type) + } + } +} + func TestProcessExecutorRunsCommandInConfiguredWorkDir(t *testing.T) { bus := events.NewBus(100) s := store.New() @@ -614,3 +700,14 @@ func collectStdoutUntilFinished(t *testing.T, ch <-chan events.EventEnvelope) st } } } + +func nextEventWithin(t *testing.T, ch <-chan events.EventEnvelope, timeout time.Duration) events.EventEnvelope { + t.Helper() + select { + case evt := <-ch: + return evt + case <-time.After(timeout): + t.Fatalf("timed out after %s waiting for event", timeout) + return events.EventEnvelope{} + } +}