@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
|
@@ -92,6 +92,9 @@ describe("job adapter", () => {
|
|
|
92
92
|
totalCost: 0,
|
|
93
93
|
totalInputCost: 0,
|
|
94
94
|
totalOutputCost: 0,
|
|
95
|
+
estimatedCost: 0,
|
|
96
|
+
hasEstimated: false,
|
|
97
|
+
unavailableCost: 0,
|
|
95
98
|
});
|
|
96
99
|
});
|
|
97
100
|
|
|
@@ -130,14 +133,14 @@ describe("job adapter", () => {
|
|
|
130
133
|
});
|
|
131
134
|
});
|
|
132
135
|
|
|
133
|
-
it("derives taskCount
|
|
136
|
+
it("derives taskCount as max(configured, actual) when pipelineConfig is available", () => {
|
|
134
137
|
const job = adaptJobSummary({
|
|
135
138
|
jobId: "job-1",
|
|
136
139
|
tasks: { build: { state: "done" }, test: { state: "pending" }, _fileTrack: { state: "pending" } },
|
|
137
140
|
pipelineConfig: { tasks: [{ name: "build" }, { name: "test" }] },
|
|
138
141
|
});
|
|
139
142
|
|
|
140
|
-
expect(job.taskCount).toBe(
|
|
143
|
+
expect(job.taskCount).toBe(3);
|
|
141
144
|
expect(job.doneCount).toBe(1);
|
|
142
145
|
});
|
|
143
146
|
|
|
@@ -227,6 +230,38 @@ describe("job adapter", () => {
|
|
|
227
230
|
expect(job.__warnings).toEqual(["Unsupported task collection shape"]);
|
|
228
231
|
});
|
|
229
232
|
|
|
233
|
+
it("passes readable:false and the full error payload through to the normalized summary", () => {
|
|
234
|
+
const error = {
|
|
235
|
+
code: "INVALID_JSON",
|
|
236
|
+
message: "Status file is unreadable. Repair or delete tasks-status.json for job \"job-1\" to recover.",
|
|
237
|
+
path: "pipeline-data/current/job-1/tasks-status.json",
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const job = adaptJobSummary({
|
|
241
|
+
jobId: "job-1",
|
|
242
|
+
status: "error",
|
|
243
|
+
tasks: {},
|
|
244
|
+
readable: false,
|
|
245
|
+
error,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(job.readable).toBe(false);
|
|
249
|
+
expect(job.error).toEqual(error);
|
|
250
|
+
expect(job.error?.path).toBe(error.path);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("omits readable and error when the input does not provide them", () => {
|
|
254
|
+
const job = adaptJobSummary({
|
|
255
|
+
jobId: "job-1",
|
|
256
|
+
tasks: { build: { state: "done" } },
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
expect(job.readable).toBeUndefined();
|
|
260
|
+
expect(Object.hasOwn(job, "readable")).toBe(false);
|
|
261
|
+
expect(job.error).toBeUndefined();
|
|
262
|
+
expect(Object.hasOwn(job, "error")).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
|
|
230
265
|
it("disables actions for running jobs", () => {
|
|
231
266
|
const job = adaptJobSummary({
|
|
232
267
|
jobId: "job-1",
|
|
@@ -270,4 +305,170 @@ describe("job adapter", () => {
|
|
|
270
305
|
|
|
271
306
|
expect(deriveAllowedActions(job, ["build", "test"])).toEqual({ start: true, restart: true });
|
|
272
307
|
});
|
|
308
|
+
|
|
309
|
+
it("adapts a canonical failed job to status 'failed' and displayCategory 'errors'", () => {
|
|
310
|
+
const job = adaptJobSummary({
|
|
311
|
+
jobId: "job-1",
|
|
312
|
+
status: "failed",
|
|
313
|
+
tasks: { build: { state: "done" } },
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(job.status).toBe("failed");
|
|
317
|
+
expect(job.displayCategory).toBe("errors");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("tolerates legacy status 'error' and emits canonical 'failed'", () => {
|
|
321
|
+
const job = adaptJobSummary({
|
|
322
|
+
jobId: "job-1",
|
|
323
|
+
status: "error",
|
|
324
|
+
tasks: { build: { state: "done" } },
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(job.status).toBe("failed");
|
|
328
|
+
expect(job.displayCategory).toBe("errors");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("uses 'task-0' and 'task-1' fallback ids for unnamed array entries to match the server fixture", () => {
|
|
332
|
+
const tasks = normalizeTasks([
|
|
333
|
+
{ state: "done" },
|
|
334
|
+
{ state: "pending" },
|
|
335
|
+
]);
|
|
336
|
+
|
|
337
|
+
expect(Object.keys(tasks).sort()).toEqual(["task-0", "task-1"]);
|
|
338
|
+
expect(tasks["task-0"]?.name).toBe("task-0");
|
|
339
|
+
expect(tasks["task-0"]?.state).toBe("done");
|
|
340
|
+
expect(tasks["task-1"]?.name).toBe("task-1");
|
|
341
|
+
expect(tasks["task-1"]?.state).toBe("pending");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("uses explicit task names as object keys when array entries are named", () => {
|
|
345
|
+
const tasks = normalizeTasks([
|
|
346
|
+
{ name: "build", state: "done" },
|
|
347
|
+
{ name: "test", state: "pending" },
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
expect(Object.keys(tasks).sort()).toEqual(["build", "test"]);
|
|
351
|
+
expect(tasks["build"]?.name).toBe("build");
|
|
352
|
+
expect(tasks["test"]?.name).toBe("test");
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("preserves gate.kind === 'control_invalid' through adaptJobDetail (AC-11)", () => {
|
|
356
|
+
const job = adaptJobDetail({
|
|
357
|
+
jobId: "job-1",
|
|
358
|
+
status: "waiting",
|
|
359
|
+
gate: {
|
|
360
|
+
afterTask: "review",
|
|
361
|
+
message: "Control file invalid for review: bad directive.",
|
|
362
|
+
requestedAt: "2026-06-19T12:00:00.000Z",
|
|
363
|
+
kind: "control_invalid",
|
|
364
|
+
},
|
|
365
|
+
tasks: {
|
|
366
|
+
review: { state: "done" },
|
|
367
|
+
deploy: { state: "pending" },
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
expect(job.gate?.kind).toBe("control_invalid");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("omits gate.kind when value is not one of the allowed strings (AC-11)", () => {
|
|
375
|
+
for (const invalidKind of [{ junk: true }, "garbage"]) {
|
|
376
|
+
const job = adaptJobDetail({
|
|
377
|
+
jobId: "job-1",
|
|
378
|
+
status: "waiting",
|
|
379
|
+
gate: {
|
|
380
|
+
afterTask: "review",
|
|
381
|
+
message: "Control file invalid.",
|
|
382
|
+
requestedAt: "2026-06-19T12:00:00.000Z",
|
|
383
|
+
kind: invalidKind,
|
|
384
|
+
},
|
|
385
|
+
tasks: {
|
|
386
|
+
review: { state: "done" },
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
expect(job.gate).not.toBeNull();
|
|
391
|
+
expect(job.gate).toBeDefined();
|
|
392
|
+
expect(Object.hasOwn(job.gate!, "kind")).toBe(false);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("preserves estimated cost summary fields through adaptJobSummary", () => {
|
|
397
|
+
const job = adaptJobSummary({
|
|
398
|
+
jobId: "job-1",
|
|
399
|
+
tasks: {},
|
|
400
|
+
costsSummary: {
|
|
401
|
+
totalTokens: 10,
|
|
402
|
+
totalInputTokens: 6,
|
|
403
|
+
totalOutputTokens: 4,
|
|
404
|
+
totalCost: 1,
|
|
405
|
+
totalInputCost: 0.6,
|
|
406
|
+
totalOutputCost: 0.4,
|
|
407
|
+
estimatedCost: 2,
|
|
408
|
+
unavailableCost: 3,
|
|
409
|
+
hasEstimated: true,
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
expect(job.costsSummary).toEqual({
|
|
414
|
+
totalTokens: 10,
|
|
415
|
+
totalInputTokens: 6,
|
|
416
|
+
totalOutputTokens: 4,
|
|
417
|
+
totalCost: 1,
|
|
418
|
+
totalInputCost: 0.6,
|
|
419
|
+
totalOutputCost: 0.4,
|
|
420
|
+
estimatedCost: 2,
|
|
421
|
+
unavailableCost: 3,
|
|
422
|
+
hasEstimated: true,
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("preserves source and failed markers in cost breakdown through adaptJobDetail", () => {
|
|
427
|
+
const detail = adaptJobDetail({
|
|
428
|
+
jobId: "job-1",
|
|
429
|
+
tasks: { build: { state: "done" } },
|
|
430
|
+
costs: {
|
|
431
|
+
build: {
|
|
432
|
+
inputTokens: 1,
|
|
433
|
+
outputTokens: 2,
|
|
434
|
+
inputCost: 3,
|
|
435
|
+
outputCost: 4,
|
|
436
|
+
totalCost: 7,
|
|
437
|
+
source: "estimated",
|
|
438
|
+
failed: true,
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
expect(detail.costs?.build).toEqual({
|
|
444
|
+
inputTokens: 1,
|
|
445
|
+
outputTokens: 2,
|
|
446
|
+
inputCost: 3,
|
|
447
|
+
outputCost: 4,
|
|
448
|
+
totalCost: 7,
|
|
449
|
+
source: "estimated",
|
|
450
|
+
failed: true,
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("drops unknown source values in cost breakdown", () => {
|
|
455
|
+
const detail = adaptJobDetail({
|
|
456
|
+
jobId: "job-1",
|
|
457
|
+
tasks: { build: { state: "done" } },
|
|
458
|
+
costs: {
|
|
459
|
+
build: {
|
|
460
|
+
inputTokens: 1,
|
|
461
|
+
outputTokens: 2,
|
|
462
|
+
inputCost: 3,
|
|
463
|
+
outputCost: 4,
|
|
464
|
+
totalCost: 7,
|
|
465
|
+
source: "bogus",
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
expect(detail.costs?.build).toBeDefined();
|
|
471
|
+
expect(Object.hasOwn(detail.costs!.build!, "source")).toBe(false);
|
|
472
|
+
expect(Object.hasOwn(detail.costs!.build!, "failed")).toBe(false);
|
|
473
|
+
});
|
|
273
474
|
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { nextLoadState } from "../load-state";
|
|
4
|
+
import type { ApiError } from "../types";
|
|
5
|
+
|
|
6
|
+
function makeError(overrides: Partial<ApiError> = {}): ApiError {
|
|
7
|
+
return {
|
|
8
|
+
code: "unknown_error",
|
|
9
|
+
message: "boom",
|
|
10
|
+
status: 500,
|
|
11
|
+
...overrides,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("nextLoadState", () => {
|
|
16
|
+
it("stores data and clears any prior error on a successful read", () => {
|
|
17
|
+
const prev = { data: null, error: makeError({ message: "stale" }) } as const;
|
|
18
|
+
const result = { ok: true, data: "fresh" } as const;
|
|
19
|
+
|
|
20
|
+
const next = nextLoadState(prev, result);
|
|
21
|
+
|
|
22
|
+
expect(next).toEqual({ data: "fresh", error: null });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("captures a scratch error when there is no prior data", () => {
|
|
26
|
+
const prev = { data: null, error: null } as const;
|
|
27
|
+
const result = { ok: false, error: makeError({ message: "offline" }) } as const;
|
|
28
|
+
|
|
29
|
+
const next = nextLoadState(prev, result);
|
|
30
|
+
|
|
31
|
+
expect(next).toEqual({ data: null, error: result.error });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("keeps prior data when a refetch fails", () => {
|
|
35
|
+
const prev = { data: ["job-1"], error: null } as const;
|
|
36
|
+
const result = { ok: false, error: makeError({ message: "refetch failed" }) } as const;
|
|
37
|
+
|
|
38
|
+
const next = nextLoadState(prev, result);
|
|
39
|
+
|
|
40
|
+
expect(next).toEqual({ data: ["job-1"], error: result.error });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("replaces a prior error on a new error while keeping prior data", () => {
|
|
44
|
+
const prev = { data: { id: "job-1" }, error: makeError({ code: "network_error" }) } as const;
|
|
45
|
+
const result = { ok: false, error: makeError({ code: "unknown_error", message: "later" }) } as const;
|
|
46
|
+
|
|
47
|
+
const next = nextLoadState(prev, result);
|
|
48
|
+
|
|
49
|
+
expect(next).toEqual({ data: { id: "job-1" }, error: result.error });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("replaces a prior error with the latest error when no data has loaded", () => {
|
|
53
|
+
const prev = { data: null, error: makeError({ message: "first" }) } as const;
|
|
54
|
+
const result = { ok: false, error: makeError({ message: "second" }) } as const;
|
|
55
|
+
|
|
56
|
+
const next = nextLoadState(prev, result);
|
|
57
|
+
|
|
58
|
+
expect(next).toEqual({ data: null, error: result.error });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns a new object so React state updates are referentially detectable", () => {
|
|
62
|
+
const prev = { data: null, error: null } as const;
|
|
63
|
+
const result = { ok: true, data: 42 } as const;
|
|
64
|
+
|
|
65
|
+
const next = nextLoadState(prev, result);
|
|
66
|
+
|
|
67
|
+
expect(next).not.toBe(prev);
|
|
68
|
+
expect(next).not.toBe(result);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { describe, expect, it } from "
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
4
|
AllowedActions,
|
|
5
5
|
AnalysisProgressState,
|
|
6
6
|
ApiError,
|
|
7
7
|
ConnectionStatus,
|
|
8
|
+
CostsSummary,
|
|
8
9
|
NormalizedJobSummary,
|
|
9
10
|
SseEventType,
|
|
10
11
|
} from "../types";
|
|
@@ -51,14 +52,76 @@ describe("ui client types", () => {
|
|
|
51
52
|
} satisfies AnalysisProgressState;
|
|
52
53
|
|
|
53
54
|
const connection: ConnectionStatus = "connected";
|
|
54
|
-
const event: SseEventType = "job:updated";
|
|
55
55
|
const actions = { start: true, restart: false } satisfies AllowedActions;
|
|
56
56
|
|
|
57
57
|
expect(error.status).toBe(404);
|
|
58
58
|
expect(job.jobId).toBe("job-1");
|
|
59
59
|
expect(analysis.status).toBe("idle");
|
|
60
60
|
expect(connection).toBe("connected");
|
|
61
|
-
expect(event).toBe("job:updated");
|
|
62
61
|
expect(actions.start).toBe(true);
|
|
63
62
|
});
|
|
64
63
|
});
|
|
64
|
+
|
|
65
|
+
describe("SseEventType", () => {
|
|
66
|
+
it("accepts every contract event name", () => {
|
|
67
|
+
const contractNames: SseEventType[] = [
|
|
68
|
+
"state:change",
|
|
69
|
+
"state:summary",
|
|
70
|
+
"job:created",
|
|
71
|
+
"job:updated",
|
|
72
|
+
"job:removed",
|
|
73
|
+
"heartbeat",
|
|
74
|
+
"seed:uploaded",
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
expect(contractNames).toHaveLength(7);
|
|
78
|
+
expect(new Set(contractNames).size).toBe(7);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("does not include the legacy task:updated name", () => {
|
|
82
|
+
const contractNames: SseEventType[] = [
|
|
83
|
+
"state:change",
|
|
84
|
+
"state:summary",
|
|
85
|
+
"job:created",
|
|
86
|
+
"job:updated",
|
|
87
|
+
"job:removed",
|
|
88
|
+
"heartbeat",
|
|
89
|
+
"seed:uploaded",
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
expect(contractNames).not.toContain("task:updated");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("rejects legacy browser event names that are not in the contract", () => {
|
|
96
|
+
// @ts-expect-error — "state" is a legacy event name not in the SseEvent contract.
|
|
97
|
+
const legacyState: SseEventType = "state";
|
|
98
|
+
// @ts-expect-error — "message" is a legacy event name not in the SseEvent contract.
|
|
99
|
+
const legacyMessage: SseEventType = "message";
|
|
100
|
+
// @ts-expect-error — "status:changed" is a legacy event name not in the SseEvent contract.
|
|
101
|
+
const legacyStatusChanged: SseEventType = "status:changed";
|
|
102
|
+
// @ts-expect-error — "task:updated" is the dead cross-process event; removed in this spec.
|
|
103
|
+
const legacyTaskUpdated: SseEventType = "task:updated";
|
|
104
|
+
|
|
105
|
+
expect([legacyState, legacyMessage, legacyStatusChanged, legacyTaskUpdated]).toHaveLength(4);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("CostsSummary estimate contract", () => {
|
|
110
|
+
it("supports the estimated-cost and unavailable-cost fields", () => {
|
|
111
|
+
const summary: CostsSummary = {
|
|
112
|
+
totalTokens: 1000,
|
|
113
|
+
totalInputTokens: 800,
|
|
114
|
+
totalOutputTokens: 200,
|
|
115
|
+
totalCost: 0.5,
|
|
116
|
+
totalInputCost: 0.4,
|
|
117
|
+
totalOutputCost: 0.1,
|
|
118
|
+
estimatedCost: 0.12,
|
|
119
|
+
hasEstimated: true,
|
|
120
|
+
unavailableCost: 0,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
expect(summary.estimatedCost).toBe(0.12);
|
|
124
|
+
expect(summary.hasEstimated).toBe(true);
|
|
125
|
+
expect(summary.unavailableCost).toBe(0);
|
|
126
|
+
});
|
|
127
|
+
});
|