feat: global extract provider config

- workflow.yaml supports config section (maxDepth, extract provider)
- ExtractProviderConfig with env: prefix for apiKey resolution
- getExtractProvider(storageRoot) returns LlmProvider from config
- workflowAsAgent uses config maxDepth (fallback 3)
- Registry read/write preserves config
- 158 tests passing

Fixes #43
This commit is contained in:
2026-05-07 13:21:38 +00:00
parent b5cc0db17e
commit cae59b589e
10 changed files with 340 additions and 10 deletions
+82 -1
View File
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import {
parseWorkflowRegistryYaml,
readWorkflowRegistry,
registerWorkflowVersion,
rollbackWorkflowToHistoryHash,
@@ -21,6 +22,7 @@ describe("workflow registry", () => {
if (!empty.ok) {
return;
}
expect(empty.value.config).toBeNull();
const r1 = registerWorkflowVersion(empty.value, "solve-issue", "AAAAAAAAAAAAA", 100);
const w1 = await writeWorkflowRegistry(dir, r1);
@@ -68,7 +70,7 @@ describe("workflow registry", () => {
});
test("rollbackWorkflowToHistoryHash swaps head with a prior version", () => {
let reg = registerWorkflowVersion({ workflows: {} }, "solve-issue", "H1", 100);
let reg = registerWorkflowVersion({ config: null, workflows: {} }, "solve-issue", "H1", 100);
reg = registerWorkflowVersion(reg, "solve-issue", "H2", 200);
reg = registerWorkflowVersion(reg, "solve-issue", "H3", 300);
const entry = reg.workflows["solve-issue"];
@@ -99,6 +101,85 @@ describe("workflow registry", () => {
expect(bad.ok).toBe(false);
});
test("parses config section and literal apiKey", () => {
const yaml = `
config:
maxDepth: 3
extract:
baseUrl: https://example.com/v1
model: qwen-plus
apiKey: secret-key
workflows:
solve-issue:
hash: SPVR4BDMSGC1W
timestamp: 1
history: []
`;
const r = parseWorkflowRegistryYaml(yaml);
expect(r.ok).toBe(true);
if (!r.ok) {
return;
}
expect(r.value.config).not.toBeNull();
if (r.value.config === null) {
return;
}
expect(r.value.config.maxDepth).toBe(3);
expect(r.value.config.extract.baseUrl).toBe("https://example.com/v1");
expect(r.value.config.extract.model).toBe("qwen-plus");
expect(r.value.config.extract.apiKey).toBe("secret-key");
});
test("parses config apiKey env: prefix from process.env", () => {
const prev = process.env.WF_REGISTRY_TEST_API_KEY;
process.env.WF_REGISTRY_TEST_API_KEY = "from-env";
try {
const yaml = `
config:
maxDepth: 1
extract:
baseUrl: https://dashscope.aliyuncs.com/compatible-mode/v1
model: qwen-plus
apiKey: env:WF_REGISTRY_TEST_API_KEY
workflows: {}
`;
const r = parseWorkflowRegistryYaml(yaml);
expect(r.ok).toBe(true);
if (!r.ok) {
return;
}
expect(r.value.config?.extract.apiKey).toBe("from-env");
} finally {
if (prev === undefined) {
delete process.env.WF_REGISTRY_TEST_API_KEY;
} else {
process.env.WF_REGISTRY_TEST_API_KEY = prev;
}
}
});
test("parse errors when env: apiKey variable is unset", () => {
const prev = process.env.WF_REGISTRY_TEST_API_KEY_UNSET;
delete process.env.WF_REGISTRY_TEST_API_KEY_UNSET;
try {
const yaml = `
config:
maxDepth: 1
extract:
baseUrl: https://example.com
model: m
apiKey: env:WF_REGISTRY_TEST_API_KEY_UNSET
workflows: {}
`;
const r = parseWorkflowRegistryYaml(yaml);
expect(r.ok).toBe(false);
} finally {
if (prev !== undefined) {
process.env.WF_REGISTRY_TEST_API_KEY_UNSET = prev;
}
}
});
test("parse errors on invalid shape", async () => {
const dir = join(tmpdir(), `wf-reg3-${process.pid}-${Date.now()}`);
await mkdir(dir, { recursive: true });