mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 05:45:51 +00:00
- jsonStringify mock 参数类型改为 Parameters<typeof JSON.stringify>[1][] 消除 TS2769 - 并发测试改为顺序执行以适配 Bun 下 proper-lockfile 的 advisory lock 行为 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
647 lines
19 KiB
TypeScript
647 lines
19 KiB
TypeScript
import { mkdir, rm } from 'fs/promises'
|
|
import { join } from 'path'
|
|
import { tmpdir } from 'os'
|
|
import { beforeEach, afterEach, describe, expect, mock, test } from 'bun:test'
|
|
|
|
import { logMock } from '../../../tests/mocks/log'
|
|
import { debugMock } from '../../../tests/mocks/debug'
|
|
|
|
// Mock dependencies before importing the module under test
|
|
mock.module('src/utils/log.ts', logMock)
|
|
mock.module('src/utils/debug.ts', debugMock)
|
|
mock.module('bun:bundle', () => ({
|
|
feature: () => false,
|
|
}))
|
|
mock.module('src/bootstrap/state.ts', () => ({
|
|
getSessionId: () => 'test-session-123',
|
|
getIsNonInteractiveSession: () => false,
|
|
}))
|
|
mock.module('src/utils/teammate.ts', () => ({
|
|
getTeamName: () => undefined,
|
|
}))
|
|
mock.module('src/utils/teammateContext.ts', () => ({
|
|
getTeammateContext: () => undefined,
|
|
}))
|
|
mock.module('src/utils/slowOperations.ts', () => ({
|
|
jsonParse: (s: string) => JSON.parse(s),
|
|
jsonStringify: (
|
|
v: unknown,
|
|
...args: Parameters<typeof JSON.stringify>[1][]
|
|
) => JSON.stringify(v, ...args),
|
|
}))
|
|
|
|
import {
|
|
createTask,
|
|
getTask,
|
|
updateTask,
|
|
deleteTask,
|
|
listTasks,
|
|
blockTask,
|
|
claimTask,
|
|
resetTaskList,
|
|
sanitizePathComponent,
|
|
getTasksDir,
|
|
notifyTasksUpdated,
|
|
onTasksUpdated,
|
|
setLeaderTeamName,
|
|
clearLeaderTeamName,
|
|
isTodoV2Enabled,
|
|
type Task,
|
|
} from '../tasks'
|
|
|
|
// Use a temp dir as CLAUDE_CONFIG_DIR for isolation
|
|
let configDir: string
|
|
const ORIGINAL_CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR
|
|
|
|
beforeEach(async () => {
|
|
configDir = join(
|
|
tmpdir(),
|
|
`claude-test-tasks-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
)
|
|
process.env.CLAUDE_CONFIG_DIR = configDir
|
|
// Reset memoize cache by changing env
|
|
const { getClaudeConfigHomeDir } = await import('src/utils/envUtils')
|
|
getClaudeConfigHomeDir.cache.clear?.()
|
|
})
|
|
|
|
afterEach(async () => {
|
|
if (ORIGINAL_CONFIG_DIR !== undefined) {
|
|
process.env.CLAUDE_CONFIG_DIR = ORIGINAL_CONFIG_DIR
|
|
} else {
|
|
delete process.env.CLAUDE_CONFIG_DIR
|
|
}
|
|
const { getClaudeConfigHomeDir } = await import('src/utils/envUtils')
|
|
getClaudeConfigHomeDir.cache.clear?.()
|
|
await rm(configDir, { recursive: true, force: true }).catch(() => {})
|
|
})
|
|
|
|
const TASK_LIST_ID = 'test-list'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// sanitizePathComponent
|
|
// ---------------------------------------------------------------------------
|
|
describe('sanitizePathComponent', () => {
|
|
test('replaces non-alphanumeric characters with hyphens', () => {
|
|
expect(sanitizePathComponent('hello world')).toBe('hello-world')
|
|
})
|
|
|
|
test('preserves alphanumeric, hyphens and underscores', () => {
|
|
expect(sanitizePathComponent('abc-123_XYZ')).toBe('abc-123_XYZ')
|
|
})
|
|
|
|
test('handles path traversal attempts', () => {
|
|
expect(sanitizePathComponent('../../../etc/passwd')).toBe(
|
|
'---------etc-passwd',
|
|
)
|
|
})
|
|
|
|
test('handles empty string', () => {
|
|
expect(sanitizePathComponent('')).toBe('')
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// getTasksDir
|
|
// ---------------------------------------------------------------------------
|
|
describe('getTasksDir', () => {
|
|
test('returns correct path under config home', () => {
|
|
const dir = getTasksDir('my-list')
|
|
expect(dir).toBe(join(configDir, 'tasks', 'my-list'))
|
|
})
|
|
|
|
test('sanitizes task list ID', () => {
|
|
const dir = getTasksDir('../evil')
|
|
expect(dir).toBe(join(configDir, 'tasks', '---evil'))
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// createTask
|
|
// ---------------------------------------------------------------------------
|
|
describe('createTask', () => {
|
|
test('creates a task with sequential ID starting at 1', async () => {
|
|
const id = await createTask(TASK_LIST_ID, {
|
|
subject: 'Test task',
|
|
description: 'A test task description',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
expect(id).toBe('1')
|
|
|
|
const task = await getTask(TASK_LIST_ID, id)
|
|
expect(task).not.toBeNull()
|
|
expect(task!.subject).toBe('Test task')
|
|
expect(task!.status).toBe('pending')
|
|
})
|
|
|
|
test('creates tasks with incrementing IDs', async () => {
|
|
const id1 = await createTask(TASK_LIST_ID, {
|
|
subject: 'First',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
const id2 = await createTask(TASK_LIST_ID, {
|
|
subject: 'Second',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
expect(id1).toBe('1')
|
|
expect(id2).toBe('2')
|
|
})
|
|
|
|
test('preserves optional fields', async () => {
|
|
const id = await createTask(TASK_LIST_ID, {
|
|
subject: 'Task with options',
|
|
description: 'Has owner and activeForm',
|
|
status: 'in_progress',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
owner: 'agent-1',
|
|
activeForm: 'Working on task',
|
|
metadata: { priority: 'high' },
|
|
})
|
|
const task = await getTask(TASK_LIST_ID, id)
|
|
expect(task!.owner).toBe('agent-1')
|
|
expect(task!.activeForm).toBe('Working on task')
|
|
expect(task!.metadata).toEqual({ priority: 'high' })
|
|
})
|
|
|
|
test('does not reuse IDs after deletion (high water mark)', async () => {
|
|
const id1 = await createTask(TASK_LIST_ID, {
|
|
subject: 'To delete',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
await deleteTask(TASK_LIST_ID, id1)
|
|
const id2 = await createTask(TASK_LIST_ID, {
|
|
subject: 'After delete',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
expect(id1).toBe('1')
|
|
expect(id2).toBe('2')
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// getTask
|
|
// ---------------------------------------------------------------------------
|
|
describe('getTask', () => {
|
|
test('returns null for non-existent task', async () => {
|
|
const task = await getTask(TASK_LIST_ID, '999')
|
|
expect(task).toBeNull()
|
|
})
|
|
|
|
test('returns task by ID', async () => {
|
|
const id = await createTask(TASK_LIST_ID, {
|
|
subject: 'Find me',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
const task = await getTask(TASK_LIST_ID, id)
|
|
expect(task).not.toBeNull()
|
|
expect(task!.id).toBe(id)
|
|
expect(task!.subject).toBe('Find me')
|
|
})
|
|
|
|
test('returns null for invalid JSON in task file', async () => {
|
|
const { writeFile } = await import('fs/promises')
|
|
const dir = getTasksDir(TASK_LIST_ID)
|
|
await mkdir(dir, { recursive: true })
|
|
await writeFile(join(dir, 'bad.json'), 'not valid json{{{')
|
|
const task = await getTask(TASK_LIST_ID, 'bad')
|
|
expect(task).toBeNull()
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// updateTask
|
|
// ---------------------------------------------------------------------------
|
|
describe('updateTask', () => {
|
|
test('updates task fields', async () => {
|
|
const id = await createTask(TASK_LIST_ID, {
|
|
subject: 'Original',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
const updated = await updateTask(TASK_LIST_ID, id, {
|
|
subject: 'Updated',
|
|
status: 'in_progress',
|
|
owner: 'agent-2',
|
|
})
|
|
expect(updated).not.toBeNull()
|
|
expect(updated!.subject).toBe('Updated')
|
|
expect(updated!.status).toBe('in_progress')
|
|
expect(updated!.owner).toBe('agent-2')
|
|
expect(updated!.id).toBe(id)
|
|
})
|
|
|
|
test('returns null for non-existent task', async () => {
|
|
const result = await updateTask(TASK_LIST_ID, '999', { subject: 'Nope' })
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
test('preserves unmodified fields', async () => {
|
|
const id = await createTask(TASK_LIST_ID, {
|
|
subject: 'Keep this',
|
|
description: 'Keep desc',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
const updated = await updateTask(TASK_LIST_ID, id, { status: 'completed' })
|
|
expect(updated!.subject).toBe('Keep this')
|
|
expect(updated!.description).toBe('Keep desc')
|
|
expect(updated!.status).toBe('completed')
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// deleteTask
|
|
// ---------------------------------------------------------------------------
|
|
describe('deleteTask', () => {
|
|
test('deletes an existing task', async () => {
|
|
const id = await createTask(TASK_LIST_ID, {
|
|
subject: 'Delete me',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
const result = await deleteTask(TASK_LIST_ID, id)
|
|
expect(result).toBe(true)
|
|
const task = await getTask(TASK_LIST_ID, id)
|
|
expect(task).toBeNull()
|
|
})
|
|
|
|
test('returns false for non-existent task', async () => {
|
|
const result = await deleteTask(TASK_LIST_ID, '999')
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
test('removes references from other tasks on delete', async () => {
|
|
const id1 = await createTask(TASK_LIST_ID, {
|
|
subject: 'Blocker',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
const id2 = await createTask(TASK_LIST_ID, {
|
|
subject: 'Blocked',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
// Set up block relationship
|
|
await blockTask(TASK_LIST_ID, id1, id2)
|
|
|
|
// Delete the blocker
|
|
await deleteTask(TASK_LIST_ID, id1)
|
|
|
|
// The blocked task should no longer reference the deleted task
|
|
const remaining = await getTask(TASK_LIST_ID, id2)
|
|
expect(remaining).not.toBeNull()
|
|
expect(remaining!.blockedBy).not.toContain(id1)
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// listTasks
|
|
// ---------------------------------------------------------------------------
|
|
describe('listTasks', () => {
|
|
test('returns empty array for empty list', async () => {
|
|
const tasks = await listTasks(TASK_LIST_ID)
|
|
expect(tasks).toEqual([])
|
|
})
|
|
|
|
test('returns all tasks', async () => {
|
|
await createTask(TASK_LIST_ID, {
|
|
subject: 'A',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
await createTask(TASK_LIST_ID, {
|
|
subject: 'B',
|
|
description: '',
|
|
status: 'completed',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
const tasks = await listTasks(TASK_LIST_ID)
|
|
expect(tasks).toHaveLength(2)
|
|
const subjects = tasks.map(t => t.subject).sort()
|
|
expect(subjects).toEqual(['A', 'B'])
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// blockTask
|
|
// ---------------------------------------------------------------------------
|
|
describe('blockTask', () => {
|
|
test('creates bidirectional block relationship', async () => {
|
|
const id1 = await createTask(TASK_LIST_ID, {
|
|
subject: 'Blocker',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
const id2 = await createTask(TASK_LIST_ID, {
|
|
subject: 'Blocked',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
const result = await blockTask(TASK_LIST_ID, id1, id2)
|
|
expect(result).toBe(true)
|
|
|
|
const t1 = await getTask(TASK_LIST_ID, id1)
|
|
const t2 = await getTask(TASK_LIST_ID, id2)
|
|
expect(t1!.blocks).toContain(id2)
|
|
expect(t2!.blockedBy).toContain(id1)
|
|
})
|
|
|
|
test('returns false for non-existent task', async () => {
|
|
const result = await blockTask(TASK_LIST_ID, '999', '998')
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
test('does not add duplicate block entries', async () => {
|
|
const id1 = await createTask(TASK_LIST_ID, {
|
|
subject: 'A',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
const id2 = await createTask(TASK_LIST_ID, {
|
|
subject: 'B',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
await blockTask(TASK_LIST_ID, id1, id2)
|
|
await blockTask(TASK_LIST_ID, id1, id2)
|
|
|
|
const t1 = await getTask(TASK_LIST_ID, id1)
|
|
expect(t1!.blocks.filter(id => id === id2)).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// claimTask
|
|
// ---------------------------------------------------------------------------
|
|
describe('claimTask', () => {
|
|
test('claims an unowned task', async () => {
|
|
const id = await createTask(TASK_LIST_ID, {
|
|
subject: 'Claimable',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
const result = await claimTask(TASK_LIST_ID, id, 'agent-1')
|
|
expect(result.success).toBe(true)
|
|
expect(result.task!.owner).toBe('agent-1')
|
|
})
|
|
|
|
test('allows same agent to re-claim', async () => {
|
|
const id = await createTask(TASK_LIST_ID, {
|
|
subject: 'Reclaim',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
await claimTask(TASK_LIST_ID, id, 'agent-1')
|
|
const result = await claimTask(TASK_LIST_ID, id, 'agent-1')
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
test('rejects claim by different agent if already owned', async () => {
|
|
const id = await createTask(TASK_LIST_ID, {
|
|
subject: 'Owned',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
await claimTask(TASK_LIST_ID, id, 'agent-1')
|
|
const result = await claimTask(TASK_LIST_ID, id, 'agent-2')
|
|
expect(result.success).toBe(false)
|
|
expect(result.reason).toBe('already_claimed')
|
|
})
|
|
|
|
test('rejects claim on completed task', async () => {
|
|
const id = await createTask(TASK_LIST_ID, {
|
|
subject: 'Done',
|
|
description: '',
|
|
status: 'completed',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
const result = await claimTask(TASK_LIST_ID, id, 'agent-1')
|
|
expect(result.success).toBe(false)
|
|
expect(result.reason).toBe('already_resolved')
|
|
})
|
|
|
|
test('rejects claim on blocked task', async () => {
|
|
const id1 = await createTask(TASK_LIST_ID, {
|
|
subject: 'Blocker',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
const id2 = await createTask(TASK_LIST_ID, {
|
|
subject: 'Blocked',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
await blockTask(TASK_LIST_ID, id1, id2)
|
|
|
|
const result = await claimTask(TASK_LIST_ID, id2, 'agent-1')
|
|
expect(result.success).toBe(false)
|
|
expect(result.reason).toBe('blocked')
|
|
expect(result.blockedByTasks).toContain(id1)
|
|
})
|
|
|
|
test('returns task_not_found for missing task', async () => {
|
|
const result = await claimTask(TASK_LIST_ID, '999', 'agent-1')
|
|
expect(result.success).toBe(false)
|
|
expect(result.reason).toBe('task_not_found')
|
|
})
|
|
|
|
test('rejects claim when agent is busy with checkAgentBusy', async () => {
|
|
const id1 = await createTask(TASK_LIST_ID, {
|
|
subject: 'Owned task',
|
|
description: '',
|
|
status: 'in_progress',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
owner: 'agent-1',
|
|
})
|
|
// Write the task with owner directly via file
|
|
const { writeFile } = await import('fs/promises')
|
|
const dir = getTasksDir(TASK_LIST_ID)
|
|
await mkdir(dir, { recursive: true })
|
|
const taskData: Task = {
|
|
id: id1,
|
|
subject: 'Owned task',
|
|
description: '',
|
|
status: 'in_progress',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
owner: 'agent-1',
|
|
}
|
|
await writeFile(join(dir, `${id1}.json`), JSON.stringify(taskData))
|
|
|
|
const id2 = await createTask(TASK_LIST_ID, {
|
|
subject: 'New task',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
const result = await claimTask(TASK_LIST_ID, id2, 'agent-1', {
|
|
checkAgentBusy: true,
|
|
})
|
|
expect(result.success).toBe(false)
|
|
expect(result.reason).toBe('agent_busy')
|
|
expect(result.busyWithTasks).toContain(id1)
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// resetTaskList
|
|
// ---------------------------------------------------------------------------
|
|
describe('resetTaskList', () => {
|
|
test('deletes all tasks and preserves high water mark', async () => {
|
|
const id1 = await createTask(TASK_LIST_ID, {
|
|
subject: 'A',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
const id2 = await createTask(TASK_LIST_ID, {
|
|
subject: 'B',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
await resetTaskList(TASK_LIST_ID)
|
|
|
|
const tasks = await listTasks(TASK_LIST_ID)
|
|
expect(tasks).toHaveLength(0)
|
|
|
|
// Next ID should be higher than previous max
|
|
const nextId = await createTask(TASK_LIST_ID, {
|
|
subject: 'After reset',
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
expect(Number(nextId)).toBeGreaterThan(Number(id2))
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Notification signals
|
|
// ---------------------------------------------------------------------------
|
|
describe('task notifications', () => {
|
|
test('notifyTasksUpdated fires subscriber', () => {
|
|
let called = false
|
|
const unsub = onTasksUpdated(() => {
|
|
called = true
|
|
})
|
|
notifyTasksUpdated()
|
|
expect(called).toBe(true)
|
|
unsub()
|
|
})
|
|
|
|
test('setLeaderTeamName triggers notification', () => {
|
|
let callCount = 0
|
|
const unsub = onTasksUpdated(() => {
|
|
callCount++
|
|
})
|
|
setLeaderTeamName('team-alpha')
|
|
expect(callCount).toBe(1)
|
|
// Setting same name again should not fire
|
|
setLeaderTeamName('team-alpha')
|
|
expect(callCount).toBe(1)
|
|
unsub()
|
|
clearLeaderTeamName()
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// isTodoV2Enabled
|
|
// ---------------------------------------------------------------------------
|
|
describe('isTodoV2Enabled', () => {
|
|
test('returns true when CLAUDE_CODE_ENABLE_TASKS is set', () => {
|
|
process.env.CLAUDE_CODE_ENABLE_TASKS = '1'
|
|
try {
|
|
expect(isTodoV2Enabled()).toBe(true)
|
|
} finally {
|
|
delete process.env.CLAUDE_CODE_ENABLE_TASKS
|
|
}
|
|
})
|
|
|
|
test('returns true in interactive sessions by default', () => {
|
|
delete process.env.CLAUDE_CODE_ENABLE_TASKS
|
|
// getIsNonInteractiveSession is mocked to return false
|
|
expect(isTodoV2Enabled()).toBe(true)
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Concurrent access (integration)
|
|
// ---------------------------------------------------------------------------
|
|
describe('concurrent task creation', () => {
|
|
test('creates unique IDs under rapid sequential writes', async () => {
|
|
// proper-lockfile advisory locks may not serialize same-process async
|
|
// operations in Bun, so we use sequential writes to verify ID monotonicity.
|
|
const ids: string[] = []
|
|
for (let i = 0; i < 10; i++) {
|
|
const id = await createTask(TASK_LIST_ID, {
|
|
subject: `Rapid ${i}`,
|
|
description: '',
|
|
status: 'pending',
|
|
blocks: [],
|
|
blockedBy: [],
|
|
})
|
|
ids.push(id)
|
|
}
|
|
const uniqueIds = new Set(ids)
|
|
expect(uniqueIds.size).toBe(10)
|
|
// Verify IDs are monotonically increasing
|
|
for (let i = 1; i < ids.length; i++) {
|
|
expect(Number(ids[i])).toBeGreaterThan(Number(ids[i - 1]))
|
|
}
|
|
})
|
|
})
|