@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
|
@@ -1,56 +1,8 @@
|
|
|
1
1
|
import { describe, test, expect, afterEach } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
import type { AppConfig } from "../config";
|
|
4
|
-
import { mkdtemp, writeFile, mkdir, rm } from "node:fs/promises";
|
|
2
|
+
import { readFileSync, readdirSync, statSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
5
3
|
import { tmpdir } from "node:os";
|
|
6
4
|
import { join } from "node:path";
|
|
7
|
-
|
|
8
|
-
// Tests for validateConfig behavior are covered indirectly through loadConfig tests.
|
|
9
|
-
// Direct tests for edge cases:
|
|
10
|
-
|
|
11
|
-
describe("config environment overrides", () => {
|
|
12
|
-
const originalEnv = { ...process.env };
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
process.env = { ...originalEnv };
|
|
15
|
-
resetConfig();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test("PO_SHUTDOWN_TIMEOUT overrides orchestrator.shutdownTimeout", async () => {
|
|
19
|
-
// Tested via loadConfig in step 7
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe("loadConfig", () => {
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
resetConfig();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("throws when PO_ROOT is not set", async () => {
|
|
29
|
-
const origRoot = process.env.PO_ROOT;
|
|
30
|
-
delete process.env.PO_ROOT;
|
|
31
|
-
try {
|
|
32
|
-
await expect(loadConfig()).rejects.toThrow("PO_ROOT is required");
|
|
33
|
-
} finally {
|
|
34
|
-
if (origRoot) process.env.PO_ROOT = origRoot;
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("loads config from file and merges with defaults", async () => {
|
|
39
|
-
const dir = await mkdtemp(join(tmpdir(), "config-test-"));
|
|
40
|
-
const configDir = join(dir, "pipeline-config", "test");
|
|
41
|
-
const tasksDir = join(configDir, "tasks");
|
|
42
|
-
await mkdir(tasksDir, { recursive: true });
|
|
43
|
-
await writeFile(join(configDir, "pipeline.json"), JSON.stringify({ name: "test", tasks: ["t1"] }));
|
|
44
|
-
await writeFile(join(dir, "pipeline-config", "registry.json"), JSON.stringify({
|
|
45
|
-
pipelines: { test: { configDir, tasksDir } }
|
|
46
|
-
}));
|
|
47
|
-
process.env.PO_ROOT = dir;
|
|
48
|
-
const config = await loadConfig();
|
|
49
|
-
expect(config.paths.root).toBe(dir);
|
|
50
|
-
expect(config.pipelines.test).toBeDefined();
|
|
51
|
-
await rm(dir, { recursive: true });
|
|
52
|
-
});
|
|
53
|
-
});
|
|
5
|
+
import { defaultConfig, resetConfig, getConfig, getConfigValue, getPipelineConfig, getOrchestratorConfig } from "../config";
|
|
54
6
|
|
|
55
7
|
describe("defaultConfig", () => {
|
|
56
8
|
afterEach(() => {
|
|
@@ -62,7 +14,6 @@ describe("defaultConfig", () => {
|
|
|
62
14
|
expect(defaultConfig.taskRunner).toBeDefined();
|
|
63
15
|
expect(defaultConfig.llm).toBeDefined();
|
|
64
16
|
expect(defaultConfig.ui).toBeDefined();
|
|
65
|
-
expect(defaultConfig.paths).toBeDefined();
|
|
66
17
|
expect(defaultConfig.validation).toBeDefined();
|
|
67
18
|
expect(defaultConfig.logging).toBeDefined();
|
|
68
19
|
});
|
|
@@ -82,54 +33,6 @@ describe("defaultConfig", () => {
|
|
|
82
33
|
});
|
|
83
34
|
});
|
|
84
35
|
|
|
85
|
-
describe("validateConfig (via loadConfig)", () => {
|
|
86
|
-
afterEach(() => {
|
|
87
|
-
resetConfig();
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("throws when taskRunner.maxAttempts is 0", async () => {
|
|
91
|
-
const dir = await mkdtemp(join(tmpdir(), "config-test-"));
|
|
92
|
-
const configDir = join(dir, "pipeline-config", "test");
|
|
93
|
-
const tasksDir = join(configDir, "tasks");
|
|
94
|
-
await mkdir(tasksDir, { recursive: true });
|
|
95
|
-
await writeFile(join(configDir, "pipeline.json"), JSON.stringify({ name: "test", tasks: ["t1"] }));
|
|
96
|
-
await writeFile(join(dir, "pipeline-config", "registry.json"), JSON.stringify({
|
|
97
|
-
pipelines: { test: { configDir, tasksDir } }
|
|
98
|
-
}));
|
|
99
|
-
const overrideFile = join(dir, "config.json");
|
|
100
|
-
await writeFile(overrideFile, JSON.stringify({ taskRunner: { maxAttempts: 0 } }));
|
|
101
|
-
const origRoot = process.env.PO_ROOT;
|
|
102
|
-
process.env.PO_ROOT = dir;
|
|
103
|
-
try {
|
|
104
|
-
await expect(loadConfig({ configPath: overrideFile })).rejects.toThrow("taskRunner.maxAttempts must be an integer >= 1");
|
|
105
|
-
} finally {
|
|
106
|
-
if (origRoot) process.env.PO_ROOT = origRoot; else delete process.env.PO_ROOT;
|
|
107
|
-
await rm(dir, { recursive: true });
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("throws when taskRunner.maxAttempts is non-integer", async () => {
|
|
112
|
-
const dir = await mkdtemp(join(tmpdir(), "config-test-"));
|
|
113
|
-
const configDir = join(dir, "pipeline-config", "test");
|
|
114
|
-
const tasksDir = join(configDir, "tasks");
|
|
115
|
-
await mkdir(tasksDir, { recursive: true });
|
|
116
|
-
await writeFile(join(configDir, "pipeline.json"), JSON.stringify({ name: "test", tasks: ["t1"] }));
|
|
117
|
-
await writeFile(join(dir, "pipeline-config", "registry.json"), JSON.stringify({
|
|
118
|
-
pipelines: { test: { configDir, tasksDir } }
|
|
119
|
-
}));
|
|
120
|
-
const overrideFile = join(dir, "config.json");
|
|
121
|
-
await writeFile(overrideFile, JSON.stringify({ taskRunner: { maxAttempts: 2.5 } }));
|
|
122
|
-
const origRoot = process.env.PO_ROOT;
|
|
123
|
-
process.env.PO_ROOT = dir;
|
|
124
|
-
try {
|
|
125
|
-
await expect(loadConfig({ configPath: overrideFile })).rejects.toThrow("taskRunner.maxAttempts must be an integer >= 1");
|
|
126
|
-
} finally {
|
|
127
|
-
if (origRoot) process.env.PO_ROOT = origRoot; else delete process.env.PO_ROOT;
|
|
128
|
-
await rm(dir, { recursive: true });
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
36
|
describe("PO_TASK_MAX_ATTEMPTS env override", () => {
|
|
134
37
|
afterEach(() => {
|
|
135
38
|
delete process.env.PO_TASK_MAX_ATTEMPTS;
|
|
@@ -139,7 +42,7 @@ describe("PO_TASK_MAX_ATTEMPTS env override", () => {
|
|
|
139
42
|
test("getConfig reads PO_TASK_MAX_ATTEMPTS into taskRunner.maxAttempts", () => {
|
|
140
43
|
process.env.PO_TASK_MAX_ATTEMPTS = "5";
|
|
141
44
|
resetConfig();
|
|
142
|
-
const config
|
|
45
|
+
const config = getConfig();
|
|
143
46
|
expect(config.taskRunner.maxAttempts).toBe(5);
|
|
144
47
|
});
|
|
145
48
|
|
|
@@ -165,32 +68,32 @@ describe("PO_MAX_RUNNING_JOBS env override", () => {
|
|
|
165
68
|
test("getConfig reads PO_MAX_RUNNING_JOBS into orchestrator.maxConcurrentJobs", () => {
|
|
166
69
|
process.env.PO_MAX_RUNNING_JOBS = "7";
|
|
167
70
|
resetConfig();
|
|
168
|
-
const config
|
|
71
|
+
const config = getConfig();
|
|
169
72
|
expect(config.orchestrator.maxConcurrentJobs).toBe(7);
|
|
170
73
|
});
|
|
171
74
|
|
|
172
75
|
test("getConfig rejects PO_MAX_RUNNING_JOBS=0", () => {
|
|
173
76
|
process.env.PO_MAX_RUNNING_JOBS = "0";
|
|
174
77
|
resetConfig();
|
|
175
|
-
expect(() => getConfig()).toThrow(
|
|
78
|
+
expect(() => getConfig()).toThrow(/PO_MAX_RUNNING_JOBS/);
|
|
176
79
|
});
|
|
177
80
|
|
|
178
81
|
test("getConfig rejects negative PO_MAX_RUNNING_JOBS", () => {
|
|
179
82
|
process.env.PO_MAX_RUNNING_JOBS = "-1";
|
|
180
83
|
resetConfig();
|
|
181
|
-
expect(() => getConfig()).toThrow(
|
|
84
|
+
expect(() => getConfig()).toThrow(/PO_MAX_RUNNING_JOBS/);
|
|
182
85
|
});
|
|
183
86
|
|
|
184
87
|
test("getConfig rejects non-integer PO_MAX_RUNNING_JOBS", () => {
|
|
185
88
|
process.env.PO_MAX_RUNNING_JOBS = "1.5";
|
|
186
89
|
resetConfig();
|
|
187
|
-
expect(() => getConfig()).toThrow(
|
|
90
|
+
expect(() => getConfig()).toThrow(/PO_MAX_RUNNING_JOBS/);
|
|
188
91
|
});
|
|
189
92
|
|
|
190
93
|
test("getConfig rejects non-numeric PO_MAX_RUNNING_JOBS", () => {
|
|
191
94
|
process.env.PO_MAX_RUNNING_JOBS = "abc";
|
|
192
95
|
resetConfig();
|
|
193
|
-
expect(() => getConfig()).toThrow(
|
|
96
|
+
expect(() => getConfig()).toThrow(/PO_MAX_RUNNING_JOBS/);
|
|
194
97
|
});
|
|
195
98
|
});
|
|
196
99
|
|
|
@@ -214,13 +117,49 @@ describe("PO_STALE_RUNNING_GRACE_MS env override", () => {
|
|
|
214
117
|
test("getOrchestratorConfig rejects non-positive PO_STALE_RUNNING_GRACE_MS", () => {
|
|
215
118
|
process.env.PO_STALE_RUNNING_GRACE_MS = "-1";
|
|
216
119
|
resetConfig();
|
|
217
|
-
expect(() => getOrchestratorConfig()).toThrow(
|
|
120
|
+
expect(() => getOrchestratorConfig()).toThrow(/PO_STALE_RUNNING_GRACE_MS/);
|
|
218
121
|
});
|
|
219
122
|
|
|
220
123
|
test("getOrchestratorConfig rejects non-integer PO_STALE_RUNNING_GRACE_MS", () => {
|
|
221
124
|
process.env.PO_STALE_RUNNING_GRACE_MS = "1.5";
|
|
222
125
|
resetConfig();
|
|
223
|
-
expect(() => getOrchestratorConfig()).toThrow(
|
|
126
|
+
expect(() => getOrchestratorConfig()).toThrow(/PO_STALE_RUNNING_GRACE_MS/);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("PO_STALE_LEASE_TIMEOUT_MS env override", () => {
|
|
131
|
+
afterEach(() => {
|
|
132
|
+
delete process.env.PO_STALE_LEASE_TIMEOUT_MS;
|
|
133
|
+
resetConfig();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("getOrchestratorConfig defaults staleLeaseTimeoutMs to 30000", () => {
|
|
137
|
+
resetConfig();
|
|
138
|
+
expect(getOrchestratorConfig().staleLeaseTimeoutMs).toBe(30000);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("getOrchestratorConfig reads PO_STALE_LEASE_TIMEOUT_MS into orchestrator.staleLeaseTimeoutMs", () => {
|
|
142
|
+
process.env.PO_STALE_LEASE_TIMEOUT_MS = "60000";
|
|
143
|
+
resetConfig();
|
|
144
|
+
expect(getOrchestratorConfig().staleLeaseTimeoutMs).toBe(60000);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("getOrchestratorConfig rejects PO_STALE_LEASE_TIMEOUT_MS=0", () => {
|
|
148
|
+
process.env.PO_STALE_LEASE_TIMEOUT_MS = "0";
|
|
149
|
+
resetConfig();
|
|
150
|
+
expect(() => getOrchestratorConfig()).toThrow(/PO_STALE_LEASE_TIMEOUT_MS/);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("getOrchestratorConfig rejects negative PO_STALE_LEASE_TIMEOUT_MS", () => {
|
|
154
|
+
process.env.PO_STALE_LEASE_TIMEOUT_MS = "-1";
|
|
155
|
+
resetConfig();
|
|
156
|
+
expect(() => getOrchestratorConfig()).toThrow(/PO_STALE_LEASE_TIMEOUT_MS/);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("getOrchestratorConfig rejects non-integer PO_STALE_LEASE_TIMEOUT_MS", () => {
|
|
160
|
+
process.env.PO_STALE_LEASE_TIMEOUT_MS = "1.5";
|
|
161
|
+
resetConfig();
|
|
162
|
+
expect(() => getOrchestratorConfig()).toThrow(/PO_STALE_LEASE_TIMEOUT_MS/);
|
|
224
163
|
});
|
|
225
164
|
});
|
|
226
165
|
|
|
@@ -236,6 +175,85 @@ describe("getConfig", () => {
|
|
|
236
175
|
});
|
|
237
176
|
});
|
|
238
177
|
|
|
178
|
+
describe("getConfig immutability", () => {
|
|
179
|
+
afterEach(() => {
|
|
180
|
+
resetConfig();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("returns the same frozen reference on repeat calls", () => {
|
|
184
|
+
const a = getConfig();
|
|
185
|
+
const b = getConfig();
|
|
186
|
+
expect(a).toBe(b);
|
|
187
|
+
expect(Object.isFrozen(a)).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("top-level mutation throws at runtime", () => {
|
|
191
|
+
const c = getConfig();
|
|
192
|
+
expect(() => {
|
|
193
|
+
(c as unknown as { ui: { port: number } }).ui.port = 99;
|
|
194
|
+
}).toThrow();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("nested mutation throws at runtime", () => {
|
|
198
|
+
const c = getConfig();
|
|
199
|
+
expect(() => {
|
|
200
|
+
(c as unknown as { orchestrator: { shutdownTimeout: number } }).orchestrator.shutdownTimeout = 99;
|
|
201
|
+
}).toThrow();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("deeper nested mutation throws at runtime", () => {
|
|
205
|
+
const c = getConfig();
|
|
206
|
+
expect(() => {
|
|
207
|
+
(c as unknown as { logging: { level: string } }).logging.level = "debug";
|
|
208
|
+
}).toThrow();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("array element objects are frozen at runtime", () => {
|
|
212
|
+
const mutableDefault = defaultConfig as unknown as {
|
|
213
|
+
arrayProbe?: { items: Array<{ value: number }> };
|
|
214
|
+
};
|
|
215
|
+
mutableDefault.arrayProbe = { items: [{ value: 1 }] };
|
|
216
|
+
try {
|
|
217
|
+
resetConfig();
|
|
218
|
+
const c = getConfig() as unknown as { arrayProbe: { items: Array<{ value: number }> } };
|
|
219
|
+
const first = c.arrayProbe.items[0];
|
|
220
|
+
expect(first).toBeDefined();
|
|
221
|
+
expect(Object.isFrozen(first)).toBe(true);
|
|
222
|
+
expect(() => {
|
|
223
|
+
first!.value = 2;
|
|
224
|
+
}).toThrow();
|
|
225
|
+
} finally {
|
|
226
|
+
delete mutableDefault.arrayProbe;
|
|
227
|
+
resetConfig();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("getConfig validation", () => {
|
|
233
|
+
afterEach(() => {
|
|
234
|
+
resetConfig();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("throws before caching when maxRefinementAttempts < 1", () => {
|
|
238
|
+
const original = defaultConfig.taskRunner.maxRefinementAttempts;
|
|
239
|
+
defaultConfig.taskRunner.maxRefinementAttempts = 0;
|
|
240
|
+
try {
|
|
241
|
+
resetConfig();
|
|
242
|
+
expect(() => getConfig()).toThrow(/maxRefinementAttempts/);
|
|
243
|
+
resetConfig();
|
|
244
|
+
expect(() => getConfig()).toThrow(/maxRefinementAttempts/);
|
|
245
|
+
} finally {
|
|
246
|
+
defaultConfig.taskRunner.maxRefinementAttempts = original;
|
|
247
|
+
resetConfig();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("passes validation through getConfig with unmodified defaults", () => {
|
|
252
|
+
resetConfig();
|
|
253
|
+
expect(() => getConfig()).not.toThrow();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
239
257
|
describe("resetConfig", () => {
|
|
240
258
|
test("clears cached config", () => {
|
|
241
259
|
const config1 = getConfig();
|
|
@@ -270,3 +288,395 @@ describe("getPipelineConfig", () => {
|
|
|
270
288
|
expect(() => getPipelineConfig("nonexistent-slug")).toThrow("not found in registry");
|
|
271
289
|
});
|
|
272
290
|
});
|
|
291
|
+
|
|
292
|
+
describe("pipeline registry hydration via shared module", () => {
|
|
293
|
+
let tmpDir: string;
|
|
294
|
+
const prevPoRoot = process.env["PO_ROOT"];
|
|
295
|
+
|
|
296
|
+
afterEach(() => {
|
|
297
|
+
if (prevPoRoot === undefined) delete process.env["PO_ROOT"];
|
|
298
|
+
else process.env["PO_ROOT"] = prevPoRoot;
|
|
299
|
+
resetConfig();
|
|
300
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("getConfig hydrates config.pipelines from a canonical v1 registry", () => {
|
|
304
|
+
tmpDir = mkdtempSync(join(tmpdir(), "pop-cfg-hydrate-"));
|
|
305
|
+
const registryDir = join(tmpDir, "pipeline-config");
|
|
306
|
+
mkdirSync(registryDir, { recursive: true });
|
|
307
|
+
writeFileSync(
|
|
308
|
+
join(registryDir, "registry.json"),
|
|
309
|
+
JSON.stringify({
|
|
310
|
+
version: 1,
|
|
311
|
+
pipelines: { "demo-pipe": { name: "Demo", description: "demo pipeline" } },
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
process.env["PO_ROOT"] = tmpDir;
|
|
316
|
+
resetConfig();
|
|
317
|
+
const cfg = getConfig();
|
|
318
|
+
|
|
319
|
+
const entry = cfg.pipelines["demo-pipe"];
|
|
320
|
+
expect(entry).toBeDefined();
|
|
321
|
+
expect(entry!.configDir).toBe(join(tmpDir, "pipeline-config", "demo-pipe"));
|
|
322
|
+
expect(entry!.tasksDir).toBe(join(tmpDir, "pipeline-config", "demo-pipe", "tasks"));
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("getPipelineConfig(slug, root) resolves via the shared module", () => {
|
|
326
|
+
tmpDir = mkdtempSync(join(tmpdir(), "pop-cfg-resolve-"));
|
|
327
|
+
const slugDir = join(tmpDir, "pipeline-config", "my-slug");
|
|
328
|
+
const tasksDir = join(slugDir, "tasks");
|
|
329
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
330
|
+
writeFileSync(join(slugDir, "pipeline.json"), JSON.stringify({ name: "my-slug" }));
|
|
331
|
+
writeFileSync(
|
|
332
|
+
join(tmpDir, "pipeline-config", "registry.json"),
|
|
333
|
+
JSON.stringify({
|
|
334
|
+
version: 1,
|
|
335
|
+
pipelines: { "my-slug": { name: "My Slug", description: "scaffolded" } },
|
|
336
|
+
}),
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const result = getPipelineConfig("my-slug", tmpDir);
|
|
340
|
+
|
|
341
|
+
expect(result.pipelineJsonPath).toBe(join(slugDir, "pipeline.json"));
|
|
342
|
+
expect(result.tasksDir).toBe(tasksDir);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("getPipelineConfig(slug, root) throws for unknown slug", () => {
|
|
346
|
+
tmpDir = mkdtempSync(join(tmpdir(), "pop-cfg-unknown-"));
|
|
347
|
+
mkdirSync(join(tmpDir, "pipeline-config"), { recursive: true });
|
|
348
|
+
writeFileSync(
|
|
349
|
+
join(tmpDir, "pipeline-config", "registry.json"),
|
|
350
|
+
JSON.stringify({ version: 1, pipelines: {} }),
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
expect(() => getPipelineConfig("missing", tmpDir)).toThrow(
|
|
354
|
+
"Pipeline 'missing' not found in registry",
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe("PORT env override", () => {
|
|
360
|
+
afterEach(() => {
|
|
361
|
+
delete process.env.PORT;
|
|
362
|
+
resetConfig();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("getConfig reads PORT into ui.port", () => {
|
|
366
|
+
process.env.PORT = "4500";
|
|
367
|
+
resetConfig();
|
|
368
|
+
expect(getConfig().ui.port).toBe(4500);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("getConfig rejects PORT=foo", () => {
|
|
372
|
+
process.env.PORT = "foo";
|
|
373
|
+
resetConfig();
|
|
374
|
+
expect(() => getConfig()).toThrow(/PORT/);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("getConfig rejects PORT=empty string", () => {
|
|
378
|
+
process.env.PORT = "";
|
|
379
|
+
resetConfig();
|
|
380
|
+
expect(() => getConfig()).toThrow(/PORT/);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("getConfig rejects PORT=0", () => {
|
|
384
|
+
process.env.PORT = "0";
|
|
385
|
+
resetConfig();
|
|
386
|
+
expect(() => getConfig()).toThrow(/PORT/);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("getConfig rejects PORT=70000", () => {
|
|
390
|
+
process.env.PORT = "70000";
|
|
391
|
+
resetConfig();
|
|
392
|
+
expect(() => getConfig()).toThrow(/PORT/);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("PORT=0 error message names PORT, not ui.port", () => {
|
|
396
|
+
process.env.PORT = "0";
|
|
397
|
+
resetConfig();
|
|
398
|
+
expect(() => getConfig()).toThrow(/^PORT must be between/);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
describe("PO_UI_PORT env override", () => {
|
|
403
|
+
afterEach(() => {
|
|
404
|
+
delete process.env.PO_UI_PORT;
|
|
405
|
+
resetConfig();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("getConfig reads PO_UI_PORT into ui.port", () => {
|
|
409
|
+
process.env.PO_UI_PORT = "4501";
|
|
410
|
+
resetConfig();
|
|
411
|
+
expect(getConfig().ui.port).toBe(4501);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("getConfig rejects PO_UI_PORT=foo", () => {
|
|
415
|
+
process.env.PO_UI_PORT = "foo";
|
|
416
|
+
resetConfig();
|
|
417
|
+
expect(() => getConfig()).toThrow(/PO_UI_PORT/);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("getConfig rejects PO_UI_PORT=empty string", () => {
|
|
421
|
+
process.env.PO_UI_PORT = "";
|
|
422
|
+
resetConfig();
|
|
423
|
+
expect(() => getConfig()).toThrow(/PO_UI_PORT/);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("getConfig rejects PO_UI_PORT=70000", () => {
|
|
427
|
+
process.env.PO_UI_PORT = "70000";
|
|
428
|
+
resetConfig();
|
|
429
|
+
expect(() => getConfig()).toThrow(/PO_UI_PORT/);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("getConfig rejects PO_UI_PORT=1.5", () => {
|
|
433
|
+
process.env.PO_UI_PORT = "1.5";
|
|
434
|
+
resetConfig();
|
|
435
|
+
expect(() => getConfig()).toThrow(/PO_UI_PORT/);
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
describe("PO_INTERNAL_LLM_* env override", () => {
|
|
440
|
+
afterEach(() => {
|
|
441
|
+
delete process.env.PO_INTERNAL_LLM_PROVIDER;
|
|
442
|
+
delete process.env.PO_INTERNAL_LLM_MODEL;
|
|
443
|
+
delete process.env.PO_MAX_CONCURRENCY;
|
|
444
|
+
resetConfig();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("defaultConfig exposes internalProvider/internalModel defaults", () => {
|
|
448
|
+
expect(defaultConfig.llm.internalProvider).toBe("deepseek");
|
|
449
|
+
expect(defaultConfig.llm.internalModel).toBe("deepseek-v4-flash");
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("getConfig defaults to deepseek / deepseek-v4-flash when env is unset", () => {
|
|
453
|
+
resetConfig();
|
|
454
|
+
const config = getConfig();
|
|
455
|
+
expect(config.llm.internalProvider).toBe("deepseek");
|
|
456
|
+
expect(config.llm.internalModel).toBe("deepseek-v4-flash");
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("getConfig reads PO_INTERNAL_LLM_PROVIDER into llm.internalProvider", () => {
|
|
460
|
+
process.env.PO_INTERNAL_LLM_PROVIDER = "zai";
|
|
461
|
+
resetConfig();
|
|
462
|
+
expect(getConfig().llm.internalProvider).toBe("zai");
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("getConfig reads PO_INTERNAL_LLM_MODEL into llm.internalModel", () => {
|
|
466
|
+
process.env.PO_INTERNAL_LLM_MODEL = "foo:bar";
|
|
467
|
+
resetConfig();
|
|
468
|
+
expect(getConfig().llm.internalModel).toBe("foo:bar");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("getConfig rejects empty PO_INTERNAL_LLM_PROVIDER", () => {
|
|
472
|
+
process.env.PO_INTERNAL_LLM_PROVIDER = "";
|
|
473
|
+
resetConfig();
|
|
474
|
+
expect(() => getConfig()).toThrow(/PO_INTERNAL_LLM_PROVIDER/);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test("getConfig rejects empty PO_INTERNAL_LLM_MODEL", () => {
|
|
478
|
+
process.env.PO_INTERNAL_LLM_MODEL = "";
|
|
479
|
+
resetConfig();
|
|
480
|
+
expect(() => getConfig()).toThrow(/PO_INTERNAL_LLM_MODEL/);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("internal overrides do not clobber maxConcurrency", () => {
|
|
484
|
+
process.env.PO_INTERNAL_LLM_PROVIDER = "zai";
|
|
485
|
+
process.env.PO_INTERNAL_LLM_MODEL = "zai:glm-4";
|
|
486
|
+
process.env.PO_MAX_CONCURRENCY = "9";
|
|
487
|
+
resetConfig();
|
|
488
|
+
const config = getConfig();
|
|
489
|
+
expect(config.llm.internalProvider).toBe("zai");
|
|
490
|
+
expect(config.llm.internalModel).toBe("zai:glm-4");
|
|
491
|
+
expect(config.llm.maxConcurrency).toBe(9);
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
describe("PO_MAX_CONCURRENCY env override", () => {
|
|
496
|
+
afterEach(() => {
|
|
497
|
+
delete process.env.PO_MAX_CONCURRENCY;
|
|
498
|
+
resetConfig();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("getConfig reads PO_MAX_CONCURRENCY into llm.maxConcurrency", () => {
|
|
502
|
+
process.env.PO_MAX_CONCURRENCY = "7";
|
|
503
|
+
resetConfig();
|
|
504
|
+
expect(getConfig().llm.maxConcurrency).toBe(7);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("getConfig rejects PO_MAX_CONCURRENCY=foo", () => {
|
|
508
|
+
process.env.PO_MAX_CONCURRENCY = "foo";
|
|
509
|
+
resetConfig();
|
|
510
|
+
expect(() => getConfig()).toThrow(/PO_MAX_CONCURRENCY/);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test("getConfig rejects PO_MAX_CONCURRENCY=empty string", () => {
|
|
514
|
+
process.env.PO_MAX_CONCURRENCY = "";
|
|
515
|
+
resetConfig();
|
|
516
|
+
expect(() => getConfig()).toThrow(/PO_MAX_CONCURRENCY/);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
test("getConfig rejects PO_MAX_CONCURRENCY=0", () => {
|
|
520
|
+
process.env.PO_MAX_CONCURRENCY = "0";
|
|
521
|
+
resetConfig();
|
|
522
|
+
expect(() => getConfig()).toThrow(/PO_MAX_CONCURRENCY/);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("getConfig rejects PO_MAX_CONCURRENCY=1.5", () => {
|
|
526
|
+
process.env.PO_MAX_CONCURRENCY = "1.5";
|
|
527
|
+
resetConfig();
|
|
528
|
+
expect(() => getConfig()).toThrow(/PO_MAX_CONCURRENCY/);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
describe("PO_SHUTDOWN_TIMEOUT env override", () => {
|
|
533
|
+
afterEach(() => {
|
|
534
|
+
delete process.env.PO_SHUTDOWN_TIMEOUT;
|
|
535
|
+
resetConfig();
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test("getConfig reads PO_SHUTDOWN_TIMEOUT=0 (boundary)", () => {
|
|
539
|
+
process.env.PO_SHUTDOWN_TIMEOUT = "0";
|
|
540
|
+
resetConfig();
|
|
541
|
+
expect(getConfig().orchestrator.shutdownTimeout).toBe(0);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("getConfig reads PO_SHUTDOWN_TIMEOUT=5000", () => {
|
|
545
|
+
process.env.PO_SHUTDOWN_TIMEOUT = "5000";
|
|
546
|
+
resetConfig();
|
|
547
|
+
expect(getConfig().orchestrator.shutdownTimeout).toBe(5000);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
test("getConfig rejects PO_SHUTDOWN_TIMEOUT=foo", () => {
|
|
551
|
+
process.env.PO_SHUTDOWN_TIMEOUT = "foo";
|
|
552
|
+
resetConfig();
|
|
553
|
+
expect(() => getConfig()).toThrow(/PO_SHUTDOWN_TIMEOUT/);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("getConfig rejects PO_SHUTDOWN_TIMEOUT=empty string", () => {
|
|
557
|
+
process.env.PO_SHUTDOWN_TIMEOUT = "";
|
|
558
|
+
resetConfig();
|
|
559
|
+
expect(() => getConfig()).toThrow(/PO_SHUTDOWN_TIMEOUT/);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test("getConfig rejects PO_SHUTDOWN_TIMEOUT=-1 (min=0)", () => {
|
|
563
|
+
process.env.PO_SHUTDOWN_TIMEOUT = "-1";
|
|
564
|
+
resetConfig();
|
|
565
|
+
expect(() => getConfig()).toThrow(/PO_SHUTDOWN_TIMEOUT/);
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
describe("empty-string numeric env vars", () => {
|
|
570
|
+
const VARS = [
|
|
571
|
+
"PO_TASK_MAX_ATTEMPTS",
|
|
572
|
+
"PO_MAX_RUNNING_JOBS",
|
|
573
|
+
"PO_STALE_RUNNING_GRACE_MS",
|
|
574
|
+
"PO_STALE_LEASE_TIMEOUT_MS",
|
|
575
|
+
] as const;
|
|
576
|
+
|
|
577
|
+
afterEach(() => {
|
|
578
|
+
for (const name of VARS) delete process.env[name];
|
|
579
|
+
resetConfig();
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
for (const name of VARS) {
|
|
583
|
+
test(`rejects empty ${name}`, () => {
|
|
584
|
+
process.env[name] = "";
|
|
585
|
+
resetConfig();
|
|
586
|
+
expect(() => getConfig()).toThrow(new RegExp(name));
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
describe("PO_LOG_LEVEL env override", () => {
|
|
592
|
+
afterEach(() => {
|
|
593
|
+
delete process.env.PO_LOG_LEVEL;
|
|
594
|
+
resetConfig();
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
test("getConfig reads PO_LOG_LEVEL=debug", () => {
|
|
598
|
+
process.env.PO_LOG_LEVEL = "debug";
|
|
599
|
+
resetConfig();
|
|
600
|
+
expect(getConfig().logging.level).toBe("debug");
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
test("getConfig reads PO_LOG_LEVEL=warn", () => {
|
|
604
|
+
process.env.PO_LOG_LEVEL = "warn";
|
|
605
|
+
resetConfig();
|
|
606
|
+
expect(getConfig().logging.level).toBe("warn");
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test("getConfig rejects PO_LOG_LEVEL=loud", () => {
|
|
610
|
+
process.env.PO_LOG_LEVEL = "loud";
|
|
611
|
+
resetConfig();
|
|
612
|
+
expect(() => getConfig()).toThrow(/PO_LOG_LEVEL/);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test("getConfig rejects PO_LOG_LEVEL=empty string", () => {
|
|
616
|
+
process.env.PO_LOG_LEVEL = "";
|
|
617
|
+
resetConfig();
|
|
618
|
+
expect(() => getConfig()).toThrow(/PO_LOG_LEVEL/);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
test("getConfig uses default level when PO_LOG_LEVEL is unset", () => {
|
|
622
|
+
resetConfig();
|
|
623
|
+
expect(getConfig().logging.level).toBe("info");
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
describe("default fallbacks for unset numeric env vars", () => {
|
|
628
|
+
afterEach(() => {
|
|
629
|
+
delete process.env.PO_MAX_CONCURRENCY;
|
|
630
|
+
delete process.env.PO_SHUTDOWN_TIMEOUT;
|
|
631
|
+
resetConfig();
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test("unset PO_MAX_CONCURRENCY falls back to defaultConfig.llm.maxConcurrency", () => {
|
|
635
|
+
resetConfig();
|
|
636
|
+
expect(getConfig().llm.maxConcurrency).toBe(defaultConfig.llm.maxConcurrency);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test("unset PO_SHUTDOWN_TIMEOUT falls back to defaultConfig.orchestrator.shutdownTimeout", () => {
|
|
640
|
+
resetConfig();
|
|
641
|
+
expect(getConfig().orchestrator.shutdownTimeout).toBe(defaultConfig.orchestrator.shutdownTimeout);
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
describe("source deletion scan", () => {
|
|
646
|
+
const SRC_ROOT = join(import.meta.dir, "..", "..");
|
|
647
|
+
const DELETED_TOKENS = [
|
|
648
|
+
"loadConfig",
|
|
649
|
+
"LoadConfigOptions",
|
|
650
|
+
"PathsConfig",
|
|
651
|
+
"AppConfig.paths",
|
|
652
|
+
] as const;
|
|
653
|
+
|
|
654
|
+
function walkTs(dir: string): string[] {
|
|
655
|
+
const out: string[] = [];
|
|
656
|
+
for (const entry of readdirSync(dir)) {
|
|
657
|
+
const full = join(dir, entry);
|
|
658
|
+
if (statSync(full).isDirectory()) {
|
|
659
|
+
out.push(...walkTs(full));
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (/\.tsx?$/.test(entry)) out.push(full);
|
|
663
|
+
}
|
|
664
|
+
return out;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
test("no src/ file references deleted config symbols", () => {
|
|
668
|
+
const files = walkTs(SRC_ROOT).filter((f) => !f.includes("/__tests__/"));
|
|
669
|
+
const offending: Array<{ file: string; token: string }> = [];
|
|
670
|
+
for (const file of files) {
|
|
671
|
+
const content = readFileSync(file, "utf8");
|
|
672
|
+
for (const token of DELETED_TOKENS) {
|
|
673
|
+
if (content.includes(token)) offending.push({ file, token });
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (offending.length > 0) {
|
|
677
|
+
const lines = offending.map((o) => ` ${o.file}: ${o.token}`).join("\n");
|
|
678
|
+
throw new Error(`Deleted config symbols still referenced under src/:\n${lines}`);
|
|
679
|
+
}
|
|
680
|
+
expect(offending).toHaveLength(0);
|
|
681
|
+
});
|
|
682
|
+
});
|