@zhushanwen/pi-scheduler
定时任务调度扩展:按 duration(`5m` / `2h` / `1d`)间隔或 cron 表达式,在指定时间向 agent 注入消息。支持一次性提醒(once)与过期策略(expires)。创建入口两路:**人侧 `/schedule` 命令打开创建表单**(表单异步打开,填表时长不受命令通道超时约束),**模型侧 `schedule` tool 直建**(不再弹确认表单;参数不完整时必须先向用户澄清——见「创建方式」)。任务随 owner session 持久化,resume 后继续触发。
Package details
Install @zhushanwen/pi-scheduler from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@zhushanwen/pi-scheduler- Package
@zhushanwen/pi-scheduler- Version
0.13.1- Published
- Nov 3, 2026
- Downloads
- 2,070/mo · 715/wk
- Author
- zhushanwen321
- License
- unknown
- Types
- extension, skill
- Size
- 450.3 KB
- Dependencies
- 6 dependencies · 4 peers
Pi manifest JSON
{
"skills": [
"./skills"
],
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
scheduler
定时任务调度扩展:按 duration(5m / 2h / 1d)间隔或 cron 表达式,在指定时间向 agent 注入消息。支持一次性提醒(once)与过期策略(expires)。创建入口两路:人侧 /schedule 命令打开创建表单(表单异步打开,填表时长不受命令通道超时约束),模型侧 schedule tool 直建(不再弹确认表单;参数不完整时必须先向用户澄清——见「创建方式」)。任务随 owner session 持久化,resume 后继续触发。
产品定位
pi-scheduler 是 session 存活期间的 AI 提醒器——在 pi 进程运行、session 打开时,按计划向当前 session 注入消息。
非系统级 cron:
- pi 进程不开 = 不触发(与常驻后台的系统 cron daemon 不同,本扩展不是后台守护进程)
- 电脑睡眠 / 关机 = 不触发
- 不依赖系统 crontab,不注册任何开机自启
任务归属创建它的 session:任务物理存储为独立 JSON 快照分片(按 owner sessionId 分片,见「数据存储位置」),只在 owner session 打开(继续对话 / resume)时才触发。
这是设计决策,不是 bug(D9):本扩展定位是 session 级 AI 提醒器——任务归属创建它的 session,只在 owner session 存活时触发。如果你每天开新 session,昨天建的"明早检查 CI"任务今天不会响,除非你 resume 昨天创建该任务的那个 session。这不是"任务丢了":任务随 session 持久化,session 不打开就不调度。用户若发现「昨天建的任务今天没响」,这是预期行为,请 resume 创建该任务的 session。
简介与安装
pi-scheduler 是 taiji 的 mandatory 扩展(packages/shared/src/mandatory-extensions.json,tier: feature)——taiji 启动时自动安装并启用,无需手动操作。
独立 pi 环境手动安装:
npm install @zhushanwen/pi-scheduler
安装后扩展在 pi 会话启动时自动装配(见下节),无需额外配置。
工具激活
schedule / schedule_control 两工具均为 direct 常驻——注册不声明 exposure 字段(pi SDK 缺省 direct),工具声明随会话常驻、对模型默认可见,无需经 tool_search 检索激活,独立 pi 开箱即用。
激活方式
扩展 factory(src/index.ts)监听 pi 的 session 生命周期事件,在 session_start 时装配完整调用链:
session_start
├─ 推导任务库分片路径:schedulerStoreFilePath(getAgentDir(), ctx.cwd, shardKey)
│ (shardKey = sessionId;--no-session 等空/非法值归一为 'no-session')
├─ PiSchedulerBackend(投递端口 dispatch + 快照文件读写 + 时间源)
│ └─ SchedulerRuntime(内存态 + 30s tick 调度 + 限流)
│ └─ SchedulerService(tool/command 唯一业务入口)
├─ runtime.loadTasks(backend.loadTasks()) ← 一次 JSON.parse 直读快照分片重建任务集
│ (分片不存在 = 空任务集,正常路径;
│ fork 新 sessionId → 新分片,天然不继承)
├─ runtime.startScheduler() ← 启动 30s tick
└─ 注册 scheduler widget(每个 tick 后刷新)
运行时每次任务集变更(创建 / 更新 / 推进 / 启停 / 删除)都把内存任务集全量快照原子落盘到该 session 的分片文件(persistTasks:tmp + fsync + rename 原子替换,见「数据存储位置」)。会话 JSONL 只承载 dispatch 注入消息与用户对话,不承载任务数据(任务库与会话事实分离,2026-10-30 裁决)。
/schedule 命令用法
注册为 /schedule。无参数 → 打开创建表单(空草稿,默认 循环 + 每天 09:00);带参 <schedule> <prompt> → 打开预填表单;第一个参数匹配子命令关键词则走子命令分支。命令 handler 异步打开表单后立即返回——用户在表单里停留多久都不会触发命令通道的 60s 超时(会话级提醒器不该因为「用户想了两分钟」而报错)。
子命令
| 子命令 | 行为 |
|---|---|
/schedule |
打开创建表单(空草稿;默认循环 + 每天 09:00) |
/schedule <schedule> <prompt> |
打开预填时间/提示词的创建表单(如 /schedule 5m 'check build') |
/schedule list |
列出所有任务(id、名称、调度、下次执行时间) |
/schedule on <id> |
启用任务 |
/schedule off <id> |
停用任务(推荐临时暂停用 off,不用 rm) |
/schedule rm <id> |
删除任务 |
/schedule run <id> |
立即执行任务 |
一次性(once)与 cron 的形态由表单内选择/填写(执行模式切换 + 自定义 cron 输入),不再有 once / cron 关键字子命令。任务 id 由 8 位 hex 自动生成,list 后从输出中获取。
引号转义
参数用 shell 风格引号解析(tokenizeQuoted,src/commands.ts):
- 单引号
'...'或双引号"..."内的内容作为一个 token,引号字符本身被剥离 - 含空格的多词参数(cron 表达式、prompt)必须加引号,否则会被拆成多个 token
例如 prompt check build 写成 /schedule 5m 'check build'(预填表单的提示词字段)。引号只包住含空格的那个参数本身,不要在外层再套引号(嵌套引号会拆出错误 token)。
子命令补全
输入 /schedule 后 Tab 补全子命令关键词(list/on/off/rm/run);on/off/rm/run 之后补全当前任务 id。
schedule 语法
duration(间隔调度)
<数字><单位>,数字与单位间不留空格,大小写不敏感:
| 单位 | 含义 | 乘数 |
|---|---|---|
s / sec / second / seconds |
秒 | 1,000 ms |
m / min / minute / minutes |
分 | 60,000 ms |
h / hr / hour / hours |
时 | 3,600,000 ms |
d / day / days |
天 | 86,400,000 ms |
例如:5m、2h、1d、30seconds、2hours。非法输入(裸数字、未知单位、空串、负值)解析失败:schedule tool 在预校验即报错(Invalid parameters: unrecognized schedule "...",修正参数后重调);/schedule 命令在 json/print 模式带参直建时报 Invalid schedule: "..."(rpc/tui 模式则由表单提交后报错)。
cron(时间点调度)
标准 cron 表达式,支持 5 字段与 6 字段:
- 5 字段(分 时 日 月 周):自动补
0秒字段(如0 9 * * 1-5→0 0 9 * * 1-5) - 6 字段(秒 分 时 日 月 周):原样使用
含空格自动走 cron 分支:schedule 输入中不含空格 → 按 duration 解析;含空格 → 按 cron 解析。因此 cron 表达式必须包含空格(正常写法天然如此),duration 不得含空格。
选项语义
任务创建与管理由两个 tool 承担:schedule(prompt / schedule / kind / name / expires / model)与 schedule_control(action: list / toggle / delete / run,附 id / enabled)。schedule 的参数即创建参数:调用直接创建任务,无确认表单(见「创建方式」)。下表为 schedule tool 的选项语义(/schedule 表单的 once / cron 形态对应 kind / 自定义 cron):
| 选项 | 取值 | 语义 |
|---|---|---|
kind |
recurring(默认)/ once |
recurring 每次触发后按 schedule 重算下次时间;once 触发一次后自动删除 |
name |
字符串 | 任务可读名称,缺省从 prompt 自动生成(≤30 字原样,超长截前 27 字加省略号) |
expires |
duration 字符串 / never |
recurring 任务的过期时间:now + duration;never 永不过期;缺省 7 天。once 任务忽略 expires 参数(触发即删,传不传都不生效) |
model |
scoped model id(provider/model) |
任务执行所用模型;缺省跟随会话当前模型 |
创建方式(触发反转)
两条路径,职责不同:
人侧 /schedule:打开创建表单(GUI 由 FormOverlay 的 ScheduleForm 渲染,TUI 由 ScheduleCreateComponent),用户在表单里自选模式/时间/模型/提示词后创建。命令 handler 异步打开表单后立即返回——填表时长不受命令通道超时约束。模式矩阵:
| 形态 | 会话模式 | 交互 |
|---|---|---|
| GUI 表单 | rpc | 统一表单协议(uiFormInteract + marker select 通道):FormOverlay 弹出 schedule 表单,预填草稿经 initial 直传;命令立即返回,交互在回包窗口之外续跑 |
| TUI 表单 | tui | ctx.ui.custom 挂 ScheduleCreateComponent:模式 → 时间 → 模型 → 提示词 → 提交 五 tab 逐项确认,两段 Esc 取消 |
| 非交互 | json / print | 不做交互:带参 <schedule> <prompt> → 直建(成功看 session entry,/schedule list 可验)。无参或带参失败(非法表达式 / 缺 prompt)→ 抛错(该模式 ctx.ui.notify 是 no-op、handler 返回值被丢弃,stderr 是唯一可见通道) |
模型侧 schedule tool:直接创建(无确认表单、无 headless「未经确认」附注)。参数不完整时必须先经 ask-user / 对话澄清,禁止猜测频率后静默创建(promptGuidelines 硬性条目)。创建成功的 tool result 含任务 id 与后续运行时刻,模型应复述给用户以便核对/撤销。
表单通道/协议的失败折叠(命令路径):
| 态 | 触发 | 结果 |
|---|---|---|
| cancelled | 用户取消表单 | 不创建、无 toast(取消不是错误) |
| channel-error | RPC 交互通道不可用:select reject,或回包回显请求 payload(宿主不识别表单协议的旧组合) | ctx.ui.notify(..., 'error') 提示通道不可用 / 升级宿主;本会话后续 /schedule 直接给同样提示,不重复试探 |
| non-json | 回包形状非法(非协议 JSON / 非 ScheduleFormResult)= 协议版本错配 | ctx.ui.notify(..., 'error') 提示协议版本错配 |
| abort | session_shutdown(quit/reload/new/resume/fork)→ 命令路径自持 AbortController abort |
不创建、无 toast(taiji 内切换会话不触发,表单保留且仍有效) |
取消/超时不是错误:不创建、无 toast。命令路径的错误出口统一按 mode 分流——rpc / tui 走 notify(..., 'error')(pi 会把 handler 异常折成 renderer 不消费的 extension.error,throw 在 rpc 下静默丢弃);json / print 一律 rethrow。
文案与语言(L2 本地化)
命令反馈(创建/列表/启停/删除/用法/错误提示)与 TUI widget 文本、托盘标题均由扩展侧词典(src/i18n.ts)按当前界面语言渲染;语言经 <dataDir>/ui-preferences.json(runtime 写、扩展只读,见 data-source-registry 登记行)就地读取,文件缺失/损坏回落 en-US。/schedule 的 registerCommand.description 是注册期静态串、托盘标题随 widget 刷新(≤30s)跟进语言——两者切语言后不会立即热更(已接受滞后)。
模型可见文案(schedule tool 的 description / promptGuidelines / tool result)保持英文,不经 L2 词典——两受众不串(本地化不得污染 tool result)。
示例
recurring 间隔任务(/schedule 打开预填表单,确认后创建):
/schedule 5m 'check build'
交互创建(无参打开空表单,表单里选每天 09:00):
/schedule
一次性提醒:在 /schedule 表单里把执行模式切到「一次性」并选时刻。
cron 任务:在 /schedule 表单里选自定义 cron 并填表达式(如 0 9 * * 1-5)。
非交互创建(json/print 模式,无表单可开,带参直建):
pi -p "/schedule 5m 'check build'"
立即执行(schedule_control tool 调用:马上 dispatch 现有任务):
{"action": "run", "id": "<task-id>"}
永不过期(schedule tool 调用:直接创建长期 recurring 任务):
{"prompt": "monthly report", "schedule": "1d", "expires": "never"}
限制与运行时行为
| 限制/行为 | 值 | 说明 |
|---|---|---|
| 任务上限 | 50(MAX_TASKS) |
超过抛 Task limit reached (50),需先删除任务 |
| 触发频率上限 | 6 次/分钟(RATE_LIMIT_PER_MINUTE) |
滑动 60s 窗口。/schedule run 超限返回 not dispatched(disabled, rate-limited, or dispatch in flight);tick 自动 dispatch 超限静默跳过 |
| tick 间隔 | 30s(TICK_INTERVAL_MS) |
到期任务在下一个 tick 被 dispatch;实际触发时间可能比计划晚最多 30s |
| 默认过期 | 7 天(DEFAULT_EXPIRY_MS) |
recurring 任务缺省 expires 时;expires: 'never' 关闭 |
| once 任务 | 触发后自动删除 | 不参与后续调度 |
| cron 失效 | 任务停用 + lastStatus=failed + lastError='cron expression invalid' |
不会用 now() 兜底导致每 tick 重触发死循环 |
| 忙时 dispatch | steer 直投(scheduler-steer-direct-dispatch) | busy 时消息插入当前 turn、idle 时开新 turn({deliverAs:'steer', triggerTurn:true}),受理即记账不排队;同任务 in-flight 检查防双投 |
| 任务级模型切换 | dispatch 前 setModel(task.model),完成后恢复原模型 | task.model 非空且 ≠ 会话当前模型时按「互斥校验 → setModel → dispatch」执行,busy 亦生效(steer turn 开新 turn 取模型快照);恢复挂 agent_settled(run 完全沉降)+ tick 事实查询兜底;全部 setModel 经模型 op 串行队列防并发交错;模型切换秩序设计(design D3)原文未归档,裁决内容与背景以 commit 1def857fe(初版与 P-MODEL 探针修订)/ 005fb44bf(模型 op 串行化收敛)/ 8159e3fe7(agent_settled 恢复锚点)message 为溯源锚 |
| widget 推送节流 | 指纹跳推(任务集不变即零推送) | widget 行内容静态化(不含随时间流逝变化的元素)是跳推前提;每次推送前对比任务集指纹,不变即跳过本次推送(判定在 index.ts 的 refreshWidget);widget 推送修正设计原文未归档,裁决内容与背景以 commit 4bd690a54 message 为溯源锚 |
| history | 保留最近 20 条执行记录 | 超出丢弃最旧(上限由 appendExecutionRecord 在 runtime 记账侧单点裁剪);快照写入口不裁剪——只剥离运行时标记 pending 后逐项拷贝 history 全量序列化 |
| 持久化 | 任务集全量快照原子落盘(每次任务集变更即写,tmp+fsync+rename) | 任务随 owner session 分片持久化,resume 后直读恢复;落盘失败 warn + 内存态照常推进、下写点重试 |
| 交付语义 | 受理即记账(投递端口 at-most-once 注入) | dispatch 经投递端口受理后注入失败首败即弃(forget,2026-10-25 裁决:丢一轮 << 重投造成重复提醒);端口拒收(rejected)走 failed 记账、下个 tick 单通道重试 |
| 延迟写入窗口 | 无(写盘不依赖 pi 会话首写门控) | 任务库独立于会话 JSONL,每次变更即落盘——旧「pi 首条 user 或 assistant 消息前不 flush」的丢失窗口已随任务库分离消除 |
| 触发条件 | pi 进程需存活且 session 打开 | 电脑睡眠 / pi 进程未运行 = 不触发(非系统 cron,无后台守护) |
错误语义(无错误码;双受众分派——message 英文回退供 tool result / messageKey+params 供命令层词典渲染,见「文案与语言」):schedule tool 创建时参数预校验失败(prompt 空 / schedule 非法)→ 直接 throw Invalid parameters: ...(修正参数后重调);/schedule 命令 json/print 模式带参解析失败 → throw 词典文案(en: Invalid schedule: "...");run/toggle/delete 引用不存在的 id / run 时任务 disabled / rate-limited / 同任务在途 / 任务数超上限 → 命令层 toast 按词典本地化(en 示例:Task <id> not found / Task <id> not dispatched (disabled, rate-limited, or dispatch in flight) / Task limit reached (50) — delete one first)。表单交互的取消/abort 不是错误(不创建、无 toast);通道失败/回包非法在 rpc/tui 走 notify(..., 'error')、在 json/print 走 throw——见「创建方式」。
数据存储位置
当前机制(独立 JSON 快照分片)
任务存储为独立 JSON 快照文件(scheduler-redesign 方案 B:任务库与会话事实分离,2026-10-30 裁决;方案 B 设计原文未归档,裁决内容与背景以 commit 73073b169 message 为溯源锚),路径由 schedulerStoreFilePath(getAgentDir(), cwd, sessionId) 推导:
<agentDir>/scheduler/<root>/<segments>/<sessionId>.json
agentDir= pi 的getAgentDir()官方数据目录<root>/<segments>= owner session 工作目录(cwd)的路径隔离层——同 agentDir 下不同 cwd 互不干扰<sessionId>.json= 分片文件:持久会话按 sessionId 分片(resume 同 id 同文件恢复;fork 新 id → 新分片空任务集,天然不继承,等价旧 fork owner 过滤语义);--no-session等空/非法 sessionId 归一为固定分片no-session(任务仍持久化,跨重启命中同一分片恢复)
文件形状:{ schemaVersion: 2, sessionFile?: string, tasks: ScheduledTask[] }——tasks 为内存任务集全量快照(写入口统一剥离运行时标记 pending、逐项拷贝 history);sessionFile 为冗余排障字段。
原子替换写
每次任务集 mutation 全量落盘(backend.persistTasks → writeSchedulerStore):序列化到 tmp 文件 → fsync → rename(POSIX rename 原子性)。崩溃时要么旧文件完整、要么新文件完整,无半写形态。目录不存在则递归创建(首写建目录)。写失败 throw,由 runtime 的 persistTasksSafe 捕获:warn + 内存态照常推进,下一次任务集变化的写点重试。
读路径三分支
backend.loadTasks(装配点 session_start 调用,非接口成员)→ readSchedulerStore 一次 JSON.parse:
- 文件不存在(missing)→ 空任务集(新 session / fork / 从未建任务,正常路径)
- 损坏(corrupt:解析或形状校验失败)→ 改名
<path>.corrupt-<ts>留证 + warn,按空任务集继续(调度不中断);改名自身失败不抛,仍返回 corrupt - 其他读错误(EACCES 等)→ 上抛,由装配点显式降级(warn + 空任务集继续装配,恢复失败计数累计可见)
会话 JSONL 的边界
会话 JSONL 只承载 dispatch 注入消息(到点注入时会话必已含 user 或 assistant 消息——任务只能经用户命令或模型 tool 创建,二者都以 user 或 assistant 消息为前提,pi 首写门控早已通过)与用户对话;任务定义与调度记账不写会话 JSONL——无首写门控依赖,任务库写盘独立于会话流。旧 event sourcing 写链(pi-scheduler: custom entry 族 + replay 重放折叠)已随 plan B 整体删除。
单一写者 + 同源单实现
分片键 = sessionId 保证单写者:同一 session 同一时刻只有一个 pi 进程,分片文件天然无并发写。已知例外:--no-session 等空/非法 sessionId 归一为固定分片 no-session——同 cwd 并存多个 pi --no-session 进程时共写同一文件,写侧整文件 RMW(tmp → fsync → rename)无跨进程锁,后写覆盖先写。store 模块(@zhushanwen/extension-protocol/scheduler-store)是扩展(pi 进程内,唯一写者)与 taiji scheduler-manager 面板(runtime worker,只读者)的同源单实现——路径推导与文件形状两侧构造性一致,双实现会漂移。
不进入 LLM context
任务数据存独立文件,天然不进 pi 的 context 构建——零 token 污染,不依赖 pi 对 custom entry 的过滤行为。
归属即结构性质
任务物理存在于 owner session 的分片文件。分片文件删除 = 任务消失,无残留、无需 GC。但 session 删除不回收分片:分片在 <agentDir>/scheduler/...(pi agent 数据目录)不在 session 存储树内,无 owner、无清理路径(已知例外,见 data-source-registry #88)——fork 出的 session 用新 sessionId → 新分片(不加载、不执行继承的任务副本,原 session resume 照常),subagent 也不受主 session 任务干扰。
旧 store(v1)处置
旧版 store(cwd 共享的 scheduler.json,v1 schema)与新分片同目录不同名、互不冲突;按用户裁决不迁移、留盘为孤儿——升级后旧文件原样保留,不读不写。
依赖的 pi 行为清单
本扩展的存储与投递方案(独立快照分片 + 投递端口 dispatch)依赖以下 pi 源码行为。这些是实测存在但非 SDK 契约承诺的隐式行为,pi 升级后需逐条复核。各条锚点实测自 pi v1.0.0:路径相对 pi-mono 仓库 packages/coding-agent/src/(core/ 前缀照写;config.ts 在包根,agent.ts 取自 packages/agent/src/),行号为该版本源码行;pi 升级后按文件名 + 符号名重新定位行号复核(行号会漂移,符号名是稳定锚点):
getAgentDir()返回进程级数据目录:任务库分片根路径来源。实测:PI_CODING_AGENT_DIR环境变量覆盖优先、缺省~/.pi/agent(config.ts:563-569;常量名定义 config.ts:543)。runtime 注入 pi 进程的PI_CODING_AGENT_DIR与 taiji 面板侧getPiAgentDir()必须指向同一目录,否则两侧分片路径漂移(同源单实现的前提)sessionManager.getSessionId()/getSessionFile()稳定语义:sessionId 是分片键(resume 同 id / fork 新 id 的恢复语义全系于此);getSessionFile()为空 =--no-session模式的判据(归一no-session分片)。实测:两 getter 直读字段(core/session-manager.ts:1152-1158);resume 加载时 sessionId 取自文件 header(core/session-manager.ts:1090),fork / 新 session 换新 id(core/session-manager.ts:1713、1745)。若 pi 改变 sessionId 的稳定性(如 resume 换 id),任务恢复会失效host.sendMessagesteer 档注入形态:dispatch 经投递端口翻译为 steer 档sendMessage(busy 时消息插入当前 turn、idle 时开新 turn,{deliverAs:'steer', triggerTurn:true})。实测:扩展 sendMessage 绑定到sendCustomMessage(core/agent-session.ts:3341-3349);isStreaming时 steer 档转入agent.steer、非 streaming 且triggerTurn时开新 turn(core/agent-session.ts:2259-2272);steer 消息在当前 assistant turn 的工具调用结束后、下一次 LLM 调用前注入(agent.ts:299-301 入队,注入时机注释 core/agent-session.ts:2156-2158)。若 pi 改变 steer 语义(注入失败形态、triggerTurn 行为),dispatch 交付语义需重新评估ctx.isIdle()同步事实查询(U4 dispatch 模型切换恢复判定锚点):agent_settled 事件的「run 完全沉降」复核依赖它。实测:接口声明 core/extensions/types.ts:348,ctx 绑定经 runner 转发(core/extensions/runner.ts:909-912),getter 实装 =!_isAgentRunActive && !isCompacting(core/agent-session.ts:1435-1437)。若语义变化,模型恢复判定需重新评估- 会话首写门控(user 或 assistant 消息):会话 JSONL 文件在会话首次含 user 或 assistant 消息时才创建(首 flush);setup entry(model / thinkingLevel / system prompt)与扩展注入的 custom message 单独存在时不落盘。任务库写盘不依赖该门控(任务库独立于会话 JSONL),但「任务创建后会话文件已存在」的推断以它为前提。实测:
_hasConversation()判据 =fileEntries中类型message且 role 为user或assistant(core/session-manager.ts:1166-1170),_persist首次 flush 在该判据成立时以openSync(..., 'wx')建文件(core/session-manager.ts:1172-1177),注释原文「Starting at the user message (not the first assistant reply)」(core/session-manager.ts:1161-1164;pi 包 dist 编译产物对应 core/session-manager.js:786-798)。若 pi 改变门控(如首条任意 entry 即落盘,或扩展 custom message 也算会话事实),依赖该前提的叙述与验收需重新评估
任一条行为变更都需重新验证验收场景(尤其 resume、fork、dispatch 场景)。
开发
pnpm test # 运行全部 vitest 测试(等价 npx vitest run)
npx vitest run src/__tests__/<file>.test.ts # 单个文件
测试策略:
- 依赖反转:
SchedulerRuntime只依赖SchedulerBackend接口(deliverDispatch/persistTasks/now),不触碰 FS/pi。测试注入MockSchedulerBackend(src/__tests__/mock-backend.ts测试专用实现,记录 deliverDispatch/persistTasks 调用、支持注入固定时间与落盘失败)实现零副作用测试;backend.loadTasks类方法由 store 模块的真实读写覆盖——持久化只经 scheduler-store 纯 FS 读写(无进程内状态),测试注入临时目录路径即可全行为覆盖 - 纯函数:
parseDuration/formatDuration/parseSchedule/computeNextRunAt/computeNextRuns(src/parsing.ts)无副作用,可直接断言 - 快照读写:store 模块三分支读(ok / missing / corrupt 改名留证)+ 原子替换写 + 写入口 pending 剥离与 history 逐项拷贝(不裁剪——20 条上限属
appendExecutionRecord,@zhushanwen/extension-protocolscheduler-store 子出口测试) - 投递链:backend deliverDispatch 的端口受理/拒收/幂等键路径(
src/__tests__/backend-delivery.test.ts) - 直建流:
handleSchedule预校验 / abort / 参数透传(src/__tests__/tool-create-flow.test.ts);命令路径表单(无参 / 带参预填 / 取消 / 提交 / channel-error / non-json / echo / 模式矩阵 / 异步不阻塞 / json·print throw,src/__tests__/commands-form.test.ts);交互生命周期(草稿构造 + AbortController 注册表,src/__tests__/interaction.test.ts)
扩展内部结构:backend.ts(后端抽象:投递端口 dispatch + 快照读写)→ runtime.ts(调度核心,任务集 mutation 后 persistTasksSafe 全量快照落盘)→ service.ts(业务入口)→ tool.ts(tool 直建流)/ commands.ts(/schedule 命令适配层)→ interaction.ts(表单交互 / 协议错配 / AbortController 生命周期)→ create-form-component.ts(TUI 创建表单组件,由 interaction.ts 消费)→ widget.ts(双模状态栏 widget:GUI 结构化 meta + TUI 文本行)→ i18n.ts(L2 词典 / locale 读取 / 结果渲染)。快照文件读写实装在 @zhushanwen/extension-protocol 的 scheduler-store 子出口(packages/extension-protocol/src/extensions/scheduler/store.ts,与 taiji scheduler-manager 面板同源单实现)。