@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
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach, mock } from "bun:test";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
existsSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import {
|
|
14
|
+
REGISTRY_VERSION,
|
|
15
|
+
RegistryError,
|
|
16
|
+
registryFilePath,
|
|
17
|
+
readRegistrySync,
|
|
18
|
+
resolvePipelineEntry,
|
|
19
|
+
resolvePipelineSync,
|
|
20
|
+
resolveAllPipelinesSync,
|
|
21
|
+
resolveTaskRegistryPath,
|
|
22
|
+
writeRegistry,
|
|
23
|
+
registerPipeline,
|
|
24
|
+
} from "../pipeline-registry";
|
|
25
|
+
import type { PipelineRegistry } from "../pipeline-registry";
|
|
26
|
+
|
|
27
|
+
let renameShouldFail = false;
|
|
28
|
+
|
|
29
|
+
mock.module("node:fs/promises", () => {
|
|
30
|
+
const actual = require("node:fs/promises");
|
|
31
|
+
return {
|
|
32
|
+
...actual,
|
|
33
|
+
rename: (...args: Parameters<typeof actual.rename>) => {
|
|
34
|
+
if (renameShouldFail) {
|
|
35
|
+
return Promise.reject(new Error("forced rename failure"));
|
|
36
|
+
}
|
|
37
|
+
return actual.rename(...args);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("REGISTRY_VERSION", () => {
|
|
43
|
+
it("equals 1", () => {
|
|
44
|
+
expect(REGISTRY_VERSION).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("RegistryError", () => {
|
|
49
|
+
it("is an instance of Error", () => {
|
|
50
|
+
expect(new RegistryError("boom")).toBeInstanceOf(Error);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("registryFilePath", () => {
|
|
55
|
+
it("returns join(root, 'pipeline-config', 'registry.json')", () => {
|
|
56
|
+
expect(registryFilePath("/data")).toBe("/data/pipeline-config/registry.json");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("works with a nested root", () => {
|
|
60
|
+
expect(registryFilePath("/home/user/project")).toBe(
|
|
61
|
+
"/home/user/project/pipeline-config/registry.json",
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("resolvePipelineEntry", () => {
|
|
67
|
+
const baseEntry = { name: "Content Gen", description: "Generates content" };
|
|
68
|
+
|
|
69
|
+
describe("defaults", () => {
|
|
70
|
+
it("defaults configDir to join(root, 'pipeline-config', slug) and tasksDir to join(configDir, 'tasks')", () => {
|
|
71
|
+
const { configDir, tasksDir } = resolvePipelineEntry(
|
|
72
|
+
"/work",
|
|
73
|
+
"content-gen",
|
|
74
|
+
baseEntry,
|
|
75
|
+
);
|
|
76
|
+
expect(configDir).toBe("/work/pipeline-config/content-gen");
|
|
77
|
+
expect(tasksDir).toBe("/work/pipeline-config/content-gen/tasks");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("defaults tasksDir off an explicit absolute configDir", () => {
|
|
81
|
+
const { configDir, tasksDir } = resolvePipelineEntry("/work", "s", {
|
|
82
|
+
...baseEntry,
|
|
83
|
+
configDir: "/abs/cfg",
|
|
84
|
+
});
|
|
85
|
+
expect(configDir).toBe("/abs/cfg");
|
|
86
|
+
expect(tasksDir).toBe("/abs/cfg/tasks");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("relative values resolve against root", () => {
|
|
91
|
+
it("joins relative configDir and tasksDir under root", () => {
|
|
92
|
+
const { configDir, tasksDir } = resolvePipelineEntry("/work", "s", {
|
|
93
|
+
...baseEntry,
|
|
94
|
+
configDir: "custom/cfg",
|
|
95
|
+
tasksDir: "custom/tasks",
|
|
96
|
+
});
|
|
97
|
+
expect(configDir).toBe("/work/custom/cfg");
|
|
98
|
+
expect(tasksDir).toBe("/work/custom/tasks");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("absolute values pass through", () => {
|
|
103
|
+
it("leaves absolute configDir and tasksDir unchanged", () => {
|
|
104
|
+
const { configDir, tasksDir } = resolvePipelineEntry("/work", "s", {
|
|
105
|
+
...baseEntry,
|
|
106
|
+
configDir: "/abs/cfg",
|
|
107
|
+
tasksDir: "/abs/tasks",
|
|
108
|
+
});
|
|
109
|
+
expect(configDir).toBe("/abs/cfg");
|
|
110
|
+
expect(tasksDir).toBe("/abs/tasks");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("mixed relative and absolute", () => {
|
|
115
|
+
it("resolves each field independently", () => {
|
|
116
|
+
const { configDir, tasksDir } = resolvePipelineEntry("/work", "s", {
|
|
117
|
+
...baseEntry,
|
|
118
|
+
configDir: "rel/cfg",
|
|
119
|
+
tasksDir: "/abs/tasks",
|
|
120
|
+
});
|
|
121
|
+
expect(configDir).toBe("/work/rel/cfg");
|
|
122
|
+
expect(tasksDir).toBe("/abs/tasks");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("readRegistrySync", () => {
|
|
128
|
+
let tmp: string;
|
|
129
|
+
|
|
130
|
+
afterEach(() => {
|
|
131
|
+
if (tmp) rmSync(tmp, { recursive: true, force: true });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const setupTmp = (): string => {
|
|
135
|
+
tmp = mkdtempSync(join(tmpdir(), "pop-reg-"));
|
|
136
|
+
mkdirSync(join(tmp, "pipeline-config"), { recursive: true });
|
|
137
|
+
return tmp;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const writeRegistry = (root: string, contents: string): string => {
|
|
141
|
+
const filePath = registryFilePath(root);
|
|
142
|
+
writeFileSync(filePath, contents);
|
|
143
|
+
return filePath;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
describe("absent file", () => {
|
|
147
|
+
it("returns a fresh v1 empty registry", () => {
|
|
148
|
+
tmp = mkdtempSync(join(tmpdir(), "pop-reg-"));
|
|
149
|
+
const result = readRegistrySync(tmp);
|
|
150
|
+
expect(result).toEqual({ version: 1, pipelines: {} });
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("malformed JSON", () => {
|
|
155
|
+
it("throws RegistryError with the file path in the message", () => {
|
|
156
|
+
const root = setupTmp();
|
|
157
|
+
const filePath = writeRegistry(root, "{ not json");
|
|
158
|
+
expect(() => readRegistrySync(root)).toThrow(RegistryError);
|
|
159
|
+
expect(() => readRegistrySync(root)).toThrow(filePath);
|
|
160
|
+
expect(() => readRegistrySync(root)).toThrow("not valid JSON");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("empty file", () => {
|
|
165
|
+
it("throws RegistryError naming the path", () => {
|
|
166
|
+
const root = setupTmp();
|
|
167
|
+
const filePath = writeRegistry(root, "");
|
|
168
|
+
expect(() => readRegistrySync(root)).toThrow(RegistryError);
|
|
169
|
+
expect(() => readRegistrySync(root)).toThrow(filePath);
|
|
170
|
+
expect(() => readRegistrySync(root)).toThrow("registry file is empty");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("whitespace-only file", () => {
|
|
175
|
+
it("throws RegistryError naming the path", () => {
|
|
176
|
+
const root = setupTmp();
|
|
177
|
+
const filePath = writeRegistry(root, " \n\t ");
|
|
178
|
+
expect(() => readRegistrySync(root)).toThrow(RegistryError);
|
|
179
|
+
expect(() => readRegistrySync(root)).toThrow(filePath);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("non-object JSON", () => {
|
|
184
|
+
it("rejects an array", () => {
|
|
185
|
+
const root = setupTmp();
|
|
186
|
+
writeRegistry(root, JSON.stringify([1, 2, 3]));
|
|
187
|
+
expect(() => readRegistrySync(root)).toThrow(RegistryError);
|
|
188
|
+
expect(() => readRegistrySync(root)).toThrow(
|
|
189
|
+
"registry must be a JSON object",
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("rejects a string", () => {
|
|
194
|
+
const root = setupTmp();
|
|
195
|
+
writeRegistry(root, JSON.stringify("hello"));
|
|
196
|
+
expect(() => readRegistrySync(root)).toThrow(
|
|
197
|
+
"registry must be a JSON object",
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("rejects a number", () => {
|
|
202
|
+
const root = setupTmp();
|
|
203
|
+
writeRegistry(root, JSON.stringify(42));
|
|
204
|
+
expect(() => readRegistrySync(root)).toThrow(
|
|
205
|
+
"registry must be a JSON object",
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("rejects null", () => {
|
|
210
|
+
const root = setupTmp();
|
|
211
|
+
writeRegistry(root, JSON.stringify(null));
|
|
212
|
+
expect(() => readRegistrySync(root)).toThrow(
|
|
213
|
+
"registry must be a JSON object",
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("missing pipelines", () => {
|
|
219
|
+
it("rejects a document without the pipelines key", () => {
|
|
220
|
+
const root = setupTmp();
|
|
221
|
+
writeRegistry(root, JSON.stringify({ version: 1 }));
|
|
222
|
+
expect(() => readRegistrySync(root)).toThrow(
|
|
223
|
+
"missing 'pipelines' object",
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("rejects pipelines: null", () => {
|
|
228
|
+
const root = setupTmp();
|
|
229
|
+
writeRegistry(root, JSON.stringify({ version: 1, pipelines: null }));
|
|
230
|
+
expect(() => readRegistrySync(root)).toThrow(
|
|
231
|
+
"missing 'pipelines' object",
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("legacy shapes", () => {
|
|
238
|
+
it("tolerates the legacy { slugs: [] } shape as an empty registry", () => {
|
|
239
|
+
const root = setupTmp();
|
|
240
|
+
writeRegistry(root, JSON.stringify({ slugs: [] }));
|
|
241
|
+
expect(readRegistrySync(root)).toEqual({ version: 1, pipelines: {} });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("tolerates the legacy { defaultSlug, slugs } shape as an empty registry", () => {
|
|
245
|
+
const root = setupTmp();
|
|
246
|
+
writeRegistry(
|
|
247
|
+
root,
|
|
248
|
+
JSON.stringify({ defaultSlug: "x", slugs: { x: {} } }),
|
|
249
|
+
);
|
|
250
|
+
expect(readRegistrySync(root)).toEqual({ version: 1, pipelines: {} });
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe("entry validation", () => {
|
|
255
|
+
it("rejects an entry missing 'name', naming the slug", () => {
|
|
256
|
+
const root = setupTmp();
|
|
257
|
+
writeRegistry(
|
|
258
|
+
root,
|
|
259
|
+
JSON.stringify({ version: 1, pipelines: { foo: { description: "d" } } }),
|
|
260
|
+
);
|
|
261
|
+
expect(() => readRegistrySync(root)).toThrow(RegistryError);
|
|
262
|
+
expect(() => readRegistrySync(root)).toThrow(
|
|
263
|
+
"entry 'foo': 'name' must be a string",
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("rejects an entry missing 'description', naming the slug", () => {
|
|
268
|
+
const root = setupTmp();
|
|
269
|
+
writeRegistry(
|
|
270
|
+
root,
|
|
271
|
+
JSON.stringify({ version: 1, pipelines: { foo: { name: "n" } } }),
|
|
272
|
+
);
|
|
273
|
+
expect(() => readRegistrySync(root)).toThrow(
|
|
274
|
+
"entry 'foo': 'description' must be a string",
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("rejects a non-string configDir, naming the slug", () => {
|
|
279
|
+
const root = setupTmp();
|
|
280
|
+
writeRegistry(
|
|
281
|
+
root,
|
|
282
|
+
JSON.stringify({
|
|
283
|
+
version: 1,
|
|
284
|
+
pipelines: {
|
|
285
|
+
foo: { name: "n", description: "d", configDir: 7 },
|
|
286
|
+
},
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
expect(() => readRegistrySync(root)).toThrow(
|
|
290
|
+
"entry 'foo': 'configDir' must be a string when present",
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("rejects a non-string tasksDir, naming the slug", () => {
|
|
295
|
+
const root = setupTmp();
|
|
296
|
+
writeRegistry(
|
|
297
|
+
root,
|
|
298
|
+
JSON.stringify({
|
|
299
|
+
version: 1,
|
|
300
|
+
pipelines: {
|
|
301
|
+
foo: { name: "n", description: "d", tasksDir: false },
|
|
302
|
+
},
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
expect(() => readRegistrySync(root)).toThrow(
|
|
306
|
+
"entry 'foo': 'tasksDir' must be a string when present",
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe("entry must be an object", () => {
|
|
312
|
+
it("rejects a null entry, naming the slug", () => {
|
|
313
|
+
const root = setupTmp();
|
|
314
|
+
writeRegistry(
|
|
315
|
+
root,
|
|
316
|
+
JSON.stringify({ version: 1, pipelines: { foo: null } }),
|
|
317
|
+
);
|
|
318
|
+
expect(() => readRegistrySync(root)).toThrow(RegistryError);
|
|
319
|
+
expect(() => readRegistrySync(root)).toThrow(
|
|
320
|
+
"entry 'foo': must be an object",
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("rejects a number entry, naming the slug", () => {
|
|
325
|
+
const root = setupTmp();
|
|
326
|
+
writeRegistry(
|
|
327
|
+
root,
|
|
328
|
+
JSON.stringify({ version: 1, pipelines: { foo: 42 } }),
|
|
329
|
+
);
|
|
330
|
+
expect(() => readRegistrySync(root)).toThrow(RegistryError);
|
|
331
|
+
expect(() => readRegistrySync(root)).toThrow(
|
|
332
|
+
"entry 'foo': must be an object",
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("rejects an array entry, naming the slug", () => {
|
|
337
|
+
const root = setupTmp();
|
|
338
|
+
writeRegistry(
|
|
339
|
+
root,
|
|
340
|
+
JSON.stringify({ version: 1, pipelines: { foo: [] } }),
|
|
341
|
+
);
|
|
342
|
+
expect(() => readRegistrySync(root)).toThrow(RegistryError);
|
|
343
|
+
expect(() => readRegistrySync(root)).toThrow(
|
|
344
|
+
"entry 'foo': must be an object",
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe("version", () => {
|
|
350
|
+
it("rejects an unsupported numeric version", () => {
|
|
351
|
+
const root = setupTmp();
|
|
352
|
+
writeRegistry(root, JSON.stringify({ version: 2, pipelines: {} }));
|
|
353
|
+
expect(() => readRegistrySync(root)).toThrow(RegistryError);
|
|
354
|
+
expect(() => readRegistrySync(root)).toThrow(
|
|
355
|
+
"unsupported registry version 2 (expected 1)",
|
|
356
|
+
);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("rejects a non-number version value", () => {
|
|
360
|
+
const root = setupTmp();
|
|
361
|
+
writeRegistry(root, JSON.stringify({ version: "1", pipelines: {} }));
|
|
362
|
+
expect(() => readRegistrySync(root)).toThrow(
|
|
363
|
+
'unsupported registry version "1" (expected 1)',
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("accepts a document missing top-level version as version 1", () => {
|
|
368
|
+
const root = setupTmp();
|
|
369
|
+
writeRegistry(
|
|
370
|
+
root,
|
|
371
|
+
JSON.stringify({
|
|
372
|
+
pipelines: { foo: { name: "n", description: "d" } },
|
|
373
|
+
}),
|
|
374
|
+
);
|
|
375
|
+
const result = readRegistrySync(root);
|
|
376
|
+
expect(result.version).toBe(1);
|
|
377
|
+
expect(result.pipelines.foo?.name).toBe("n");
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe("permissive valid surface", () => {
|
|
382
|
+
it("accepts an entry carrying unknown extra keys", () => {
|
|
383
|
+
const root = setupTmp();
|
|
384
|
+
writeRegistry(
|
|
385
|
+
root,
|
|
386
|
+
JSON.stringify({
|
|
387
|
+
version: 1,
|
|
388
|
+
pipelines: {
|
|
389
|
+
foo: {
|
|
390
|
+
name: "n",
|
|
391
|
+
description: "d",
|
|
392
|
+
extra: "ok",
|
|
393
|
+
nested: { a: 1 },
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
}),
|
|
397
|
+
);
|
|
398
|
+
const result = readRegistrySync(root);
|
|
399
|
+
expect(result.pipelines.foo?.name).toBe("n");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("returns { version: 1, pipelines: {} } for an empty pipelines map", () => {
|
|
403
|
+
const root = setupTmp();
|
|
404
|
+
writeRegistry(root, JSON.stringify({ version: 1, pipelines: {} }));
|
|
405
|
+
expect(readRegistrySync(root)).toEqual({ version: 1, pipelines: {} });
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
describe("resolvePipelineSync", () => {
|
|
411
|
+
let tmp: string;
|
|
412
|
+
|
|
413
|
+
afterEach(() => {
|
|
414
|
+
if (tmp) rmSync(tmp, { recursive: true, force: true });
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const setupTmp = (): string => {
|
|
418
|
+
tmp = mkdtempSync(join(tmpdir(), "pop-reg-"));
|
|
419
|
+
mkdirSync(join(tmp, "pipeline-config"), { recursive: true });
|
|
420
|
+
return tmp;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const writeRegistryFile = (root: string, registry: unknown): void => {
|
|
424
|
+
writeFileSync(registryFilePath(root), JSON.stringify(registry));
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
it("registered slug resolves to pipeline.json path and tasksDir (defaults)", () => {
|
|
428
|
+
const root = setupTmp();
|
|
429
|
+
writeRegistryFile(root, {
|
|
430
|
+
version: 1,
|
|
431
|
+
pipelines: { foo: { name: "Foo", description: "d" } },
|
|
432
|
+
});
|
|
433
|
+
const result = resolvePipelineSync(root, "foo");
|
|
434
|
+
expect(result.pipelineJsonPath).toBe(
|
|
435
|
+
join(root, "pipeline-config", "foo", "pipeline.json"),
|
|
436
|
+
);
|
|
437
|
+
expect(result.tasksDir).toBe(join(root, "pipeline-config", "foo", "tasks"));
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("registered slug with explicit absolute configDir derives tasksDir and pipelineJsonPath", () => {
|
|
441
|
+
const root = setupTmp();
|
|
442
|
+
const customCfg = join(root, "custom-cfg");
|
|
443
|
+
writeRegistryFile(root, {
|
|
444
|
+
version: 1,
|
|
445
|
+
pipelines: {
|
|
446
|
+
foo: { name: "Foo", description: "d", configDir: customCfg },
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
const result = resolvePipelineSync(root, "foo");
|
|
450
|
+
expect(result.pipelineJsonPath).toBe(join(customCfg, "pipeline.json"));
|
|
451
|
+
expect(result.tasksDir).toBe(join(customCfg, "tasks"));
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("registered slug with relative configDir/tasksDir resolves against root", () => {
|
|
455
|
+
const root = setupTmp();
|
|
456
|
+
writeRegistryFile(root, {
|
|
457
|
+
version: 1,
|
|
458
|
+
pipelines: {
|
|
459
|
+
foo: {
|
|
460
|
+
name: "Foo",
|
|
461
|
+
description: "d",
|
|
462
|
+
configDir: "rel-cfg",
|
|
463
|
+
tasksDir: "rel-tasks",
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
const result = resolvePipelineSync(root, "foo");
|
|
468
|
+
expect(result.pipelineJsonPath).toBe(
|
|
469
|
+
join(root, "rel-cfg", "pipeline.json"),
|
|
470
|
+
);
|
|
471
|
+
expect(result.tasksDir).toBe(join(root, "rel-tasks"));
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("unknown slug throws 'Pipeline '<slug>' not found in registry'", () => {
|
|
475
|
+
const root = setupTmp();
|
|
476
|
+
writeRegistryFile(root, { version: 1, pipelines: {} });
|
|
477
|
+
expect(() => resolvePipelineSync(root, "missing")).toThrow(
|
|
478
|
+
"Pipeline 'missing' not found in registry",
|
|
479
|
+
);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it("unknown slug throws the same message when the registry file is absent", () => {
|
|
483
|
+
tmp = mkdtempSync(join(tmpdir(), "pop-reg-"));
|
|
484
|
+
expect(() => resolvePipelineSync(tmp, "missing")).toThrow(
|
|
485
|
+
"Pipeline 'missing' not found in registry",
|
|
486
|
+
);
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe("resolveAllPipelinesSync", () => {
|
|
491
|
+
let tmp: string;
|
|
492
|
+
|
|
493
|
+
afterEach(() => {
|
|
494
|
+
if (tmp) rmSync(tmp, { recursive: true, force: true });
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const setupTmp = (): string => {
|
|
498
|
+
tmp = mkdtempSync(join(tmpdir(), "pop-reg-"));
|
|
499
|
+
mkdirSync(join(tmp, "pipeline-config"), { recursive: true });
|
|
500
|
+
return tmp;
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const writeRegistryFile = (root: string, registry: unknown): void => {
|
|
504
|
+
writeFileSync(registryFilePath(root), JSON.stringify(registry));
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
it("returns empty map when pipelines is empty", () => {
|
|
508
|
+
const root = setupTmp();
|
|
509
|
+
writeRegistryFile(root, { version: 1, pipelines: {} });
|
|
510
|
+
expect(resolveAllPipelinesSync(root)).toEqual({});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("returns empty map when the registry file is absent", () => {
|
|
514
|
+
tmp = mkdtempSync(join(tmpdir(), "pop-reg-"));
|
|
515
|
+
expect(resolveAllPipelinesSync(tmp)).toEqual({});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it("multi-entry registry returns per-slug { configDir, tasksDir } matching resolvePipelineEntry", () => {
|
|
519
|
+
const root = setupTmp();
|
|
520
|
+
const alphaEntry = { name: "A", description: "d" };
|
|
521
|
+
const betaEntry = {
|
|
522
|
+
name: "B",
|
|
523
|
+
description: "d",
|
|
524
|
+
configDir: "custom/beta",
|
|
525
|
+
};
|
|
526
|
+
writeRegistryFile(root, {
|
|
527
|
+
version: 1,
|
|
528
|
+
pipelines: { alpha: alphaEntry, beta: betaEntry },
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
const result = resolveAllPipelinesSync(root);
|
|
532
|
+
|
|
533
|
+
expect(result.alpha).toEqual(
|
|
534
|
+
resolvePipelineEntry(root, "alpha", alphaEntry),
|
|
535
|
+
);
|
|
536
|
+
expect(result.beta).toEqual(resolvePipelineEntry(root, "beta", betaEntry));
|
|
537
|
+
expect(result.alpha?.configDir).toBe(
|
|
538
|
+
join(root, "pipeline-config", "alpha"),
|
|
539
|
+
);
|
|
540
|
+
expect(result.beta?.configDir).toBe(join(root, "custom", "beta"));
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("does NOT include pipelineJsonPath in the per-entry shape", () => {
|
|
544
|
+
const root = setupTmp();
|
|
545
|
+
writeRegistryFile(root, {
|
|
546
|
+
version: 1,
|
|
547
|
+
pipelines: { alpha: { name: "A", description: "d" } },
|
|
548
|
+
});
|
|
549
|
+
const result = resolveAllPipelinesSync(root);
|
|
550
|
+
expect(result.alpha).not.toHaveProperty("pipelineJsonPath");
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
describe("resolveTaskRegistryPath", () => {
|
|
555
|
+
let tmp: string;
|
|
556
|
+
|
|
557
|
+
afterEach(() => {
|
|
558
|
+
if (tmp) rmSync(tmp, { recursive: true, force: true });
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const setupTmp = (): string => {
|
|
562
|
+
tmp = mkdtempSync(join(tmpdir(), "pop-task-"));
|
|
563
|
+
return tmp;
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
it("returns index.js path when only index.js exists", () => {
|
|
567
|
+
const tasksDir = setupTmp();
|
|
568
|
+
writeFileSync(join(tasksDir, "index.js"), "");
|
|
569
|
+
expect(resolveTaskRegistryPath(tasksDir)).toBe(join(tasksDir, "index.js"));
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it("returns index.ts path when only index.ts exists", () => {
|
|
573
|
+
const tasksDir = setupTmp();
|
|
574
|
+
writeFileSync(join(tasksDir, "index.ts"), "");
|
|
575
|
+
expect(resolveTaskRegistryPath(tasksDir)).toBe(join(tasksDir, "index.ts"));
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it("prefers index.js when both index.js and index.ts exist", () => {
|
|
579
|
+
const tasksDir = setupTmp();
|
|
580
|
+
writeFileSync(join(tasksDir, "index.js"), "");
|
|
581
|
+
writeFileSync(join(tasksDir, "index.ts"), "");
|
|
582
|
+
expect(resolveTaskRegistryPath(tasksDir)).toBe(join(tasksDir, "index.js"));
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it("returns index.js path when neither exists", () => {
|
|
586
|
+
const tasksDir = setupTmp();
|
|
587
|
+
expect(resolveTaskRegistryPath(tasksDir)).toBe(join(tasksDir, "index.js"));
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
describe("writeRegistry + registerPipeline", () => {
|
|
592
|
+
let tmp: string;
|
|
593
|
+
|
|
594
|
+
afterEach(() => {
|
|
595
|
+
if (tmp) rmSync(tmp, { recursive: true, force: true });
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const setupTmp = (): string => {
|
|
599
|
+
tmp = mkdtempSync(join(tmpdir(), "pop-reg-write-"));
|
|
600
|
+
mkdirSync(join(tmp, "pipeline-config"), { recursive: true });
|
|
601
|
+
return tmp;
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
it("registerPipeline round-trips the entry through readRegistrySync", async () => {
|
|
605
|
+
const root = setupTmp();
|
|
606
|
+
await registerPipeline(root, "foo", {
|
|
607
|
+
name: "Foo",
|
|
608
|
+
description: "makes the foos",
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
const result = readRegistrySync(root);
|
|
612
|
+
|
|
613
|
+
expect(result.version).toBe(REGISTRY_VERSION);
|
|
614
|
+
expect(result.pipelines.foo?.name).toBe("Foo");
|
|
615
|
+
expect(result.pipelines.foo?.description).toBe("makes the foos");
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it("writes the file at registryFilePath(root) containing version: 1", async () => {
|
|
619
|
+
const root = setupTmp();
|
|
620
|
+
await registerPipeline(root, "foo", {
|
|
621
|
+
name: "Foo",
|
|
622
|
+
description: "d",
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
const filePath = registryFilePath(root);
|
|
626
|
+
expect(existsSync(filePath)).toBe(true);
|
|
627
|
+
expect(existsSync(join(root, "registry.json"))).toBe(false);
|
|
628
|
+
|
|
629
|
+
const text = readFileSync(filePath, "utf8");
|
|
630
|
+
expect(text).toContain('"version":1');
|
|
631
|
+
expect(text).toContain('"foo"');
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it("registerPipeline preserves existing entries when adding a new one", async () => {
|
|
635
|
+
const root = setupTmp();
|
|
636
|
+
writeFileSync(
|
|
637
|
+
registryFilePath(root),
|
|
638
|
+
JSON.stringify({
|
|
639
|
+
version: 1,
|
|
640
|
+
pipelines: { a: { name: "Alpha", description: "first" } },
|
|
641
|
+
}),
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
await registerPipeline(root, "b", {
|
|
645
|
+
name: "Beta",
|
|
646
|
+
description: "second",
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
const result = readRegistrySync(root);
|
|
650
|
+
expect(Object.keys(result.pipelines).sort()).toEqual(["a", "b"]);
|
|
651
|
+
expect(result.pipelines.a?.name).toBe("Alpha");
|
|
652
|
+
expect(result.pipelines.b?.name).toBe("Beta");
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it("forced pre-rename failure leaves the previous registry content unchanged and removes the temp file", async () => {
|
|
656
|
+
const root = setupTmp();
|
|
657
|
+
const original: PipelineRegistry = {
|
|
658
|
+
version: 1,
|
|
659
|
+
pipelines: {
|
|
660
|
+
existing: { name: "Existing", description: "preserve me" },
|
|
661
|
+
},
|
|
662
|
+
};
|
|
663
|
+
writeFileSync(registryFilePath(root), JSON.stringify(original));
|
|
664
|
+
|
|
665
|
+
renameShouldFail = true;
|
|
666
|
+
try {
|
|
667
|
+
await expect(
|
|
668
|
+
writeRegistry(root, {
|
|
669
|
+
version: 1,
|
|
670
|
+
pipelines: { other: { name: "Other", description: "x" } },
|
|
671
|
+
}),
|
|
672
|
+
).rejects.toThrow("forced rename failure");
|
|
673
|
+
} finally {
|
|
674
|
+
renameShouldFail = false;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
expect(readRegistrySync(root)).toEqual(original);
|
|
678
|
+
|
|
679
|
+
const lingeringTemps = readdirSync(join(tmp, "pipeline-config")).filter(
|
|
680
|
+
(f) => f.includes("registry.json.tmp"),
|
|
681
|
+
);
|
|
682
|
+
expect(lingeringTemps).toEqual([]);
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
describe("demo registry", () => {
|
|
687
|
+
const demoRoot = resolve(process.cwd(), "demo");
|
|
688
|
+
|
|
689
|
+
it("readRegistrySync accepts the demo registry as a v1 document", () => {
|
|
690
|
+
const reg = readRegistrySync(demoRoot);
|
|
691
|
+
expect(reg.version).toBe(1);
|
|
692
|
+
expect(reg.pipelines["content-generation"]?.name).toBe(
|
|
693
|
+
"Content Generation Pipeline",
|
|
694
|
+
);
|
|
695
|
+
expect(reg.pipelines["agent-harness-demo"]?.name).toBe(
|
|
696
|
+
"Agent Harness Demo Pipeline",
|
|
697
|
+
);
|
|
698
|
+
expect(reg.pipelines["agent-js-task-demo"]?.name).toBe(
|
|
699
|
+
"Agent JS Task Demo Pipeline",
|
|
700
|
+
);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it("resolvePipelineSync resolves content-generation to existing pipeline.json and tasks dir", () => {
|
|
704
|
+
const resolved = resolvePipelineSync(demoRoot, "content-generation");
|
|
705
|
+
expect(existsSync(resolved.pipelineJsonPath)).toBe(true);
|
|
706
|
+
expect(existsSync(resolved.tasksDir)).toBe(true);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it("resolvePipelineSync resolves agent-harness-demo via default resolution", () => {
|
|
710
|
+
const resolved = resolvePipelineSync(demoRoot, "agent-harness-demo");
|
|
711
|
+
expect(resolved.pipelineJsonPath).toBe(
|
|
712
|
+
join(demoRoot, "pipeline-config", "agent-harness-demo", "pipeline.json"),
|
|
713
|
+
);
|
|
714
|
+
expect(resolved.tasksDir).toBe(
|
|
715
|
+
join(demoRoot, "pipeline-config", "agent-harness-demo", "tasks"),
|
|
716
|
+
);
|
|
717
|
+
expect(existsSync(resolved.pipelineJsonPath)).toBe(true);
|
|
718
|
+
expect(existsSync(resolved.tasksDir)).toBe(true);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it("resolvePipelineSync resolves agent-js-task-demo via default resolution", () => {
|
|
722
|
+
const resolved = resolvePipelineSync(demoRoot, "agent-js-task-demo");
|
|
723
|
+
expect(resolved.pipelineJsonPath).toBe(
|
|
724
|
+
join(demoRoot, "pipeline-config", "agent-js-task-demo", "pipeline.json"),
|
|
725
|
+
);
|
|
726
|
+
expect(resolved.tasksDir).toBe(
|
|
727
|
+
join(demoRoot, "pipeline-config", "agent-js-task-demo", "tasks"),
|
|
728
|
+
);
|
|
729
|
+
expect(existsSync(resolved.pipelineJsonPath)).toBe(true);
|
|
730
|
+
expect(existsSync(resolved.tasksDir)).toBe(true);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it("content-generation entry carries explicit configDir and tasksDir in the file", () => {
|
|
734
|
+
const raw = readFileSync(registryFilePath(demoRoot), "utf8");
|
|
735
|
+
expect(raw).toContain('"configDir"');
|
|
736
|
+
expect(raw).toContain('"tasksDir"');
|
|
737
|
+
|
|
738
|
+
const parsed = JSON.parse(raw) as {
|
|
739
|
+
pipelines: Record<string, Record<string, unknown>>;
|
|
740
|
+
};
|
|
741
|
+
const entry = parsed.pipelines["content-generation"];
|
|
742
|
+
expect(entry?.["configDir"]).toBe("pipeline-config/content-generation");
|
|
743
|
+
expect(entry?.["tasksDir"]).toBe("pipeline-config/content-generation/tasks");
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
it("agent-harness-demo relies on default resolution (no configDir/tasksDir)", () => {
|
|
747
|
+
const raw = readFileSync(registryFilePath(demoRoot), "utf8");
|
|
748
|
+
const parsed = JSON.parse(raw) as {
|
|
749
|
+
pipelines: Record<string, Record<string, unknown>>;
|
|
750
|
+
};
|
|
751
|
+
const entry = parsed.pipelines["agent-harness-demo"];
|
|
752
|
+
expect(entry).toBeDefined();
|
|
753
|
+
expect(entry?.["configDir"]).toBeUndefined();
|
|
754
|
+
expect(entry?.["tasksDir"]).toBeUndefined();
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it("agent-js-task-demo relies on default resolution (no configDir/tasksDir)", () => {
|
|
758
|
+
const raw = readFileSync(registryFilePath(demoRoot), "utf8");
|
|
759
|
+
const parsed = JSON.parse(raw) as {
|
|
760
|
+
pipelines: Record<string, Record<string, unknown>>;
|
|
761
|
+
};
|
|
762
|
+
const entry = parsed.pipelines["agent-js-task-demo"];
|
|
763
|
+
expect(entry).toBeDefined();
|
|
764
|
+
expect(entry?.["configDir"]).toBeUndefined();
|
|
765
|
+
expect(entry?.["tasksDir"]).toBeUndefined();
|
|
766
|
+
});
|
|
767
|
+
});
|