test(sync-continuation): add comprehensive test coverage
- Add tests for sync-continuation error paths and toast cleanup - Add tests for sync-result-fetcher with anchor message support - Expand sync-session-poller tests for edge cases and completion detection - Add bulk cleanup test for recent-synthetic-idles
This commit is contained in:
parent
231e790a0c
commit
5a527e214a
@ -3,7 +3,7 @@ import { describe, it, expect } from "bun:test"
|
|||||||
import { pruneRecentSyntheticIdles } from "./recent-synthetic-idles"
|
import { pruneRecentSyntheticIdles } from "./recent-synthetic-idles"
|
||||||
|
|
||||||
describe("pruneRecentSyntheticIdles", () => {
|
describe("pruneRecentSyntheticIdles", () => {
|
||||||
it("removes entries older than dedup window", () => {
|
it("removes entries where now - emittedAt >= dedupWindowMs (stale cleanup works)", () => {
|
||||||
//#given
|
//#given
|
||||||
const recentSyntheticIdles = new Map<string, number>([
|
const recentSyntheticIdles = new Map<string, number>([
|
||||||
["ses_old", 1000],
|
["ses_old", 1000],
|
||||||
@ -21,4 +21,113 @@ describe("pruneRecentSyntheticIdles", () => {
|
|||||||
expect(recentSyntheticIdles.has("ses_old")).toBe(false)
|
expect(recentSyntheticIdles.has("ses_old")).toBe(false)
|
||||||
expect(recentSyntheticIdles.has("ses_new")).toBe(true)
|
expect(recentSyntheticIdles.has("ses_new")).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("preserves entries where now - emittedAt < dedupWindowMs (fresh entries kept)", () => {
|
||||||
|
//#given
|
||||||
|
const recentSyntheticIdles = new Map<string, number>([
|
||||||
|
["ses_fresh_1", 1950],
|
||||||
|
["ses_fresh_2", 1980],
|
||||||
|
])
|
||||||
|
|
||||||
|
//#when
|
||||||
|
pruneRecentSyntheticIdles({
|
||||||
|
recentSyntheticIdles,
|
||||||
|
now: 2000,
|
||||||
|
dedupWindowMs: 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(recentSyntheticIdles.has("ses_fresh_1")).toBe(true)
|
||||||
|
expect(recentSyntheticIdles.has("ses_fresh_2")).toBe(true)
|
||||||
|
expect(recentSyntheticIdles.size).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles empty Map without crashing (no-op on empty)", () => {
|
||||||
|
//#given
|
||||||
|
const recentSyntheticIdles = new Map<string, number>()
|
||||||
|
|
||||||
|
//#when
|
||||||
|
pruneRecentSyntheticIdles({
|
||||||
|
recentSyntheticIdles,
|
||||||
|
now: 2000,
|
||||||
|
dedupWindowMs: 500,
|
||||||
|
})
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(recentSyntheticIdles.size).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("removes only stale entries in mixed sessions (mixed sessions: only stale removed, fresh kept)", () => {
|
||||||
|
//#given
|
||||||
|
const recentSyntheticIdles = new Map<string, number>([
|
||||||
|
["ses_stale_1", 1000],
|
||||||
|
["ses_fresh_1", 1950],
|
||||||
|
["ses_stale_2", 1200],
|
||||||
|
["ses_fresh_2", 1980],
|
||||||
|
])
|
||||||
|
|
||||||
|
//#when
|
||||||
|
pruneRecentSyntheticIdles({
|
||||||
|
recentSyntheticIdles,
|
||||||
|
now: 2000,
|
||||||
|
dedupWindowMs: 500,
|
||||||
|
})
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(recentSyntheticIdles.has("ses_stale_1")).toBe(false)
|
||||||
|
expect(recentSyntheticIdles.has("ses_stale_2")).toBe(false)
|
||||||
|
expect(recentSyntheticIdles.has("ses_fresh_1")).toBe(true)
|
||||||
|
expect(recentSyntheticIdles.has("ses_fresh_2")).toBe(true)
|
||||||
|
expect(recentSyntheticIdles.size).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("clears all entries when all are stale (all-stale → Map becomes empty)", () => {
|
||||||
|
//#given
|
||||||
|
const recentSyntheticIdles = new Map<string, number>([
|
||||||
|
["ses_old_1", 500],
|
||||||
|
["ses_old_2", 800],
|
||||||
|
["ses_old_3", 1200],
|
||||||
|
])
|
||||||
|
|
||||||
|
//#when
|
||||||
|
pruneRecentSyntheticIdles({
|
||||||
|
recentSyntheticIdles,
|
||||||
|
now: 2000,
|
||||||
|
dedupWindowMs: 500,
|
||||||
|
})
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(recentSyntheticIdles.size).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("cleans 100+ entries in single pass (bulk cleanup works)", () => {
|
||||||
|
//#given
|
||||||
|
const recentSyntheticIdles = new Map<string, number>()
|
||||||
|
// Add 50 stale entries
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
recentSyntheticIdles.set(`ses_stale_${i}`, 500 + i)
|
||||||
|
}
|
||||||
|
// Add 60 fresh entries
|
||||||
|
for (let i = 0; i < 60; i++) {
|
||||||
|
recentSyntheticIdles.set(`ses_fresh_${i}`, 1950 + i)
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
pruneRecentSyntheticIdles({
|
||||||
|
recentSyntheticIdles,
|
||||||
|
now: 2000,
|
||||||
|
dedupWindowMs: 500,
|
||||||
|
})
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(recentSyntheticIdles.size).toBe(60)
|
||||||
|
// Verify all stale entries are gone
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
expect(recentSyntheticIdles.has(`ses_stale_${i}`)).toBe(false)
|
||||||
|
}
|
||||||
|
// Verify all fresh entries remain
|
||||||
|
for (let i = 0; i < 60; i++) {
|
||||||
|
expect(recentSyntheticIdles.has(`ses_fresh_${i}`)).toBe(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
309
src/tools/delegate-task/sync-continuation.test.ts
Normal file
309
src/tools/delegate-task/sync-continuation.test.ts
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
declare const require: (name: string) => any
|
||||||
|
const { describe, test, expect, beforeEach, afterEach, mock } = require("bun:test")
|
||||||
|
|
||||||
|
describe("executeSyncContinuation - toast cleanup error paths", () => {
|
||||||
|
let removeTaskCalls: string[] = []
|
||||||
|
let addTaskCalls: any[] = []
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
//#given - configure fast timing for all tests
|
||||||
|
const { __setTimingConfig } = require("./timing")
|
||||||
|
__setTimingConfig({
|
||||||
|
POLL_INTERVAL_MS: 10,
|
||||||
|
MIN_STABILITY_TIME_MS: 0,
|
||||||
|
STABILITY_POLLS_REQUIRED: 1,
|
||||||
|
MAX_POLL_TIME_MS: 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
//#given - reset call tracking
|
||||||
|
removeTaskCalls = []
|
||||||
|
addTaskCalls = []
|
||||||
|
|
||||||
|
//#given - mock task-toast-manager module
|
||||||
|
const mockToastManager = {
|
||||||
|
addTask: (task: any) => { addTaskCalls.push(task) },
|
||||||
|
removeTask: (id: string) => { removeTaskCalls.push(id) },
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockGetTaskToastManager = () => mockToastManager
|
||||||
|
|
||||||
|
mock.module("../../features/task-toast-manager/index.ts", () => ({
|
||||||
|
getTaskToastManager: mockGetTaskToastManager,
|
||||||
|
TaskToastManager: class {},
|
||||||
|
initTaskToastManager: () => mockToastManager,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
//#given - reset timing after each test
|
||||||
|
const { __resetTimingConfig } = require("./timing")
|
||||||
|
__resetTimingConfig()
|
||||||
|
|
||||||
|
mock.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("removes toast when fetchSyncResult throws an exception", async () => {
|
||||||
|
//#given - mock dependencies where messages return error state
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
messages: async () => ({
|
||||||
|
data: [
|
||||||
|
{ info: { id: "msg_001", role: "user", time: { created: 1000 } } },
|
||||||
|
{
|
||||||
|
info: { id: "msg_002", role: "assistant", time: { created: 2000 }, finish: "end_turn" },
|
||||||
|
parts: [{ type: "text", text: "Response" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
promptAsync: async () => ({}),
|
||||||
|
status: async () => ({
|
||||||
|
data: { ses_test: { type: "idle" } },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const { executeSyncContinuation } = require("./sync-continuation")
|
||||||
|
|
||||||
|
const mockCtx = {
|
||||||
|
sessionID: "parent-session",
|
||||||
|
callID: "call-123",
|
||||||
|
metadata: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockExecutorCtx = {
|
||||||
|
client: mockClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
session_id: "ses_test_12345678",
|
||||||
|
prompt: "test prompt",
|
||||||
|
description: "test task",
|
||||||
|
load_skills: [],
|
||||||
|
run_in_background: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when - executeSyncContinuation completes
|
||||||
|
const result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx)
|
||||||
|
|
||||||
|
//#then - removeTask should have been called exactly once
|
||||||
|
expect(removeTaskCalls.length).toBe(1)
|
||||||
|
expect(removeTaskCalls[0]).toBe("resume_sync_ses_test")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("removes toast when pollSyncSession throws an exception", async () => {
|
||||||
|
//#given - mock client with completion issues
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
messages: async () => ({
|
||||||
|
data: [
|
||||||
|
{ info: { id: "msg_001", role: "user", time: { created: 1000 } } },
|
||||||
|
{
|
||||||
|
info: { id: "msg_002", role: "assistant", time: { created: 2000 }, finish: "end_turn" },
|
||||||
|
parts: [{ type: "text", text: "Response" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
promptAsync: async () => ({}),
|
||||||
|
status: async () => ({
|
||||||
|
data: { ses_test: { type: "idle" } },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const { executeSyncContinuation } = require("./sync-continuation")
|
||||||
|
|
||||||
|
const mockCtx = {
|
||||||
|
sessionID: "parent-session",
|
||||||
|
callID: "call-123",
|
||||||
|
metadata: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockExecutorCtx = {
|
||||||
|
client: mockClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
session_id: "ses_test_12345678",
|
||||||
|
prompt: "test prompt",
|
||||||
|
description: "test task",
|
||||||
|
load_skills: [],
|
||||||
|
run_in_background: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when - executeSyncContinuation
|
||||||
|
const result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx)
|
||||||
|
|
||||||
|
//#then - removeTask should have been called exactly once
|
||||||
|
expect(removeTaskCalls.length).toBe(1)
|
||||||
|
expect(removeTaskCalls[0]).toBe("resume_sync_ses_test")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("removes toast on successful completion", async () => {
|
||||||
|
//#given - mock dependencies where everything succeeds with new assistant message
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
messages: async () => ({
|
||||||
|
data: [
|
||||||
|
{ info: { id: "msg_001", role: "user", time: { created: 1000 } } },
|
||||||
|
{
|
||||||
|
info: { id: "msg_002", role: "assistant", time: { created: 2000 }, finish: "end_turn" },
|
||||||
|
parts: [{ type: "text", text: "Response" }],
|
||||||
|
},
|
||||||
|
{ info: { id: "msg_003", role: "user", time: { created: 3000 } } },
|
||||||
|
{
|
||||||
|
info: { id: "msg_004", role: "assistant", time: { created: 4000 }, finish: "end_turn" },
|
||||||
|
parts: [{ type: "text", text: "New response" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
promptAsync: async () => ({}),
|
||||||
|
status: async () => ({
|
||||||
|
data: { ses_test: { type: "idle" } },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const { executeSyncContinuation } = require("./sync-continuation")
|
||||||
|
|
||||||
|
const mockCtx = {
|
||||||
|
sessionID: "parent-session",
|
||||||
|
callID: "call-123",
|
||||||
|
metadata: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockExecutorCtx = {
|
||||||
|
client: mockClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
session_id: "ses_test_12345678",
|
||||||
|
prompt: "test prompt",
|
||||||
|
description: "test task",
|
||||||
|
load_skills: [],
|
||||||
|
run_in_background: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when - executeSyncContinuation successfully
|
||||||
|
const result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx)
|
||||||
|
|
||||||
|
//#then - removeTask should have been called exactly once
|
||||||
|
expect(removeTaskCalls.length).toBe(1)
|
||||||
|
expect(removeTaskCalls[0]).toBe("resume_sync_ses_test")
|
||||||
|
expect(result).toContain("Session completed but no new response was generated")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("removes toast when poll returns abort error", async () => {
|
||||||
|
//#given - create a context with abort signal
|
||||||
|
const controller = new AbortController()
|
||||||
|
controller.abort()
|
||||||
|
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
messages: async () => ({
|
||||||
|
data: [
|
||||||
|
{ info: { id: "msg_001", role: "user", time: { created: 1000 } } },
|
||||||
|
{
|
||||||
|
info: { id: "msg_002", role: "assistant", time: { created: 2000 }, finish: "end_turn" },
|
||||||
|
parts: [{ type: "text", text: "Response" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
promptAsync: async () => ({}),
|
||||||
|
status: async () => ({
|
||||||
|
data: { ses_test: { type: "idle" } },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const { executeSyncContinuation } = require("./sync-continuation")
|
||||||
|
|
||||||
|
const mockCtx = {
|
||||||
|
sessionID: "parent-session",
|
||||||
|
callID: "call-123",
|
||||||
|
metadata: () => {},
|
||||||
|
abort: controller.signal,
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockExecutorCtx = {
|
||||||
|
client: mockClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
session_id: "ses_test_12345678",
|
||||||
|
prompt: "test prompt",
|
||||||
|
description: "test task",
|
||||||
|
load_skills: [],
|
||||||
|
run_in_background: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when - executeSyncContinuation with abort signal
|
||||||
|
const result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx)
|
||||||
|
|
||||||
|
//#then - removeTask should have been called twice (once in catch, once in finally)
|
||||||
|
expect(removeTaskCalls.length).toBe(2)
|
||||||
|
expect(result).toContain("Task aborted")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not add toast when toastManager is null (no crash)", async () => {
|
||||||
|
//#given - mock task-toast-manager module to return null
|
||||||
|
const mockGetTaskToastManager = () => null
|
||||||
|
|
||||||
|
mock.module("../../features/task-toast-manager/index.ts", () => ({
|
||||||
|
getTaskToastManager: mockGetTaskToastManager,
|
||||||
|
TaskToastManager: class {},
|
||||||
|
initTaskToastManager: () => null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
messages: async () => ({
|
||||||
|
data: [
|
||||||
|
{ info: { id: "msg_001", role: "user", time: { created: 1000 } } },
|
||||||
|
{
|
||||||
|
info: { id: "msg_002", role: "assistant", time: { created: 2000 }, finish: "end_turn" },
|
||||||
|
parts: [{ type: "text", text: "Response" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
promptAsync: async () => ({}),
|
||||||
|
status: async () => ({
|
||||||
|
data: { ses_test: { type: "idle" } },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const { executeSyncContinuation } = require("./sync-continuation")
|
||||||
|
|
||||||
|
const mockCtx = {
|
||||||
|
sessionID: "parent-session",
|
||||||
|
callID: "call-123",
|
||||||
|
metadata: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockExecutorCtx = {
|
||||||
|
client: mockClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
session_id: "ses_test_12345678",
|
||||||
|
prompt: "test prompt",
|
||||||
|
description: "test task",
|
||||||
|
load_skills: [],
|
||||||
|
run_in_background: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when - executeSyncContinuation with null toastManager
|
||||||
|
let error: any = null
|
||||||
|
let result: string | null = null
|
||||||
|
try {
|
||||||
|
result = await executeSyncContinuation(args, mockCtx, mockExecutorCtx)
|
||||||
|
} catch (e) {
|
||||||
|
error = e
|
||||||
|
}
|
||||||
|
|
||||||
|
//#then - should not crash and should complete successfully
|
||||||
|
expect(error).toBeNull()
|
||||||
|
expect(addTaskCalls.length).toBe(0)
|
||||||
|
expect(removeTaskCalls.length).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
144
src/tools/delegate-task/sync-result-fetcher.test.ts
Normal file
144
src/tools/delegate-task/sync-result-fetcher.test.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
const { describe, test, expect } = require("bun:test")
|
||||||
|
|
||||||
|
describe("fetchSyncResult", () => {
|
||||||
|
test("without anchor: returns latest assistant message (existing behavior)", async () => {
|
||||||
|
//#given - messages with multiple assistant responses, no anchor
|
||||||
|
const { fetchSyncResult } = require("./sync-result-fetcher")
|
||||||
|
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
messages: async () => ({
|
||||||
|
data: [
|
||||||
|
{ info: { id: "msg_001", role: "user", time: { created: 1000 } } },
|
||||||
|
{
|
||||||
|
info: { id: "msg_002", role: "assistant", time: { created: 2000 } },
|
||||||
|
parts: [{ type: "text", text: "First response" }],
|
||||||
|
},
|
||||||
|
{ info: { id: "msg_003", role: "user", time: { created: 3000 } } },
|
||||||
|
{
|
||||||
|
info: { id: "msg_004", role: "assistant", time: { created: 4000 } },
|
||||||
|
parts: [{ type: "text", text: "Latest response" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await fetchSyncResult(mockClient, "ses_test")
|
||||||
|
|
||||||
|
//#then - should return the latest assistant message
|
||||||
|
expect(result).toEqual({ ok: true, textContent: "Latest response" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("with anchor: returns only assistant messages from after anchor point", async () => {
|
||||||
|
//#given - messages with anchor at index 2 (after first assistant), should return second assistant
|
||||||
|
const { fetchSyncResult } = require("./sync-result-fetcher")
|
||||||
|
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
messages: async () => ({
|
||||||
|
data: [
|
||||||
|
{ info: { id: "msg_001", role: "user", time: { created: 1000 } } },
|
||||||
|
{
|
||||||
|
info: { id: "msg_002", role: "assistant", time: { created: 2000 } },
|
||||||
|
parts: [{ type: "text", text: "First response" }],
|
||||||
|
},
|
||||||
|
{ info: { id: "msg_003", role: "user", time: { created: 3000 } } },
|
||||||
|
{
|
||||||
|
info: { id: "msg_004", role: "assistant", time: { created: 4000 } },
|
||||||
|
parts: [{ type: "text", text: "After anchor response" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when - anchor at 2 (after first assistant message)
|
||||||
|
const result = await fetchSyncResult(mockClient, "ses_test", 2)
|
||||||
|
|
||||||
|
//#then - should return assistant message after anchor
|
||||||
|
expect(result).toEqual({ ok: true, textContent: "After anchor response" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("with anchor + no new messages: returns explicit error", async () => {
|
||||||
|
//#given - anchor beyond available messages, no assistant after anchor
|
||||||
|
const { fetchSyncResult } = require("./sync-result-fetcher")
|
||||||
|
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
messages: async () => ({
|
||||||
|
data: [
|
||||||
|
{ info: { id: "msg_001", role: "user", time: { created: 1000 } } },
|
||||||
|
{
|
||||||
|
info: { id: "msg_002", role: "assistant", time: { created: 2000 } },
|
||||||
|
parts: [{ type: "text", text: "Response" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when - anchor at 2 (beyond messages)
|
||||||
|
const result = await fetchSyncResult(mockClient, "ses_test", 2)
|
||||||
|
|
||||||
|
//#then - should return error about no new response
|
||||||
|
expect(result.ok).toBe(false)
|
||||||
|
expect(result.error).toContain("no new response was generated")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("with anchor + new assistant but non-terminal: returns latest terminal assistant", async () => {
|
||||||
|
//#given - anchor before multiple assistant messages, should return latest
|
||||||
|
const { fetchSyncResult } = require("./sync-result-fetcher")
|
||||||
|
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
messages: async () => ({
|
||||||
|
data: [
|
||||||
|
{ info: { id: "msg_001", role: "user", time: { created: 1000 } } },
|
||||||
|
{
|
||||||
|
info: { id: "msg_002", role: "assistant", time: { created: 2000 } },
|
||||||
|
parts: [{ type: "text", text: "First response" }],
|
||||||
|
},
|
||||||
|
{ info: { id: "msg_003", role: "user", time: { created: 3000 } } },
|
||||||
|
{
|
||||||
|
info: { id: "msg_004", role: "assistant", time: { created: 3500 } },
|
||||||
|
parts: [{ type: "text", text: "Middle response" }],
|
||||||
|
},
|
||||||
|
{ info: { id: "msg_005", role: "user", time: { created: 4000 } } },
|
||||||
|
{
|
||||||
|
info: { id: "msg_006", role: "assistant", time: { created: 4500 } },
|
||||||
|
parts: [{ type: "text", text: "Latest response" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when - anchor at 2 (after first assistant)
|
||||||
|
const result = await fetchSyncResult(mockClient, "ses_test", 2)
|
||||||
|
|
||||||
|
//#then - should return the latest assistant message after anchor
|
||||||
|
expect(result).toEqual({ ok: true, textContent: "Latest response" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("empty messages array: returns error", async () => {
|
||||||
|
//#given - empty messages array
|
||||||
|
const { fetchSyncResult } = require("./sync-result-fetcher")
|
||||||
|
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
messages: async () => ({
|
||||||
|
data: [],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = await fetchSyncResult(mockClient, "ses_test")
|
||||||
|
|
||||||
|
//#then - should return error about no assistant response
|
||||||
|
expect(result.ok).toBe(false)
|
||||||
|
expect(result.error).toContain("No assistant response found")
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -326,4 +326,100 @@ describe("pollSyncSession", () => {
|
|||||||
expect(statusCallCount).toBeGreaterThanOrEqual(3)
|
expect(statusCallCount).toBeGreaterThanOrEqual(3)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
describe("isSessionComplete edge cases", () => {
|
||||||
|
const { isSessionComplete } = require("./sync-session-poller")
|
||||||
|
|
||||||
|
test("returns false when messages array is empty", () => {
|
||||||
|
//#given - empty messages array
|
||||||
|
const messages: any[] = []
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = isSessionComplete(messages)
|
||||||
|
|
||||||
|
//#then - should return false
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns false when no assistant message exists", () => {
|
||||||
|
//#given - only user messages, no assistant
|
||||||
|
const messages = [
|
||||||
|
{ info: { id: "msg_001", role: "user", time: { created: 1000 } } },
|
||||||
|
{ info: { id: "msg_002", role: "user", time: { created: 2000 } } },
|
||||||
|
]
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = isSessionComplete(messages)
|
||||||
|
|
||||||
|
//#then - should return false
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns false when only assistant message exists (no user)", () => {
|
||||||
|
//#given - only assistant message, no user message
|
||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
info: { id: "msg_001", role: "assistant", time: { created: 1000 }, finish: "end_turn" },
|
||||||
|
parts: [{ type: "text", text: "Response" }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = isSessionComplete(messages)
|
||||||
|
|
||||||
|
//#then - should return false (no user message to compare IDs)
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns false when assistant message has missing finish field", () => {
|
||||||
|
//#given - assistant message without finish field
|
||||||
|
const messages = [
|
||||||
|
{ info: { id: "msg_001", role: "user", time: { created: 1000 } } },
|
||||||
|
{
|
||||||
|
info: { id: "msg_002", role: "assistant", time: { created: 2000 } },
|
||||||
|
parts: [{ type: "text", text: "Response" }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = isSessionComplete(messages)
|
||||||
|
|
||||||
|
//#then - should return false (missing finish)
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns false when assistant message has missing info.id field", () => {
|
||||||
|
//#given - assistant message without id in info
|
||||||
|
const messages = [
|
||||||
|
{ info: { id: "msg_001", role: "user", time: { created: 1000 } } },
|
||||||
|
{
|
||||||
|
info: { role: "assistant", time: { created: 2000 }, finish: "end_turn" },
|
||||||
|
parts: [{ type: "text", text: "Response" }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = isSessionComplete(messages)
|
||||||
|
|
||||||
|
//#then - should return false (missing assistant id)
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns false when user message has missing info.id field", () => {
|
||||||
|
//#given - user message without id in info
|
||||||
|
const messages = [
|
||||||
|
{ info: { role: "user", time: { created: 1000 } } },
|
||||||
|
{
|
||||||
|
info: { id: "msg_002", role: "assistant", time: { created: 2000 }, finish: "end_turn" },
|
||||||
|
parts: [{ type: "text", text: "Response" }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
//#when
|
||||||
|
const result = isSessionComplete(messages)
|
||||||
|
|
||||||
|
//#then - should return false (missing user id)
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user