@jsonstudio/rcc 0.89.935 → 0.89.1083
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 +1 -42
- package/dist/build-info.js +2 -2
- package/dist/build-info.js.map +1 -1
- package/dist/cli.js +120 -16
- package/dist/cli.js.map +1 -1
- package/dist/commands/quota-daemon.d.ts +2 -0
- package/dist/commands/quota-daemon.js +89 -0
- package/dist/commands/quota-daemon.js.map +1 -0
- package/dist/commands/token-daemon.js +1 -1
- package/dist/commands/token-daemon.js.map +1 -1
- package/dist/docs/daemon-admin-ui.html +958 -0
- package/dist/index.js +54 -4
- package/dist/index.js.map +1 -1
- package/dist/manager/modules/quota/index.d.ts +34 -0
- package/dist/manager/modules/quota/index.js +291 -0
- package/dist/manager/modules/quota/index.js.map +1 -1
- package/dist/manager/modules/token/index.js +14 -3
- package/dist/manager/modules/token/index.js.map +1 -1
- package/dist/manager/quota/provider-quota-center.d.ts +48 -0
- package/dist/manager/quota/provider-quota-center.js +239 -0
- package/dist/manager/quota/provider-quota-center.js.map +1 -0
- package/dist/manager/quota/provider-quota-store.d.ts +17 -0
- package/dist/manager/quota/provider-quota-store.js +88 -0
- package/dist/manager/quota/provider-quota-store.js.map +1 -0
- package/dist/providers/auth/token-scanner/index.js +11 -3
- package/dist/providers/auth/token-scanner/index.js.map +1 -1
- package/dist/providers/core/runtime/http-request-executor.js +24 -7
- package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
- package/dist/providers/core/runtime/http-transport-provider.js +11 -3
- package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
- package/dist/providers/core/runtime/responses-provider.js +9 -3
- package/dist/providers/core/runtime/responses-provider.js.map +1 -1
- package/dist/providers/core/utils/http-client.d.ts +1 -0
- package/dist/providers/core/utils/http-client.js +139 -4
- package/dist/providers/core/utils/http-client.js.map +1 -1
- package/dist/providers/core/utils/snapshot-writer.d.ts +12 -0
- package/dist/providers/core/utils/snapshot-writer.js +99 -18
- package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
- package/dist/providers/mock/mock-provider-runtime.d.ts +3 -0
- package/dist/providers/mock/mock-provider-runtime.js +176 -4
- package/dist/providers/mock/mock-provider-runtime.js.map +1 -1
- package/dist/server/handlers/chat-handler.js +13 -1
- package/dist/server/handlers/chat-handler.js.map +1 -1
- package/dist/server/handlers/handler-utils.js +5 -0
- package/dist/server/handlers/handler-utils.js.map +1 -1
- package/dist/server/handlers/messages-handler.js +13 -1
- package/dist/server/handlers/messages-handler.js.map +1 -1
- package/dist/server/handlers/responses-handler.js +73 -1
- package/dist/server/handlers/responses-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +174 -2
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +519 -0
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
- package/dist/server/runtime/http-server/executor-response.js +6 -0
- package/dist/server/runtime/http-server/executor-response.js.map +1 -1
- package/dist/server/runtime/http-server/index.d.ts +5 -0
- package/dist/server/runtime/http-server/index.js +205 -4
- package/dist/server/runtime/http-server/index.js.map +1 -1
- package/dist/server/runtime/http-server/middleware.d.ts +2 -0
- package/dist/server/runtime/http-server/middleware.js +63 -0
- package/dist/server/runtime/http-server/middleware.js.map +1 -1
- package/dist/server/runtime/http-server/request-executor.d.ts +2 -0
- package/dist/server/runtime/http-server/request-executor.js +57 -10
- package/dist/server/runtime/http-server/request-executor.js.map +1 -1
- package/dist/server/runtime/http-server/routes.js +38 -1
- package/dist/server/runtime/http-server/routes.js.map +1 -1
- package/dist/server/runtime/http-server/stats-manager.d.ts +55 -0
- package/dist/server/runtime/http-server/stats-manager.js +462 -4
- package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
- package/dist/server/runtime/http-server/types.d.ts +1 -0
- package/dist/token-daemon/token-daemon.d.ts +3 -1
- package/dist/token-daemon/token-daemon.js +130 -8
- package/dist/token-daemon/token-daemon.js.map +1 -1
- package/dist/token-daemon/token-utils.d.ts +1 -0
- package/dist/token-daemon/token-utils.js +9 -1
- package/dist/token-daemon/token-utils.js.map +1 -1
- package/dist/tools/semantic-replay.js +29 -0
- package/dist/tools/semantic-replay.js.map +1 -1
- package/dist/utils/snapshot-writer.d.ts +2 -0
- package/dist/utils/snapshot-writer.js +47 -4
- package/dist/utils/snapshot-writer.js.map +1 -1
- package/package.json +2 -3
- package/scripts/analyze-apply-patch-exec-failures.mjs +153 -0
- package/scripts/analyze-apply-patch-samples.mjs +242 -0
- package/scripts/analyze-codex-error-failures.mjs +24 -14
- package/scripts/classify-codex-samples.mjs +0 -35
- package/scripts/copy-modules-config.mjs +17 -1
- package/scripts/generate-snapshot-data.mjs +41 -11
- package/scripts/mock-provider/extract.mjs +254 -21
- package/scripts/mock-provider/run-regressions.mjs +97 -16
- package/scripts/quota-dryrun.mjs +124 -0
- package/scripts/tests/apply-patch-loop.mjs +5 -1
- package/scripts/tests/exec-command-loop.mjs +16 -19
- package/scripts/verify-apply-patch.mjs +335 -5
- package/scripts/verify-e2e-toolcall.mjs +49 -10
- package/scripts/toon-suite.mjs +0 -141
|
@@ -8,6 +8,7 @@ import { Readable } from 'node:stream';
|
|
|
8
8
|
import http from 'node:http';
|
|
9
9
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
10
10
|
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import chalk from 'chalk';
|
|
11
12
|
import { createTempConfig, startServer, stopServer } from '../lib/routecodex-runner.mjs';
|
|
12
13
|
import { GeminiSemanticMapper } from '../../sharedmodule/llmswitch-core/dist/conversion/hub/semantic-mappers/gemini-mapper.js';
|
|
13
14
|
|
|
@@ -30,6 +31,8 @@ const STAGE_SUFFIX = '_req_outbound_stage2_format_build.json';
|
|
|
30
31
|
const STAGE1_SUFFIX = '_req_outbound_stage1_semantic_map.json';
|
|
31
32
|
const MOCK_PROVIDER_ID = 'mock.apply_patch.toolloop';
|
|
32
33
|
|
|
34
|
+
const chalkError = typeof chalk?.redBright === 'function' ? chalk.redBright : (value) => value;
|
|
35
|
+
|
|
33
36
|
function listProcessesOnPort(port) {
|
|
34
37
|
try {
|
|
35
38
|
const res = spawnSync('lsof', ['-ti', `tcp:${port}`], { encoding: 'utf-8' });
|
|
@@ -702,6 +705,7 @@ async function main() {
|
|
|
702
705
|
}
|
|
703
706
|
|
|
704
707
|
main().catch((error) => {
|
|
705
|
-
|
|
708
|
+
const msg = error instanceof Error ? (error.stack || error.message) : String(error ?? '');
|
|
709
|
+
console.error(chalkError(`[tool-loop] FAILED: ${msg}`));
|
|
706
710
|
process.exit(1);
|
|
707
711
|
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* exec_command
|
|
3
|
+
* exec_command JSON 形态回环验证(模拟 Responses 客户端)。
|
|
4
4
|
*
|
|
5
5
|
* 目标:
|
|
6
|
-
* - 构造一条带 exec_command
|
|
7
|
-
* - 通过 llmswitch-core 的 response
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
6
|
+
* - 构造一条带 exec_command JSON arguments 的 chat 响应;
|
|
7
|
+
* - 通过 llmswitch-core 的 response 工具过滤管线做统一治理;
|
|
8
|
+
* - 校验最终 JSON 形状(必须包含 cmd,且不暴露 toon);
|
|
9
|
+
* - 再通过 Responses 映射验证 /v1/responses 视图同样保持 JSON 语义。
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import path from 'node:path';
|
|
@@ -25,9 +25,9 @@ async function main() {
|
|
|
25
25
|
'conversion/responses/responses-openai-bridge'
|
|
26
26
|
);
|
|
27
27
|
|
|
28
|
-
// 构造一条模拟的 chat 响应,其中 exec_command
|
|
28
|
+
// 构造一条模拟的 chat 响应,其中 exec_command 直接使用 JSON 编码参数。
|
|
29
29
|
const chatPayload = {
|
|
30
|
-
id: '
|
|
30
|
+
id: 'chatcmpl_exec_args',
|
|
31
31
|
object: 'chat.completion',
|
|
32
32
|
created: Math.floor(Date.now() / 1000),
|
|
33
33
|
model: 'gpt-5.2-codex',
|
|
@@ -39,18 +39,15 @@ async function main() {
|
|
|
39
39
|
content: null,
|
|
40
40
|
tool_calls: [
|
|
41
41
|
{
|
|
42
|
-
id: '
|
|
42
|
+
id: 'call_exec_args',
|
|
43
43
|
type: 'function',
|
|
44
44
|
function: {
|
|
45
45
|
name: 'exec_command',
|
|
46
46
|
arguments: JSON.stringify({
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
'shell: /bin/bash',
|
|
52
|
-
'login: false'
|
|
53
|
-
].join('\n')
|
|
47
|
+
cmd: 'echo 1',
|
|
48
|
+
workdir: '.',
|
|
49
|
+
yield_time_ms: 500,
|
|
50
|
+
max_output_tokens: 128
|
|
54
51
|
})
|
|
55
52
|
}
|
|
56
53
|
}
|
|
@@ -61,10 +58,10 @@ async function main() {
|
|
|
61
58
|
]
|
|
62
59
|
};
|
|
63
60
|
|
|
64
|
-
// 通过 response
|
|
61
|
+
// 通过 response 工具管线运行,触发统一的工具参数治理/归一化。
|
|
65
62
|
const filtered = await runChatResponseToolFilters(chatPayload, {
|
|
66
63
|
entryEndpoint: '/v1/chat/completions',
|
|
67
|
-
requestId: '
|
|
64
|
+
requestId: 'req_exec_args',
|
|
68
65
|
profile: 'openai-chat'
|
|
69
66
|
});
|
|
70
67
|
|
|
@@ -116,7 +113,7 @@ async function main() {
|
|
|
116
113
|
// 延伸验证:基于 chat 结果构建 Responses payload,确保 /v1/responses 视图中的
|
|
117
114
|
// function_call.arguments 同样保持 exec_command JSON 语义,而不会重新出现 toon。
|
|
118
115
|
const responsesPayload = buildResponsesPayloadFromChat(filtered, {
|
|
119
|
-
requestId: '
|
|
116
|
+
requestId: 'verify_exec_command_args'
|
|
120
117
|
});
|
|
121
118
|
const outputItems = Array.isArray(responsesPayload?.output) ? responsesPayload.output : [];
|
|
122
119
|
const fnCall = outputItems.find(
|
|
@@ -153,7 +150,7 @@ async function main() {
|
|
|
153
150
|
console.log(
|
|
154
151
|
`[exec-command-loop] decoded cmd="${args.cmd}" yield_time_ms=${args.yield_time_ms ?? 'n/a'} max_output_tokens=${args.max_output_tokens ?? 'n/a'}`
|
|
155
152
|
);
|
|
156
|
-
console.log('✅ exec_command
|
|
153
|
+
console.log('✅ exec_command arguments normalization passed (chat + responses views are JSON-only)');
|
|
157
154
|
}
|
|
158
155
|
|
|
159
156
|
main().catch((error) => {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
10
|
+
import chalk from 'chalk';
|
|
10
11
|
|
|
11
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const repoRoot = path.resolve(__dirname, '..');
|
|
@@ -14,6 +15,8 @@ const coreLoaderPath = path.join(repoRoot, 'dist', 'modules', 'llmswitch', 'core
|
|
|
14
15
|
const coreLoaderUrl = pathToFileURL(coreLoaderPath).href;
|
|
15
16
|
const { importCoreModule } = await import(coreLoaderUrl);
|
|
16
17
|
|
|
18
|
+
const chalkError = typeof chalk?.redBright === 'function' ? chalk.redBright : (value) => value;
|
|
19
|
+
|
|
17
20
|
async function loadCoreModule(subpath) {
|
|
18
21
|
return importCoreModule(subpath);
|
|
19
22
|
}
|
|
@@ -99,6 +102,47 @@ async function runApplyPatchTextCase(label, payloadText) {
|
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
async function runApplyPatchArgsCase(label, argsString) {
|
|
106
|
+
const { validateToolCall } = await loadCoreModule('tools/tool-registry');
|
|
107
|
+
const validation = validateToolCall('apply_patch', argsString);
|
|
108
|
+
if (!validation?.ok) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`[verify-apply-patch] ${label}: validateToolCall failed with reason=${validation?.reason}`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
let parsed;
|
|
114
|
+
try {
|
|
115
|
+
parsed = JSON.parse(validation.normalizedArgs || '{}');
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`[verify-apply-patch] ${label}: normalized arguments not valid JSON: ${(error && error.message) || String(error)}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
const normalizedPatchText =
|
|
122
|
+
typeof parsed?.patch === 'string'
|
|
123
|
+
? parsed.patch
|
|
124
|
+
: typeof parsed?.input === 'string'
|
|
125
|
+
? parsed.input
|
|
126
|
+
: '';
|
|
127
|
+
if (!normalizedPatchText) {
|
|
128
|
+
throw new Error(`[verify-apply-patch] ${label}: missing normalized patch text`);
|
|
129
|
+
}
|
|
130
|
+
if (!normalizedPatchText.startsWith('*** Begin Patch')) {
|
|
131
|
+
throw new Error(`[verify-apply-patch] ${label}: patch does not start with *** Begin Patch`);
|
|
132
|
+
}
|
|
133
|
+
if (!normalizedPatchText.includes('\n*** End Patch')) {
|
|
134
|
+
throw new Error(`[verify-apply-patch] ${label}: patch missing *** End Patch`);
|
|
135
|
+
}
|
|
136
|
+
if (
|
|
137
|
+
normalizedPatchText.includes('*** End Patch","input":"*** Begin Patch') ||
|
|
138
|
+
normalizedPatchText.includes('*** End Patch","patch":"*** Begin Patch') ||
|
|
139
|
+
normalizedPatchText.includes('*** End Patch\\",\\"input\\":\\"*** Begin Patch') ||
|
|
140
|
+
normalizedPatchText.includes('*** End Patch\\",\\"patch\\":\\"*** Begin Patch')
|
|
141
|
+
) {
|
|
142
|
+
throw new Error(`[verify-apply-patch] ${label}: patch still contains stitched JSON keys`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
102
146
|
async function main() {
|
|
103
147
|
if (String(process.env.ROUTECODEX_VERIFY_SKIP || '').trim() === '1') {
|
|
104
148
|
console.log('[verify-apply-patch] 跳过(ROUTECODEX_VERIFY_SKIP=1)');
|
|
@@ -106,6 +150,294 @@ async function main() {
|
|
|
106
150
|
}
|
|
107
151
|
|
|
108
152
|
try {
|
|
153
|
+
const { validateToolCall } = await loadCoreModule('tools/tool-registry');
|
|
154
|
+
const escapeNewlines = (value) => String(value || '').replace(/\n/g, '\\n');
|
|
155
|
+
|
|
156
|
+
// Regression: tolerate newline-escaped patch text (e.g. "*** Begin Patch\\n...") and
|
|
157
|
+
// normalize into a real multi-line unified diff string.
|
|
158
|
+
{
|
|
159
|
+
const escapedPatch = '*** Begin Patch\\n*** End Patch';
|
|
160
|
+
const validation = validateToolCall('apply_patch', escapedPatch);
|
|
161
|
+
if (!validation?.ok) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`[verify-apply-patch] escaped_patch: validateToolCall failed with reason=${validation?.reason}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
let parsed;
|
|
167
|
+
try {
|
|
168
|
+
parsed = JSON.parse(validation.normalizedArgs);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`[verify-apply-patch] escaped_patch: normalized arguments not valid JSON: ${(error && error.message) || String(error)}`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const patchText =
|
|
175
|
+
typeof parsed?.patch === 'string'
|
|
176
|
+
? parsed.patch
|
|
177
|
+
: typeof parsed?.input === 'string'
|
|
178
|
+
? parsed.input
|
|
179
|
+
: '';
|
|
180
|
+
if (!patchText || typeof patchText !== 'string') {
|
|
181
|
+
throw new Error('[verify-apply-patch] escaped_patch: missing patch text in normalized args');
|
|
182
|
+
}
|
|
183
|
+
if (patchText.includes('\\n') && !patchText.includes('\n')) {
|
|
184
|
+
throw new Error('[verify-apply-patch] escaped_patch: patch still contains literal \\\\n without real newlines');
|
|
185
|
+
}
|
|
186
|
+
if (patchText.split('\n')[0] !== '*** Begin Patch') {
|
|
187
|
+
throw new Error('[verify-apply-patch] escaped_patch: patch first line is not *** Begin Patch');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Regression: validateToolCall should be idempotent for already-normalized JSON arguments.
|
|
192
|
+
// Previously we could mis-detect the whole JSON as patch text, producing a merged line like:
|
|
193
|
+
// "*** End Patch\",\"input\":\"*** Begin Patch"
|
|
194
|
+
{
|
|
195
|
+
const patchText = '*** Begin Patch\n*** End Patch';
|
|
196
|
+
const alreadyNormalized = JSON.stringify({ patch: patchText, input: patchText });
|
|
197
|
+
const validation = validateToolCall('apply_patch', alreadyNormalized);
|
|
198
|
+
if (!validation?.ok) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`[verify-apply-patch] already_normalized: validateToolCall failed with reason=${validation?.reason}`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
const parsed = JSON.parse(validation.normalizedArgs);
|
|
204
|
+
const normalizedPatchText =
|
|
205
|
+
typeof parsed?.patch === 'string'
|
|
206
|
+
? parsed.patch
|
|
207
|
+
: typeof parsed?.input === 'string'
|
|
208
|
+
? parsed.input
|
|
209
|
+
: '';
|
|
210
|
+
if (!normalizedPatchText) {
|
|
211
|
+
throw new Error('[verify-apply-patch] already_normalized: missing patch text in normalized args');
|
|
212
|
+
}
|
|
213
|
+
if (normalizedPatchText.includes('","input":"*** Begin Patch') || normalizedPatchText.includes('*** End Patch","input":"')) {
|
|
214
|
+
throw new Error('[verify-apply-patch] already_normalized: patch text incorrectly contains serialized JSON keys');
|
|
215
|
+
}
|
|
216
|
+
if (normalizedPatchText.split('\n')[0] !== '*** Begin Patch') {
|
|
217
|
+
throw new Error('[verify-apply-patch] already_normalized: patch first line is not *** Begin Patch');
|
|
218
|
+
}
|
|
219
|
+
if (!normalizedPatchText.includes('*** End Patch')) {
|
|
220
|
+
throw new Error('[verify-apply-patch] already_normalized: patch missing *** End Patch');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Regression: accept a typical sequence of apply_patch calls (add/update/append/escape/multi-hunk/delete).
|
|
225
|
+
// Note: we only validate tool governance & patch text normalization, not filesystem application.
|
|
226
|
+
{
|
|
227
|
+
const addBasic = [
|
|
228
|
+
'*** Begin Patch',
|
|
229
|
+
'*** Add File: .apply_patch_basic_add.txt',
|
|
230
|
+
'+apply_patch basic add ok',
|
|
231
|
+
'+line2',
|
|
232
|
+
'*** End Patch'
|
|
233
|
+
].join('\n');
|
|
234
|
+
const updateBasic = [
|
|
235
|
+
'*** Begin Patch',
|
|
236
|
+
'*** Update File: .apply_patch_basic_add.txt',
|
|
237
|
+
'@@',
|
|
238
|
+
'-apply_patch basic add ok',
|
|
239
|
+
'+apply_patch basic add+update ok',
|
|
240
|
+
' line2',
|
|
241
|
+
'*** End Patch'
|
|
242
|
+
].join('\n');
|
|
243
|
+
const appendBasic = [
|
|
244
|
+
'*** Begin Patch',
|
|
245
|
+
'*** Update File: .apply_patch_basic_add.txt',
|
|
246
|
+
'@@',
|
|
247
|
+
' apply_patch basic add+update ok',
|
|
248
|
+
' line2',
|
|
249
|
+
'+line3 (append)',
|
|
250
|
+
'*** End Patch'
|
|
251
|
+
].join('\n');
|
|
252
|
+
const addEscapeChars = [
|
|
253
|
+
'*** Begin Patch',
|
|
254
|
+
'*** Add File: .apply_patch_escape_chars.txt',
|
|
255
|
+
'+quotes: "double" and \'single\'',
|
|
256
|
+
'+backslash: \\',
|
|
257
|
+
'+json: {"a":1,"b":"x"}',
|
|
258
|
+
'+template: ${notInterpolated}',
|
|
259
|
+
'*** End Patch'
|
|
260
|
+
].join('\n');
|
|
261
|
+
const addMulti = [
|
|
262
|
+
'*** Begin Patch',
|
|
263
|
+
'*** Add File: .apply_patch_multi_hunk.txt',
|
|
264
|
+
'+Header',
|
|
265
|
+
'+Section A',
|
|
266
|
+
'+Section B',
|
|
267
|
+
'+Footer',
|
|
268
|
+
'*** End Patch'
|
|
269
|
+
].join('\n');
|
|
270
|
+
const updateMulti = [
|
|
271
|
+
'*** Begin Patch',
|
|
272
|
+
'*** Update File: .apply_patch_multi_hunk.txt',
|
|
273
|
+
'@@',
|
|
274
|
+
' Header',
|
|
275
|
+
'-Section A',
|
|
276
|
+
'+Section A (updated)',
|
|
277
|
+
' Section B',
|
|
278
|
+
' Footer',
|
|
279
|
+
'@@',
|
|
280
|
+
' Header',
|
|
281
|
+
' Section A (updated)',
|
|
282
|
+
'-Section B',
|
|
283
|
+
'+Section B (updated)',
|
|
284
|
+
' Footer',
|
|
285
|
+
'*** End Patch'
|
|
286
|
+
].join('\n');
|
|
287
|
+
const deleteBasic = [
|
|
288
|
+
'*** Begin Patch',
|
|
289
|
+
'*** Delete File: .apply_patch_basic_add.txt',
|
|
290
|
+
'*** End Patch'
|
|
291
|
+
].join('\n');
|
|
292
|
+
const deleteEscapeChars = [
|
|
293
|
+
'*** Begin Patch',
|
|
294
|
+
'*** Delete File: .apply_patch_escape_chars.txt',
|
|
295
|
+
'*** End Patch'
|
|
296
|
+
].join('\n');
|
|
297
|
+
const deleteMulti = [
|
|
298
|
+
'*** Begin Patch',
|
|
299
|
+
'*** Delete File: .apply_patch_multi_hunk.txt',
|
|
300
|
+
'*** End Patch'
|
|
301
|
+
].join('\n');
|
|
302
|
+
|
|
303
|
+
const cases = [
|
|
304
|
+
['seq_basic_add_text', addBasic],
|
|
305
|
+
['seq_basic_update_text', updateBasic],
|
|
306
|
+
['seq_basic_append_text', appendBasic],
|
|
307
|
+
['seq_escape_chars_text', addEscapeChars],
|
|
308
|
+
['seq_multi_add_text', addMulti],
|
|
309
|
+
['seq_multi_update_text', updateMulti],
|
|
310
|
+
['seq_basic_delete_text', deleteBasic],
|
|
311
|
+
['seq_escape_chars_delete_text', deleteEscapeChars],
|
|
312
|
+
['seq_multi_delete_text', deleteMulti]
|
|
313
|
+
];
|
|
314
|
+
|
|
315
|
+
for (const [label, patchText] of cases) {
|
|
316
|
+
await runApplyPatchArgsCase(label, patchText);
|
|
317
|
+
await runApplyPatchArgsCase(
|
|
318
|
+
`${label}_json`,
|
|
319
|
+
JSON.stringify({ patch: patchText, input: patchText })
|
|
320
|
+
);
|
|
321
|
+
await runApplyPatchArgsCase(
|
|
322
|
+
`${label}_json_escaped_newlines`,
|
|
323
|
+
JSON.stringify({ patch: escapeNewlines(patchText), input: escapeNewlines(patchText) })
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Regression: tolerate <arg_key>/<arg_value> artifacts stitched into patch strings.
|
|
329
|
+
// Some upstream providers may leak XML-like markup into JSON string fields.
|
|
330
|
+
{
|
|
331
|
+
const patchText = [
|
|
332
|
+
'*** Begin Patch',
|
|
333
|
+
'*** Delete File: .apply_patch_escape_test.txt',
|
|
334
|
+
'*** End Patch'
|
|
335
|
+
].join('\n');
|
|
336
|
+
const injected = `${patchText}</arg_key><arg_value>input</arg_key><arg_value>${patchText}`;
|
|
337
|
+
const argsString = JSON.stringify({ patch: injected });
|
|
338
|
+
const validation = validateToolCall('apply_patch', argsString);
|
|
339
|
+
if (!validation?.ok) {
|
|
340
|
+
throw new Error(
|
|
341
|
+
`[verify-apply-patch] arg_key_artifacts: validateToolCall failed with reason=${validation?.reason}`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
const parsed = JSON.parse(validation.normalizedArgs);
|
|
345
|
+
const normalizedPatchText =
|
|
346
|
+
typeof parsed?.patch === 'string'
|
|
347
|
+
? parsed.patch
|
|
348
|
+
: typeof parsed?.input === 'string'
|
|
349
|
+
? parsed.input
|
|
350
|
+
: '';
|
|
351
|
+
if (normalizedPatchText.includes('</arg_key><arg_value>')) {
|
|
352
|
+
throw new Error('[verify-apply-patch] arg_key_artifacts: patch still contains arg_key artifacts');
|
|
353
|
+
}
|
|
354
|
+
if (parsed?.patch !== parsed?.input) {
|
|
355
|
+
throw new Error('[verify-apply-patch] arg_key_artifacts: patch/input mismatch after normalization');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Regression: invalid JSON containers should be classified as invalid_json (not missing_changes).
|
|
360
|
+
{
|
|
361
|
+
const invalidJson = '{"file":"a.ts","changes":[{"kind":"create_file","lines":["x"],"file</arg_key><arg_value>a.ts"}]}';
|
|
362
|
+
const validation = validateToolCall('apply_patch', invalidJson);
|
|
363
|
+
if (validation?.ok || validation?.reason !== 'invalid_json') {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`[verify-apply-patch] invalid_json: expected invalid_json reason, got ok=${validation?.ok} reason=${validation?.reason}`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Regression: patches can contain ``` blocks inside file content; do not treat them as outer fences.
|
|
371
|
+
{
|
|
372
|
+
const patchText = [
|
|
373
|
+
'*** Begin Patch',
|
|
374
|
+
'*** Add File: src/demo-codefence.md',
|
|
375
|
+
'+```json',
|
|
376
|
+
'+{\"ok\":true}',
|
|
377
|
+
'+```',
|
|
378
|
+
'*** End Patch'
|
|
379
|
+
].join('\n');
|
|
380
|
+
const validation = validateToolCall('apply_patch', patchText);
|
|
381
|
+
if (!validation?.ok) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
`[verify-apply-patch] inner_codefence: validateToolCall failed with reason=${validation?.reason}`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
const parsed = JSON.parse(validation.normalizedArgs);
|
|
387
|
+
const normalizedPatchText =
|
|
388
|
+
typeof parsed?.patch === 'string'
|
|
389
|
+
? parsed.patch
|
|
390
|
+
: typeof parsed?.input === 'string'
|
|
391
|
+
? parsed.input
|
|
392
|
+
: '';
|
|
393
|
+
if (!normalizedPatchText.startsWith('*** Begin Patch')) {
|
|
394
|
+
throw new Error('[verify-apply-patch] inner_codefence: patch lost *** Begin Patch header');
|
|
395
|
+
}
|
|
396
|
+
if (!normalizedPatchText.includes('*** Add File: src/demo-codefence.md')) {
|
|
397
|
+
throw new Error('[verify-apply-patch] inner_codefence: missing Add File header');
|
|
398
|
+
}
|
|
399
|
+
if (!normalizedPatchText.includes('+```json') || !normalizedPatchText.includes('+```')) {
|
|
400
|
+
throw new Error('[verify-apply-patch] inner_codefence: missing fenced lines inside patch');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Regression: tolerate newline-escaped snippets inside structured payload fields.
|
|
405
|
+
// Some models/clients double-escape multi-line anchors/targets (e.g. "\\n ").
|
|
406
|
+
{
|
|
407
|
+
const structuredArgs = JSON.stringify({
|
|
408
|
+
file: 'src/demo-escaped-snippet.ts',
|
|
409
|
+
changes: [
|
|
410
|
+
{
|
|
411
|
+
kind: 'replace',
|
|
412
|
+
target: 'const alpha = 1;\\n const beta = 2;',
|
|
413
|
+
lines: ['const alpha = 1;', ' const beta = 3;']
|
|
414
|
+
}
|
|
415
|
+
]
|
|
416
|
+
});
|
|
417
|
+
const validation = validateToolCall('apply_patch', structuredArgs);
|
|
418
|
+
if (!validation?.ok) {
|
|
419
|
+
throw new Error(
|
|
420
|
+
`[verify-apply-patch] escaped_structured_snippet: validateToolCall failed with reason=${validation?.reason}`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
const parsed = JSON.parse(validation.normalizedArgs);
|
|
424
|
+
const patchText =
|
|
425
|
+
typeof parsed?.patch === 'string'
|
|
426
|
+
? parsed.patch
|
|
427
|
+
: typeof parsed?.input === 'string'
|
|
428
|
+
? parsed.input
|
|
429
|
+
: '';
|
|
430
|
+
if (!patchText) {
|
|
431
|
+
throw new Error('[verify-apply-patch] escaped_structured_snippet: missing patch text in normalized args');
|
|
432
|
+
}
|
|
433
|
+
if (patchText.includes('const alpha = 1;\\n') || patchText.includes('\\n const beta = 2;')) {
|
|
434
|
+
throw new Error('[verify-apply-patch] escaped_structured_snippet: patch still contains literal \\\\n in target');
|
|
435
|
+
}
|
|
436
|
+
if (!patchText.includes('-const alpha = 1;') || !patchText.includes('- const beta = 2;')) {
|
|
437
|
+
throw new Error('[verify-apply-patch] escaped_structured_snippet: expected multi-line "-" target not found');
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
109
441
|
const plainJson = JSON.stringify({
|
|
110
442
|
file: 'src/demo.ts',
|
|
111
443
|
changes: [
|
|
@@ -136,11 +468,9 @@ async function main() {
|
|
|
136
468
|
|
|
137
469
|
console.log('✅ verify-apply-patch: text→tool_calls pipeline passed');
|
|
138
470
|
} catch (error) {
|
|
139
|
-
|
|
140
|
-
console.error(
|
|
141
|
-
|
|
142
|
-
error instanceof Error ? error.message : String(error ?? 'Unknown error')
|
|
143
|
-
);
|
|
471
|
+
const msg = error instanceof Error ? (error.stack || error.message) : String(error ?? 'Unknown error');
|
|
472
|
+
console.error(chalkError(msg));
|
|
473
|
+
console.error(chalkError(`❌ verify-apply-patch 失败: ${error instanceof Error ? error.message : String(error ?? 'Unknown error')}`));
|
|
144
474
|
process.exit(1);
|
|
145
475
|
}
|
|
146
476
|
}
|
|
@@ -29,6 +29,24 @@ const AGENTS_INSTRUCTIONS = (() => {
|
|
|
29
29
|
}
|
|
30
30
|
})();
|
|
31
31
|
|
|
32
|
+
function readServerApiKeyFromConfig(configPath) {
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
35
|
+
const json = raw && raw.trim() ? JSON.parse(raw) : {};
|
|
36
|
+
const apikey = json?.httpserver?.apikey;
|
|
37
|
+
return typeof apikey === 'string' && apikey.trim() ? apikey.trim() : '';
|
|
38
|
+
} catch {
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildAuthHeaders(serverApiKey) {
|
|
44
|
+
if (!serverApiKey) {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
return { 'x-api-key': serverApiKey };
|
|
48
|
+
}
|
|
49
|
+
|
|
32
50
|
async function main() {
|
|
33
51
|
if (!VERIFY_CONFIG) {
|
|
34
52
|
console.error('❌ ROUTECODEX_VERIFY_CONFIG 未设置,无法运行端到端校验');
|
|
@@ -36,6 +54,8 @@ async function main() {
|
|
|
36
54
|
}
|
|
37
55
|
|
|
38
56
|
console.log(`[verify:e2e-toolcall] 使用配置: ${VERIFY_CONFIG}`);
|
|
57
|
+
const serverApiKey = readServerApiKeyFromConfig(VERIFY_CONFIG);
|
|
58
|
+
const authHeaders = buildAuthHeaders(serverApiKey);
|
|
39
59
|
const serverEnv = {
|
|
40
60
|
...process.env,
|
|
41
61
|
ROUTECODEX_CONFIG_PATH: VERIFY_CONFIG,
|
|
@@ -61,11 +81,12 @@ async function main() {
|
|
|
61
81
|
try {
|
|
62
82
|
await waitForServer();
|
|
63
83
|
await waitForRouterWarmup();
|
|
64
|
-
await
|
|
84
|
+
await runModelsSmokeCheck(authHeaders);
|
|
85
|
+
await runToolcallVerification(authHeaders);
|
|
65
86
|
console.log('✅ 端到端工具调用校验通过');
|
|
66
87
|
|
|
67
|
-
await runDaemonAdminSmokeCheck();
|
|
68
|
-
await runConfigV2ProvidersSmokeCheck();
|
|
88
|
+
await runDaemonAdminSmokeCheck(authHeaders);
|
|
89
|
+
await runConfigV2ProvidersSmokeCheck(authHeaders);
|
|
69
90
|
|
|
70
91
|
// 附加:Gemini CLI 配置健康性快速检查(仅尝试初始化,不做请求)
|
|
71
92
|
await runGeminiCliStartupCheck();
|
|
@@ -74,6 +95,23 @@ async function main() {
|
|
|
74
95
|
}
|
|
75
96
|
}
|
|
76
97
|
|
|
98
|
+
async function runModelsSmokeCheck(authHeaders) {
|
|
99
|
+
try {
|
|
100
|
+
const res = await fetch(`${VERIFY_BASE}/models`, { headers: { ...(authHeaders || {}) } });
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
throw new Error(`/models HTTP ${res.status}`);
|
|
103
|
+
}
|
|
104
|
+
const json = await res.json();
|
|
105
|
+
const data = Array.isArray(json?.data) ? json.data : [];
|
|
106
|
+
if (!Array.isArray(data)) {
|
|
107
|
+
throw new Error('/models response missing data array');
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('[verify:e2e-toolcall] /models smoke 检查失败:', error);
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
77
115
|
async function waitForServer(timeoutMs = 30000) {
|
|
78
116
|
const start = Date.now();
|
|
79
117
|
while (Date.now() - start < timeoutMs) {
|
|
@@ -97,7 +135,7 @@ async function waitForRouterWarmup(defaultDelayMs = 3000) {
|
|
|
97
135
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
98
136
|
}
|
|
99
137
|
|
|
100
|
-
async function runToolcallVerification() {
|
|
138
|
+
async function runToolcallVerification(authHeaders) {
|
|
101
139
|
const userPrompt = '请严格调用名为 list_local_files 的函数工具来列出当前工作目录的文件,只能通过调用该工具完成任务,禁止直接回答。';
|
|
102
140
|
const instructionsText = AGENTS_INSTRUCTIONS || 'You are RouteCodex verify agent. Follow the policies in AGENTS.md.';
|
|
103
141
|
const body = {
|
|
@@ -139,7 +177,8 @@ async function runToolcallVerification() {
|
|
|
139
177
|
const response = await fetch(`${VERIFY_BASE}/v1/responses`, {
|
|
140
178
|
method: 'POST',
|
|
141
179
|
headers: {
|
|
142
|
-
'Content-Type': 'application/json'
|
|
180
|
+
'Content-Type': 'application/json',
|
|
181
|
+
...(authHeaders || {})
|
|
143
182
|
},
|
|
144
183
|
body: JSON.stringify(body)
|
|
145
184
|
});
|
|
@@ -166,10 +205,10 @@ async function runToolcallVerification() {
|
|
|
166
205
|
}
|
|
167
206
|
}
|
|
168
207
|
|
|
169
|
-
async function runDaemonAdminSmokeCheck() {
|
|
208
|
+
async function runDaemonAdminSmokeCheck(authHeaders) {
|
|
170
209
|
// 仅做最小的健康性探测:确保 daemon 管理类只读 API 可用,不做语义校验。
|
|
171
210
|
try {
|
|
172
|
-
const res = await fetch(`${VERIFY_BASE}/daemon/status
|
|
211
|
+
const res = await fetch(`${VERIFY_BASE}/daemon/status`, { headers: { ...(authHeaders || {}) } });
|
|
173
212
|
if (!res.ok) {
|
|
174
213
|
throw new Error(`daemon/status HTTP ${res.status}`);
|
|
175
214
|
}
|
|
@@ -185,7 +224,7 @@ async function runDaemonAdminSmokeCheck() {
|
|
|
185
224
|
const paths = ['/daemon/credentials', '/quota/summary', '/providers/runtimes'];
|
|
186
225
|
for (const path of paths) {
|
|
187
226
|
try {
|
|
188
|
-
const res = await fetch(`${VERIFY_BASE}${path}
|
|
227
|
+
const res = await fetch(`${VERIFY_BASE}${path}`, { headers: { ...(authHeaders || {}) } });
|
|
189
228
|
if (!res.ok) {
|
|
190
229
|
throw new Error(`${path} HTTP ${res.status}`);
|
|
191
230
|
}
|
|
@@ -198,9 +237,9 @@ async function runDaemonAdminSmokeCheck() {
|
|
|
198
237
|
}
|
|
199
238
|
}
|
|
200
239
|
|
|
201
|
-
async function runConfigV2ProvidersSmokeCheck() {
|
|
240
|
+
async function runConfigV2ProvidersSmokeCheck(authHeaders) {
|
|
202
241
|
try {
|
|
203
|
-
const res = await fetch(`${VERIFY_BASE}/config/providers/v2
|
|
242
|
+
const res = await fetch(`${VERIFY_BASE}/config/providers/v2`, { headers: { ...(authHeaders || {}) } });
|
|
204
243
|
if (!res.ok) {
|
|
205
244
|
throw new Error(`/config/providers/v2 HTTP ${res.status}`);
|
|
206
245
|
}
|