Files
wangjia 740d2c2637 feat: PR 审核流程 + 自动合并 + macOS 通知 + 订阅额度透传 + 归档详情
PR 流程(开发→提PR→CodeReview→安全审计→审核→通过即合并):
- reviewer 拆分为 code review 与安全审计两个独立 CC run(kind=reviewer/security),
  TaskResult 四字段(summary/verdict + securitySummary/securityVerdict),闸上两节报告两枚结论徽章
- executor/merge.ts: 通过即合并——临时 worktree 内 merge --no-ff,绝不碰用户工作区/不 push;
  冲突安全拒绝(报冲突文件);重复合并幂等;合并后回收执行 worktree + 删分支
- decide 路由 merge:false 逃生口 + 看板「仅通过」按钮(目标分支被工作区检出时用)

通知(daemon/notify.ts):
- macOS 原生通知: 进审核闸(复审建议拒绝标⚠)/连续失败需人工/合并完成
- 同任务同类型 60s 抑制、osascript 转义截断、MAESTRO_NOTIFY=0 关闭

订阅额度透传(daemon/usage.ts):
- OAuth usage API(与 Claude Code/claude-hud 同源),凭证 keychain→内存零泄漏
- 60s 成败双缓存+并发去重+5s 超时,失败降级 null
- GET /api/agents 顶层 usage 字段;Agent 面板显示 5h/周用量条+重置倒计时(>80%琥珀/>95%红)

看板与生命周期:
- 归档区: 深度1整树完成沉底,时间倒序分页(尺寸 chip 10/20/50/100 置底)
- 归档详情对话框: 全属性/执行历史与时长/审批记录/状态流转时间线(GET /api/tasks/:id/events)
- 容器收口: 已拆解 Hard 子任务全 done 自动 done(afterDone 逐级向上)
- 同步按钮收进配置面板;执行白名单扩测试命令(npm/go/shellcheck/make/pytest)
- Agent 面板显示调度模式与各复杂度模型;被依赖阻塞→被阻塞

测试: 74/74(新增 merge 6/notify 10/usage 7/容器收口/双复审适配)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 03:22:30 +08:00

105 lines
3.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createUsageFetcher, USAGE_CACHE_MS, USAGE_ENDPOINT } from '../src/daemon/usage.js';
const SECRET = 'tok-secret-do-not-leak';
/** 构造可控时钟 + 计数 fetch 的 fetcher(不真打 API */
function setup(opts: {
body?: unknown;
status?: number;
fetchError?: Error;
token?: string | null;
} = {}): {
getUsage: () => Promise<import('../src/daemon/usage.js').UsageInfo | null>;
calls: { fetch: number; token: number };
logs: string[];
clock: { t: number };
} {
const calls = { fetch: 0, token: 0 };
const logs: string[] = [];
const clock = { t: 1_000_000 };
const body = opts.body ?? {
five_hour: { utilization: 47.0, resets_at: '2026-06-12T22:30:00+00:00' },
seven_day: { utilization: 57.0, resets_at: '2026-06-13T12:00:00+00:00' },
};
const fetchFn = (async (url: string | URL | Request, init?: RequestInit) => {
calls.fetch += 1;
assert.equal(String(url), USAGE_ENDPOINT);
assert.match(String((init?.headers as Record<string, string>).Authorization), /^Bearer /);
if (opts.fetchError) throw opts.fetchError;
return {
ok: (opts.status ?? 200) < 400,
status: opts.status ?? 200,
json: async () => body,
} as Response;
}) as typeof fetch;
const getUsage = createUsageFetcher({
fetchFn,
readToken: async () => { calls.token += 1; return opts.token === undefined ? SECRET : opts.token; },
now: () => clock.t,
log: { info: (m) => logs.push(m), error: (m) => logs.push(m) },
});
return { getUsage, calls, logs, clock };
}
test('usage:成功解析 five_hour/seven_day → UsageInfopercent 取整,resetsAt 透传)', async () => {
const { getUsage } = setup();
const u = await getUsage();
assert.deepEqual(u, {
session: { percent: 47, resetsAt: '2026-06-12T22:30:00+00:00' },
weekly: { percent: 57, resetsAt: '2026-06-13T12:00:00+00:00' },
});
});
test('usage:60s 内存缓存——窗口内不重复请求,过期后重新拉取', async () => {
const { getUsage, calls, clock } = setup();
await getUsage();
await getUsage();
clock.t += USAGE_CACHE_MS - 1;
await getUsage();
assert.equal(calls.fetch, 1);
clock.t += 2; // 越过缓存窗口
await getUsage();
assert.equal(calls.fetch, 2);
});
test('usage:并发调用共享同一 in-flight 请求', async () => {
const { getUsage, calls } = setup();
const [a, b] = await Promise.all([getUsage(), getUsage()]);
assert.equal(calls.fetch, 1);
assert.deepEqual(a, b);
});
test('usagefetch 抛错 → null 且负缓存 60s(不重试风暴),日志不含 token', async () => {
const { getUsage, calls, logs } = setup({ fetchError: new Error('network down') });
assert.equal(await getUsage(), null);
assert.equal(await getUsage(), null);
assert.equal(calls.fetch, 1); // 失败结果同样被缓存
assert.ok(logs.some((m) => m.includes('network down')));
assert.ok(logs.every((m) => !m.includes(SECRET)));
});
test('usageHTTP 非 200 → null', async () => {
const { getUsage, logs } = setup({ status: 401 });
assert.equal(await getUsage(), null);
assert.ok(logs.some((m) => m.includes('401')));
});
test('usage:无凭证 → null 且不发起请求', async () => {
const { getUsage, calls } = setup({ token: null });
assert.equal(await getUsage(), null);
assert.equal(calls.fetch, 0);
});
test('usage:响应缺少两个窗口字段 → null;只有一个窗口 → 另一侧为 null', async () => {
const none = setup({ body: { unrelated: true } });
assert.equal(await none.getUsage(), null);
const onlyWeek = setup({ body: { seven_day: { utilization: 88.6, resets_at: null } } });
assert.deepEqual(await onlyWeek.getUsage(), {
session: null,
weekly: { percent: 89, resetsAt: null },
});
});