@studio-foundation/engine 0.3.0-beta.1 → 0.3.0-beta.6
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/dist/__tests__/engine.conditions-group.test.d.ts +2 -0
- package/dist/__tests__/engine.conditions-group.test.d.ts.map +1 -0
- package/dist/__tests__/engine.conditions-group.test.js +222 -0
- package/dist/__tests__/engine.conditions-group.test.js.map +1 -0
- package/dist/__tests__/engine.conditions.test.d.ts +2 -0
- package/dist/__tests__/engine.conditions.test.d.ts.map +1 -0
- package/dist/__tests__/engine.conditions.test.js +164 -0
- package/dist/__tests__/engine.conditions.test.js.map +1 -0
- package/dist/__tests__/engine.resume.test.d.ts +2 -0
- package/dist/__tests__/engine.resume.test.d.ts.map +1 -0
- package/dist/__tests__/engine.resume.test.js +93 -0
- package/dist/__tests__/engine.resume.test.js.map +1 -0
- package/dist/__tests__/engine.script-stage.test.d.ts +2 -0
- package/dist/__tests__/engine.script-stage.test.d.ts.map +1 -0
- package/dist/__tests__/engine.script-stage.test.js +108 -0
- package/dist/__tests__/engine.script-stage.test.js.map +1 -0
- package/dist/db/client.d.ts +3 -0
- package/dist/db/client.d.ts.map +1 -0
- package/{src/db/client.ts → dist/db/client.js} +1 -2
- package/dist/db/client.js.map +1 -0
- package/dist/engine.context-event.test.d.ts +2 -0
- package/dist/engine.context-event.test.d.ts.map +1 -0
- package/dist/engine.context-event.test.js +111 -0
- package/dist/engine.context-event.test.js.map +1 -0
- package/dist/engine.d.ts +52 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +364 -0
- package/dist/engine.js.map +1 -0
- package/dist/events.d.ts +163 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +12 -0
- package/dist/events.js.map +1 -0
- package/{src/index.ts → dist/index.d.ts} +3 -45
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline/agent-loader.d.ts +4 -0
- package/dist/pipeline/agent-loader.d.ts.map +1 -0
- package/dist/pipeline/agent-loader.js +28 -0
- package/dist/pipeline/agent-loader.js.map +1 -0
- package/dist/pipeline/agent-loader.test.d.ts +2 -0
- package/dist/pipeline/agent-loader.test.d.ts.map +1 -0
- package/dist/pipeline/agent-loader.test.js +134 -0
- package/dist/pipeline/agent-loader.test.js.map +1 -0
- package/dist/pipeline/condition-evaluator.d.ts +6 -0
- package/dist/pipeline/condition-evaluator.d.ts.map +1 -0
- package/dist/pipeline/condition-evaluator.js +105 -0
- package/dist/pipeline/condition-evaluator.js.map +1 -0
- package/dist/pipeline/condition-evaluator.test.d.ts +2 -0
- package/dist/pipeline/condition-evaluator.test.d.ts.map +1 -0
- package/dist/pipeline/condition-evaluator.test.js +103 -0
- package/dist/pipeline/condition-evaluator.test.js.map +1 -0
- package/dist/pipeline/context-pack-loader.d.ts +3 -0
- package/dist/pipeline/context-pack-loader.d.ts.map +1 -0
- package/dist/pipeline/context-pack-loader.js +51 -0
- package/dist/pipeline/context-pack-loader.js.map +1 -0
- package/dist/pipeline/context-propagation.d.ts +28 -0
- package/dist/pipeline/context-propagation.d.ts.map +1 -0
- package/dist/pipeline/context-propagation.js +161 -0
- package/dist/pipeline/context-propagation.js.map +1 -0
- package/dist/pipeline/context-propagation.test.d.ts +2 -0
- package/dist/pipeline/context-propagation.test.d.ts.map +1 -0
- package/dist/pipeline/context-propagation.test.js +194 -0
- package/dist/pipeline/context-propagation.test.js.map +1 -0
- package/dist/pipeline/contract-loader.d.ts +4 -0
- package/dist/pipeline/contract-loader.d.ts.map +1 -0
- package/dist/pipeline/contract-loader.js +30 -0
- package/dist/pipeline/contract-loader.js.map +1 -0
- package/dist/pipeline/group-orchestrator.d.ts +19 -0
- package/dist/pipeline/group-orchestrator.d.ts.map +1 -0
- package/dist/pipeline/group-orchestrator.js +376 -0
- package/dist/pipeline/group-orchestrator.js.map +1 -0
- package/dist/pipeline/hook-executor.d.ts +28 -0
- package/dist/pipeline/hook-executor.d.ts.map +1 -0
- package/dist/pipeline/hook-executor.js +63 -0
- package/dist/pipeline/hook-executor.js.map +1 -0
- package/dist/pipeline/hook-executor.test.d.ts +2 -0
- package/dist/pipeline/hook-executor.test.d.ts.map +1 -0
- package/dist/pipeline/hook-executor.test.js +71 -0
- package/dist/pipeline/hook-executor.test.js.map +1 -0
- package/dist/pipeline/invariants-loader.d.ts +6 -0
- package/dist/pipeline/invariants-loader.d.ts.map +1 -0
- package/dist/pipeline/invariants-loader.js +15 -0
- package/dist/pipeline/invariants-loader.js.map +1 -0
- package/dist/pipeline/invariants-loader.test.d.ts +2 -0
- package/dist/pipeline/invariants-loader.test.d.ts.map +1 -0
- package/dist/pipeline/invariants-loader.test.js +39 -0
- package/dist/pipeline/invariants-loader.test.js.map +1 -0
- package/dist/pipeline/loader.d.ts +5 -0
- package/dist/pipeline/loader.d.ts.map +1 -0
- package/dist/pipeline/loader.js +132 -0
- package/dist/pipeline/loader.js.map +1 -0
- package/dist/pipeline/loader.test.d.ts +2 -0
- package/dist/pipeline/loader.test.d.ts.map +1 -0
- package/dist/pipeline/loader.test.js +120 -0
- package/dist/pipeline/loader.test.js.map +1 -0
- package/dist/pipeline/output-validator.d.ts +10 -0
- package/dist/pipeline/output-validator.d.ts.map +1 -0
- package/dist/pipeline/output-validator.js +20 -0
- package/dist/pipeline/output-validator.js.map +1 -0
- package/dist/pipeline/output-validator.test.d.ts +2 -0
- package/dist/pipeline/output-validator.test.d.ts.map +1 -0
- package/dist/pipeline/output-validator.test.js +77 -0
- package/dist/pipeline/output-validator.test.js.map +1 -0
- package/dist/pipeline/post-validator.d.ts +8 -0
- package/dist/pipeline/post-validator.d.ts.map +1 -0
- package/dist/pipeline/post-validator.js +103 -0
- package/dist/pipeline/post-validator.js.map +1 -0
- package/dist/pipeline/skill-loader.d.ts +10 -0
- package/dist/pipeline/skill-loader.d.ts.map +1 -0
- package/dist/pipeline/skill-loader.js +26 -0
- package/dist/pipeline/skill-loader.js.map +1 -0
- package/dist/pipeline/skill-loader.test.d.ts +2 -0
- package/dist/pipeline/skill-loader.test.d.ts.map +1 -0
- package/dist/pipeline/skill-loader.test.js +38 -0
- package/dist/pipeline/skill-loader.test.js.map +1 -0
- package/dist/pipeline/stage-executor.d.ts +26 -0
- package/dist/pipeline/stage-executor.d.ts.map +1 -0
- package/dist/pipeline/stage-executor.js +540 -0
- package/dist/pipeline/stage-executor.js.map +1 -0
- package/dist/pipeline/stage-resolver.d.ts +3 -0
- package/dist/pipeline/stage-resolver.d.ts.map +1 -0
- package/dist/pipeline/stage-resolver.js +6 -0
- package/dist/pipeline/stage-resolver.js.map +1 -0
- package/dist/pipeline/startup-executor.d.ts +3 -0
- package/dist/pipeline/startup-executor.d.ts.map +1 -0
- package/dist/pipeline/startup-executor.js +23 -0
- package/dist/pipeline/startup-executor.js.map +1 -0
- package/dist/pipeline/startup-executor.test.d.ts +2 -0
- package/dist/pipeline/startup-executor.test.d.ts.map +1 -0
- package/dist/pipeline/startup-executor.test.js +34 -0
- package/dist/pipeline/startup-executor.test.js.map +1 -0
- package/dist/pipeline/types.d.ts +28 -0
- package/dist/pipeline/types.d.ts.map +1 -0
- package/dist/pipeline/types.js +12 -0
- package/dist/pipeline/types.js.map +1 -0
- package/dist/repo-resolver.d.ts +10 -0
- package/dist/repo-resolver.d.ts.map +1 -0
- package/dist/repo-resolver.js +39 -0
- package/dist/repo-resolver.js.map +1 -0
- package/dist/spawners/direct-engine-spawner.d.ts +8 -0
- package/dist/spawners/direct-engine-spawner.d.ts.map +1 -0
- package/dist/spawners/direct-engine-spawner.js +24 -0
- package/dist/spawners/direct-engine-spawner.js.map +1 -0
- package/dist/state/run-store.d.ts +74 -0
- package/dist/state/run-store.d.ts.map +1 -0
- package/dist/state/run-store.js +269 -0
- package/dist/state/run-store.js.map +1 -0
- package/dist/state/run-store.test.d.ts +2 -0
- package/dist/state/run-store.test.d.ts.map +1 -0
- package/dist/state/run-store.test.js +130 -0
- package/dist/state/run-store.test.js.map +1 -0
- package/dist/state/state-machine.d.ts +6 -0
- package/dist/state/state-machine.d.ts.map +1 -0
- package/dist/state/state-machine.js +27 -0
- package/dist/state/state-machine.js.map +1 -0
- package/dist/state/status-derivation.d.ts +12 -0
- package/dist/state/status-derivation.d.ts.map +1 -0
- package/dist/state/status-derivation.js +30 -0
- package/dist/state/status-derivation.js.map +1 -0
- package/package.json +7 -4
- package/ARCHITECTURE.md +0 -57
- package/src/__tests__/__fixtures__/script-stage/contracts/book-context.contract.yaml +0 -4
- package/src/__tests__/engine.conditions-group.test.ts +0 -244
- package/src/__tests__/engine.conditions.test.ts +0 -186
- package/src/__tests__/engine.resume.test.ts +0 -108
- package/src/__tests__/engine.script-stage.test.ts +0 -125
- package/src/engine.context-event.test.ts +0 -175
- package/src/engine.ts +0 -491
- package/src/events.ts +0 -167
- package/src/pipeline/agent-loader.test.ts +0 -151
- package/src/pipeline/agent-loader.ts +0 -39
- package/src/pipeline/condition-evaluator.test.ts +0 -129
- package/src/pipeline/condition-evaluator.ts +0 -121
- package/src/pipeline/context-pack-loader.ts +0 -63
- package/src/pipeline/context-propagation.test.ts +0 -237
- package/src/pipeline/context-propagation.ts +0 -235
- package/src/pipeline/contract-loader.ts +0 -41
- package/src/pipeline/group-orchestrator.ts +0 -483
- package/src/pipeline/hook-executor.test.ts +0 -121
- package/src/pipeline/hook-executor.ts +0 -87
- package/src/pipeline/invariants-loader.test.ts +0 -51
- package/src/pipeline/invariants-loader.ts +0 -14
- package/src/pipeline/loader.test.ts +0 -128
- package/src/pipeline/loader.ts +0 -149
- package/src/pipeline/output-validator.test.ts +0 -100
- package/src/pipeline/output-validator.ts +0 -40
- package/src/pipeline/post-validator.ts +0 -124
- package/src/pipeline/skill-loader.test.ts +0 -44
- package/src/pipeline/skill-loader.ts +0 -32
- package/src/pipeline/stage-executor.ts +0 -654
- package/src/pipeline/stage-resolver.ts +0 -8
- package/src/pipeline/startup-executor.test.ts +0 -37
- package/src/pipeline/startup-executor.ts +0 -32
- package/src/pipeline/types.ts +0 -42
- package/src/repo-resolver.ts +0 -61
- package/src/spawners/direct-engine-spawner.ts +0 -26
- package/src/state/run-store.test.ts +0 -157
- package/src/state/run-store.ts +0 -362
- package/src/state/state-machine.ts +0 -35
- package/src/state/status-derivation.ts +0 -36
- package/tests/context-pack-loader.test.ts +0 -113
- package/tests/context-propagation.test.ts +0 -267
- package/tests/direct-engine-spawner.test.ts +0 -102
- package/tests/e2e/feature-v5.test.ts +0 -57
- package/tests/events.test.ts +0 -56
- package/tests/fixtures/agents/test-agent.agent.yaml +0 -5
- package/tests/fixtures/contracts/code-gen.contract.yaml +0 -6
- package/tests/fixtures/contracts/qa-gate.contract.yaml +0 -15
- package/tests/fixtures/contracts/test-contract.contract.yaml +0 -6
- package/tests/fixtures/pipelines/group-test.pipeline.yaml +0 -40
- package/tests/fixtures/pipelines/simple.pipeline.yaml +0 -15
- package/tests/fixtures/pipelines/two-stage.pipeline.yaml +0 -24
- package/tests/fixtures/software/pipelines/feature-builder.pipeline.yaml +0 -75
- package/tests/fixtures/test-project/agents/test-agent.agent.yaml +0 -5
- package/tests/fixtures/test-project/contracts/basic-result.contract.yaml +0 -6
- package/tests/fixtures/test-project/contracts/code-gen.contract.yaml +0 -6
- package/tests/fixtures/test-project/contracts/maximum-only.contract.yaml +0 -8
- package/tests/fixtures/test-project/contracts/qa-gate.contract.yaml +0 -15
- package/tests/fixtures/test-project/contracts/strict-result.contract.yaml +0 -7
- package/tests/fixtures/test-project/contracts/test-contract.contract.yaml +0 -6
- package/tests/fixtures/test-project/pipelines/group-simple.pipeline.yaml +0 -26
- package/tests/fixtures/test-project/pipelines/group-test.pipeline.yaml +0 -40
- package/tests/fixtures/test-project/pipelines/hook-output-template.pipeline.yaml +0 -19
- package/tests/fixtures/test-project/pipelines/hook-reject-on-failure.pipeline.yaml +0 -19
- package/tests/fixtures/test-project/pipelines/maximum-only.pipeline.yaml +0 -15
- package/tests/fixtures/test-project/pipelines/parallel-collect-all-test.pipeline.yaml +0 -39
- package/tests/fixtures/test-project/pipelines/parallel-context-isolation-test.pipeline.yaml +0 -38
- package/tests/fixtures/test-project/pipelines/parallel-fail-test.pipeline.yaml +0 -39
- package/tests/fixtures/test-project/pipelines/parallel-test.pipeline.yaml +0 -39
- package/tests/fixtures/test-project/pipelines/parallel-then-sequential-test.pipeline.yaml +0 -38
- package/tests/fixtures/test-project/pipelines/simple.pipeline.yaml +0 -15
- package/tests/fixtures/test-project/pipelines/two-stage.pipeline.yaml +0 -24
- package/tests/fixtures/test-project/pipelines/with-startup.pipeline.yaml +0 -19
- package/tests/loader.test.ts +0 -385
- package/tests/post-validator.test.ts +0 -297
- package/tests/repo-resolver.test.ts +0 -102
- package/tests/run-store.test.ts +0 -143
- package/tests/state-machine.test.ts +0 -110
- package/tests/unit/context-propagation.test.ts +0 -45
- package/tests/unit/engine.test.ts +0 -770
- package/tests/unit/group-loop.test.ts +0 -389
- package/tests/unit/group-parallel.test.ts +0 -423
- package/tests/unit/state/status-derivation.test.ts +0 -88
- package/tsconfig.json +0 -24
- package/vitest.config.ts +0 -14
package/tests/loader.test.ts
DELETED
|
@@ -1,385 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { parsePipelineYaml, loadPipeline, loadPipelineByName } from '../src/pipeline/loader.js';
|
|
4
|
-
import { parseAgentYaml } from '../src/pipeline/agent-loader.js';
|
|
5
|
-
import { parseContractYaml } from '../src/pipeline/contract-loader.js';
|
|
6
|
-
import { isStageGroup } from '@studio-foundation/contracts';
|
|
7
|
-
|
|
8
|
-
const FIXTURES_DIR = join(import.meta.dirname, 'fixtures');
|
|
9
|
-
const PIPELINES_DIR = join(FIXTURES_DIR, 'software', 'pipelines');
|
|
10
|
-
|
|
11
|
-
describe('parsePipelineYaml', () => {
|
|
12
|
-
it('parses a valid pipeline YAML', () => {
|
|
13
|
-
const yamlContent = `
|
|
14
|
-
name: test-pipeline
|
|
15
|
-
description: A test pipeline
|
|
16
|
-
version: 1
|
|
17
|
-
stages:
|
|
18
|
-
- name: stage-1
|
|
19
|
-
kind: analysis
|
|
20
|
-
agent: analyst
|
|
21
|
-
ralph:
|
|
22
|
-
max_attempts: 3
|
|
23
|
-
`;
|
|
24
|
-
const pipeline = parsePipelineYaml(yamlContent);
|
|
25
|
-
expect(pipeline.name).toBe('test-pipeline');
|
|
26
|
-
expect(pipeline.stages).toHaveLength(1);
|
|
27
|
-
expect(pipeline.stages[0].name).toBe('stage-1');
|
|
28
|
-
expect(pipeline.stages[0].kind).toBe('analysis');
|
|
29
|
-
expect(pipeline.stages[0].agent).toBe('analyst');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('throws when name is missing', () => {
|
|
33
|
-
const yamlContent = `
|
|
34
|
-
stages:
|
|
35
|
-
- name: s1
|
|
36
|
-
kind: analysis
|
|
37
|
-
agent: a
|
|
38
|
-
`;
|
|
39
|
-
expect(() => parsePipelineYaml(yamlContent)).toThrow("missing required field 'name'");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('throws when stages is missing', () => {
|
|
43
|
-
const yamlContent = `
|
|
44
|
-
name: bad-pipeline
|
|
45
|
-
`;
|
|
46
|
-
expect(() => parsePipelineYaml(yamlContent)).toThrow("missing required field 'stages'");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('throws when stages is empty', () => {
|
|
50
|
-
const yamlContent = `
|
|
51
|
-
name: empty-pipeline
|
|
52
|
-
stages: []
|
|
53
|
-
`;
|
|
54
|
-
expect(() => parsePipelineYaml(yamlContent)).toThrow('at least one stage');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('throws when a stage is missing kind', () => {
|
|
58
|
-
const yamlContent = `
|
|
59
|
-
name: bad-stages
|
|
60
|
-
stages:
|
|
61
|
-
- name: no-kind
|
|
62
|
-
agent: analyst
|
|
63
|
-
`;
|
|
64
|
-
expect(() => parsePipelineYaml(yamlContent)).toThrow("missing 'kind'");
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('parses pipeline with repo config', () => {
|
|
68
|
-
const yamlContent = `
|
|
69
|
-
name: test-pipeline
|
|
70
|
-
description: Test
|
|
71
|
-
version: 1
|
|
72
|
-
repo:
|
|
73
|
-
url: https://github.com/test/repo
|
|
74
|
-
branch: main
|
|
75
|
-
stages:
|
|
76
|
-
- name: stage1
|
|
77
|
-
kind: analysis
|
|
78
|
-
agent: test-agent
|
|
79
|
-
`;
|
|
80
|
-
const pipeline = parsePipelineYaml(yamlContent);
|
|
81
|
-
expect(pipeline.repo?.url).toBe('https://github.com/test/repo');
|
|
82
|
-
expect(pipeline.repo?.branch).toBe('main');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('parses pipeline without repo config', () => {
|
|
86
|
-
const yamlContent = `
|
|
87
|
-
name: test-pipeline
|
|
88
|
-
description: Test
|
|
89
|
-
version: 1
|
|
90
|
-
stages:
|
|
91
|
-
- name: stage1
|
|
92
|
-
kind: analysis
|
|
93
|
-
agent: test-agent
|
|
94
|
-
`;
|
|
95
|
-
const pipeline = parsePipelineYaml(yamlContent);
|
|
96
|
-
expect(pipeline.repo).toBeUndefined();
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('throws when a stage is missing agent', () => {
|
|
100
|
-
const yamlContent = `
|
|
101
|
-
name: bad-stages
|
|
102
|
-
stages:
|
|
103
|
-
- name: no-agent
|
|
104
|
-
kind: analysis
|
|
105
|
-
`;
|
|
106
|
-
expect(() => parsePipelineYaml(yamlContent)).toThrow("missing 'agent'");
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('parses a pipeline with a group entry', () => {
|
|
110
|
-
const yamlContent = `
|
|
111
|
-
name: grouped-pipeline
|
|
112
|
-
description: Pipeline with a group
|
|
113
|
-
version: 2
|
|
114
|
-
stages:
|
|
115
|
-
- name: analysis
|
|
116
|
-
kind: analysis
|
|
117
|
-
agent: analyst
|
|
118
|
-
- group: review-loop
|
|
119
|
-
max_iterations: 3
|
|
120
|
-
stages:
|
|
121
|
-
- name: code-gen
|
|
122
|
-
kind: code_generation
|
|
123
|
-
agent: coder
|
|
124
|
-
- name: qa
|
|
125
|
-
kind: qa
|
|
126
|
-
agent: analyst
|
|
127
|
-
`;
|
|
128
|
-
const pipeline = parsePipelineYaml(yamlContent);
|
|
129
|
-
expect(pipeline.stages).toHaveLength(2);
|
|
130
|
-
|
|
131
|
-
// First entry is a regular stage
|
|
132
|
-
const first = pipeline.stages[0];
|
|
133
|
-
expect(isStageGroup(first)).toBe(false);
|
|
134
|
-
expect((first as any).name).toBe('analysis');
|
|
135
|
-
|
|
136
|
-
// Second entry is a group
|
|
137
|
-
const second = pipeline.stages[1];
|
|
138
|
-
expect(isStageGroup(second)).toBe(true);
|
|
139
|
-
if (isStageGroup(second)) {
|
|
140
|
-
expect(second.group).toBe('review-loop');
|
|
141
|
-
expect(second.max_iterations).toBe(3);
|
|
142
|
-
expect(second.stages).toHaveLength(2);
|
|
143
|
-
expect(second.stages[0].name).toBe('code-gen');
|
|
144
|
-
expect(second.stages[1].name).toBe('qa');
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('parses max_tool_calls from the ralph block', () => {
|
|
149
|
-
const yamlContent = `
|
|
150
|
-
name: test-pipeline
|
|
151
|
-
version: 1
|
|
152
|
-
stages:
|
|
153
|
-
- name: code-gen
|
|
154
|
-
kind: code_generation
|
|
155
|
-
agent: coder
|
|
156
|
-
ralph:
|
|
157
|
-
max_attempts: 5
|
|
158
|
-
max_tool_calls: 200
|
|
159
|
-
`;
|
|
160
|
-
const pipeline = parsePipelineYaml(yamlContent);
|
|
161
|
-
const stage = pipeline.stages[0] as any;
|
|
162
|
-
expect(stage.ralph?.max_tool_calls).toBe(200);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('leaves max_tool_calls undefined when not specified', () => {
|
|
166
|
-
const yamlContent = `
|
|
167
|
-
name: test-pipeline
|
|
168
|
-
version: 1
|
|
169
|
-
stages:
|
|
170
|
-
- name: brief-analysis
|
|
171
|
-
kind: analysis
|
|
172
|
-
agent: analyst
|
|
173
|
-
ralph:
|
|
174
|
-
max_attempts: 3
|
|
175
|
-
`;
|
|
176
|
-
const pipeline = parsePipelineYaml(yamlContent);
|
|
177
|
-
const stage = pipeline.stages[0] as any;
|
|
178
|
-
expect(stage.ralph?.max_tool_calls).toBeUndefined();
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('defaults max_iterations to 3 when not specified', () => {
|
|
182
|
-
const yamlContent = `
|
|
183
|
-
name: default-iters
|
|
184
|
-
version: 1
|
|
185
|
-
stages:
|
|
186
|
-
- group: loop
|
|
187
|
-
stages:
|
|
188
|
-
- name: s1
|
|
189
|
-
kind: analysis
|
|
190
|
-
agent: a
|
|
191
|
-
- name: s2
|
|
192
|
-
kind: qa
|
|
193
|
-
agent: a
|
|
194
|
-
`;
|
|
195
|
-
const pipeline = parsePipelineYaml(yamlContent);
|
|
196
|
-
const g = pipeline.stages[0];
|
|
197
|
-
expect(isStageGroup(g)).toBe(true);
|
|
198
|
-
if (isStageGroup(g)) {
|
|
199
|
-
expect(g.max_iterations).toBe(3);
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('throws when a group has no stages', () => {
|
|
204
|
-
const yamlContent = `
|
|
205
|
-
name: bad-group
|
|
206
|
-
version: 1
|
|
207
|
-
stages:
|
|
208
|
-
- group: empty-loop
|
|
209
|
-
stages: []
|
|
210
|
-
`;
|
|
211
|
-
expect(() => parsePipelineYaml(yamlContent)).toThrow('at least 2 stages');
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('throws when a group has only 1 stage', () => {
|
|
215
|
-
const yamlContent = `
|
|
216
|
-
name: bad-group
|
|
217
|
-
version: 1
|
|
218
|
-
stages:
|
|
219
|
-
- group: solo-loop
|
|
220
|
-
stages:
|
|
221
|
-
- name: only-one
|
|
222
|
-
kind: analysis
|
|
223
|
-
agent: a
|
|
224
|
-
`;
|
|
225
|
-
expect(() => parsePipelineYaml(yamlContent)).toThrow('at least 2 stages');
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
describe('loadPipeline — real files', () => {
|
|
230
|
-
it('loads feature-builder.pipeline.yaml', async () => {
|
|
231
|
-
const pipeline = await loadPipeline(join(PIPELINES_DIR, 'feature-builder.pipeline.yaml'));
|
|
232
|
-
expect(pipeline.name).toBe('feature-builder');
|
|
233
|
-
// 2 simple stages + 1 group = 3 entries
|
|
234
|
-
expect(pipeline.stages).toHaveLength(3);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it('loadPipelineByName resolves the path', async () => {
|
|
238
|
-
const pipeline = await loadPipelineByName('feature-builder', PIPELINES_DIR);
|
|
239
|
-
expect(pipeline.name).toBe('feature-builder');
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('throws for non-existent pipeline', async () => {
|
|
243
|
-
await expect(
|
|
244
|
-
loadPipeline('/nonexistent/path/pipeline.yaml')
|
|
245
|
-
).rejects.toThrow('Failed to load pipeline');
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
describe('parseAgentYaml', () => {
|
|
250
|
-
it('parses a valid agent YAML', () => {
|
|
251
|
-
const yamlContent = `
|
|
252
|
-
name: test-agent
|
|
253
|
-
provider: anthropic
|
|
254
|
-
model: claude-sonnet-4-20250514
|
|
255
|
-
temperature: 0.3
|
|
256
|
-
tools:
|
|
257
|
-
- repo_manager.read_file
|
|
258
|
-
system_prompt: You are a test agent.
|
|
259
|
-
`;
|
|
260
|
-
const agent = parseAgentYaml(yamlContent);
|
|
261
|
-
expect(agent.name).toBe('test-agent');
|
|
262
|
-
expect(agent.provider).toBe('anthropic');
|
|
263
|
-
expect(agent.model).toBe('claude-sonnet-4-20250514');
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it('throws when name is missing', () => {
|
|
267
|
-
expect(() => parseAgentYaml('provider: x\nmodel: y')).toThrow("missing required field 'name'");
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it('allows missing provider (defaults applied at runtime)', () => {
|
|
271
|
-
const agent = parseAgentYaml('name: x\nmodel: y');
|
|
272
|
-
expect(agent.name).toBe('x');
|
|
273
|
-
expect(agent.provider).toBeUndefined();
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it('allows missing model (defaults applied at runtime)', () => {
|
|
277
|
-
const agent = parseAgentYaml('name: x\nprovider: y');
|
|
278
|
-
expect(agent.name).toBe('x');
|
|
279
|
-
expect(agent.model).toBeUndefined();
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
describe('parseContractYaml', () => {
|
|
284
|
-
it('parses a valid contract YAML', () => {
|
|
285
|
-
const yamlContent = `
|
|
286
|
-
name: test-contract
|
|
287
|
-
version: 1
|
|
288
|
-
schema:
|
|
289
|
-
required_fields:
|
|
290
|
-
- summary
|
|
291
|
-
- details
|
|
292
|
-
`;
|
|
293
|
-
const contract = parseContractYaml(yamlContent);
|
|
294
|
-
expect(contract.name).toBe('test-contract');
|
|
295
|
-
expect(contract.version).toBe(1);
|
|
296
|
-
expect(contract.schema?.required_fields).toEqual(['summary', 'details']);
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it('throws when name is missing', () => {
|
|
300
|
-
expect(() => parseContractYaml('version: 1')).toThrow("missing required field 'name'");
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it('throws when version is missing', () => {
|
|
304
|
-
expect(() => parseContractYaml('name: x')).toThrow("missing required field 'version'");
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
describe('parsePipelineYaml — parallel group', () => {
|
|
309
|
-
it('parses mode: parallel and on_failure: collect-all', () => {
|
|
310
|
-
const yaml = `
|
|
311
|
-
name: parallel-test
|
|
312
|
-
description: parallel
|
|
313
|
-
version: 1
|
|
314
|
-
stages:
|
|
315
|
-
- group: work
|
|
316
|
-
mode: parallel
|
|
317
|
-
on_failure: collect-all
|
|
318
|
-
stages:
|
|
319
|
-
- name: stage-a
|
|
320
|
-
kind: analysis
|
|
321
|
-
agent: analyst
|
|
322
|
-
- name: stage-b
|
|
323
|
-
kind: analysis
|
|
324
|
-
agent: analyst
|
|
325
|
-
`;
|
|
326
|
-
const pipeline = parsePipelineYaml(yaml);
|
|
327
|
-
const group = pipeline.stages[0];
|
|
328
|
-
expect(isStageGroup(group)).toBe(true);
|
|
329
|
-
if (isStageGroup(group)) {
|
|
330
|
-
expect(group.mode).toBe('parallel');
|
|
331
|
-
expect(group.on_failure).toBe('collect-all');
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
it('defaults mode to sequential when omitted', () => {
|
|
336
|
-
const yaml = `
|
|
337
|
-
name: seq-test
|
|
338
|
-
description: seq
|
|
339
|
-
version: 1
|
|
340
|
-
stages:
|
|
341
|
-
- group: work
|
|
342
|
-
stages:
|
|
343
|
-
- name: stage-a
|
|
344
|
-
kind: analysis
|
|
345
|
-
agent: analyst
|
|
346
|
-
- name: stage-b
|
|
347
|
-
kind: analysis
|
|
348
|
-
agent: analyst
|
|
349
|
-
`;
|
|
350
|
-
const pipeline = parsePipelineYaml(yaml);
|
|
351
|
-
const group = pipeline.stages[0];
|
|
352
|
-
if (isStageGroup(group)) {
|
|
353
|
-
expect(group.mode).toBeUndefined();
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it('warns and sets max_iterations to 1 when mode is parallel and max_iterations > 1', () => {
|
|
358
|
-
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
359
|
-
const yaml = `
|
|
360
|
-
name: parallel-warn-test
|
|
361
|
-
description: parallel warn
|
|
362
|
-
version: 1
|
|
363
|
-
stages:
|
|
364
|
-
- group: work
|
|
365
|
-
mode: parallel
|
|
366
|
-
max_iterations: 3
|
|
367
|
-
stages:
|
|
368
|
-
- name: stage-a
|
|
369
|
-
kind: analysis
|
|
370
|
-
agent: analyst
|
|
371
|
-
- name: stage-b
|
|
372
|
-
kind: analysis
|
|
373
|
-
agent: analyst
|
|
374
|
-
`;
|
|
375
|
-
const pipeline = parsePipelineYaml(yaml);
|
|
376
|
-
const group = pipeline.stages[0];
|
|
377
|
-
if (isStageGroup(group)) {
|
|
378
|
-
expect(group.max_iterations).toBe(1);
|
|
379
|
-
expect(consoleSpy).toHaveBeenCalledWith(
|
|
380
|
-
expect.stringContaining("parallel group 'work' has max_iterations > 1")
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
consoleSpy.mockRestore();
|
|
384
|
-
});
|
|
385
|
-
});
|
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { postValidate } from '../src/pipeline/post-validator.js';
|
|
3
|
-
import type { OutputContract } from '@studio-foundation/contracts';
|
|
4
|
-
|
|
5
|
-
describe('postValidate', () => {
|
|
6
|
-
it('accepts when no post_validation config', () => {
|
|
7
|
-
const contract: OutputContract = { name: 'test', version: 1 };
|
|
8
|
-
const result = postValidate({ status: 'rejected' }, contract);
|
|
9
|
-
expect(result.accepted).toBe(true);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('accepts when status is in approved_values', () => {
|
|
13
|
-
const contract: OutputContract = {
|
|
14
|
-
name: 'qa',
|
|
15
|
-
version: 1,
|
|
16
|
-
post_validation: {
|
|
17
|
-
rejection_detection: { field: 'status', approved_values: ['approved'] },
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
const result = postValidate({ status: 'approved', summary: 'ok', issues: [] }, contract);
|
|
21
|
-
expect(result.accepted).toBe(true);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('accepts with approved_with_notes', () => {
|
|
25
|
-
const contract: OutputContract = {
|
|
26
|
-
name: 'qa',
|
|
27
|
-
version: 1,
|
|
28
|
-
post_validation: {
|
|
29
|
-
rejection_detection: { field: 'status', approved_values: ['approved', 'approved_with_notes'] },
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
const result = postValidate({ status: 'approved_with_notes', summary: 'minor issues', issues: [] }, contract);
|
|
33
|
-
expect(result.accepted).toBe(true);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('rejects when status is not in approved_values', () => {
|
|
37
|
-
const contract: OutputContract = {
|
|
38
|
-
name: 'qa',
|
|
39
|
-
version: 1,
|
|
40
|
-
post_validation: {
|
|
41
|
-
rejection_detection: {
|
|
42
|
-
field: 'status',
|
|
43
|
-
approved_values: ['approved', 'approved_with_notes'],
|
|
44
|
-
details_field: 'issues',
|
|
45
|
-
summary_field: 'summary',
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
const result = postValidate(
|
|
50
|
-
{ status: 'implementation_incomplete', summary: 'missing props', issues: ['bug 1', 'bug 2'] },
|
|
51
|
-
contract,
|
|
52
|
-
);
|
|
53
|
-
expect(result.accepted).toBe(false);
|
|
54
|
-
expect(result.rejection_reason).toContain('implementation_incomplete');
|
|
55
|
-
expect(result.rejection_reason).toContain('missing props');
|
|
56
|
-
expect(result.rejection_details).toEqual(['bug 1', 'bug 2']);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('extracts details from structured issues', () => {
|
|
60
|
-
const contract: OutputContract = {
|
|
61
|
-
name: 'qa',
|
|
62
|
-
version: 1,
|
|
63
|
-
post_validation: {
|
|
64
|
-
rejection_detection: {
|
|
65
|
-
field: 'status',
|
|
66
|
-
approved_values: ['approved'],
|
|
67
|
-
details_field: 'issues',
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
const result = postValidate(
|
|
72
|
-
{
|
|
73
|
-
status: 'needs_changes',
|
|
74
|
-
summary: 'issues found',
|
|
75
|
-
issues: [
|
|
76
|
-
{ severity: 'high', description: 'Missing prop' },
|
|
77
|
-
{ severity: 'low', description: 'Naming convention' },
|
|
78
|
-
],
|
|
79
|
-
},
|
|
80
|
-
contract,
|
|
81
|
-
);
|
|
82
|
-
expect(result.accepted).toBe(false);
|
|
83
|
-
expect(result.rejection_details).toEqual(['Missing prop', 'Naming convention']);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('accepts when output is not an object', () => {
|
|
87
|
-
const contract: OutputContract = {
|
|
88
|
-
name: 'qa',
|
|
89
|
-
version: 1,
|
|
90
|
-
post_validation: {
|
|
91
|
-
rejection_detection: { field: 'status', approved_values: ['approved'] },
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
expect(postValidate('just a string', contract).accepted).toBe(true);
|
|
95
|
-
expect(postValidate(null, contract).accepted).toBe(true);
|
|
96
|
-
expect(postValidate(undefined, contract).accepted).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('accepts when status field is not a string', () => {
|
|
100
|
-
const contract: OutputContract = {
|
|
101
|
-
name: 'qa',
|
|
102
|
-
version: 1,
|
|
103
|
-
post_validation: {
|
|
104
|
-
rejection_detection: { field: 'status', approved_values: ['approved'] },
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
const result = postValidate({ status: 42, summary: 'ok' }, contract);
|
|
108
|
-
expect(result.accepted).toBe(true);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('includes expected values in rejection reason', () => {
|
|
112
|
-
const contract: OutputContract = {
|
|
113
|
-
name: 'qa',
|
|
114
|
-
version: 1,
|
|
115
|
-
post_validation: {
|
|
116
|
-
rejection_detection: { field: 'status', approved_values: ['approved', 'approved_with_notes'] },
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
const result = postValidate({ status: 'failed', issues: [] }, contract);
|
|
120
|
-
expect(result.accepted).toBe(false);
|
|
121
|
-
expect(result.rejection_reason).toContain('approved or approved_with_notes');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('handles string issues field', () => {
|
|
125
|
-
const contract: OutputContract = {
|
|
126
|
-
name: 'qa',
|
|
127
|
-
version: 1,
|
|
128
|
-
post_validation: {
|
|
129
|
-
rejection_detection: {
|
|
130
|
-
field: 'status',
|
|
131
|
-
approved_values: ['approved'],
|
|
132
|
-
details_field: 'issues',
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
};
|
|
136
|
-
const result = postValidate(
|
|
137
|
-
{ status: 'rejected', issues: 'Something is wrong' },
|
|
138
|
-
contract,
|
|
139
|
-
);
|
|
140
|
-
expect(result.accepted).toBe(false);
|
|
141
|
-
expect(result.rejection_details).toEqual(['Something is wrong']);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('rejects when reject_if_non_empty field has items, even if status is approved', () => {
|
|
145
|
-
const contract: OutputContract = {
|
|
146
|
-
name: 'critique',
|
|
147
|
-
version: 1,
|
|
148
|
-
post_validation: {
|
|
149
|
-
rejection_detection: {
|
|
150
|
-
field: 'status',
|
|
151
|
-
approved_values: ['approved'],
|
|
152
|
-
details_field: 'issues',
|
|
153
|
-
summary_field: 'feedback',
|
|
154
|
-
reject_if_non_empty: 'issues',
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
const result = postValidate(
|
|
159
|
-
{ status: 'approved', feedback: 'Minor issues', issues: ['Too much salt'] },
|
|
160
|
-
contract,
|
|
161
|
-
);
|
|
162
|
-
expect(result.accepted).toBe(false);
|
|
163
|
-
expect(result.rejection_reason).toContain('non-empty');
|
|
164
|
-
expect(result.rejection_reason).toContain('Minor issues');
|
|
165
|
-
expect(result.rejection_details).toEqual(['Too much salt']);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('accepts when reject_if_non_empty field is an empty array', () => {
|
|
169
|
-
const contract: OutputContract = {
|
|
170
|
-
name: 'critique',
|
|
171
|
-
version: 1,
|
|
172
|
-
post_validation: {
|
|
173
|
-
rejection_detection: {
|
|
174
|
-
field: 'status',
|
|
175
|
-
approved_values: ['approved'],
|
|
176
|
-
reject_if_non_empty: 'issues',
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
};
|
|
180
|
-
const result = postValidate(
|
|
181
|
-
{ status: 'approved', issues: [] },
|
|
182
|
-
contract,
|
|
183
|
-
);
|
|
184
|
-
expect(result.accepted).toBe(true);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it('rejects with structured issues via reject_if_non_empty', () => {
|
|
188
|
-
const contract: OutputContract = {
|
|
189
|
-
name: 'critique',
|
|
190
|
-
version: 1,
|
|
191
|
-
post_validation: {
|
|
192
|
-
rejection_detection: {
|
|
193
|
-
field: 'status',
|
|
194
|
-
approved_values: ['approved'],
|
|
195
|
-
reject_if_non_empty: 'issues',
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
};
|
|
199
|
-
const result = postValidate(
|
|
200
|
-
{ status: 'approved', issues: [{ severity: 'low', description: 'Bland seasoning' }] },
|
|
201
|
-
contract,
|
|
202
|
-
);
|
|
203
|
-
expect(result.accepted).toBe(false);
|
|
204
|
-
expect(result.rejection_details).toEqual(['Bland seasoning']);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('extracts details from objects using "issue" field when "description" is absent', () => {
|
|
208
|
-
const contract: OutputContract = {
|
|
209
|
-
name: 'qa',
|
|
210
|
-
version: 1,
|
|
211
|
-
post_validation: {
|
|
212
|
-
rejection_detection: {
|
|
213
|
-
field: 'status',
|
|
214
|
-
approved_values: ['approved'],
|
|
215
|
-
details_field: 'issues',
|
|
216
|
-
},
|
|
217
|
-
},
|
|
218
|
-
};
|
|
219
|
-
const result = postValidate(
|
|
220
|
-
{
|
|
221
|
-
status: 'rejected',
|
|
222
|
-
issues: [
|
|
223
|
-
{ issue: 'Missing error handling', severity: 'high' },
|
|
224
|
-
{ issue: 'No input validation' },
|
|
225
|
-
],
|
|
226
|
-
},
|
|
227
|
-
contract,
|
|
228
|
-
);
|
|
229
|
-
expect(result.accepted).toBe(false);
|
|
230
|
-
expect(result.rejection_details).toEqual(['Missing error handling', 'No input validation']);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('extracts details from objects using "message" field as fallback', () => {
|
|
234
|
-
const contract: OutputContract = {
|
|
235
|
-
name: 'qa',
|
|
236
|
-
version: 1,
|
|
237
|
-
post_validation: {
|
|
238
|
-
rejection_detection: {
|
|
239
|
-
field: 'status',
|
|
240
|
-
approved_values: ['approved'],
|
|
241
|
-
details_field: 'issues',
|
|
242
|
-
},
|
|
243
|
-
},
|
|
244
|
-
};
|
|
245
|
-
const result = postValidate(
|
|
246
|
-
{
|
|
247
|
-
status: 'rejected',
|
|
248
|
-
issues: [
|
|
249
|
-
{ message: 'Auth token not refreshed', file: 'src/auth.ts' },
|
|
250
|
-
],
|
|
251
|
-
},
|
|
252
|
-
contract,
|
|
253
|
-
);
|
|
254
|
-
expect(result.accepted).toBe(false);
|
|
255
|
-
expect(result.rejection_details).toEqual(['Auth token not refreshed']);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('falls back to first string value for objects with unknown field names', () => {
|
|
259
|
-
const contract: OutputContract = {
|
|
260
|
-
name: 'qa',
|
|
261
|
-
version: 1,
|
|
262
|
-
post_validation: {
|
|
263
|
-
rejection_detection: {
|
|
264
|
-
field: 'status',
|
|
265
|
-
approved_values: ['approved'],
|
|
266
|
-
details_field: 'issues',
|
|
267
|
-
},
|
|
268
|
-
},
|
|
269
|
-
};
|
|
270
|
-
const result = postValidate(
|
|
271
|
-
{
|
|
272
|
-
status: 'rejected',
|
|
273
|
-
issues: [
|
|
274
|
-
{ problem: 'Missing test coverage', severity: 'medium' },
|
|
275
|
-
],
|
|
276
|
-
},
|
|
277
|
-
contract,
|
|
278
|
-
);
|
|
279
|
-
expect(result.accepted).toBe(false);
|
|
280
|
-
expect(result.rejection_details).toHaveLength(1);
|
|
281
|
-
// Should extract some string value from the object
|
|
282
|
-
expect(typeof result.rejection_details![0]).toBe('string');
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it('omits rejection_details when no details_field configured', () => {
|
|
286
|
-
const contract: OutputContract = {
|
|
287
|
-
name: 'qa',
|
|
288
|
-
version: 1,
|
|
289
|
-
post_validation: {
|
|
290
|
-
rejection_detection: { field: 'status', approved_values: ['approved'] },
|
|
291
|
-
},
|
|
292
|
-
};
|
|
293
|
-
const result = postValidate({ status: 'rejected' }, contract);
|
|
294
|
-
expect(result.accepted).toBe(false);
|
|
295
|
-
expect(result.rejection_details).toBeUndefined();
|
|
296
|
-
});
|
|
297
|
-
});
|