@ryanfw/prompt-orchestration-pipeline 1.3.2 → 1.3.4
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/docs/http-api.md +80 -4
- package/package.json +1 -4
- package/src/api/__tests__/index.test.ts +443 -32
- package/src/api/index.ts +108 -127
- package/src/cli/__tests__/analyze-task.test.ts +40 -7
- package/src/cli/__tests__/index.test.ts +337 -17
- package/src/cli/__tests__/types.test.ts +0 -18
- package/src/cli/__tests__/update-pipeline-json.test.ts +19 -9
- package/src/cli/analyze-task.ts +3 -14
- package/src/cli/index.ts +73 -61
- package/src/cli/types.ts +0 -13
- package/src/cli/update-pipeline-json.ts +3 -14
- package/src/config/__tests__/paths.test.ts +46 -1
- package/src/config/__tests__/pipeline-registry.test.ts +767 -0
- package/src/config/__tests__/sse-events.test.ts +470 -0
- package/src/config/__tests__/statuses.test.ts +41 -0
- package/src/config/paths.ts +11 -2
- package/src/config/pipeline-registry.ts +258 -0
- package/src/config/sse-events.ts +122 -0
- package/src/config/statuses.ts +20 -1
- package/src/core/__tests__/agent-step.test.ts +115 -0
- package/src/core/__tests__/config.test.ts +545 -105
- package/src/core/__tests__/core-boot-resolution.test.ts +181 -0
- package/src/core/__tests__/job-concurrency.test.ts +260 -0
- package/src/core/__tests__/job-submission.test.ts +396 -0
- package/src/core/__tests__/job-view.test.ts +1341 -0
- package/src/core/__tests__/json-file.test.ts +111 -0
- package/src/core/__tests__/lifecycle-policy.test.ts +81 -7
- package/src/core/__tests__/logger.test.ts +22 -0
- package/src/core/__tests__/orchestrator-no-deprecated-exports.test.ts +41 -0
- package/src/core/__tests__/orchestrator.test.ts +615 -2
- package/src/core/__tests__/pipeline-runner.test.ts +946 -9
- package/src/core/__tests__/redact.test.ts +153 -0
- package/src/core/__tests__/runner-liveness.test.ts +910 -0
- package/src/core/__tests__/seed-naming.test.ts +57 -0
- package/src/core/__tests__/single-derivation.test.ts +159 -0
- package/src/core/__tests__/task-runner.test.ts +594 -3
- package/src/core/__tests__/task-telemetry.test.ts +241 -0
- package/src/core/__tests__/workspace-root-resolution-guard.test.ts +62 -0
- package/src/core/agent-step.ts +4 -1
- package/src/core/agent-types.ts +5 -4
- package/src/core/config.ts +134 -222
- package/src/core/control.ts +3 -0
- package/src/core/job-concurrency.ts +56 -20
- package/src/core/job-submission.ts +133 -0
- package/src/core/job-view.ts +473 -0
- package/src/core/json-file.ts +45 -0
- package/src/core/lifecycle-policy.ts +23 -8
- package/src/core/logger.ts +0 -29
- package/src/core/orchestrator.ts +200 -74
- package/src/core/pipeline-runner.ts +85 -53
- package/src/core/redact.ts +40 -0
- package/src/core/runner-liveness.ts +280 -0
- package/src/core/seed-naming.ts +9 -0
- package/src/core/status-writer.ts +27 -33
- package/src/core/task-runner.ts +356 -319
- package/src/core/task-telemetry.ts +107 -0
- package/src/harness/__tests__/subprocess.test.ts +112 -1
- package/src/harness/subprocess.ts +55 -16
- package/src/llm/__tests__/index.test.ts +684 -33
- package/src/llm/index.ts +310 -67
- package/src/providers/__tests__/alibaba.test.ts +67 -6
- package/src/providers/__tests__/anthropic.test.ts +35 -14
- package/src/providers/__tests__/base.test.ts +62 -0
- package/src/providers/__tests__/claude-code.test.ts +99 -14
- package/src/providers/__tests__/deepseek.test.ts +16 -6
- package/src/providers/__tests__/gemini.test.ts +47 -25
- package/src/providers/__tests__/moonshot.test.ts +27 -0
- package/src/providers/__tests__/openai.test.ts +262 -74
- package/src/providers/__tests__/opencode.test.ts +77 -0
- package/src/providers/__tests__/request-timeout-contract.test.ts +226 -0
- package/src/providers/__tests__/stream-accumulator.test.ts +52 -0
- package/src/providers/__tests__/types.test.ts +85 -0
- package/src/providers/__tests__/zero-fill-guard.test.ts +62 -0
- package/src/providers/__tests__/zhipu.test.ts +27 -14
- package/src/providers/alibaba.ts +20 -39
- package/src/providers/anthropic.ts +23 -11
- package/src/providers/base.ts +19 -0
- package/src/providers/claude-code.ts +27 -18
- package/src/providers/deepseek.ts +9 -28
- package/src/providers/gemini.ts +20 -58
- package/src/providers/moonshot.ts +15 -11
- package/src/providers/openai.ts +79 -61
- package/src/providers/opencode.ts +16 -33
- package/src/providers/stream-accumulator.ts +27 -21
- package/src/providers/types.ts +29 -4
- package/src/providers/zhipu.ts +15 -44
- package/src/task-analysis/__tests__/analyzer.test.ts +0 -0
- package/src/task-analysis/__tests__/enrichers-schema-deducer.test.ts +34 -2
- package/src/task-analysis/__tests__/no-ast-imports.test.ts +52 -0
- package/src/task-analysis/__tests__/repository.test.ts +65 -0
- package/src/task-analysis/__tests__/types.test.ts +63 -130
- package/src/task-analysis/analyzer.ts +91 -0
- package/src/task-analysis/enrichers/schema-deducer.ts +13 -2
- package/src/task-analysis/index.ts +2 -36
- package/src/task-analysis/repository.ts +45 -0
- package/src/task-analysis/types.ts +42 -58
- package/src/ui/client/__tests__/api.test.ts +143 -1
- package/src/ui/client/__tests__/bootstrap.test.ts +178 -62
- package/src/ui/client/__tests__/job-adapter.test.ts +203 -2
- package/src/ui/client/__tests__/load-state.test.ts +70 -0
- package/src/ui/client/__tests__/types.test.ts +66 -3
- package/src/ui/client/__tests__/useJobDetailWithUpdates.test.ts +390 -77
- package/src/ui/client/__tests__/useJobList.test.ts +198 -23
- package/src/ui/client/__tests__/useJobListWithUpdates.test.ts +218 -7
- package/src/ui/client/adapters/__tests__/job-adapter.test.ts +186 -0
- package/src/ui/client/adapters/job-adapter.ts +41 -16
- package/src/ui/client/api.ts +38 -15
- package/src/ui/client/bootstrap.ts +19 -14
- package/src/ui/client/hooks/useJobDetailWithUpdates.ts +73 -97
- package/src/ui/client/hooks/useJobList.ts +26 -31
- package/src/ui/client/hooks/useJobListWithUpdates.ts +42 -76
- package/src/ui/client/load-state.ts +20 -0
- package/src/ui/client/reducers/__tests__/job-events.test.ts +568 -0
- package/src/ui/client/reducers/job-events.ts +137 -0
- package/src/ui/client/types.ts +16 -20
- package/src/ui/components/DAGGrid.tsx +6 -1
- package/src/ui/components/JobDetail.tsx +12 -4
- package/src/ui/components/JobTable.tsx +41 -13
- package/src/ui/components/StageTimeline.tsx +8 -20
- package/src/ui/components/TaskAnalysisDisplay.tsx +48 -6
- package/src/ui/components/__tests__/DAGGrid.test.tsx +36 -0
- package/src/ui/components/__tests__/JobDetail.test.tsx +112 -0
- package/src/ui/components/__tests__/JobTable.test.tsx +137 -1
- package/src/ui/components/__tests__/StageTimeline.test.tsx +5 -6
- package/src/ui/components/__tests__/TaskAnalysisDisplay.test.tsx +64 -0
- package/src/ui/components/types.ts +35 -15
- package/src/ui/dist/assets/{index--RH3sAt3.js → index-DN3-zvtP.js} +4324 -263
- package/src/ui/dist/assets/index-DN3-zvtP.js.map +1 -0
- package/src/ui/dist/assets/style-CtZBnjlR.css +2 -0
- package/src/ui/dist/index.html +2 -2
- package/src/ui/pages/PipelineDetail.tsx +60 -4
- package/src/ui/pages/PromptPipelineDashboard.tsx +59 -7
- package/src/ui/pages/__tests__/PromptPipelineDashboard.test.tsx +114 -32
- package/src/ui/pages/__tests__/pages.test.tsx +236 -42
- package/src/ui/server/__tests__/concurrency-endpoint.test.ts +54 -0
- package/src/ui/server/__tests__/config-bridge-node.test.ts +34 -1
- package/src/ui/server/__tests__/config-bridge.test.ts +9 -1
- package/src/ui/server/__tests__/embedded-assets.test.ts +66 -0
- package/src/ui/server/__tests__/file-endpoints.test.ts +183 -5
- package/src/ui/server/__tests__/gate-endpoints.test.ts +55 -0
- package/src/ui/server/__tests__/index.test.ts +63 -0
- package/src/ui/server/__tests__/job-control-endpoints.test.ts +512 -2
- package/src/ui/server/__tests__/job-endpoints.test.ts +158 -2
- package/src/ui/server/__tests__/read-static-path-guard.test.ts +94 -0
- package/src/ui/server/__tests__/router-root-isolation.test.ts +204 -0
- package/src/ui/server/__tests__/router-threading.test.ts +148 -0
- package/src/ui/server/__tests__/router.test.ts +104 -0
- package/src/ui/server/__tests__/sse-broadcast.test.ts +44 -0
- package/src/ui/server/__tests__/sse-enhancer.test.ts +70 -0
- package/src/ui/server/config-bridge-node.ts +8 -26
- package/src/ui/server/config-bridge.ts +3 -4
- package/src/ui/server/embedded-assets-imports.d.ts +44 -0
- package/src/ui/server/embedded-assets.ts +13 -2
- package/src/ui/server/endpoints/__tests__/meta-endpoint.test.ts +1 -0
- package/src/ui/server/endpoints/__tests__/pipeline-analysis-endpoint.test.ts +167 -0
- package/src/ui/server/endpoints/__tests__/schema-file-endpoint.test.ts +104 -0
- package/src/ui/server/endpoints/__tests__/task-analysis-endpoint.test.ts +103 -0
- package/src/ui/server/endpoints/__tests__/upload-endpoints.test.ts +162 -96
- package/src/ui/server/endpoints/concurrency-endpoint.ts +2 -1
- package/src/ui/server/endpoints/create-pipeline-endpoint.ts +14 -29
- package/src/ui/server/endpoints/file-endpoints.ts +15 -10
- package/src/ui/server/endpoints/gate-endpoints.ts +2 -6
- package/src/ui/server/endpoints/job-control-endpoints.ts +129 -141
- package/src/ui/server/endpoints/job-endpoints.ts +7 -0
- package/src/ui/server/endpoints/meta-endpoint.ts +1 -1
- package/src/ui/server/endpoints/pipeline-analysis-endpoint.ts +99 -11
- package/src/ui/server/endpoints/schema-file-endpoint.ts +11 -3
- package/src/ui/server/endpoints/task-analysis-endpoint.ts +21 -5
- package/src/ui/server/endpoints/task-save-endpoint.ts +1 -2
- package/src/ui/server/endpoints/upload-endpoints.ts +5 -40
- package/src/ui/server/index.ts +19 -14
- package/src/ui/server/job-reader.ts +14 -2
- package/src/ui/server/router.ts +33 -10
- package/src/ui/server/sse-broadcast.ts +14 -3
- package/src/ui/server/sse-enhancer.ts +12 -2
- package/src/ui/state/__tests__/schema-loader.test.ts +11 -1
- package/src/ui/state/__tests__/snapshot.test.ts +120 -14
- package/src/ui/state/__tests__/types.test.ts +104 -5
- package/src/ui/state/snapshot.ts +2 -2
- package/src/ui/state/transformers/__tests__/list-transformer.test.ts +85 -2
- package/src/ui/state/transformers/__tests__/status-transformer.test.ts +250 -22
- package/src/ui/state/transformers/list-transformer.ts +13 -3
- package/src/ui/state/transformers/status-transformer.ts +36 -170
- package/src/ui/state/types.ts +15 -48
- package/src/utils/__tests__/path-containment.test.ts +160 -0
- package/src/{ui/server/utils → utils}/path-containment.ts +21 -0
- package/src/task-analysis/__tests__/enrichers-analysis-writer.test.ts +0 -86
- package/src/task-analysis/__tests__/enrichers-artifact-resolver.test.ts +0 -124
- package/src/task-analysis/__tests__/extractors-artifacts.test.ts +0 -133
- package/src/task-analysis/__tests__/extractors-llm-calls.test.ts +0 -46
- package/src/task-analysis/__tests__/extractors-stages.test.ts +0 -52
- package/src/task-analysis/__tests__/index.test.ts +0 -143
- package/src/task-analysis/__tests__/parser.test.ts +0 -41
- package/src/task-analysis/__tests__/utils-ast.test.ts +0 -82
- package/src/task-analysis/enrichers/analysis-writer.ts +0 -75
- package/src/task-analysis/enrichers/artifact-resolver.ts +0 -89
- package/src/task-analysis/extractors/artifacts.ts +0 -143
- package/src/task-analysis/extractors/llm-calls.ts +0 -117
- package/src/task-analysis/extractors/stages.ts +0 -45
- package/src/task-analysis/parser.ts +0 -20
- package/src/task-analysis/utils/ast.ts +0 -45
- package/src/ui/dist/assets/index--RH3sAt3.js.map +0 -1
- package/src/ui/dist/assets/style-CSSKuMOe.css +0 -2
- package/src/ui/embedded-assets.js +0 -12
- package/src/ui/server/__tests__/path-containment.test.ts +0 -54
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
createProviderError,
|
|
4
4
|
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
5
5
|
ensureJsonResponseFormat,
|
|
6
|
+
extractResponseSchema,
|
|
6
7
|
extractMessages,
|
|
8
|
+
isJsonResponseFormat,
|
|
7
9
|
isRetryableError,
|
|
8
10
|
sleep,
|
|
9
11
|
stripMarkdownFences,
|
|
@@ -305,6 +307,66 @@ describe("ensureJsonResponseFormat", () => {
|
|
|
305
307
|
});
|
|
306
308
|
});
|
|
307
309
|
|
|
310
|
+
describe("isJsonResponseFormat", () => {
|
|
311
|
+
it('returns true for "json"', () => {
|
|
312
|
+
expect(isJsonResponseFormat("json")).toBe(true);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('returns true for "json_object"', () => {
|
|
316
|
+
expect(isJsonResponseFormat("json_object")).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('returns true for { type: "json_object" }', () => {
|
|
320
|
+
expect(isJsonResponseFormat({ type: "json_object" })).toBe(true);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("returns true for { json_schema: {} }", () => {
|
|
324
|
+
expect(isJsonResponseFormat({ json_schema: {} })).toBe(true);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("returns false for undefined", () => {
|
|
328
|
+
expect(isJsonResponseFormat(undefined)).toBe(false);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("returns false for null", () => {
|
|
332
|
+
expect(isJsonResponseFormat(null)).toBe(false);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('returns false for ""', () => {
|
|
336
|
+
expect(isJsonResponseFormat("")).toBe(false);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('returns false for "text"', () => {
|
|
340
|
+
expect(isJsonResponseFormat("text")).toBe(false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("returns false for an object with neither json_object type nor json_schema", () => {
|
|
344
|
+
expect(isJsonResponseFormat({ type: "text" })).toBe(false);
|
|
345
|
+
expect(isJsonResponseFormat({ foo: "bar" })).toBe(false);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe("extractResponseSchema", () => {
|
|
350
|
+
it("returns the schema when json_schema is present", () => {
|
|
351
|
+
const schema = { type: "object", properties: { name: { type: "string" } } };
|
|
352
|
+
expect(extractResponseSchema({ json_schema: schema })).toBe(schema);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("returns undefined for a plain string responseFormat", () => {
|
|
356
|
+
expect(extractResponseSchema("json")).toBeUndefined();
|
|
357
|
+
expect(extractResponseSchema("json_object")).toBeUndefined();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("returns undefined when json_schema is absent", () => {
|
|
361
|
+
expect(extractResponseSchema({ type: "json_object" })).toBeUndefined();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("returns undefined for undefined/null", () => {
|
|
365
|
+
expect(extractResponseSchema(undefined)).toBeUndefined();
|
|
366
|
+
expect(extractResponseSchema(null)).toBeUndefined();
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
308
370
|
describe("createProviderError", () => {
|
|
309
371
|
it("returns an Error with status, code, and details", () => {
|
|
310
372
|
const err = createProviderError(500, { message: "server error" }, "fallback");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { ProviderJsonParseError } from "../types.ts";
|
|
3
3
|
import type { ClaudeCodeOptions } from "../types.ts";
|
|
4
4
|
|
|
5
5
|
const baseOptions: ClaudeCodeOptions = {
|
|
@@ -86,18 +86,15 @@ describe("claudeCodeChat", () => {
|
|
|
86
86
|
expect(result.content).toEqual(jsonPayload);
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
it("
|
|
89
|
+
it("does not report usage on success", async () => {
|
|
90
90
|
spawnMock.mockReturnValue(
|
|
91
91
|
createMockProcFromText(JSON.stringify({ ok: true })),
|
|
92
92
|
);
|
|
93
93
|
|
|
94
94
|
const result = await claudeCodeChat(baseOptions);
|
|
95
95
|
|
|
96
|
-
expect(result.usage).
|
|
97
|
-
|
|
98
|
-
completion_tokens: 0,
|
|
99
|
-
total_tokens: 0,
|
|
100
|
-
});
|
|
96
|
+
expect(result.usage).toBeUndefined();
|
|
97
|
+
expect(result.usageAvailable).toBeUndefined();
|
|
101
98
|
});
|
|
102
99
|
|
|
103
100
|
it("uses custom model and maxTurns when provided", async () => {
|
|
@@ -143,6 +140,7 @@ describe("claudeCodeChat", () => {
|
|
|
143
140
|
{ role: "system", content: "Be concise." },
|
|
144
141
|
{ role: "user", content: "Hello" },
|
|
145
142
|
],
|
|
143
|
+
responseFormat: "json",
|
|
146
144
|
});
|
|
147
145
|
|
|
148
146
|
const callArgs = spawnMock.mock.calls[0]![0] as string[];
|
|
@@ -166,13 +164,17 @@ describe("claudeCodeChat", () => {
|
|
|
166
164
|
expect(callArgs[callArgs.indexOf("--max-tokens") + 1]).toBe("2048");
|
|
167
165
|
});
|
|
168
166
|
|
|
169
|
-
it("
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
167
|
+
it("returns plain text content when responseFormat is omitted (AC-13)", async () => {
|
|
168
|
+
spawnMock.mockReturnValue(
|
|
169
|
+
createMockProcFromText("not json, just text"),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const result = await claudeCodeChat({
|
|
173
|
+
messages: [{ role: "user", content: "Give me json." }],
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(typeof result.content).toBe("string");
|
|
177
|
+
expect(result.content).toBe("not json, just text");
|
|
176
178
|
});
|
|
177
179
|
|
|
178
180
|
describe("streaming accumulation", () => {
|
|
@@ -200,6 +202,89 @@ describe("claudeCodeChat", () => {
|
|
|
200
202
|
expect(result.content).toEqual({ ok: true });
|
|
201
203
|
});
|
|
202
204
|
});
|
|
205
|
+
|
|
206
|
+
describe("requestTimeoutMs (AC-12)", () => {
|
|
207
|
+
let realClearTimeout: typeof globalThis.clearTimeout;
|
|
208
|
+
let realSetTimeout: typeof globalThis.setTimeout;
|
|
209
|
+
|
|
210
|
+
beforeEach(() => {
|
|
211
|
+
vi.useFakeTimers();
|
|
212
|
+
realClearTimeout = globalThis.clearTimeout;
|
|
213
|
+
realSetTimeout = globalThis.setTimeout;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
afterEach(() => {
|
|
217
|
+
vi.useRealTimers();
|
|
218
|
+
globalThis.clearTimeout = realClearTimeout;
|
|
219
|
+
globalThis.setTimeout = realSetTimeout;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("applies requestTimeoutMs to the subprocess idle abort", async () => {
|
|
223
|
+
const TIMEOUT_MS = 1_000;
|
|
224
|
+
const killFn = vi.fn();
|
|
225
|
+
let resolveExit: (code: number | null) => void = () => {};
|
|
226
|
+
const mockExit = new Promise<number | null>((resolve) => {
|
|
227
|
+
resolveExit = resolve;
|
|
228
|
+
});
|
|
229
|
+
let closed = false;
|
|
230
|
+
const mockStdout = new ReadableStream<Uint8Array>({
|
|
231
|
+
start(controller) {
|
|
232
|
+
killFn.mockImplementation(() => {
|
|
233
|
+
if (!closed) {
|
|
234
|
+
closed = true;
|
|
235
|
+
controller.close();
|
|
236
|
+
resolveExit(1);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
const mockStderr = new ReadableStream<Uint8Array>({
|
|
242
|
+
start(controller) {
|
|
243
|
+
controller.close();
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
spawnMock.mockReturnValue({
|
|
248
|
+
stdout: mockStdout,
|
|
249
|
+
stderr: mockStderr,
|
|
250
|
+
exited: mockExit,
|
|
251
|
+
exitCode: null,
|
|
252
|
+
pid: 12345,
|
|
253
|
+
kill: killFn,
|
|
254
|
+
ref: vi.fn(),
|
|
255
|
+
unref: vi.fn(),
|
|
256
|
+
} as unknown as ReturnType<typeof Bun.spawn>);
|
|
257
|
+
|
|
258
|
+
const promise = claudeCodeChat({
|
|
259
|
+
...baseOptions,
|
|
260
|
+
requestTimeoutMs: TIMEOUT_MS,
|
|
261
|
+
maxRetries: 0,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
let state: "pending" | "resolved" | "rejected" = "pending";
|
|
265
|
+
promise.then(
|
|
266
|
+
() => {
|
|
267
|
+
state = "resolved";
|
|
268
|
+
},
|
|
269
|
+
() => {
|
|
270
|
+
state = "rejected";
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// Before the threshold: kill must not have been called.
|
|
275
|
+
vi.advanceTimersByTime(TIMEOUT_MS - 1);
|
|
276
|
+
for (let i = 0; i < 10; i++) await Promise.resolve();
|
|
277
|
+
expect(killFn).not.toHaveBeenCalled();
|
|
278
|
+
expect(state).toBe("pending");
|
|
279
|
+
|
|
280
|
+
// Cross the threshold: idle timer fires → onAbort → kill → stdout closes → adapter throws.
|
|
281
|
+
vi.advanceTimersByTime(1);
|
|
282
|
+
for (let i = 0; i < 50; i++) await Promise.resolve();
|
|
283
|
+
|
|
284
|
+
expect(state).toBe("rejected");
|
|
285
|
+
expect(killFn).toHaveBeenCalledTimes(1);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
203
288
|
});
|
|
204
289
|
|
|
205
290
|
describe("isClaudeCodeAvailable", () => {
|
|
@@ -198,6 +198,7 @@ describe("deepseekChat", () => {
|
|
|
198
198
|
|
|
199
199
|
await deepseekChat({
|
|
200
200
|
messages: baseOptions.messages,
|
|
201
|
+
responseFormat: "json_object",
|
|
201
202
|
});
|
|
202
203
|
|
|
203
204
|
const body = JSON.parse(
|
|
@@ -207,6 +208,18 @@ describe("deepseekChat", () => {
|
|
|
207
208
|
expect(body.temperature).toBe(0.7);
|
|
208
209
|
});
|
|
209
210
|
|
|
211
|
+
it("returns plain text content when responseFormat is omitted (AC-13)", async () => {
|
|
212
|
+
const events = makeOpenAiSseEvents(["not json, just text"]);
|
|
213
|
+
fetchMock.mockResolvedValue(mockStreamingResponse(events));
|
|
214
|
+
|
|
215
|
+
const result = await deepseekChat({
|
|
216
|
+
messages: [{ role: "user", content: "Give me json output please." }],
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(typeof result.content).toBe("string");
|
|
220
|
+
expect(result.content).toBe("not json, just text");
|
|
221
|
+
});
|
|
222
|
+
|
|
210
223
|
it("passes optional parameters when provided", async () => {
|
|
211
224
|
const events = makeOpenAiSseEvents([JSON.stringify({ ok: true })]);
|
|
212
225
|
fetchMock.mockResolvedValue(mockStreamingResponse(events));
|
|
@@ -277,7 +290,7 @@ describe("deepseekChat", () => {
|
|
|
277
290
|
});
|
|
278
291
|
});
|
|
279
292
|
|
|
280
|
-
it("
|
|
293
|
+
it("returns undefined usage when accumulated stream reports none", async () => {
|
|
281
294
|
const events = [
|
|
282
295
|
'data: {"choices":[{"delta":{"content":"{\\"ok\\":true}"}}]}\n\n',
|
|
283
296
|
'data: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n\n',
|
|
@@ -287,11 +300,8 @@ describe("deepseekChat", () => {
|
|
|
287
300
|
fetchMock.mockResolvedValue(mockStreamingResponse(events));
|
|
288
301
|
|
|
289
302
|
const result = await deepseekChat(baseOptions);
|
|
290
|
-
expect(result.
|
|
291
|
-
|
|
292
|
-
completion_tokens: 0,
|
|
293
|
-
total_tokens: 0,
|
|
294
|
-
});
|
|
303
|
+
expect(result.content).toEqual({ ok: true });
|
|
304
|
+
expect(result.usage).toBeUndefined();
|
|
295
305
|
});
|
|
296
306
|
|
|
297
307
|
it("skips SSE comment lines and empty lines", async () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { geminiChat } from "../gemini.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { ProviderJsonParseError } from "../types.ts";
|
|
4
4
|
import type { GeminiOptions } from "../types.ts";
|
|
5
5
|
import type { Mock } from "vitest";
|
|
6
6
|
|
|
@@ -171,7 +171,7 @@ describe("geminiChat", () => {
|
|
|
171
171
|
]);
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
-
it("
|
|
174
|
+
it("sets native responseSchema in generationConfig when responseFormat has json_schema (AC-14)", async () => {
|
|
175
175
|
const schema = {
|
|
176
176
|
type: "object",
|
|
177
177
|
properties: { name: { type: "string" } },
|
|
@@ -191,29 +191,28 @@ describe("geminiChat", () => {
|
|
|
191
191
|
(fetchMock.mock.calls[0] as [string, RequestInit])[1].body as string,
|
|
192
192
|
);
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
expect(
|
|
196
|
-
|
|
197
|
-
expect(systemText).toContain(
|
|
194
|
+
expect(body.generationConfig.responseMimeType).toBe("application/json");
|
|
195
|
+
expect(body.generationConfig.responseSchema).toEqual(schema);
|
|
196
|
+
const systemText = body.systemInstruction?.parts?.[0]?.text ?? "";
|
|
197
|
+
expect(systemText).not.toContain("Respond with JSON matching this schema");
|
|
198
|
+
expect(systemText).toBe("You are helpful.");
|
|
198
199
|
});
|
|
199
200
|
|
|
200
|
-
it("
|
|
201
|
-
const
|
|
202
|
-
const events = makeGeminiSseEvents(JSON.stringify({}));
|
|
201
|
+
it("does not set responseSchema when responseFormat is a plain json_object", async () => {
|
|
202
|
+
const events = makeGeminiSseEvents(JSON.stringify({ ok: true }));
|
|
203
203
|
fetchMock.mockResolvedValue(mockStreamingResponse(events));
|
|
204
204
|
|
|
205
205
|
await geminiChat({
|
|
206
|
-
messages: [{ role: "user", content: "
|
|
207
|
-
responseFormat:
|
|
206
|
+
messages: [{ role: "user", content: "ok" }],
|
|
207
|
+
responseFormat: "json_object",
|
|
208
208
|
});
|
|
209
209
|
|
|
210
210
|
const body = JSON.parse(
|
|
211
211
|
(fetchMock.mock.calls[0] as [string, RequestInit])[1].body as string,
|
|
212
212
|
);
|
|
213
213
|
|
|
214
|
-
expect(body.
|
|
215
|
-
|
|
216
|
-
expect(systemText).toContain("Respond with JSON matching this schema:");
|
|
214
|
+
expect(body.generationConfig.responseMimeType).toBe("application/json");
|
|
215
|
+
expect(body.generationConfig.responseSchema).toBeUndefined();
|
|
217
216
|
});
|
|
218
217
|
|
|
219
218
|
it("normalizes usage from Gemini's usageMetadata format", async () => {
|
|
@@ -295,13 +294,21 @@ describe("geminiChat", () => {
|
|
|
295
294
|
expect(result.content).toEqual(jsonPayload);
|
|
296
295
|
});
|
|
297
296
|
|
|
298
|
-
it("
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
)
|
|
297
|
+
it("does not set responseMimeType when responseFormat is omitted even if the message says 'json' (AC-13)", async () => {
|
|
298
|
+
const events = makeGeminiSseEvents("not json, just text");
|
|
299
|
+
fetchMock.mockResolvedValue(mockStreamingResponse(events));
|
|
300
|
+
|
|
301
|
+
const result = await geminiChat({
|
|
302
|
+
messages: [{ role: "user", content: "Give me json output please." }],
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const body = JSON.parse(
|
|
306
|
+
(fetchMock.mock.calls[0] as [string, RequestInit])[1].body as string,
|
|
307
|
+
);
|
|
308
|
+
expect(body.generationConfig.responseMimeType).toBeUndefined();
|
|
309
|
+
expect(body.generationConfig.responseSchema).toBeUndefined();
|
|
310
|
+
expect(typeof result.content).toBe("string");
|
|
311
|
+
expect(result.content).toBe("not json, just text");
|
|
305
312
|
});
|
|
306
313
|
|
|
307
314
|
it("throws ProviderJsonParseError for non-JSON text in JSON mode", async () => {
|
|
@@ -371,7 +378,7 @@ describe("geminiChat", () => {
|
|
|
371
378
|
expect(init.signal).toBeInstanceOf(AbortSignal);
|
|
372
379
|
});
|
|
373
380
|
|
|
374
|
-
it("
|
|
381
|
+
it("returns undefined usage when accumulated stream reports none", async () => {
|
|
375
382
|
// Stream without usageMetadata
|
|
376
383
|
const events = [
|
|
377
384
|
`data: ${JSON.stringify({
|
|
@@ -382,10 +389,25 @@ describe("geminiChat", () => {
|
|
|
382
389
|
|
|
383
390
|
const result = await geminiChat(baseOptions);
|
|
384
391
|
|
|
392
|
+
expect(result.content).toEqual({ ok: true });
|
|
393
|
+
expect(result.usage).toBeUndefined();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("passes through reported partial usage without coercion", async () => {
|
|
397
|
+
const jsonPayload = { partial: true };
|
|
398
|
+
const events = [
|
|
399
|
+
`data: ${JSON.stringify({
|
|
400
|
+
candidates: [{ content: { parts: [{ text: JSON.stringify(jsonPayload) }], role: "model" }, finishReason: "STOP" }],
|
|
401
|
+
usageMetadata: { promptTokenCount: 7 },
|
|
402
|
+
})}\n\n`,
|
|
403
|
+
];
|
|
404
|
+
fetchMock.mockResolvedValue(mockStreamingResponse(events));
|
|
405
|
+
|
|
406
|
+
const result = await geminiChat(baseOptions);
|
|
407
|
+
|
|
408
|
+
expect(result.content).toEqual(jsonPayload);
|
|
385
409
|
expect(result.usage).toEqual({
|
|
386
|
-
prompt_tokens:
|
|
387
|
-
completion_tokens: 0,
|
|
388
|
-
total_tokens: 0,
|
|
410
|
+
prompt_tokens: 7,
|
|
389
411
|
});
|
|
390
412
|
});
|
|
391
413
|
|
|
@@ -74,6 +74,7 @@ const baseOptions: MoonshotOptions = {
|
|
|
74
74
|
{ role: "system", content: "You are helpful." },
|
|
75
75
|
{ role: "user", content: "Return JSON." },
|
|
76
76
|
],
|
|
77
|
+
responseFormat: "json",
|
|
77
78
|
};
|
|
78
79
|
|
|
79
80
|
describe("moonshotChat", () => {
|
|
@@ -141,6 +142,18 @@ describe("moonshotChat", () => {
|
|
|
141
142
|
expect(body.response_format).toBeUndefined();
|
|
142
143
|
});
|
|
143
144
|
|
|
145
|
+
it("returns plain text content when responseFormat is omitted (AC-13)", async () => {
|
|
146
|
+
const events = makeOpenAiSseEvents(["not json, just text"]);
|
|
147
|
+
fetchMock.mockResolvedValue(mockStreamingResponse(events));
|
|
148
|
+
|
|
149
|
+
const result = await moonshotChat({
|
|
150
|
+
messages: [{ role: "user", content: "Give me json output please." }],
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(typeof result.content).toBe("string");
|
|
154
|
+
expect(result.content).toBe("not json, just text");
|
|
155
|
+
});
|
|
156
|
+
|
|
144
157
|
it("throws before fetch when messages are empty", async () => {
|
|
145
158
|
await expect(
|
|
146
159
|
moonshotChat({ messages: [] }),
|
|
@@ -384,6 +397,20 @@ describe("moonshotChat", () => {
|
|
|
384
397
|
});
|
|
385
398
|
});
|
|
386
399
|
|
|
400
|
+
it("returns undefined usage when accumulated stream reports none", async () => {
|
|
401
|
+
const events = [
|
|
402
|
+
'data: {"choices":[{"delta":{"content":"{\\"ok\\":true}"}}]}\n\n',
|
|
403
|
+
'data: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n\n',
|
|
404
|
+
"data: [DONE]\n\n",
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
fetchMock.mockResolvedValue(mockStreamingResponse(events));
|
|
408
|
+
|
|
409
|
+
const result = await moonshotChat(baseOptions);
|
|
410
|
+
expect(result.content).toEqual({ ok: true });
|
|
411
|
+
expect(result.usage).toBeUndefined();
|
|
412
|
+
});
|
|
413
|
+
|
|
387
414
|
it("retries on timeout then succeeds on second attempt", async () => {
|
|
388
415
|
const events = makeOpenAiSseEvents([JSON.stringify({ ok: true })]);
|
|
389
416
|
|