feat: edge node registry, opsconsole, service 수정 및 agent-ops 스크립트 업데이트
- edge node registry: 노드 레지스트리 기능 개선 - edge opsconsole: 콘솔 및 이벤트 처리 로직 수정 - edge service: 서비스 레이어 변경 - agent-ops: init-agent-ops, sync-pull, sync-push 스킬 및 스크립트 업데이트 - 새 스크립트 추가: entry-files.sh
This commit is contained in:
parent
92042bb2a3
commit
cd9d5830c7
13 changed files with 241 additions and 87 deletions
24
agent-ops/bin/entry-files.sh
Normal file
24
agent-ops/bin/entry-files.sh
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Shared entry-point file list for init/sync scripts.
|
||||
AGENT_OPS_ENTRY_FILES=("GEMINI.md" "CLAUDE.md" "AGENTS.md" ".cursorrules" ".clinerules")
|
||||
|
||||
apply_agent_ops_entry_files() {
|
||||
local rules_md="$1"
|
||||
local target_root="$2"
|
||||
|
||||
if [[ ! -f "$rules_md" ]]; then
|
||||
if [[ -n "${YELLOW:-}" && -n "${RESET:-}" ]]; then
|
||||
echo -e "${YELLOW} rules.md 없음, 진입 파일 재적용 건너뜀${RESET}"
|
||||
else
|
||||
echo " rules.md 없음, 진입 파일 재적용 건너뜀"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
local f
|
||||
for f in "${AGENT_OPS_ENTRY_FILES[@]}"; do
|
||||
cp "$rules_md" "$target_root/$f"
|
||||
echo " 진입 파일 적용: $f"
|
||||
done
|
||||
}
|
||||
|
|
@ -5,13 +5,16 @@
|
|||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR=$(realpath "$(dirname "$0")")
|
||||
SOURCE_DIR=$(realpath "$SCRIPT_DIR/..")
|
||||
source "$SCRIPT_DIR/entry-files.sh"
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <target_directory>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TARGET_DIR=$(realpath "$1")
|
||||
SOURCE_DIR=$(realpath "$(dirname "$0")/..")
|
||||
ARCHIVE_IGNORE_PATTERN="agent-task/archive/**"
|
||||
|
||||
append_unique_line() {
|
||||
|
|
@ -34,16 +37,14 @@ mkdir -p "$TARGET_DIR/agent-ops/skills/project"
|
|||
|
||||
# 2. 공통 요소 복사 (프로젝트 전용 설정은 제외)
|
||||
cp "$SOURCE_DIR/.version" "$TARGET_DIR/agent-ops/"
|
||||
rm -rf "$TARGET_DIR/agent-ops/bin"
|
||||
cp -r "$SOURCE_DIR/bin" "$TARGET_DIR/agent-ops/"
|
||||
cp -r "$SOURCE_DIR/rules/common" "$TARGET_DIR/agent-ops/rules/"
|
||||
cp -r "$SOURCE_DIR/skills/common" "$TARGET_DIR/agent-ops/skills/"
|
||||
|
||||
# 3. 에이전트 진입 파일 생성 (common/rules.md 복사)
|
||||
COMMON_RULES="$SOURCE_DIR/rules/common/rules.md"
|
||||
cp "$COMMON_RULES" "$TARGET_DIR/GEMINI.md"
|
||||
cp "$COMMON_RULES" "$TARGET_DIR/CLAUDE.md"
|
||||
cp "$COMMON_RULES" "$TARGET_DIR/AGENTS.md"
|
||||
cp "$COMMON_RULES" "$TARGET_DIR/.cursorrules"
|
||||
cp "$COMMON_RULES" "$TARGET_DIR/.clinerules"
|
||||
apply_agent_ops_entry_files "$COMMON_RULES" "$TARGET_DIR"
|
||||
|
||||
# 4. AI ignore / permission 설정
|
||||
append_unique_line "$TARGET_DIR/.geminiignore" "$ARCHIVE_IGNORE_PATTERN"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ PROJECT_ROOT="$(cd "$AGENT_OPS_DIR/.." && pwd)"
|
|||
# ── 색상 ─────────────────────────────────────────────────────────────────────
|
||||
RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'; RESET='\033[0m'
|
||||
|
||||
# ── 진입 파일 공통 관리 ──────────────────────────────────────────────────────
|
||||
source "$SCRIPT_DIR/entry-files.sh"
|
||||
|
||||
# ── 대상 경로 해석 (폴더명 / 상대경로 / 절대경로) ────────────────────────────
|
||||
resolve_target() {
|
||||
local input="$1"
|
||||
|
|
@ -68,34 +71,6 @@ sync_common() {
|
|||
sync_folder "$src/bin" "$dst/bin"
|
||||
}
|
||||
|
||||
# ── rules.md 내용을 진입 파일들에 반영 ───────────────────────────────────────
|
||||
apply_entry_files() {
|
||||
local rules_md="$1"
|
||||
local target_root="$2"
|
||||
|
||||
if [[ ! -f "$rules_md" ]]; then
|
||||
echo -e "${YELLOW} rules.md 없음, 진입 파일 재적용 건너뜀${RESET}"
|
||||
return
|
||||
fi
|
||||
|
||||
# 기본 자동 생성/갱신 대상
|
||||
local default_files=("CLAUDE.md" "GEMINI.md" ".cursorrules" "AGENTS.md")
|
||||
# 존재할 때만 갱신
|
||||
local optional_files=(".clinerules")
|
||||
|
||||
for f in "${default_files[@]}"; do
|
||||
cp "$rules_md" "$target_root/$f"
|
||||
echo " 진입 파일 적용: $f"
|
||||
done
|
||||
|
||||
for f in "${optional_files[@]}"; do
|
||||
if [[ -f "$target_root/$f" ]]; then
|
||||
cp "$rules_md" "$target_root/$f"
|
||||
echo " 진입 파일 적용: $f"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ── 사용법 ────────────────────────────────────────────────────────────────────
|
||||
usage() {
|
||||
echo "사용법: $0 [--pull] <target>"
|
||||
|
|
@ -144,10 +119,10 @@ if [[ "$PULL_MODE" == "1" ]]; then
|
|||
echo " 현재 버전: $SRC_VER | framework 버전: $DST_VER"
|
||||
sync_common "$DST_AO" "$SRC_AO"
|
||||
cp "$DST_AO/.version" "$SRC_AO/.version"
|
||||
apply_entry_files "$DST_AO/rules/common/rules.md" "$PROJECT_ROOT"
|
||||
apply_agent_ops_entry_files "$DST_AO/rules/common/rules.md" "$PROJECT_ROOT"
|
||||
cd "$PROJECT_ROOT"
|
||||
git add agent-ops/rules/common agent-ops/skills/common agent-ops/bin agent-ops/.version \
|
||||
CLAUDE.md GEMINI.md .cursorrules AGENTS.md
|
||||
"${AGENT_OPS_ENTRY_FILES[@]}"
|
||||
git commit -m "sync: pull from agentic-framework v$DST_VER"
|
||||
git push
|
||||
echo -e "${GREEN}✓ 완료 (pull v$DST_VER)${RESET}"
|
||||
|
|
@ -161,12 +136,13 @@ if [[ "$IS_FRAMEWORK" == "1" ]]; then
|
|||
if [[ ! -d "$DST_AO" ]]; then
|
||||
echo " 최초 진행: agent-ops 전체 복사"
|
||||
cp -r "$SRC_AO" "$TARGET/"
|
||||
apply_agent_ops_entry_files "$SRC_AO/rules/common/rules.md" "$TARGET"
|
||||
echo -e "${YELLOW} init-agent-ops 스킬로 초기화를 진행하세요.${RESET}"
|
||||
else
|
||||
echo " 이후 진행: common/ 동기화"
|
||||
sync_common "$SRC_AO" "$DST_AO"
|
||||
cp "$SRC_AO/.version" "$DST_AO/.version"
|
||||
apply_entry_files "$SRC_AO/rules/common/rules.md" "$TARGET"
|
||||
apply_agent_ops_entry_files "$SRC_AO/rules/common/rules.md" "$TARGET"
|
||||
fi
|
||||
echo -e "${GREEN}✓ 완료 → $(basename "$TARGET")${RESET}"
|
||||
exit 0
|
||||
|
|
|
|||
|
|
@ -62,8 +62,9 @@ description: 프로젝트 상태를 판별하고 agent-ops 기본 스캐폴드
|
|||
|
||||
| 파일 | 방법 |
|
||||
|------|------|
|
||||
| `CLAUDE.md`,`GEMINI.md`,`AGENTS.md`, `.cursorrules`, `.clinerules` 등 진입 파일 | `rules/common/rules.md`를 에이전트별 파일명으로 프로젝트 루트에 복사 후 `rules/common/rules.md` 삭제 |
|
||||
| `GEMINI.md`, `CLAUDE.md`, `AGENTS.md`, `.cursorrules`, `.clinerules` 등 진입 파일 | `rules/common/rules.md`를 `agent-ops/bin/entry-files.sh`의 파일 목록으로 프로젝트 루트에 복사 |
|
||||
| `agent-ops/.version` | 공통 관리 레포의 VERSION 파일을 그대로 복사 (프레임워크 버전 추적용) |
|
||||
| `agent-ops/bin/` | 공통 스크립트 전체 복사 (진입 파일 목록의 단일 기준인 `entry-files.sh` 포함) |
|
||||
| `agent-ops/rules/common/` | 공통 폴더 전체 복사 (수정 금지) |
|
||||
| `agent-ops/skills/common/` | 공통 폴더 전체 복사 (수정 금지) |
|
||||
| `agent-ops/rules/project/rules.md` | 프로젝트 분석 후 생성 |
|
||||
|
|
@ -75,10 +76,12 @@ description: 프로젝트 상태를 판별하고 agent-ops 기본 스캐폴드
|
|||
|
||||
에이전트별 파일명:
|
||||
|
||||
실제 생성/동기화 대상 목록은 `agent-ops/bin/entry-files.sh`의 `AGENT_OPS_ENTRY_FILES`를 단일 기준으로 사용한다.
|
||||
|
||||
| 에이전트 | 파일명 |
|
||||
|---------|--------|
|
||||
| Claude | `CLAUDE.md` |
|
||||
| Gemini | `GEMINI.md` |
|
||||
| Claude | `CLAUDE.md` |
|
||||
| Kilo Code / OpenCode | `AGENTS.md` |
|
||||
| Cursor | `.cursorrules` |
|
||||
| Cline | `.clinerules` |
|
||||
|
|
@ -167,8 +170,7 @@ common/rules.md와 내용이 중복되지 않도록 한다.
|
|||
- 프로젝트 구조, 모듈 경계, agent-ops 파일 유무를 분석하여 신규/운영중을 판별한다
|
||||
|
||||
2. **에이전트 진입 파일 생성**
|
||||
- `rules/common/rules.md`를 에이전트별 파일명으로 프로젝트 루트에 복사한다
|
||||
- 복사 후 `rules/common/rules.md`를 삭제한다
|
||||
- `rules/common/rules.md`를 `agent-ops/bin/entry-files.sh`의 파일 목록으로 프로젝트 루트에 복사한다
|
||||
|
||||
3. **AI ignore / permission 기본 설정**
|
||||
- `.geminiignore`, `.aiexclude`, `.cursorignore`, `.clineignore`에 `agent-task/archive/**`를 추가한다
|
||||
|
|
@ -176,7 +178,7 @@ common/rules.md와 내용이 중복되지 않도록 한다.
|
|||
- `agent-task/archive/**` 제외는 `.gitignore`에 추가하지 않는다
|
||||
|
||||
4. **agent-ops 폴더 구조 복사**
|
||||
- 공통 관리 레포의 `agent-ops/` 공통 폴더(rules/common, skills/common)를 복사한다
|
||||
- 공통 관리 레포의 `agent-ops/` 공통 폴더(bin, rules/common, skills/common)를 복사한다
|
||||
- `agent-ops/.version` 파일을 복사한다
|
||||
|
||||
5. **rules/project/rules.md 생성**
|
||||
|
|
@ -221,7 +223,7 @@ common/rules.md와 내용이 중복되지 않도록 한다.
|
|||
|
||||
## 실행 결과 검증
|
||||
|
||||
- [ ] 진입 파일이 프로젝트 루트에 존재하고, 내용이 `rules/common/rules.md`와 일치하는가
|
||||
- [ ] `agent-ops/bin/entry-files.sh`의 모든 진입 파일이 프로젝트 루트에 존재하고, 내용이 초기화에 사용한 `rules/common/rules.md`와 일치하는가
|
||||
- [ ] `agent-ops/rules/project/rules.md`가 생성되었고, 필수 항목(응답 언어, 프로젝트 개요, 기술 스택)이 포함되어 있는가
|
||||
- [ ] 생성된 domain rules.md가 `domain-rule-template.md` 형식을 따르는가
|
||||
- [ ] `rules/project/rules.md`의 도메인 매핑 테이블에 생성된 도메인이 모두 등록되어 있는가
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ agent-ops/bin/sync.sh --pull <target>
|
|||
|
||||
- [ ] sync.sh 가 오류 없이 완료됐는가
|
||||
- [ ] 현재 프로젝트 버전이 framework 버전과 일치하는가
|
||||
- [ ] 진입 파일(CLAUDE.md 등)이 갱신됐는가
|
||||
- [ ] `agent-ops/bin/entry-files.sh`의 모든 진입 파일이 갱신됐고, 내용이 framework의 `agent-ops/rules/common/rules.md`와 일치하는가
|
||||
|
||||
## 금지 사항
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
---
|
||||
name: sync-push
|
||||
description: 현재 프로젝트의 agent-ops를 agentic-framework로 올린다. "agent-ops 싱크해", "agent-ops 동기화해", "agentic-framework에 올려줘" 요청 시 사용한다.
|
||||
description: 현재 프로젝트의 agent-ops를 agentic-framework로 올리거나, agentic-framework의 공통 agent-ops를 대상 프로젝트로 push한다. "agent-ops 싱크해", "agent-ops 동기화해", "agentic-framework에 올려줘" 요청 시 사용한다.
|
||||
---
|
||||
|
||||
# sync-push
|
||||
|
||||
## 목적
|
||||
|
||||
`agent-ops/bin/sync.sh`를 호출해 현재 프로젝트 → agentic-framework 방향으로 agent-ops를 올린다.
|
||||
`agent-ops/bin/sync.sh`를 호출해 agent-ops 공통 파일을 push 방향으로 동기화한다.
|
||||
|
||||
- 일반 프로젝트에서는 현재 프로젝트 → agentic-framework 방향으로 올린다.
|
||||
- agentic-framework 원본 프로젝트에서는 agentic-framework → 대상 프로젝트 방향으로 보낸다.
|
||||
|
||||
## 언제 호출할지
|
||||
|
||||
|
|
@ -28,6 +31,19 @@ description: 현재 프로젝트의 agent-ops를 agentic-framework로 올린다.
|
|||
1. 사용자 요청에서 target 프로젝트명 또는 경로를 추출한다
|
||||
2. **명시된 경우**: `agent-ops/bin/sync.sh <target>` 실행
|
||||
3. **명시되지 않은 경우**: 사용자에게 대상 프로젝트 입력을 요청한다
|
||||
4. `sync.sh`는 현재 agentic-framework의 `agent-ops/rules/common/rules.md` 내용을 대상 프로젝트 루트의 진입 파일에 덮어쓴다
|
||||
|
||||
덮어쓰기 대상은 `init-agent-ops` 초기 세팅과 동일하며, 실제 목록은 `agent-ops/bin/entry-files.sh`의 `AGENT_OPS_ENTRY_FILES`를 단일 기준으로 사용한다.
|
||||
|
||||
| 에이전트 | 파일명 |
|
||||
|---------|--------|
|
||||
| Gemini | `GEMINI.md` |
|
||||
| Claude | `CLAUDE.md` |
|
||||
| Kilo Code / OpenCode | `AGENTS.md` |
|
||||
| Cursor | `.cursorrules` |
|
||||
| Cline | `.clinerules` |
|
||||
|
||||
대상 프로젝트에 기존 진입 파일이 있어도 보존하거나 병합하지 않고 `agent-ops/rules/common/rules.md` 내용으로 교체한다.
|
||||
|
||||
```bash
|
||||
agent-ops/bin/sync.sh <target>
|
||||
|
|
@ -37,6 +53,7 @@ agent-ops/bin/sync.sh <target>
|
|||
|
||||
- [ ] sync.sh 가 오류 없이 완료됐는가
|
||||
- [ ] 버전 충돌 경고가 없었는가 — 있었다면 사용자에게 수동 머지 필요함을 알린다
|
||||
- [ ] agentic-framework에서 대상 프로젝트로 push한 경우, `agent-ops/bin/entry-files.sh`의 모든 진입 파일 내용이 현재 agentic-framework의 `agent-ops/rules/common/rules.md`와 일치하는가
|
||||
|
||||
## 금지 사항
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ package node
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
toki "git.toki-labs.com/toki/common-proto-socket/go"
|
||||
|
|
@ -21,6 +24,7 @@ type Registry struct {
|
|||
mu sync.RWMutex
|
||||
byID map[string]*NodeEntry
|
||||
byAlias map[string]*NodeEntry
|
||||
byIndex map[int]*NodeEntry
|
||||
indexByID map[string]int
|
||||
nextIndex int
|
||||
}
|
||||
|
|
@ -29,6 +33,7 @@ func NewRegistry() *Registry {
|
|||
return &Registry{
|
||||
byID: make(map[string]*NodeEntry),
|
||||
byAlias: make(map[string]*NodeEntry),
|
||||
byIndex: make(map[int]*NodeEntry),
|
||||
indexByID: make(map[string]int),
|
||||
}
|
||||
}
|
||||
|
|
@ -50,6 +55,7 @@ func (r *Registry) Register(entry *NodeEntry) {
|
|||
}
|
||||
}
|
||||
r.byID[entry.NodeID] = entry
|
||||
r.byIndex[entry.Index] = entry
|
||||
if entry.Alias != "" {
|
||||
r.byAlias[entry.Alias] = entry
|
||||
}
|
||||
|
|
@ -78,6 +84,7 @@ func (r *Registry) Unregister(nodeID string) {
|
|||
if entry.Alias != "" {
|
||||
delete(r.byAlias, entry.Alias)
|
||||
}
|
||||
delete(r.byIndex, entry.Index)
|
||||
delete(r.byID, nodeID)
|
||||
}
|
||||
}
|
||||
|
|
@ -100,6 +107,9 @@ func (r *Registry) Resolve(ref string) (*NodeEntry, error) {
|
|||
if entry, ok := r.byAlias[ref]; ok {
|
||||
return entry, nil
|
||||
}
|
||||
if entry, ok := r.resolveDisplayLabelLocked(ref); ok {
|
||||
return entry, nil
|
||||
}
|
||||
return nil, fmt.Errorf("node %q not found", ref)
|
||||
}
|
||||
|
||||
|
|
@ -114,6 +124,30 @@ func (r *Registry) Resolve(ref string) (*NodeEntry, error) {
|
|||
return nil, fmt.Errorf("multiple nodes connected; select one with /node <id|alias>")
|
||||
}
|
||||
|
||||
func (r *Registry) resolveDisplayLabelLocked(ref string) (*NodeEntry, bool) {
|
||||
idx, ok := parseDisplayNodeIndex(ref)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
entry, ok := r.byIndex[idx]
|
||||
return entry, ok
|
||||
}
|
||||
|
||||
func parseDisplayNodeIndex(ref string) (int, bool) {
|
||||
if !strings.HasPrefix(ref, "node") {
|
||||
return 0, false
|
||||
}
|
||||
raw := strings.TrimPrefix(ref, "node")
|
||||
if raw == "" {
|
||||
return 0, false
|
||||
}
|
||||
idx, err := strconv.Atoi(raw)
|
||||
if err != nil || idx < 0 {
|
||||
return 0, false
|
||||
}
|
||||
return idx, true
|
||||
}
|
||||
|
||||
func (r *Registry) All() []*NodeEntry {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
|
@ -121,6 +155,15 @@ func (r *Registry) All() []*NodeEntry {
|
|||
for _, e := range r.byID {
|
||||
out = append(out, e)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].HasIndex != out[j].HasIndex {
|
||||
return out[i].HasIndex
|
||||
}
|
||||
if out[i].HasIndex && out[i].Index != out[j].Index {
|
||||
return out[i].Index < out[j].Index
|
||||
}
|
||||
return out[i].NodeID < out[j].NodeID
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,19 @@ func TestRegistry_Resolve_ByAliasOrID(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRegistry_Resolve_ByDisplayLabel(t *testing.T) {
|
||||
reg := edgenode.NewRegistry()
|
||||
reg.Register(&edgenode.NodeEntry{NodeID: "node-001", Alias: "local-node"})
|
||||
reg.Register(&edgenode.NodeEntry{NodeID: "node-002", Alias: "remote-node"})
|
||||
|
||||
if e, err := reg.Resolve("node0"); err != nil || e.NodeID != "node-001" {
|
||||
t.Fatalf("Resolve(node0) = %+v, %v; want node-001", e, err)
|
||||
}
|
||||
if e, err := reg.Resolve("node1"); err != nil || e.NodeID != "node-002" {
|
||||
t.Fatalf("Resolve(node1) = %+v, %v; want node-002", e, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistry_Resolve_ImplicitSingleNodeOnly(t *testing.T) {
|
||||
reg := edgenode.NewRegistry()
|
||||
|
||||
|
|
|
|||
|
|
@ -212,10 +212,10 @@ func PrintNodes(out io.Writer, nodes []edgeservice.NodeSnapshot, selectedRef str
|
|||
}
|
||||
for _, node := range nodes {
|
||||
marker := " "
|
||||
if selectedRef != "" && (node.NodeID == selectedRef || node.Alias == selectedRef) {
|
||||
if selectedRef != "" && (node.NodeID == selectedRef || node.Alias == selectedRef || node.Label == selectedRef) {
|
||||
marker = "* "
|
||||
}
|
||||
fmt.Fprintf(out, "%s%s (%s)\n", marker, node.NodeID, node.Alias)
|
||||
fmt.Fprintf(out, "%s%s = %s (%s)\n", marker, node.Label, node.NodeID, node.Alias)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -121,10 +121,10 @@ func TestPrintNodes_ShowsSelectedNode(t *testing.T) {
|
|||
PrintNodes(&out, svc.ListNodeSnapshots(), "alias-1")
|
||||
|
||||
got := out.String()
|
||||
if !strings.Contains(got, "* node-1 (alias-1)") {
|
||||
if !strings.Contains(got, "* node0 = node-1 (alias-1)") {
|
||||
t.Errorf("expected selected node-1 to have marker, got:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(got, " node-2 (alias-2)") {
|
||||
if !strings.Contains(got, " node1 = node-2 (alias-2)") {
|
||||
t.Errorf("expected non-selected node-2 to have no marker, got:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
|
@ -132,11 +132,11 @@ func TestPrintNodes_ShowsSelectedNode(t *testing.T) {
|
|||
func TestPrintNodes_AcceptsSnapshotDTO(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
PrintNodes(&out, []edgeservice.NodeSnapshot{
|
||||
{NodeID: "node-a", Alias: "alpha", Label: "alpha"},
|
||||
{NodeID: "node-a", Alias: "alpha", Label: "node0"},
|
||||
}, "alpha")
|
||||
|
||||
got := out.String()
|
||||
if !strings.Contains(got, "* node-a (alpha)") {
|
||||
if !strings.Contains(got, "* node0 = node-a (alpha)") {
|
||||
t.Errorf("expected DTO render to mark selected, got:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,21 +18,25 @@ import (
|
|||
// Foreground runs tracked via TrackForeground are suppressed from the
|
||||
// asynchronous stream so the SendRun loop owns their output.
|
||||
type EventRouter struct {
|
||||
mu sync.Mutex
|
||||
out io.Writer
|
||||
registry *edgenode.Registry
|
||||
logger *zap.Logger
|
||||
foreground map[string]struct{}
|
||||
streams map[string]*ResponseStream
|
||||
mu sync.Mutex
|
||||
out io.Writer
|
||||
registry *edgenode.Registry
|
||||
logger *zap.Logger
|
||||
foreground map[string]struct{}
|
||||
streams map[string]*ResponseStream
|
||||
nodeLabels map[string]string
|
||||
aliasLabels map[string]string
|
||||
}
|
||||
|
||||
func NewEventRouter(out io.Writer, registry *edgenode.Registry, logger *zap.Logger) *EventRouter {
|
||||
return &EventRouter{
|
||||
out: out,
|
||||
registry: registry,
|
||||
logger: logger,
|
||||
foreground: make(map[string]struct{}),
|
||||
streams: make(map[string]*ResponseStream),
|
||||
out: out,
|
||||
registry: registry,
|
||||
logger: logger,
|
||||
foreground: make(map[string]struct{}),
|
||||
streams: make(map[string]*ResponseStream),
|
||||
nodeLabels: make(map[string]string),
|
||||
aliasLabels: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -95,18 +99,49 @@ func (r *EventRouter) Drain(ctx context.Context, runEvents <-chan *iop.RunEvent,
|
|||
}
|
||||
|
||||
func (r *EventRouter) NodeLabel(event *iop.RunEvent) string {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.nodeLabelLocked(event)
|
||||
}
|
||||
|
||||
func (r *EventRouter) nodeLabelLocked(event *iop.RunEvent) string {
|
||||
nodeID := event.GetNodeId()
|
||||
alias := event.GetNodeAlias()
|
||||
return r.resolveNodeLabelLocked(nodeID, alias)
|
||||
}
|
||||
|
||||
func (r *EventRouter) edgeNodeLabelLocked(event *iop.EdgeNodeEvent) string {
|
||||
nodeID := event.GetNodeId()
|
||||
alias := event.GetAlias()
|
||||
return r.resolveNodeLabelLocked(nodeID, alias)
|
||||
}
|
||||
|
||||
func (r *EventRouter) resolveNodeLabelLocked(nodeID, alias string) string {
|
||||
if r.registry != nil {
|
||||
if entry, ok := r.registry.Get(nodeID); ok {
|
||||
return entry.DisplayLabel()
|
||||
return r.cacheEntryLabelLocked(entry)
|
||||
}
|
||||
if alias := event.GetNodeAlias(); alias != "" {
|
||||
if alias != "" {
|
||||
if entry, err := r.registry.Resolve(alias); err == nil {
|
||||
return entry.DisplayLabel()
|
||||
return r.cacheEntryLabelLocked(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
if alias := event.GetNodeAlias(); alias != "" {
|
||||
if nodeID != "" {
|
||||
if label := r.nodeLabels[nodeID]; label != "" {
|
||||
if alias != "" {
|
||||
r.aliasLabels[alias] = label
|
||||
}
|
||||
return label
|
||||
}
|
||||
}
|
||||
if alias != "" {
|
||||
if label := r.aliasLabels[alias]; label != "" {
|
||||
if nodeID != "" {
|
||||
r.nodeLabels[nodeID] = label
|
||||
}
|
||||
return label
|
||||
}
|
||||
return alias
|
||||
}
|
||||
if nodeID != "" {
|
||||
|
|
@ -115,25 +150,15 @@ func (r *EventRouter) NodeLabel(event *iop.RunEvent) string {
|
|||
return "unknown"
|
||||
}
|
||||
|
||||
func (r *EventRouter) edgeNodeLabel(event *iop.EdgeNodeEvent) string {
|
||||
nodeID := event.GetNodeId()
|
||||
if r.registry != nil {
|
||||
if entry, ok := r.registry.Get(nodeID); ok {
|
||||
return entry.DisplayLabel()
|
||||
}
|
||||
if alias := event.GetAlias(); alias != "" {
|
||||
if entry, err := r.registry.Resolve(alias); err == nil {
|
||||
return entry.DisplayLabel()
|
||||
}
|
||||
}
|
||||
func (r *EventRouter) cacheEntryLabelLocked(entry *edgenode.NodeEntry) string {
|
||||
label := entry.DisplayLabel()
|
||||
if entry.NodeID != "" {
|
||||
r.nodeLabels[entry.NodeID] = label
|
||||
}
|
||||
if alias := event.GetAlias(); alias != "" {
|
||||
return alias
|
||||
if entry.Alias != "" {
|
||||
r.aliasLabels[entry.Alias] = label
|
||||
}
|
||||
if nodeID != "" {
|
||||
return nodeID
|
||||
}
|
||||
return "unknown"
|
||||
return label
|
||||
}
|
||||
|
||||
func (r *EventRouter) printNodeEventLocked(event *iop.EdgeNodeEvent) {
|
||||
|
|
@ -141,7 +166,7 @@ func (r *EventRouter) printNodeEventLocked(event *iop.EdgeNodeEvent) {
|
|||
return
|
||||
}
|
||||
|
||||
label := r.edgeNodeLabel(event)
|
||||
label := r.edgeNodeLabelLocked(event)
|
||||
detail := transportCloseDetail(event.GetMetadata())
|
||||
switch event.GetType() {
|
||||
case eventpkg.TypeNodeConnected:
|
||||
|
|
@ -173,7 +198,7 @@ func (r *EventRouter) printAsyncLocked(event *iop.RunEvent) {
|
|||
}
|
||||
|
||||
runID := event.GetRunId()
|
||||
label := r.NodeLabel(event)
|
||||
label := r.nodeLabelLocked(event)
|
||||
switch event.GetType() {
|
||||
case "start":
|
||||
fmt.Fprintf(r.out, "%s start run_id=%s session=%s background=%v\n", nodeEventPrefix(label), runID, event.GetSessionId(), event.GetBackground())
|
||||
|
|
|
|||
|
|
@ -177,6 +177,59 @@ func TestEventRouterPrintsNodeLifecycleEvents(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestEventRouterKeepsCachedDisplayLabelForLateEvents(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
reg := edgenode.NewRegistry()
|
||||
reg.Register(&edgenode.NodeEntry{NodeID: "node-1", Alias: "alias-1"})
|
||||
router := NewEventRouter(&out, reg, nil)
|
||||
|
||||
router.HandleNodeEvent(&iop.EdgeNodeEvent{
|
||||
Type: eventpkg.TypeNodeConnected,
|
||||
NodeId: "node-1",
|
||||
Alias: "alias-1",
|
||||
Reason: eventpkg.ReasonRegistered,
|
||||
})
|
||||
reg.Unregister("node-1")
|
||||
|
||||
router.Handle(&iop.RunEvent{
|
||||
RunId: "run-late",
|
||||
Type: "start",
|
||||
NodeId: "node-1",
|
||||
NodeAlias: "alias-1",
|
||||
Background: true,
|
||||
})
|
||||
router.Handle(&iop.RunEvent{
|
||||
RunId: "run-late",
|
||||
Type: "delta",
|
||||
Delta: "late payload",
|
||||
NodeId: "node-1",
|
||||
NodeAlias: "alias-1",
|
||||
Background: true,
|
||||
})
|
||||
router.Handle(&iop.RunEvent{
|
||||
RunId: "run-late",
|
||||
Type: "complete",
|
||||
Message: "done",
|
||||
NodeId: "node-1",
|
||||
NodeAlias: "alias-1",
|
||||
Background: true,
|
||||
})
|
||||
|
||||
got := out.String()
|
||||
for _, want := range []string{
|
||||
"[node0-evt] start run_id=run-late",
|
||||
"[node0-msg] late payload",
|
||||
"[node0-evt] complete run_id=run-late detail=\"done\"",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("expected cached label output %q, got:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
if strings.Contains(got, "[alias-1-") || strings.Contains(got, "[node-1-") {
|
||||
t.Fatalf("late event leaked fallback label, got:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNodeCommandRequest_StatusUsesCurrentTarget(t *testing.T) {
|
||||
req, reqID := BuildNodeCommandRequest("cli", "codex", "default", 30)
|
||||
if reqID == "" {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func (s *Service) ListNodes() []*edgenode.NodeEntry {
|
|||
}
|
||||
|
||||
// NodeSnapshot is a surface-neutral view of a registered node, suitable for
|
||||
// CLI, HTTP, or other transports. Label resolves to Alias when present.
|
||||
// CLI, HTTP, or other transports. Label is the short display label (node0, node1).
|
||||
type NodeSnapshot struct {
|
||||
NodeID string
|
||||
Alias string
|
||||
|
|
|
|||
Loading…
Reference in a new issue