@open-mercato/cli 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2
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/.turbo/turbo-build.log +1 -1
- package/dist/agentic/shared/AGENTS.md.template +1 -1
- package/dist/lib/__integration__/TC-INT-007.spec.js +201 -0
- package/dist/lib/__integration__/TC-INT-007.spec.js.map +7 -0
- package/dist/lib/dev-env-reload.js +89 -0
- package/dist/lib/dev-env-reload.js.map +7 -0
- package/dist/lib/generators/extensions/ai-agents.js +218 -0
- package/dist/lib/generators/extensions/ai-agents.js.map +7 -0
- package/dist/lib/generators/extensions/ai-tools.js +56 -1
- package/dist/lib/generators/extensions/ai-tools.js.map +2 -2
- package/dist/lib/generators/extensions/index.js +2 -0
- package/dist/lib/generators/extensions/index.js.map +2 -2
- package/dist/lib/testing/integration-discovery.js +102 -5
- package/dist/lib/testing/integration-discovery.js.map +2 -2
- package/dist/mercato.js +85 -44
- package/dist/mercato.js.map +2 -2
- package/package.json +5 -5
- package/src/__tests__/mercato.test.ts +112 -0
- package/src/lib/__integration__/TC-INT-007.spec.ts +228 -0
- package/src/lib/__tests__/dev-env-reload.test.ts +62 -0
- package/src/lib/dev-env-reload.ts +110 -0
- package/src/lib/generators/__tests__/module-subset.test.ts +14 -0
- package/src/lib/generators/__tests__/output-snapshots.test.ts +17 -0
- package/src/lib/generators/__tests__/scanner.test.ts +1 -1
- package/src/lib/generators/__tests__/structural-contracts.test.ts +26 -0
- package/src/lib/generators/extensions/ai-agents.ts +240 -0
- package/src/lib/generators/extensions/ai-tools.ts +72 -1
- package/src/lib/generators/extensions/index.ts +2 -0
- package/src/lib/testing/__tests__/integration-discovery.test.ts +68 -0
- package/src/lib/testing/integration-discovery.ts +127 -3
- package/src/mercato.ts +100 -46
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { VariableDeclarationKind, type WriterFunction } from 'ts-morph'
|
|
2
|
+
import type { GeneratorExtension } from '../extension'
|
|
3
|
+
import {
|
|
4
|
+
arrayLiteral,
|
|
5
|
+
arrowFunction,
|
|
6
|
+
binaryExpression,
|
|
7
|
+
identifier,
|
|
8
|
+
methodCall,
|
|
9
|
+
objectLiteral,
|
|
10
|
+
parenthesized,
|
|
11
|
+
propertyAccess,
|
|
12
|
+
writeValue,
|
|
13
|
+
} from '../ast'
|
|
14
|
+
import {
|
|
15
|
+
emptyArray,
|
|
16
|
+
emptyObject,
|
|
17
|
+
moduleEntry,
|
|
18
|
+
namespaceFallback,
|
|
19
|
+
namespaceImportSpec,
|
|
20
|
+
renderGeneratedTsSource,
|
|
21
|
+
} from './shared'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generator extension for `<module>/ai-agents.ts` files.
|
|
25
|
+
*
|
|
26
|
+
* Each module's `ai-agents.ts` may export both base agent contributions
|
|
27
|
+
* (`aiAgents`), additive extensions for existing agents
|
|
28
|
+
* (`aiAgentExtensions`), AND cross-module override declarations
|
|
29
|
+
* (`aiAgentOverrides`). The generator scans the file once, emits the
|
|
30
|
+
* configuration entry with all fields, and produces filtered
|
|
31
|
+
* exports inside `ai-agents.generated.ts`:
|
|
32
|
+
*
|
|
33
|
+
* - `aiAgentConfigEntries` (entries that declare base agents)
|
|
34
|
+
* - `aiAgentExtensionEntries` / `allAiAgentExtensions` (entries that append to agents)
|
|
35
|
+
* - `aiAgentOverrideEntries` (entries that declare overrides)
|
|
36
|
+
*
|
|
37
|
+
* The runtime (`@open-mercato/ai-assistant`) reads
|
|
38
|
+
* `aiAgentConfigEntries` to populate the agent registry,
|
|
39
|
+
* `aiAgentOverrideEntries` to apply cross-module replacements, and
|
|
40
|
+
* `allAiAgentExtensions` to append safe metadata after the base load. See spec
|
|
41
|
+
* `.ai/specs/2026-04-30-ai-overrides-and-module-disable.md`.
|
|
42
|
+
*/
|
|
43
|
+
export function createAiAgentsExtension(): GeneratorExtension {
|
|
44
|
+
const imports = [] as Array<ReturnType<typeof namespaceImportSpec>>
|
|
45
|
+
const entries: WriterFunction[] = []
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
id: 'registry.ai-agents',
|
|
49
|
+
outputFiles: ['ai-agents.generated.ts'],
|
|
50
|
+
scanModule(ctx) {
|
|
51
|
+
ctx.processStandaloneConfig({
|
|
52
|
+
roots: ctx.roots,
|
|
53
|
+
imps: ctx.imps,
|
|
54
|
+
modId: ctx.moduleId,
|
|
55
|
+
relativePath: 'ai-agents.ts',
|
|
56
|
+
prefix: 'AI_AGENTS',
|
|
57
|
+
importIdRef: ctx.importIdRef,
|
|
58
|
+
standaloneImports: imports,
|
|
59
|
+
standaloneEntries: entries,
|
|
60
|
+
writeConfig: ({ importName, moduleId }) =>
|
|
61
|
+
moduleEntry(moduleId, [
|
|
62
|
+
{
|
|
63
|
+
name: 'agents',
|
|
64
|
+
value: namespaceFallback({
|
|
65
|
+
importName,
|
|
66
|
+
members: ['aiAgents', 'default'],
|
|
67
|
+
fallback: emptyArray(),
|
|
68
|
+
castType: 'unknown[]',
|
|
69
|
+
}),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'overrides',
|
|
73
|
+
value: namespaceFallback({
|
|
74
|
+
importName,
|
|
75
|
+
members: ['aiAgentOverrides'],
|
|
76
|
+
fallback: emptyObject(),
|
|
77
|
+
castType: 'Record<string, unknown>',
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'extensions',
|
|
82
|
+
value: namespaceFallback({
|
|
83
|
+
importName,
|
|
84
|
+
members: ['aiAgentExtensions'],
|
|
85
|
+
fallback: emptyArray(),
|
|
86
|
+
castType: 'unknown[]',
|
|
87
|
+
}),
|
|
88
|
+
},
|
|
89
|
+
]),
|
|
90
|
+
})
|
|
91
|
+
},
|
|
92
|
+
generateOutput() {
|
|
93
|
+
const output = renderGeneratedTsSource({
|
|
94
|
+
fileName: 'ai-agents.generated.ts',
|
|
95
|
+
imports,
|
|
96
|
+
build(sourceFile) {
|
|
97
|
+
sourceFile.addTypeAlias({
|
|
98
|
+
name: 'AiAgentConfigEntry',
|
|
99
|
+
type: '{ moduleId: string; agents: unknown[]; overrides: Record<string, unknown>; extensions: unknown[] }',
|
|
100
|
+
})
|
|
101
|
+
sourceFile.addTypeAlias({
|
|
102
|
+
name: 'AiAgentOverrideConfigEntry',
|
|
103
|
+
type: '{ moduleId: string; overrides: Record<string, unknown> }',
|
|
104
|
+
})
|
|
105
|
+
sourceFile.addTypeAlias({
|
|
106
|
+
name: 'AiAgentExtensionConfigEntry',
|
|
107
|
+
type: '{ moduleId: string; extensions: unknown[] }',
|
|
108
|
+
})
|
|
109
|
+
sourceFile.addVariableStatement({
|
|
110
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
111
|
+
declarations: [
|
|
112
|
+
{
|
|
113
|
+
name: 'aiAgentConfigEntriesRaw',
|
|
114
|
+
type: 'AiAgentConfigEntry[]',
|
|
115
|
+
initializer: arrayLiteral(entries, writeValue),
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
})
|
|
119
|
+
sourceFile.addVariableStatement({
|
|
120
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
121
|
+
isExported: true,
|
|
122
|
+
declarations: [
|
|
123
|
+
{
|
|
124
|
+
name: 'aiAgentConfigEntries',
|
|
125
|
+
type: 'AiAgentConfigEntry[]',
|
|
126
|
+
initializer: methodCall(identifier('aiAgentConfigEntriesRaw'), 'filter', [
|
|
127
|
+
arrowFunction({
|
|
128
|
+
parameters: ['entry'],
|
|
129
|
+
body: binaryExpression(propertyAccess(propertyAccess(identifier('entry'), 'agents'), 'length'), '>', 0),
|
|
130
|
+
}),
|
|
131
|
+
]),
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
})
|
|
135
|
+
sourceFile.addVariableStatement({
|
|
136
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
137
|
+
isExported: true,
|
|
138
|
+
declarations: [
|
|
139
|
+
{
|
|
140
|
+
name: 'aiAgentExtensionEntries',
|
|
141
|
+
type: 'AiAgentExtensionConfigEntry[]',
|
|
142
|
+
initializer: methodCall(
|
|
143
|
+
methodCall(identifier('aiAgentConfigEntriesRaw'), 'filter', [
|
|
144
|
+
arrowFunction({
|
|
145
|
+
parameters: ['entry'],
|
|
146
|
+
body: binaryExpression(propertyAccess(propertyAccess(identifier('entry'), 'extensions'), 'length'), '>', 0),
|
|
147
|
+
}),
|
|
148
|
+
]),
|
|
149
|
+
'map',
|
|
150
|
+
[
|
|
151
|
+
arrowFunction({
|
|
152
|
+
parameters: ['entry'],
|
|
153
|
+
body: parenthesized(
|
|
154
|
+
objectLiteral([
|
|
155
|
+
{ name: 'moduleId', value: propertyAccess(identifier('entry'), 'moduleId') },
|
|
156
|
+
{ name: 'extensions', value: propertyAccess(identifier('entry'), 'extensions') },
|
|
157
|
+
]),
|
|
158
|
+
),
|
|
159
|
+
}),
|
|
160
|
+
],
|
|
161
|
+
),
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
})
|
|
165
|
+
sourceFile.addVariableStatement({
|
|
166
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
167
|
+
isExported: true,
|
|
168
|
+
declarations: [
|
|
169
|
+
{
|
|
170
|
+
name: 'allAiAgentExtensions',
|
|
171
|
+
initializer: methodCall(identifier('aiAgentExtensionEntries'), 'flatMap', [
|
|
172
|
+
arrowFunction({
|
|
173
|
+
parameters: ['entry'],
|
|
174
|
+
body: propertyAccess(identifier('entry'), 'extensions'),
|
|
175
|
+
}),
|
|
176
|
+
]),
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
})
|
|
180
|
+
sourceFile.addVariableStatement({
|
|
181
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
182
|
+
isExported: true,
|
|
183
|
+
declarations: [
|
|
184
|
+
{
|
|
185
|
+
name: 'allAiAgents',
|
|
186
|
+
initializer: methodCall(identifier('aiAgentConfigEntries'), 'flatMap', [
|
|
187
|
+
arrowFunction({
|
|
188
|
+
parameters: ['entry'],
|
|
189
|
+
body: propertyAccess(identifier('entry'), 'agents'),
|
|
190
|
+
}),
|
|
191
|
+
]),
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
})
|
|
195
|
+
sourceFile.addVariableStatement({
|
|
196
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
197
|
+
isExported: true,
|
|
198
|
+
declarations: [
|
|
199
|
+
{
|
|
200
|
+
name: 'aiAgentOverrideEntries',
|
|
201
|
+
type: 'AiAgentOverrideConfigEntry[]',
|
|
202
|
+
initializer: methodCall(
|
|
203
|
+
methodCall(identifier('aiAgentConfigEntriesRaw'), 'filter', [
|
|
204
|
+
arrowFunction({
|
|
205
|
+
parameters: ['entry'],
|
|
206
|
+
body: binaryExpression(
|
|
207
|
+
propertyAccess(
|
|
208
|
+
methodCall(identifier('Object'), 'keys', [
|
|
209
|
+
propertyAccess(identifier('entry'), 'overrides'),
|
|
210
|
+
]),
|
|
211
|
+
'length',
|
|
212
|
+
),
|
|
213
|
+
'>',
|
|
214
|
+
0,
|
|
215
|
+
),
|
|
216
|
+
}),
|
|
217
|
+
]),
|
|
218
|
+
'map',
|
|
219
|
+
[
|
|
220
|
+
arrowFunction({
|
|
221
|
+
parameters: ['entry'],
|
|
222
|
+
body: parenthesized(
|
|
223
|
+
objectLiteral([
|
|
224
|
+
{ name: 'moduleId', value: propertyAccess(identifier('entry'), 'moduleId') },
|
|
225
|
+
{ name: 'overrides', value: propertyAccess(identifier('entry'), 'overrides') },
|
|
226
|
+
]),
|
|
227
|
+
),
|
|
228
|
+
}),
|
|
229
|
+
],
|
|
230
|
+
),
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
})
|
|
234
|
+
},
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
return new Map([['ai-agents.generated.ts', output]])
|
|
238
|
+
},
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -6,17 +6,36 @@ import {
|
|
|
6
6
|
binaryExpression,
|
|
7
7
|
identifier,
|
|
8
8
|
methodCall,
|
|
9
|
+
objectLiteral,
|
|
10
|
+
parenthesized,
|
|
9
11
|
propertyAccess,
|
|
10
12
|
writeValue,
|
|
11
13
|
} from '../ast'
|
|
12
14
|
import {
|
|
13
15
|
emptyArray,
|
|
16
|
+
emptyObject,
|
|
14
17
|
moduleEntry,
|
|
15
18
|
namespaceFallback,
|
|
16
19
|
namespaceImportSpec,
|
|
17
20
|
renderGeneratedTsSource,
|
|
18
21
|
} from './shared'
|
|
19
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Generator extension for `<module>/ai-tools.ts` files.
|
|
25
|
+
*
|
|
26
|
+
* Each module's `ai-tools.ts` may export both base tool contributions
|
|
27
|
+
* (`aiTools`) AND cross-module override declarations (`aiToolOverrides`).
|
|
28
|
+
* The generator scans the file once and emits two filtered exports
|
|
29
|
+
* inside `ai-tools.generated.ts`:
|
|
30
|
+
*
|
|
31
|
+
* - `aiToolConfigEntries` (entries that declare base tools)
|
|
32
|
+
* - `aiToolOverrideEntries` (entries that declare overrides)
|
|
33
|
+
*
|
|
34
|
+
* The runtime (`@open-mercato/ai-assistant`) reads `aiToolConfigEntries`
|
|
35
|
+
* to populate the tool registry and `aiToolOverrideEntries` to apply
|
|
36
|
+
* cross-module replacements after the base load. See spec
|
|
37
|
+
* `.ai/specs/2026-04-30-ai-overrides-and-module-disable.md`.
|
|
38
|
+
*/
|
|
20
39
|
export function createAiToolsExtension(): GeneratorExtension {
|
|
21
40
|
const imports = [] as Array<ReturnType<typeof namespaceImportSpec>>
|
|
22
41
|
const entries: WriterFunction[] = []
|
|
@@ -45,6 +64,15 @@ export function createAiToolsExtension(): GeneratorExtension {
|
|
|
45
64
|
castType: 'unknown[]',
|
|
46
65
|
}),
|
|
47
66
|
},
|
|
67
|
+
{
|
|
68
|
+
name: 'overrides',
|
|
69
|
+
value: namespaceFallback({
|
|
70
|
+
importName,
|
|
71
|
+
members: ['aiToolOverrides'],
|
|
72
|
+
fallback: emptyObject(),
|
|
73
|
+
castType: 'Record<string, unknown>',
|
|
74
|
+
}),
|
|
75
|
+
},
|
|
48
76
|
]),
|
|
49
77
|
})
|
|
50
78
|
},
|
|
@@ -55,7 +83,11 @@ export function createAiToolsExtension(): GeneratorExtension {
|
|
|
55
83
|
build(sourceFile) {
|
|
56
84
|
sourceFile.addTypeAlias({
|
|
57
85
|
name: 'AiToolConfigEntry',
|
|
58
|
-
type: '{ moduleId: string; tools: unknown[] }',
|
|
86
|
+
type: '{ moduleId: string; tools: unknown[]; overrides: Record<string, unknown> }',
|
|
87
|
+
})
|
|
88
|
+
sourceFile.addTypeAlias({
|
|
89
|
+
name: 'AiToolOverrideConfigEntry',
|
|
90
|
+
type: '{ moduleId: string; overrides: Record<string, unknown> }',
|
|
59
91
|
})
|
|
60
92
|
sourceFile.addVariableStatement({
|
|
61
93
|
declarationKind: VariableDeclarationKind.Const,
|
|
@@ -98,6 +130,45 @@ export function createAiToolsExtension(): GeneratorExtension {
|
|
|
98
130
|
},
|
|
99
131
|
],
|
|
100
132
|
})
|
|
133
|
+
sourceFile.addVariableStatement({
|
|
134
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
135
|
+
isExported: true,
|
|
136
|
+
declarations: [
|
|
137
|
+
{
|
|
138
|
+
name: 'aiToolOverrideEntries',
|
|
139
|
+
type: 'AiToolOverrideConfigEntry[]',
|
|
140
|
+
initializer: methodCall(
|
|
141
|
+
methodCall(identifier('aiToolConfigEntriesRaw'), 'filter', [
|
|
142
|
+
arrowFunction({
|
|
143
|
+
parameters: ['entry'],
|
|
144
|
+
body: binaryExpression(
|
|
145
|
+
propertyAccess(
|
|
146
|
+
methodCall(identifier('Object'), 'keys', [
|
|
147
|
+
propertyAccess(identifier('entry'), 'overrides'),
|
|
148
|
+
]),
|
|
149
|
+
'length',
|
|
150
|
+
),
|
|
151
|
+
'>',
|
|
152
|
+
0,
|
|
153
|
+
),
|
|
154
|
+
}),
|
|
155
|
+
]),
|
|
156
|
+
'map',
|
|
157
|
+
[
|
|
158
|
+
arrowFunction({
|
|
159
|
+
parameters: ['entry'],
|
|
160
|
+
body: parenthesized(
|
|
161
|
+
objectLiteral([
|
|
162
|
+
{ name: 'moduleId', value: propertyAccess(identifier('entry'), 'moduleId') },
|
|
163
|
+
{ name: 'overrides', value: propertyAccess(identifier('entry'), 'overrides') },
|
|
164
|
+
]),
|
|
165
|
+
),
|
|
166
|
+
}),
|
|
167
|
+
],
|
|
168
|
+
),
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
})
|
|
101
172
|
},
|
|
102
173
|
})
|
|
103
174
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GeneratorExtension } from '../extension'
|
|
2
|
+
import { createAiAgentsExtension } from './ai-agents'
|
|
2
3
|
import { createAiToolsExtension } from './ai-tools'
|
|
3
4
|
import { createAnalyticsExtension } from './analytics'
|
|
4
5
|
import { createCommandInterceptorsExtension } from './command-interceptors'
|
|
@@ -22,6 +23,7 @@ export function loadGeneratorExtensions(): GeneratorExtension[] {
|
|
|
22
23
|
createNotificationsExtension(),
|
|
23
24
|
createMessagesExtension(),
|
|
24
25
|
createAiToolsExtension(),
|
|
26
|
+
createAiAgentsExtension(),
|
|
25
27
|
createEventsExtension(),
|
|
26
28
|
createAnalyticsExtension(),
|
|
27
29
|
createTranslatableFieldsExtension(),
|
|
@@ -12,10 +12,20 @@ async function writeTestFile(projectRoot: string, relativePath: string, content
|
|
|
12
12
|
describe('integration discovery', () => {
|
|
13
13
|
let tempRoot = ''
|
|
14
14
|
const previousEnterpriseFlag = process.env.OM_ENABLE_ENTERPRISE_MODULES
|
|
15
|
+
const testEnvKeys = [
|
|
16
|
+
'OM_TEST_DISCOVERY_AI_KEY',
|
|
17
|
+
'OM_TEST_DISCOVERY_FALLBACK_AI_KEY',
|
|
18
|
+
'OM_TEST_DISCOVERY_REQUIRED_KEY',
|
|
19
|
+
] as const
|
|
20
|
+
const previousTestEnvValues = new Map<string, string | undefined>()
|
|
15
21
|
|
|
16
22
|
beforeEach(async () => {
|
|
17
23
|
tempRoot = await mkdtemp(path.join(os.tmpdir(), 'om-integration-discovery-'))
|
|
18
24
|
delete process.env.OM_ENABLE_ENTERPRISE_MODULES
|
|
25
|
+
for (const key of testEnvKeys) {
|
|
26
|
+
previousTestEnvValues.set(key, process.env[key])
|
|
27
|
+
delete process.env[key]
|
|
28
|
+
}
|
|
19
29
|
})
|
|
20
30
|
|
|
21
31
|
afterEach(async () => {
|
|
@@ -27,6 +37,15 @@ describe('integration discovery', () => {
|
|
|
27
37
|
} else {
|
|
28
38
|
process.env.OM_ENABLE_ENTERPRISE_MODULES = previousEnterpriseFlag
|
|
29
39
|
}
|
|
40
|
+
for (const key of testEnvKeys) {
|
|
41
|
+
const previousValue = previousTestEnvValues.get(key)
|
|
42
|
+
if (previousValue === undefined) {
|
|
43
|
+
delete process.env[key]
|
|
44
|
+
} else {
|
|
45
|
+
process.env[key] = previousValue
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
previousTestEnvValues.clear()
|
|
30
49
|
})
|
|
31
50
|
|
|
32
51
|
it('applies folder and per-test metadata dependencies', async () => {
|
|
@@ -139,4 +158,53 @@ describe('integration discovery', () => {
|
|
|
139
158
|
'node_modules/@open-mercato/core/src/modules/customers/__integration__/TC-CRM-020.spec.ts',
|
|
140
159
|
])
|
|
141
160
|
})
|
|
161
|
+
|
|
162
|
+
it('applies folder and per-test metadata environment requirements', async () => {
|
|
163
|
+
await writeTestFile(tempRoot, 'packages/ai-assistant/src/modules/ai_assistant/.gitkeep')
|
|
164
|
+
await writeTestFile(
|
|
165
|
+
tempRoot,
|
|
166
|
+
'packages/ai-assistant/src/modules/ai_assistant/__integration__/TC-AI-001.spec.ts',
|
|
167
|
+
'export {}\n',
|
|
168
|
+
)
|
|
169
|
+
await writeTestFile(
|
|
170
|
+
tempRoot,
|
|
171
|
+
'packages/ai-assistant/src/modules/ai_assistant/__integration__/TC-AI-002.spec.ts',
|
|
172
|
+
'export {}\n',
|
|
173
|
+
)
|
|
174
|
+
await writeTestFile(
|
|
175
|
+
tempRoot,
|
|
176
|
+
'packages/ai-assistant/src/modules/ai_assistant/__integration__/TC-AI-002.meta.ts',
|
|
177
|
+
"export const integrationMeta = { requiredAnyEnvVars: ['OM_TEST_DISCOVERY_AI_KEY', 'OM_TEST_DISCOVERY_FALLBACK_AI_KEY'] }\n",
|
|
178
|
+
)
|
|
179
|
+
await writeTestFile(
|
|
180
|
+
tempRoot,
|
|
181
|
+
'packages/ai-assistant/src/modules/ai_assistant/__integration__/live/meta.ts',
|
|
182
|
+
"export const integrationMeta = { requiredEnvVars: ['OM_TEST_DISCOVERY_REQUIRED_KEY'] }\n",
|
|
183
|
+
)
|
|
184
|
+
await writeTestFile(
|
|
185
|
+
tempRoot,
|
|
186
|
+
'packages/ai-assistant/src/modules/ai_assistant/__integration__/live/TC-AI-003.spec.ts',
|
|
187
|
+
'export {}\n',
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
let discovered = discoverIntegrationSpecFiles(tempRoot, path.join(tempRoot, '.ai', 'qa', 'tests'))
|
|
191
|
+
expect(discovered.map((entry) => entry.path)).toEqual([
|
|
192
|
+
'packages/ai-assistant/src/modules/ai_assistant/__integration__/TC-AI-001.spec.ts',
|
|
193
|
+
])
|
|
194
|
+
|
|
195
|
+
process.env.OM_TEST_DISCOVERY_FALLBACK_AI_KEY = 'test-key'
|
|
196
|
+
discovered = discoverIntegrationSpecFiles(tempRoot, path.join(tempRoot, '.ai', 'qa', 'tests'))
|
|
197
|
+
expect(discovered.map((entry) => entry.path)).toEqual([
|
|
198
|
+
'packages/ai-assistant/src/modules/ai_assistant/__integration__/TC-AI-001.spec.ts',
|
|
199
|
+
'packages/ai-assistant/src/modules/ai_assistant/__integration__/TC-AI-002.spec.ts',
|
|
200
|
+
])
|
|
201
|
+
|
|
202
|
+
process.env.OM_TEST_DISCOVERY_REQUIRED_KEY = 'required-key'
|
|
203
|
+
discovered = discoverIntegrationSpecFiles(tempRoot, path.join(tempRoot, '.ai', 'qa', 'tests'))
|
|
204
|
+
expect(discovered.map((entry) => entry.path)).toEqual([
|
|
205
|
+
'packages/ai-assistant/src/modules/ai_assistant/__integration__/live/TC-AI-003.spec.ts',
|
|
206
|
+
'packages/ai-assistant/src/modules/ai_assistant/__integration__/TC-AI-001.spec.ts',
|
|
207
|
+
'packages/ai-assistant/src/modules/ai_assistant/__integration__/TC-AI-002.spec.ts',
|
|
208
|
+
])
|
|
209
|
+
})
|
|
142
210
|
})
|
|
@@ -6,6 +6,8 @@ export type IntegrationSpecDiscoveryItem = {
|
|
|
6
6
|
moduleName: string | null
|
|
7
7
|
isOverlay: boolean
|
|
8
8
|
requiredModules: string[]
|
|
9
|
+
requiredEnvVars: string[]
|
|
10
|
+
requiredAnyEnvVars: string[]
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
const MODULE_INTEGRATION_DIRECTORY_NAME = '__integration__'
|
|
@@ -26,6 +28,8 @@ const DISCOVERY_IGNORED_DIRS = new Set([
|
|
|
26
28
|
])
|
|
27
29
|
const INTEGRATION_META_FILE_NAMES = ['meta.ts', 'index.ts'] as const
|
|
28
30
|
const INTEGRATION_META_DEPENDENCY_KEYS = ['dependsOnModules', 'requiredModules', 'requiresModules'] as const
|
|
31
|
+
const INTEGRATION_META_REQUIRED_ENV_KEYS = ['requiredEnvVars', 'requiresEnvVars'] as const
|
|
32
|
+
const INTEGRATION_META_REQUIRED_ANY_ENV_KEYS = ['requiredAnyEnvVars', 'requiresAnyEnvVars'] as const
|
|
29
33
|
const DEFAULT_OVERLAY_ROOT = 'packages/enterprise'
|
|
30
34
|
|
|
31
35
|
export function normalizePath(filePath: string): string {
|
|
@@ -160,15 +164,15 @@ function isOverlayIntegrationPath(relativePath: string, overlayRoot: string): bo
|
|
|
160
164
|
return normalizePath(relativePath).startsWith(`${normalizePath(overlayRoot)}/`)
|
|
161
165
|
}
|
|
162
166
|
|
|
163
|
-
function
|
|
167
|
+
function extractStringArrayFromSource(source: string, keys: readonly string[]): string[] {
|
|
164
168
|
const collected = new Set<string>()
|
|
165
|
-
for (const key of
|
|
169
|
+
for (const key of keys) {
|
|
166
170
|
const keyPattern = new RegExp(`${key}\\s*:\\s*\\[([\\s\\S]*?)\\]`, 'm')
|
|
167
171
|
const keyMatch = source.match(keyPattern)
|
|
168
172
|
if (!keyMatch?.[1]) continue
|
|
169
173
|
const valuesPattern = /['"`]([a-zA-Z0-9_.-]+)['"`]/g
|
|
170
174
|
for (const valueMatch of keyMatch[1].matchAll(valuesPattern)) {
|
|
171
|
-
const value =
|
|
175
|
+
const value = String(valueMatch[1] ?? '').trim()
|
|
172
176
|
if (value) {
|
|
173
177
|
collected.add(value)
|
|
174
178
|
}
|
|
@@ -177,6 +181,18 @@ function extractDependencyListFromSource(source: string): string[] {
|
|
|
177
181
|
return Array.from(collected)
|
|
178
182
|
}
|
|
179
183
|
|
|
184
|
+
function extractDependencyListFromSource(source: string): string[] {
|
|
185
|
+
return extractStringArrayFromSource(source, INTEGRATION_META_DEPENDENCY_KEYS).map(normalizeModuleId)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function extractRequiredEnvVarsFromSource(source: string): string[] {
|
|
189
|
+
return extractStringArrayFromSource(source, INTEGRATION_META_REQUIRED_ENV_KEYS)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function extractRequiredAnyEnvVarsFromSource(source: string): string[] {
|
|
193
|
+
return extractStringArrayFromSource(source, INTEGRATION_META_REQUIRED_ANY_ENV_KEYS)
|
|
194
|
+
}
|
|
195
|
+
|
|
180
196
|
function readDependenciesFromMetadataFile(absolutePath: string): string[] {
|
|
181
197
|
try {
|
|
182
198
|
return extractDependencyListFromSource(readFileSync(absolutePath, 'utf8'))
|
|
@@ -185,6 +201,22 @@ function readDependenciesFromMetadataFile(absolutePath: string): string[] {
|
|
|
185
201
|
}
|
|
186
202
|
}
|
|
187
203
|
|
|
204
|
+
function readRequiredEnvVarsFromMetadataFile(absolutePath: string): string[] {
|
|
205
|
+
try {
|
|
206
|
+
return extractRequiredEnvVarsFromSource(readFileSync(absolutePath, 'utf8'))
|
|
207
|
+
} catch {
|
|
208
|
+
return []
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function readRequiredAnyEnvVarsFromMetadataFile(absolutePath: string): string[] {
|
|
213
|
+
try {
|
|
214
|
+
return extractRequiredAnyEnvVarsFromSource(readFileSync(absolutePath, 'utf8'))
|
|
215
|
+
} catch {
|
|
216
|
+
return []
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
188
220
|
function resolveIntegrationRootDirectory(relativeSpecPath: string): string | null {
|
|
189
221
|
const segments = normalizePath(relativeSpecPath).split('/')
|
|
190
222
|
const integrationDirectoryIndex = segments.indexOf(MODULE_INTEGRATION_DIRECTORY_NAME)
|
|
@@ -231,6 +263,85 @@ function resolveRequiredModulesForSpec(projectRoot: string, relativeSpecPath: st
|
|
|
231
263
|
return Array.from(requiredModules)
|
|
232
264
|
}
|
|
233
265
|
|
|
266
|
+
function resolveRequiredEnvVarsForSpec(projectRoot: string, relativeSpecPath: string): string[] {
|
|
267
|
+
const requiredEnvVars = new Set<string>()
|
|
268
|
+
const normalizedSpecPath = normalizePath(relativeSpecPath)
|
|
269
|
+
const absoluteSpecPath = path.join(projectRoot, normalizedSpecPath)
|
|
270
|
+
const integrationRoot = resolveIntegrationRootDirectory(normalizedSpecPath)
|
|
271
|
+
|
|
272
|
+
if (integrationRoot) {
|
|
273
|
+
const relativeDirectory = normalizePath(path.dirname(normalizedSpecPath))
|
|
274
|
+
const integrationRootAbsolute = path.join(projectRoot, integrationRoot)
|
|
275
|
+
const directoryAbsolute = path.join(projectRoot, relativeDirectory)
|
|
276
|
+
const relativeFromIntegrationRoot = normalizePath(path.relative(integrationRootAbsolute, directoryAbsolute))
|
|
277
|
+
const pathSegments = relativeFromIntegrationRoot === '.'
|
|
278
|
+
? []
|
|
279
|
+
: relativeFromIntegrationRoot.split('/').filter(Boolean)
|
|
280
|
+
|
|
281
|
+
let currentDirectory = integrationRootAbsolute
|
|
282
|
+
const traversal = [currentDirectory, ...pathSegments.map((segment) => {
|
|
283
|
+
currentDirectory = path.join(currentDirectory, segment)
|
|
284
|
+
return currentDirectory
|
|
285
|
+
})]
|
|
286
|
+
|
|
287
|
+
for (const directoryPath of traversal) {
|
|
288
|
+
for (const metadataFileName of INTEGRATION_META_FILE_NAMES) {
|
|
289
|
+
const metadataPath = path.join(directoryPath, metadataFileName)
|
|
290
|
+
readRequiredEnvVarsFromMetadataFile(metadataPath).forEach((envVar) => requiredEnvVars.add(envVar))
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
readRequiredEnvVarsFromMetadataFile(absoluteSpecPath).forEach((envVar) => requiredEnvVars.add(envVar))
|
|
296
|
+
const specFileName = path.basename(normalizedSpecPath, '.spec.ts')
|
|
297
|
+
const perTestMetadataPath = path.join(path.dirname(absoluteSpecPath), `${specFileName}.meta.ts`)
|
|
298
|
+
readRequiredEnvVarsFromMetadataFile(perTestMetadataPath).forEach((envVar) => requiredEnvVars.add(envVar))
|
|
299
|
+
|
|
300
|
+
return Array.from(requiredEnvVars)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function resolveRequiredAnyEnvVarsForSpec(projectRoot: string, relativeSpecPath: string): string[] {
|
|
304
|
+
const requiredAnyEnvVars = new Set<string>()
|
|
305
|
+
const normalizedSpecPath = normalizePath(relativeSpecPath)
|
|
306
|
+
const absoluteSpecPath = path.join(projectRoot, normalizedSpecPath)
|
|
307
|
+
const integrationRoot = resolveIntegrationRootDirectory(normalizedSpecPath)
|
|
308
|
+
|
|
309
|
+
if (integrationRoot) {
|
|
310
|
+
const relativeDirectory = normalizePath(path.dirname(normalizedSpecPath))
|
|
311
|
+
const integrationRootAbsolute = path.join(projectRoot, integrationRoot)
|
|
312
|
+
const directoryAbsolute = path.join(projectRoot, relativeDirectory)
|
|
313
|
+
const relativeFromIntegrationRoot = normalizePath(path.relative(integrationRootAbsolute, directoryAbsolute))
|
|
314
|
+
const pathSegments = relativeFromIntegrationRoot === '.'
|
|
315
|
+
? []
|
|
316
|
+
: relativeFromIntegrationRoot.split('/').filter(Boolean)
|
|
317
|
+
|
|
318
|
+
let currentDirectory = integrationRootAbsolute
|
|
319
|
+
const traversal = [currentDirectory, ...pathSegments.map((segment) => {
|
|
320
|
+
currentDirectory = path.join(currentDirectory, segment)
|
|
321
|
+
return currentDirectory
|
|
322
|
+
})]
|
|
323
|
+
|
|
324
|
+
for (const directoryPath of traversal) {
|
|
325
|
+
for (const metadataFileName of INTEGRATION_META_FILE_NAMES) {
|
|
326
|
+
const metadataPath = path.join(directoryPath, metadataFileName)
|
|
327
|
+
readRequiredAnyEnvVarsFromMetadataFile(metadataPath).forEach((envVar) => requiredAnyEnvVars.add(envVar))
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
readRequiredAnyEnvVarsFromMetadataFile(absoluteSpecPath).forEach((envVar) => requiredAnyEnvVars.add(envVar))
|
|
333
|
+
const specFileName = path.basename(normalizedSpecPath, '.spec.ts')
|
|
334
|
+
const perTestMetadataPath = path.join(path.dirname(absoluteSpecPath), `${specFileName}.meta.ts`)
|
|
335
|
+
readRequiredAnyEnvVarsFromMetadataFile(perTestMetadataPath).forEach((envVar) => requiredAnyEnvVars.add(envVar))
|
|
336
|
+
|
|
337
|
+
return Array.from(requiredAnyEnvVars)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function isEnvironmentVariableConfigured(name: string): boolean {
|
|
341
|
+
const value = process.env[name]
|
|
342
|
+
return typeof value === 'string' && value.trim().length > 0
|
|
343
|
+
}
|
|
344
|
+
|
|
234
345
|
export function discoverIntegrationSpecFiles(projectRoot: string, legacyIntegrationRoot: string): IntegrationSpecDiscoveryItem[] {
|
|
235
346
|
const discoveredByPath = new Map<string, IntegrationSpecDiscoveryItem>()
|
|
236
347
|
const overlayRoot = resolveOverlayRootPath()
|
|
@@ -249,6 +360,8 @@ export function discoverIntegrationSpecFiles(projectRoot: string, legacyIntegrat
|
|
|
249
360
|
moduleName: extractModuleNameFromIntegrationPath(relativePath),
|
|
250
361
|
isOverlay: isOverlayIntegrationPath(relativePath, overlayRoot),
|
|
251
362
|
requiredModules: resolveRequiredModulesForSpec(projectRoot, relativePath),
|
|
363
|
+
requiredEnvVars: resolveRequiredEnvVarsForSpec(projectRoot, relativePath),
|
|
364
|
+
requiredAnyEnvVars: resolveRequiredAnyEnvVarsForSpec(projectRoot, relativePath),
|
|
252
365
|
})
|
|
253
366
|
}
|
|
254
367
|
|
|
@@ -261,6 +374,8 @@ export function discoverIntegrationSpecFiles(projectRoot: string, legacyIntegrat
|
|
|
261
374
|
moduleName: extractModuleNameFromIntegrationPath(relativePath),
|
|
262
375
|
isOverlay: isOverlayIntegrationPath(relativePath, overlayRoot),
|
|
263
376
|
requiredModules: resolveRequiredModulesForSpec(projectRoot, relativePath),
|
|
377
|
+
requiredEnvVars: resolveRequiredEnvVarsForSpec(projectRoot, relativePath),
|
|
378
|
+
requiredAnyEnvVars: resolveRequiredAnyEnvVarsForSpec(projectRoot, relativePath),
|
|
264
379
|
})
|
|
265
380
|
}
|
|
266
381
|
}
|
|
@@ -280,6 +395,15 @@ export function discoverIntegrationSpecFiles(projectRoot: string, legacyIntegrat
|
|
|
280
395
|
if (file.requiredModules.some((moduleId) => !enabledModules.has(normalizeModuleId(moduleId)))) {
|
|
281
396
|
return false
|
|
282
397
|
}
|
|
398
|
+
if (file.requiredEnvVars.some((envVar) => !isEnvironmentVariableConfigured(envVar))) {
|
|
399
|
+
return false
|
|
400
|
+
}
|
|
401
|
+
if (
|
|
402
|
+
file.requiredAnyEnvVars.length > 0 &&
|
|
403
|
+
!file.requiredAnyEnvVars.some((envVar) => isEnvironmentVariableConfigured(envVar))
|
|
404
|
+
) {
|
|
405
|
+
return false
|
|
406
|
+
}
|
|
283
407
|
if (!file.isOverlay) {
|
|
284
408
|
return true
|
|
285
409
|
}
|