@renxqoo/renx-code 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -51
- package/bin/renx.cjs +16 -0
- package/package.json +2 -45
- package/src/agent/runtime/runtime.context-usage.test.ts +4 -5
- package/src/agent/runtime/runtime.error-handling.test.ts +4 -5
- package/src/agent/runtime/runtime.test.ts +7 -4
- package/src/agent/runtime/runtime.ts +3 -9
- package/src/agent/runtime/runtime.usage-forwarding.test.ts +4 -5
- package/src/agent/runtime/source-modules.test.ts +16 -35
- package/src/agent/runtime/source-modules.ts +17 -0
- package/vendor/agent-root/src/agent/ENTERPRISE_ACCEPTANCE_CHECKLIST.md +95 -0
- package/vendor/agent-root/src/agent/ENTERPRISE_REALTIME.html +1345 -0
- package/vendor/agent-root/src/agent/ENTERPRISE_REALTIME.md +1353 -0
- package/vendor/agent-root/src/agent/ERROR_CONTRACT.md +60 -0
- package/vendor/agent-root/src/agent/TEST_COVERAGE_ANALYSIS.md +278 -0
- package/vendor/agent-root/src/agent/__test__/error-contract.test.ts +72 -0
- package/vendor/agent-root/src/agent/__test__/types.test.ts +137 -0
- package/vendor/agent-root/src/agent/agent/__test__/abort-runtime.test.ts +83 -0
- package/vendor/agent-root/src/agent/agent/__test__/callback-safety.test.ts +34 -0
- package/vendor/agent-root/src/agent/agent/__test__/compaction.test.ts +323 -0
- package/vendor/agent-root/src/agent/agent/__test__/concurrency.test.ts +290 -0
- package/vendor/agent-root/src/agent/agent/__test__/error-normalizer.test.ts +377 -0
- package/vendor/agent-root/src/agent/agent/__test__/error.test.ts +212 -0
- package/vendor/agent-root/src/agent/agent/__test__/fault-injection.test.ts +295 -0
- package/vendor/agent-root/src/agent/agent/__test__/index.test.ts +3607 -0
- package/vendor/agent-root/src/agent/agent/__test__/logger.test.ts +35 -0
- package/vendor/agent-root/src/agent/agent/__test__/message-utils.test.ts +517 -0
- package/vendor/agent-root/src/agent/agent/__test__/telemetry.test.ts +97 -0
- package/vendor/agent-root/src/agent/agent/__test__/timeout-budget.test.ts +479 -0
- package/vendor/agent-root/src/agent/agent/__test__/tool-call-merge.test.ts +80 -0
- package/vendor/agent-root/src/agent/agent/__test__/tool-execution-ledger.test.ts +76 -0
- package/vendor/agent-root/src/agent/agent/__test__/write-buffer.test.ts +173 -0
- package/vendor/agent-root/src/agent/agent/__test__/write-file-session.test.ts +109 -0
- package/vendor/agent-root/src/agent/agent/abort-runtime.ts +71 -0
- package/vendor/agent-root/src/agent/agent/callback-safety.ts +33 -0
- package/vendor/agent-root/src/agent/agent/compaction.ts +291 -0
- package/vendor/agent-root/src/agent/agent/concurrency.ts +103 -0
- package/vendor/agent-root/src/agent/agent/error-normalizer.ts +190 -0
- package/vendor/agent-root/src/agent/agent/error.ts +198 -0
- package/vendor/agent-root/src/agent/agent/index.ts +1772 -0
- package/vendor/agent-root/src/agent/agent/logger.ts +65 -0
- package/vendor/agent-root/src/agent/agent/message-utils.ts +101 -0
- package/vendor/agent-root/src/agent/agent/stream-events.ts +61 -0
- package/vendor/agent-root/src/agent/agent/telemetry.ts +123 -0
- package/vendor/agent-root/src/agent/agent/timeout-budget.ts +227 -0
- package/vendor/agent-root/src/agent/agent/tool-call-merge.ts +111 -0
- package/vendor/agent-root/src/agent/agent/tool-execution-ledger.ts +164 -0
- package/vendor/agent-root/src/agent/agent/write-buffer.ts +188 -0
- package/vendor/agent-root/src/agent/agent/write-file-session.ts +238 -0
- package/vendor/agent-root/src/agent/app/__test__/agent-app-service.test.ts +1053 -0
- package/vendor/agent-root/src/agent/app/__test__/minimal-agent-application.test.ts +158 -0
- package/vendor/agent-root/src/agent/app/__test__/sqlite-agent-app-store.test.ts +437 -0
- package/vendor/agent-root/src/agent/app/agent-app-service.ts +748 -0
- package/vendor/agent-root/src/agent/app/contracts.ts +109 -0
- package/vendor/agent-root/src/agent/app/index.ts +5 -0
- package/vendor/agent-root/src/agent/app/minimal-agent-application.ts +151 -0
- package/vendor/agent-root/src/agent/app/ports.ts +72 -0
- package/vendor/agent-root/src/agent/app/sqlite-agent-app-store.ts +1182 -0
- package/vendor/agent-root/src/agent/app/sqlite-client.ts +177 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/00-README.md +36 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/01-scope-and-goals.md +33 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/02-architecture-overview.md +40 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/03-domain-model-and-contracts.md +91 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/04-ports-and-interfaces.md +116 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/05-run-orchestration-and-state-machine.md +52 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/06-cli-commands-and-ux.md +53 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/07-storage-design-local.md +52 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/08-error-and-observability.md +40 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/09-security-and-policy-boundary.md +19 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/10-test-plan-and-acceptance.md +28 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/11-implementation-phases.md +26 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/12-open-questions-and-risks.md +30 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/13-sqlite-schema-fields-and-rationale.md +567 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/14-project-flow-mermaid.md +583 -0
- package/vendor/agent-root/src/agent/docs/cli-app-layer/15-openclaw-style-project-blueprint.md +972 -0
- package/vendor/agent-root/src/agent/error-contract.ts +154 -0
- package/vendor/agent-root/src/agent/prompts/system.ts +246 -0
- package/vendor/agent-root/src/agent/prompts/system1.ts +208 -0
- package/vendor/agent-root/src/agent/storage/__test__/file-history-store.test.ts +98 -0
- package/vendor/agent-root/src/agent/storage/file-history-store.ts +313 -0
- package/vendor/agent-root/src/agent/storage/file-storage-config.ts +94 -0
- package/vendor/agent-root/src/agent/storage/file-system.ts +31 -0
- package/vendor/agent-root/src/agent/storage/file-write-service.ts +21 -0
- package/vendor/agent-root/src/agent/tool/__test__/base-tool.test.ts +413 -0
- package/vendor/agent-root/src/agent/tool/__test__/bash-policy.test.ts +356 -0
- package/vendor/agent-root/src/agent/tool/__test__/bash.mocked-coverage.test.ts +375 -0
- package/vendor/agent-root/src/agent/tool/__test__/bash.test.ts +372 -0
- package/vendor/agent-root/src/agent/tool/__test__/error.test.ts +108 -0
- package/vendor/agent-root/src/agent/tool/__test__/file-edit-tool.test.ts +258 -0
- package/vendor/agent-root/src/agent/tool/__test__/file-history-tools.test.ts +121 -0
- package/vendor/agent-root/src/agent/tool/__test__/file-read-tool.test.ts +210 -0
- package/vendor/agent-root/src/agent/tool/__test__/glob.test.ts +139 -0
- package/vendor/agent-root/src/agent/tool/__test__/grep.mocked-coverage.test.ts +456 -0
- package/vendor/agent-root/src/agent/tool/__test__/grep.test.ts +192 -0
- package/vendor/agent-root/src/agent/tool/__test__/lsp.test.ts +300 -0
- package/vendor/agent-root/src/agent/tool/__test__/outside-workspace-confirmation.test.ts +214 -0
- package/vendor/agent-root/src/agent/tool/__test__/path-security.test.ts +336 -0
- package/vendor/agent-root/src/agent/tool/__test__/skill-loader.test.ts +494 -0
- package/vendor/agent-root/src/agent/tool/__test__/skill-parser.test.ts +543 -0
- package/vendor/agent-root/src/agent/tool/__test__/skill-tool.test.ts +172 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-concurrency-and-version.test.ts +116 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-create-get-list-update.test.ts +267 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-create.test.ts +519 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-errors.test.ts +225 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-output-blocking.test.ts +223 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-output.test.ts +184 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-parent-abort.test.ts +287 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-real-runner-adapter.test.ts +190 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-run-lifecycle.test.ts +352 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-store-runner-branches.test.ts +395 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-store.test.ts +391 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-subagent-config-integration.test.ts +176 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-subagent-config.test.ts +68 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-tools-core-edges.test.ts +630 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-tools-runtime-edges.test.ts +732 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-types.test.ts +494 -0
- package/vendor/agent-root/src/agent/tool/__test__/task-utils-branches.test.ts +175 -0
- package/vendor/agent-root/src/agent/tool/__test__/tool-manager.test.ts +505 -0
- package/vendor/agent-root/src/agent/tool/__test__/types.test.ts +55 -0
- package/vendor/agent-root/src/agent/tool/__test__/web-fetch.test.ts +244 -0
- package/vendor/agent-root/src/agent/tool/__test__/web-search.test.ts +290 -0
- package/vendor/agent-root/src/agent/tool/__test__/write-file.test.ts +368 -0
- package/vendor/agent-root/src/agent/tool/base-tool.ts +345 -0
- package/vendor/agent-root/src/agent/tool/bash-policy.ts +636 -0
- package/vendor/agent-root/src/agent/tool/bash.ts +688 -0
- package/vendor/agent-root/src/agent/tool/error.ts +131 -0
- package/vendor/agent-root/src/agent/tool/file-edit-tool.ts +264 -0
- package/vendor/agent-root/src/agent/tool/file-history-list.ts +103 -0
- package/vendor/agent-root/src/agent/tool/file-history-restore.ts +149 -0
- package/vendor/agent-root/src/agent/tool/file-read-tool.ts +211 -0
- package/vendor/agent-root/src/agent/tool/glob.ts +171 -0
- package/vendor/agent-root/src/agent/tool/grep.ts +496 -0
- package/vendor/agent-root/src/agent/tool/lsp.ts +481 -0
- package/vendor/agent-root/src/agent/tool/path-security.ts +117 -0
- package/vendor/agent-root/src/agent/tool/search/common.ts +153 -0
- package/vendor/agent-root/src/agent/tool/skill/index.ts +13 -0
- package/vendor/agent-root/src/agent/tool/skill/loader.ts +229 -0
- package/vendor/agent-root/src/agent/tool/skill/parser.ts +124 -0
- package/vendor/agent-root/src/agent/tool/skill/types.ts +27 -0
- package/vendor/agent-root/src/agent/tool/skill-tool.ts +143 -0
- package/vendor/agent-root/src/agent/tool/task-create.ts +186 -0
- package/vendor/agent-root/src/agent/tool/task-errors.ts +42 -0
- package/vendor/agent-root/src/agent/tool/task-get.ts +116 -0
- package/vendor/agent-root/src/agent/tool/task-graph.ts +78 -0
- package/vendor/agent-root/src/agent/tool/task-list.ts +141 -0
- package/vendor/agent-root/src/agent/tool/task-mock-runner-adapter.ts +232 -0
- package/vendor/agent-root/src/agent/tool/task-output.ts +223 -0
- package/vendor/agent-root/src/agent/tool/task-parent-abort.ts +115 -0
- package/vendor/agent-root/src/agent/tool/task-real-runner-adapter.ts +336 -0
- package/vendor/agent-root/src/agent/tool/task-runner-adapter.ts +55 -0
- package/vendor/agent-root/src/agent/tool/task-stop.ts +187 -0
- package/vendor/agent-root/src/agent/tool/task-store.ts +217 -0
- package/vendor/agent-root/src/agent/tool/task-subagent-config.ts +149 -0
- package/vendor/agent-root/src/agent/tool/task-types.ts +264 -0
- package/vendor/agent-root/src/agent/tool/task-update.ts +315 -0
- package/vendor/agent-root/src/agent/tool/task.ts +209 -0
- package/vendor/agent-root/src/agent/tool/tool-manager.ts +362 -0
- package/vendor/agent-root/src/agent/tool/tool-prompts.ts +242 -0
- package/vendor/agent-root/src/agent/tool/types.ts +116 -0
- package/vendor/agent-root/src/agent/tool/web-fetch.ts +227 -0
- package/vendor/agent-root/src/agent/tool/web-search.ts +208 -0
- package/vendor/agent-root/src/agent/tool/write-file.ts +497 -0
- package/vendor/agent-root/src/agent/types.ts +232 -0
- package/vendor/agent-root/src/agent/utils/__tests__/index.test.ts +18 -0
- package/vendor/agent-root/src/agent/utils/__tests__/message-utils.test.ts +610 -0
- package/vendor/agent-root/src/agent/utils/__tests__/message.test.ts +223 -0
- package/vendor/agent-root/src/agent/utils/__tests__/token.test.ts +42 -0
- package/vendor/agent-root/src/agent/utils/index.ts +16 -0
- package/vendor/agent-root/src/agent/utils/message.ts +171 -0
- package/vendor/agent-root/src/agent/utils/token.ts +28 -0
- package/vendor/agent-root/src/config/__tests__/load-config-to-env.test.ts +129 -0
- package/vendor/agent-root/src/config/__tests__/loader.test.ts +247 -0
- package/vendor/agent-root/src/config/__tests__/runtime.test.ts +88 -0
- package/vendor/agent-root/src/config/index.ts +54 -0
- package/vendor/agent-root/src/config/loader.ts +431 -0
- package/vendor/agent-root/src/config/paths.ts +30 -0
- package/vendor/agent-root/src/config/runtime.ts +163 -0
- package/vendor/agent-root/src/config/types.ts +70 -0
- package/vendor/agent-root/src/logger/index.ts +57 -0
- package/vendor/agent-root/src/logger/logger.ts +819 -0
- package/vendor/agent-root/src/logger/types.ts +150 -0
- package/vendor/agent-root/src/providers/__tests__/errors.test.ts +441 -0
- package/vendor/agent-root/src/providers/__tests__/index.test.ts +16 -0
- package/vendor/agent-root/src/providers/__tests__/openai-compatible.options.test.ts +318 -0
- package/vendor/agent-root/src/providers/__tests__/openai-compatible.test.ts +600 -0
- package/vendor/agent-root/src/providers/__tests__/registry.test.ts +449 -0
- package/vendor/agent-root/src/providers/__tests__/responses-adapter.test.ts +298 -0
- package/vendor/agent-root/src/providers/adapters/__tests__/anthropic.test.ts +354 -0
- package/vendor/agent-root/src/providers/adapters/__tests__/kimi.test.ts +58 -0
- package/vendor/agent-root/src/providers/adapters/__tests__/standard.test.ts +261 -0
- package/vendor/agent-root/src/providers/adapters/anthropic.ts +572 -0
- package/vendor/agent-root/src/providers/adapters/base.ts +131 -0
- package/vendor/agent-root/src/providers/adapters/kimi.ts +48 -0
- package/vendor/agent-root/src/providers/adapters/responses.ts +732 -0
- package/vendor/agent-root/src/providers/adapters/standard.ts +120 -0
- package/vendor/agent-root/src/providers/http/__tests__/client.timeout.test.ts +313 -0
- package/vendor/agent-root/src/providers/http/client.ts +289 -0
- package/vendor/agent-root/src/providers/http/stream-parser.ts +109 -0
- package/vendor/agent-root/src/providers/index.ts +76 -0
- package/vendor/agent-root/src/providers/kimi-headers.ts +177 -0
- package/vendor/agent-root/src/providers/openai-compatible.ts +387 -0
- package/vendor/agent-root/src/providers/registry/model-config.ts +230 -0
- package/vendor/agent-root/src/providers/registry/provider-factory.ts +123 -0
- package/vendor/agent-root/src/providers/registry.ts +135 -0
- package/vendor/agent-root/src/providers/types/api.ts +284 -0
- package/vendor/agent-root/src/providers/types/config.ts +58 -0
- package/vendor/agent-root/src/providers/types/errors.ts +323 -0
- package/vendor/agent-root/src/providers/types/index.ts +72 -0
- package/vendor/agent-root/src/providers/types/provider.ts +45 -0
- package/vendor/agent-root/src/providers/types/registry.ts +88 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { LLMProvider } from '../../../providers';
|
|
3
|
+
import type { Message } from '../../types';
|
|
4
|
+
|
|
5
|
+
const encodeMock = vi.hoisted(() =>
|
|
6
|
+
vi.fn((text: string) => Array.from(text).map((_ch, index) => index))
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
vi.mock('js-tiktoken', () => ({
|
|
10
|
+
getEncoding: vi.fn(() => ({
|
|
11
|
+
encode: encodeMock,
|
|
12
|
+
})),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
import { compact, estimateMessagesTokens, estimateTokens } from '../compaction';
|
|
16
|
+
|
|
17
|
+
function createProvider(
|
|
18
|
+
overrides?: Partial<{
|
|
19
|
+
generate: LLMProvider['generate'];
|
|
20
|
+
getTimeTimeout: LLMProvider['getTimeTimeout'];
|
|
21
|
+
model: string;
|
|
22
|
+
}>
|
|
23
|
+
): LLMProvider {
|
|
24
|
+
return {
|
|
25
|
+
config: { model: overrides?.model ?? 'mock-model' } as Record<string, unknown>,
|
|
26
|
+
generate:
|
|
27
|
+
overrides?.generate ||
|
|
28
|
+
(vi.fn().mockResolvedValue({
|
|
29
|
+
choices: [{ message: { content: 'mock summary' } }],
|
|
30
|
+
}) as unknown as LLMProvider['generate']),
|
|
31
|
+
generateStream: vi.fn() as unknown as LLMProvider['generateStream'],
|
|
32
|
+
getTimeTimeout: overrides?.getTimeTimeout || vi.fn(() => 50),
|
|
33
|
+
getLLMMaxTokens: vi.fn(() => 1000),
|
|
34
|
+
getMaxOutputTokens: vi.fn(() => 100),
|
|
35
|
+
} as unknown as LLMProvider;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createMessage(partial: Partial<Message>): Message {
|
|
39
|
+
return {
|
|
40
|
+
messageId: partial.messageId || crypto.randomUUID(),
|
|
41
|
+
type: partial.type || 'assistant-text',
|
|
42
|
+
role: partial.role || 'assistant',
|
|
43
|
+
content: partial.content || '',
|
|
44
|
+
timestamp: partial.timestamp ?? Date.now(),
|
|
45
|
+
...partial,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('renx compaction', () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
vi.clearAllMocks();
|
|
52
|
+
encodeMock.mockImplementation((text: string) => Array.from(text).map((_ch, index) => index));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('estimateTokens uses tiktoken encoder output length', () => {
|
|
56
|
+
expect(estimateTokens('abc')).toBe(3);
|
|
57
|
+
expect(estimateTokens('')).toBe(0);
|
|
58
|
+
expect(encodeMock).toHaveBeenCalledWith('abc');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('estimateTokens falls back to heuristic when encoder throws', () => {
|
|
62
|
+
encodeMock.mockImplementationOnce(() => {
|
|
63
|
+
throw new Error('encode failed');
|
|
64
|
+
});
|
|
65
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
66
|
+
|
|
67
|
+
expect(estimateTokens('中文ab')).toBe(5);
|
|
68
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
69
|
+
warnSpy.mockRestore();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('estimateMessagesTokens applies different costs for low/high image detail', () => {
|
|
73
|
+
const lowImageMessage = createMessage({
|
|
74
|
+
role: 'user',
|
|
75
|
+
type: 'user',
|
|
76
|
+
content: [{ type: 'image_url', image_url: { url: 'u', detail: 'low' } }],
|
|
77
|
+
});
|
|
78
|
+
const highImageMessage = createMessage({
|
|
79
|
+
role: 'user',
|
|
80
|
+
type: 'user',
|
|
81
|
+
content: [{ type: 'image_url', image_url: { url: 'u', detail: 'high' } }],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const low = estimateMessagesTokens([lowImageMessage]);
|
|
85
|
+
const high = estimateMessagesTokens([highImageMessage]);
|
|
86
|
+
|
|
87
|
+
expect(high - low).toBe(680);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('estimateMessagesTokens includes tool_calls, tool_call_id and tools schema overhead', () => {
|
|
91
|
+
const base = createMessage({
|
|
92
|
+
role: 'assistant',
|
|
93
|
+
type: 'assistant-text',
|
|
94
|
+
content: 'hello',
|
|
95
|
+
});
|
|
96
|
+
const withToolMetadata = createMessage({
|
|
97
|
+
role: 'assistant',
|
|
98
|
+
type: 'tool-call',
|
|
99
|
+
content: 'hello',
|
|
100
|
+
tool_calls: [
|
|
101
|
+
{
|
|
102
|
+
id: 'call_1',
|
|
103
|
+
type: 'function',
|
|
104
|
+
function: { name: 'bash', arguments: '{"cmd":"echo"}' },
|
|
105
|
+
},
|
|
106
|
+
] as Message['tool_calls'],
|
|
107
|
+
tool_call_id: 'call_1',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const withoutExtra = estimateMessagesTokens([base]);
|
|
111
|
+
const withExtra = estimateMessagesTokens([withToolMetadata], [
|
|
112
|
+
{
|
|
113
|
+
type: 'function',
|
|
114
|
+
function: { name: 'bash', description: 'run shell', parameters: { type: 'object' } },
|
|
115
|
+
},
|
|
116
|
+
] as never);
|
|
117
|
+
|
|
118
|
+
expect(withExtra).toBeGreaterThan(withoutExtra);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('estimateMessagesTokens counts name field and array text parts', () => {
|
|
122
|
+
const msg = createMessage({
|
|
123
|
+
role: 'assistant',
|
|
124
|
+
type: 'assistant-text',
|
|
125
|
+
content: [{ type: 'text', text: 'chunk-text' }],
|
|
126
|
+
}) as Message & { name?: string };
|
|
127
|
+
msg.name = 'assistant_name';
|
|
128
|
+
|
|
129
|
+
const baseline = createMessage({
|
|
130
|
+
role: 'assistant',
|
|
131
|
+
type: 'assistant-text',
|
|
132
|
+
content: '',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const tokensWithNameAndParts = estimateMessagesTokens([msg]);
|
|
136
|
+
const baselineTokens = estimateMessagesTokens([baseline]);
|
|
137
|
+
expect(tokensWithNameAndParts).toBeGreaterThan(baselineTokens);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('compact builds summary and returns removed message ids', async () => {
|
|
141
|
+
const generateMock = vi.fn().mockResolvedValue({
|
|
142
|
+
choices: [{ message: { content: 'Summary text' } }],
|
|
143
|
+
});
|
|
144
|
+
const provider = createProvider({
|
|
145
|
+
generate: generateMock as unknown as LLMProvider['generate'],
|
|
146
|
+
});
|
|
147
|
+
const logger = { info: vi.fn(), warn: vi.fn() };
|
|
148
|
+
const messages: Message[] = [
|
|
149
|
+
createMessage({
|
|
150
|
+
messageId: 's1',
|
|
151
|
+
type: 'system',
|
|
152
|
+
role: 'system',
|
|
153
|
+
content: 'sys',
|
|
154
|
+
timestamp: 1,
|
|
155
|
+
}),
|
|
156
|
+
createMessage({
|
|
157
|
+
messageId: 'u1',
|
|
158
|
+
type: 'user',
|
|
159
|
+
role: 'user',
|
|
160
|
+
content: 'old question',
|
|
161
|
+
timestamp: 2,
|
|
162
|
+
}),
|
|
163
|
+
createMessage({
|
|
164
|
+
messageId: 'a1',
|
|
165
|
+
type: 'assistant-text',
|
|
166
|
+
role: 'assistant',
|
|
167
|
+
content: 'old answer',
|
|
168
|
+
timestamp: 3,
|
|
169
|
+
}),
|
|
170
|
+
createMessage({
|
|
171
|
+
messageId: 'u2',
|
|
172
|
+
type: 'user',
|
|
173
|
+
role: 'user',
|
|
174
|
+
content: 'latest question',
|
|
175
|
+
timestamp: 4,
|
|
176
|
+
}),
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
const result = await compact(messages, { provider, keepMessagesNum: 1, logger });
|
|
180
|
+
|
|
181
|
+
expect(result.summaryMessage).toMatchObject({
|
|
182
|
+
role: 'assistant',
|
|
183
|
+
type: 'summary',
|
|
184
|
+
});
|
|
185
|
+
expect(String(result.summaryMessage?.content)).toContain('Summary text');
|
|
186
|
+
expect(result.removedMessageIds.sort()).toEqual(['a1', 'u1']);
|
|
187
|
+
expect(result.messages.map((m) => m.messageId)).toEqual([
|
|
188
|
+
's1',
|
|
189
|
+
result.summaryMessage!.messageId,
|
|
190
|
+
'u2',
|
|
191
|
+
]);
|
|
192
|
+
const secondArg = (generateMock as unknown as { mock: { calls: unknown[][] } }).mock
|
|
193
|
+
.calls[0]?.[1] as {
|
|
194
|
+
model?: string;
|
|
195
|
+
abortSignal?: AbortSignal;
|
|
196
|
+
};
|
|
197
|
+
expect(secondArg.model).toBe('mock-model');
|
|
198
|
+
expect(secondArg.abortSignal).toBeDefined();
|
|
199
|
+
expect(
|
|
200
|
+
(logger.info as unknown as { mock: { calls: unknown[][] } }).mock.calls.length
|
|
201
|
+
).toBeGreaterThan(0);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('compact handles invalid summary response and returns null summary', async () => {
|
|
205
|
+
const provider = createProvider({
|
|
206
|
+
generate: vi.fn().mockResolvedValue(null) as unknown as LLMProvider['generate'],
|
|
207
|
+
getTimeTimeout: vi.fn(() => 0),
|
|
208
|
+
});
|
|
209
|
+
const logger = { info: vi.fn(), warn: vi.fn() };
|
|
210
|
+
const messages = [
|
|
211
|
+
createMessage({ messageId: 's1', type: 'system', role: 'system', content: 'sys' }),
|
|
212
|
+
createMessage({ messageId: 'u1', type: 'user', role: 'user', content: 'u1' }),
|
|
213
|
+
createMessage({ messageId: 'u2', type: 'user', role: 'user', content: 'u2' }),
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
const result = await compact(messages, {
|
|
217
|
+
provider,
|
|
218
|
+
keepMessagesNum: 1,
|
|
219
|
+
logger,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(result.summaryMessage).toBeNull();
|
|
223
|
+
expect(logger.warn).toHaveBeenCalled();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('compact skips llm summary generation when pending is empty', async () => {
|
|
227
|
+
const generateMock = vi.fn();
|
|
228
|
+
const provider = createProvider({
|
|
229
|
+
generate: generateMock as unknown as LLMProvider['generate'],
|
|
230
|
+
});
|
|
231
|
+
const messages = [
|
|
232
|
+
createMessage({ messageId: 's1', type: 'system', role: 'system', content: 'sys' }),
|
|
233
|
+
createMessage({ messageId: 'u1', type: 'user', role: 'user', content: 'u1' }),
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
const result = await compact(messages, { provider, keepMessagesNum: 10 });
|
|
237
|
+
|
|
238
|
+
expect(result.summaryMessage).toBeNull();
|
|
239
|
+
expect(generateMock).not.toHaveBeenCalled();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('compact handles provider.generate throw and logs warning', async () => {
|
|
243
|
+
const provider = createProvider({
|
|
244
|
+
generate: vi
|
|
245
|
+
.fn()
|
|
246
|
+
.mockRejectedValue(new Error('network failed')) as unknown as LLMProvider['generate'],
|
|
247
|
+
});
|
|
248
|
+
const logger = { info: vi.fn(), warn: vi.fn() };
|
|
249
|
+
const messages = [
|
|
250
|
+
createMessage({ messageId: 's1', type: 'system', role: 'system', content: 'sys' }),
|
|
251
|
+
createMessage({ messageId: 'a1', type: 'assistant-text', role: 'assistant', content: 'a1' }),
|
|
252
|
+
createMessage({ messageId: 'u1', type: 'user', role: 'user', content: 'u1' }),
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
const result = await compact(messages, {
|
|
256
|
+
provider,
|
|
257
|
+
keepMessagesNum: 1,
|
|
258
|
+
logger,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
expect(result.summaryMessage).toBeNull();
|
|
262
|
+
expect(logger.warn).toHaveBeenCalled();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('compact passes previous summary block and handles empty choices response', async () => {
|
|
266
|
+
const generateMock = vi.fn().mockResolvedValue({ choices: [] });
|
|
267
|
+
const provider = createProvider({
|
|
268
|
+
generate: generateMock as unknown as LLMProvider['generate'],
|
|
269
|
+
model: ' ',
|
|
270
|
+
getTimeTimeout: vi.fn(() => 0),
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const messages = [
|
|
274
|
+
createMessage({ messageId: 's1', type: 'system', role: 'system', content: 'sys' }),
|
|
275
|
+
createMessage({
|
|
276
|
+
messageId: 'sum_1',
|
|
277
|
+
type: 'summary',
|
|
278
|
+
role: 'assistant',
|
|
279
|
+
content: '[Conversation Summary]\nold summary',
|
|
280
|
+
}),
|
|
281
|
+
createMessage({ messageId: 'u2', type: 'user', role: 'user', content: 'latest' }),
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
const result = await compact(messages, { provider, keepMessagesNum: 1 });
|
|
285
|
+
expect(result.summaryMessage).toBeNull();
|
|
286
|
+
|
|
287
|
+
const callArgs = (generateMock as unknown as { mock: { calls: unknown[][] } }).mock.calls[0];
|
|
288
|
+
const requestMessages = callArgs?.[0] as Array<{ role: string; content: string }>;
|
|
289
|
+
const options = callArgs?.[1] as { model?: string; abortSignal?: AbortSignal };
|
|
290
|
+
expect(requestMessages[1]?.content).toContain('<previous_summary>');
|
|
291
|
+
expect(options.model).toBeUndefined();
|
|
292
|
+
expect(options.abortSignal).toBeUndefined();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('compact ignores AbortSignal.timeout failure and still requests summary', async () => {
|
|
296
|
+
const timeoutSpy = vi.spyOn(AbortSignal, 'timeout').mockImplementation(() => {
|
|
297
|
+
throw new Error('timeout unsupported');
|
|
298
|
+
});
|
|
299
|
+
const generateMock = vi.fn().mockResolvedValue({
|
|
300
|
+
choices: [{ message: { content: 'summary ok' } }],
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const provider = createProvider({
|
|
304
|
+
generate: generateMock as unknown as LLMProvider['generate'],
|
|
305
|
+
getTimeTimeout: vi.fn(() => 10),
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const messages = [
|
|
309
|
+
createMessage({ messageId: 's1', type: 'system', role: 'system', content: 'sys' }),
|
|
310
|
+
createMessage({ messageId: 'u1', type: 'user', role: 'user', content: 'old q' }),
|
|
311
|
+
createMessage({ messageId: 'u2', type: 'user', role: 'user', content: 'latest q' }),
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
const result = await compact(messages, { provider, keepMessagesNum: 1 });
|
|
315
|
+
expect(result.summaryMessage?.content).toContain('summary ok');
|
|
316
|
+
const options = (generateMock as unknown as { mock: { calls: unknown[][] } }).mock
|
|
317
|
+
.calls[0]?.[1] as {
|
|
318
|
+
abortSignal?: AbortSignal;
|
|
319
|
+
};
|
|
320
|
+
expect(options.abortSignal).toBeUndefined();
|
|
321
|
+
timeoutSpy.mockRestore();
|
|
322
|
+
});
|
|
323
|
+
});
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { buildExecutionWaves, runWithConcurrencyAndLock } from '../concurrency';
|
|
3
|
+
import type { ToolCall } from '../../../providers';
|
|
4
|
+
import type { ToolConcurrencyPolicy } from '../../tool/types';
|
|
5
|
+
|
|
6
|
+
function createToolCall(name: string, id: string = `call_${name}`): ToolCall {
|
|
7
|
+
return {
|
|
8
|
+
id,
|
|
9
|
+
type: 'function',
|
|
10
|
+
index: 0,
|
|
11
|
+
function: {
|
|
12
|
+
name,
|
|
13
|
+
arguments: '{}',
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createPlan(
|
|
19
|
+
toolCall: ToolCall,
|
|
20
|
+
mode: ToolConcurrencyPolicy['mode'] = 'exclusive',
|
|
21
|
+
lockKey?: string
|
|
22
|
+
): { toolCall: ToolCall; policy: ToolConcurrencyPolicy } {
|
|
23
|
+
return {
|
|
24
|
+
toolCall,
|
|
25
|
+
policy: { mode, lockKey },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('buildExecutionWaves', () => {
|
|
30
|
+
it('returns empty array for empty input', () => {
|
|
31
|
+
expect(buildExecutionWaves([])).toEqual([]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('creates exclusive wave for single exclusive plan', () => {
|
|
35
|
+
const plan = createPlan(createToolCall('bash'));
|
|
36
|
+
const waves = buildExecutionWaves([plan]);
|
|
37
|
+
|
|
38
|
+
expect(waves).toHaveLength(1);
|
|
39
|
+
expect(waves[0].type).toBe('exclusive');
|
|
40
|
+
expect(waves[0].plans).toHaveLength(1);
|
|
41
|
+
expect(waves[0].plans[0]).toBe(plan);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('creates parallel wave for single parallel-safe plan', () => {
|
|
45
|
+
const plan = createPlan(createToolCall('file_read'), 'parallel-safe');
|
|
46
|
+
const waves = buildExecutionWaves([plan]);
|
|
47
|
+
|
|
48
|
+
expect(waves).toHaveLength(1);
|
|
49
|
+
expect(waves[0].type).toBe('parallel');
|
|
50
|
+
expect(waves[0].plans).toHaveLength(1);
|
|
51
|
+
expect(waves[0].plans[0]).toBe(plan);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('groups consecutive parallel-safe plans into single wave', () => {
|
|
55
|
+
const plans = [
|
|
56
|
+
createPlan(createToolCall('file_read'), 'parallel-safe'),
|
|
57
|
+
createPlan(createToolCall('glob'), 'parallel-safe'),
|
|
58
|
+
createPlan(createToolCall('grep'), 'parallel-safe'),
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const waves = buildExecutionWaves(plans);
|
|
62
|
+
|
|
63
|
+
expect(waves).toHaveLength(1);
|
|
64
|
+
expect(waves[0].type).toBe('parallel');
|
|
65
|
+
expect(waves[0].plans).toHaveLength(3);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('creates separate waves for exclusive plans', () => {
|
|
69
|
+
const plans = [
|
|
70
|
+
createPlan(createToolCall('bash'), 'exclusive'),
|
|
71
|
+
createPlan(createToolCall('write_file'), 'exclusive'),
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const waves = buildExecutionWaves(plans);
|
|
75
|
+
|
|
76
|
+
expect(waves).toHaveLength(2);
|
|
77
|
+
expect(waves[0].type).toBe('exclusive');
|
|
78
|
+
expect(waves[1].type).toBe('exclusive');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('creates mixed waves correctly', () => {
|
|
82
|
+
const plans = [
|
|
83
|
+
createPlan(createToolCall('file_read'), 'parallel-safe'),
|
|
84
|
+
createPlan(createToolCall('bash'), 'exclusive'),
|
|
85
|
+
createPlan(createToolCall('glob'), 'parallel-safe'),
|
|
86
|
+
createPlan(createToolCall('grep'), 'parallel-safe'),
|
|
87
|
+
createPlan(createToolCall('write_file'), 'exclusive'),
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
const waves = buildExecutionWaves(plans);
|
|
91
|
+
|
|
92
|
+
expect(waves).toHaveLength(4);
|
|
93
|
+
expect(waves[0].type).toBe('parallel');
|
|
94
|
+
expect(waves[0].plans).toHaveLength(1);
|
|
95
|
+
expect(waves[1].type).toBe('exclusive');
|
|
96
|
+
expect(waves[1].plans).toHaveLength(1);
|
|
97
|
+
expect(waves[2].type).toBe('parallel');
|
|
98
|
+
expect(waves[2].plans).toHaveLength(2);
|
|
99
|
+
expect(waves[3].type).toBe('exclusive');
|
|
100
|
+
expect(waves[3].plans).toHaveLength(1);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('handles parallel-safe with lock keys', () => {
|
|
104
|
+
const plans = [
|
|
105
|
+
createPlan(createToolCall('file_read'), 'parallel-safe', 'file:read'),
|
|
106
|
+
createPlan(createToolCall('file_read'), 'parallel-safe', 'file:read'),
|
|
107
|
+
createPlan(createToolCall('file_read'), 'parallel-safe', 'file:write'),
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const waves = buildExecutionWaves(plans);
|
|
111
|
+
|
|
112
|
+
expect(waves).toHaveLength(1);
|
|
113
|
+
expect(waves[0].type).toBe('parallel');
|
|
114
|
+
expect(waves[0].plans).toHaveLength(3);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('runWithConcurrencyAndLock', () => {
|
|
119
|
+
it('returns empty array for empty tasks', async () => {
|
|
120
|
+
const result = await runWithConcurrencyAndLock([], 5);
|
|
121
|
+
expect(result).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('runs single task successfully', async () => {
|
|
125
|
+
const task = { run: vi.fn().mockResolvedValue('result') };
|
|
126
|
+
const result = await runWithConcurrencyAndLock([task], 5);
|
|
127
|
+
|
|
128
|
+
expect(result).toEqual(['result']);
|
|
129
|
+
expect(task.run).toHaveBeenCalledOnce();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('runs multiple tasks concurrently within limit', async () => {
|
|
133
|
+
const executionOrder: string[] = [];
|
|
134
|
+
const tasks = [
|
|
135
|
+
{
|
|
136
|
+
run: vi.fn().mockImplementation(async () => {
|
|
137
|
+
executionOrder.push('task1-start');
|
|
138
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
139
|
+
executionOrder.push('task1-end');
|
|
140
|
+
return 'result1';
|
|
141
|
+
}),
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
run: vi.fn().mockImplementation(async () => {
|
|
145
|
+
executionOrder.push('task2-start');
|
|
146
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
147
|
+
executionOrder.push('task2-end');
|
|
148
|
+
return 'result2';
|
|
149
|
+
}),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
run: vi.fn().mockImplementation(async () => {
|
|
153
|
+
executionOrder.push('task3-start');
|
|
154
|
+
await new Promise((resolve) => setTimeout(resolve, 15));
|
|
155
|
+
executionOrder.push('task3-end');
|
|
156
|
+
return 'result3';
|
|
157
|
+
}),
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const result = await runWithConcurrencyAndLock(tasks, 2);
|
|
162
|
+
|
|
163
|
+
expect(result).toEqual(['result1', 'result2', 'result3']);
|
|
164
|
+
expect(executionOrder).toContain('task1-start');
|
|
165
|
+
expect(executionOrder).toContain('task2-start');
|
|
166
|
+
expect(executionOrder).toContain('task3-start');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('respects lock keys', async () => {
|
|
170
|
+
const executionOrder: string[] = [];
|
|
171
|
+
const tasks = [
|
|
172
|
+
{
|
|
173
|
+
lockKey: 'resource:1',
|
|
174
|
+
run: vi.fn().mockImplementation(async () => {
|
|
175
|
+
executionOrder.push('task1-start');
|
|
176
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
177
|
+
executionOrder.push('task1-end');
|
|
178
|
+
return 'result1';
|
|
179
|
+
}),
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
lockKey: 'resource:1',
|
|
183
|
+
run: vi.fn().mockImplementation(async () => {
|
|
184
|
+
executionOrder.push('task2-start');
|
|
185
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
186
|
+
executionOrder.push('task2-end');
|
|
187
|
+
return 'result2';
|
|
188
|
+
}),
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
lockKey: 'resource:2',
|
|
192
|
+
run: vi.fn().mockImplementation(async () => {
|
|
193
|
+
executionOrder.push('task3-start');
|
|
194
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
195
|
+
executionOrder.push('task3-end');
|
|
196
|
+
return 'result3';
|
|
197
|
+
}),
|
|
198
|
+
},
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
const result = await runWithConcurrencyAndLock(tasks, 3);
|
|
202
|
+
|
|
203
|
+
expect(result).toEqual(['result1', 'result2', 'result3']);
|
|
204
|
+
// Tasks with same lock key should not run concurrently
|
|
205
|
+
const task1End = executionOrder.indexOf('task1-end');
|
|
206
|
+
const task2Start = executionOrder.indexOf('task2-start');
|
|
207
|
+
|
|
208
|
+
// Task2 should start after task1 ends (same lock key)
|
|
209
|
+
expect(task2Start).toBeGreaterThan(task1End);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('handles task failure', async () => {
|
|
213
|
+
const error = new Error('Task failed');
|
|
214
|
+
const tasks = [
|
|
215
|
+
{ run: vi.fn().mockResolvedValue('success') },
|
|
216
|
+
{ run: vi.fn().mockRejectedValue(error) },
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
await expect(runWithConcurrencyAndLock(tasks, 2)).rejects.toThrow('Task failed');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('handles concurrent limit of 1', async () => {
|
|
223
|
+
const executionOrder: string[] = [];
|
|
224
|
+
const tasks = [
|
|
225
|
+
{
|
|
226
|
+
run: vi.fn().mockImplementation(async () => {
|
|
227
|
+
executionOrder.push('task1');
|
|
228
|
+
return 'result1';
|
|
229
|
+
}),
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
run: vi.fn().mockImplementation(async () => {
|
|
233
|
+
executionOrder.push('task2');
|
|
234
|
+
return 'result2';
|
|
235
|
+
}),
|
|
236
|
+
},
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
const result = await runWithConcurrencyAndLock(tasks, 1);
|
|
240
|
+
|
|
241
|
+
expect(result).toEqual(['result1', 'result2']);
|
|
242
|
+
expect(executionOrder).toEqual(['task1', 'task2']);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('handles tasks without lock keys', async () => {
|
|
246
|
+
const tasks = [
|
|
247
|
+
{ run: vi.fn().mockResolvedValue('result1') },
|
|
248
|
+
{ run: vi.fn().mockResolvedValue('result2') },
|
|
249
|
+
{ run: vi.fn().mockResolvedValue('result3') },
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
const result = await runWithConcurrencyAndLock(tasks, 2);
|
|
253
|
+
|
|
254
|
+
expect(result).toEqual(['result1', 'result2', 'result3']);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('handles mixed lock keys and no lock keys', async () => {
|
|
258
|
+
const executionOrder: string[] = [];
|
|
259
|
+
const tasks = [
|
|
260
|
+
{
|
|
261
|
+
lockKey: 'resource:1',
|
|
262
|
+
run: vi.fn().mockImplementation(async () => {
|
|
263
|
+
executionOrder.push('task1');
|
|
264
|
+
return 'result1';
|
|
265
|
+
}),
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
run: vi.fn().mockImplementation(async () => {
|
|
269
|
+
executionOrder.push('task2');
|
|
270
|
+
return 'result2';
|
|
271
|
+
}),
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
lockKey: 'resource:1',
|
|
275
|
+
run: vi.fn().mockImplementation(async () => {
|
|
276
|
+
executionOrder.push('task3');
|
|
277
|
+
return 'result3';
|
|
278
|
+
}),
|
|
279
|
+
},
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
const result = await runWithConcurrencyAndLock(tasks, 3);
|
|
283
|
+
|
|
284
|
+
expect(result).toEqual(['result1', 'result2', 'result3']);
|
|
285
|
+
// Task3 should wait for task1 to complete (same lock key)
|
|
286
|
+
const task1Index = executionOrder.indexOf('task1');
|
|
287
|
+
const task3Index = executionOrder.indexOf('task3');
|
|
288
|
+
expect(task3Index).toBeGreaterThan(task1Index);
|
|
289
|
+
});
|
|
290
|
+
});
|