@renseiai/agentfactory 0.8.12 → 0.8.13
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/src/config/repository-config.d.ts +24 -0
- package/dist/src/config/repository-config.d.ts.map +1 -1
- package/dist/src/config/repository-config.js +21 -0
- package/dist/src/config/repository-config.test.js +202 -0
- package/dist/src/governor/decision-engine.d.ts +2 -0
- package/dist/src/governor/decision-engine.d.ts.map +1 -1
- package/dist/src/governor/decision-engine.js +7 -0
- package/dist/src/governor/decision-engine.test.js +63 -0
- package/dist/src/governor/governor-types.d.ts +2 -1
- package/dist/src/governor/governor-types.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/merge-queue/conflict-resolver.d.ts +62 -0
- package/dist/src/merge-queue/conflict-resolver.d.ts.map +1 -0
- package/dist/src/merge-queue/conflict-resolver.js +168 -0
- package/dist/src/merge-queue/conflict-resolver.test.d.ts +2 -0
- package/dist/src/merge-queue/conflict-resolver.test.d.ts.map +1 -0
- package/dist/src/merge-queue/conflict-resolver.test.js +405 -0
- package/dist/src/merge-queue/lock-file-regeneration.d.ts +14 -0
- package/dist/src/merge-queue/lock-file-regeneration.d.ts.map +1 -0
- package/dist/src/merge-queue/lock-file-regeneration.js +82 -0
- package/dist/src/merge-queue/lock-file-regeneration.test.d.ts +2 -0
- package/dist/src/merge-queue/lock-file-regeneration.test.d.ts.map +1 -0
- package/dist/src/merge-queue/lock-file-regeneration.test.js +236 -0
- package/dist/src/merge-queue/merge-worker.d.ts +79 -0
- package/dist/src/merge-queue/merge-worker.d.ts.map +1 -0
- package/dist/src/merge-queue/merge-worker.js +221 -0
- package/dist/src/merge-queue/merge-worker.test.d.ts +2 -0
- package/dist/src/merge-queue/merge-worker.test.d.ts.map +1 -0
- package/dist/src/merge-queue/merge-worker.test.js +883 -0
- package/dist/src/merge-queue/strategies/index.d.ts +19 -0
- package/dist/src/merge-queue/strategies/index.d.ts.map +1 -0
- package/dist/src/merge-queue/strategies/index.js +30 -0
- package/dist/src/merge-queue/strategies/merge-commit-strategy.d.ts +14 -0
- package/dist/src/merge-queue/strategies/merge-commit-strategy.d.ts.map +1 -0
- package/dist/src/merge-queue/strategies/merge-commit-strategy.js +58 -0
- package/dist/src/merge-queue/strategies/rebase-strategy.d.ts +14 -0
- package/dist/src/merge-queue/strategies/rebase-strategy.d.ts.map +1 -0
- package/dist/src/merge-queue/strategies/rebase-strategy.js +62 -0
- package/dist/src/merge-queue/strategies/squash-strategy.d.ts +14 -0
- package/dist/src/merge-queue/strategies/squash-strategy.d.ts.map +1 -0
- package/dist/src/merge-queue/strategies/squash-strategy.js +59 -0
- package/dist/src/merge-queue/strategies/strategies.test.d.ts +2 -0
- package/dist/src/merge-queue/strategies/strategies.test.d.ts.map +1 -0
- package/dist/src/merge-queue/strategies/strategies.test.js +354 -0
- package/dist/src/merge-queue/strategies/types.d.ts +62 -0
- package/dist/src/merge-queue/strategies/types.d.ts.map +1 -0
- package/dist/src/merge-queue/strategies/types.js +7 -0
- package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
- package/dist/src/orchestrator/parse-work-result.js +22 -0
- package/dist/src/orchestrator/parse-work-result.test.js +49 -0
- package/dist/src/providers/index.d.ts +1 -0
- package/dist/src/providers/index.d.ts.map +1 -1
- package/dist/src/providers/plugin-types.d.ts +177 -0
- package/dist/src/providers/plugin-types.d.ts.map +1 -0
- package/dist/src/providers/plugin-types.js +10 -0
- package/dist/src/providers/plugin-types.test.d.ts +2 -0
- package/dist/src/providers/plugin-types.test.d.ts.map +1 -0
- package/dist/src/providers/plugin-types.test.js +810 -0
- package/dist/src/registry/index.d.ts +4 -0
- package/dist/src/registry/index.d.ts.map +1 -0
- package/dist/src/registry/index.js +2 -0
- package/dist/src/registry/loader.d.ts +25 -0
- package/dist/src/registry/loader.d.ts.map +1 -0
- package/dist/src/registry/loader.js +88 -0
- package/dist/src/registry/node-type-registry.d.ts +52 -0
- package/dist/src/registry/node-type-registry.d.ts.map +1 -0
- package/dist/src/registry/node-type-registry.js +130 -0
- package/dist/src/registry/types.d.ts +65 -0
- package/dist/src/registry/types.d.ts.map +1 -0
- package/dist/src/registry/types.js +10 -0
- package/dist/src/workflow/expression/ast.d.ts +1 -1
- package/dist/src/workflow/expression/ast.d.ts.map +1 -1
- package/dist/src/workflow/expression/context.d.ts +4 -0
- package/dist/src/workflow/expression/context.d.ts.map +1 -1
- package/dist/src/workflow/expression/context.js +5 -1
- package/dist/src/workflow/expression/evaluator.d.ts.map +1 -1
- package/dist/src/workflow/expression/evaluator.js +24 -1
- package/dist/src/workflow/expression/evaluator.test.js +174 -0
- package/dist/src/workflow/expression/expression.test.js +140 -1
- package/dist/src/workflow/expression/helpers.d.ts +4 -0
- package/dist/src/workflow/expression/helpers.d.ts.map +1 -1
- package/dist/src/workflow/expression/helpers.js +51 -0
- package/dist/src/workflow/expression/index.d.ts +14 -0
- package/dist/src/workflow/expression/index.d.ts.map +1 -1
- package/dist/src/workflow/expression/index.js +28 -1
- package/dist/src/workflow/expression/lexer.d.ts.map +1 -1
- package/dist/src/workflow/expression/lexer.js +43 -0
- package/dist/src/workflow/expression/parser.js +1 -1
- package/dist/src/workflow/index.d.ts +3 -3
- package/dist/src/workflow/index.d.ts.map +1 -1
- package/dist/src/workflow/index.js +4 -2
- package/dist/src/workflow/workflow-loader.d.ts +8 -2
- package/dist/src/workflow/workflow-loader.d.ts.map +1 -1
- package/dist/src/workflow/workflow-loader.js +21 -2
- package/dist/src/workflow/workflow-types.d.ts +781 -12
- package/dist/src/workflow/workflow-types.d.ts.map +1 -1
- package/dist/src/workflow/workflow-types.js +248 -3
- package/dist/src/workflow/workflow-types.test.js +621 -1
- package/package.json +3 -2
|
@@ -2,7 +2,9 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { describe, it, expect, vi } from 'vitest';
|
|
4
4
|
import { parse as parseYaml } from 'yaml';
|
|
5
|
-
import { PhaseDefinitionSchema, PhaseOutputDeclarationSchema, PhaseInputDeclarationSchema, TransitionDefinitionSchema, EscalationLadderRungSchema, EscalationConfigSchema, GateDefinitionSchema, ParallelismGroupDefinitionSchema, WorkflowDefinitionSchema, validateWorkflowDefinition,
|
|
5
|
+
import { PhaseDefinitionSchema, PhaseOutputDeclarationSchema, PhaseInputDeclarationSchema, TransitionDefinitionSchema, EscalationLadderRungSchema, EscalationConfigSchema, GateDefinitionSchema, ParallelismGroupDefinitionSchema, WorkflowDefinitionSchema, validateWorkflowDefinition,
|
|
6
|
+
// v2 schemas
|
|
7
|
+
WorkflowTriggerDefinitionSchema, ProviderRequirementSchema, WorkflowConfigSchema, StepDefinitionSchema, NodeDefinitionSchema, WorkflowDefinitionV2Schema, AnyWorkflowDefinitionSchema, validateAnyWorkflowDefinition, crossValidateWorkflowV2, } from './workflow-types.js';
|
|
6
8
|
describe('PhaseDefinitionSchema', () => {
|
|
7
9
|
it('validates a minimal phase', () => {
|
|
8
10
|
const result = PhaseDefinitionSchema.parse({
|
|
@@ -729,3 +731,621 @@ describe('validateWorkflowDefinition cross-validation', () => {
|
|
|
729
731
|
expect(result.phases.find(p => p.name === 'qa')?.inputs).toBeDefined();
|
|
730
732
|
});
|
|
731
733
|
});
|
|
734
|
+
// ===========================================================================
|
|
735
|
+
// v2 Schema Tests
|
|
736
|
+
// ===========================================================================
|
|
737
|
+
describe('WorkflowTriggerDefinitionSchema', () => {
|
|
738
|
+
it('validates a webhook trigger', () => {
|
|
739
|
+
const result = WorkflowTriggerDefinitionSchema.parse({
|
|
740
|
+
name: 'issue-moved',
|
|
741
|
+
type: 'webhook',
|
|
742
|
+
source: 'linear',
|
|
743
|
+
event: 'issue.status_changed',
|
|
744
|
+
filter: { status: 'Backlog' },
|
|
745
|
+
});
|
|
746
|
+
expect(result.name).toBe('issue-moved');
|
|
747
|
+
expect(result.type).toBe('webhook');
|
|
748
|
+
expect(result.source).toBe('linear');
|
|
749
|
+
expect(result.event).toBe('issue.status_changed');
|
|
750
|
+
expect(result.filter).toEqual({ status: 'Backlog' });
|
|
751
|
+
});
|
|
752
|
+
it('validates a schedule trigger', () => {
|
|
753
|
+
const result = WorkflowTriggerDefinitionSchema.parse({
|
|
754
|
+
name: 'nightly-sweep',
|
|
755
|
+
type: 'schedule',
|
|
756
|
+
schedule: '0 2 * * *',
|
|
757
|
+
});
|
|
758
|
+
expect(result.type).toBe('schedule');
|
|
759
|
+
expect(result.schedule).toBe('0 2 * * *');
|
|
760
|
+
});
|
|
761
|
+
it('validates a manual trigger', () => {
|
|
762
|
+
const result = WorkflowTriggerDefinitionSchema.parse({
|
|
763
|
+
name: 'manual',
|
|
764
|
+
type: 'manual',
|
|
765
|
+
});
|
|
766
|
+
expect(result.type).toBe('manual');
|
|
767
|
+
expect(result.source).toBeUndefined();
|
|
768
|
+
});
|
|
769
|
+
it('rejects missing name', () => {
|
|
770
|
+
expect(() => WorkflowTriggerDefinitionSchema.parse({
|
|
771
|
+
type: 'webhook',
|
|
772
|
+
})).toThrow();
|
|
773
|
+
});
|
|
774
|
+
it('rejects empty name', () => {
|
|
775
|
+
expect(() => WorkflowTriggerDefinitionSchema.parse({
|
|
776
|
+
name: '',
|
|
777
|
+
type: 'webhook',
|
|
778
|
+
})).toThrow();
|
|
779
|
+
});
|
|
780
|
+
it('rejects invalid type', () => {
|
|
781
|
+
expect(() => WorkflowTriggerDefinitionSchema.parse({
|
|
782
|
+
name: 'test',
|
|
783
|
+
type: 'invalid',
|
|
784
|
+
})).toThrow();
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
describe('ProviderRequirementSchema', () => {
|
|
788
|
+
it('validates a provider with config', () => {
|
|
789
|
+
const result = ProviderRequirementSchema.parse({
|
|
790
|
+
name: 'coding-agent',
|
|
791
|
+
type: 'claude',
|
|
792
|
+
config: { model: 'claude-sonnet-4-5-20250929' },
|
|
793
|
+
});
|
|
794
|
+
expect(result.name).toBe('coding-agent');
|
|
795
|
+
expect(result.type).toBe('claude');
|
|
796
|
+
expect(result.config).toEqual({ model: 'claude-sonnet-4-5-20250929' });
|
|
797
|
+
});
|
|
798
|
+
it('validates a minimal provider', () => {
|
|
799
|
+
const result = ProviderRequirementSchema.parse({
|
|
800
|
+
name: 'tracker',
|
|
801
|
+
type: 'linear',
|
|
802
|
+
});
|
|
803
|
+
expect(result.config).toBeUndefined();
|
|
804
|
+
});
|
|
805
|
+
it('rejects missing name', () => {
|
|
806
|
+
expect(() => ProviderRequirementSchema.parse({ type: 'claude' })).toThrow();
|
|
807
|
+
});
|
|
808
|
+
it('rejects missing type', () => {
|
|
809
|
+
expect(() => ProviderRequirementSchema.parse({ name: 'agent' })).toThrow();
|
|
810
|
+
});
|
|
811
|
+
it('rejects empty type', () => {
|
|
812
|
+
expect(() => ProviderRequirementSchema.parse({ name: 'agent', type: '' })).toThrow();
|
|
813
|
+
});
|
|
814
|
+
});
|
|
815
|
+
describe('WorkflowConfigSchema', () => {
|
|
816
|
+
it('validates config with projectMapping', () => {
|
|
817
|
+
const result = WorkflowConfigSchema.parse({
|
|
818
|
+
projectMapping: { AgentFactory: './packages/core' },
|
|
819
|
+
});
|
|
820
|
+
expect(result.projectMapping).toEqual({ AgentFactory: './packages/core' });
|
|
821
|
+
});
|
|
822
|
+
it('validates empty config', () => {
|
|
823
|
+
const result = WorkflowConfigSchema.parse({});
|
|
824
|
+
expect(result.projectMapping).toBeUndefined();
|
|
825
|
+
});
|
|
826
|
+
it('allows extensible keys (passthrough)', () => {
|
|
827
|
+
const result = WorkflowConfigSchema.parse({
|
|
828
|
+
projectMapping: { AgentFactory: './packages/core' },
|
|
829
|
+
customSetting: true,
|
|
830
|
+
});
|
|
831
|
+
expect(result.customSetting).toBe(true);
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
describe('StepDefinitionSchema', () => {
|
|
835
|
+
it('validates a step with all fields', () => {
|
|
836
|
+
const result = StepDefinitionSchema.parse({
|
|
837
|
+
id: 'implement',
|
|
838
|
+
action: 'spawn-session',
|
|
839
|
+
with: {
|
|
840
|
+
template: 'development',
|
|
841
|
+
issue: '{{ trigger.issue.identifier }}',
|
|
842
|
+
},
|
|
843
|
+
when: '{{ trigger.event eq "issue.status_changed" }}',
|
|
844
|
+
});
|
|
845
|
+
expect(result.id).toBe('implement');
|
|
846
|
+
expect(result.action).toBe('spawn-session');
|
|
847
|
+
expect(result.with?.template).toBe('development');
|
|
848
|
+
expect(result.when).toContain('trigger.event');
|
|
849
|
+
});
|
|
850
|
+
it('validates a minimal step', () => {
|
|
851
|
+
const result = StepDefinitionSchema.parse({
|
|
852
|
+
id: 'run',
|
|
853
|
+
action: 'tracker.create-comment',
|
|
854
|
+
});
|
|
855
|
+
expect(result.with).toBeUndefined();
|
|
856
|
+
expect(result.when).toBeUndefined();
|
|
857
|
+
});
|
|
858
|
+
it('preserves {{ }} interpolation markers as strings', () => {
|
|
859
|
+
const result = StepDefinitionSchema.parse({
|
|
860
|
+
id: 'post-pr',
|
|
861
|
+
action: 'tracker.create-comment',
|
|
862
|
+
with: {
|
|
863
|
+
body: 'PR: {{ steps.implement.output.prUrl }}',
|
|
864
|
+
},
|
|
865
|
+
});
|
|
866
|
+
expect(result.with?.body).toBe('PR: {{ steps.implement.output.prUrl }}');
|
|
867
|
+
});
|
|
868
|
+
it('rejects missing id', () => {
|
|
869
|
+
expect(() => StepDefinitionSchema.parse({
|
|
870
|
+
action: 'spawn-session',
|
|
871
|
+
})).toThrow();
|
|
872
|
+
});
|
|
873
|
+
it('rejects empty id', () => {
|
|
874
|
+
expect(() => StepDefinitionSchema.parse({
|
|
875
|
+
id: '',
|
|
876
|
+
action: 'spawn-session',
|
|
877
|
+
})).toThrow();
|
|
878
|
+
});
|
|
879
|
+
it('rejects missing action', () => {
|
|
880
|
+
expect(() => StepDefinitionSchema.parse({
|
|
881
|
+
id: 'step1',
|
|
882
|
+
})).toThrow();
|
|
883
|
+
});
|
|
884
|
+
it('rejects empty action', () => {
|
|
885
|
+
expect(() => StepDefinitionSchema.parse({
|
|
886
|
+
id: 'step1',
|
|
887
|
+
action: '',
|
|
888
|
+
})).toThrow();
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
describe('NodeDefinitionSchema', () => {
|
|
892
|
+
it('validates a node with multi-step sequence', () => {
|
|
893
|
+
const result = NodeDefinitionSchema.parse({
|
|
894
|
+
name: 'develop',
|
|
895
|
+
description: 'Implement feature',
|
|
896
|
+
provider: 'coding-agent',
|
|
897
|
+
when: '{{ trigger.filter.status eq "Backlog" }}',
|
|
898
|
+
steps: [
|
|
899
|
+
{ id: 'implement', action: 'spawn-session', with: { template: 'development' } },
|
|
900
|
+
{ id: 'post-pr', action: 'tracker.create-comment', with: { body: '{{ steps.implement.output.prUrl }}' } },
|
|
901
|
+
],
|
|
902
|
+
timeout: { duration: '2h', action: 'escalate' },
|
|
903
|
+
retry: { maxAttempts: 3 },
|
|
904
|
+
});
|
|
905
|
+
expect(result.name).toBe('develop');
|
|
906
|
+
expect(result.steps).toHaveLength(2);
|
|
907
|
+
expect(result.provider).toBe('coding-agent');
|
|
908
|
+
expect(result.when).toContain('trigger.filter.status');
|
|
909
|
+
});
|
|
910
|
+
it('validates a minimal node', () => {
|
|
911
|
+
const result = NodeDefinitionSchema.parse({
|
|
912
|
+
name: 'simple',
|
|
913
|
+
});
|
|
914
|
+
expect(result.steps).toBeUndefined();
|
|
915
|
+
expect(result.provider).toBeUndefined();
|
|
916
|
+
expect(result.when).toBeUndefined();
|
|
917
|
+
});
|
|
918
|
+
it('validates a node with template reference (v1 compat)', () => {
|
|
919
|
+
const result = NodeDefinitionSchema.parse({
|
|
920
|
+
name: 'legacy-node',
|
|
921
|
+
template: 'development',
|
|
922
|
+
});
|
|
923
|
+
expect(result.template).toBe('development');
|
|
924
|
+
});
|
|
925
|
+
it('validates a node with outputs', () => {
|
|
926
|
+
const result = NodeDefinitionSchema.parse({
|
|
927
|
+
name: 'develop',
|
|
928
|
+
outputs: {
|
|
929
|
+
prUrl: { type: 'url', required: true },
|
|
930
|
+
branch: { type: 'string' },
|
|
931
|
+
},
|
|
932
|
+
});
|
|
933
|
+
expect(result.outputs?.prUrl.type).toBe('url');
|
|
934
|
+
expect(result.outputs?.prUrl.required).toBe(true);
|
|
935
|
+
});
|
|
936
|
+
it('rejects missing name', () => {
|
|
937
|
+
expect(() => NodeDefinitionSchema.parse({
|
|
938
|
+
provider: 'coding-agent',
|
|
939
|
+
})).toThrow();
|
|
940
|
+
});
|
|
941
|
+
it('rejects empty name', () => {
|
|
942
|
+
expect(() => NodeDefinitionSchema.parse({
|
|
943
|
+
name: '',
|
|
944
|
+
})).toThrow();
|
|
945
|
+
});
|
|
946
|
+
it('preserves when conditions as strings', () => {
|
|
947
|
+
const result = NodeDefinitionSchema.parse({
|
|
948
|
+
name: 'test',
|
|
949
|
+
when: '{{ trigger.event eq "issue.status_changed" and trigger.filter.status eq "Backlog" }}',
|
|
950
|
+
});
|
|
951
|
+
expect(result.when).toContain('trigger.event');
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
describe('WorkflowDefinitionV2Schema', () => {
|
|
955
|
+
it('validates a minimal v2 definition', () => {
|
|
956
|
+
const result = WorkflowDefinitionV2Schema.parse({
|
|
957
|
+
apiVersion: 'v2',
|
|
958
|
+
kind: 'WorkflowDefinition',
|
|
959
|
+
metadata: { name: 'minimal-v2' },
|
|
960
|
+
triggers: [{ name: 'manual', type: 'manual' }],
|
|
961
|
+
});
|
|
962
|
+
expect(result.apiVersion).toBe('v2');
|
|
963
|
+
expect(result.triggers).toHaveLength(1);
|
|
964
|
+
});
|
|
965
|
+
it('validates v2 with all optional sections', () => {
|
|
966
|
+
const result = WorkflowDefinitionV2Schema.parse({
|
|
967
|
+
apiVersion: 'v2',
|
|
968
|
+
kind: 'WorkflowDefinition',
|
|
969
|
+
metadata: { name: 'full-v2', description: 'Full v2 workflow' },
|
|
970
|
+
triggers: [{ name: 'webhook', type: 'webhook', source: 'linear', event: 'issue.status_changed' }],
|
|
971
|
+
providers: [{ name: 'agent', type: 'claude' }],
|
|
972
|
+
config: { projectMapping: { MyProject: './src' } },
|
|
973
|
+
nodes: [{ name: 'develop', provider: 'agent', steps: [{ id: 'run', action: 'spawn-session' }] }],
|
|
974
|
+
phases: [{ name: 'dev', template: 'development' }],
|
|
975
|
+
transitions: [{ from: 'Backlog', to: 'dev' }],
|
|
976
|
+
escalation: {
|
|
977
|
+
ladder: [{ cycle: 1, strategy: 'normal' }],
|
|
978
|
+
circuitBreaker: { maxSessionsPerIssue: 8 },
|
|
979
|
+
},
|
|
980
|
+
});
|
|
981
|
+
expect(result.triggers).toHaveLength(1);
|
|
982
|
+
expect(result.providers).toHaveLength(1);
|
|
983
|
+
expect(result.config?.projectMapping).toBeDefined();
|
|
984
|
+
expect(result.nodes).toHaveLength(1);
|
|
985
|
+
expect(result.phases).toHaveLength(1);
|
|
986
|
+
expect(result.transitions).toHaveLength(1);
|
|
987
|
+
expect(result.escalation).toBeDefined();
|
|
988
|
+
});
|
|
989
|
+
it('validates nodes with multi-step sequences', () => {
|
|
990
|
+
const result = WorkflowDefinitionV2Schema.parse({
|
|
991
|
+
apiVersion: 'v2',
|
|
992
|
+
kind: 'WorkflowDefinition',
|
|
993
|
+
metadata: { name: 'multi-step' },
|
|
994
|
+
nodes: [{
|
|
995
|
+
name: 'develop',
|
|
996
|
+
provider: 'coding-agent',
|
|
997
|
+
steps: [
|
|
998
|
+
{ id: 'implement', action: 'spawn-session', with: { template: 'development' } },
|
|
999
|
+
{ id: 'post-pr', action: 'tracker.create-comment', with: { body: '{{ steps.implement.output.prUrl }}' } },
|
|
1000
|
+
{ id: 'transition', action: 'tracker.update-issue', when: '{{ steps.implement.output.success }}' },
|
|
1001
|
+
],
|
|
1002
|
+
}],
|
|
1003
|
+
});
|
|
1004
|
+
expect(result.nodes[0].steps).toHaveLength(3);
|
|
1005
|
+
});
|
|
1006
|
+
it('preserves {{ }} template expressions in with parameters', () => {
|
|
1007
|
+
const result = WorkflowDefinitionV2Schema.parse({
|
|
1008
|
+
apiVersion: 'v2',
|
|
1009
|
+
kind: 'WorkflowDefinition',
|
|
1010
|
+
metadata: { name: 'template-test' },
|
|
1011
|
+
nodes: [{
|
|
1012
|
+
name: 'test-node',
|
|
1013
|
+
steps: [{
|
|
1014
|
+
id: 'step1',
|
|
1015
|
+
action: 'do-thing',
|
|
1016
|
+
with: {
|
|
1017
|
+
issue: '{{ trigger.issue.identifier }}',
|
|
1018
|
+
projectPath: '{{ config.projectMapping[trigger.issue.project] }}',
|
|
1019
|
+
},
|
|
1020
|
+
}],
|
|
1021
|
+
}],
|
|
1022
|
+
});
|
|
1023
|
+
const withParams = result.nodes[0].steps[0].with;
|
|
1024
|
+
expect(withParams.issue).toBe('{{ trigger.issue.identifier }}');
|
|
1025
|
+
expect(withParams.projectPath).toBe('{{ config.projectMapping[trigger.issue.project] }}');
|
|
1026
|
+
});
|
|
1027
|
+
it('preserves when conditions on nodes and steps', () => {
|
|
1028
|
+
const result = WorkflowDefinitionV2Schema.parse({
|
|
1029
|
+
apiVersion: 'v2',
|
|
1030
|
+
kind: 'WorkflowDefinition',
|
|
1031
|
+
metadata: { name: 'when-test' },
|
|
1032
|
+
nodes: [{
|
|
1033
|
+
name: 'conditional',
|
|
1034
|
+
when: '{{ trigger.event eq "issue.status_changed" }}',
|
|
1035
|
+
steps: [{
|
|
1036
|
+
id: 'step1',
|
|
1037
|
+
action: 'run',
|
|
1038
|
+
when: '{{ steps.prev.output.success }}',
|
|
1039
|
+
}],
|
|
1040
|
+
}],
|
|
1041
|
+
});
|
|
1042
|
+
expect(result.nodes[0].when).toBe('{{ trigger.event eq "issue.status_changed" }}');
|
|
1043
|
+
expect(result.nodes[0].steps[0].when).toBe('{{ steps.prev.output.success }}');
|
|
1044
|
+
});
|
|
1045
|
+
it('rejects invalid apiVersion', () => {
|
|
1046
|
+
expect(() => WorkflowDefinitionV2Schema.parse({
|
|
1047
|
+
apiVersion: 'v3',
|
|
1048
|
+
kind: 'WorkflowDefinition',
|
|
1049
|
+
metadata: { name: 'test' },
|
|
1050
|
+
})).toThrow();
|
|
1051
|
+
});
|
|
1052
|
+
it('rejects invalid kind', () => {
|
|
1053
|
+
expect(() => WorkflowDefinitionV2Schema.parse({
|
|
1054
|
+
apiVersion: 'v2',
|
|
1055
|
+
kind: 'WorkflowTemplate',
|
|
1056
|
+
metadata: { name: 'test' },
|
|
1057
|
+
})).toThrow();
|
|
1058
|
+
});
|
|
1059
|
+
it('rejects missing metadata.name', () => {
|
|
1060
|
+
expect(() => WorkflowDefinitionV2Schema.parse({
|
|
1061
|
+
apiVersion: 'v2',
|
|
1062
|
+
kind: 'WorkflowDefinition',
|
|
1063
|
+
metadata: {},
|
|
1064
|
+
})).toThrow();
|
|
1065
|
+
});
|
|
1066
|
+
it('rejects empty metadata.name', () => {
|
|
1067
|
+
expect(() => WorkflowDefinitionV2Schema.parse({
|
|
1068
|
+
apiVersion: 'v2',
|
|
1069
|
+
kind: 'WorkflowDefinition',
|
|
1070
|
+
metadata: { name: '' },
|
|
1071
|
+
})).toThrow();
|
|
1072
|
+
});
|
|
1073
|
+
});
|
|
1074
|
+
describe('AnyWorkflowDefinitionSchema', () => {
|
|
1075
|
+
it('dispatches v1.1 to WorkflowDefinitionSchema', () => {
|
|
1076
|
+
const result = AnyWorkflowDefinitionSchema.parse({
|
|
1077
|
+
apiVersion: 'v1.1',
|
|
1078
|
+
kind: 'WorkflowDefinition',
|
|
1079
|
+
metadata: { name: 'v1-test' },
|
|
1080
|
+
phases: [{ name: 'dev', template: 'development' }],
|
|
1081
|
+
transitions: [{ from: 'Backlog', to: 'dev' }],
|
|
1082
|
+
});
|
|
1083
|
+
expect(result.apiVersion).toBe('v1.1');
|
|
1084
|
+
});
|
|
1085
|
+
it('dispatches v2 to WorkflowDefinitionV2Schema', () => {
|
|
1086
|
+
const result = AnyWorkflowDefinitionSchema.parse({
|
|
1087
|
+
apiVersion: 'v2',
|
|
1088
|
+
kind: 'WorkflowDefinition',
|
|
1089
|
+
metadata: { name: 'v2-test' },
|
|
1090
|
+
triggers: [{ name: 'manual', type: 'manual' }],
|
|
1091
|
+
});
|
|
1092
|
+
expect(result.apiVersion).toBe('v2');
|
|
1093
|
+
});
|
|
1094
|
+
it('rejects missing apiVersion', () => {
|
|
1095
|
+
expect(() => AnyWorkflowDefinitionSchema.parse({
|
|
1096
|
+
kind: 'WorkflowDefinition',
|
|
1097
|
+
metadata: { name: 'test' },
|
|
1098
|
+
})).toThrow();
|
|
1099
|
+
});
|
|
1100
|
+
it('rejects unsupported apiVersion', () => {
|
|
1101
|
+
expect(() => AnyWorkflowDefinitionSchema.parse({
|
|
1102
|
+
apiVersion: 'v3',
|
|
1103
|
+
kind: 'WorkflowDefinition',
|
|
1104
|
+
metadata: { name: 'test' },
|
|
1105
|
+
})).toThrow();
|
|
1106
|
+
});
|
|
1107
|
+
});
|
|
1108
|
+
// ---------------------------------------------------------------------------
|
|
1109
|
+
// v2 Cross-Validation Tests
|
|
1110
|
+
// ---------------------------------------------------------------------------
|
|
1111
|
+
describe('crossValidateWorkflowV2', () => {
|
|
1112
|
+
/** Helper to build a minimal valid v2 workflow with overrides */
|
|
1113
|
+
function makeV2Workflow(overrides = {}) {
|
|
1114
|
+
return {
|
|
1115
|
+
apiVersion: 'v2',
|
|
1116
|
+
kind: 'WorkflowDefinition',
|
|
1117
|
+
metadata: { name: 'test-v2-cross-validation' },
|
|
1118
|
+
triggers: [{ name: 'manual', type: 'manual' }],
|
|
1119
|
+
providers: [
|
|
1120
|
+
{ name: 'coding-agent', type: 'claude' },
|
|
1121
|
+
{ name: 'tracker', type: 'linear' },
|
|
1122
|
+
],
|
|
1123
|
+
nodes: [{
|
|
1124
|
+
name: 'develop',
|
|
1125
|
+
provider: 'coding-agent',
|
|
1126
|
+
steps: [
|
|
1127
|
+
{ id: 'implement', action: 'spawn-session', with: { template: 'development' } },
|
|
1128
|
+
{ id: 'post-pr', action: 'tracker.create-comment', with: { body: '{{ steps.implement.output.prUrl }}' } },
|
|
1129
|
+
],
|
|
1130
|
+
}],
|
|
1131
|
+
...overrides,
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
it('passes for valid v2 workflow', () => {
|
|
1135
|
+
const workflow = WorkflowDefinitionV2Schema.parse(makeV2Workflow());
|
|
1136
|
+
expect(() => crossValidateWorkflowV2(workflow)).not.toThrow();
|
|
1137
|
+
});
|
|
1138
|
+
it('rejects duplicate node names', () => {
|
|
1139
|
+
const workflow = WorkflowDefinitionV2Schema.parse(makeV2Workflow({
|
|
1140
|
+
nodes: [
|
|
1141
|
+
{ name: 'develop', steps: [{ id: 's1', action: 'run' }] },
|
|
1142
|
+
{ name: 'develop', steps: [{ id: 's2', action: 'run' }] },
|
|
1143
|
+
],
|
|
1144
|
+
}));
|
|
1145
|
+
expect(() => crossValidateWorkflowV2(workflow)).toThrow('Duplicate node name "develop"');
|
|
1146
|
+
});
|
|
1147
|
+
it('rejects node referencing undefined provider', () => {
|
|
1148
|
+
const workflow = WorkflowDefinitionV2Schema.parse(makeV2Workflow({
|
|
1149
|
+
providers: [{ name: 'tracker', type: 'linear' }],
|
|
1150
|
+
nodes: [{
|
|
1151
|
+
name: 'develop',
|
|
1152
|
+
provider: 'coding-agent',
|
|
1153
|
+
steps: [{ id: 's1', action: 'run' }],
|
|
1154
|
+
}],
|
|
1155
|
+
}));
|
|
1156
|
+
expect(() => crossValidateWorkflowV2(workflow)).toThrow('Node "develop" references undefined provider "coding-agent"');
|
|
1157
|
+
});
|
|
1158
|
+
it('rejects duplicate step IDs within a node', () => {
|
|
1159
|
+
const workflow = WorkflowDefinitionV2Schema.parse(makeV2Workflow({
|
|
1160
|
+
nodes: [{
|
|
1161
|
+
name: 'develop',
|
|
1162
|
+
provider: 'coding-agent',
|
|
1163
|
+
steps: [
|
|
1164
|
+
{ id: 'run', action: 'spawn-session' },
|
|
1165
|
+
{ id: 'run', action: 'tracker.create-comment' },
|
|
1166
|
+
],
|
|
1167
|
+
}],
|
|
1168
|
+
}));
|
|
1169
|
+
expect(() => crossValidateWorkflowV2(workflow)).toThrow('Node "develop" has duplicate step ID "run"');
|
|
1170
|
+
});
|
|
1171
|
+
it('rejects step referencing undefined step output', () => {
|
|
1172
|
+
const workflow = WorkflowDefinitionV2Schema.parse(makeV2Workflow({
|
|
1173
|
+
nodes: [{
|
|
1174
|
+
name: 'develop',
|
|
1175
|
+
provider: 'coding-agent',
|
|
1176
|
+
steps: [
|
|
1177
|
+
{ id: 'implement', action: 'spawn-session' },
|
|
1178
|
+
{ id: 'post-pr', action: 'tracker.create-comment', with: { body: '{{ steps.implment.output.prUrl }}' } },
|
|
1179
|
+
],
|
|
1180
|
+
}],
|
|
1181
|
+
}));
|
|
1182
|
+
expect(() => crossValidateWorkflowV2(workflow)).toThrow('Node "develop" step "post-pr" references undefined step "implment"');
|
|
1183
|
+
});
|
|
1184
|
+
it('rejects trigger references when no triggers declared', () => {
|
|
1185
|
+
const workflow = WorkflowDefinitionV2Schema.parse(makeV2Workflow({
|
|
1186
|
+
triggers: undefined,
|
|
1187
|
+
nodes: [{
|
|
1188
|
+
name: 'develop',
|
|
1189
|
+
provider: 'coding-agent',
|
|
1190
|
+
when: '{{ trigger.event eq "issue.status_changed" }}',
|
|
1191
|
+
steps: [{ id: 's1', action: 'run' }],
|
|
1192
|
+
}],
|
|
1193
|
+
}));
|
|
1194
|
+
expect(() => crossValidateWorkflowV2(workflow)).toThrow('Node "develop" uses trigger references but no triggers are declared');
|
|
1195
|
+
});
|
|
1196
|
+
it('rejects trigger references in step with params when no triggers declared', () => {
|
|
1197
|
+
const workflow = WorkflowDefinitionV2Schema.parse(makeV2Workflow({
|
|
1198
|
+
triggers: undefined,
|
|
1199
|
+
nodes: [{
|
|
1200
|
+
name: 'develop',
|
|
1201
|
+
provider: 'coding-agent',
|
|
1202
|
+
steps: [{
|
|
1203
|
+
id: 's1',
|
|
1204
|
+
action: 'spawn-session',
|
|
1205
|
+
with: { issue: '{{ trigger.issue.identifier }}' },
|
|
1206
|
+
}],
|
|
1207
|
+
}],
|
|
1208
|
+
}));
|
|
1209
|
+
expect(() => crossValidateWorkflowV2(workflow)).toThrow('Node "develop" uses trigger references but no triggers are declared');
|
|
1210
|
+
});
|
|
1211
|
+
it('allows trigger references when triggers are declared', () => {
|
|
1212
|
+
const workflow = WorkflowDefinitionV2Schema.parse(makeV2Workflow({
|
|
1213
|
+
triggers: [{ name: 'webhook', type: 'webhook', source: 'linear', event: 'issue.status_changed' }],
|
|
1214
|
+
nodes: [{
|
|
1215
|
+
name: 'develop',
|
|
1216
|
+
provider: 'coding-agent',
|
|
1217
|
+
when: '{{ trigger.event eq "issue.status_changed" }}',
|
|
1218
|
+
steps: [{
|
|
1219
|
+
id: 's1',
|
|
1220
|
+
action: 'spawn-session',
|
|
1221
|
+
with: { issue: '{{ trigger.issue.identifier }}' },
|
|
1222
|
+
}],
|
|
1223
|
+
}],
|
|
1224
|
+
}));
|
|
1225
|
+
expect(() => crossValidateWorkflowV2(workflow)).not.toThrow();
|
|
1226
|
+
});
|
|
1227
|
+
it('rejects unbalanced brackets in when conditions', () => {
|
|
1228
|
+
const workflow = WorkflowDefinitionV2Schema.parse(makeV2Workflow({
|
|
1229
|
+
nodes: [{
|
|
1230
|
+
name: 'develop',
|
|
1231
|
+
provider: 'coding-agent',
|
|
1232
|
+
when: '{{ trigger.event eq "test"',
|
|
1233
|
+
steps: [{ id: 's1', action: 'run' }],
|
|
1234
|
+
}],
|
|
1235
|
+
}));
|
|
1236
|
+
expect(() => crossValidateWorkflowV2(workflow)).toThrow('unbalanced brackets');
|
|
1237
|
+
});
|
|
1238
|
+
it('rejects unbalanced brackets in step when conditions', () => {
|
|
1239
|
+
const workflow = WorkflowDefinitionV2Schema.parse(makeV2Workflow({
|
|
1240
|
+
nodes: [{
|
|
1241
|
+
name: 'develop',
|
|
1242
|
+
provider: 'coding-agent',
|
|
1243
|
+
steps: [{
|
|
1244
|
+
id: 's1',
|
|
1245
|
+
action: 'run',
|
|
1246
|
+
when: 'steps.implement.output.success }}',
|
|
1247
|
+
}],
|
|
1248
|
+
}],
|
|
1249
|
+
}));
|
|
1250
|
+
expect(() => crossValidateWorkflowV2(workflow)).toThrow('unbalanced brackets');
|
|
1251
|
+
});
|
|
1252
|
+
it('rejects empty config.projectMapping values', () => {
|
|
1253
|
+
const workflow = WorkflowDefinitionV2Schema.parse(makeV2Workflow({
|
|
1254
|
+
config: { projectMapping: { AgentFactory: '' } },
|
|
1255
|
+
}));
|
|
1256
|
+
expect(() => crossValidateWorkflowV2(workflow)).toThrow('config.projectMapping["AgentFactory"] has empty value');
|
|
1257
|
+
});
|
|
1258
|
+
it('includes file path in error messages', () => {
|
|
1259
|
+
const workflow = WorkflowDefinitionV2Schema.parse(makeV2Workflow({
|
|
1260
|
+
nodes: [
|
|
1261
|
+
{ name: 'a', steps: [{ id: 's1', action: 'run' }] },
|
|
1262
|
+
{ name: 'a', steps: [{ id: 's2', action: 'run' }] },
|
|
1263
|
+
],
|
|
1264
|
+
}));
|
|
1265
|
+
expect(() => crossValidateWorkflowV2(workflow, '/path/to/workflow.yaml')).toThrow('in /path/to/workflow.yaml');
|
|
1266
|
+
});
|
|
1267
|
+
it('validates node without provider (allowed)', () => {
|
|
1268
|
+
const workflow = WorkflowDefinitionV2Schema.parse(makeV2Workflow({
|
|
1269
|
+
nodes: [{
|
|
1270
|
+
name: 'utility',
|
|
1271
|
+
steps: [{ id: 's1', action: 'log' }],
|
|
1272
|
+
}],
|
|
1273
|
+
}));
|
|
1274
|
+
expect(() => crossValidateWorkflowV2(workflow)).not.toThrow();
|
|
1275
|
+
});
|
|
1276
|
+
});
|
|
1277
|
+
// ---------------------------------------------------------------------------
|
|
1278
|
+
// validateAnyWorkflowDefinition integration tests
|
|
1279
|
+
// ---------------------------------------------------------------------------
|
|
1280
|
+
describe('validateAnyWorkflowDefinition', () => {
|
|
1281
|
+
it('validates v1.1 workflows with cross-validation', () => {
|
|
1282
|
+
const result = validateAnyWorkflowDefinition({
|
|
1283
|
+
apiVersion: 'v1.1',
|
|
1284
|
+
kind: 'WorkflowDefinition',
|
|
1285
|
+
metadata: { name: 'test-v1' },
|
|
1286
|
+
phases: [{ name: 'dev', template: 'development' }],
|
|
1287
|
+
transitions: [{ from: 'Backlog', to: 'dev' }],
|
|
1288
|
+
});
|
|
1289
|
+
expect(result.apiVersion).toBe('v1.1');
|
|
1290
|
+
});
|
|
1291
|
+
it('validates v2 workflows with cross-validation', () => {
|
|
1292
|
+
const result = validateAnyWorkflowDefinition({
|
|
1293
|
+
apiVersion: 'v2',
|
|
1294
|
+
kind: 'WorkflowDefinition',
|
|
1295
|
+
metadata: { name: 'test-v2' },
|
|
1296
|
+
triggers: [{ name: 'manual', type: 'manual' }],
|
|
1297
|
+
providers: [{ name: 'agent', type: 'claude' }],
|
|
1298
|
+
nodes: [{
|
|
1299
|
+
name: 'develop',
|
|
1300
|
+
provider: 'agent',
|
|
1301
|
+
steps: [{ id: 'run', action: 'spawn-session' }],
|
|
1302
|
+
}],
|
|
1303
|
+
});
|
|
1304
|
+
expect(result.apiVersion).toBe('v2');
|
|
1305
|
+
});
|
|
1306
|
+
it('includes file path in error messages', () => {
|
|
1307
|
+
expect(() => validateAnyWorkflowDefinition({ invalid: true }, '/path/to/workflow.yaml')).toThrow('/path/to/workflow.yaml');
|
|
1308
|
+
});
|
|
1309
|
+
it('loads existing v1.1 workflow.yaml and validates', () => {
|
|
1310
|
+
const workflowPath = path.join(path.dirname(new URL(import.meta.url).pathname), 'defaults', 'workflow.yaml');
|
|
1311
|
+
const content = fs.readFileSync(workflowPath, 'utf-8');
|
|
1312
|
+
const data = parseYaml(content);
|
|
1313
|
+
const result = validateAnyWorkflowDefinition(data, workflowPath);
|
|
1314
|
+
expect(result.apiVersion).toBe('v1.1');
|
|
1315
|
+
expect(result.phases.length).toBeGreaterThan(0);
|
|
1316
|
+
});
|
|
1317
|
+
it('loads v1.1 parallel-example.yaml and validates', () => {
|
|
1318
|
+
const examplePath = path.join(path.dirname(new URL(import.meta.url).pathname), 'defaults', 'workflow-parallel-example.yaml');
|
|
1319
|
+
const content = fs.readFileSync(examplePath, 'utf-8');
|
|
1320
|
+
const data = parseYaml(content);
|
|
1321
|
+
const result = validateAnyWorkflowDefinition(data, examplePath);
|
|
1322
|
+
expect(result.apiVersion).toBe('v1.1');
|
|
1323
|
+
});
|
|
1324
|
+
it('loads v2 sdlc-loop.yaml and validates', () => {
|
|
1325
|
+
const sdlcPath = path.join(path.dirname(new URL(import.meta.url).pathname), 'defaults', 'sdlc-loop.yaml');
|
|
1326
|
+
const content = fs.readFileSync(sdlcPath, 'utf-8');
|
|
1327
|
+
const data = parseYaml(content);
|
|
1328
|
+
const result = validateAnyWorkflowDefinition(data, sdlcPath);
|
|
1329
|
+
expect(result.apiVersion).toBe('v2');
|
|
1330
|
+
if (result.apiVersion === 'v2') {
|
|
1331
|
+
expect(result.triggers.length).toBeGreaterThan(0);
|
|
1332
|
+
expect(result.providers.length).toBeGreaterThan(0);
|
|
1333
|
+
expect(result.nodes.length).toBeGreaterThan(0);
|
|
1334
|
+
expect(result.config?.projectMapping).toBeDefined();
|
|
1335
|
+
// v1.1 backwards compat sections
|
|
1336
|
+
expect(result.phases.length).toBeGreaterThan(0);
|
|
1337
|
+
expect(result.transitions.length).toBeGreaterThan(0);
|
|
1338
|
+
expect(result.escalation).toBeDefined();
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
it('validateWorkflowDefinition still works for v1.1 only', () => {
|
|
1342
|
+
const result = validateWorkflowDefinition({
|
|
1343
|
+
apiVersion: 'v1.1',
|
|
1344
|
+
kind: 'WorkflowDefinition',
|
|
1345
|
+
metadata: { name: 'test' },
|
|
1346
|
+
phases: [{ name: 'dev', template: 'development' }],
|
|
1347
|
+
transitions: [{ from: 'Backlog', to: 'dev' }],
|
|
1348
|
+
});
|
|
1349
|
+
expect(result.apiVersion).toBe('v1.1');
|
|
1350
|
+
});
|
|
1351
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@renseiai/agentfactory",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Multi-agent fleet management for coding agents — orchestrator, providers, crash recovery",
|
|
6
6
|
"author": "Rensei AI (https://rensei.ai)",
|
|
@@ -52,10 +52,11 @@
|
|
|
52
52
|
"zod": "^4.3.6"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
+
"@types/json-schema": "^7.0.15",
|
|
55
56
|
"@types/node": "^22.5.4",
|
|
56
57
|
"typescript": "^5.7.3",
|
|
57
58
|
"vitest": "^3.2.3",
|
|
58
|
-
"@renseiai/create-agentfactory-app": "0.8.
|
|
59
|
+
"@renseiai/create-agentfactory-app": "0.8.13"
|
|
59
60
|
},
|
|
60
61
|
"scripts": {
|
|
61
62
|
"build": "tsc",
|