Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9f4336b
test(agent): cover security and docker pipelines
ZingerLittleBee Aug 12, 2026
2989eae
docs: highlight rust coverage quality
ZingerLittleBee Aug 12, 2026
32ac573
test: cover high-value rust protocol paths
ZingerLittleBee Aug 12, 2026
9de3dcc
ci: enforce rust coverage floor
ZingerLittleBee Aug 12, 2026
3ba5a6b
ci: avoid duplicate coverage test run
ZingerLittleBee Aug 12, 2026
a5f72ac
test(agent): cover file protocol workflows
ZingerLittleBee Aug 12, 2026
ac9b6f5
test: cover file recovery and terminal frames
ZingerLittleBee Aug 12, 2026
14fe9ca
test: cover terminal revocation and file errors
ZingerLittleBee Aug 12, 2026
0b6dd1e
fix(server): close terminals when agents disconnect
ZingerLittleBee Aug 12, 2026
a3a1bf7
test(agent): cover upgrade transaction recovery and guards
ZingerLittleBee Aug 12, 2026
e74b593
test(agent): cover upgrade rejection and rollback reporting
ZingerLittleBee Aug 12, 2026
64de2c9
test(agent): cover terminal reader error paths
ZingerLittleBee Aug 12, 2026
9df67c0
test(server): cover expired offer terminal enforcement
ZingerLittleBee Aug 12, 2026
e518df2
test(server): cover full sync report projection
ZingerLittleBee Aug 12, 2026
965874e
test(server): cover offer api errors and ws auth rejects
ZingerLittleBee Aug 12, 2026
cf4c208
test(server): cover oauth callback nonce rejection
ZingerLittleBee Aug 12, 2026
a3d06c4
test(server): cover cleanup cascade pruning
ZingerLittleBee Aug 12, 2026
3653c68
refactor(server): extract http keyword parse and verdict logic
ZingerLittleBee Aug 12, 2026
3ecf3a5
test(server): cover upload failure paths and size limit
ZingerLittleBee Aug 12, 2026
ab36dec
test(server): cover geoip download guard and status
ZingerLittleBee Aug 12, 2026
b81d673
test(server): cover scheduler retry and dispatch failures
ZingerLittleBee Aug 12, 2026
ec55985
test(agent): cover external ip refresh reporting
ZingerLittleBee Aug 12, 2026
8b0ced4
fix(server): fail pending agent requests fast on disconnect
ZingerLittleBee Aug 12, 2026
441e6a9
refactor(server): ignore binary frames on agent ws
ZingerLittleBee Aug 12, 2026
01c6048
chore(agent): remove dead reporter run wrapper
ZingerLittleBee Aug 12, 2026
84ba9cf
fix(server): empty status page selection includes every server
ZingerLittleBee Aug 12, 2026
19edb03
fix(web): generate uuids outside secure contexts
ZingerLittleBee Aug 12, 2026
888f1dc
test: record vps regression testloop report
ZingerLittleBee Aug 12, 2026
e0c3f0c
docs: remove plaintext vps password from plan
ZingerLittleBee Aug 12, 2026
30d71bb
docs: remove plaintext vps password from ip-quality plan
ZingerLittleBee Aug 12, 2026
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,5 @@ agent.toml
# Git worktrees
.worktrees
.superpowers/
.testloop/
.testloop/*
!.testloop/reports/
48 changes: 48 additions & 0 deletions .testloop/reports/2026-08-12-broad-vps-regression.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# testloop — 2026-08-12 — broad VPS regression

**Verdict:** pass · **Rounds:** 3

Fresh install of current main on the dedicated Linux test VPS (Debian 13,
plain HTTP on :9527), one real agent on the same box with terminal / exec /
file / docker capabilities enabled. The deployment itself exercised the real
first-run path: random admin password from the logs → forced onboarding
password change → enrollment code → agent claim.

## Covered

- Auth (login, wrong-password error, session persistence, logout, member
role isolation, audit log), first-run onboarding, agent enrollment UI.
- Dashboards: create / rename / delete / edit mode, add-widget flow, layout
persistence; live WebSocket metric updates.
- Server detail: metrics charts, system info, terminal (PTY commands), file
manager (mkdir, upload, download with byte-for-byte verification, rename,
delete, root-path escape rejection), docker (list + log streaming,
read-only).
- Service monitors (HTTP keyword up/down verdicts), ping tasks (ICMP results
chart), scheduled commands (run-now via exec), alert rules (threshold
trigger with 5-min debounce, webhook failure handling), security events
(injected SSH auth failures), public status page.

## Found & fixed

- `crypto.randomUUID` is unavailable on plain-HTTP origins: the /servers page
crashed with an error boundary and the dashboard Add Widget flow failed
silently. Fixed with a `getRandomValues`-based fallback in
`apps/web/src/lib/uuid.ts` (used by `add-server-dialog.tsx` and
`use-dashboard-editor.ts`).
- Public status page: the admin UI promises "leave empty to include every
server" but an empty selection resolved to zero servers, so /status showed
"No servers available". Fixed in
`crates/server/src/service/public_status.rs` (`resolve_scope`), with an
integration regression test in `tests/public_status_gating.rs`.

## Still open

- UX observation, not a defect: ping-task latency lives only under
Settings → Ping Tasks; the server-detail Network tab belongs to the
separate network-probe feature and shows "No probe targets configured"
even when ping tasks are collecting. Two independent test rounds went
looking for ping data there first — worth considering a cross-link or an
empty-state hint.
- Round-1 terminal first-connect needed one manual Reconnect; not
reproducible in round 2 (connected first try). Watch for recurrence.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ ServerBee watches all your servers from one place. A central **server** receives
- 📦 **Single binary** — server + embedded web UI in one file. Deploy with Docker, a one-line script, or Railway.
- 🔋 **Batteries included** — alerts, notifications, web terminal, file manager, Docker, firewall, status pages, and more.
- 🔒 **Secure by default** — OAuth + 2FA, RBAC, audit logs, one-time agent enrollment, agent-owned capability gates.
- ✅ **Battle-tested** — 3,800+ automated tests, including integration suites that drive the real router over HTTP + WebSocket via a mock-agent harness; green on every CI run.
- ✅ **Battle-tested** — 3,800+ automated tests and over 91% measured Rust region coverage, including integration suites that drive the real router over HTTP + WebSocket via a mock-agent harness; green on every CI run.

> [!NOTE]
> ServerBee is in active development (`v1.0.0-alpha.12`). Expect rapid iteration.
Expand Down Expand Up @@ -136,7 +136,7 @@ make cargo-clippy # Rust lint

### Testing & quality

ServerBee ships with **3,800+ automated tests** — 3,200+ Rust (unit + 42 integration suites that drive the real Axum router over HTTP/WebSocket with a mock-agent harness) and 600+ frontend Vitest tests. Coverage is measured with `cargo-llvm-cov` (common ~98%, agent ~90%, server ~92% region). Every push runs zero-warning Clippy, the full Rust suite, and frontend type/lint checks in CI. See the [Testing & Quality](https://docs.serverbee.app/en/docs/testing) guide.
ServerBee ships with **3,800+ automated tests** — 3,200+ Rust (unit + 42 integration suites that drive the real Axum router over HTTP/WebSocket with a mock-agent harness) and 600+ frontend Vitest tests. Coverage is measured with `cargo-llvm-cov` (over 91% region overall; common ~98%, agent ~90%, server ~92%). Every push runs zero-warning Clippy, the full Rust suite, and frontend type/lint checks in CI. See the [Testing & Quality](https://docs.serverbee.app/en/docs/testing) guide.

## API

Expand Down
4 changes: 2 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ ServerBee 在一处统一监控你的所有服务器。中心 **Server** 通过
- 📦 **单一二进制** —— Server 与内嵌 Web UI 打包成一个文件,支持 Docker、一行脚本、Railway 部署。
- 🔋 **开箱即用** —— 告警、通知、Web 终端、文件管理、Docker、防火墙、状态页等功能。
- 🔒 **默认安全** —— OAuth + 2FA、RBAC、审计日志、一次性 Agent 注册、Agent 自主掌管的能力门控。
- ✅ **充分测试** —— 3800+ 自动化测试,其中集成套件通过 mock-agent 测试桩驱动真实路由、经由 HTTP + WebSocket 演练控制面;每次 CI 全部通过。
- ✅ **充分测试** —— 3800+ 自动化测试,Rust 区域覆盖率实测超过 91%;其中集成套件通过 mock-agent 测试桩驱动真实路由、经由 HTTP + WebSocket 演练控制面每次 CI 全部通过。

> [!NOTE]
> ServerBee 正在活跃开发中(`v1.0.0-alpha.12`),迭代频繁。
Expand Down Expand Up @@ -136,7 +136,7 @@ make cargo-clippy # Rust 代码检查

### 测试与质量

ServerBee 自带 **3800+ 自动化测试** —— 其中 3200+ 为 Rust(单元测试 + 42 个集成套件,通过 mock-agent 测试桩驱动真实 Axum 路由、经由 HTTP/WebSocket 演练),以及 600+ 前端 Vitest 测试。覆盖率使用 `cargo-llvm-cov` 测量(common ~98%、agent ~90%、server ~92% region)。每次 push 都会在 CI 中运行零警告 Clippy、完整 Rust 测试套件,以及前端类型/lint 检查。详见 [测试与质量](https://docs.serverbee.app/zh/docs/testing) 指南。
ServerBee 自带 **3800+ 自动化测试** —— 其中 3200+ 为 Rust单元测试 + 42 个集成套件通过 mock-agent 测试桩驱动真实 Axum 路由、经由 HTTP/WebSocket 演练),以及 600+ 前端 Vitest 测试。覆盖率使用 `cargo-llvm-cov` 测量(总体区域覆盖率超过 91%;common ~98%、agent ~90%、server ~92%。每次 push 都会在 CI 中运行零警告 Clippy、完整 Rust 测试套件以及前端类型/lint 检查。详见 [测试与质量](https://docs.serverbee.app/zh/docs/testing) 指南。

## API

Expand Down
10 changes: 5 additions & 5 deletions apps/docs/content/docs/en/testing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ ServerBee treats reliability as a feature. The codebase ships with a large, fast

| Area | Tests | What it covers |
|------|-------|----------------|
| `common` crate | 148 | Protocol messages, capability bitmask, SSRF guard, shared types |
| `agent` crate | 865 | Collectors, reporter WebSocket loop, pinger, file manager, security detectors, firewall, IP quality |
| `server` crate | 2,188 | REST handlers, WebSocket handlers, services, background tasks, migrations |
| `common` crate | 149 | Protocol messages, capability bitmask, SSRF guard, shared types |
| `agent` crate | 891 | Collectors, reporter WebSocket loop, pinger, file manager, security detectors, firewall, IP quality |
| `server` crate | 2,223 | REST handlers, WebSocket handlers, services, background tasks, migrations |
| Frontend (`apps/web`) | 600+ | Hooks, components, stores, and utilities (Vitest) |

That is **3,800+ automated tests** in total, all green on every CI run.
Expand Down Expand Up @@ -50,10 +50,10 @@ Coverage is measured with [`cargo-llvm-cov`](https://github.com/taiki-e/cargo-ll
| Crate | Region | Line |
|-------|--------|------|
| `common` | 98% | 97% |
| `agent` | 90% | 89% |
| `agent` | 90% | 90% |
| `server` | 92% | 93% |

The remaining uncovered lines are I/O paths that only execute in a live environment — a Docker daemon, Linux kernel facilities (nftables / conntrack / journald), raw ICMP sockets, a PTY, or external services (OAuth, SMTP, APNs). These are exercised through the integration harness and the manual end-to-end checklists in `tests/` rather than unit tests.
Overall Rust region coverage is above 91%. The largest remaining gaps are concentrated in process startup and I/O paths that need a live Docker daemon, Linux kernel facilities (nftables / conntrack / journald), raw ICMP sockets, a PTY, or external services (OAuth, SMTP, APNs). Those environment-dependent paths are exercised through the integration harness and the manual end-to-end checklists in `tests/` where practical.

## Running tests locally

Expand Down
10 changes: 5 additions & 5 deletions apps/docs/content/docs/zh/testing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ ServerBee 把可靠性当作一项功能来对待。代码库自带一套规模

| 范围 | 测试数 | 覆盖内容 |
|------|--------|----------|
| `common` crate | 148 | 协议消息、能力位掩码、SSRF guard、共享类型 |
| `agent` crate | 865 | 采集器、reporter WebSocket 循环、pinger、文件管理、安全检测器、防火墙、IP 质量 |
| `server` crate | 2,188 | REST 处理器、WebSocket 处理器、服务层、后台任务、数据库迁移 |
| `common` crate | 149 | 协议消息、能力位掩码、SSRF guard、共享类型 |
| `agent` crate | 891 | 采集器、reporter WebSocket 循环、pinger、文件管理、安全检测器、防火墙、IP 质量 |
| `server` crate | 2,223 | REST 处理器、WebSocket 处理器、服务层、后台任务、数据库迁移 |
| 前端(`apps/web`) | 600+ | Hooks、组件、状态 store、工具函数(Vitest) |

合计 **3800+ 自动化测试**,每次 CI 运行全部通过。
Expand Down Expand Up @@ -50,10 +50,10 @@ React 应用由 [Vitest](https://vitest.dev) 覆盖,分布在 89 个测试文件
| Crate | Region | Line |
|-------|--------|------|
| `common` | 98% | 97% |
| `agent` | 90% | 89% |
| `agent` | 90% | 90% |
| `server` | 92% | 93% |

剩余未覆盖的代码是只能在真实环境中执行的 I/O 路径 —— Docker 守护进程、Linux 内核设施(nftables / conntrack / journald)、原始 ICMP 套接字、PTY,或外部服务(OAuth、SMTP、APNs)。这些路径通过集成测试桩以及 `tests/` 目录下的手动端到端检查清单来演练,而非单元测试
Rust 总体区域覆盖率超过 91%。当前较大的剩余缺口集中在进程启动和依赖真实环境的 I/O 路径,包括 Docker 守护进程、Linux 内核设施nftables / conntrack / journald、原始 ICMP 套接字、PTY,以及 OAuth、SMTP、APNs 等外部服务。在可行范围内,这些环境相关路径通过集成测试桩和 `tests/` 目录下的手动端到端检查清单演练

## 本地运行测试

Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/server/add-server-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
} from '@/lib/api-schema'
import { refreshServerCatalog } from '@/lib/server-catalog'
import { cn } from '@/lib/utils'
import { randomUUID } from '@/lib/uuid'

const TAG_SPLIT_RE = /[\s,]+/
const TAG_VALID_RE = /^[A-Za-z0-9_.-]+$/
Expand Down Expand Up @@ -171,7 +172,7 @@ function initialAddServerFormState(): AddServerFormState {
groupId: '',
issued: null,
name: '',
onboardingRequestId: crypto.randomUUID(),
onboardingRequestId: randomUUID(),
price: '',
publicRemark: '',
remark: '',
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/hooks/use-dashboard-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
mergeLayoutPatch,
normalizeNewWidgetPlacement
} from '@/components/dashboard/dashboard-layout'
import { randomUUID } from '@/lib/uuid'
import { parseConfig } from '@/lib/widget-helpers'
import type { DashboardWidget } from '@/lib/widget-types'
import { WIDGET_TYPES, type WidgetTypeDefinition } from '@/lib/widget-types'
Expand Down Expand Up @@ -60,7 +61,7 @@ export function useDashboardEditor() {
}

function addWidget({ configJson, dashboardId, gridH, gridW, moduleId, title, widgetType }: AddWidgetInput): string {
const id = `temp-${crypto.randomUUID()}`
const id = `temp-${randomUUID()}`
setDraftWidgets((current) => {
const defaults = getWidgetTypeDefaults(widgetType)
const newWidget: DashboardWidget = {
Expand Down
21 changes: 21 additions & 0 deletions apps/web/src/lib/uuid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* UUID v4 that also works outside secure contexts.
*
* `crypto.randomUUID` is only exposed on HTTPS/localhost origins; self-hosted
* dashboards are routinely served over plain HTTP on a LAN or VPS IP, where
* calling it throws. `crypto.getRandomValues` has no such restriction, so fall
* back to assembling the UUID manually from it.
*/
export function randomUUID(): string {
if (typeof crypto.randomUUID === 'function') {
return crypto.randomUUID()
}
const bytes = crypto.getRandomValues(new Uint8Array(16))
// Per RFC 4122 §4.4: version 4, variant 10xx.
// biome-ignore lint/suspicious/noBitwiseOperators: RFC 4122 version bits require masking
bytes[6] = (bytes[6] & 0x0f) | 0x40
// biome-ignore lint/suspicious/noBitwiseOperators: RFC 4122 variant bits require masking
bytes[8] = (bytes[8] & 0x3f) | 0x80
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0'))
return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10).join('')}`
}
138 changes: 138 additions & 0 deletions crates/agent/src/docker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,4 +665,142 @@ mod tests {
other => panic!("expected DockerUnavailable, got {other:?}"),
}
}

#[tokio::test]
async fn verify_connection_reports_an_unreachable_daemon() {
let (manager, _rx) = make_manager(CAP_DOCKER);
assert!(manager.verify_connection().await.is_err());
}

#[tokio::test]
async fn poll_stats_skips_daemon_access_without_capability() {
let (mut manager, mut rx) = make_manager(0);
manager.poll_stats().await.unwrap();
assert!(rx.try_recv().is_err());
}

#[tokio::test]
async fn poll_stats_notifies_when_the_daemon_is_unreachable() {
let (mut manager, mut rx) = make_manager(CAP_DOCKER);
assert!(manager.poll_stats().await.is_err());
assert!(matches!(
rx.recv().await,
Some(AgentMessage::DockerUnavailable { msg_id: None })
));
}

#[tokio::test]
async fn daemon_read_requests_preserve_their_message_ids_on_failure() {
let cases = [
ServerMessage::DockerListContainers {
msg_id: "containers".to_string(),
},
ServerMessage::DockerGetInfo {
msg_id: "info".to_string(),
},
ServerMessage::DockerListNetworks {
msg_id: "networks".to_string(),
},
ServerMessage::DockerListVolumes {
msg_id: "volumes".to_string(),
},
];

for (request, expected_id) in
cases
.into_iter()
.zip(["containers", "info", "networks", "volumes"])
{
let (mut manager, mut rx) = make_manager(CAP_DOCKER);
assert!(manager.handle_server_message(request).await.is_err());
assert!(matches!(
rx.recv().await,
Some(AgentMessage::DockerUnavailable { msg_id: Some(id) }) if id == expected_id
));
}
}

#[tokio::test]
async fn container_actions_report_daemon_errors_without_failing_dispatch() {
let actions = [
DockerAction::Start,
DockerAction::Stop { timeout: None },
DockerAction::Restart { timeout: Some(3) },
DockerAction::Remove { force: true },
];

for (index, action) in actions.into_iter().enumerate() {
let (mut manager, mut rx) = make_manager(CAP_DOCKER);
let msg_id = format!("action-{index}");
manager
.handle_server_message(ServerMessage::DockerContainerAction {
msg_id: msg_id.clone(),
container_id: "missing-container".to_string(),
action,
})
.await
.unwrap();
match rx.recv().await {
Some(AgentMessage::DockerActionResult {
msg_id: actual_id,
success,
error,
}) => {
assert_eq!(actual_id, msg_id);
assert!(!success);
assert!(error.is_some());
}
other => panic!("expected DockerActionResult, got {other:?}"),
}
}
}

#[tokio::test]
async fn stream_messages_replace_and_stop_existing_tasks() {
let (mut manager, _rx) = make_manager(CAP_DOCKER);

manager
.handle_server_message(ServerMessage::DockerLogsStart {
session_id: "logs-1".to_string(),
container_id: "container-1".to_string(),
tail: Some(25),
follow: true,
})
.await
.unwrap();
assert_eq!(manager.log_sessions.len(), 1);
manager
.handle_server_message(ServerMessage::DockerLogsStart {
session_id: "logs-1".to_string(),
container_id: "container-2".to_string(),
tail: None,
follow: false,
})
.await
.unwrap();
assert_eq!(manager.log_sessions.len(), 1);
manager
.handle_server_message(ServerMessage::DockerLogsStop {
session_id: "logs-1".to_string(),
})
.await
.unwrap();
assert!(manager.log_sessions.is_empty());

manager
.handle_server_message(ServerMessage::DockerEventsStart)
.await
.unwrap();
assert!(manager.event_stream_handle.is_some());
manager
.handle_server_message(ServerMessage::DockerEventsStart)
.await
.unwrap();
assert!(manager.event_stream_handle.is_some());
manager
.handle_server_message(ServerMessage::DockerEventsStop)
.await
.unwrap();
assert!(manager.event_stream_handle.is_none());
}
}
Loading
Loading