@open-mercato/cli 0.7.1-develop.7191.1.3c9346566b → 0.7.1-develop.7194.1.ab4fc81f82
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 +6 -2
- package/dist/agentic/github-copilot/copilot-instructions.md.template +37 -0
- package/dist/agentic/github-copilot/instructions/entity-guard.instructions.md +27 -0
- package/dist/agentic/github-copilot/instructions/generated-guard.instructions.md +10 -0
- package/dist/agentic/github-copilot/mcp.json.example +13 -0
- package/dist/agentic/guides/module-facts.json +11527 -283
- package/dist/agentic/guides/module-facts.v2.json +11527 -283
- package/dist/agentic/guides/modules/agent_orchestrator/acl-features.md +31 -0
- package/dist/agentic/guides/modules/agent_orchestrator/active-extension-bindings.md +12 -0
- package/dist/agentic/guides/modules/agent_orchestrator/ai-agents.md +12 -0
- package/dist/agentic/guides/modules/agent_orchestrator/ai-tools-mcp-capabilities.md +13 -0
- package/dist/agentic/guides/modules/agent_orchestrator/backend-pages.md +28 -0
- package/dist/agentic/guides/modules/agent_orchestrator/cli-commands.md +14 -0
- package/dist/agentic/guides/modules/agent_orchestrator/contribution-resolutions.md +49 -0
- package/dist/agentic/guides/modules/agent_orchestrator/di-registrations-rich.md +42 -0
- package/dist/agentic/guides/modules/agent_orchestrator/di-service-tokens.md +24 -0
- package/dist/agentic/guides/modules/agent_orchestrator/domain-commands.md +28 -0
- package/dist/agentic/guides/modules/agent_orchestrator/encryption.md +20 -0
- package/dist/agentic/guides/modules/agent_orchestrator/entities.md +31 -0
- package/dist/agentic/guides/modules/agent_orchestrator/events.md +38 -0
- package/dist/agentic/guides/modules/agent_orchestrator/exact-override-targets.md +122 -0
- package/dist/agentic/guides/modules/agent_orchestrator/host-extension-points.md +10 -0
- package/dist/agentic/guides/modules/agent_orchestrator/index.md +31 -0
- package/dist/agentic/guides/modules/agent_orchestrator/owned-contract-module-metadata.md +11 -0
- package/dist/agentic/guides/modules/agent_orchestrator/setup.md +11 -0
- package/dist/agentic/guides/modules/agent_orchestrator/umes-contributions.md +49 -0
- package/dist/agentic/guides/modules/agent_orchestrator/umes-hosts.md +61 -0
- package/dist/agentic/guides/modules/agent_orchestrator/workers.md +14 -0
- package/dist/agentic/guides/modules/ai_assistant/di-registrations-rich.md +3 -2
- package/dist/agentic/guides/modules/ai_assistant/di-service-tokens.md +2 -1
- package/dist/agentic/guides/modules/ai_assistant/exact-override-targets.md +3 -2
- package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
- package/dist/agentic/guides/modules/api_docs/index.md +1 -1
- package/dist/agentic/guides/modules/api_keys/index.md +1 -1
- package/dist/agentic/guides/modules/attachments/index.md +1 -1
- package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
- package/dist/agentic/guides/modules/auth/index.md +1 -1
- package/dist/agentic/guides/modules/business_rules/index.md +1 -1
- package/dist/agentic/guides/modules/catalog/index.md +1 -1
- package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
- package/dist/agentic/guides/modules/channel_discord/index.md +1 -1
- package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
- package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
- package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
- package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
- package/dist/agentic/guides/modules/channel_resend/index.md +1 -1
- package/dist/agentic/guides/modules/channel_ses/index.md +1 -1
- package/dist/agentic/guides/modules/checkout/index.md +1 -1
- package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
- package/dist/agentic/guides/modules/configs/index.md +1 -1
- package/dist/agentic/guides/modules/content/index.md +1 -1
- package/dist/agentic/guides/modules/currencies/index.md +1 -1
- package/dist/agentic/guides/modules/customer_accounts/cli-commands.md +11 -0
- package/dist/agentic/guides/modules/customer_accounts/exact-override-targets.md +5 -4
- package/dist/agentic/guides/modules/customer_accounts/index.md +3 -2
- package/dist/agentic/guides/modules/customer_accounts/setup.md +1 -1
- package/dist/agentic/guides/modules/customers/contribution-resolutions.md +1 -0
- package/dist/agentic/guides/modules/customers/exact-override-targets.md +1 -0
- package/dist/agentic/guides/modules/customers/incoming-installed-contributions.md +3 -0
- package/dist/agentic/guides/modules/customers/index.md +2 -2
- package/dist/agentic/guides/modules/customers/umes-contributions.md +1 -0
- package/dist/agentic/guides/modules/dashboards/index.md +1 -1
- package/dist/agentic/guides/modules/data_sync/index.md +1 -1
- package/dist/agentic/guides/modules/design_system/index.md +1 -1
- package/dist/agentic/guides/modules/devices/index.md +1 -1
- package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
- package/dist/agentic/guides/modules/directory/index.md +1 -1
- package/dist/agentic/guides/modules/documents/index.md +1 -1
- package/dist/agentic/guides/modules/entities/index.md +1 -1
- package/dist/agentic/guides/modules/eudr/index.md +1 -1
- package/dist/agentic/guides/modules/events/index.md +1 -1
- package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
- package/dist/agentic/guides/modules/generators/index.md +1 -1
- package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
- package/dist/agentic/guides/modules/integrations/index.md +1 -1
- package/dist/agentic/guides/modules/messages/index.md +1 -1
- package/dist/agentic/guides/modules/notifications/index.md +1 -1
- package/dist/agentic/guides/modules/onboarding/index.md +1 -1
- package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
- package/dist/agentic/guides/modules/perspectives/index.md +1 -1
- package/dist/agentic/guides/modules/phone_calls/index.md +1 -1
- package/dist/agentic/guides/modules/planner/index.md +1 -1
- package/dist/agentic/guides/modules/portal/index.md +1 -1
- package/dist/agentic/guides/modules/progress/index.md +1 -1
- package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
- package/dist/agentic/guides/modules/query_index/index.md +1 -1
- package/dist/agentic/guides/modules/record_locks/index.md +1 -1
- package/dist/agentic/guides/modules/resources/index.md +1 -1
- package/dist/agentic/guides/modules/sales/index.md +1 -1
- package/dist/agentic/guides/modules/scheduler/index.md +1 -1
- package/dist/agentic/guides/modules/search/index.md +1 -1
- package/dist/agentic/guides/modules/security/index.md +1 -1
- package/dist/agentic/guides/modules/seeds/cli-commands.md +14 -0
- package/dist/agentic/guides/modules/seeds/exact-override-targets.md +14 -0
- package/dist/agentic/guides/modules/seeds/index.md +14 -0
- package/dist/agentic/guides/modules/seeds/owned-contract-module-metadata.md +11 -0
- package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
- package/dist/agentic/guides/modules/sso/index.md +1 -1
- package/dist/agentic/guides/modules/staff/index.md +1 -1
- package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
- package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
- package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
- package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
- package/dist/agentic/guides/modules/tillio/index.md +1 -1
- package/dist/agentic/guides/modules/translations/index.md +1 -1
- package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
- package/dist/agentic/guides/modules/webhooks/index.md +1 -1
- package/dist/agentic/guides/modules/wms/index.md +1 -1
- package/dist/agentic/guides/modules/workflows/acl-features.md +11 -1
- package/dist/agentic/guides/modules/workflows/active-extension-bindings.md +4 -1
- package/dist/agentic/guides/modules/workflows/ai-tools-mcp-capabilities.md +17 -0
- package/dist/agentic/guides/modules/workflows/backend-pages.md +3 -0
- package/dist/agentic/guides/modules/workflows/contribution-resolutions.md +24 -0
- package/dist/agentic/guides/modules/workflows/di-registrations-rich.md +11 -7
- package/dist/agentic/guides/modules/workflows/di-service-tokens.md +11 -7
- package/dist/agentic/guides/modules/workflows/domain-commands.md +11 -0
- package/dist/agentic/guides/modules/workflows/entities.md +2 -0
- package/dist/agentic/guides/modules/workflows/events.md +14 -8
- package/dist/agentic/guides/modules/workflows/exact-override-targets.md +45 -10
- package/dist/agentic/guides/modules/workflows/frontend-pages.md +2 -0
- package/dist/agentic/guides/modules/workflows/incoming-installed-contributions.md +17 -0
- package/dist/agentic/guides/modules/workflows/index.md +15 -12
- package/dist/agentic/guides/modules/workflows/notifications.md +2 -0
- package/dist/agentic/guides/modules/workflows/setup.md +1 -1
- package/dist/agentic/guides/modules/workflows/umes-contributions.md +24 -0
- package/dist/agentic/guides/modules/workflows/umes-hosts.md +14 -6
- package/dist/agentic/guides/modules/workflows/workers.md +4 -1
- package/dist/agentic/guides/reference-module-facts.json +2 -2
- package/dist/agentic/guides/reference-modules/example/index.md +1 -1
- package/dist/agentic/guides/upstream/AGENTS.md +3 -1
- package/dist/agentic/guides/upstream/manifest.json +2 -2
- package/dist/agentic/shared/ai/harness/design-system-inventory.json +118 -106
- package/dist/agentic/shared/ai/harness/source-link-inventory.json +8 -8
- package/dist/lib/agentic-init.js +4 -0
- package/dist/lib/agentic-init.js.map +2 -2
- package/dist/lib/agentic-setup.js +19 -5
- package/dist/lib/agentic-setup.js.map +2 -2
- package/dist/lib/generators/extensions/agent-files.js +913 -0
- package/dist/lib/generators/extensions/agent-files.js.map +7 -0
- package/dist/lib/generators/extensions/index.js +2 -0
- package/dist/lib/generators/extensions/index.js.map +2 -2
- package/dist/lib/generators/index.js +3 -1
- package/dist/lib/generators/index.js.map +2 -2
- package/dist/lib/generators/module-registry.js +40 -11
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/generators/route-collisions.js +35 -0
- package/dist/lib/generators/route-collisions.js.map +7 -0
- package/dist/lib/generators/web-research-adapters.js +95 -0
- package/dist/lib/generators/web-research-adapters.js.map +7 -0
- package/dist/mercato.js +5 -2
- package/dist/mercato.js.map +2 -2
- package/package.json +6 -6
- package/src/__tests__/agent-files.test.ts +199 -0
- package/src/__tests__/mercato.test.ts +14 -0
- package/src/lib/__tests__/agentic-init.test.ts +11 -0
- package/src/lib/__tests__/agentic-setup.ownership.test.ts +5 -0
- package/src/lib/agentic-init.ts +4 -0
- package/src/lib/agentic-setup.ts +30 -5
- package/src/lib/generators/__tests__/agent-files-extension.test.ts +477 -0
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +25 -8
- package/src/lib/generators/__tests__/route-collisions.test.ts +155 -0
- package/src/lib/generators/extensions/agent-files.ts +1332 -0
- package/src/lib/generators/extensions/index.ts +2 -0
- package/src/lib/generators/index.ts +1 -0
- package/src/lib/generators/module-registry.ts +66 -17
- package/src/lib/generators/route-collisions.ts +57 -0
- package/src/lib/generators/web-research-adapters.ts +133 -0
- package/src/mercato.ts +4 -1
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import type { ModuleScanContext } from '../extension'
|
|
5
|
+
import { createAgentFilesExtension } from '../extensions/agent-files'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build a fake repo root with `docker/opencode` + `packages` markers (so the
|
|
9
|
+
* extension's `findRepoRoot` resolves it) and a module whose `agents/<id>/`
|
|
10
|
+
* fixture lives under `appBase`.
|
|
11
|
+
*/
|
|
12
|
+
function makeRepoFixture(): { repoRoot: string; appBase: string; pkgBase: string } {
|
|
13
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-files-repo-'))
|
|
14
|
+
fs.mkdirSync(path.join(repoRoot, 'docker', 'opencode'), { recursive: true })
|
|
15
|
+
fs.mkdirSync(path.join(repoRoot, 'packages'), { recursive: true })
|
|
16
|
+
|
|
17
|
+
const appBase = path.join(repoRoot, 'apps', 'mercato', 'src', 'modules', 'agent_examples')
|
|
18
|
+
const pkgBase = path.join(repoRoot, 'packages', 'core', 'src', 'modules', 'agent_examples')
|
|
19
|
+
const agentDir = path.join(appBase, 'agents', 'deals_health_check')
|
|
20
|
+
fs.mkdirSync(agentDir, { recursive: true })
|
|
21
|
+
|
|
22
|
+
fs.writeFileSync(
|
|
23
|
+
path.join(agentDir, 'AGENT.md'),
|
|
24
|
+
[
|
|
25
|
+
'---',
|
|
26
|
+
'id: deals.health_check',
|
|
27
|
+
'label: Deal health check',
|
|
28
|
+
'description: Assess a deal.',
|
|
29
|
+
'maxSteps: 12',
|
|
30
|
+
'---',
|
|
31
|
+
'You assess a deal.',
|
|
32
|
+
].join('\n'),
|
|
33
|
+
'utf8',
|
|
34
|
+
)
|
|
35
|
+
fs.writeFileSync(
|
|
36
|
+
path.join(agentDir, 'OUTCOME.md'),
|
|
37
|
+
[
|
|
38
|
+
'---',
|
|
39
|
+
'kind: proposal',
|
|
40
|
+
'---',
|
|
41
|
+
'```json',
|
|
42
|
+
JSON.stringify({
|
|
43
|
+
type: 'object',
|
|
44
|
+
required: ['confidence'],
|
|
45
|
+
properties: { confidence: { type: 'number', minimum: 0, maximum: 1 } },
|
|
46
|
+
}),
|
|
47
|
+
'```',
|
|
48
|
+
].join('\n'),
|
|
49
|
+
'utf8',
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return { repoRoot, appBase, pkgBase }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createScanContext(moduleId: string, appBase: string, pkgBase: string): ModuleScanContext {
|
|
56
|
+
return {
|
|
57
|
+
moduleId,
|
|
58
|
+
roots: { appBase, pkgBase },
|
|
59
|
+
imps: { appBase: `@/modules/${moduleId}`, pkgBase: `@open-mercato/core/modules/${moduleId}` },
|
|
60
|
+
importIdRef: { value: 0 },
|
|
61
|
+
sharedImports: [],
|
|
62
|
+
resolveModuleFile: (() => null) as ModuleScanContext['resolveModuleFile'],
|
|
63
|
+
resolveFirstModuleFile: (() => null) as ModuleScanContext['resolveFirstModuleFile'],
|
|
64
|
+
processStandaloneConfig: () => null,
|
|
65
|
+
sanitizeGeneratedModuleSpecifier: (importPath) => importPath,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('createAgentFilesExtension', () => {
|
|
70
|
+
let repoRoot = ''
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
if (repoRoot) fs.rmSync(repoRoot, { recursive: true, force: true })
|
|
73
|
+
repoRoot = ''
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('discovers agents/<id>/ and emits the manifest + docker agent file', () => {
|
|
77
|
+
const fixture = makeRepoFixture()
|
|
78
|
+
repoRoot = fixture.repoRoot
|
|
79
|
+
|
|
80
|
+
const extension = createAgentFilesExtension()
|
|
81
|
+
expect(extension.id).toBe('registry.agent-files')
|
|
82
|
+
|
|
83
|
+
extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase))
|
|
84
|
+
const output = extension.generateOutput()
|
|
85
|
+
expect(output.size).toBe(0)
|
|
86
|
+
|
|
87
|
+
const manifestPath = path.join(
|
|
88
|
+
repoRoot,
|
|
89
|
+
'packages/enterprise/src/modules/agent_orchestrator/generated/file-agents.generated.ts',
|
|
90
|
+
)
|
|
91
|
+
expect(fs.existsSync(manifestPath)).toBe(true)
|
|
92
|
+
const manifest = fs.readFileSync(manifestPath, 'utf8')
|
|
93
|
+
expect(manifest).toContain('AUTO-GENERATED by mercato generate registry')
|
|
94
|
+
expect(manifest).toContain("id: \"deals.health_check\"")
|
|
95
|
+
expect(manifest).toContain("openCodeAgentName: \"deals_health_check\"")
|
|
96
|
+
expect(manifest).toContain('export const fileAgentDescriptors')
|
|
97
|
+
|
|
98
|
+
const dockerFile = path.join(repoRoot, 'docker/opencode/agents/deals_health_check.md')
|
|
99
|
+
expect(fs.existsSync(dockerFile)).toBe(true)
|
|
100
|
+
const dockerContent = fs.readFileSync(dockerFile, 'utf8')
|
|
101
|
+
expect(dockerContent).toContain('mode: primary')
|
|
102
|
+
expect(dockerContent).toContain('open-mercato_agent_orchestrator_submit_outcome')
|
|
103
|
+
expect(dockerContent).toContain('write: deny')
|
|
104
|
+
// No sub-agents declared → `task` denied and no delegate tool (unchanged).
|
|
105
|
+
expect(dockerContent).toContain('task: deny')
|
|
106
|
+
expect(dockerContent).not.toContain('delegate_agent')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('routes file-agent delegation through delegate_agent instead of the task tool', () => {
|
|
110
|
+
const fixture = makeRepoFixture()
|
|
111
|
+
repoRoot = fixture.repoRoot
|
|
112
|
+
const agentDir = path.join(fixture.appBase, 'agents', 'deals_health_check')
|
|
113
|
+
|
|
114
|
+
// Declare a sub-agent under sub-agents/<subid>/ (researcher + non-delegating).
|
|
115
|
+
fs.writeFileSync(
|
|
116
|
+
path.join(agentDir, 'AGENT.md'),
|
|
117
|
+
[
|
|
118
|
+
'---',
|
|
119
|
+
'id: deals.health_check',
|
|
120
|
+
'label: Deal health check',
|
|
121
|
+
'description: Assess a deal.',
|
|
122
|
+
'---',
|
|
123
|
+
'You assess a deal.',
|
|
124
|
+
].join('\n'),
|
|
125
|
+
'utf8',
|
|
126
|
+
)
|
|
127
|
+
const subDir = path.join(agentDir, 'sub-agents', 'company_researcher')
|
|
128
|
+
fs.mkdirSync(subDir, { recursive: true })
|
|
129
|
+
fs.writeFileSync(
|
|
130
|
+
path.join(subDir, 'AGENT.md'),
|
|
131
|
+
[
|
|
132
|
+
'---',
|
|
133
|
+
'id: deals.company_researcher',
|
|
134
|
+
'label: Company researcher',
|
|
135
|
+
'description: Research a company.',
|
|
136
|
+
'---',
|
|
137
|
+
'You research a company.',
|
|
138
|
+
].join('\n'),
|
|
139
|
+
'utf8',
|
|
140
|
+
)
|
|
141
|
+
fs.writeFileSync(
|
|
142
|
+
path.join(subDir, 'OUTCOME.md'),
|
|
143
|
+
[
|
|
144
|
+
'---',
|
|
145
|
+
'kind: research',
|
|
146
|
+
'---',
|
|
147
|
+
'```json',
|
|
148
|
+
JSON.stringify({ type: 'object', properties: { summary: { type: 'string' } } }),
|
|
149
|
+
'```',
|
|
150
|
+
].join('\n'),
|
|
151
|
+
'utf8',
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
const extension = createAgentFilesExtension()
|
|
155
|
+
extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase))
|
|
156
|
+
extension.generateOutput()
|
|
157
|
+
|
|
158
|
+
const primary = fs.readFileSync(
|
|
159
|
+
path.join(repoRoot, 'docker/opencode/agents/deals_health_check.md'),
|
|
160
|
+
'utf8',
|
|
161
|
+
)
|
|
162
|
+
// The orchestrator gains the delegate_agent MCP tool, denies `task`, and
|
|
163
|
+
// never grants the built-in task tool.
|
|
164
|
+
expect(primary).toContain('open-mercato_agent_orchestrator_delegate_agent')
|
|
165
|
+
expect(primary).toContain('task: deny')
|
|
166
|
+
expect(primary).not.toContain('"task": true')
|
|
167
|
+
// The prompt delegates through delegate_agent (not `task`) using the FULL
|
|
168
|
+
// sub-agent id.
|
|
169
|
+
expect(primary).toContain('## Sub-agents')
|
|
170
|
+
expect(primary).toContain('delegate_agent')
|
|
171
|
+
expect(primary).toContain('deals.company_researcher')
|
|
172
|
+
expect(primary).not.toContain('by calling the `task` tool')
|
|
173
|
+
|
|
174
|
+
// The sub-agent file gets neither the delegate tool nor `task` (depth cap = 1).
|
|
175
|
+
const sub = fs.readFileSync(
|
|
176
|
+
path.join(repoRoot, 'docker/opencode/agents/deals_company_researcher.md'),
|
|
177
|
+
'utf8',
|
|
178
|
+
)
|
|
179
|
+
expect(sub).toContain('mode: subagent')
|
|
180
|
+
expect(sub).toContain('task: deny')
|
|
181
|
+
expect(sub).not.toContain('delegate_agent')
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('emits an agent SAMPLE.json into the manifest as sampleInput', () => {
|
|
185
|
+
const fixture = makeRepoFixture()
|
|
186
|
+
repoRoot = fixture.repoRoot
|
|
187
|
+
const agentDir = path.join(fixture.appBase, 'agents', 'deals_health_check')
|
|
188
|
+
fs.writeFileSync(
|
|
189
|
+
path.join(agentDir, 'SAMPLE.json'),
|
|
190
|
+
JSON.stringify({ deal: { id: 'demo-deal-1', stage: 'Proposal' } }),
|
|
191
|
+
'utf8',
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
const extension = createAgentFilesExtension()
|
|
195
|
+
extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase))
|
|
196
|
+
extension.generateOutput()
|
|
197
|
+
|
|
198
|
+
const manifest = fs.readFileSync(
|
|
199
|
+
path.join(repoRoot, 'packages/enterprise/src/modules/agent_orchestrator/generated/file-agents.generated.ts'),
|
|
200
|
+
'utf8',
|
|
201
|
+
)
|
|
202
|
+
expect(manifest).toContain('sampleInput: {"deal":{"id":"demo-deal-1","stage":"Proposal"}}')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('fails generation on a malformed SAMPLE.json', () => {
|
|
206
|
+
const fixture = makeRepoFixture()
|
|
207
|
+
repoRoot = fixture.repoRoot
|
|
208
|
+
const agentDir = path.join(fixture.appBase, 'agents', 'deals_health_check')
|
|
209
|
+
fs.writeFileSync(path.join(agentDir, 'SAMPLE.json'), '{ not valid json', 'utf8')
|
|
210
|
+
|
|
211
|
+
const extension = createAgentFilesExtension()
|
|
212
|
+
expect(() =>
|
|
213
|
+
extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase)),
|
|
214
|
+
).toThrow(/malformed SAMPLE\.json/)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('emits an agent FACTS.json into the manifest as facts', () => {
|
|
218
|
+
const fixture = makeRepoFixture()
|
|
219
|
+
repoRoot = fixture.repoRoot
|
|
220
|
+
const agentDir = path.join(fixture.appBase, 'agents', 'deals_health_check')
|
|
221
|
+
fs.writeFileSync(
|
|
222
|
+
path.join(agentDir, 'FACTS.json'),
|
|
223
|
+
JSON.stringify({
|
|
224
|
+
facts: [
|
|
225
|
+
{ label: 'Stage', source: 'input', path: 'deal.stage' },
|
|
226
|
+
{ label: 'Confidence', source: 'payload', path: 'confidence', format: 'percent' },
|
|
227
|
+
],
|
|
228
|
+
}),
|
|
229
|
+
'utf8',
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
const extension = createAgentFilesExtension()
|
|
233
|
+
extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase))
|
|
234
|
+
extension.generateOutput()
|
|
235
|
+
|
|
236
|
+
const manifest = fs.readFileSync(
|
|
237
|
+
path.join(repoRoot, 'packages/enterprise/src/modules/agent_orchestrator/generated/file-agents.generated.ts'),
|
|
238
|
+
'utf8',
|
|
239
|
+
)
|
|
240
|
+
expect(manifest).toContain(
|
|
241
|
+
'facts: [{"label":"Stage","source":"input","path":"deal.stage"},{"label":"Confidence","source":"payload","path":"confidence","format":"percent"}]',
|
|
242
|
+
)
|
|
243
|
+
expect(manifest).toContain('export type FileAgentFact')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('fails generation on a FACTS.json entry missing label/source/path', () => {
|
|
247
|
+
const fixture = makeRepoFixture()
|
|
248
|
+
repoRoot = fixture.repoRoot
|
|
249
|
+
const agentDir = path.join(fixture.appBase, 'agents', 'deals_health_check')
|
|
250
|
+
fs.writeFileSync(
|
|
251
|
+
path.join(agentDir, 'FACTS.json'),
|
|
252
|
+
JSON.stringify({ facts: [{ label: 'Stage', source: 'nowhere', path: 'deal.stage' }] }),
|
|
253
|
+
'utf8',
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
const extension = createAgentFilesExtension()
|
|
257
|
+
expect(() =>
|
|
258
|
+
extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase)),
|
|
259
|
+
).toThrow(/malformed FACTS\.json/)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('fails generation on a malformed agent dir (missing OUTCOME.md)', () => {
|
|
263
|
+
const fixture = makeRepoFixture()
|
|
264
|
+
repoRoot = fixture.repoRoot
|
|
265
|
+
// Remove OUTCOME.md to make the dir malformed.
|
|
266
|
+
fs.rmSync(path.join(fixture.appBase, 'agents', 'deals_health_check', 'OUTCOME.md'))
|
|
267
|
+
|
|
268
|
+
const extension = createAgentFilesExtension()
|
|
269
|
+
expect(() =>
|
|
270
|
+
extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase)),
|
|
271
|
+
).toThrow(/malformed file agent/)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('emits native SKILL.md, unions skill tools, and writes skillsContent to the manifest', () => {
|
|
275
|
+
const fixture = makeRepoFixture()
|
|
276
|
+
repoRoot = fixture.repoRoot
|
|
277
|
+
const agentDir = path.join(fixture.appBase, 'agents', 'deals_health_check')
|
|
278
|
+
|
|
279
|
+
// Reference a skill in AGENT.md and author it under skills/<skill_id>/.
|
|
280
|
+
fs.writeFileSync(
|
|
281
|
+
path.join(agentDir, 'AGENT.md'),
|
|
282
|
+
[
|
|
283
|
+
'---',
|
|
284
|
+
'id: deals.health_check',
|
|
285
|
+
'label: Deal health check',
|
|
286
|
+
'description: Assess a deal.',
|
|
287
|
+
'skills: [stage_playbook]',
|
|
288
|
+
'tools: [customers.get_deal]',
|
|
289
|
+
'---',
|
|
290
|
+
'You assess a deal.',
|
|
291
|
+
].join('\n'),
|
|
292
|
+
'utf8',
|
|
293
|
+
)
|
|
294
|
+
const skillDir = path.join(agentDir, 'skills', 'stage_playbook')
|
|
295
|
+
fs.mkdirSync(path.join(skillDir, 'examples'), { recursive: true })
|
|
296
|
+
fs.writeFileSync(
|
|
297
|
+
path.join(skillDir, 'SKILL.md'),
|
|
298
|
+
[
|
|
299
|
+
'---',
|
|
300
|
+
'id: stage_playbook',
|
|
301
|
+
'label: Stage playbook',
|
|
302
|
+
'description: Pick the next stage.',
|
|
303
|
+
'tools: [customers.analyze_deals]',
|
|
304
|
+
'---',
|
|
305
|
+
'STAGE_PLAYBOOK_BODY',
|
|
306
|
+
].join('\n'),
|
|
307
|
+
'utf8',
|
|
308
|
+
)
|
|
309
|
+
fs.writeFileSync(path.join(skillDir, 'TEMPLATE.md'), 'TEMPLATE_BODY', 'utf8')
|
|
310
|
+
fs.writeFileSync(path.join(skillDir, 'examples', 'a.md'), 'EXAMPLE_ONE', 'utf8')
|
|
311
|
+
|
|
312
|
+
const extension = createAgentFilesExtension()
|
|
313
|
+
extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase))
|
|
314
|
+
extension.generateOutput()
|
|
315
|
+
|
|
316
|
+
// Native OpenCode skill file (sanitized dir name: stage_playbook → stage-playbook).
|
|
317
|
+
const nativeSkill = path.join(repoRoot, 'docker/opencode/skills/stage-playbook/SKILL.md')
|
|
318
|
+
expect(fs.existsSync(nativeSkill)).toBe(true)
|
|
319
|
+
const nativeContent = fs.readFileSync(nativeSkill, 'utf8')
|
|
320
|
+
expect(nativeContent).toContain('name: stage-playbook')
|
|
321
|
+
expect(nativeContent).toContain('STAGE_PLAYBOOK_BODY')
|
|
322
|
+
|
|
323
|
+
// Manifest carries skillsContent and the unioned tool allowlist.
|
|
324
|
+
const manifest = fs.readFileSync(
|
|
325
|
+
path.join(repoRoot, 'packages/enterprise/src/modules/agent_orchestrator/generated/file-agents.generated.ts'),
|
|
326
|
+
'utf8',
|
|
327
|
+
)
|
|
328
|
+
expect(manifest).toContain('skillsContent')
|
|
329
|
+
expect(manifest).toContain('STAGE_PLAYBOOK_BODY')
|
|
330
|
+
expect(manifest).toContain('TEMPLATE_BODY')
|
|
331
|
+
expect(manifest).toContain('EXAMPLE_ONE')
|
|
332
|
+
expect(manifest).toContain('customers.analyze_deals')
|
|
333
|
+
|
|
334
|
+
// The docker agent file's tools block includes the skill tool too.
|
|
335
|
+
const dockerAgent = fs.readFileSync(
|
|
336
|
+
path.join(repoRoot, 'docker/opencode/agents/deals_health_check.md'),
|
|
337
|
+
'utf8',
|
|
338
|
+
)
|
|
339
|
+
// Declared OM tool ids are mapped to OpenCode's MCP tool id form in the allowlist.
|
|
340
|
+
expect(dockerAgent).toContain('open-mercato_customers_analyze_deals')
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('fails generation on a malformed SKILL.md (no frontmatter)', () => {
|
|
344
|
+
const fixture = makeRepoFixture()
|
|
345
|
+
repoRoot = fixture.repoRoot
|
|
346
|
+
const agentDir = path.join(fixture.appBase, 'agents', 'deals_health_check')
|
|
347
|
+
fs.writeFileSync(
|
|
348
|
+
path.join(agentDir, 'AGENT.md'),
|
|
349
|
+
[
|
|
350
|
+
'---',
|
|
351
|
+
'id: deals.health_check',
|
|
352
|
+
'label: Deal health check',
|
|
353
|
+
'description: Assess a deal.',
|
|
354
|
+
'skills: [stage_playbook]',
|
|
355
|
+
'---',
|
|
356
|
+
'You assess a deal.',
|
|
357
|
+
].join('\n'),
|
|
358
|
+
'utf8',
|
|
359
|
+
)
|
|
360
|
+
const skillDir = path.join(agentDir, 'skills', 'stage_playbook')
|
|
361
|
+
fs.mkdirSync(skillDir, { recursive: true })
|
|
362
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), 'no frontmatter at all', 'utf8')
|
|
363
|
+
|
|
364
|
+
const extension = createAgentFilesExtension()
|
|
365
|
+
expect(() =>
|
|
366
|
+
extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase)),
|
|
367
|
+
).toThrow(/malformed SKILL\.md/)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
it('fails generation when an OUTCOME.md schema is outside the supported subset (M2)', () => {
|
|
371
|
+
const fixture = makeRepoFixture()
|
|
372
|
+
const agentDir = path.join(fixture.appBase, 'agents', 'deals_health_check')
|
|
373
|
+
// `oneOf` parses as valid JSON but cannot compile to Zod — must fail at scan,
|
|
374
|
+
// not silently parse and get dropped at load.
|
|
375
|
+
fs.writeFileSync(
|
|
376
|
+
path.join(agentDir, 'OUTCOME.md'),
|
|
377
|
+
[
|
|
378
|
+
'---',
|
|
379
|
+
'kind: proposal',
|
|
380
|
+
'---',
|
|
381
|
+
'```json',
|
|
382
|
+
JSON.stringify({ type: 'object', properties: { x: { oneOf: [{ type: 'string' }] } } }),
|
|
383
|
+
'```',
|
|
384
|
+
].join('\n'),
|
|
385
|
+
'utf8',
|
|
386
|
+
)
|
|
387
|
+
const extension = createAgentFilesExtension()
|
|
388
|
+
expect(() =>
|
|
389
|
+
extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase)),
|
|
390
|
+
).toThrow(/unsupported keyword "oneOf"/)
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
it('fails generation when an OUTCOME.md schema node has no supported type (M2)', () => {
|
|
394
|
+
const fixture = makeRepoFixture()
|
|
395
|
+
const agentDir = path.join(fixture.appBase, 'agents', 'deals_health_check')
|
|
396
|
+
fs.writeFileSync(
|
|
397
|
+
path.join(agentDir, 'OUTCOME.md'),
|
|
398
|
+
[
|
|
399
|
+
'---',
|
|
400
|
+
'kind: research',
|
|
401
|
+
'---',
|
|
402
|
+
'```json',
|
|
403
|
+
JSON.stringify({ type: 'object', properties: { x: { minimum: 0 } } }),
|
|
404
|
+
'```',
|
|
405
|
+
].join('\n'),
|
|
406
|
+
'utf8',
|
|
407
|
+
)
|
|
408
|
+
const extension = createAgentFilesExtension()
|
|
409
|
+
expect(() =>
|
|
410
|
+
extension.scanModule(createScanContext('agent_examples', fixture.appBase, fixture.pkgBase)),
|
|
411
|
+
).toThrow(/missing\/unsupported "type"/)
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it('keeps committed agent artifacts when neither the orchestrator nor an agent module is enabled', () => {
|
|
415
|
+
const fixture = makeRepoFixture()
|
|
416
|
+
repoRoot = fixture.repoRoot
|
|
417
|
+
const manifestPath = path.join(
|
|
418
|
+
repoRoot,
|
|
419
|
+
'packages/enterprise/src/modules/agent_orchestrator/generated/file-agents.generated.ts',
|
|
420
|
+
)
|
|
421
|
+
const dockerFile = path.join(repoRoot, 'docker/opencode/agents/deals_health_check.md')
|
|
422
|
+
const skillFile = path.join(repoRoot, 'docker/opencode/skills/stage-playbook/SKILL.md')
|
|
423
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true })
|
|
424
|
+
fs.mkdirSync(path.dirname(dockerFile), { recursive: true })
|
|
425
|
+
fs.mkdirSync(path.dirname(skillFile), { recursive: true })
|
|
426
|
+
fs.writeFileSync(manifestPath, 'committed manifest', 'utf8')
|
|
427
|
+
fs.writeFileSync(dockerFile, 'committed agent file', 'utf8')
|
|
428
|
+
fs.writeFileSync(skillFile, 'committed skill file', 'utf8')
|
|
429
|
+
|
|
430
|
+
const extension = createAgentFilesExtension()
|
|
431
|
+
// A module with no `agents/` tree, standing in for a run with the
|
|
432
|
+
// enterprise agents flag off.
|
|
433
|
+
const otherBase = path.join(repoRoot, 'packages', 'core', 'src', 'modules', 'customers')
|
|
434
|
+
fs.mkdirSync(otherBase, { recursive: true })
|
|
435
|
+
extension.scanModule(createScanContext('customers', otherBase, otherBase))
|
|
436
|
+
extension.generateOutput()
|
|
437
|
+
|
|
438
|
+
expect(fs.readFileSync(manifestPath, 'utf8')).toBe('committed manifest')
|
|
439
|
+
expect(fs.readFileSync(dockerFile, 'utf8')).toBe('committed agent file')
|
|
440
|
+
expect(fs.readFileSync(skillFile, 'utf8')).toBe('committed skill file')
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it('still prunes stale artifacts when the orchestrator is enabled without any agent', () => {
|
|
444
|
+
const fixture = makeRepoFixture()
|
|
445
|
+
repoRoot = fixture.repoRoot
|
|
446
|
+
const dockerFile = path.join(repoRoot, 'docker/opencode/agents/deals_health_check.md')
|
|
447
|
+
const skillDir = path.join(repoRoot, 'docker/opencode/skills/stage-playbook')
|
|
448
|
+
fs.mkdirSync(path.dirname(dockerFile), { recursive: true })
|
|
449
|
+
fs.mkdirSync(skillDir, { recursive: true })
|
|
450
|
+
fs.writeFileSync(dockerFile, 'stale agent file', 'utf8')
|
|
451
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), 'stale skill file', 'utf8')
|
|
452
|
+
|
|
453
|
+
const extension = createAgentFilesExtension()
|
|
454
|
+
const orchestratorBase = path.join(
|
|
455
|
+
repoRoot,
|
|
456
|
+
'packages',
|
|
457
|
+
'enterprise',
|
|
458
|
+
'src',
|
|
459
|
+
'modules',
|
|
460
|
+
'agent_orchestrator',
|
|
461
|
+
)
|
|
462
|
+
fs.mkdirSync(orchestratorBase, { recursive: true })
|
|
463
|
+
extension.scanModule(createScanContext('agent_orchestrator', orchestratorBase, orchestratorBase))
|
|
464
|
+
extension.generateOutput()
|
|
465
|
+
|
|
466
|
+
expect(fs.existsSync(dockerFile)).toBe(false)
|
|
467
|
+
expect(fs.existsSync(skillDir)).toBe(false)
|
|
468
|
+
const manifest = fs.readFileSync(
|
|
469
|
+
path.join(
|
|
470
|
+
repoRoot,
|
|
471
|
+
'packages/enterprise/src/modules/agent_orchestrator/generated/file-agents.generated.ts',
|
|
472
|
+
),
|
|
473
|
+
'utf8',
|
|
474
|
+
)
|
|
475
|
+
expect(manifest).toContain('export const fileAgentDescriptors: FileAgentDescriptor[] = []')
|
|
476
|
+
})
|
|
477
|
+
})
|
|
@@ -182,20 +182,37 @@ describe('module-facts BC resolve guard (T2)', () => {
|
|
|
182
182
|
// facts, plus query-lifecycle, portal-page and strategy-registry hosts that
|
|
183
183
|
// exist only in the v2 surface. Same linear, one-module growth the block
|
|
184
184
|
// above describes -- the caps below are re-measured on the merged tree.
|
|
185
|
-
|
|
185
|
+
//
|
|
186
|
+
// Re-measured once more on the agent_orchestrator union (#5718). The caps
|
|
187
|
+
// above were measured on `develop`'s tree, and `develop`'s tree does not
|
|
188
|
+
// contain `agent_orchestrator`, the expanded `workflows` surface or the
|
|
189
|
+
// `web-research*` packages — so three of the four sat below the union, which
|
|
190
|
+
// is the "neither side's cap covers the union" case the note above names.
|
|
191
|
+
//
|
|
192
|
+
// The growth is linear and it is checkable: all four measures grew by very
|
|
193
|
+
// nearly the same proportion (+17.1% / +17.6% / +16.8% / +16.2%), which is
|
|
194
|
+
// what a union of two additive module sets produces. A real blow-up is
|
|
195
|
+
// multiplicative and would move one measure far out of step with the others,
|
|
196
|
+
// so uniform growth across all four is the evidence that this is repo size,
|
|
197
|
+
// not a duplicated payload. The headroom ratio is kept at the ~1.15 the
|
|
198
|
+
// previous raise used, so the guard keeps the same detection power it had.
|
|
199
|
+
expect(Buffer.byteLength(completeJson)).toBeLessThan(5_550_000)
|
|
186
200
|
// Measured on the merged tree (`yarn jest module-facts.bc-guard`):
|
|
187
|
-
// completeJson 4,
|
|
188
|
-
// directory markdown 1,
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
expect(Buffer.byteLength(completeJson) - Buffer.byteLength(legacyJson)).toBeLessThan(2_050_000)
|
|
201
|
+
// completeJson 4,823,226 · legacy delta 2,111,784 · markdown 1,820,787 ·
|
|
202
|
+
// directory markdown 1,985,779.
|
|
203
|
+
// (Previous, on develop's tree: 4,120,374 · 1,795,214 · 1,558,590 · 1,709,511.)
|
|
204
|
+
expect(Buffer.byteLength(completeJson) - Buffer.byteLength(legacyJson)).toBeLessThan(2_430_000)
|
|
192
205
|
// Markdown cap raised with the source-link contract: entities, events, ACL
|
|
193
206
|
// features, DI tokens, search entities, notifications, UMES hosts and UMES
|
|
194
207
|
// contributions all render a resolved Source cell, and contribution
|
|
195
208
|
// resolutions render as their own source-linked section — plus the additive
|
|
196
209
|
// `channel_discord` module's own render, per the growth note above.
|
|
197
|
-
expect(markdownBytes).toBeLessThan(
|
|
198
|
-
|
|
210
|
+
expect(markdownBytes).toBeLessThan(2_100_000)
|
|
211
|
+
// Raised even though it was still passing: at 2,050,000 it sat 3.2% above
|
|
212
|
+
// the union measurement, and a cap that close has stopped detecting blow-ups
|
|
213
|
+
// and started rejecting the next additive module — the failure mode the note
|
|
214
|
+
// above warns about, one merge away.
|
|
215
|
+
expect(directoryMarkdownBytes).toBeLessThan(2_290_000)
|
|
199
216
|
})
|
|
200
217
|
|
|
201
218
|
it('keeps every shipped directory section resumable and every advertised subsection anchor exact', () => {
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import type { PackageResolver, ModuleEntry } from '../../resolver'
|
|
5
|
+
import { generateModuleRegistry } from '../module-registry'
|
|
6
|
+
import { detectBackendRouteCollisions } from '../route-collisions'
|
|
7
|
+
|
|
8
|
+
let tmpDir: string
|
|
9
|
+
|
|
10
|
+
const PAGE_CONTENT = `export default function Page() { return null }
|
|
11
|
+
`
|
|
12
|
+
|
|
13
|
+
function createTmpDir(): string {
|
|
14
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'route-collisions-test-'))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function touchFile(filePath: string, content = ''): void {
|
|
18
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
19
|
+
fs.writeFileSync(filePath, content)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function scaffoldBackendPage(tmpDir: string, modId: string, location: 'pkg' | 'app', pageSubPath: string): void {
|
|
23
|
+
const base =
|
|
24
|
+
location === 'pkg'
|
|
25
|
+
? path.join(tmpDir, 'packages', 'core', 'src', 'modules', modId)
|
|
26
|
+
: path.join(tmpDir, 'app', 'src', 'modules', modId)
|
|
27
|
+
touchFile(path.join(base, 'backend', pageSubPath), PAGE_CONTENT)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createMockResolver(tmpDir: string, enabled: ModuleEntry[]): PackageResolver {
|
|
31
|
+
const outputDir = path.join(tmpDir, 'output', 'generated')
|
|
32
|
+
fs.mkdirSync(outputDir, { recursive: true })
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
isMonorepo: () => true,
|
|
36
|
+
getRootDir: () => tmpDir,
|
|
37
|
+
getAppDir: () => path.join(tmpDir, 'app'),
|
|
38
|
+
getOutputDir: () => outputDir,
|
|
39
|
+
getModulesConfigPath: () => path.join(tmpDir, 'app', 'src', 'modules.ts'),
|
|
40
|
+
discoverPackages: () => [],
|
|
41
|
+
loadEnabledModules: () => enabled,
|
|
42
|
+
getModulePaths: (entry: ModuleEntry) => ({
|
|
43
|
+
appBase: path.join(tmpDir, 'app', 'src', 'modules', entry.id),
|
|
44
|
+
pkgBase: path.join(tmpDir, 'packages', 'core', 'src', 'modules', entry.id),
|
|
45
|
+
}),
|
|
46
|
+
getModuleImportBase: (entry: ModuleEntry) => ({
|
|
47
|
+
appBase: `@/modules/${entry.id}`,
|
|
48
|
+
pkgBase: `@open-mercato/core/modules/${entry.id}`,
|
|
49
|
+
}),
|
|
50
|
+
getPackageOutputDir: () => outputDir,
|
|
51
|
+
getPackageRoot: () => path.join(tmpDir, 'packages', 'core'),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
tmpDir = createTmpDir()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('detectBackendRouteCollisions', () => {
|
|
64
|
+
it('errors when two package modules register the same backend path, naming both and the path', () => {
|
|
65
|
+
const result = detectBackendRouteCollisions([
|
|
66
|
+
{ routePath: '/backend/tasks', moduleId: 'workflows', packageSourced: true },
|
|
67
|
+
{ routePath: '/backend/tasks', moduleId: 'agent_orchestrator', packageSourced: true },
|
|
68
|
+
])
|
|
69
|
+
expect(result.errors).toHaveLength(1)
|
|
70
|
+
expect(result.errors[0]).toContain('/backend/tasks')
|
|
71
|
+
expect(result.errors[0]).toContain('"workflows"')
|
|
72
|
+
expect(result.errors[0]).toContain('"agent_orchestrator"')
|
|
73
|
+
expect(result.notes).toHaveLength(0)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('emits an informational note (no error) when an app module shadows a package module path', () => {
|
|
77
|
+
const result = detectBackendRouteCollisions([
|
|
78
|
+
{ routePath: '/backend/orders', moduleId: 'sales', packageSourced: true },
|
|
79
|
+
{ routePath: '/backend/orders', moduleId: 'my_overrides', packageSourced: false },
|
|
80
|
+
])
|
|
81
|
+
expect(result.errors).toHaveLength(0)
|
|
82
|
+
expect(result.notes).toHaveLength(1)
|
|
83
|
+
expect(result.notes[0]).toContain('/backend/orders')
|
|
84
|
+
expect(result.notes[0]).toContain('"sales"')
|
|
85
|
+
expect(result.notes[0]).toContain('"my_overrides"')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('ignores duplicate registrations from a single module (app override of its own package page)', () => {
|
|
89
|
+
const result = detectBackendRouteCollisions([
|
|
90
|
+
{ routePath: '/backend/orders', moduleId: 'sales', packageSourced: true },
|
|
91
|
+
{ routePath: '/backend/orders', moduleId: 'sales', packageSourced: false },
|
|
92
|
+
])
|
|
93
|
+
expect(result.errors).toHaveLength(0)
|
|
94
|
+
expect(result.notes).toHaveLength(0)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('stays silent when there is no collision', () => {
|
|
98
|
+
const result = detectBackendRouteCollisions([
|
|
99
|
+
{ routePath: '/backend/tasks', moduleId: 'workflows', packageSourced: true },
|
|
100
|
+
{ routePath: '/backend/agentic-tasks', moduleId: 'agent_orchestrator', packageSourced: true },
|
|
101
|
+
])
|
|
102
|
+
expect(result.errors).toHaveLength(0)
|
|
103
|
+
expect(result.notes).toHaveLength(0)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('generateModuleRegistry backend route collision guard', () => {
|
|
108
|
+
it('fails generation when two package modules register the same backend path', async () => {
|
|
109
|
+
scaffoldBackendPage(tmpDir, 'workflows_like', 'pkg', 'tasks/page.tsx')
|
|
110
|
+
scaffoldBackendPage(tmpDir, 'launcher_like', 'pkg', 'tasks/page.tsx')
|
|
111
|
+
const resolver = createMockResolver(tmpDir, [
|
|
112
|
+
{ id: 'workflows_like', from: '@open-mercato/core' },
|
|
113
|
+
{ id: 'launcher_like', from: '@open-mercato/core' },
|
|
114
|
+
])
|
|
115
|
+
|
|
116
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
117
|
+
try {
|
|
118
|
+
// Two package modules are refused by whichever guard sees the pattern
|
|
119
|
+
// first: the per-pattern assertion during generation, or the aggregate
|
|
120
|
+
// sweep that runs after it. Accept either message so the test asserts the
|
|
121
|
+
// refusal rather than which guard won the race.
|
|
122
|
+
await expect(generateModuleRegistry({ resolver, quiet: true })).rejects.toThrow(
|
|
123
|
+
/Backend route collision|Duplicate backend route pattern/,
|
|
124
|
+
)
|
|
125
|
+
const printed = consoleErrorSpy.mock.calls.map((call) => call.join(' ')).join('\n')
|
|
126
|
+
expect(printed).toContain('/backend/tasks')
|
|
127
|
+
expect(printed).toContain('"workflows_like"')
|
|
128
|
+
expect(printed).toContain('"launcher_like"')
|
|
129
|
+
} finally {
|
|
130
|
+
consoleErrorSpy.mockRestore()
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('allows an app module to shadow a package module page', async () => {
|
|
135
|
+
scaffoldBackendPage(tmpDir, 'workflows_like', 'pkg', 'tasks/page.tsx')
|
|
136
|
+
scaffoldBackendPage(tmpDir, 'app_overrides', 'app', 'tasks/page.tsx')
|
|
137
|
+
const resolver = createMockResolver(tmpDir, [
|
|
138
|
+
{ id: 'workflows_like', from: '@open-mercato/core' },
|
|
139
|
+
{ id: 'app_overrides', from: '@app' },
|
|
140
|
+
])
|
|
141
|
+
|
|
142
|
+
await expect(generateModuleRegistry({ resolver, quiet: true })).resolves.toBeDefined()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('passes when package modules register distinct backend paths', async () => {
|
|
146
|
+
scaffoldBackendPage(tmpDir, 'workflows_like', 'pkg', 'tasks/page.tsx')
|
|
147
|
+
scaffoldBackendPage(tmpDir, 'launcher_like', 'pkg', 'agentic-tasks/page.tsx')
|
|
148
|
+
const resolver = createMockResolver(tmpDir, [
|
|
149
|
+
{ id: 'workflows_like', from: '@open-mercato/core' },
|
|
150
|
+
{ id: 'launcher_like', from: '@open-mercato/core' },
|
|
151
|
+
])
|
|
152
|
+
|
|
153
|
+
await expect(generateModuleRegistry({ resolver, quiet: true })).resolves.toBeDefined()
|
|
154
|
+
})
|
|
155
|
+
})
|