@ryanfw/prompt-orchestration-pipeline 1.3.3 → 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 +14 -4
- package/package.json +1 -4
- package/src/api/__tests__/index.test.ts +144 -41
- package/src/api/index.ts +15 -83
- 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/paths.ts +11 -2
- package/src/config/pipeline-registry.ts +258 -0
- package/src/config/sse-events.ts +122 -0
- package/src/core/__tests__/agent-step.test.ts +115 -0
- package/src/core/__tests__/config.test.ts +517 -107
- package/src/core/__tests__/core-boot-resolution.test.ts +181 -0
- package/src/core/__tests__/job-concurrency.test.ts +200 -0
- package/src/core/__tests__/job-submission.test.ts +396 -0
- package/src/core/__tests__/job-view.test.ts +485 -3
- 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 +373 -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 +45 -0
- package/src/core/__tests__/seed-naming.test.ts +57 -0
- package/src/core/__tests__/single-derivation.test.ts +1 -1
- 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 +127 -234
- package/src/core/control.ts +3 -0
- package/src/core/job-concurrency.ts +50 -19
- package/src/core/job-submission.ts +133 -0
- package/src/core/job-view.ts +162 -18
- 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 +124 -70
- package/src/core/pipeline-runner.ts +85 -53
- package/src/core/redact.ts +40 -0
- package/src/core/runner-liveness.ts +8 -4
- package/src/core/seed-naming.ts +9 -0
- package/src/core/status-writer.ts +3 -28
- 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 +125 -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 +31 -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 +14 -20
- package/src/ui/components/DAGGrid.tsx +6 -1
- package/src/ui/components/JobDetail.tsx +12 -4
- package/src/ui/components/JobTable.tsx +13 -2
- 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 +83 -0
- 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 +33 -15
- package/src/ui/dist/assets/{index-L6cvsCAx.js → index-DN3-zvtP.js} +4299 -251
- 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__/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 +455 -1
- package/src/ui/server/__tests__/job-endpoints.test.ts +43 -1
- 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 +39 -0
- package/src/ui/server/config-bridge-node.ts +8 -26
- 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__/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/job-control-endpoints.ts +122 -73
- package/src/ui/server/endpoints/job-endpoints.ts +1 -0
- 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 +70 -15
- package/src/ui/state/__tests__/types.test.ts +6 -0
- package/src/ui/state/snapshot.ts +1 -1
- package/src/ui/state/transformers/__tests__/list-transformer.test.ts +42 -0
- package/src/ui/state/transformers/__tests__/status-transformer.test.ts +45 -3
- package/src/ui/state/transformers/list-transformer.ts +6 -0
- package/src/ui/state/types.ts +6 -1
- 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-L6cvsCAx.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
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { APIJob } from "../../ui/state/types.ts";
|
|
3
|
+
import {
|
|
4
|
+
DOCUMENTED_SSE_EVENTS,
|
|
5
|
+
emitSseEvent,
|
|
6
|
+
parseSseEvent,
|
|
7
|
+
SseEvent,
|
|
8
|
+
type JobWire,
|
|
9
|
+
type SseEventType,
|
|
10
|
+
} from "../sse-events.ts";
|
|
11
|
+
|
|
12
|
+
type _AssertJobWireAssignableToAPIJob = JobWire extends APIJob ? true : false;
|
|
13
|
+
const _assertJobWireAssignable: _AssertJobWireAssignableToAPIJob = true;
|
|
14
|
+
|
|
15
|
+
describe("SseEvent", () => {
|
|
16
|
+
describe("parseSseEvent round-trips each variant", () => {
|
|
17
|
+
it("parses state:change with required jobId and path", () => {
|
|
18
|
+
// Arrange
|
|
19
|
+
const data = { jobId: "job-1", path: "pipeline-data/current/job-1/tasks-status.json" };
|
|
20
|
+
|
|
21
|
+
// Act
|
|
22
|
+
const event = parseSseEvent("state:change", data);
|
|
23
|
+
|
|
24
|
+
// Assert
|
|
25
|
+
expect(event).not.toBeNull();
|
|
26
|
+
expect(event?.type).toBe("state:change");
|
|
27
|
+
expect(event?.data).toEqual(data);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("parses state:summary with changeCount", () => {
|
|
31
|
+
// Arrange
|
|
32
|
+
const data = { changeCount: 7 };
|
|
33
|
+
|
|
34
|
+
// Act
|
|
35
|
+
const event = parseSseEvent("state:summary", data);
|
|
36
|
+
|
|
37
|
+
// Assert
|
|
38
|
+
expect(event).not.toBeNull();
|
|
39
|
+
expect(event?.type).toBe("state:summary");
|
|
40
|
+
expect(event?.data).toEqual(data);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("parses job:created with a complete JobWire payload", () => {
|
|
44
|
+
// Arrange
|
|
45
|
+
const data = {
|
|
46
|
+
jobId: "job-abc",
|
|
47
|
+
title: "My Job",
|
|
48
|
+
status: "running",
|
|
49
|
+
progress: 42,
|
|
50
|
+
createdAt: "2026-06-19T10:00:00.000Z",
|
|
51
|
+
updatedAt: "2026-06-19T10:05:00.000Z",
|
|
52
|
+
location: "current",
|
|
53
|
+
tasks: { research: { state: "done" } },
|
|
54
|
+
costsSummary: {
|
|
55
|
+
totalTokens: 100,
|
|
56
|
+
totalInputTokens: 60,
|
|
57
|
+
totalOutputTokens: 40,
|
|
58
|
+
totalCost: 0.5,
|
|
59
|
+
totalInputCost: 0.3,
|
|
60
|
+
totalOutputCost: 0.2,
|
|
61
|
+
estimatedCost: 0,
|
|
62
|
+
hasEstimated: false,
|
|
63
|
+
unavailableCost: 0,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Act
|
|
68
|
+
const event = parseSseEvent("job:created", data);
|
|
69
|
+
|
|
70
|
+
// Assert
|
|
71
|
+
expect(event).not.toBeNull();
|
|
72
|
+
expect(event?.type).toBe("job:created");
|
|
73
|
+
expect(event?.data).toEqual(data);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("parses job:updated with a complete JobWire payload", () => {
|
|
77
|
+
// Arrange
|
|
78
|
+
const data = {
|
|
79
|
+
jobId: "job-abc",
|
|
80
|
+
title: "My Job",
|
|
81
|
+
status: "complete",
|
|
82
|
+
progress: 100,
|
|
83
|
+
createdAt: "2026-06-19T10:00:00.000Z",
|
|
84
|
+
updatedAt: "2026-06-19T10:30:00.000Z",
|
|
85
|
+
location: "current",
|
|
86
|
+
tasks: {},
|
|
87
|
+
costsSummary: {
|
|
88
|
+
totalTokens: 200,
|
|
89
|
+
totalInputTokens: 120,
|
|
90
|
+
totalOutputTokens: 80,
|
|
91
|
+
totalCost: 1.0,
|
|
92
|
+
totalInputCost: 0.6,
|
|
93
|
+
totalOutputCost: 0.4,
|
|
94
|
+
estimatedCost: 0,
|
|
95
|
+
hasEstimated: false,
|
|
96
|
+
unavailableCost: 0,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Act
|
|
101
|
+
const event = parseSseEvent("job:updated", data);
|
|
102
|
+
|
|
103
|
+
// Assert
|
|
104
|
+
expect(event).not.toBeNull();
|
|
105
|
+
expect(event?.type).toBe("job:updated");
|
|
106
|
+
expect(event?.data).toEqual(data);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("parses job:removed with required jobId", () => {
|
|
110
|
+
// Arrange
|
|
111
|
+
const data = { jobId: "job-removed" };
|
|
112
|
+
|
|
113
|
+
// Act
|
|
114
|
+
const event = parseSseEvent("job:removed", data);
|
|
115
|
+
|
|
116
|
+
// Assert
|
|
117
|
+
expect(event).not.toBeNull();
|
|
118
|
+
expect(event?.type).toBe("job:removed");
|
|
119
|
+
expect(event?.data).toEqual(data);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("parses heartbeat with ok and timestamp", () => {
|
|
123
|
+
// Arrange
|
|
124
|
+
const data = { ok: true, timestamp: "2026-06-19T11:00:00.000Z" };
|
|
125
|
+
|
|
126
|
+
// Act
|
|
127
|
+
const event = parseSseEvent("heartbeat", data);
|
|
128
|
+
|
|
129
|
+
// Assert
|
|
130
|
+
expect(event).not.toBeNull();
|
|
131
|
+
expect(event?.type).toBe("heartbeat");
|
|
132
|
+
expect(event?.data).toEqual(data);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("parses seed:uploaded with an empty payload", () => {
|
|
136
|
+
// Arrange
|
|
137
|
+
const data = {};
|
|
138
|
+
|
|
139
|
+
// Act
|
|
140
|
+
const event = parseSseEvent("seed:uploaded", data);
|
|
141
|
+
|
|
142
|
+
// Assert
|
|
143
|
+
expect(event).not.toBeNull();
|
|
144
|
+
expect(event?.type).toBe("seed:uploaded");
|
|
145
|
+
expect(event?.data).toEqual({});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("parseSseEvent rejects unknown or invalid input", () => {
|
|
150
|
+
it("returns null for an unknown type string", () => {
|
|
151
|
+
// Arrange / Act
|
|
152
|
+
const event = parseSseEvent("task:updated", { jobId: "job-1" });
|
|
153
|
+
|
|
154
|
+
// Assert
|
|
155
|
+
expect(event).toBeNull();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("returns null for the empty string", () => {
|
|
159
|
+
// Arrange / Act
|
|
160
|
+
const event = parseSseEvent("", { jobId: "job-1" });
|
|
161
|
+
|
|
162
|
+
// Assert
|
|
163
|
+
expect(event).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("returns null for a non-string type", () => {
|
|
167
|
+
// Arrange / Act
|
|
168
|
+
const event = parseSseEvent(undefined as unknown as string, { jobId: "job-1" });
|
|
169
|
+
|
|
170
|
+
// Assert
|
|
171
|
+
expect(event).toBeNull();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("returns null when state:change is missing jobId", () => {
|
|
175
|
+
// Arrange
|
|
176
|
+
const data = { path: "pipeline-data/current/job-1/tasks-status.json" };
|
|
177
|
+
|
|
178
|
+
// Act
|
|
179
|
+
const event = parseSseEvent("state:change", data);
|
|
180
|
+
|
|
181
|
+
// Assert
|
|
182
|
+
expect(event).toBeNull();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("returns null when job:created is missing jobId", () => {
|
|
186
|
+
// Arrange
|
|
187
|
+
const data = {
|
|
188
|
+
title: "x",
|
|
189
|
+
status: "pending",
|
|
190
|
+
progress: 0,
|
|
191
|
+
createdAt: null,
|
|
192
|
+
updatedAt: null,
|
|
193
|
+
location: null,
|
|
194
|
+
tasks: {},
|
|
195
|
+
costsSummary: {
|
|
196
|
+
totalTokens: 0,
|
|
197
|
+
totalInputTokens: 0,
|
|
198
|
+
totalOutputTokens: 0,
|
|
199
|
+
totalCost: 0,
|
|
200
|
+
totalInputCost: 0,
|
|
201
|
+
totalOutputCost: 0,
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Act
|
|
206
|
+
const event = parseSseEvent("job:created", data);
|
|
207
|
+
|
|
208
|
+
// Assert
|
|
209
|
+
expect(event).toBeNull();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("returns null when job:updated is missing jobId", () => {
|
|
213
|
+
// Arrange
|
|
214
|
+
const data = {
|
|
215
|
+
title: "x",
|
|
216
|
+
status: "running",
|
|
217
|
+
progress: 50,
|
|
218
|
+
createdAt: null,
|
|
219
|
+
updatedAt: null,
|
|
220
|
+
location: null,
|
|
221
|
+
tasks: {},
|
|
222
|
+
costsSummary: {
|
|
223
|
+
totalTokens: 0,
|
|
224
|
+
totalInputTokens: 0,
|
|
225
|
+
totalOutputTokens: 0,
|
|
226
|
+
totalCost: 0,
|
|
227
|
+
totalInputCost: 0,
|
|
228
|
+
totalOutputCost: 0,
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Act
|
|
233
|
+
const event = parseSseEvent("job:updated", data);
|
|
234
|
+
|
|
235
|
+
// Assert
|
|
236
|
+
expect(event).toBeNull();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("returns null when job:removed is missing jobId", () => {
|
|
240
|
+
// Arrange
|
|
241
|
+
const data = {};
|
|
242
|
+
|
|
243
|
+
// Act
|
|
244
|
+
const event = parseSseEvent("job:removed", data);
|
|
245
|
+
|
|
246
|
+
// Assert
|
|
247
|
+
expect(event).toBeNull();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("returns null when heartbeat has a non-boolean ok", () => {
|
|
251
|
+
// Arrange
|
|
252
|
+
const data = { ok: "yes", timestamp: "2026-06-19T11:00:00.000Z" };
|
|
253
|
+
|
|
254
|
+
// Act
|
|
255
|
+
const event = parseSseEvent("heartbeat", data);
|
|
256
|
+
|
|
257
|
+
// Assert
|
|
258
|
+
expect(event).toBeNull();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("returns null when job:created has a mistyped status", () => {
|
|
262
|
+
// Arrange
|
|
263
|
+
const data = {
|
|
264
|
+
jobId: "job-1",
|
|
265
|
+
title: "x",
|
|
266
|
+
status: "done",
|
|
267
|
+
progress: 0,
|
|
268
|
+
createdAt: null,
|
|
269
|
+
updatedAt: null,
|
|
270
|
+
location: null,
|
|
271
|
+
tasks: {},
|
|
272
|
+
costsSummary: {
|
|
273
|
+
totalTokens: 0,
|
|
274
|
+
totalInputTokens: 0,
|
|
275
|
+
totalOutputTokens: 0,
|
|
276
|
+
totalCost: 0,
|
|
277
|
+
totalInputCost: 0,
|
|
278
|
+
totalOutputCost: 0,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Act
|
|
283
|
+
const event = parseSseEvent("job:created", data);
|
|
284
|
+
|
|
285
|
+
// Assert
|
|
286
|
+
expect(event).toBeNull();
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe("forward-compat: extra unknown fields are preserved (passthrough)", () => {
|
|
291
|
+
it("state:change accepts and preserves unknown fields alongside jobId and path", () => {
|
|
292
|
+
// Arrange
|
|
293
|
+
const data = {
|
|
294
|
+
jobId: "job-1",
|
|
295
|
+
path: "pipeline-data/current/job-1/tasks-status.json",
|
|
296
|
+
lifecycle: "current",
|
|
297
|
+
unknownField: 42,
|
|
298
|
+
nested: { foo: "bar" },
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// Act
|
|
302
|
+
const event = parseSseEvent("state:change", data);
|
|
303
|
+
|
|
304
|
+
// Assert
|
|
305
|
+
expect(event).not.toBeNull();
|
|
306
|
+
expect(event?.data).toEqual(data);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("job:updated accepts and preserves unknown fields beyond the schema", () => {
|
|
310
|
+
// Arrange
|
|
311
|
+
const data = {
|
|
312
|
+
jobId: "job-1",
|
|
313
|
+
title: "x",
|
|
314
|
+
status: "running",
|
|
315
|
+
progress: 10,
|
|
316
|
+
createdAt: null,
|
|
317
|
+
updatedAt: null,
|
|
318
|
+
location: null,
|
|
319
|
+
tasks: {},
|
|
320
|
+
costsSummary: {
|
|
321
|
+
totalTokens: 0,
|
|
322
|
+
totalInputTokens: 0,
|
|
323
|
+
totalOutputTokens: 0,
|
|
324
|
+
totalCost: 0,
|
|
325
|
+
totalInputCost: 0,
|
|
326
|
+
totalOutputCost: 0,
|
|
327
|
+
},
|
|
328
|
+
futureField: "ok",
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// Act
|
|
332
|
+
const event = parseSseEvent("job:updated", data);
|
|
333
|
+
|
|
334
|
+
// Assert
|
|
335
|
+
expect(event).not.toBeNull();
|
|
336
|
+
expect((event?.data as Record<string, unknown>).futureField).toBe("ok");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("heartbeat accepts extra fields beyond ok and timestamp", () => {
|
|
340
|
+
// Arrange
|
|
341
|
+
const data = {
|
|
342
|
+
ok: true,
|
|
343
|
+
timestamp: "2026-06-19T11:00:00.000Z",
|
|
344
|
+
serverVersion: "1.2.3",
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Act
|
|
348
|
+
const event = parseSseEvent("heartbeat", data);
|
|
349
|
+
|
|
350
|
+
// Assert
|
|
351
|
+
expect(event).not.toBeNull();
|
|
352
|
+
expect((event?.data as Record<string, unknown>).serverVersion).toBe("1.2.3");
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("seed:uploaded accepts any payload shape", () => {
|
|
356
|
+
// Arrange
|
|
357
|
+
const data = { filename: "seed.json", size: 1234 };
|
|
358
|
+
|
|
359
|
+
// Act
|
|
360
|
+
const event = parseSseEvent("seed:uploaded", data);
|
|
361
|
+
|
|
362
|
+
// Assert
|
|
363
|
+
expect(event).not.toBeNull();
|
|
364
|
+
expect(event?.data).toEqual(data);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe("emitSseEvent", () => {
|
|
370
|
+
it("broadcasts a validated event with type and data", () => {
|
|
371
|
+
// Arrange
|
|
372
|
+
const broadcasts: Array<{ type: string; data: unknown }> = [];
|
|
373
|
+
const registry = {
|
|
374
|
+
broadcast(type: string, data: unknown) {
|
|
375
|
+
broadcasts.push({ type, data });
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
const event = {
|
|
379
|
+
type: "heartbeat" as const,
|
|
380
|
+
data: { ok: true, timestamp: "2026-06-19T11:00:00.000Z" },
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// Act
|
|
384
|
+
emitSseEvent(registry, event);
|
|
385
|
+
|
|
386
|
+
// Assert
|
|
387
|
+
expect(broadcasts).toHaveLength(1);
|
|
388
|
+
expect(broadcasts[0]?.type).toBe("heartbeat");
|
|
389
|
+
expect(broadcasts[0]?.data).toEqual(event.data);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("throws and does not broadcast when the event fails re-validation", () => {
|
|
393
|
+
// Arrange
|
|
394
|
+
const broadcasts: Array<{ type: string; data: unknown }> = [];
|
|
395
|
+
const registry = {
|
|
396
|
+
broadcast(type: string, data: unknown) {
|
|
397
|
+
broadcasts.push({ type, data });
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
const badEvent = {
|
|
401
|
+
type: "heartbeat" as const,
|
|
402
|
+
data: { ok: "not-a-boolean", timestamp: "2026-06-19T11:00:00.000Z" },
|
|
403
|
+
} as unknown as Parameters<typeof emitSseEvent>[1];
|
|
404
|
+
|
|
405
|
+
// Act + Assert
|
|
406
|
+
expect(() => emitSseEvent(registry, badEvent)).toThrow(/heartbeat/);
|
|
407
|
+
expect(broadcasts).toHaveLength(0);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe("SseEvent discriminated union", () => {
|
|
412
|
+
it("SseEvent.parse accepts a state:change payload", () => {
|
|
413
|
+
// Arrange
|
|
414
|
+
const payload = {
|
|
415
|
+
type: "state:change",
|
|
416
|
+
data: { jobId: "j", path: "/p" },
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// Act
|
|
420
|
+
const result = SseEvent.safeParse(payload);
|
|
421
|
+
|
|
422
|
+
// Assert
|
|
423
|
+
expect(result.success).toBe(true);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("SseEvent.parse rejects a payload whose data does not match the discriminator", () => {
|
|
427
|
+
// Arrange
|
|
428
|
+
const payload = {
|
|
429
|
+
type: "state:change",
|
|
430
|
+
data: { jobId: 123, path: "/p" },
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// Act
|
|
434
|
+
const result = SseEvent.safeParse(payload);
|
|
435
|
+
|
|
436
|
+
// Assert
|
|
437
|
+
expect(result.success).toBe(false);
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
describe("DOCUMENTED_SSE_EVENTS", () => {
|
|
442
|
+
it("lists exactly the seven browser-facing event names", () => {
|
|
443
|
+
// Arrange / Act
|
|
444
|
+
const names = [...DOCUMENTED_SSE_EVENTS].sort();
|
|
445
|
+
|
|
446
|
+
// Assert
|
|
447
|
+
expect(names).toEqual([
|
|
448
|
+
"heartbeat",
|
|
449
|
+
"job:created",
|
|
450
|
+
"job:removed",
|
|
451
|
+
"job:updated",
|
|
452
|
+
"seed:uploaded",
|
|
453
|
+
"state:change",
|
|
454
|
+
"state:summary",
|
|
455
|
+
]);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("does not list task:updated (legacy dead event)", () => {
|
|
459
|
+
// Arrange / Act / Assert
|
|
460
|
+
expect(DOCUMENTED_SSE_EVENTS).not.toContain("task:updated");
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it("SseEventType is assignable from each documented name", () => {
|
|
464
|
+
// Arrange
|
|
465
|
+
const sample: SseEventType = DOCUMENTED_SSE_EVENTS[0];
|
|
466
|
+
|
|
467
|
+
// Assert — this assignment compiles only if every name is a valid SseEventType
|
|
468
|
+
expect(typeof sample).toBe("string");
|
|
469
|
+
});
|
|
470
|
+
});
|
package/src/config/paths.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
type JobLocationValue = "pending" | "current" | "complete" | "rejected";
|
|
3
4
|
|
|
4
5
|
export interface PipelinePaths {
|
|
5
6
|
readonly pending: string;
|
|
@@ -57,3 +58,11 @@ export function getJobPipelinePath(
|
|
|
57
58
|
): string {
|
|
58
59
|
return join(getJobDirectoryPath(baseDir, jobId, location), "pipeline.json");
|
|
59
60
|
}
|
|
61
|
+
|
|
62
|
+
export function resolveWorkspaceRoot(explicit?: string): string {
|
|
63
|
+
const raw = explicit ?? process.env["PO_ROOT"];
|
|
64
|
+
if (!raw) {
|
|
65
|
+
throw new Error("Workspace root is required: pass --root or set PO_ROOT");
|
|
66
|
+
}
|
|
67
|
+
return resolve(raw);
|
|
68
|
+
}
|