Files
united-workforce/packages/protocol/src/__tests__/thread-index.test.ts
T
xingyue 23e2ae9eb4 refactor(protocol): add status + completedAt to ThreadIndexEntry
- ThreadIndexEntry gains status and completedAt fields
- createThreadIndexEntry defaults to idle/null
- normalizeThreadIndexEntry backward-compat defaults
- updateThreadHead resets to idle (衔尾蛇 resume prep)
- markThreadSuspended sets status=suspended
- New markThreadCompleted(entry, status, now) function
- serializeThreadIndexEntry includes new fields

Part of #39, closes #40
2026-06-04 14:42:14 +08:00

170 lines
4.8 KiB
TypeScript

import { describe, expect, test } from "vitest";
import {
createThreadIndexEntry,
markThreadCompleted,
markThreadSuspended,
normalizeThreadIndexEntry,
parseThreadsIndex,
serializeThreadIndexEntry,
serializeThreadsIndex,
updateThreadHead,
} from "../thread-index.js";
describe("thread-index", () => {
test("parse legacy string head hash", () => {
const entry = normalizeThreadIndexEntry("0123456789ABC");
expect(entry).toEqual({
head: "0123456789ABC",
suspendedRole: null,
suspendMessage: null,
status: "idle",
completedAt: null,
});
});
test("parse suspended object entry", () => {
const entry = normalizeThreadIndexEntry({
head: "0123456789ABC",
suspendedRole: "worker",
suspendMessage: "Please clarify: Which API?",
});
expect(entry).toEqual({
head: "0123456789ABC",
suspendedRole: "worker",
suspendMessage: "Please clarify: Which API?",
status: "idle",
completedAt: null,
});
});
test("normalizeThreadIndexEntry preserves status and completedAt from new data", () => {
const entry = normalizeThreadIndexEntry({
head: "0123456789ABC",
suspendedRole: null,
suspendMessage: null,
status: "completed",
completedAt: 1234567890,
});
expect(entry).toEqual({
head: "0123456789ABC",
suspendedRole: null,
suspendMessage: null,
status: "completed",
completedAt: 1234567890,
});
});
test("normalizeThreadIndexEntry defaults status=idle, completedAt=null for old data", () => {
const entry = normalizeThreadIndexEntry({
head: "0123456789ABC",
suspendedRole: null,
suspendMessage: null,
});
expect(entry).toEqual({
head: "0123456789ABC",
suspendedRole: null,
suspendMessage: null,
status: "idle",
completedAt: null,
});
});
test("serialize non-suspended entry as compact string", () => {
const entry = createThreadIndexEntry("0123456789ABC");
expect(serializeThreadIndexEntry(entry)).toBe("0123456789ABC");
});
test("serialize suspended entry as object", () => {
const entry = markThreadSuspended(
createThreadIndexEntry("0123456789ABC"),
"worker",
"Please clarify: Which API?",
);
expect(serializeThreadIndexEntry(entry)).toEqual({
head: "0123456789ABC",
suspendedRole: "worker",
suspendMessage: "Please clarify: Which API?",
status: "suspended",
});
});
test("serialize completed entry as object", () => {
const entry = markThreadCompleted(
createThreadIndexEntry("0123456789ABC"),
"completed",
1234567890,
);
expect(serializeThreadIndexEntry(entry)).toEqual({
head: "0123456789ABC",
status: "completed",
completedAt: 1234567890,
});
});
test("updateThreadHead clears suspend metadata and resets status to idle", () => {
const suspended = markThreadSuspended(
createThreadIndexEntry("OLDHEAD0123456"),
"worker",
"Waiting",
);
const resumed = updateThreadHead(suspended, "NEWHEAD01234567");
expect(resumed).toEqual({
head: "NEWHEAD01234567",
suspendedRole: null,
suspendMessage: null,
status: "idle",
completedAt: null,
});
});
test("markThreadSuspended sets status to suspended", () => {
const entry = createThreadIndexEntry("0123456789ABC");
const suspended = markThreadSuspended(entry, "worker", "Waiting for input");
expect(suspended).toEqual({
head: "0123456789ABC",
suspendedRole: "worker",
suspendMessage: "Waiting for input",
status: "suspended",
completedAt: null,
});
});
test("markThreadCompleted sets status and completedAt", () => {
const entry = createThreadIndexEntry("0123456789ABC");
const completed = markThreadCompleted(entry, "completed", 1234567890);
expect(completed).toEqual({
head: "0123456789ABC",
suspendedRole: null,
suspendMessage: null,
status: "completed",
completedAt: 1234567890,
});
});
test("markThreadCompleted with cancelled status", () => {
const entry = createThreadIndexEntry("0123456789ABC");
const cancelled = markThreadCompleted(entry, "cancelled", 9876543210);
expect(cancelled).toEqual({
head: "0123456789ABC",
suspendedRole: null,
suspendMessage: null,
status: "cancelled",
completedAt: 9876543210,
});
});
test("parseThreadsIndex round-trip", () => {
const raw = {
"01THREAD0000000000000001": "HEAD00000000001",
"01THREAD0000000000000002": {
head: "HEAD00000000002",
suspendedRole: "reviewer",
suspendMessage: "Need input",
status: "suspended",
},
};
const parsed = parseThreadsIndex(raw);
expect(serializeThreadsIndex(parsed)).toEqual(raw);
});
});