@featurevisor/core 2.5.0 → 2.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/coverage/clover.xml +444 -78
  3. package/coverage/coverage-final.json +9 -3
  4. package/coverage/lcov-report/index.html +42 -42
  5. package/coverage/lcov-report/lib/builder/allocator.js.html +1 -1
  6. package/coverage/lcov-report/lib/builder/buildScopedConditions.js.html +373 -0
  7. package/coverage/lcov-report/lib/builder/buildScopedDatafile.js.html +403 -0
  8. package/coverage/lcov-report/lib/builder/buildScopedSegments.js.html +379 -0
  9. package/coverage/lcov-report/lib/builder/index.html +53 -8
  10. package/coverage/lcov-report/lib/builder/revision.js.html +1 -1
  11. package/coverage/lcov-report/lib/builder/traffic.js.html +1 -1
  12. package/coverage/lcov-report/lib/list/index.html +14 -14
  13. package/coverage/lcov-report/lib/list/matrix.js.html +23 -8
  14. package/coverage/lcov-report/lib/tester/helpers.js.html +1 -1
  15. package/coverage/lcov-report/lib/tester/index.html +1 -1
  16. package/coverage/lcov-report/src/builder/allocator.ts.html +1 -1
  17. package/coverage/lcov-report/src/builder/buildScopedConditions.ts.html +487 -0
  18. package/coverage/lcov-report/src/builder/buildScopedDatafile.ts.html +604 -0
  19. package/coverage/lcov-report/src/builder/buildScopedSegments.ts.html +544 -0
  20. package/coverage/lcov-report/src/builder/index.html +55 -10
  21. package/coverage/lcov-report/src/builder/revision.ts.html +1 -1
  22. package/coverage/lcov-report/src/builder/traffic.ts.html +3 -3
  23. package/coverage/lcov-report/src/list/index.html +14 -14
  24. package/coverage/lcov-report/src/list/matrix.ts.html +33 -12
  25. package/coverage/lcov-report/src/tester/helpers.ts.html +1 -1
  26. package/coverage/lcov-report/src/tester/index.html +1 -1
  27. package/coverage/lcov.info +810 -129
  28. package/jest.config.js +8 -0
  29. package/lib/builder/buildDatafile.d.ts +10 -0
  30. package/lib/builder/buildDatafile.js +27 -0
  31. package/lib/builder/buildDatafile.js.map +1 -1
  32. package/lib/builder/buildProject.d.ts +2 -0
  33. package/lib/builder/buildProject.js +38 -4
  34. package/lib/builder/buildProject.js.map +1 -1
  35. package/lib/builder/buildScopedConditions.d.ts +5 -0
  36. package/lib/builder/buildScopedConditions.js +97 -0
  37. package/lib/builder/buildScopedConditions.js.map +1 -0
  38. package/lib/builder/buildScopedConditions.spec.d.ts +1 -0
  39. package/lib/builder/buildScopedConditions.spec.js +2167 -0
  40. package/lib/builder/buildScopedConditions.spec.js.map +1 -0
  41. package/lib/builder/buildScopedDatafile.d.ts +2 -0
  42. package/lib/builder/buildScopedDatafile.js +107 -0
  43. package/lib/builder/buildScopedDatafile.js.map +1 -0
  44. package/lib/builder/buildScopedDatafile.spec.d.ts +1 -0
  45. package/lib/builder/buildScopedDatafile.spec.js +1988 -0
  46. package/lib/builder/buildScopedDatafile.spec.js.map +1 -0
  47. package/lib/builder/buildScopedSegments.d.ts +5 -0
  48. package/lib/builder/buildScopedSegments.js +99 -0
  49. package/lib/builder/buildScopedSegments.js.map +1 -0
  50. package/lib/builder/buildScopedSegments.spec.d.ts +1 -0
  51. package/lib/builder/buildScopedSegments.spec.js +1062 -0
  52. package/lib/builder/buildScopedSegments.spec.js.map +1 -0
  53. package/lib/builder/index.d.ts +1 -0
  54. package/lib/builder/index.js +1 -0
  55. package/lib/builder/index.js.map +1 -1
  56. package/lib/config/projectConfig.d.ts +9 -1
  57. package/lib/config/projectConfig.js +1 -0
  58. package/lib/config/projectConfig.js.map +1 -1
  59. package/lib/datasource/adapter.d.ts +3 -1
  60. package/lib/datasource/adapter.js.map +1 -1
  61. package/lib/datasource/filesystemAdapter.js +3 -1
  62. package/lib/datasource/filesystemAdapter.js.map +1 -1
  63. package/lib/linter/testSchema.d.ts +5 -0
  64. package/lib/linter/testSchema.js +8 -0
  65. package/lib/linter/testSchema.js.map +1 -1
  66. package/lib/list/matrix.js +5 -0
  67. package/lib/list/matrix.js.map +1 -1
  68. package/lib/tester/testFeature.d.ts +5 -3
  69. package/lib/tester/testFeature.js +34 -9
  70. package/lib/tester/testFeature.js.map +1 -1
  71. package/lib/tester/testProject.d.ts +3 -2
  72. package/lib/tester/testProject.js +40 -6
  73. package/lib/tester/testProject.js.map +1 -1
  74. package/package.json +5 -5
  75. package/src/builder/buildDatafile.ts +47 -0
  76. package/src/builder/buildProject.ts +58 -3
  77. package/src/builder/buildScopedConditions.spec.ts +2659 -0
  78. package/src/builder/buildScopedConditions.ts +134 -0
  79. package/src/builder/buildScopedDatafile.spec.ts +2236 -0
  80. package/src/builder/buildScopedDatafile.ts +173 -0
  81. package/src/builder/buildScopedSegments.spec.ts +1573 -0
  82. package/src/builder/buildScopedSegments.ts +153 -0
  83. package/src/builder/index.ts +1 -0
  84. package/src/config/projectConfig.ts +11 -1
  85. package/src/datasource/adapter.ts +4 -1
  86. package/src/datasource/filesystemAdapter.ts +4 -1
  87. package/src/linter/testSchema.ts +12 -0
  88. package/src/list/matrix.ts +7 -0
  89. package/src/tester/testFeature.ts +50 -16
  90. package/src/tester/testProject.ts +68 -8
@@ -0,0 +1,153 @@
1
+ import type {
2
+ GroupSegment,
3
+ AndGroupSegment,
4
+ OrGroupSegment,
5
+ NotGroupSegment,
6
+ Context,
7
+ } from "@featurevisor/types";
8
+ import type { DatafileReader } from "@featurevisor/sdk";
9
+
10
+ export function buildScopedSegments(
11
+ datafileReader: DatafileReader,
12
+ segments: GroupSegment | GroupSegment[],
13
+ context: Context,
14
+ removeSegments: string[] = [],
15
+ ): GroupSegment | GroupSegment[] {
16
+ const scoped = buildScopedGroupSegments(datafileReader, segments, context, removeSegments);
17
+ const removed = removeRedundantGroupSegments(scoped);
18
+
19
+ return removed;
20
+ }
21
+
22
+ export function removeRedundantGroupSegments(
23
+ groupSegments: GroupSegment | GroupSegment[],
24
+ ): GroupSegment | GroupSegment[] {
25
+ if (groupSegments === "*") {
26
+ return groupSegments;
27
+ }
28
+
29
+ if (Array.isArray(groupSegments)) {
30
+ // Recursively process each group segment
31
+ const processed = groupSegments.map((gs) => removeRedundantGroupSegments(gs)) as GroupSegment[];
32
+
33
+ // Filter out "*" values
34
+ const filtered = processed.filter((gs) => gs !== "*");
35
+
36
+ // If all were "*", return "*"
37
+ if (filtered.length === 0) {
38
+ return "*";
39
+ }
40
+
41
+ return filtered;
42
+ }
43
+
44
+ if (typeof groupSegments === "object") {
45
+ if ("and" in groupSegments) {
46
+ const processed = groupSegments.and.map((gs) =>
47
+ removeRedundantGroupSegments(gs),
48
+ ) as GroupSegment[];
49
+ const filtered = processed.filter((gs) => gs !== "*");
50
+
51
+ // If all were "*", return "*"
52
+ if (filtered.length === 0) {
53
+ return "*";
54
+ }
55
+
56
+ return {
57
+ and: filtered,
58
+ } as AndGroupSegment;
59
+ }
60
+
61
+ if ("or" in groupSegments) {
62
+ const processed = groupSegments.or.map((gs) =>
63
+ removeRedundantGroupSegments(gs),
64
+ ) as GroupSegment[];
65
+ const filtered = processed.filter((gs) => gs !== "*");
66
+
67
+ // If all were "*", return "*"
68
+ if (filtered.length === 0) {
69
+ return "*";
70
+ }
71
+
72
+ return {
73
+ or: filtered,
74
+ } as OrGroupSegment;
75
+ }
76
+
77
+ if ("not" in groupSegments) {
78
+ const processed = groupSegments.not.map((gs) =>
79
+ removeRedundantGroupSegments(gs),
80
+ ) as GroupSegment[];
81
+ const filtered = processed.filter((gs) => gs !== "*");
82
+
83
+ // If all were "*", return "*"
84
+ if (filtered.length === 0) {
85
+ return "*";
86
+ }
87
+
88
+ return {
89
+ not: filtered,
90
+ } as NotGroupSegment;
91
+ }
92
+ }
93
+
94
+ return groupSegments;
95
+ }
96
+
97
+ export function buildScopedGroupSegments(
98
+ datafileReader: DatafileReader,
99
+ groupSegments: GroupSegment | GroupSegment[],
100
+ context: Context,
101
+ removeSegments: string[] = [],
102
+ ): GroupSegment | GroupSegment[] {
103
+ if (groupSegments === "*") {
104
+ return groupSegments;
105
+ }
106
+
107
+ if (Array.isArray(groupSegments)) {
108
+ return groupSegments.map((gs) =>
109
+ buildScopedGroupSegments(datafileReader, gs, context, removeSegments),
110
+ ) as GroupSegment[];
111
+ }
112
+
113
+ if (typeof groupSegments === "string") {
114
+ if (removeSegments.includes(groupSegments)) {
115
+ return "*";
116
+ }
117
+
118
+ const matched = datafileReader.allSegmentsAreMatched(groupSegments, context);
119
+
120
+ if (matched) {
121
+ return "*";
122
+ }
123
+ }
124
+
125
+ if (typeof groupSegments === "object") {
126
+ // AND, OR, NOT group segments
127
+ if ("and" in groupSegments) {
128
+ return {
129
+ and: groupSegments.and.map((gs) =>
130
+ buildScopedGroupSegments(datafileReader, gs, context, removeSegments),
131
+ ) as GroupSegment[],
132
+ } as AndGroupSegment;
133
+ }
134
+
135
+ if ("or" in groupSegments) {
136
+ return {
137
+ or: groupSegments.or.map((gs) =>
138
+ buildScopedGroupSegments(datafileReader, gs, context, removeSegments),
139
+ ) as GroupSegment[],
140
+ } as OrGroupSegment;
141
+ }
142
+
143
+ if ("not" in groupSegments) {
144
+ return {
145
+ not: groupSegments.not.map((gs) =>
146
+ buildScopedGroupSegments(datafileReader, gs, context, removeSegments),
147
+ ) as GroupSegment[],
148
+ } as NotGroupSegment;
149
+ }
150
+ }
151
+
152
+ return groupSegments;
153
+ }
@@ -1,2 +1,3 @@
1
1
  export * from "./buildDatafile";
2
+ export * from "./buildScopedDatafile";
2
3
  export * from "./buildProject";
@@ -1,10 +1,11 @@
1
1
  import * as path from "path";
2
2
 
3
- import type { BucketBy } from "@featurevisor/types";
3
+ import type { BucketBy, Context, Tag } from "@featurevisor/types";
4
4
 
5
5
  import { Parser, parsers } from "./parsers";
6
6
  import { FilesystemAdapter } from "../datasource/filesystemAdapter";
7
7
  import type { Plugin } from "../cli";
8
+ import type { BuildTags } from "../builder/buildDatafile";
8
9
 
9
10
  export const FEATURES_DIRECTORY_NAME = "features";
10
11
  export const SEGMENTS_DIRECTORY_NAME = "segments";
@@ -31,6 +32,13 @@ export const DEFAULT_PARSER: Parser = "yml";
31
32
 
32
33
  export const SCHEMA_VERSION = "2"; // default schema version
33
34
 
35
+ export interface Scope {
36
+ name: string;
37
+ context: Context;
38
+ tag?: Tag;
39
+ tags?: BuildTags;
40
+ }
41
+
34
42
  export interface ProjectConfig {
35
43
  featuresDirectoryPath: string;
36
44
  segmentsDirectoryPath: string;
@@ -45,6 +53,7 @@ export interface ProjectConfig {
45
53
 
46
54
  environments: string[] | false;
47
55
  tags: string[];
56
+ scopes?: Scope[];
48
57
 
49
58
  adapter: any; // @NOTE: type this properly later
50
59
  plugins: Plugin[];
@@ -68,6 +77,7 @@ export function getProjectConfig(rootDirectoryPath: string): ProjectConfig {
68
77
  const baseConfig: ProjectConfig = {
69
78
  environments: DEFAULT_ENVIRONMENTS,
70
79
  tags: DEFAULT_TAGS,
80
+ scopes: [],
71
81
  defaultBucketBy: "userId",
72
82
 
73
83
  parser: DEFAULT_PARSER,
@@ -8,9 +8,12 @@ import type {
8
8
  EntityType,
9
9
  } from "@featurevisor/types";
10
10
 
11
+ import type { Scope } from "../config";
12
+
11
13
  export interface DatafileOptions {
12
14
  environment: EnvironmentKey | false;
13
- tag: string;
15
+ tag?: string;
16
+ scope?: Scope;
14
17
  datafilesDir?: string;
15
18
  }
16
19
 
@@ -223,7 +223,10 @@ export class FilesystemAdapter extends Adapter {
223
223
  getDatafilePath(options: DatafileOptions): string {
224
224
  const pattern = this.config.datafileNamePattern || "featurevisor-%s.json";
225
225
 
226
- const fileName = pattern.replace("%s", `tag-${options.tag}`);
226
+ const fileName = options.scope
227
+ ? pattern.replace("%s", `scope-${options.scope.name}`)
228
+ : pattern.replace("%s", `tag-${options.tag}`);
229
+
227
230
  const dir = options.datafilesDir || this.config.datafilesDirectoryPath;
228
231
 
229
232
  if (options.environment) {
@@ -7,6 +7,8 @@ export function getTestsZodSchema(
7
7
  availableFeatureKeys: [string, ...string[]],
8
8
  availableSegmentKeys: [string, ...string[]],
9
9
  ) {
10
+ const scopeNames = projectConfig.scopes ? projectConfig.scopes.map((scope) => scope.name) : [];
11
+
10
12
  const matrixZodSchema = z.record(
11
13
  z.array(
12
14
  z.union([
@@ -82,6 +84,16 @@ export function getTestsZodSchema(
82
84
  }),
83
85
  )
84
86
  : z.never().optional(),
87
+ // @TODO: add tag later, similar to `scope` below
88
+ scope: z
89
+ .string()
90
+ .refine(
91
+ (value) => scopeNames.includes(value),
92
+ (value) => ({
93
+ message: `Unknown scope "${value}"`,
94
+ }),
95
+ )
96
+ .optional(),
85
97
 
86
98
  // parent
87
99
  sticky: z.record(z.record(z.any())).optional(),
@@ -96,6 +96,13 @@ export function applyCombinationToFeatureAssertion(
96
96
  );
97
97
  }
98
98
 
99
+ // scope
100
+ if (flattenedAssertion.scope) {
101
+ flattenedAssertion.scope = applyCombinationToValue(flattenedAssertion.scope, combination);
102
+ }
103
+
104
+ // @TODO: support `tag` later, similar to `scope` above
105
+
99
106
  return flattenedAssertion;
100
107
  }
101
108
 
@@ -17,14 +17,22 @@ import { Datasource } from "../datasource";
17
17
  import { ProjectConfig } from "../config";
18
18
 
19
19
  import { checkIfArraysAreEqual, checkIfObjectsAreEqual } from "./helpers";
20
- import type { DatafileContentByEnvironment } from "./testProject";
20
+ import type { DatafileContentByKey } from "./testProject";
21
+
22
+ export interface TestFeatureOptions {
23
+ verbose?: boolean;
24
+ quiet?: boolean;
25
+ showDatafile?: boolean;
26
+ withScopes?: boolean;
27
+ [key: string]: any;
28
+ }
21
29
 
22
30
  export async function testFeature(
23
31
  datasource: Datasource,
24
32
  projectConfig: ProjectConfig,
25
33
  test: TestFeature,
26
- options: { verbose?: boolean; quiet?: boolean; showDatafile?: boolean; [key: string]: any } = {},
27
- datafileContentByEnvironment: DatafileContentByEnvironment,
34
+ options: TestFeatureOptions = {},
35
+ datafileContentByKey: DatafileContentByKey,
28
36
  ): Promise<TestResult> {
29
37
  const testStartTime = Date.now();
30
38
  const featureKey = test.feature;
@@ -51,7 +59,14 @@ export async function testFeature(
51
59
  errors: [],
52
60
  };
53
61
 
54
- const datafileContent = datafileContentByEnvironment.get(assertion.environment || false);
62
+ let datafileContent = datafileContentByKey.get(assertion.environment || false);
63
+
64
+ const scopedDatafileKey = `${assertion.environment}-scope-${assertion.scope}`;
65
+ if (assertion.scope && datafileContentByKey.has(scopedDatafileKey)) {
66
+ datafileContent = datafileContentByKey.get(scopedDatafileKey);
67
+ }
68
+
69
+ // @TODO: do similar like `scope`, but for `tag`
55
70
 
56
71
  if (options.showDatafile) {
57
72
  console.log("");
@@ -92,15 +107,39 @@ export async function testFeature(
92
107
  return testResult;
93
108
  }
94
109
 
110
+ let context = {};
111
+
112
+ if (assertion.scope) {
113
+ if (!options.withScopes) {
114
+ // if not testing with scoped datafiles,
115
+ // then we need to add the scope's context to the context
116
+ const scope = projectConfig.scopes?.find((s) => s.name === assertion.scope);
117
+
118
+ if (scope) {
119
+ context = {
120
+ ...(scope.context || {}),
121
+ ...context,
122
+ };
123
+ }
124
+ }
125
+ }
126
+
95
127
  if (assertion.context) {
96
- sdk.setContext(assertion.context);
128
+ context = {
129
+ ...context,
130
+ ...assertion.context,
131
+ };
132
+ }
133
+
134
+ if (context) {
135
+ sdk.setContext(context);
97
136
  }
98
137
 
99
138
  /**
100
139
  * expectedToBeEnabled
101
140
  */
102
141
  function testExpectedToBeEnabled(sdk, assertion, details = {}) {
103
- const isEnabled = sdk.isEnabled(featureKey, assertion.context || {});
142
+ const isEnabled = sdk.isEnabled(featureKey, context);
104
143
 
105
144
  if (isEnabled !== assertion.expectedToBeEnabled) {
106
145
  testResult.passed = false;
@@ -128,7 +167,7 @@ export async function testFeature(
128
167
  overrideOptions.defaultVariationValue = assertion.defaultVariationValue;
129
168
  }
130
169
 
131
- const variation = sdk.getVariation(featureKey, assertion.context || {}, overrideOptions);
170
+ const variation = sdk.getVariation(featureKey, context, overrideOptions);
132
171
 
133
172
  if (variation !== assertion.expectedVariation) {
134
173
  testResult.passed = false;
@@ -160,12 +199,7 @@ export async function testFeature(
160
199
  overrideOptions.defaultVariableValue = assertion.defaultVariableValues[variableKey];
161
200
  }
162
201
 
163
- const actualValue = sdk.getVariable(
164
- featureKey,
165
- variableKey,
166
- assertion.context || {},
167
- overrideOptions,
168
- );
202
+ const actualValue = sdk.getVariable(featureKey, variableKey, context, overrideOptions);
169
203
 
170
204
  let passed;
171
205
 
@@ -271,12 +305,12 @@ export async function testFeature(
271
305
  }
272
306
 
273
307
  if (assertion.expectedEvaluations.flag) {
274
- const evaluation = sdk.evaluateFlag(featureKey, assertion.context || {});
308
+ const evaluation = sdk.evaluateFlag(featureKey, context);
275
309
  testEvaluation("flag", evaluation, assertion.expectedEvaluations.flag);
276
310
  }
277
311
 
278
312
  if (assertion.expectedEvaluations.variation) {
279
- const evaluation = sdk.evaluateVariation(featureKey, assertion.context || {});
313
+ const evaluation = sdk.evaluateVariation(featureKey, context);
280
314
  testEvaluation("variation", evaluation, assertion.expectedEvaluations.variation);
281
315
  }
282
316
 
@@ -284,7 +318,7 @@ export async function testFeature(
284
318
  const variableKeys = Object.keys(assertion.expectedEvaluations.variables);
285
319
 
286
320
  for (const variableKey of variableKeys) {
287
- const evaluation = sdk.evaluateVariable(featureKey, variableKey, assertion.context || {});
321
+ const evaluation = sdk.evaluateVariable(featureKey, variableKey, context);
288
322
  testEvaluation(
289
323
  "variable",
290
324
  evaluation,
@@ -9,7 +9,7 @@ import { Dependencies } from "../dependencies";
9
9
  import { prettyDuration } from "./prettyDuration";
10
10
  import { printTestResult } from "./printTestResult";
11
11
 
12
- import { buildDatafile } from "../builder";
12
+ import { buildDatafile, buildScopedDatafile } from "../builder";
13
13
  import { SCHEMA_VERSION } from "../config";
14
14
  import { Plugin } from "../cli";
15
15
  import { listEntities } from "../list";
@@ -22,6 +22,7 @@ export interface TestProjectOptions {
22
22
  onlyFailures?: boolean;
23
23
  schemaVersion?: string;
24
24
  inflate?: number;
25
+ withScopes?: boolean;
25
26
  }
26
27
 
27
28
  export interface ExecutionResult {
@@ -32,13 +33,14 @@ export interface ExecutionResult {
32
33
  };
33
34
  }
34
35
 
35
- export type DatafileContentByEnvironment = Map<string | false, DatafileContent>;
36
+ // the key can be either "<EnvironmentKey>" or "<EnvironmentKey>scope-<Scope>"
37
+ export type DatafileContentByKey = Map<string | false, DatafileContent>;
36
38
 
37
39
  export async function executeTest(
38
40
  test: Test,
39
41
  deps: Dependencies,
40
42
  options: TestProjectOptions,
41
- datafileContentByEnvironment: DatafileContentByEnvironment,
43
+ datafileContentByKey: DatafileContentByKey,
42
44
  ): Promise<ExecutionResult | undefined> {
43
45
  const { datasource, projectConfig, rootDirectoryPath } = deps;
44
46
 
@@ -74,7 +76,7 @@ export async function executeTest(
74
76
  projectConfig,
75
77
  tAsFeature,
76
78
  options,
77
- datafileContentByEnvironment,
79
+ datafileContentByKey,
78
80
  );
79
81
  }
80
82
 
@@ -124,7 +126,7 @@ export async function testProject(
124
126
  let passedAssertionsCount = 0;
125
127
  let failedAssertionsCount = 0;
126
128
 
127
- const datafileContentByEnvironment: DatafileContentByEnvironment = new Map();
129
+ const datafileContentByKey: DatafileContentByKey = new Map();
128
130
 
129
131
  // with environments
130
132
  if (Array.isArray(projectConfig.environments)) {
@@ -142,7 +144,36 @@ export async function testProject(
142
144
  existingState,
143
145
  );
144
146
 
145
- datafileContentByEnvironment.set(environment, datafileContent as DatafileContent);
147
+ datafileContentByKey.set(environment, datafileContent as DatafileContent);
148
+
149
+ // by scope
150
+ if (projectConfig.scopes && options.withScopes) {
151
+ for (const scope of projectConfig.scopes) {
152
+ const existingState = await datasource.readState(environment);
153
+ const datafileContent = await buildDatafile(
154
+ projectConfig,
155
+ datasource,
156
+ {
157
+ schemaVersion: options.schemaVersion || SCHEMA_VERSION,
158
+ revision: "include-scoped-features",
159
+ environment: environment,
160
+ inflate: options.inflate,
161
+ tag: scope.tag,
162
+ tags: scope.tags,
163
+ },
164
+ existingState,
165
+ );
166
+
167
+ const scopedDatafileContent = buildScopedDatafile(
168
+ datafileContent as DatafileContent,
169
+ scope.context,
170
+ );
171
+
172
+ datafileContentByKey.set(`${environment}-scope-${scope.name}`, scopedDatafileContent);
173
+ }
174
+ }
175
+
176
+ // @TODO: by tag
146
177
  }
147
178
  }
148
179
 
@@ -161,7 +192,36 @@ export async function testProject(
161
192
  existingState,
162
193
  );
163
194
 
164
- datafileContentByEnvironment.set(false, datafileContent as DatafileContent);
195
+ datafileContentByKey.set(false, datafileContent as DatafileContent);
196
+
197
+ // by scope
198
+ if (projectConfig.scopes && options.withScopes) {
199
+ for (const scope of projectConfig.scopes) {
200
+ const existingState = await datasource.readState(false);
201
+ const datafileContent = await buildDatafile(
202
+ projectConfig,
203
+ datasource,
204
+ {
205
+ schemaVersion: options.schemaVersion || SCHEMA_VERSION,
206
+ revision: "include-scoped-features",
207
+ environment: false,
208
+ inflate: options.inflate,
209
+ tag: scope.tag,
210
+ tags: scope.tags,
211
+ },
212
+ existingState,
213
+ );
214
+
215
+ const scopedDatafileContent = buildScopedDatafile(
216
+ datafileContent as DatafileContent,
217
+ scope.context,
218
+ );
219
+
220
+ datafileContentByKey.set(`scope-${scope.name}`, scopedDatafileContent);
221
+ }
222
+ }
223
+
224
+ // @TODO: by tag
165
225
  }
166
226
 
167
227
  const tests = await listEntities<Test>(
@@ -178,7 +238,7 @@ export async function testProject(
178
238
  );
179
239
 
180
240
  for (const test of tests) {
181
- const executionResult = await executeTest(test, deps, options, datafileContentByEnvironment);
241
+ const executionResult = await executeTest(test, deps, options, datafileContentByKey);
182
242
 
183
243
  if (!executionResult) {
184
244
  continue;