34 lines
2.4 KiB
TypeScript
34 lines
2.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { CapabilityId, Cursor, EventId, ProjectId, ServerId, SessionId, Task, TaskId, Timestamp } from "../../protocol/model";
|
|
import { activityReducer, canCancelTask, filterEvents, initialActivityState, isTaskTransitionAllowed } from "./state";
|
|
|
|
const ids = { server_id: "server-1" as ServerId, project_id: "project-1" as ProjectId, session_id: "session-1" as SessionId };
|
|
const task = (state: Task["state"], cancellable = true): Task => ({ ...ids, task_id: "task-1" as TaskId, capability_id: "shell" as CapabilityId, state, cancellable, created_at: "2026-01-01T00:00:00Z" as Timestamp, updated_at: "2026-01-01T00:00:01Z" as Timestamp });
|
|
const event = (id: string, cursor: string, type = "TASK_UPDATED") => ({ event_id: id as EventId, type, timestamp: "2026-01-01T00:00:00Z" as Timestamp, cursor: cursor as Cursor, sequence: 1, context: ids, payload: {} });
|
|
|
|
describe("activity state", () => {
|
|
it("仅接受合法任务状态转换并保护终态", () => {
|
|
expect(isTaskTransitionAllowed("PENDING", "RUNNING")).toBe(true);
|
|
expect(isTaskTransitionAllowed("SUCCEEDED", "RUNNING")).toBe(false);
|
|
const finished = activityReducer(initialActivityState, { type: "task_received", task: task("SUCCEEDED") });
|
|
expect(activityReducer(finished, { type: "task_received", task: task("RUNNING") })).toBe(finished);
|
|
});
|
|
|
|
it("只允许可取消的非终态任务取消", () => {
|
|
expect(canCancelTask(task("RUNNING"))).toBe(true);
|
|
expect(canCancelTask(task("RUNNING", false))).toBe(false);
|
|
expect(canCancelTask(task("FAILED"))).toBe(false);
|
|
});
|
|
|
|
it("记录最新事件游标、去重并保留恢复缺口", () => {
|
|
const once = activityReducer(initialActivityState, { type: "event_received", event: event("e1", "c1") });
|
|
expect(activityReducer(once, { type: "event_received", event: event("e1", "c1") }).events).toHaveLength(1);
|
|
const resumed = activityReducer(once, { type: "resume_completed", cursor: "c3" as Cursor, gap: { requested_cursor: "c1" as Cursor, resumed_cursor: "c3" as Cursor, message: "缺口" } });
|
|
expect(resumed).toMatchObject({ stream_phase: "streaming", last_cursor: "c3", gap: { message: "缺口" } });
|
|
});
|
|
|
|
it("按类型和固定上下文筛选事件", () => {
|
|
expect(filterEvents([event("e1", "c1"), event("e2", "c2", "TASK_OUTPUT")], { types: ["TASK_OUTPUT"], project_id: ids.project_id })).toHaveLength(1);
|
|
});
|
|
});
|