Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5144039
style(desktop): cargo fmt 清零(仅排版,零语义改动)
Jul 18, 2026
dba0428
fix(desktop): cargo clippy --all-targets -D warnings 清零(机械修复)
Jul 18, 2026
17e1e1a
ci(desktop): 把 cargo fmt --check / clippy -D warnings 接进 rust-system-…
Jul 18, 2026
b4c46ef
fix(auth): make account deactivation cleanup re-entrant and observabl…
Jul 18, 2026
bba6141
feat(conversations): 会话 rename 发领域事件跨端同步 (R20 P2-04)
Jul 18, 2026
eba9e23
feat(events): transactional outbox for conversation message pushes (R…
Jul 18, 2026
b52e0fe
fix(web,onboarding): P2-09 stop swallowing failed locale preference sync
Jul 18, 2026
2f5ece0
fix(web,a11y): P2-10 avatar crop modal keyboard focus lifecycle
Jul 18, 2026
e4c013b
fix(web,qa): P2-11 launchChrome surfaces root cause on failure
Jul 18, 2026
6cd7d0b
feat(web,api-client): R19-27 render the work item cross-run audit tim…
Jul 18, 2026
2fbc0b1
fix(agent-runner): make budget lease renewal failures observable (P3-02)
Jul 18, 2026
f63e140
fix(api,sdk): remove three dead read endpoints (R19-29)
Jul 18, 2026
f40a822
feat(desktop): thread the trace after-cursor into live incremental po…
Jul 18, 2026
c02f96a
fix(web): resolve conversation-mirror sender nicknames from the works…
Jul 18, 2026
7e89b43
merge(r20): W5-1 消息事务性outbox (P2-01, 迁移0069)
Jul 18, 2026
88112ac
merge(r20): W5-3 会话rename领域事件 (P2-04)
Jul 18, 2026
30d0ff6
merge(r20): W5-2 offboarding可重入 (P2-02)
Jul 18, 2026
9c00462
merge(r20): W5-6 预算续租可观测+死端点清理+trace游标+P1-08尾巴 (P3-02/R19-29/R19-30)
Jul 18, 2026
52bf070
merge(r20): W5-4 语言偏好吞错+弹窗焦点+QA日志+审计时间线 (P2-09/10/11/R19-27)
Jul 18, 2026
979ef3a
merge(r20): W5-5 cargo fmt/clippy清零进CI (P3-01)
Jul 18, 2026
3670e4e
test(db): journal 守卫同步到 0069 event_outbox(集成修复)
Jul 18, 2026
22014d1
fix(desktop): MacosVibrancy 变体补非 mac 平台 dead_code 放行(Linux clippy 门,集…
Jul 18, 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
11 changes: 11 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
workspaces: client-tauri/src-tauri
- name: Ensure rustfmt/clippy components
run: rustup component add rustfmt clippy
# 注:runner 是 ubuntu-latest。rustfmt 只做语法层排版,不评估 cfg,所以下面 fmt --check
# 覆盖全部源码,包括 windows.rs/main.rs 里 `#[cfg(target_os = "macos"/"windows")]` 门下的分支。
# 但 clippy 是真编译再 lint,Linux target 编译期会把那些平台专属分支直接剔除——
# 因此 clippy 这一步只对 Linux 生效的代码路径生效,macOS/Windows 专属分支的 clippy
# 仍需在对应平台本机跑(与现有 rust-system-i18n job 里 cargo test 的覆盖范围一致,非本次新增的局限)。
- name: cargo fmt --check (desktop shell, all cfg branches)
run: cargo fmt --manifest-path client-tauri/src-tauri/Cargo.toml --check
- name: cargo clippy -D warnings (desktop shell, Linux cfg paths only)
run: cargo clippy --manifest-path client-tauri/src-tauri/Cargo.toml --all-targets -- -D warnings
- uses: pnpm/action-setup@v4
with:
version: 11.0.9
Expand Down
94 changes: 91 additions & 3 deletions apps/api/src/agent-runs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3276,7 +3276,7 @@ test("agent run read routes fall back to the run owner/admin gate when work item
const readRoutes = [
`/api/agent-runs/${queued.run_id}`,
`/api/agent-runs/${queued.run_id}/trace`,
`/api/agent-runs/${queued.run_id}/handoff`,
// R20 R19-29:/handoff 端点已删(死冗余,见 routes/agent-runs.ts)。
`/api/agent-runs/${queued.run_id}/replay`
];

Expand Down Expand Up @@ -3339,7 +3339,7 @@ test("agent run direct routes stay scoped to the actor workspace", async () => {
const readRoutes = [
`/api/agent-runs/${queued.run_id}`,
`/api/agent-runs/${queued.run_id}/trace`,
`/api/agent-runs/${queued.run_id}/handoff`,
// R20 R19-29:/handoff 端点已删(死冗余,见 routes/agent-runs.ts)。
`/api/agent-runs/${queued.run_id}/replay`
];
const ownerCookie = await cookie(runtimeSettings);
Expand Down Expand Up @@ -3409,7 +3409,7 @@ test("agent run read routes allow users who can open the backing work item", asy
for (const route of [
`/api/agent-runs/${queued.run_id}`,
`/api/agent-runs/${queued.run_id}/trace`,
`/api/agent-runs/${queued.run_id}/handoff`,
// R20 R19-29:/handoff 端点已删(死冗余,见 routes/agent-runs.ts)。
`/api/agent-runs/${queued.run_id}/replay`
]) {
const response = await app.request(route, { headers: { Cookie: collaboratorCookie } });
Expand Down Expand Up @@ -5434,6 +5434,94 @@ test("agent run queue keeps the lease alive during a long provider call", async
assert.notEqual(duringProvider?.claim?.heartbeat_at, duringProvider?.claim?.claimed_at);
});

// P3-02:claim 心跳成功续租时,refreshClaim 会顺带续预留租约(reservationRepo.refreshLease)。此前那次调用
// 是 `.catch(() => {})`——不管是 DB 抛错还是命中 0 行(预留早被 releaseExpired 判过期/从未成功 reserve 过),
// 全部悄悄吞掉,运维完全看不出这个仍在跑的 run 已经没有生效预留、outstanding 计算正在漏计它。根因测试:
// 让 refreshLease 命中 0 行,断言必须能在结构化日志里看到 agent_run_budget_lease_renew_no_rows——
// 修复前这条断言会红(吞掉后日志管道里什么都没有),修复后转绿。
test("P3-02 budget lease renewal that updates 0 rows surfaces a structured log instead of being silently swallowed", async () => {
const runtimeSettings = settings();
const persistence = new MemoryAgentRunPersistence();
const renewSeen = deferred<void>();
const releaseProvider = deferred<void>();
const workdir = await mkdtemp(path.join(os.tmpdir(), "workhub-agent-run-budget-lease-renew-test-"));
let tick = 0;
const longProviderClient: AgentLoopClient = {
model: "deepseek-v4-flash",
messages: {
async create() {
await releaseProvider.promise;
return {
id: "msg-budget-lease-renew",
stopReason: "end_turn",
usage: { inputTokens: 1, outputTokens: 1 },
usageRecord: {
provider: "deepseek",
model: "deepseek-v4-flash",
task: "worker",
inputTokens: 1,
outputTokens: 1,
estimatedCostCny: "0.001",
source: "agent_step",
createdAt: "2026-06-05T00:00:00.000Z"
},
content: [{ type: "text", text: "done" }]
};
}
}
};
const fakeReservationRepo = {
reserve: async () => ({ ok: true as const }),
reconcile: async () => 0,
releaseExpired: async () => 0,
refreshLease: async () => {
renewSeen.resolve();
return 0;
},
outstandingForScopes: async () => new Map()
};
const logLines: string[] = [];
const originalWrite = process.stdout.write.bind(process.stdout);
process.stdout.write = ((chunk: string | Uint8Array) => {
logLines.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
return true;
}) as typeof process.stdout.write;
try {
const queue = createInMemoryAgentRunQueue({
settings: runtimeSettings,
now: () => new Date(now.getTime() + tick++ * 100),
id: () => "40000000-0000-4000-8000-00000000002f",
workerId: "worker-budget-lease-renew",
leaseMs: 300,
heartbeatIntervalMs: 10,
workdir: () => workdir,
client: () => longProviderClient,
persistence,
reservationRepo: fakeReservationRepo as unknown as BudgetReservationRepository,
confidence: false,
proposals: false,
notifications: false,
eventBus: false,
requireDeliverable: false
});
const run = await queue.enqueue({
workItemId,
actorId: userId,
title: "Budget lease renew 0-row run"
});

const running = queue.runNext();
await renewSeen.promise;
releaseProvider.resolve();
await running;
} finally {
process.stdout.write = originalWrite;
}

const sawNoRowsLog = logLines.some((line) => line.includes("agent_run_budget_lease_renew_no_rows"));
assert.equal(sawNoRowsLog, true, "0-row budget lease renewal must be observable via structured log, not silently swallowed");
});

test("agent run abort propagates an AbortSignal to the in-flight provider request", async () => {
const runtimeSettings = settings();
const providerStarted = deferred<void>();
Expand Down
86 changes: 41 additions & 45 deletions apps/api/src/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ test("GET /api/openapi.json exposes the headless daemon contract seed", async ()
["get", "/api/projects/{id}/instructions"],
["patch", "/api/projects/{id}/instructions"],
["post", "/api/workitems/{id}/proposals"],
["get", "/api/workitems/{id}/proposals"],
// R20 R19-29:GET /api/workitems/{id}/proposals(list-work-item-proposals)已删(死冗余,无消费)。
["get", "/api/workitems/{id}/conflicts"],
["get", "/api/proposals/{id}"],
["post", "/api/proposals/{id}/review"],
Expand All @@ -397,7 +397,7 @@ test("GET /api/openapi.json exposes the headless daemon contract seed", async ()
["post", "/api/workitems/{id}/agent-runs"],
["get", "/api/agent-runs/{id}"],
["get", "/api/agent-runs/{id}/trace"],
["get", "/api/agent-runs/{id}/handoff"],
// R20 R19-29:GET /api/agent-runs/{id}/handoff 已删(死冗余,无消费)。
["post", "/api/agent-runs/{id}/abort"],
["get", "/api/agent-runs/{id}/replay"],
["post", "/api/agent-runs/{id}/revert"],
Expand All @@ -420,7 +420,7 @@ test("GET /api/openapi.json exposes the headless daemon contract seed", async ()
["post", "/api/knowledge/search"],
["get", "/api/workitems/{id}/audit"],
["get", "/api/pilot/day1/metrics"],
["get", "/api/ai-worklog/today"],
// R20 R19-29:GET /api/ai-worklog/today 已删(死冗余,无消费;数据早已内嵌进 attention 页 VM)。
["get", "/api/conversations/{id}/army"],
["get", "/api/me/army"],
["post", "/api/action-card-items/{id}/decide"],
Expand Down Expand Up @@ -853,6 +853,30 @@ test("runtime API routes stay in lockstep with the OpenAPI document", async () =
});
});

// R20 R19-29:三个冗余死读端点(无任何前端/客户端消费,专用端点 + SDK 桩无调用者)——根因回归:
// 基线上这三条路径仍然可达/仍被文档化,删除后必须整条从运行时路由与 OpenAPI 文档一起消失。
// 这条测试在删除改动落地前会红(路径仍存在/仍是 200-401 而非 404),落地后转绿。
test("R19-29 dead read endpoints (handoff / ai-worklog·today / list-work-item-proposals) are fully removed", async () => {
const response = await app.request("/api/openapi.json");
const body = await response.json() as { paths: Record<string, Record<string, unknown>> };

// GET /api/agent-runs/{id}/handoff:整条路径已删(唯一方法就是这个 get)。
assert.equal(body.paths["/api/agent-runs/{id}/handoff"], undefined, "handoff path must be gone from OpenAPI");

// GET /api/ai-worklog/today:整条路径已删;无鉴权直接命中路由层 404(不再进 requireCurrentUser 中间件,
// 也就不会是 401)——这一步是活的 HTTP 回归,不只是静态 OpenAPI 检查。
assert.equal(body.paths["/api/ai-worklog/today"], undefined, "ai-worklog/today path must be gone from OpenAPI");
const worklogHttp = await app.request("/api/ai-worklog/today");
assert.equal(worklogHttp.status, 404, "GET /api/ai-worklog/today must 404 once the route is removed");

// GET /api/workitems/{id}/proposals:只删 get,POST(创建提议)必须原样保留。
const workItemProposalsPath = body.paths["/api/workitems/{id}/proposals"] as
| Record<string, unknown>
| undefined;
assert.equal(workItemProposalsPath?.["get"], undefined, "list-work-item-proposals get must be gone");
assert.ok(workItemProposalsPath?.["post"], "create-proposal post must stay (still consumed)");
});

test("templated OpenAPI paths declare their required path parameters", async () => {
const response = await app.request("/api/openapi.json");
const body = await response.json() as { paths: Record<string, Record<string, unknown>> };
Expand Down Expand Up @@ -1080,7 +1104,6 @@ test("project and drive OpenAPI routes document runtime path and query parameter
["/api/meetings/projects/{projectId}/insights/{insightId}/dismiss", "post", ["projectId", "insightId"]],
["/api/meetings/workitems/{workItemId}/proposal-draft", "post", ["workItemId"]],
["/api/workitems/{id}/proposals", "post", ["id"]],
["/api/workitems/{id}/proposals", "get", ["id"]],
["/api/workitems/{id}/conflicts", "get", ["id"]],
["/api/proposals/{id}", "get", ["id"]],
["/api/proposals/{id}/review", "post", ["id"]],
Expand All @@ -1094,7 +1117,6 @@ test("project and drive OpenAPI routes document runtime path and query parameter
["/api/workitems/{id}/agent-runs", "post", ["id"]],
["/api/agent-runs/{id}", "get", ["id"]],
["/api/agent-runs/{id}/trace", "get", ["id"]],
["/api/agent-runs/{id}/handoff", "get", ["id"]],
["/api/agent-runs/{id}/abort", "post", ["id"]],
["/api/agent-runs/{id}/replay", "get", ["id"]],
["/api/agent-runs/{id}/revert", "post", ["id"]],
Expand Down Expand Up @@ -1360,7 +1382,7 @@ test("push streams and audit OpenAPI routes document runtime UUID guards and res
assert.deepEqual(auditNotFoundError?.properties?.code, { type: "string", enum: ["not_found"] });
});

test("pilot metrics and AI worklog OpenAPI routes document query and response contracts", async () => {
test("pilot metrics OpenAPI route documents query and response contracts", async () => {
const response = await app.request("/api/openapi.json");
const body = await response.json() as { paths: Record<string, Record<string, unknown>> };

Expand Down Expand Up @@ -1398,32 +1420,8 @@ test("pilot metrics and AI worklog OpenAPI routes document query and response co
enum: ["validation_error", "invalid_range"]
});

const worklogResponse = jsonResponseSchema(body.paths, "/api/ai-worklog/today", "get", "200");
const worklogData = worklogResponse?.properties?.data as { required?: string[]; properties?: Record<string, unknown> } | undefined;
assert.deepEqual(worklogResponse?.required, ["ok", "data"]);
assert.deepEqual(worklogData?.required, [
"runs_today",
"autonomy_rate",
"accepted_today",
"saved_hours_estimate",
"skills_promoted_today",
"skills_refined_today",
"generated_at"
]);
assert.deepEqual(Object.keys(worklogData?.properties ?? {}).sort(), [
"accepted_today",
"autonomy_rate",
"generated_at",
"range_label",
"runs_today",
"saved_hours_estimate",
"skills_promoted_today",
"skills_refined_today"
]);
const worklogAuth = jsonResponseSchema(body.paths, "/api/ai-worklog/today", "get", "401");
const worklogAuthError = worklogAuth?.properties?.error as { properties?: Record<string, unknown> } | undefined;
assert.deepEqual(worklogAuth?.required, ["ok", "error"]);
assert.deepEqual(worklogAuthError?.properties?.code, { type: "string", enum: ["not_identified"] });
// R20 R19-29:GET /api/ai-worklog/today 已删(死冗余,无消费;数据早已内嵌进 attention 页 VM)——
// 原先这里的 worklogResponse/worklogAuth 契约断言随路由一并删除,见下方新增的删除回归断言。
});

test("Task intake and AgentRun OpenAPI responses document the execution chain", async () => {
Expand Down Expand Up @@ -1615,7 +1613,7 @@ test("Task intake and AgentRun OpenAPI responses document the execution chain",
["/api/workitems/{id}/agent-runs", "post"],
["/api/agent-runs/{id}", "get"],
["/api/agent-runs/{id}/trace", "get"],
["/api/agent-runs/{id}/handoff", "get"],
// R20 R19-29:/handoff 端点已删(死冗余,无消费)。
["/api/agent-runs/{id}/abort", "post"],
["/api/agent-runs/{id}/replay", "get"]
] as const) {
Expand All @@ -1629,7 +1627,7 @@ test("Task intake and AgentRun OpenAPI responses document the execution chain",
["/api/workitems/{id}/agent-runs", "post"],
["/api/agent-runs/{id}", "get"],
["/api/agent-runs/{id}/trace", "get"],
["/api/agent-runs/{id}/handoff", "get"],
// R20 R19-29:/handoff 端点已删(死冗余,无消费)。
["/api/agent-runs/{id}/abort", "post"],
["/api/agent-runs/{id}/replay", "get"]
] as const) {
Expand All @@ -1645,7 +1643,7 @@ test("Task intake and AgentRun OpenAPI responses document the execution chain",
["/api/workitems/{id}/agent-runs", "post"],
["/api/agent-runs/{id}", "get"],
["/api/agent-runs/{id}/trace", "get"],
["/api/agent-runs/{id}/handoff", "get"],
// R20 R19-29:/handoff 端点已删(死冗余,无消费)。
["/api/agent-runs/{id}/abort", "post"],
["/api/agent-runs/{id}/replay", "get"]
] as const) {
Expand All @@ -1671,9 +1669,8 @@ test("Task intake and AgentRun OpenAPI responses document the execution chain",
enum: ["validation_error"]
});

const handoffResponse = jsonResponseSchema(body.paths, "/api/agent-runs/{id}/handoff", "get", "200");
assert.deepEqual(handoffResponse?.required, ["ok", "data"]);
assert.ok(handoffResponse?.properties?.data, "GET /api/agent-runs/{id}/handoff missing nullable handoff data schema");
// R20 R19-29:GET /api/agent-runs/{id}/handoff 已删(死冗余,无消费)——原先这里的 handoffResponse
// 契约断言随路由/openapi 条目一并删除;同样的结构化 handoff 数据已在下面 replayResponse 里覆盖。

const replayResponse = jsonResponseSchema(body.paths, "/api/agent-runs/{id}/replay", "get", "200");
assert.deepEqual(replayResponse?.required, ["ok", "data", "meta"]);
Expand Down Expand Up @@ -2121,7 +2118,7 @@ test("Proposal OpenAPI contracts document review, merge, and conflict action pay

for (const [path, method] of [
["/api/workitems/{id}/proposals", "post"],
["/api/workitems/{id}/proposals", "get"],
// R20 R19-29:GET /api/workitems/{id}/proposals(list)已删(死冗余,无消费)。
["/api/workitems/{id}/conflicts", "get"],
["/api/proposals/{id}", "get"],
["/api/proposals/{id}/review", "post"],
Expand Down Expand Up @@ -2171,12 +2168,11 @@ test("Proposal OpenAPI contracts document review, merge, and conflict action pay
enum: ["proposal_already_exists"]
});

for (const [path, method] of [
["/api/workitems/{id}/proposals", "get"],
["/api/proposals/{id}", "get"]
] as const) {
const schema = jsonResponseSchema(body.paths, path, method, "200");
assert.deepEqual(schema?.required, ["ok", "data"], `${method.toUpperCase()} ${path} missing proposal response`);
// R20 R19-29:GET /api/workitems/{id}/proposals(list)已删(死冗余,无消费)——原先与它同批断言的
// 200 响应形状检查随路由/openapi 条目一并删除,/api/proposals/{id} 的等价检查已在别处覆盖。
{
const schema = jsonResponseSchema(body.paths, "/api/proposals/{id}", "get", "200");
assert.deepEqual(schema?.required, ["ok", "data"], "GET /api/proposals/{id} missing proposal response");
}

const reviewRequest = jsonRequestSchema(body.paths, "/api/proposals/{id}/review", "post");
Expand Down
5 changes: 3 additions & 2 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { createPushRoutes } from "./routes/push.js";
import { createNotificationRoutes } from "./routes/notifications.js";
import { createAuditRoutes } from "./routes/audit.js";
import { createPageRoutes } from "./routes/pages.js";
import { createAiWorklogRoutes } from "./routes/ai-worklog.js";
import { createDriveRoutes } from "./routes/drive.js";
import { createMeetingRoutes } from "./routes/meetings.js";
import { createPilotRoutes } from "./routes/pilot.js";
Expand Down Expand Up @@ -317,7 +316,9 @@ app.route("/api", createPersonalProjectRoutes());
// R20 P2A(R19-21):工作区级审计列表(GET /api/workspace/audit,仅管理员,工作区硬隔离)。
app.route("/api", createWorkspaceAuditRoutes());
app.route("/api/pilot", createPilotRoutes());
app.route("/api/ai-worklog", createAiWorklogRoutes());
// R20 R19-29:/api/ai-worklog/today(createAiWorklogRoutes)已删——web/desktop 均无调用者,同样的今日
// AI 工作量数据早已由 GET /api/pages/attention 等页面 VM 内嵌 AiWorklogMetricsService 交付。核实零消费
// 后连路由文件(routes/ai-worklog.ts)一并删除;服务本身(services/ai-worklog-metrics.ts)仍在用,未动。

app.onError((error, c) => {
if (error instanceof ZodError) {
Expand Down
Loading
Loading