Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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。

## 路线图分层

Expand Down Expand Up @@ -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。

## 验收门槛

Expand All @@ -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 规划最小测试夹具。
Original file line number Diff line number Diff line change
@@ -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=<id>` / `project=<id>` / `thread=<id>` 和最终 `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 ./...`
3 changes: 2 additions & 1 deletion docs/roadmaps/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -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。

## 近期任务

Expand All @@ -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。

Expand Down
97 changes: 97 additions & 0 deletions edge-server/internal/lifecycle/process_executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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{}
}
}
Loading