@unlaxer/tramli-plugins 3.5.1 → 3.6.0

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.
@@ -1,4 +1,4 @@
1
- import type { FlowDefinition, FlowEngine } from '@unlaxer/tramli';
1
+ import type { FlowDefinition, FlowEngine, Builder } from '@unlaxer/tramli';
2
2
  import type { FlowPlugin } from './types.js';
3
3
  import { PluginReport } from './types.js';
4
4
  /**
@@ -10,6 +10,8 @@ export declare class PluginRegistry<S extends string> {
10
10
  register(plugin: FlowPlugin): this;
11
11
  /** Run all analysis plugins against a FlowDefinition. */
12
12
  analyzeAll(definition: FlowDefinition<S>): PluginReport;
13
+ /** Build a FlowDefinition and run all analysis plugins. Throws if any ERROR findings. */
14
+ buildAndAnalyze(builder: Builder<S>): FlowDefinition<S>;
13
15
  /** Apply all store plugins (wrapping in registration order). */
14
16
  applyStorePlugins(store: any): any;
15
17
  /** Install all engine plugins. */
@@ -22,6 +22,16 @@ class PluginRegistry {
22
22
  }
23
23
  return report;
24
24
  }
25
+ /** Build a FlowDefinition and run all analysis plugins. Throws if any ERROR findings. */
26
+ buildAndAnalyze(builder) {
27
+ const def = builder.build();
28
+ const report = this.analyzeAll(def);
29
+ const errors = report.findings().filter(f => f.severity === 'ERROR');
30
+ if (errors.length > 0) {
31
+ throw new Error(`Analysis errors:\n${errors.map(e => ` [${e.pluginId}] ${e.message}`).join('\n')}`);
32
+ }
33
+ return def;
34
+ }
25
35
  /** Apply all store plugins (wrapping in registration order). */
26
36
  applyStorePlugins(store) {
27
37
  let wrapped = store;
@@ -31,6 +31,7 @@ export { PolicyLintPlugin } from './lint/policy-lint-plugin.js';
31
31
  export { allDefaultPolicies } from './lint/default-flow-policies.js';
32
32
  export type { FlowPolicy } from './lint/types.js';
33
33
  export { ScenarioTestPlugin } from './testing/scenario-test-plugin.js';
34
+ export type { TestFramework } from './testing/scenario-test-plugin.js';
34
35
  export { ScenarioGenerationPlugin } from './testing/scenario-generation-plugin.js';
35
36
  export type { FlowScenario, FlowTestPlan, ScenarioKind } from './testing/types.js';
36
37
  export { GuaranteedSubflowValidator } from './subflow/guaranteed-subflow-validator.js';
@@ -1,9 +1,16 @@
1
1
  import type { FlowDefinition } from '@unlaxer/tramli';
2
2
  import type { FlowTestPlan } from './types.js';
3
+ export type TestFramework = 'vitest' | 'jest';
3
4
  /**
4
5
  * Generates BDD-style test scenarios from a flow definition.
5
6
  * Covers happy paths, error transitions, guard rejections, and timeout expiry.
6
7
  */
7
8
  export declare class ScenarioTestPlugin {
9
+ /**
10
+ * Generate executable test code from a flow definition.
11
+ * Produces a string of vitest/jest test code that validates transitions
12
+ * against the definition's structure (no FlowEngine required).
13
+ */
14
+ generateCode<S extends string>(definition: FlowDefinition<S>, framework?: TestFramework): string;
8
15
  generate<S extends string>(definition: FlowDefinition<S>): FlowTestPlan;
9
16
  }
@@ -6,6 +6,68 @@ exports.ScenarioTestPlugin = void 0;
6
6
  * Covers happy paths, error transitions, guard rejections, and timeout expiry.
7
7
  */
8
8
  class ScenarioTestPlugin {
9
+ /**
10
+ * Generate executable test code from a flow definition.
11
+ * Produces a string of vitest/jest test code that validates transitions
12
+ * against the definition's structure (no FlowEngine required).
13
+ */
14
+ generateCode(definition, framework = 'vitest') {
15
+ const plan = this.generate(definition);
16
+ const lines = [];
17
+ const imp = framework === 'vitest' ? "import { describe, it, expect } from 'vitest';" : '';
18
+ if (imp)
19
+ lines.push(imp);
20
+ lines.push('');
21
+ lines.push(`describe('${definition.name} scenarios', () => {`);
22
+ for (const scenario of plan.scenarios) {
23
+ lines.push(` it('${scenario.name}', () => {`);
24
+ for (const step of scenario.steps) {
25
+ lines.push(` // ${step}`);
26
+ }
27
+ // Add assertion based on scenario kind
28
+ switch (scenario.kind) {
29
+ case 'happy': {
30
+ const fromMatch = scenario.steps[0]?.match(/given flow in (\S+)/);
31
+ const toMatch = scenario.steps[scenario.steps.length - 1]?.match(/then flow reaches (\S+)/);
32
+ if (fromMatch && toMatch) {
33
+ const from = fromMatch[1];
34
+ const to = toMatch[1];
35
+ lines.push(` const transitions = definition.transitionsFrom('${from}');`);
36
+ lines.push(` expect(transitions.some(t => t.to === '${to}')).toBe(true);`);
37
+ }
38
+ break;
39
+ }
40
+ case 'error': {
41
+ const fromMatch = scenario.steps[0]?.match(/given flow in (\S+)/);
42
+ const toMatch = scenario.steps[scenario.steps.length - 1]?.match(/then flow transitions to (\S+)/);
43
+ if (fromMatch && toMatch) {
44
+ lines.push(` expect(definition.errorTransitions.get('${fromMatch[1]}')).toBe('${toMatch[1]}');`);
45
+ }
46
+ break;
47
+ }
48
+ case 'guard_rejection': {
49
+ const guardMatch = scenario.steps[1]?.match(/when guard (\S+) rejects/);
50
+ if (guardMatch) {
51
+ lines.push(` expect(definition.maxGuardRetries).toBeGreaterThan(0);`);
52
+ }
53
+ break;
54
+ }
55
+ case 'timeout': {
56
+ const fromMatch = scenario.steps[0]?.match(/given flow in (\S+)/);
57
+ if (fromMatch) {
58
+ lines.push(` const t = definition.transitionsFrom('${fromMatch[1]}').find(t => t.timeout != null);`);
59
+ lines.push(` expect(t).toBeDefined();`);
60
+ }
61
+ break;
62
+ }
63
+ }
64
+ lines.push(' });');
65
+ lines.push('');
66
+ }
67
+ lines.push('});');
68
+ lines.push('');
69
+ return lines.join('\n');
70
+ }
9
71
  generate(definition) {
10
72
  const scenarios = [];
11
73
  // Happy path scenarios from transitions
@@ -1,4 +1,4 @@
1
- import type { FlowDefinition, FlowEngine } from '@unlaxer/tramli';
1
+ import type { FlowDefinition, FlowEngine, Builder } from '@unlaxer/tramli';
2
2
  import type { FlowPlugin } from './types.js';
3
3
  import { PluginReport } from './types.js';
4
4
  /**
@@ -10,6 +10,8 @@ export declare class PluginRegistry<S extends string> {
10
10
  register(plugin: FlowPlugin): this;
11
11
  /** Run all analysis plugins against a FlowDefinition. */
12
12
  analyzeAll(definition: FlowDefinition<S>): PluginReport;
13
+ /** Build a FlowDefinition and run all analysis plugins. Throws if any ERROR findings. */
14
+ buildAndAnalyze(builder: Builder<S>): FlowDefinition<S>;
13
15
  /** Apply all store plugins (wrapping in registration order). */
14
16
  applyStorePlugins(store: any): any;
15
17
  /** Install all engine plugins. */
@@ -19,6 +19,16 @@ export class PluginRegistry {
19
19
  }
20
20
  return report;
21
21
  }
22
+ /** Build a FlowDefinition and run all analysis plugins. Throws if any ERROR findings. */
23
+ buildAndAnalyze(builder) {
24
+ const def = builder.build();
25
+ const report = this.analyzeAll(def);
26
+ const errors = report.findings().filter(f => f.severity === 'ERROR');
27
+ if (errors.length > 0) {
28
+ throw new Error(`Analysis errors:\n${errors.map(e => ` [${e.pluginId}] ${e.message}`).join('\n')}`);
29
+ }
30
+ return def;
31
+ }
22
32
  /** Apply all store plugins (wrapping in registration order). */
23
33
  applyStorePlugins(store) {
24
34
  let wrapped = store;
@@ -31,6 +31,7 @@ export { PolicyLintPlugin } from './lint/policy-lint-plugin.js';
31
31
  export { allDefaultPolicies } from './lint/default-flow-policies.js';
32
32
  export type { FlowPolicy } from './lint/types.js';
33
33
  export { ScenarioTestPlugin } from './testing/scenario-test-plugin.js';
34
+ export type { TestFramework } from './testing/scenario-test-plugin.js';
34
35
  export { ScenarioGenerationPlugin } from './testing/scenario-generation-plugin.js';
35
36
  export type { FlowScenario, FlowTestPlan, ScenarioKind } from './testing/types.js';
36
37
  export { GuaranteedSubflowValidator } from './subflow/guaranteed-subflow-validator.js';
@@ -1,9 +1,16 @@
1
1
  import type { FlowDefinition } from '@unlaxer/tramli';
2
2
  import type { FlowTestPlan } from './types.js';
3
+ export type TestFramework = 'vitest' | 'jest';
3
4
  /**
4
5
  * Generates BDD-style test scenarios from a flow definition.
5
6
  * Covers happy paths, error transitions, guard rejections, and timeout expiry.
6
7
  */
7
8
  export declare class ScenarioTestPlugin {
9
+ /**
10
+ * Generate executable test code from a flow definition.
11
+ * Produces a string of vitest/jest test code that validates transitions
12
+ * against the definition's structure (no FlowEngine required).
13
+ */
14
+ generateCode<S extends string>(definition: FlowDefinition<S>, framework?: TestFramework): string;
8
15
  generate<S extends string>(definition: FlowDefinition<S>): FlowTestPlan;
9
16
  }
@@ -3,6 +3,68 @@
3
3
  * Covers happy paths, error transitions, guard rejections, and timeout expiry.
4
4
  */
5
5
  export class ScenarioTestPlugin {
6
+ /**
7
+ * Generate executable test code from a flow definition.
8
+ * Produces a string of vitest/jest test code that validates transitions
9
+ * against the definition's structure (no FlowEngine required).
10
+ */
11
+ generateCode(definition, framework = 'vitest') {
12
+ const plan = this.generate(definition);
13
+ const lines = [];
14
+ const imp = framework === 'vitest' ? "import { describe, it, expect } from 'vitest';" : '';
15
+ if (imp)
16
+ lines.push(imp);
17
+ lines.push('');
18
+ lines.push(`describe('${definition.name} scenarios', () => {`);
19
+ for (const scenario of plan.scenarios) {
20
+ lines.push(` it('${scenario.name}', () => {`);
21
+ for (const step of scenario.steps) {
22
+ lines.push(` // ${step}`);
23
+ }
24
+ // Add assertion based on scenario kind
25
+ switch (scenario.kind) {
26
+ case 'happy': {
27
+ const fromMatch = scenario.steps[0]?.match(/given flow in (\S+)/);
28
+ const toMatch = scenario.steps[scenario.steps.length - 1]?.match(/then flow reaches (\S+)/);
29
+ if (fromMatch && toMatch) {
30
+ const from = fromMatch[1];
31
+ const to = toMatch[1];
32
+ lines.push(` const transitions = definition.transitionsFrom('${from}');`);
33
+ lines.push(` expect(transitions.some(t => t.to === '${to}')).toBe(true);`);
34
+ }
35
+ break;
36
+ }
37
+ case 'error': {
38
+ const fromMatch = scenario.steps[0]?.match(/given flow in (\S+)/);
39
+ const toMatch = scenario.steps[scenario.steps.length - 1]?.match(/then flow transitions to (\S+)/);
40
+ if (fromMatch && toMatch) {
41
+ lines.push(` expect(definition.errorTransitions.get('${fromMatch[1]}')).toBe('${toMatch[1]}');`);
42
+ }
43
+ break;
44
+ }
45
+ case 'guard_rejection': {
46
+ const guardMatch = scenario.steps[1]?.match(/when guard (\S+) rejects/);
47
+ if (guardMatch) {
48
+ lines.push(` expect(definition.maxGuardRetries).toBeGreaterThan(0);`);
49
+ }
50
+ break;
51
+ }
52
+ case 'timeout': {
53
+ const fromMatch = scenario.steps[0]?.match(/given flow in (\S+)/);
54
+ if (fromMatch) {
55
+ lines.push(` const t = definition.transitionsFrom('${fromMatch[1]}').find(t => t.timeout != null);`);
56
+ lines.push(` expect(t).toBeDefined();`);
57
+ }
58
+ break;
59
+ }
60
+ }
61
+ lines.push(' });');
62
+ lines.push('');
63
+ }
64
+ lines.push('});');
65
+ lines.push('');
66
+ return lines.join('\n');
67
+ }
6
68
  generate(definition) {
7
69
  const scenarios = [];
8
70
  // Happy path scenarios from transitions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unlaxer/tramli-plugins",
3
- "version": "3.5.1",
3
+ "version": "3.6.0",
4
4
  "description": "Plugin pack for tramli — audit, eventstore, observability, resume, idempotency, hierarchy, diagram, docs, lint, testing, subflow",
5
5
  "type": "module",
6
6
  "exports": {