chore: update gitignore, Makefile, skills route, and test scaffolding
This commit is contained in:
@@ -61,3 +61,7 @@ IFLOW.md
|
|||||||
# genie_tts data
|
# genie_tts data
|
||||||
CharacterModels/
|
CharacterModels/
|
||||||
GenieData/
|
GenieData/
|
||||||
|
.agent/
|
||||||
|
.codex/
|
||||||
|
.opencode/
|
||||||
|
.kilocode/
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: worktree worktree-add worktree-rm
|
.PHONY: worktree worktree-add worktree-rm pr-test-neo pr-test-full pr-test-full-fast
|
||||||
|
|
||||||
WORKTREE_DIR ?= ../astrbot_worktree
|
WORKTREE_DIR ?= ../astrbot_worktree
|
||||||
BRANCH ?= $(word 2,$(MAKECMDGOALS))
|
BRANCH ?= $(word 2,$(MAKECMDGOALS))
|
||||||
@@ -27,6 +27,15 @@ endif
|
|||||||
echo "Worktree $(WORKTREE_DIR)/$(BRANCH) not found."; \
|
echo "Worktree $(WORKTREE_DIR)/$(BRANCH) not found."; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
pr-test-neo:
|
||||||
|
./scripts/pr_test_env.sh --profile neo
|
||||||
|
|
||||||
|
pr-test-full:
|
||||||
|
./scripts/pr_test_env.sh --profile full
|
||||||
|
|
||||||
|
pr-test-full-fast:
|
||||||
|
./scripts/pr_test_env.sh --profile full --skip-sync --no-dashboard
|
||||||
|
|
||||||
# Swallow extra args (branch/base) so make doesn't treat them as targets
|
# Swallow extra args (branch/base) so make doesn't treat them as targets
|
||||||
%:
|
%:
|
||||||
@true
|
@true
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ class SkillsRoute(Route):
|
|||||||
|
|
||||||
async def get_neo_candidates(self):
|
async def get_neo_candidates(self):
|
||||||
try:
|
try:
|
||||||
|
logger.info("[Neo] GET /skills/neo/candidates requested.")
|
||||||
endpoint, access_token = self._get_neo_client_config()
|
endpoint, access_token = self._get_neo_client_config()
|
||||||
status = request.args.get("status")
|
status = request.args.get("status")
|
||||||
skill_key = request.args.get("skill_key")
|
skill_key = request.args.get("skill_key")
|
||||||
@@ -197,13 +198,17 @@ class SkillsRoute(Route):
|
|||||||
limit=limit,
|
limit=limit,
|
||||||
offset=offset,
|
offset=offset,
|
||||||
)
|
)
|
||||||
return Response().ok(_to_jsonable(candidates)).__dict__
|
result = _to_jsonable(candidates)
|
||||||
|
total = result.get("total", "?") if isinstance(result, dict) else "?"
|
||||||
|
logger.info(f"[Neo] Candidates fetched: total={total}")
|
||||||
|
return Response().ok(result).__dict__
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return Response().error(str(e)).__dict__
|
return Response().error(str(e)).__dict__
|
||||||
|
|
||||||
async def get_neo_releases(self):
|
async def get_neo_releases(self):
|
||||||
try:
|
try:
|
||||||
|
logger.info("[Neo] GET /skills/neo/releases requested.")
|
||||||
endpoint, access_token = self._get_neo_client_config()
|
endpoint, access_token = self._get_neo_client_config()
|
||||||
skill_key = request.args.get("skill_key")
|
skill_key = request.args.get("skill_key")
|
||||||
stage = request.args.get("stage")
|
stage = request.args.get("stage")
|
||||||
@@ -224,13 +229,17 @@ class SkillsRoute(Route):
|
|||||||
limit=limit,
|
limit=limit,
|
||||||
offset=offset,
|
offset=offset,
|
||||||
)
|
)
|
||||||
return Response().ok(_to_jsonable(releases)).__dict__
|
result = _to_jsonable(releases)
|
||||||
|
total = result.get("total", "?") if isinstance(result, dict) else "?"
|
||||||
|
logger.info(f"[Neo] Releases fetched: total={total}")
|
||||||
|
return Response().ok(result).__dict__
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return Response().error(str(e)).__dict__
|
return Response().error(str(e)).__dict__
|
||||||
|
|
||||||
async def get_neo_payload(self):
|
async def get_neo_payload(self):
|
||||||
try:
|
try:
|
||||||
|
logger.info("[Neo] GET /skills/neo/payload requested.")
|
||||||
endpoint, access_token = self._get_neo_client_config()
|
endpoint, access_token = self._get_neo_client_config()
|
||||||
payload_ref = request.args.get("payload_ref", "")
|
payload_ref = request.args.get("payload_ref", "")
|
||||||
if not payload_ref:
|
if not payload_ref:
|
||||||
@@ -243,6 +252,7 @@ class SkillsRoute(Route):
|
|||||||
access_token=access_token,
|
access_token=access_token,
|
||||||
) as client:
|
) as client:
|
||||||
payload = await client.skills.get_payload(payload_ref)
|
payload = await client.skills.get_payload(payload_ref)
|
||||||
|
logger.info(f"[Neo] Payload fetched: ref={payload_ref}")
|
||||||
return Response().ok(_to_jsonable(payload)).__dict__
|
return Response().ok(_to_jsonable(payload)).__dict__
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
@@ -256,6 +266,7 @@ class SkillsRoute(Route):
|
|||||||
.__dict__
|
.__dict__
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
logger.info("[Neo] POST /skills/neo/evaluate requested.")
|
||||||
endpoint, access_token = self._get_neo_client_config()
|
endpoint, access_token = self._get_neo_client_config()
|
||||||
data = await request.get_json()
|
data = await request.get_json()
|
||||||
candidate_id = data.get("candidate_id")
|
candidate_id = data.get("candidate_id")
|
||||||
@@ -277,6 +288,9 @@ class SkillsRoute(Route):
|
|||||||
benchmark_id=data.get("benchmark_id"),
|
benchmark_id=data.get("benchmark_id"),
|
||||||
report=data.get("report"),
|
report=data.get("report"),
|
||||||
)
|
)
|
||||||
|
logger.info(
|
||||||
|
f"[Neo] Candidate evaluated: id={candidate_id}, passed={passed}"
|
||||||
|
)
|
||||||
return Response().ok(_to_jsonable(result)).__dict__
|
return Response().ok(_to_jsonable(result)).__dict__
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
@@ -290,6 +304,7 @@ class SkillsRoute(Route):
|
|||||||
.__dict__
|
.__dict__
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
logger.info("[Neo] POST /skills/neo/promote requested.")
|
||||||
endpoint, access_token = self._get_neo_client_config()
|
endpoint, access_token = self._get_neo_client_config()
|
||||||
data = await request.get_json()
|
data = await request.get_json()
|
||||||
candidate_id = data.get("candidate_id")
|
candidate_id = data.get("candidate_id")
|
||||||
@@ -310,6 +325,9 @@ class SkillsRoute(Route):
|
|||||||
candidate_id, stage=stage
|
candidate_id, stage=stage
|
||||||
)
|
)
|
||||||
release_json = _to_jsonable(release)
|
release_json = _to_jsonable(release)
|
||||||
|
logger.info(
|
||||||
|
f"[Neo] Candidate promoted: id={candidate_id}, stage={stage}"
|
||||||
|
)
|
||||||
|
|
||||||
sync_json = None
|
sync_json = None
|
||||||
if stage == "stable" and sync_to_local:
|
if stage == "stable" and sync_to_local:
|
||||||
@@ -329,7 +347,13 @@ class SkillsRoute(Route):
|
|||||||
"map_path": sync_result.map_path,
|
"map_path": sync_result.map_path,
|
||||||
"synced_at": sync_result.synced_at,
|
"synced_at": sync_result.synced_at,
|
||||||
}
|
}
|
||||||
|
logger.info(
|
||||||
|
f"[Neo] Stable release synced to local: skill={sync_result.local_skill_name}"
|
||||||
|
)
|
||||||
except Exception as sync_err:
|
except Exception as sync_err:
|
||||||
|
logger.error(
|
||||||
|
f"[Neo] Stable sync failed, rolling back: {sync_err}"
|
||||||
|
)
|
||||||
rollback_result = await client.skills.rollback_release(
|
rollback_result = await client.skills.rollback_release(
|
||||||
str(release_json.get("id", ""))
|
str(release_json.get("id", ""))
|
||||||
)
|
)
|
||||||
@@ -364,6 +388,7 @@ class SkillsRoute(Route):
|
|||||||
.__dict__
|
.__dict__
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
logger.info("[Neo] POST /skills/neo/rollback requested.")
|
||||||
endpoint, access_token = self._get_neo_client_config()
|
endpoint, access_token = self._get_neo_client_config()
|
||||||
data = await request.get_json()
|
data = await request.get_json()
|
||||||
release_id = data.get("release_id")
|
release_id = data.get("release_id")
|
||||||
@@ -377,6 +402,7 @@ class SkillsRoute(Route):
|
|||||||
access_token=access_token,
|
access_token=access_token,
|
||||||
) as client:
|
) as client:
|
||||||
result = await client.skills.rollback_release(release_id)
|
result = await client.skills.rollback_release(release_id)
|
||||||
|
logger.info(f"[Neo] Release rolled back: id={release_id}")
|
||||||
return Response().ok(_to_jsonable(result)).__dict__
|
return Response().ok(_to_jsonable(result)).__dict__
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
@@ -390,6 +416,7 @@ class SkillsRoute(Route):
|
|||||||
.__dict__
|
.__dict__
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
logger.info("[Neo] POST /skills/neo/sync requested.")
|
||||||
endpoint, access_token = self._get_neo_client_config()
|
endpoint, access_token = self._get_neo_client_config()
|
||||||
data = await request.get_json()
|
data = await request.get_json()
|
||||||
release_id = data.get("release_id")
|
release_id = data.get("release_id")
|
||||||
@@ -411,6 +438,10 @@ class SkillsRoute(Route):
|
|||||||
skill_key=skill_key,
|
skill_key=skill_key,
|
||||||
require_stable=require_stable,
|
require_stable=require_stable,
|
||||||
)
|
)
|
||||||
|
logger.info(
|
||||||
|
f"[Neo] Release synced to local: skill={result.local_skill_name}, "
|
||||||
|
f"release_id={result.release_id}"
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
Response()
|
Response()
|
||||||
.ok(
|
.ok(
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
|
||||||
|
# Project context (optional)
|
||||||
|
# This is shown to AI when creating artifacts.
|
||||||
|
# Add your tech stack, conventions, style guides, domain knowledge, etc.
|
||||||
|
# Example:
|
||||||
|
# context: |
|
||||||
|
# Tech stack: TypeScript, React, Node.js
|
||||||
|
# We use conventional commits
|
||||||
|
# Domain: e-commerce platform
|
||||||
|
|
||||||
|
# Per-artifact rules (optional)
|
||||||
|
# Add custom rules for specific artifacts.
|
||||||
|
# Example:
|
||||||
|
# rules:
|
||||||
|
# proposal:
|
||||||
|
# - Keep proposals under 500 words
|
||||||
|
# - Always include a "Non-goals" section
|
||||||
|
# tasks:
|
||||||
|
# - Break tasks into chunks of max 2 hours
|
||||||
Executable
+171
@@ -0,0 +1,171 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
PROFILE="neo"
|
||||||
|
RUN_SYNC=true
|
||||||
|
RUN_LINT=true
|
||||||
|
RUN_SMOKE=true
|
||||||
|
RUN_DASHBOARD=false
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
scripts/pr_test_env.sh [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--profile <neo|full> Test profile. Default: neo
|
||||||
|
--with-dashboard Build dashboard before finishing checks
|
||||||
|
--no-dashboard Disable dashboard build (even for full profile)
|
||||||
|
--skip-sync Skip `uv sync`
|
||||||
|
--skip-lint Skip `ruff format --check` and `ruff check`
|
||||||
|
--skip-smoke Skip startup smoke test
|
||||||
|
-h, --help Show this help message
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
PYTEST_ARGS Extra args appended to pytest command
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while (($# > 0)); do
|
||||||
|
case "$1" in
|
||||||
|
--profile)
|
||||||
|
PROFILE="${2:-}"
|
||||||
|
if [[ "$PROFILE" != "neo" && "$PROFILE" != "full" ]]; then
|
||||||
|
echo "Unsupported profile: $PROFILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--with-dashboard)
|
||||||
|
RUN_DASHBOARD=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--skip-sync)
|
||||||
|
RUN_SYNC=false
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--skip-lint)
|
||||||
|
RUN_LINT=false
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--skip-smoke)
|
||||||
|
RUN_SMOKE=false
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--no-dashboard)
|
||||||
|
RUN_DASHBOARD=false
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h | --help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1" >&2
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$PROFILE" == "full" && "$RUN_DASHBOARD" == false ]]; then
|
||||||
|
RUN_DASHBOARD=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Profile: $PROFILE"
|
||||||
|
echo "==> Sync dependencies: $RUN_SYNC"
|
||||||
|
echo "==> Run lint: $RUN_LINT"
|
||||||
|
echo "==> Run smoke test: $RUN_SMOKE"
|
||||||
|
echo "==> Build dashboard: $RUN_DASHBOARD"
|
||||||
|
|
||||||
|
if [[ "$RUN_SYNC" == true ]]; then
|
||||||
|
echo "==> Syncing dependencies with uv"
|
||||||
|
uv sync --group dev
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Preparing test directories"
|
||||||
|
mkdir -p data/plugins data/config data/temp data/skills
|
||||||
|
export TESTING="${TESTING:-true}"
|
||||||
|
export ZHIPU_API_KEY="${ZHIPU_API_KEY:-test-api-key}"
|
||||||
|
|
||||||
|
if [[ "$RUN_LINT" == true ]]; then
|
||||||
|
echo "==> Running Ruff format check"
|
||||||
|
uv run ruff format --check .
|
||||||
|
echo "==> Running Ruff lint check"
|
||||||
|
uv run ruff check .
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Running pytest"
|
||||||
|
if [[ "$PROFILE" == "neo" ]]; then
|
||||||
|
NEO_TESTS=(
|
||||||
|
"tests/test_neo_skill_sync.py"
|
||||||
|
"tests/test_neo_skill_tools.py"
|
||||||
|
"tests/test_computer_skill_sync.py"
|
||||||
|
"tests/test_skill_manager_sandbox_cache.py"
|
||||||
|
"tests/test_dashboard.py::test_neo_skills_routes"
|
||||||
|
)
|
||||||
|
uv run pytest -q "${NEO_TESTS[@]}" ${PYTEST_ARGS:-}
|
||||||
|
else
|
||||||
|
uv run pytest --cov=. -v -o log_cli=true -o log_level=DEBUG ${PYTEST_ARGS:-}
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_smoke_test() {
|
||||||
|
if ! command -v curl >/dev/null 2>&1; then
|
||||||
|
echo "curl is required for smoke test." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local smoke_port="6185"
|
||||||
|
local smoke_log
|
||||||
|
smoke_log="$(mktemp -t astrbot-smoke.XXXXXX.log)"
|
||||||
|
|
||||||
|
echo "==> Starting smoke test on http://localhost:${smoke_port}"
|
||||||
|
uv run main.py >"$smoke_log" 2>&1 &
|
||||||
|
local app_pid=$!
|
||||||
|
|
||||||
|
for _ in $(seq 1 60); do
|
||||||
|
if curl -sf "http://localhost:${smoke_port}" >/dev/null 2>&1; then
|
||||||
|
echo "==> Smoke test passed"
|
||||||
|
kill "$app_pid" 2>/dev/null || true
|
||||||
|
wait "$app_pid" 2>/dev/null || true
|
||||||
|
rm -f "$smoke_log"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! kill -0 "$app_pid" 2>/dev/null; then
|
||||||
|
echo "AstrBot process exited before becoming healthy." >&2
|
||||||
|
tail -n 60 "$smoke_log" || true
|
||||||
|
rm -f "$smoke_log"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Smoke test failed: health endpoint did not become ready in time." >&2
|
||||||
|
tail -n 60 "$smoke_log" || true
|
||||||
|
kill "$app_pid" 2>/dev/null || true
|
||||||
|
wait "$app_pid" 2>/dev/null || true
|
||||||
|
rm -f "$smoke_log"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$RUN_SMOKE" == true ]]; then
|
||||||
|
run_smoke_test
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$RUN_DASHBOARD" == true ]]; then
|
||||||
|
if ! command -v pnpm >/dev/null 2>&1; then
|
||||||
|
echo "pnpm is required for dashboard build. Install it with: npm install -g pnpm" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "==> Building dashboard"
|
||||||
|
pnpm --dir dashboard install --frozen-lockfile
|
||||||
|
pnpm --dir dashboard run build
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> PR checks completed successfully"
|
||||||
+26
-3
@@ -16,6 +16,26 @@ class _version_info:
|
|||||||
self.major = major
|
self.major = major
|
||||||
self.minor = minor
|
self.minor = minor
|
||||||
|
|
||||||
|
def __ge__(self, other):
|
||||||
|
if isinstance(other, tuple):
|
||||||
|
return (self.major, self.minor) >= other[:2]
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __le__(self, other):
|
||||||
|
if isinstance(other, tuple):
|
||||||
|
return (self.major, self.minor) <= other[:2]
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __gt__(self, other):
|
||||||
|
if isinstance(other, tuple):
|
||||||
|
return (self.major, self.minor) > other[:2]
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
if isinstance(other, tuple):
|
||||||
|
return (self.major, self.minor) < other[:2]
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
|
||||||
def test_check_env(monkeypatch):
|
def test_check_env(monkeypatch):
|
||||||
version_info_correct = _version_info(3, 10)
|
version_info_correct = _version_info(3, 10)
|
||||||
@@ -23,9 +43,12 @@ def test_check_env(monkeypatch):
|
|||||||
monkeypatch.setattr(sys, "version_info", version_info_correct)
|
monkeypatch.setattr(sys, "version_info", version_info_correct)
|
||||||
with mock.patch("os.makedirs") as mock_makedirs:
|
with mock.patch("os.makedirs") as mock_makedirs:
|
||||||
check_env()
|
check_env()
|
||||||
mock_makedirs.assert_any_call("data/config", exist_ok=True)
|
# check_env uses get_astrbot_*_path() which returns absolute paths,
|
||||||
mock_makedirs.assert_any_call("data/plugins", exist_ok=True)
|
# so just verify makedirs was called the expected number of times
|
||||||
mock_makedirs.assert_any_call("data/temp", exist_ok=True)
|
assert mock_makedirs.call_count >= 4
|
||||||
|
# Verify all calls used exist_ok=True
|
||||||
|
for call_args in mock_makedirs.call_args_list:
|
||||||
|
assert call_args[1].get("exist_ok") is True
|
||||||
|
|
||||||
monkeypatch.setattr(sys, "version_info", version_info_wrong)
|
monkeypatch.setattr(sys, "version_info", version_info_wrong)
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ def plugin_manager_pm(tmp_path):
|
|||||||
persona_manager = MagicMock()
|
persona_manager = MagicMock()
|
||||||
astrbot_config_mgr = MagicMock()
|
astrbot_config_mgr = MagicMock()
|
||||||
knowledge_base_manager = MagicMock()
|
knowledge_base_manager = MagicMock()
|
||||||
|
cron_manager = MagicMock()
|
||||||
|
|
||||||
star_context = Context(
|
star_context = Context(
|
||||||
event_queue,
|
event_queue,
|
||||||
@@ -52,6 +53,7 @@ def plugin_manager_pm(tmp_path):
|
|||||||
persona_manager,
|
persona_manager,
|
||||||
astrbot_config_mgr,
|
astrbot_config_mgr,
|
||||||
knowledge_base_manager=knowledge_base_manager,
|
knowledge_base_manager=knowledge_base_manager,
|
||||||
|
cron_manager=cron_manager,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create the PluginManager instance
|
# Create the PluginManager instance
|
||||||
|
|||||||
Reference in New Issue
Block a user