@hed-hog/core 0.0.373 → 0.0.375
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/README.md +954 -873
- package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.js +4 -0
- package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
- package/dist/integration-profile/integration-profile.utils.d.ts +6 -0
- package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.utils.js +1 -1
- package/dist/integration-profile/integration-profile.utils.js.map +1 -1
- package/dist/oauth/oauth.controller.d.ts +11 -3
- package/dist/oauth/oauth.controller.d.ts.map +1 -1
- package/dist/oauth/oauth.controller.js +52 -14
- package/dist/oauth/oauth.controller.js.map +1 -1
- package/dist/oauth/oauth.module.d.ts.map +1 -1
- package/dist/oauth/oauth.module.js +4 -0
- package/dist/oauth/oauth.module.js.map +1 -1
- package/dist/oauth/oauth.service.d.ts +38 -2
- package/dist/oauth/oauth.service.d.ts.map +1 -1
- package/dist/oauth/oauth.service.js +122 -5
- package/dist/oauth/oauth.service.js.map +1 -1
- package/dist/oauth/providers/apple.provider.d.ts +34 -0
- package/dist/oauth/providers/apple.provider.d.ts.map +1 -0
- package/dist/oauth/providers/apple.provider.js +197 -0
- package/dist/oauth/providers/apple.provider.js.map +1 -0
- package/dist/oauth/providers/facebook.provider.d.ts +1 -1
- package/dist/oauth/providers/facebook.provider.d.ts.map +1 -1
- package/dist/oauth/providers/facebook.provider.js +3 -2
- package/dist/oauth/providers/facebook.provider.js.map +1 -1
- package/dist/oauth/providers/google.provider.d.ts +1 -1
- package/dist/oauth/providers/google.provider.d.ts.map +1 -1
- package/dist/oauth/providers/google.provider.js +3 -2
- package/dist/oauth/providers/google.provider.js.map +1 -1
- package/dist/oauth/providers/linkedin.provider.d.ts +25 -0
- package/dist/oauth/providers/linkedin.provider.d.ts.map +1 -0
- package/dist/oauth/providers/linkedin.provider.js +99 -0
- package/dist/oauth/providers/linkedin.provider.js.map +1 -0
- package/dist/oauth/providers/microsoft-entra-id.provider.js +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
- package/dist/oauth/providers/microsoft.provider.d.ts +1 -1
- package/dist/oauth/providers/microsoft.provider.d.ts.map +1 -1
- package/dist/oauth/providers/microsoft.provider.js +3 -2
- package/dist/oauth/providers/microsoft.provider.js.map +1 -1
- package/dist/pdf/pdf.service.d.ts.map +1 -1
- package/dist/pdf/pdf.service.js +19 -2
- package/dist/pdf/pdf.service.js.map +1 -1
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +4 -1
- package/dist/setting/setting.service.js.map +1 -1
- package/hedhog/data/integration_provider.yaml +16 -0
- package/hedhog/data/setting_group.yaml +98 -0
- package/hedhog/data/setting_subgroup.yaml +16 -0
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +2 -0
- package/hedhog/frontend/app/dashboard/dashboard.css.ejs +11 -11
- package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +42 -0
- package/hedhog/frontend/app/menu/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/users/user-sheet.tsx.ejs +8 -0
- package/hedhog/frontend/e2e/core.spec.ts.ejs +28 -0
- package/hedhog/frontend/messages/en.json +35 -0
- package/hedhog/frontend/messages/pt.json +35 -0
- package/hedhog/table/user_account.yaml +1 -0
- package/package.json +3 -3
- package/src/access-log/access-log.service.spec.ts +235 -0
- package/src/ai-instruction/ai-prompt-builder.service.spec.ts +250 -0
- package/src/auth/auth.service.spec.ts +891 -0
- package/src/auth/guards/auth.guard.spec.ts +177 -0
- package/src/challenge/challenge.service.spec.ts +334 -0
- package/src/dashboard/dashboard-core/dashboard-core.service.spec.ts +193 -0
- package/src/file/image-optimization.service.spec.ts +283 -0
- package/src/file/provider/provider.factory.spec.ts +111 -0
- package/src/install/install.service.spec.ts +139 -0
- package/src/integration/README.md +2 -0
- package/src/integration/services/event-subscriber.registry.spec.ts +143 -0
- package/src/integration/services/inbox.service.spec.ts +183 -0
- package/src/integration/services/integration-link.service.spec.ts +217 -0
- package/src/integration/services/outbox.service.spec.ts +290 -0
- package/src/integration-profile/integration-credential-crypto.service.spec.ts +289 -0
- package/src/integration-profile/integration-profile.secrets.ts +4 -0
- package/src/integration-profile/integration-profile.utils.spec.ts +332 -0
- package/src/integration-profile/integration-profile.utils.ts +14 -1
- package/src/locale/locale.service.spec.ts +78 -0
- package/src/mail/mail.service.spec.ts +257 -0
- package/src/mcp/guards/mcp-auth.guard.spec.ts +87 -0
- package/src/mcp/guards/mcp-permission.guard.spec.ts +64 -0
- package/src/mcp/mcp-api-key.service.spec.ts +125 -0
- package/src/mcp/mcp-registry.service.spec.ts +96 -0
- package/src/mcp/mcp-server.service.spec.ts +187 -0
- package/src/menu/menu.service.spec.ts +76 -0
- package/src/notification/notification.service.spec.ts +297 -0
- package/src/oauth/oauth.controller.ts +53 -10
- package/src/oauth/oauth.module.ts +4 -0
- package/src/oauth/oauth.service.spec.ts +332 -0
- package/src/oauth/oauth.service.ts +156 -1
- package/src/oauth/providers/apple.provider.ts +175 -0
- package/src/oauth/providers/facebook.provider.ts +3 -2
- package/src/oauth/providers/google.provider.ts +3 -2
- package/src/oauth/providers/linkedin.provider.ts +96 -0
- package/src/oauth/providers/microsoft-entra-id.provider.ts +1 -1
- package/src/oauth/providers/microsoft.provider.ts +3 -2
- package/src/pdf/pdf.service.ts +20 -2
- package/src/role/guards/role.guard.spec.ts +157 -0
- package/src/role/role.service.spec.ts +179 -0
- package/src/session/session.service.spec.ts +376 -0
- package/src/setting/setting.service.spec.ts +316 -0
- package/src/setting/setting.service.ts +4 -1
- package/src/task/task.service.spec.ts +47 -0
- package/src/token/token.service.spec.ts +351 -0
- package/src/user/user.service.spec.ts +256 -0
- package/src/utils/locale-context.spec.ts +22 -0
- package/src/utils/webauthn-origin.spec.ts +73 -0
- package/src/webhook-integration/integration-event-catalog.service.spec.ts +149 -0
|
@@ -992,7 +992,7 @@ function MenuTreeWorkspace({
|
|
|
992
992
|
'group relative mr-1 flex cursor-pointer items-center gap-2 rounded-xl border px-2 py-1.5 transition-all duration-150 select-none active:scale-[0.99]',
|
|
993
993
|
isDraggingThis ? 'opacity-40' : 'opacity-100',
|
|
994
994
|
selectedId === node.id
|
|
995
|
-
? 'border-primary/70 bg-linear-to-r from-primary/10 via-primary/5 to-background shadow-[0_10px_24px_-
|
|
995
|
+
? 'border-primary/70 bg-linear-to-r from-primary/10 via-primary/5 to-background shadow-[0_10px_24px_-18px_var(--primary)] ring-1 ring-primary/15'
|
|
996
996
|
: 'border-transparent hover:border-primary/30 hover:bg-muted/60 hover:shadow-sm',
|
|
997
997
|
isInside ? 'ring-2 ring-primary bg-primary/10' : ''
|
|
998
998
|
)}
|
|
@@ -147,6 +147,14 @@ const OAUTH_PROVIDER_TOGGLE_MAP: Array<{
|
|
|
147
147
|
name: 'Microsoft Entra ID',
|
|
148
148
|
},
|
|
149
149
|
},
|
|
150
|
+
{
|
|
151
|
+
settingSlug: 'oauth-apple-enabled',
|
|
152
|
+
provider: { id: 'apple', slug: 'apple-oauth', name: 'Apple' },
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
settingSlug: 'oauth-linkedin-enabled',
|
|
156
|
+
provider: { id: 'linkedin', slug: 'linkedin-oauth', name: 'LinkedIn' },
|
|
157
|
+
},
|
|
150
158
|
];
|
|
151
159
|
|
|
152
160
|
function generateRandomPassword(length = 16) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { test } from '@playwright/test';
|
|
2
|
+
import { expectPageLoads } from './support';
|
|
3
|
+
|
|
4
|
+
test.describe('Módulo core', () => {
|
|
5
|
+
const urls = [
|
|
6
|
+
'/core/menu',
|
|
7
|
+
'/core/dashboard/management',
|
|
8
|
+
'/core/roles',
|
|
9
|
+
'/core/routes',
|
|
10
|
+
'/core/configurations',
|
|
11
|
+
'/core/mail',
|
|
12
|
+
'/core/mail/template',
|
|
13
|
+
'/core/mail/log',
|
|
14
|
+
'/core/integration/profiles',
|
|
15
|
+
'/core/ai_agent',
|
|
16
|
+
'/core/integrations-webhooks',
|
|
17
|
+
'/core/setting-list',
|
|
18
|
+
'/core/access-log',
|
|
19
|
+
'/core/users',
|
|
20
|
+
'/core/mcp_chat',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
for (const url of urls) {
|
|
24
|
+
test(`${url} carrega sem crash`, async ({ page }) => {
|
|
25
|
+
await expectPageLoads(page, url);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
@@ -870,6 +870,38 @@
|
|
|
870
870
|
"loading": "Loading...",
|
|
871
871
|
"errorOccurred": "An error occurred.",
|
|
872
872
|
"alreadyProcessed": "This authentication callback has already been processed. Start the flow again."
|
|
873
|
+
},
|
|
874
|
+
"apple": {
|
|
875
|
+
"loginTitle": "Completing login...",
|
|
876
|
+
"loginDescription": "Please wait while we process your Apple authentication",
|
|
877
|
+
"loginSuccess": "Login successful!",
|
|
878
|
+
"registerTitle": "Completing registration...",
|
|
879
|
+
"registerDescription": "Please wait while we process your Apple authentication",
|
|
880
|
+
"registerSuccess": "Account created successfully!",
|
|
881
|
+
"connectTitle": "Completing account connection...",
|
|
882
|
+
"connectDescription": "Please wait while we process your Apple authentication",
|
|
883
|
+
"connectSuccess": "Account connected successfully!",
|
|
884
|
+
"errorTitle": "Unable to authenticate",
|
|
885
|
+
"buttonRetry": "Try again",
|
|
886
|
+
"loading": "Loading...",
|
|
887
|
+
"errorOccurred": "An error occurred.",
|
|
888
|
+
"alreadyProcessed": "This authentication callback has already been processed. Start the flow again."
|
|
889
|
+
},
|
|
890
|
+
"linkedin": {
|
|
891
|
+
"loginTitle": "Completing login...",
|
|
892
|
+
"loginDescription": "Please wait while we process your LinkedIn authentication",
|
|
893
|
+
"loginSuccess": "Login successful!",
|
|
894
|
+
"registerTitle": "Completing registration...",
|
|
895
|
+
"registerDescription": "Please wait while we process your LinkedIn authentication",
|
|
896
|
+
"registerSuccess": "Account created successfully!",
|
|
897
|
+
"connectTitle": "Completing account connection...",
|
|
898
|
+
"connectDescription": "Please wait while we process your LinkedIn authentication",
|
|
899
|
+
"connectSuccess": "Account connected successfully!",
|
|
900
|
+
"errorTitle": "Unable to authenticate",
|
|
901
|
+
"buttonRetry": "Try again",
|
|
902
|
+
"loading": "Loading...",
|
|
903
|
+
"errorOccurred": "An error occurred.",
|
|
904
|
+
"alreadyProcessed": "This authentication callback has already been processed. Start the flow again."
|
|
873
905
|
}
|
|
874
906
|
},
|
|
875
907
|
"ConnectedAccounts": {
|
|
@@ -2408,6 +2440,9 @@
|
|
|
2408
2440
|
"fieldOrganization": "Organization ID",
|
|
2409
2441
|
"fieldRedirectUri": "Redirect URI",
|
|
2410
2442
|
"fieldTenantId": "Tenant ID",
|
|
2443
|
+
"fieldTeamId": "Team ID",
|
|
2444
|
+
"fieldKeyId": "Key ID",
|
|
2445
|
+
"fieldPrivateKey": "Private Key (.p8)",
|
|
2411
2446
|
"fieldPublishableKey": "Publishable Key",
|
|
2412
2447
|
"fieldSecretKey": "Secret Key",
|
|
2413
2448
|
"fieldWebhookSecret": "Webhook Secret",
|
|
@@ -870,6 +870,38 @@
|
|
|
870
870
|
"loading": "Carregando...",
|
|
871
871
|
"errorOccurred": "Ocorreu um erro.",
|
|
872
872
|
"alreadyProcessed": "Esta autenticacao ja foi processada. Inicie o fluxo novamente."
|
|
873
|
+
},
|
|
874
|
+
"apple": {
|
|
875
|
+
"loginTitle": "Finalizando login...",
|
|
876
|
+
"loginDescription": "Aguarde enquanto processamos sua autenticação com a Apple",
|
|
877
|
+
"loginSuccess": "Login realizado com sucesso!",
|
|
878
|
+
"registerTitle": "Finalizando cadastro...",
|
|
879
|
+
"registerDescription": "Aguarde enquanto processamos sua autenticação com a Apple",
|
|
880
|
+
"registerSuccess": "Conta criada com sucesso!",
|
|
881
|
+
"connectTitle": "Finalizando conexão da conta...",
|
|
882
|
+
"connectDescription": "Aguarde enquanto processamos sua autenticação com a Apple",
|
|
883
|
+
"connectSuccess": "Conta conectada com sucesso!",
|
|
884
|
+
"errorTitle": "Não foi possível autenticar",
|
|
885
|
+
"buttonRetry": "Tentar novamente",
|
|
886
|
+
"loading": "Carregando...",
|
|
887
|
+
"errorOccurred": "Ocorreu um erro.",
|
|
888
|
+
"alreadyProcessed": "Esta autenticacao ja foi processada. Inicie o fluxo novamente."
|
|
889
|
+
},
|
|
890
|
+
"linkedin": {
|
|
891
|
+
"loginTitle": "Finalizando login...",
|
|
892
|
+
"loginDescription": "Aguarde enquanto processamos sua autenticação com o LinkedIn",
|
|
893
|
+
"loginSuccess": "Login realizado com sucesso!",
|
|
894
|
+
"registerTitle": "Finalizando cadastro...",
|
|
895
|
+
"registerDescription": "Aguarde enquanto processamos sua autenticação com o LinkedIn",
|
|
896
|
+
"registerSuccess": "Conta criada com sucesso!",
|
|
897
|
+
"connectTitle": "Finalizando conexão da conta...",
|
|
898
|
+
"connectDescription": "Aguarde enquanto processamos sua autenticação com o LinkedIn",
|
|
899
|
+
"connectSuccess": "Conta conectada com sucesso!",
|
|
900
|
+
"errorTitle": "Não foi possível autenticar",
|
|
901
|
+
"buttonRetry": "Tentar novamente",
|
|
902
|
+
"loading": "Carregando...",
|
|
903
|
+
"errorOccurred": "Ocorreu um erro.",
|
|
904
|
+
"alreadyProcessed": "Esta autenticacao ja foi processada. Inicie o fluxo novamente."
|
|
873
905
|
}
|
|
874
906
|
},
|
|
875
907
|
"ConnectedAccounts": {
|
|
@@ -2463,6 +2495,9 @@
|
|
|
2463
2495
|
"fieldOrganization": "ID da Organização",
|
|
2464
2496
|
"fieldRedirectUri": "URI de Redirecionamento",
|
|
2465
2497
|
"fieldTenantId": "Tenant ID",
|
|
2498
|
+
"fieldTeamId": "Team ID",
|
|
2499
|
+
"fieldKeyId": "Key ID",
|
|
2500
|
+
"fieldPrivateKey": "Chave Privada (.p8)",
|
|
2466
2501
|
"fieldPublishableKey": "Chave Publicável",
|
|
2467
2502
|
"fieldSecretKey": "Chave Secreta",
|
|
2468
2503
|
"fieldWebhookSecret": "Segredo do Webhook",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.375",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"@hed-hog/api-locale": "0.0.14",
|
|
46
46
|
"@hed-hog/api-mail": "0.0.9",
|
|
47
47
|
"@hed-hog/api-pagination": "0.0.7",
|
|
48
|
-
"@hed-hog/api-
|
|
49
|
-
"@hed-hog/api-
|
|
48
|
+
"@hed-hog/api-prisma": "0.0.6",
|
|
49
|
+
"@hed-hog/api-types": "0.0.1"
|
|
50
50
|
},
|
|
51
51
|
"exports": {
|
|
52
52
|
".": {
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
|
|
2
|
+
|
|
3
|
+
jest.mock('@hed-hog/api-prisma', () => ({
|
|
4
|
+
__esModule: true,
|
|
5
|
+
PrismaService: class {},
|
|
6
|
+
Prisma: {
|
|
7
|
+
sql: (...args: any[]) => ({ __sql: args }),
|
|
8
|
+
raw: (v: string) => ({ __raw: v }),
|
|
9
|
+
},
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
jest.mock('@hed-hog/api-pagination', () => ({
|
|
13
|
+
__esModule: true,
|
|
14
|
+
PaginationService: class {},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
jest.mock('geoip-lite', () => ({
|
|
18
|
+
__esModule: true,
|
|
19
|
+
lookup: jest.fn(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
jest.mock('ua-parser-js', () => ({
|
|
23
|
+
__esModule: true,
|
|
24
|
+
UAParser: jest.fn(),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
import * as geoip from 'geoip-lite';
|
|
28
|
+
import { UAParser } from 'ua-parser-js';
|
|
29
|
+
import { AccessLogService } from './access-log.service';
|
|
30
|
+
|
|
31
|
+
const mockLookup = geoip.lookup as unknown as jest.Mock;
|
|
32
|
+
const mockUAParser = UAParser as unknown as jest.Mock;
|
|
33
|
+
|
|
34
|
+
const makeService = () => {
|
|
35
|
+
const prisma = {
|
|
36
|
+
access_log: { create: jest.fn() },
|
|
37
|
+
$executeRaw: jest.fn(),
|
|
38
|
+
};
|
|
39
|
+
const pagination = {
|
|
40
|
+
paginate: jest.fn(),
|
|
41
|
+
};
|
|
42
|
+
const service = new AccessLogService(prisma as any, pagination as any);
|
|
43
|
+
return { service, prisma, pagination };
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
describe('AccessLogService', () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
mockUAParser.mockImplementation(() => ({
|
|
49
|
+
getResult: () => ({
|
|
50
|
+
browser: { name: 'Chrome', version: '120' },
|
|
51
|
+
os: { name: 'Windows', version: '11' },
|
|
52
|
+
}),
|
|
53
|
+
}));
|
|
54
|
+
mockLookup.mockReturnValue({ country: 'BR', city: 'Sao Paulo' });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
jest.clearAllMocks();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('recordHttp', () => {
|
|
62
|
+
it('persiste o log HTTP com os campos mapeados', () => {
|
|
63
|
+
const { service, prisma } = makeService();
|
|
64
|
+
prisma.access_log.create.mockResolvedValue({});
|
|
65
|
+
|
|
66
|
+
service.recordHttp({
|
|
67
|
+
userId: 3,
|
|
68
|
+
ip: '1.2.3.4',
|
|
69
|
+
userAgent: 'UA',
|
|
70
|
+
method: 'GET',
|
|
71
|
+
path: '/api/x',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(prisma.access_log.create).toHaveBeenCalledWith({
|
|
75
|
+
data: {
|
|
76
|
+
user_id: 3,
|
|
77
|
+
ip: '1.2.3.4',
|
|
78
|
+
user_agent: 'UA',
|
|
79
|
+
method: 'GET',
|
|
80
|
+
path: '/api/x',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('usa null para campos opcionais ausentes', () => {
|
|
86
|
+
const { service, prisma } = makeService();
|
|
87
|
+
prisma.access_log.create.mockResolvedValue({});
|
|
88
|
+
|
|
89
|
+
service.recordHttp({ method: 'POST', path: '/x' });
|
|
90
|
+
|
|
91
|
+
expect(prisma.access_log.create).toHaveBeenCalledWith({
|
|
92
|
+
data: { user_id: null, ip: null, user_agent: null, method: 'POST', path: '/x' },
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('não propaga erro de persistência (loga warn)', async () => {
|
|
97
|
+
const { service, prisma } = makeService();
|
|
98
|
+
prisma.access_log.create.mockRejectedValue(new Error('db down'));
|
|
99
|
+
const warnSpy = jest
|
|
100
|
+
.spyOn((service as any).logger, 'warn')
|
|
101
|
+
.mockImplementation(() => undefined);
|
|
102
|
+
|
|
103
|
+
expect(() => service.recordHttp({ method: 'GET', path: '/x' })).not.toThrow();
|
|
104
|
+
await new Promise((r) => process.nextTick(r));
|
|
105
|
+
|
|
106
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('recordMcp', () => {
|
|
111
|
+
it('persiste o log MCP com tool e prompt', () => {
|
|
112
|
+
const { service, prisma } = makeService();
|
|
113
|
+
prisma.access_log.create.mockResolvedValue({});
|
|
114
|
+
|
|
115
|
+
service.recordMcp({ userId: 9, mcpTool: 'search', mcpPrompt: 'q' });
|
|
116
|
+
|
|
117
|
+
expect(prisma.access_log.create).toHaveBeenCalledWith({
|
|
118
|
+
data: { user_id: 9, mcp_tool: 'search', mcp_prompt: 'q' },
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('list', () => {
|
|
124
|
+
const paginateResult = (data: any[]) => ({ data, total: data.length, page: 1 });
|
|
125
|
+
|
|
126
|
+
it('sem filtros usa where vazio e mantém orderBy/include', async () => {
|
|
127
|
+
const { service, pagination } = makeService();
|
|
128
|
+
pagination.paginate.mockResolvedValue(paginateResult([]));
|
|
129
|
+
|
|
130
|
+
await service.list({});
|
|
131
|
+
|
|
132
|
+
const [, , opts] = pagination.paginate.mock.calls[0];
|
|
133
|
+
expect(opts.where).toEqual({});
|
|
134
|
+
expect(opts.orderBy).toEqual({ created_at: 'desc' });
|
|
135
|
+
expect(opts.include.user.select).toMatchObject({ id: true, name: true });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('filtra por userId', async () => {
|
|
139
|
+
const { service, pagination } = makeService();
|
|
140
|
+
pagination.paginate.mockResolvedValue(paginateResult([]));
|
|
141
|
+
|
|
142
|
+
await service.list({ userId: 5 });
|
|
143
|
+
|
|
144
|
+
expect(pagination.paginate.mock.calls[0][2].where).toMatchObject({ user_id: 5 });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('filtra por userSearch com contains insensitive', async () => {
|
|
148
|
+
const { service, pagination } = makeService();
|
|
149
|
+
pagination.paginate.mockResolvedValue(paginateResult([]));
|
|
150
|
+
|
|
151
|
+
await service.list({ userSearch: 'ana' });
|
|
152
|
+
|
|
153
|
+
expect(pagination.paginate.mock.calls[0][2].where.user).toEqual({
|
|
154
|
+
OR: [{ name: { contains: 'ana', mode: 'insensitive' } }],
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('monta o range de datas com fim de dia em createdAtTo', async () => {
|
|
159
|
+
const { service, pagination } = makeService();
|
|
160
|
+
pagination.paginate.mockResolvedValue(paginateResult([]));
|
|
161
|
+
|
|
162
|
+
await service.list({ createdAtFrom: '2026-01-01', createdAtTo: '2026-01-31' });
|
|
163
|
+
|
|
164
|
+
const where = pagination.paginate.mock.calls[0][2].where;
|
|
165
|
+
expect(where.created_at.gte).toEqual(new Date('2026-01-01'));
|
|
166
|
+
const lte: Date = where.created_at.lte;
|
|
167
|
+
expect(lte.getHours()).toBe(23);
|
|
168
|
+
expect(lte.getMinutes()).toBe(59);
|
|
169
|
+
expect(lte.getSeconds()).toBe(59);
|
|
170
|
+
expect(lte.getMilliseconds()).toBe(999);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('type=http filtra method not null; type=mcp filtra mcp_tool not null', async () => {
|
|
174
|
+
const { service, pagination } = makeService();
|
|
175
|
+
pagination.paginate.mockResolvedValue(paginateResult([]));
|
|
176
|
+
|
|
177
|
+
await service.list({ type: 'http' });
|
|
178
|
+
expect(pagination.paginate.mock.calls[0][2].where).toMatchObject({
|
|
179
|
+
method: { not: null },
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await service.list({ type: 'mcp' });
|
|
183
|
+
expect(pagination.paginate.mock.calls[1][2].where).toMatchObject({
|
|
184
|
+
mcp_tool: { not: null },
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('enriquece cada log com dados de user-agent e geoip', async () => {
|
|
189
|
+
const { service, pagination } = makeService();
|
|
190
|
+
pagination.paginate.mockResolvedValue(
|
|
191
|
+
paginateResult([{ id: 1, user_agent: 'UA', ip: '8.8.8.8' }]),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const result = await service.list({});
|
|
195
|
+
|
|
196
|
+
expect(result.data[0]).toMatchObject({
|
|
197
|
+
browser_name: 'Chrome',
|
|
198
|
+
browser_version: '120',
|
|
199
|
+
os_name: 'Windows',
|
|
200
|
+
os_version: '11',
|
|
201
|
+
country: 'BR',
|
|
202
|
+
city: 'Sao Paulo',
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('usa null quando não há user_agent nem ip', async () => {
|
|
207
|
+
const { service, pagination } = makeService();
|
|
208
|
+
pagination.paginate.mockResolvedValue(
|
|
209
|
+
paginateResult([{ id: 2, user_agent: null, ip: null }]),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const result = await service.list({});
|
|
213
|
+
|
|
214
|
+
expect(result.data[0]).toMatchObject({
|
|
215
|
+
browser_name: null,
|
|
216
|
+
os_name: null,
|
|
217
|
+
country: null,
|
|
218
|
+
city: null,
|
|
219
|
+
});
|
|
220
|
+
expect(mockUAParser).not.toHaveBeenCalled();
|
|
221
|
+
expect(mockLookup).not.toHaveBeenCalled();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('clearOld', () => {
|
|
226
|
+
it('executa o DELETE em lote via $executeRaw', async () => {
|
|
227
|
+
const { service, prisma } = makeService();
|
|
228
|
+
prisma.$executeRaw.mockResolvedValue(1);
|
|
229
|
+
|
|
230
|
+
await service.clearOld(30, 500);
|
|
231
|
+
|
|
232
|
+
expect(prisma.$executeRaw).toHaveBeenCalledTimes(1);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
|
|
2
|
+
import { AiPromptBuilderService } from './ai-prompt-builder.service';
|
|
3
|
+
|
|
4
|
+
const makeService = () => {
|
|
5
|
+
const instructionService = {
|
|
6
|
+
getLayer: jest.fn(),
|
|
7
|
+
getAgentInstruction: jest.fn(),
|
|
8
|
+
};
|
|
9
|
+
const service = new AiPromptBuilderService(instructionService as any);
|
|
10
|
+
return { service, instructionService };
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe('AiPromptBuilderService', () => {
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('build', () => {
|
|
19
|
+
it('inclui apenas a camada system quando é a única disponível', async () => {
|
|
20
|
+
const { service, instructionService } = makeService();
|
|
21
|
+
instructionService.getLayer.mockImplementation(async (layer: string) =>
|
|
22
|
+
layer === 'system' ? 'SYS' : null,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const result = await service.build({});
|
|
26
|
+
|
|
27
|
+
expect(result.layers).toEqual(['SYS']);
|
|
28
|
+
expect(result.layerNames).toEqual(['system']);
|
|
29
|
+
expect(result.systemPrompt).toBe('SYS');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('junta system + product com o separador', async () => {
|
|
33
|
+
const { service, instructionService } = makeService();
|
|
34
|
+
instructionService.getLayer.mockImplementation(async (layer: string) => {
|
|
35
|
+
if (layer === 'system') return 'SYS';
|
|
36
|
+
if (layer === 'product') return 'PROD';
|
|
37
|
+
return null;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const result = await service.build({});
|
|
41
|
+
|
|
42
|
+
expect(result.layers).toEqual(['SYS', 'PROD']);
|
|
43
|
+
expect(result.layerNames).toEqual(['system', 'product']);
|
|
44
|
+
expect(result.systemPrompt).toBe('SYS\n\n---\n\nPROD');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('inclui a camada module quando moduleSlug é fornecido e existe', async () => {
|
|
48
|
+
const { service, instructionService } = makeService();
|
|
49
|
+
instructionService.getLayer.mockImplementation(
|
|
50
|
+
async (layer: string, slug?: string) => {
|
|
51
|
+
if (layer === 'system') return 'SYS';
|
|
52
|
+
if (layer === 'module' && slug === 'crm') return 'MOD';
|
|
53
|
+
return null;
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const result = await service.build({ moduleSlug: 'crm' });
|
|
58
|
+
|
|
59
|
+
expect(result.layerNames).toEqual(['system', 'module:crm']);
|
|
60
|
+
expect(instructionService.getLayer).toHaveBeenCalledWith('module', 'crm');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('não inclui module quando moduleSlug é fornecido mas não há conteúdo', async () => {
|
|
64
|
+
const { service, instructionService } = makeService();
|
|
65
|
+
instructionService.getLayer.mockImplementation(async (layer: string) =>
|
|
66
|
+
layer === 'system' ? 'SYS' : null,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const result = await service.build({ moduleSlug: 'crm' });
|
|
70
|
+
|
|
71
|
+
expect(result.layerNames).toEqual(['system']);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('inclui a camada agent quando agentSlug é fornecido e existe', async () => {
|
|
75
|
+
const { service, instructionService } = makeService();
|
|
76
|
+
instructionService.getLayer.mockImplementation(async (layer: string) =>
|
|
77
|
+
layer === 'system' ? 'SYS' : null,
|
|
78
|
+
);
|
|
79
|
+
instructionService.getAgentInstruction.mockResolvedValue('AGENT');
|
|
80
|
+
|
|
81
|
+
const result = await service.build({ agentSlug: 'support' });
|
|
82
|
+
|
|
83
|
+
expect(result.layerNames).toEqual(['system', 'agent:support']);
|
|
84
|
+
expect(result.layers).toEqual(['SYS', 'AGENT']);
|
|
85
|
+
expect(instructionService.getAgentInstruction).toHaveBeenCalledWith('support');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('compõe as quatro camadas na ordem de prioridade', async () => {
|
|
89
|
+
const { service, instructionService } = makeService();
|
|
90
|
+
instructionService.getLayer.mockImplementation(
|
|
91
|
+
async (layer: string, slug?: string) => {
|
|
92
|
+
if (layer === 'system') return 'L1';
|
|
93
|
+
if (layer === 'product') return 'L2';
|
|
94
|
+
if (layer === 'module' && slug === 'm') return 'L3';
|
|
95
|
+
return null;
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
instructionService.getAgentInstruction.mockResolvedValue('L4');
|
|
99
|
+
|
|
100
|
+
const result = await service.build({ moduleSlug: 'm', agentSlug: 'a' });
|
|
101
|
+
|
|
102
|
+
expect(result.layers).toEqual(['L1', 'L2', 'L3', 'L4']);
|
|
103
|
+
expect(result.systemPrompt).toBe('L1\n\n---\n\nL2\n\n---\n\nL3\n\n---\n\nL4');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('retorna prompt vazio quando nenhuma camada tem conteúdo', async () => {
|
|
107
|
+
const { service, instructionService } = makeService();
|
|
108
|
+
instructionService.getLayer.mockResolvedValue(null);
|
|
109
|
+
|
|
110
|
+
const result = await service.build({});
|
|
111
|
+
|
|
112
|
+
expect(result.layers).toEqual([]);
|
|
113
|
+
expect(result.layerNames).toEqual([]);
|
|
114
|
+
expect(result.systemPrompt).toBe('');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('buildRuntimeContext', () => {
|
|
119
|
+
it('inclui apenas o cabeçalho quando não há parâmetros', () => {
|
|
120
|
+
const { service } = makeService();
|
|
121
|
+
expect(service.buildRuntimeContext({})).toBe('[Runtime context]');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('inclui campos presentes na ordem esperada', () => {
|
|
125
|
+
const { service } = makeService();
|
|
126
|
+
const out = service.buildRuntimeContext({
|
|
127
|
+
userId: 7,
|
|
128
|
+
locale: 'pt',
|
|
129
|
+
userRoles: ['admin', 'user'],
|
|
130
|
+
currentDate: '2026-07-04',
|
|
131
|
+
});
|
|
132
|
+
expect(out).toBe(
|
|
133
|
+
[
|
|
134
|
+
'[Runtime context]',
|
|
135
|
+
'user_id: 7',
|
|
136
|
+
'locale: pt',
|
|
137
|
+
'roles: admin, user',
|
|
138
|
+
'date: 2026-07-04',
|
|
139
|
+
].join('\n'),
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('ignora userRoles vazio', () => {
|
|
144
|
+
const { service } = makeService();
|
|
145
|
+
const out = service.buildRuntimeContext({ userId: 1, userRoles: [] });
|
|
146
|
+
expect(out).toBe('[Runtime context]\nuser_id: 1');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('anexa extraContext como pares chave: valor', () => {
|
|
150
|
+
const { service } = makeService();
|
|
151
|
+
const out = service.buildRuntimeContext({
|
|
152
|
+
userId: 1,
|
|
153
|
+
extraContext: { plan: 'pro', tz: 'UTC' },
|
|
154
|
+
});
|
|
155
|
+
expect(out).toContain('plan: pro');
|
|
156
|
+
expect(out).toContain('tz: UTC');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('summarizeHistory', () => {
|
|
161
|
+
it('retorna as mensagens intactas quando o total <= keepLast', () => {
|
|
162
|
+
const { service } = makeService();
|
|
163
|
+
const messages = [
|
|
164
|
+
{ role: 'user', content: 'a' },
|
|
165
|
+
{ role: 'assistant', content: 'b' },
|
|
166
|
+
];
|
|
167
|
+
expect(service.summarizeHistory(messages, 6)).toBe(messages);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('resume as mensagens antigas e mantém as recentes verbatim', () => {
|
|
171
|
+
const { service } = makeService();
|
|
172
|
+
const messages = Array.from({ length: 10 }, (_, i) => ({
|
|
173
|
+
role: i % 2 === 0 ? 'user' : 'assistant',
|
|
174
|
+
content: `msg-${i}`,
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
const result = service.summarizeHistory(messages, 6);
|
|
178
|
+
|
|
179
|
+
// 1 mensagem de resumo + 6 recentes
|
|
180
|
+
expect(result).toHaveLength(7);
|
|
181
|
+
expect(result[0].role).toBe('user');
|
|
182
|
+
expect(result[0].content).toContain('[Earlier conversation summary: 4 messages]');
|
|
183
|
+
expect(result[0].content).toContain('First question: "msg-0"');
|
|
184
|
+
// As 6 últimas verbatim.
|
|
185
|
+
expect(result.slice(1)).toEqual(messages.slice(4));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('inclui a contagem de tool_call quando há chamadas de ferramenta', () => {
|
|
189
|
+
const { service } = makeService();
|
|
190
|
+
const messages = [
|
|
191
|
+
{ role: 'user', content: 'q' },
|
|
192
|
+
{ role: 'tool_call', content: 't1' },
|
|
193
|
+
{ role: 'tool_call', content: 't2' },
|
|
194
|
+
{ role: 'assistant', content: 'r' },
|
|
195
|
+
{ role: 'user', content: 'q2' },
|
|
196
|
+
{ role: 'assistant', content: 'r2' },
|
|
197
|
+
{ role: 'user', content: 'q3' },
|
|
198
|
+
{ role: 'assistant', content: 'r3' },
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
const result = service.summarizeHistory(messages, 2);
|
|
202
|
+
|
|
203
|
+
expect(result[0].content).toContain('Tools called: 2');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('trunca a primeira pergunta em 120 caracteres', () => {
|
|
207
|
+
const { service } = makeService();
|
|
208
|
+
const longQuestion = 'x'.repeat(200);
|
|
209
|
+
const messages = [
|
|
210
|
+
{ role: 'user', content: longQuestion },
|
|
211
|
+
...Array.from({ length: 6 }, (_, i) => ({
|
|
212
|
+
role: 'assistant',
|
|
213
|
+
content: `r-${i}`,
|
|
214
|
+
})),
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
const result = service.summarizeHistory(messages, 1);
|
|
218
|
+
|
|
219
|
+
expect(result[0].content).toContain(`"${'x'.repeat(120)}"`);
|
|
220
|
+
expect(result[0].content).not.toContain('x'.repeat(121));
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('interpolate', () => {
|
|
225
|
+
it('substitui os placeholders {{var}} pelos valores', () => {
|
|
226
|
+
const { service } = makeService();
|
|
227
|
+
expect(
|
|
228
|
+
service.interpolate('Olá {{name}}, seu plano é {{plan}}', {
|
|
229
|
+
name: 'Ana',
|
|
230
|
+
plan: 'pro',
|
|
231
|
+
}),
|
|
232
|
+
).toBe('Olá Ana, seu plano é pro');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('mantém o placeholder quando a variável não existe', () => {
|
|
236
|
+
const { service } = makeService();
|
|
237
|
+
expect(service.interpolate('valor {{missing}}', {})).toBe('valor {{missing}}');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('substitui múltiplas ocorrências da mesma variável', () => {
|
|
241
|
+
const { service } = makeService();
|
|
242
|
+
expect(service.interpolate('{{x}}-{{x}}', { x: 'a' })).toBe('a-a');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('não altera texto sem placeholders', () => {
|
|
246
|
+
const { service } = makeService();
|
|
247
|
+
expect(service.interpolate('sem variaveis', { a: '1' })).toBe('sem variaveis');
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|