@hed-hog/core 0.0.400 → 0.0.402
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/dist/access-log/access-log.interceptor.d.ts +10 -0
- package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
- package/dist/access-log/access-log.interceptor.js +15 -1
- package/dist/access-log/access-log.interceptor.js.map +1 -1
- package/dist/ai/ai.controller.d.ts +2 -0
- package/dist/ai/ai.controller.d.ts.map +1 -1
- package/dist/ai/ai.service.d.ts +86 -2
- package/dist/ai/ai.service.d.ts.map +1 -1
- package/dist/ai/ai.service.js +304 -23
- package/dist/ai/ai.service.js.map +1 -1
- package/dist/ai-instruction/ai-prompt-resolver.service.d.ts +12 -0
- package/dist/ai-instruction/ai-prompt-resolver.service.d.ts.map +1 -1
- package/dist/ai-instruction/ai-prompt-resolver.service.js +26 -0
- package/dist/ai-instruction/ai-prompt-resolver.service.js.map +1 -1
- package/dist/core.module.d.ts.map +1 -1
- package/dist/core.module.js +3 -0
- package/dist/core.module.js.map +1 -1
- package/dist/document-kit/document-kit.module.d.ts +3 -0
- package/dist/document-kit/document-kit.module.d.ts.map +1 -0
- package/dist/document-kit/document-kit.module.js +24 -0
- package/dist/document-kit/document-kit.module.js.map +1 -0
- package/dist/document-kit/document-kit.registry.d.ts +55 -0
- package/dist/document-kit/document-kit.registry.d.ts.map +1 -0
- package/dist/document-kit/document-kit.registry.js +91 -0
- package/dist/document-kit/document-kit.registry.js.map +1 -0
- package/dist/document-kit/document-kit.service.d.ts +49 -0
- package/dist/document-kit/document-kit.service.d.ts.map +1 -0
- package/dist/document-kit/document-kit.service.js +135 -0
- package/dist/document-kit/document-kit.service.js.map +1 -0
- package/dist/file/file-upload-ticket.service.d.ts +1 -1
- package/dist/file/file.controller.d.ts +3 -3
- package/dist/file/file.service.d.ts +136 -13
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +297 -27
- package/dist/file/file.service.js.map +1 -1
- package/dist/file/mcp-file-source.schema.d.ts.map +1 -1
- package/dist/file/mcp-file-source.schema.js +8 -5
- package/dist/file/mcp-file-source.schema.js.map +1 -1
- package/dist/file/provider/abstract.provider.d.ts +33 -0
- package/dist/file/provider/abstract.provider.d.ts.map +1 -1
- package/dist/file/provider/abstract.provider.js +9 -0
- package/dist/file/provider/abstract.provider.js.map +1 -1
- package/dist/file/provider/s3-multipart.util.d.ts +17 -0
- package/dist/file/provider/s3-multipart.util.d.ts.map +1 -0
- package/dist/file/provider/s3-multipart.util.js +88 -0
- package/dist/file/provider/s3-multipart.util.js.map +1 -0
- package/dist/file/provider/s3.provider.d.ts +9 -1
- package/dist/file/provider/s3.provider.d.ts.map +1 -1
- package/dist/file/provider/s3.provider.js +16 -0
- package/dist/file/provider/s3.provider.js.map +1 -1
- package/dist/file/provider/spaces.provider.d.ts +10 -1
- package/dist/file/provider/spaces.provider.d.ts.map +1 -1
- package/dist/file/provider/spaces.provider.js +17 -0
- package/dist/file/provider/spaces.provider.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/integration-profile/integration-profile.controller.d.ts +1 -0
- package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.js +26 -0
- package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
- package/dist/integration-profile/integration-profile.service.d.ts +1 -0
- package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.service.js +3 -3
- package/dist/integration-profile/integration-profile.service.js.map +1 -1
- package/dist/integration-profile/testers/ai.testers.d.ts.map +1 -1
- package/dist/integration-profile/testers/ai.testers.js +46 -8
- package/dist/integration-profile/testers/ai.testers.js.map +1 -1
- package/dist/integration-profile/testers/http-probe.d.ts.map +1 -1
- package/dist/integration-profile/testers/http-probe.js +5 -3
- package/dist/integration-profile/testers/http-probe.js.map +1 -1
- package/dist/integration-profile/testers/integration-tester.types.d.ts +5 -0
- package/dist/integration-profile/testers/integration-tester.types.d.ts.map +1 -1
- package/dist/mail/mail.service.d.ts.map +1 -1
- package/dist/mail/mail.service.js +16 -1
- package/dist/mail/mail.service.js.map +1 -1
- package/dist/mcp/mcp-server.service.d.ts.map +1 -1
- package/dist/mcp/mcp-server.service.js +10 -1
- package/dist/mcp/mcp-server.service.js.map +1 -1
- package/dist/mcp/mcp.module.d.ts.map +1 -1
- package/dist/mcp/mcp.module.js +2 -0
- package/dist/mcp/mcp.module.js.map +1 -1
- package/dist/mcp/tools/core-access-log.mcp-tools.d.ts +17 -0
- package/dist/mcp/tools/core-access-log.mcp-tools.d.ts.map +1 -0
- package/dist/mcp/tools/core-access-log.mcp-tools.js +74 -0
- package/dist/mcp/tools/core-access-log.mcp-tools.js.map +1 -0
- package/dist/mcp/tools/core-files.mcp-tools.js +4 -4
- package/dist/mcp/tools/core-files.mcp-tools.js.map +1 -1
- package/dist/meeting/meeting-provider.registry.d.ts +72 -0
- package/dist/meeting/meeting-provider.registry.d.ts.map +1 -1
- package/dist/meeting/meeting-provider.registry.js.map +1 -1
- package/dist/oauth/oauth-token-broker.service.d.ts +50 -1
- package/dist/oauth/oauth-token-broker.service.d.ts.map +1 -1
- package/dist/oauth/oauth-token-broker.service.js +105 -3
- package/dist/oauth/oauth-token-broker.service.js.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +6 -0
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.js +30 -4
- package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
- package/dist/pdf/document/charts/chart-bar.d.ts +24 -0
- package/dist/pdf/document/charts/chart-bar.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-bar.js +117 -0
- package/dist/pdf/document/charts/chart-bar.js.map +1 -0
- package/dist/pdf/document/charts/chart-donut.d.ts +16 -0
- package/dist/pdf/document/charts/chart-donut.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-donut.js +89 -0
- package/dist/pdf/document/charts/chart-donut.js.map +1 -0
- package/dist/pdf/document/charts/chart-gauge.d.ts +15 -0
- package/dist/pdf/document/charts/chart-gauge.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-gauge.js +60 -0
- package/dist/pdf/document/charts/chart-gauge.js.map +1 -0
- package/dist/pdf/document/charts/chart-line.d.ts +29 -0
- package/dist/pdf/document/charts/chart-line.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-line.js +89 -0
- package/dist/pdf/document/charts/chart-line.js.map +1 -0
- package/dist/pdf/document/charts/chart-primitives.d.ts +64 -0
- package/dist/pdf/document/charts/chart-primitives.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-primitives.js +110 -0
- package/dist/pdf/document/charts/chart-primitives.js.map +1 -0
- package/dist/pdf/document/charts/chart-radar.d.ts +13 -0
- package/dist/pdf/document/charts/chart-radar.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-radar.js +83 -0
- package/dist/pdf/document/charts/chart-radar.js.map +1 -0
- package/dist/pdf/document/charts/chart-scale.d.ts +39 -0
- package/dist/pdf/document/charts/chart-scale.d.ts.map +1 -0
- package/dist/pdf/document/charts/chart-scale.js +118 -0
- package/dist/pdf/document/charts/chart-scale.js.map +1 -0
- package/dist/pdf/document/charts/index.d.ts +8 -0
- package/dist/pdf/document/charts/index.d.ts.map +1 -0
- package/dist/pdf/document/charts/index.js +24 -0
- package/dist/pdf/document/charts/index.js.map +1 -0
- package/dist/pdf/document/index.d.ts +8 -0
- package/dist/pdf/document/index.d.ts.map +1 -0
- package/dist/pdf/document/index.js +24 -0
- package/dist/pdf/document/index.js.map +1 -0
- package/dist/pdf/document/pdf-document.chrome.d.ts +20 -0
- package/dist/pdf/document/pdf-document.chrome.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.chrome.js +36 -0
- package/dist/pdf/document/pdf-document.chrome.js.map +1 -0
- package/dist/pdf/document/pdf-document.css.d.ts +20 -0
- package/dist/pdf/document/pdf-document.css.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.css.js +327 -0
- package/dist/pdf/document/pdf-document.css.js.map +1 -0
- package/dist/pdf/document/pdf-document.renderer.d.ts +10 -0
- package/dist/pdf/document/pdf-document.renderer.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.renderer.js +226 -0
- package/dist/pdf/document/pdf-document.renderer.js.map +1 -0
- package/dist/pdf/document/pdf-document.service.d.ts +27 -0
- package/dist/pdf/document/pdf-document.service.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.service.js +81 -0
- package/dist/pdf/document/pdf-document.service.js.map +1 -0
- package/dist/pdf/document/pdf-document.theme.d.ts +41 -0
- package/dist/pdf/document/pdf-document.theme.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.theme.js +105 -0
- package/dist/pdf/document/pdf-document.theme.js.map +1 -0
- package/dist/pdf/document/pdf-document.types.d.ts +164 -0
- package/dist/pdf/document/pdf-document.types.d.ts.map +1 -0
- package/dist/pdf/document/pdf-document.types.js +18 -0
- package/dist/pdf/document/pdf-document.types.js.map +1 -0
- package/dist/pdf/pdf.module.d.ts.map +1 -1
- package/dist/pdf/pdf.module.js +3 -2
- package/dist/pdf/pdf.module.js.map +1 -1
- package/dist/pdf/pdf.service.d.ts +75 -1
- package/dist/pdf/pdf.service.d.ts.map +1 -1
- package/dist/pdf/pdf.service.js +167 -63
- package/dist/pdf/pdf.service.js.map +1 -1
- package/dist/pdf/templates/commercial-proposal.template.d.ts +96 -0
- package/dist/pdf/templates/commercial-proposal.template.d.ts.map +1 -0
- package/dist/pdf/templates/commercial-proposal.template.js +276 -0
- package/dist/pdf/templates/commercial-proposal.template.js.map +1 -0
- package/dist/setting/favicon.controller.d.ts +26 -0
- package/dist/setting/favicon.controller.d.ts.map +1 -0
- package/dist/setting/favicon.controller.js +130 -0
- package/dist/setting/favicon.controller.js.map +1 -0
- package/dist/setting/setting.module.d.ts.map +1 -1
- package/dist/setting/setting.module.js +2 -1
- package/dist/setting/setting.module.js.map +1 -1
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +3 -0
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/task/task.service.d.ts +6 -0
- package/dist/task/task.service.d.ts.map +1 -1
- package/dist/task/task.service.js +44 -13
- package/dist/task/task.service.js.map +1 -1
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts +35 -2
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts.map +1 -1
- package/dist/whatsapp-sender/whatsapp-sender.service.js +107 -22
- package/dist/whatsapp-sender/whatsapp-sender.service.js.map +1 -1
- package/hedhog/data/integration_type.yaml +6 -0
- package/hedhog/data/route.yaml +8 -0
- package/hedhog/data/setting_group.yaml +52 -0
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +85 -2
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +576 -491
- package/hedhog/frontend/app/users/simulate-access-dialog.test.tsx.ejs +29 -50
- package/hedhog/frontend/app/users/simulate-access-dialog.tsx.ejs +146 -50
- package/hedhog/frontend/messages/en.json +81 -2
- package/hedhog/frontend/messages/pt.json +81 -2
- package/hedhog/table/file.yaml +3 -1
- package/package.json +5 -5
- package/src/access-log/access-log.interceptor.spec.ts +50 -0
- package/src/access-log/access-log.interceptor.ts +18 -0
- package/src/ai/ai-attachment.fragments.spec.ts +56 -3
- package/src/ai/ai.service.responses.spec.ts +429 -0
- package/src/ai/ai.service.ts +409 -27
- package/src/ai-instruction/ai-prompt-resolver.service.spec.ts +44 -0
- package/src/ai-instruction/ai-prompt-resolver.service.ts +29 -0
- package/src/core.module.ts +3 -0
- package/src/document-kit/document-kit.module.ts +12 -0
- package/src/document-kit/document-kit.registry.ts +125 -0
- package/src/document-kit/document-kit.service.spec.ts +156 -0
- package/src/document-kit/document-kit.service.ts +173 -0
- package/src/file/file-direct-upload.spec.ts +344 -0
- package/src/file/file.service.spec.ts +168 -1
- package/src/file/file.service.ts +435 -36
- package/src/file/mcp-file-source.schema.ts +8 -5
- package/src/file/provider/abstract.provider.ts +57 -0
- package/src/file/provider/s3-multipart.spec.ts +123 -0
- package/src/file/provider/s3-multipart.util.ts +126 -0
- package/src/file/provider/s3.provider.ts +65 -1
- package/src/file/provider/spaces.provider.ts +67 -1
- package/src/index.ts +5 -0
- package/src/integration-profile/integration-profile.secrets.ts +26 -0
- package/src/integration-profile/integration-profile.service.ts +1 -0
- package/src/integration-profile/testers/ai.testers.ts +70 -5
- package/src/integration-profile/testers/http-probe.ts +5 -3
- package/src/integration-profile/testers/integration-tester.types.ts +5 -0
- package/src/integration-profile/testers/oauth.testers.spec.ts +13 -0
- package/src/integration-profile/testers/remaining.testers.spec.ts +65 -1
- package/src/integration-profile/testers/tester-coverage.spec.ts +12 -0
- package/src/mail/mail.service.spec.ts +25 -0
- package/src/mail/mail.service.ts +18 -1
- package/src/mcp/mcp-server.service.spec.ts +16 -0
- package/src/mcp/mcp-server.service.ts +10 -1
- package/src/mcp/mcp.module.ts +2 -0
- package/src/mcp/tools/core-access-log.mcp-tools.ts +66 -0
- package/src/mcp/tools/core-files.mcp-tools.ts +4 -4
- package/src/meeting/meeting-provider.registry.ts +80 -0
- package/src/oauth/oauth-token-broker.service.spec.ts +108 -0
- package/src/oauth/oauth-token-broker.service.ts +129 -2
- package/src/oauth/providers/microsoft-entra-id.provider.spec.ts +133 -0
- package/src/oauth/providers/microsoft-entra-id.provider.ts +29 -2
- package/src/pdf/document/charts/chart-bar.ts +217 -0
- package/src/pdf/document/charts/chart-donut.ts +163 -0
- package/src/pdf/document/charts/chart-gauge.ts +94 -0
- package/src/pdf/document/charts/chart-line.ts +159 -0
- package/src/pdf/document/charts/chart-primitives.ts +188 -0
- package/src/pdf/document/charts/chart-radar.ts +136 -0
- package/src/pdf/document/charts/chart-scale.spec.ts +93 -0
- package/src/pdf/document/charts/chart-scale.ts +141 -0
- package/src/pdf/document/charts/charts.spec.ts +157 -0
- package/src/pdf/document/charts/index.ts +7 -0
- package/src/pdf/document/index.ts +7 -0
- package/src/pdf/document/pdf-document.chrome.ts +38 -0
- package/src/pdf/document/pdf-document.css.ts +326 -0
- package/src/pdf/document/pdf-document.renderer.spec.ts +172 -0
- package/src/pdf/document/pdf-document.renderer.ts +317 -0
- package/src/pdf/document/pdf-document.service.ts +77 -0
- package/src/pdf/document/pdf-document.theme.ts +146 -0
- package/src/pdf/document/pdf-document.types.ts +149 -0
- package/src/pdf/pdf.module.ts +3 -2
- package/src/pdf/pdf.service.spec.ts +131 -0
- package/src/pdf/pdf.service.ts +256 -82
- package/src/pdf/templates/commercial-proposal.template.spec.ts +177 -0
- package/src/pdf/templates/commercial-proposal.template.ts +400 -0
- package/src/setting/favicon.controller.spec.ts +92 -0
- package/src/setting/favicon.controller.ts +135 -0
- package/src/setting/setting.module.ts +2 -1
- package/src/setting/setting.service.ts +3 -0
- package/src/task/task.service.spec.ts +77 -1
- package/src/task/task.service.ts +58 -14
- package/src/whatsapp-sender/whatsapp-sender.service.spec.ts +263 -0
- package/src/whatsapp-sender/whatsapp-sender.service.ts +154 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.402",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
"ua-parser-js": "^2.0.10",
|
|
49
49
|
"uuid": "^11.1.0",
|
|
50
50
|
"@hed-hog/api-prisma": "0.0.7",
|
|
51
|
-
"@hed-hog/api-locale": "0.0.15",
|
|
52
|
-
"@hed-hog/api-mail": "0.0.11",
|
|
53
51
|
"@hed-hog/vault-crypto": "0.0.1",
|
|
52
|
+
"@hed-hog/api-mail": "0.0.11",
|
|
53
|
+
"@hed-hog/api-locale": "0.0.15",
|
|
54
|
+
"@hed-hog/api-types": "0.0.2",
|
|
54
55
|
"@hed-hog/api": "0.0.9",
|
|
55
|
-
"@hed-hog/api-pagination": "0.0.8"
|
|
56
|
-
"@hed-hog/api-types": "0.0.2"
|
|
56
|
+
"@hed-hog/api-pagination": "0.0.8"
|
|
57
57
|
},
|
|
58
58
|
"exports": {
|
|
59
59
|
".": {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { SetMetadata } from '@nestjs/common';
|
|
3
|
+
import { lastValueFrom, of } from 'rxjs';
|
|
4
|
+
import { AccessLogInterceptor, SKIP_ACCESS_LOG_KEY } from './access-log.interceptor';
|
|
5
|
+
|
|
6
|
+
class PlainController {
|
|
7
|
+
handler() {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@SetMetadata(SKIP_ACCESS_LOG_KEY, true)
|
|
11
|
+
class AnonymousController {
|
|
12
|
+
handler() {}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function contextFor(controller: any) {
|
|
16
|
+
const request = {
|
|
17
|
+
url: '/x',
|
|
18
|
+
ip: '203.0.113.7',
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { 'user-agent': 'UA' },
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
getType: () => 'http',
|
|
24
|
+
switchToHttp: () => ({ getRequest: () => request }),
|
|
25
|
+
getHandler: () => controller.prototype.handler,
|
|
26
|
+
getClass: () => controller,
|
|
27
|
+
} as any;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('AccessLogInterceptor', () => {
|
|
31
|
+
it('records ordinary requests', async () => {
|
|
32
|
+
const recordHttp = jest.fn();
|
|
33
|
+
const interceptor = new AccessLogInterceptor({ recordHttp } as any);
|
|
34
|
+
await lastValueFrom(
|
|
35
|
+
interceptor.intercept(contextFor(PlainController), { handle: () => of(1) }),
|
|
36
|
+
);
|
|
37
|
+
expect(recordHttp).toHaveBeenCalledWith(
|
|
38
|
+
expect.objectContaining({ ip: '203.0.113.7', userAgent: 'UA' }),
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('skips routes marked with skipAccessLog (anonymous whistleblower channel)', async () => {
|
|
43
|
+
const recordHttp = jest.fn();
|
|
44
|
+
const interceptor = new AccessLogInterceptor({ recordHttp } as any);
|
|
45
|
+
await lastValueFrom(
|
|
46
|
+
interceptor.intercept(contextFor(AnonymousController), { handle: () => of(1) }),
|
|
47
|
+
);
|
|
48
|
+
expect(recordHttp).not.toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -7,6 +7,17 @@ import {
|
|
|
7
7
|
import { Observable, tap } from 'rxjs';
|
|
8
8
|
import { AccessLogService } from './access-log.service';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Metadado (handler ou classe) que tira a rota do access log.
|
|
12
|
+
*
|
|
13
|
+
* Existe para superficies cujo valor depende de NAO saber quem chamou — o canal
|
|
14
|
+
* de denuncias da library integrity: uma linha com IP, user agent e horario de
|
|
15
|
+
* `POST /integrity/public/reports` identificaria o denunciante. Quem usa aplica
|
|
16
|
+
* `SetMetadata('skipAccessLog', true)` direto, sem importar daqui, para uma
|
|
17
|
+
* library nao exigir esta versao do core (um core antigo apenas continua logando).
|
|
18
|
+
*/
|
|
19
|
+
export const SKIP_ACCESS_LOG_KEY = 'skipAccessLog';
|
|
20
|
+
|
|
10
21
|
@Injectable()
|
|
11
22
|
export class AccessLogInterceptor implements NestInterceptor {
|
|
12
23
|
constructor(private readonly accessLogService: AccessLogService) {}
|
|
@@ -23,6 +34,13 @@ export class AccessLogInterceptor implements NestInterceptor {
|
|
|
23
34
|
return next.handle();
|
|
24
35
|
}
|
|
25
36
|
|
|
37
|
+
if (
|
|
38
|
+
Reflect.getMetadata(SKIP_ACCESS_LOG_KEY, context.getHandler()) === true ||
|
|
39
|
+
Reflect.getMetadata(SKIP_ACCESS_LOG_KEY, context.getClass()) === true
|
|
40
|
+
) {
|
|
41
|
+
return next.handle();
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
return next.handle().pipe(
|
|
27
45
|
tap(() => {
|
|
28
46
|
const userId: number | undefined = request.auth?.sub;
|
|
@@ -179,6 +179,10 @@ describe('AiService — anexos no fio', () => {
|
|
|
179
179
|
return mockedAxios.post.mock.calls.at(-1)?.[1] as Record<string, any>;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
function lastUrl(): string {
|
|
183
|
+
return mockedAxios.post.mock.calls.at(-1)?.[0] as string;
|
|
184
|
+
}
|
|
185
|
+
|
|
182
186
|
describe('Gemini', () => {
|
|
183
187
|
it('embrulha anexo de texto exatamente como o código original', async () => {
|
|
184
188
|
useAttachment(TEXT_ATTACHMENT);
|
|
@@ -248,23 +252,72 @@ describe('AiService — anexos no fio', () => {
|
|
|
248
252
|
);
|
|
249
253
|
});
|
|
250
254
|
|
|
251
|
-
|
|
255
|
+
// Num modelo com visão o PDF deixou de ser achatado em texto: ele vai
|
|
256
|
+
// inteiro para a Responses API, que renderiza as páginas. Era isso que
|
|
257
|
+
// faltava para uma nota fiscal escaneada — sem camada de texto, o caminho
|
|
258
|
+
// antigo mandava ao modelo a frase dizendo que o PDF é ilegível.
|
|
259
|
+
it('PDF vai como documento para a Responses API, não achatado em texto', async () => {
|
|
252
260
|
const pdfText = 'Cláusula 1ª — objeto do contrato.';
|
|
253
261
|
useAttachment(PDF_ATTACHMENT);
|
|
254
262
|
jest.spyOn(service as any, 'extractPdfText').mockResolvedValue(pdfText);
|
|
263
|
+
jest.spyOn(service as any, 'countPdfPages').mockResolvedValue(1);
|
|
255
264
|
|
|
256
265
|
await service.chat({ message: 'Resuma.', provider: 'openai' } as any);
|
|
257
266
|
|
|
267
|
+
expect(lastUrl()).toBe('https://api.openai.com/v1/responses');
|
|
268
|
+
expect(lastBody().messages).toBeUndefined();
|
|
269
|
+
|
|
270
|
+
const [file, text] = lastBody().input[0].content;
|
|
271
|
+
expect(file.type).toBe('input_file');
|
|
272
|
+
expect(file.filename).toBe(PDF_ATTACHMENT.filename);
|
|
273
|
+
expect(file.file_data).toBe(
|
|
274
|
+
`data:application/pdf;base64,${PDF_ATTACHMENT.buffer.toString('base64')}`,
|
|
275
|
+
);
|
|
276
|
+
expect(text).toEqual({ type: 'input_text', text: 'Resuma.' });
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('PDF ilegível também vai como documento, sem marcador nenhum', async () => {
|
|
280
|
+
useAttachment(PDF_ATTACHMENT);
|
|
281
|
+
jest.spyOn(service as any, 'extractPdfText').mockResolvedValue('');
|
|
282
|
+
jest.spyOn(service as any, 'countPdfPages').mockResolvedValue(1);
|
|
283
|
+
|
|
284
|
+
await service.chat({ message: 'Resuma.', provider: 'openai' } as any);
|
|
285
|
+
|
|
286
|
+
expect(lastUrl()).toBe('https://api.openai.com/v1/responses');
|
|
287
|
+
expect(JSON.stringify(lastBody())).not.toContain(
|
|
288
|
+
legacyUnreadablePdf(PDF_ATTACHMENT),
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// O marcador continua valendo onde a Responses API não ajuda: modelo sem
|
|
293
|
+
// visão só receberia a camada de texto de volta, o mesmo nada num endpoint
|
|
294
|
+
// diferente. Então esses dois casos guardam a fidelidade byte a byte.
|
|
295
|
+
it('em modelo sem visão, PDF com texto continua no bloco de arquivo legado', async () => {
|
|
296
|
+
const pdfText = 'Cláusula 1ª — objeto do contrato.';
|
|
297
|
+
useAttachment(PDF_ATTACHMENT);
|
|
298
|
+
jest.spyOn(service as any, 'extractPdfText').mockResolvedValue(pdfText);
|
|
299
|
+
|
|
300
|
+
await service.chat({
|
|
301
|
+
message: 'Resuma.',
|
|
302
|
+
provider: 'openai',
|
|
303
|
+
model: 'gpt-3.5-turbo',
|
|
304
|
+
} as any);
|
|
305
|
+
|
|
306
|
+
expect(lastUrl()).toBe('https://api.openai.com/v1/chat/completions');
|
|
258
307
|
expect(lastBody().messages[0].content).toBe(
|
|
259
308
|
legacyOpenAiReturn('Resuma.', PDF_ATTACHMENT, pdfText),
|
|
260
309
|
);
|
|
261
310
|
});
|
|
262
311
|
|
|
263
|
-
it('PDF ilegível cai no marcador de PDF
|
|
312
|
+
it('em modelo sem visão, PDF ilegível cai no marcador de PDF', async () => {
|
|
264
313
|
useAttachment(PDF_ATTACHMENT);
|
|
265
314
|
jest.spyOn(service as any, 'extractPdfText').mockResolvedValue('');
|
|
266
315
|
|
|
267
|
-
await service.chat({
|
|
316
|
+
await service.chat({
|
|
317
|
+
message: 'Resuma.',
|
|
318
|
+
provider: 'openai',
|
|
319
|
+
model: 'gpt-3.5-turbo',
|
|
320
|
+
} as any);
|
|
268
321
|
|
|
269
322
|
expect(lastBody().messages[0].content).toBe(
|
|
270
323
|
legacyOpenAiReturn('Resuma.', PDF_ATTACHMENT, legacyUnreadablePdf(PDF_ATTACHMENT)),
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { AiService } from './ai.service';
|
|
3
|
+
|
|
4
|
+
jest.mock('axios');
|
|
5
|
+
|
|
6
|
+
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* PDF como documento.
|
|
10
|
+
*
|
|
11
|
+
* O caminho antigo achatava todo PDF em texto: sem camada de texto, o modelo
|
|
12
|
+
* recebia o marcador `[PDF attachment with unreadable text: ...]` e não tinha
|
|
13
|
+
* como extrair coisa alguma — era esse o motivo de uma nota fiscal escaneada
|
|
14
|
+
* voltar com todos os campos vazios. Estes testes fixam o caminho que entrega
|
|
15
|
+
* o documento ao modelo, e os limites em que ele não se aplica.
|
|
16
|
+
*/
|
|
17
|
+
describe('AiService — PDF como documento (Responses API)', () => {
|
|
18
|
+
let service: AiService;
|
|
19
|
+
|
|
20
|
+
const aiProfile = {
|
|
21
|
+
apiKey: 'sk-test',
|
|
22
|
+
model: undefined as string | undefined,
|
|
23
|
+
timeoutMs: undefined as number | undefined,
|
|
24
|
+
baseUrl: undefined as string | undefined,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const PDF = {
|
|
28
|
+
filename: 'nf.pdf',
|
|
29
|
+
mimeType: 'application/pdf',
|
|
30
|
+
buffer: Buffer.from('%PDF-1.7 conteudo'),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const IMAGE = {
|
|
34
|
+
filename: 'nf.png',
|
|
35
|
+
mimeType: 'image/png',
|
|
36
|
+
buffer: Buffer.from('png-bytes'),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
const aiExecution = {
|
|
41
|
+
start: jest.fn().mockResolvedValue(1),
|
|
42
|
+
recordStep: jest.fn().mockResolvedValue(undefined),
|
|
43
|
+
finish: jest.fn().mockResolvedValue(undefined),
|
|
44
|
+
} as any;
|
|
45
|
+
|
|
46
|
+
service = new AiService(
|
|
47
|
+
{} as any,
|
|
48
|
+
{ getSettingValues: jest.fn().mockResolvedValue({}) } as any,
|
|
49
|
+
{} as any,
|
|
50
|
+
aiExecution,
|
|
51
|
+
{ resolveMany: jest.fn() } as any,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
jest.spyOn(service as any, 'getAiProfile').mockResolvedValue(aiProfile);
|
|
55
|
+
jest.spyOn(service as any, 'getDefaultProvider').mockResolvedValue('openai');
|
|
56
|
+
jest.spyOn(service as any, 'resolveAttachment').mockResolvedValue(PDF);
|
|
57
|
+
jest.spyOn(service as any, 'countPdfPages').mockResolvedValue(3);
|
|
58
|
+
jest.spyOn(service as any, 'extractPdfText').mockResolvedValue('');
|
|
59
|
+
|
|
60
|
+
mockedAxios.post.mockResolvedValue({
|
|
61
|
+
data: {
|
|
62
|
+
status: 'completed',
|
|
63
|
+
output: [
|
|
64
|
+
{ type: 'message', content: [{ type: 'output_text', text: 'ok' }] },
|
|
65
|
+
],
|
|
66
|
+
choices: [{ message: { content: 'ok' } }],
|
|
67
|
+
},
|
|
68
|
+
} as any);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
jest.restoreAllMocks();
|
|
73
|
+
mockedAxios.post.mockReset();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const lastBody = () =>
|
|
77
|
+
mockedAxios.post.mock.calls.at(-1)?.[1] as Record<string, any>;
|
|
78
|
+
const lastUrl = () => mockedAxios.post.mock.calls.at(-1)?.[0] as string;
|
|
79
|
+
const lastConfig = () =>
|
|
80
|
+
mockedAxios.post.mock.calls.at(-1)?.[2] as Record<string, any>;
|
|
81
|
+
|
|
82
|
+
it('manda o PDF em input_file, com a instrução depois do arquivo', async () => {
|
|
83
|
+
await service.chat({ message: 'Extraia.', provider: 'openai' } as any);
|
|
84
|
+
|
|
85
|
+
expect(lastUrl()).toBe('https://api.openai.com/v1/responses');
|
|
86
|
+
|
|
87
|
+
const [file, text] = lastBody().input[0].content;
|
|
88
|
+
expect(file).toEqual({
|
|
89
|
+
type: 'input_file',
|
|
90
|
+
filename: 'nf.pdf',
|
|
91
|
+
file_data: `data:application/pdf;base64,${PDF.buffer.toString('base64')}`,
|
|
92
|
+
});
|
|
93
|
+
expect(text).toEqual({ type: 'input_text', text: 'Extraia.' });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('sem parâmetros, o corpo é exatamente { input, model }', async () => {
|
|
97
|
+
await service.chat({ message: 'Extraia.', provider: 'openai' } as any);
|
|
98
|
+
|
|
99
|
+
expect(Object.keys(lastBody()).sort()).toEqual(['input', 'model']);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('o system prompt vira instructions de topo, não uma mensagem', async () => {
|
|
103
|
+
await service.chat({
|
|
104
|
+
message: 'Extraia.',
|
|
105
|
+
provider: 'openai',
|
|
106
|
+
systemPrompt: 'Você é um extrator.',
|
|
107
|
+
} as any);
|
|
108
|
+
|
|
109
|
+
expect(lastBody().instructions).toBe('Você é um extrator.');
|
|
110
|
+
expect(lastBody().input.some((item: any) => item.role === 'system')).toBe(
|
|
111
|
+
false,
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('renomeia max_tokens e response_format para os nomes da Responses API', async () => {
|
|
116
|
+
await service.chat({
|
|
117
|
+
message: 'Extraia.',
|
|
118
|
+
provider: 'openai',
|
|
119
|
+
max_tokens: 500,
|
|
120
|
+
response_format: 'json_object',
|
|
121
|
+
} as any);
|
|
122
|
+
|
|
123
|
+
expect(lastBody().max_output_tokens).toBe(500);
|
|
124
|
+
expect(lastBody()).not.toHaveProperty('max_tokens');
|
|
125
|
+
expect(lastBody().text).toEqual({ format: { type: 'json_object' } });
|
|
126
|
+
expect(lastBody()).not.toHaveProperty('response_format');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('não trunca nem trava num corpo de megabytes', async () => {
|
|
130
|
+
await service.chat({ message: 'Extraia.', provider: 'openai' } as any);
|
|
131
|
+
|
|
132
|
+
expect(lastConfig().maxBodyLength).toBe(Infinity);
|
|
133
|
+
expect(lastConfig().maxContentLength).toBe(Infinity);
|
|
134
|
+
expect(lastConfig().timeout).toBe(120_000);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// `output_text` é conveniência do SDK e costuma faltar no JSON cru; ler só
|
|
138
|
+
// ele é a forma mais provável de entregar isto quebrado.
|
|
139
|
+
it('lê o texto do array output, ignorando o reasoning sem content', async () => {
|
|
140
|
+
mockedAxios.post.mockResolvedValue({
|
|
141
|
+
data: {
|
|
142
|
+
output: [
|
|
143
|
+
{ type: 'reasoning', summary: [] },
|
|
144
|
+
{
|
|
145
|
+
type: 'message',
|
|
146
|
+
content: [{ type: 'output_text', text: '{"valor":9500}' }],
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
} as any);
|
|
151
|
+
|
|
152
|
+
const result = await service.chat({
|
|
153
|
+
message: 'Extraia.',
|
|
154
|
+
provider: 'openai',
|
|
155
|
+
} as any);
|
|
156
|
+
|
|
157
|
+
expect(result.content).toBe('{"valor":9500}');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('usa output_text quando ele vem preenchido', async () => {
|
|
161
|
+
mockedAxios.post.mockResolvedValue({
|
|
162
|
+
data: { output_text: '{"valor":1}', output: [] },
|
|
163
|
+
} as any);
|
|
164
|
+
|
|
165
|
+
const result = await service.chat({
|
|
166
|
+
message: 'Extraia.',
|
|
167
|
+
provider: 'openai',
|
|
168
|
+
} as any);
|
|
169
|
+
|
|
170
|
+
expect(result.content).toBe('{"valor":1}');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('mapeia o usage da Responses API, inclusive os tokens em cache', async () => {
|
|
174
|
+
mockedAxios.post.mockResolvedValue({
|
|
175
|
+
data: {
|
|
176
|
+
output: [
|
|
177
|
+
{ type: 'message', content: [{ type: 'output_text', text: 'ok' }] },
|
|
178
|
+
],
|
|
179
|
+
usage: {
|
|
180
|
+
input_tokens: 1200,
|
|
181
|
+
output_tokens: 80,
|
|
182
|
+
total_tokens: 1280,
|
|
183
|
+
input_tokens_details: { cached_tokens: 400 },
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
} as any);
|
|
187
|
+
|
|
188
|
+
const recordStep = jest.fn().mockResolvedValue(undefined);
|
|
189
|
+
(service as any).aiExecution.recordStep = recordStep;
|
|
190
|
+
|
|
191
|
+
await service.chat({ message: 'Extraia.', provider: 'openai' } as any);
|
|
192
|
+
|
|
193
|
+
expect(recordStep.mock.calls[0][0].usage).toEqual({
|
|
194
|
+
input: 1200,
|
|
195
|
+
output: 80,
|
|
196
|
+
total: 1280,
|
|
197
|
+
cached: 400,
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('reporta que o documento chegou ao modelo', async () => {
|
|
202
|
+
const result = await service.chat({
|
|
203
|
+
message: 'Extraia.',
|
|
204
|
+
provider: 'openai',
|
|
205
|
+
} as any);
|
|
206
|
+
|
|
207
|
+
expect(result.attachmentDelivery).toBe('pdf-document');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('degrada para chat/completions quando a Responses API devolve 400', async () => {
|
|
211
|
+
mockedAxios.post
|
|
212
|
+
.mockRejectedValueOnce({
|
|
213
|
+
response: { status: 400, data: { error: { message: 'unsupported' } } },
|
|
214
|
+
})
|
|
215
|
+
.mockResolvedValue({
|
|
216
|
+
data: { choices: [{ message: { content: 'ok' } }] },
|
|
217
|
+
} as any);
|
|
218
|
+
|
|
219
|
+
const result = await service.chat({
|
|
220
|
+
message: 'Extraia.',
|
|
221
|
+
provider: 'openai',
|
|
222
|
+
} as any);
|
|
223
|
+
|
|
224
|
+
expect(mockedAxios.post).toHaveBeenCalledTimes(2);
|
|
225
|
+
expect(lastUrl()).toBe('https://api.openai.com/v1/chat/completions');
|
|
226
|
+
expect(result.content).toBe('ok');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Degradar aqui só reproduziria o marcador de PDF ilegível e devolveria
|
|
230
|
+
// "não achei nada" — escondendo uma credencial inválida.
|
|
231
|
+
it('não mascara um 401 com uma segunda chamada mais fraca', async () => {
|
|
232
|
+
mockedAxios.post.mockRejectedValue({
|
|
233
|
+
response: { status: 401, data: { error: { message: 'bad key' } } },
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
await expect(
|
|
237
|
+
service.chat({ message: 'Extraia.', provider: 'openai' } as any),
|
|
238
|
+
).rejects.toMatchObject({ response: { status: 401 } });
|
|
239
|
+
|
|
240
|
+
expect(mockedAxios.post).toHaveBeenCalledTimes(1);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('PDF acima do teto de páginas volta para o chat/completions', async () => {
|
|
244
|
+
jest.spyOn(service as any, 'countPdfPages').mockResolvedValue(50);
|
|
245
|
+
|
|
246
|
+
await service.chat({ message: 'Extraia.', provider: 'openai' } as any);
|
|
247
|
+
|
|
248
|
+
expect(lastUrl()).toBe('https://api.openai.com/v1/chat/completions');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('imagem continua pelo bloco de visão do chat/completions', async () => {
|
|
252
|
+
jest.spyOn(service as any, 'resolveAttachment').mockResolvedValue(IMAGE);
|
|
253
|
+
|
|
254
|
+
const result = await service.chat({
|
|
255
|
+
message: 'Extraia.',
|
|
256
|
+
provider: 'openai',
|
|
257
|
+
} as any);
|
|
258
|
+
|
|
259
|
+
expect(lastUrl()).toBe('https://api.openai.com/v1/chat/completions');
|
|
260
|
+
expect(lastBody().messages[0].content[1].type).toBe('image_url');
|
|
261
|
+
expect(result.attachmentDelivery).toBe('image');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// O guarda dos ~25 pontos do repo que chamam IA sem anexo nenhum.
|
|
265
|
+
it('sem anexo, nada muda: chat/completions com o corpo de sempre', async () => {
|
|
266
|
+
jest.spyOn(service as any, 'resolveAttachment').mockResolvedValue(null);
|
|
267
|
+
|
|
268
|
+
const result = await service.chat({
|
|
269
|
+
message: 'oi',
|
|
270
|
+
provider: 'openai',
|
|
271
|
+
} as any);
|
|
272
|
+
|
|
273
|
+
expect(lastUrl()).toBe('https://api.openai.com/v1/chat/completions');
|
|
274
|
+
expect(Object.keys(lastBody()).sort()).toEqual(['messages', 'model']);
|
|
275
|
+
expect(result.attachmentDelivery).toBe('none');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('o DeepSeek não tem /v1/responses: PDF segue no chat/completions', async () => {
|
|
279
|
+
await service.chat({ message: 'Extraia.', provider: 'deepseek' } as any);
|
|
280
|
+
|
|
281
|
+
expect(lastUrl()).toContain('/chat/completions');
|
|
282
|
+
expect(lastUrl()).not.toContain('api.openai.com');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('uma base URL de gateway não recebe /v1/responses', async () => {
|
|
286
|
+
jest
|
|
287
|
+
.spyOn(service as any, 'getAiProfile')
|
|
288
|
+
.mockResolvedValue({ ...aiProfile, baseUrl: 'https://gateway.interno/v1' });
|
|
289
|
+
|
|
290
|
+
await service.chat({ message: 'Extraia.', provider: 'openai' } as any);
|
|
291
|
+
|
|
292
|
+
expect(lastUrl()).toBe('https://gateway.interno/v1/chat/completions');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('modelo sem visão não usa a Responses API', async () => {
|
|
296
|
+
await service.chat({
|
|
297
|
+
message: 'Extraia.',
|
|
298
|
+
provider: 'openai',
|
|
299
|
+
model: 'gpt-3.5-turbo',
|
|
300
|
+
} as any);
|
|
301
|
+
|
|
302
|
+
expect(lastUrl()).toBe('https://api.openai.com/v1/chat/completions');
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Provider x perfil.
|
|
308
|
+
*
|
|
309
|
+
* O setting `ai-gemini-profile-id` apontando para um perfil OpenAI fazia o
|
|
310
|
+
* serviço postar a chave da OpenAI no endpoint do Google e engolir o erro de
|
|
311
|
+
* cada modelo tentado. A trava já existia para o perfil explícito; faltava no
|
|
312
|
+
* caminho que vem do setting global.
|
|
313
|
+
*/
|
|
314
|
+
describe('AiService — perfil do provider', () => {
|
|
315
|
+
let service: AiService;
|
|
316
|
+
let integrationProfile: { findUnique: jest.Mock };
|
|
317
|
+
let settingService: { getSettingValues: jest.Mock };
|
|
318
|
+
|
|
319
|
+
const geminiProfile = {
|
|
320
|
+
slug: 'gemini',
|
|
321
|
+
config: { api_key: 'AIza-test' },
|
|
322
|
+
integration_provider: { slug: 'gemini' },
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const openaiProfile = {
|
|
326
|
+
slug: 'openia-hub',
|
|
327
|
+
config: { api_key: 'sk-test' },
|
|
328
|
+
integration_provider: { slug: 'openai' },
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
beforeEach(() => {
|
|
332
|
+
integrationProfile = { findUnique: jest.fn() };
|
|
333
|
+
settingService = { getSettingValues: jest.fn() };
|
|
334
|
+
|
|
335
|
+
service = new AiService(
|
|
336
|
+
{ integration_profile: integrationProfile } as any,
|
|
337
|
+
settingService as any,
|
|
338
|
+
{} as any,
|
|
339
|
+
{
|
|
340
|
+
start: jest.fn().mockResolvedValue(1),
|
|
341
|
+
recordStep: jest.fn().mockResolvedValue(undefined),
|
|
342
|
+
finish: jest.fn().mockResolvedValue(undefined),
|
|
343
|
+
} as any,
|
|
344
|
+
{ resolveMany: jest.fn() } as any,
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
mockedAxios.post.mockResolvedValue({
|
|
348
|
+
data: { candidates: [{ content: { parts: [{ text: 'ok' }] } }] },
|
|
349
|
+
} as any);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
afterEach(() => {
|
|
353
|
+
jest.restoreAllMocks();
|
|
354
|
+
mockedAxios.post.mockReset();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('recusa quando o setting do Gemini aponta para um perfil OpenAI', async () => {
|
|
358
|
+
settingService.getSettingValues.mockResolvedValue({
|
|
359
|
+
'ai-gemini-profile-id': 'openia-hub',
|
|
360
|
+
});
|
|
361
|
+
integrationProfile.findUnique.mockResolvedValue(openaiProfile);
|
|
362
|
+
|
|
363
|
+
await expect(
|
|
364
|
+
(service as any).getAiProfile('gemini'),
|
|
365
|
+
).rejects.toThrow(/ai-gemini-profile-id/);
|
|
366
|
+
|
|
367
|
+
// O ponto todo: nenhuma requisição condenada sai.
|
|
368
|
+
expect(mockedAxios.post).not.toHaveBeenCalled();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('aceita quando o provider do perfil bate com o pedido', async () => {
|
|
372
|
+
settingService.getSettingValues.mockResolvedValue({
|
|
373
|
+
'ai-gemini-profile-id': 'gemini',
|
|
374
|
+
});
|
|
375
|
+
integrationProfile.findUnique.mockResolvedValue(geminiProfile);
|
|
376
|
+
|
|
377
|
+
await expect((service as any).getAiProfile('gemini')).resolves.toMatchObject(
|
|
378
|
+
{ apiKey: 'AIza-test' },
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('isProviderConfigured responde sem lançar', async () => {
|
|
383
|
+
settingService.getSettingValues.mockResolvedValue({
|
|
384
|
+
'ai-gemini-profile-id': 'openia-hub',
|
|
385
|
+
});
|
|
386
|
+
integrationProfile.findUnique.mockResolvedValue(openaiProfile);
|
|
387
|
+
|
|
388
|
+
const result = await service.isProviderConfigured('gemini' as any);
|
|
389
|
+
|
|
390
|
+
expect(result.ok).toBe(false);
|
|
391
|
+
expect(result.reason).toMatch(/ai-gemini-profile-id/);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('isProviderConfigured devolve ok quando o perfil está correto', async () => {
|
|
395
|
+
settingService.getSettingValues.mockResolvedValue({
|
|
396
|
+
'ai-gemini-profile-id': 'gemini',
|
|
397
|
+
});
|
|
398
|
+
integrationProfile.findUnique.mockResolvedValue(geminiProfile);
|
|
399
|
+
|
|
400
|
+
await expect(service.isProviderConfigured('gemini' as any)).resolves.toEqual(
|
|
401
|
+
{ ok: true },
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('setting vazio continua sendo "não configurado", não incompatibilidade', async () => {
|
|
406
|
+
settingService.getSettingValues.mockResolvedValue({
|
|
407
|
+
'ai-gemini-profile-id': '',
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const result = await service.isProviderConfigured('gemini' as any);
|
|
411
|
+
|
|
412
|
+
expect(result.ok).toBe(false);
|
|
413
|
+
expect(result.reason).toMatch(/No gemini AI profile configured/);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// O caminho do perfil explícito deriva o provider DO perfil, então nunca cai
|
|
417
|
+
// na trava nova — é o que o SAC, o CMS e o campaign usam.
|
|
418
|
+
it('perfil explícito segue derivando o provider do próprio perfil', async () => {
|
|
419
|
+
integrationProfile.findUnique.mockResolvedValue(openaiProfile);
|
|
420
|
+
|
|
421
|
+
const resolved = await (service as any).resolveProviderAndProfile(
|
|
422
|
+
undefined,
|
|
423
|
+
'openia-hub',
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
expect(resolved.provider).toBe('openai');
|
|
427
|
+
expect(resolved.profile.apiKey).toBe('sk-test');
|
|
428
|
+
});
|
|
429
|
+
});
|