@plumbus/core 0.3.3 → 0.4.1
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 +2 -2
- package/bin/plumbus.js +16 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/ai/ai-service.d.ts.map +1 -1
- package/dist/ai/ai-service.js +8 -3
- package/dist/ai/ai-service.js.map +1 -1
- package/dist/ai/provider.d.ts.map +1 -1
- package/dist/ai/provider.js +1 -2
- package/dist/ai/provider.js.map +1 -1
- package/dist/ai/rag/pipeline.d.ts +4 -0
- package/dist/ai/rag/pipeline.d.ts.map +1 -1
- package/dist/ai/rag/pipeline.js +17 -2
- package/dist/ai/rag/pipeline.js.map +1 -1
- package/dist/ai/security.d.ts.map +1 -1
- package/dist/ai/security.js +14 -1
- package/dist/ai/security.js.map +1 -1
- package/dist/api/route-generator.d.ts.map +1 -1
- package/dist/api/route-generator.js +57 -11
- package/dist/api/route-generator.js.map +1 -1
- package/dist/auth/adapter.d.ts +0 -4
- package/dist/auth/adapter.d.ts.map +1 -1
- package/dist/auth/adapter.js +4 -0
- package/dist/auth/adapter.js.map +1 -1
- package/dist/auth/saml-adapter.d.ts.map +1 -1
- package/dist/auth/saml-adapter.js +73 -120
- package/dist/auth/saml-adapter.js.map +1 -1
- package/dist/auth/scim.d.ts.map +1 -1
- package/dist/auth/scim.js +6 -1
- package/dist/auth/scim.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +3 -1
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/commands/browser-extension.d.ts +10 -0
- package/dist/cli/commands/browser-extension.d.ts.map +1 -0
- package/dist/cli/commands/browser-extension.js +235 -0
- package/dist/cli/commands/browser-extension.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +4 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +21 -30
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +6 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +146 -2
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +18 -0
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/index.d.ts +2 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +2 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +122 -3
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts +9 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +125 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/migrate.d.ts.map +1 -1
- package/dist/cli/commands/migrate.js +2 -1
- package/dist/cli/commands/migrate.js.map +1 -1
- package/dist/cli/commands/start.d.ts +4 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +20 -29
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/translation.d.ts.map +1 -1
- package/dist/cli/commands/translation.js +22 -10
- package/dist/cli/commands/translation.js.map +1 -1
- package/dist/cli/mcp-serve-context.d.ts +13 -0
- package/dist/cli/mcp-serve-context.d.ts.map +1 -0
- package/dist/cli/mcp-serve-context.js +119 -0
- package/dist/cli/mcp-serve-context.js.map +1 -0
- package/dist/cli/migrate-snapshot-schema.d.ts +15 -0
- package/dist/cli/migrate-snapshot-schema.d.ts.map +1 -0
- package/dist/cli/migrate-snapshot-schema.js +23 -0
- package/dist/cli/migrate-snapshot-schema.js.map +1 -0
- package/dist/cli/project-root.d.ts +9 -0
- package/dist/cli/project-root.d.ts.map +1 -0
- package/dist/cli/project-root.js +41 -0
- package/dist/cli/project-root.js.map +1 -0
- package/dist/cli/scaffold-write.d.ts +17 -0
- package/dist/cli/scaffold-write.d.ts.map +1 -0
- package/dist/cli/scaffold-write.js +40 -0
- package/dist/cli/scaffold-write.js.map +1 -0
- package/dist/cli/templates/resources.d.ts.map +1 -1
- package/dist/cli/templates/resources.js +7 -1
- package/dist/cli/templates/resources.js.map +1 -1
- package/dist/cli/utils.d.ts +2 -0
- package/dist/cli/utils.d.ts.map +1 -1
- package/dist/cli/utils.js +14 -0
- package/dist/cli/utils.js.map +1 -1
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +46 -27
- package/dist/config/loader.js.map +1 -1
- package/dist/data/connection.d.ts +16 -0
- package/dist/data/connection.d.ts.map +1 -0
- package/dist/data/connection.js +27 -0
- package/dist/data/connection.js.map +1 -0
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +1 -0
- package/dist/data/index.js.map +1 -1
- package/dist/data/migration.d.ts.map +1 -1
- package/dist/data/migration.js +24 -0
- package/dist/data/migration.js.map +1 -1
- package/dist/data/repository.d.ts.map +1 -1
- package/dist/data/repository.js +32 -9
- package/dist/data/repository.js.map +1 -1
- package/dist/define/defineCapability.d.ts +4 -2
- package/dist/define/defineCapability.d.ts.map +1 -1
- package/dist/define/defineCapability.js +68 -8
- package/dist/define/defineCapability.js.map +1 -1
- package/dist/define/defineEntity.d.ts.map +1 -1
- package/dist/define/defineEntity.js +8 -4
- package/dist/define/defineEntity.js.map +1 -1
- package/dist/define/defineEvent.d.ts.map +1 -1
- package/dist/define/defineEvent.js +7 -3
- package/dist/define/defineEvent.js.map +1 -1
- package/dist/define/defineFlow.d.ts.map +1 -1
- package/dist/define/defineFlow.js +11 -5
- package/dist/define/defineFlow.js.map +1 -1
- package/dist/define/definePrompt.d.ts.map +1 -1
- package/dist/define/definePrompt.js +11 -5
- package/dist/define/definePrompt.js.map +1 -1
- package/dist/define/defineTranslation.d.ts.map +1 -1
- package/dist/define/defineTranslation.js +12 -10
- package/dist/define/defineTranslation.js.map +1 -1
- package/dist/define/validation-error.d.ts +2 -0
- package/dist/define/validation-error.d.ts.map +1 -0
- package/dist/define/validation-error.js +9 -0
- package/dist/define/validation-error.js.map +1 -0
- package/dist/errors/hints.d.ts +13 -0
- package/dist/errors/hints.d.ts.map +1 -0
- package/dist/errors/hints.js +15 -0
- package/dist/errors/hints.js.map +1 -0
- package/dist/errors/hook-log.d.ts +3 -0
- package/dist/errors/hook-log.d.ts.map +1 -0
- package/dist/errors/hook-log.js +6 -0
- package/dist/errors/hook-log.js.map +1 -0
- package/dist/errors/http.d.ts +19 -0
- package/dist/errors/http.d.ts.map +1 -1
- package/dist/errors/http.js +32 -2
- package/dist/errors/http.js.map +1 -1
- package/dist/errors/safe-metadata.d.ts +5 -0
- package/dist/errors/safe-metadata.d.ts.map +1 -0
- package/dist/errors/safe-metadata.js +24 -0
- package/dist/errors/safe-metadata.js.map +1 -0
- package/dist/execution/auth-roles.d.ts +6 -0
- package/dist/execution/auth-roles.d.ts.map +1 -0
- package/dist/execution/auth-roles.js +12 -0
- package/dist/execution/auth-roles.js.map +1 -0
- package/dist/execution/authorization.d.ts.map +1 -1
- package/dist/execution/authorization.js +3 -5
- package/dist/execution/authorization.js.map +1 -1
- package/dist/execution/context-factory.d.ts +2 -17
- package/dist/execution/context-factory.d.ts.map +1 -1
- package/dist/execution/context-factory.js +1 -0
- package/dist/execution/context-factory.js.map +1 -1
- package/dist/flows/engine.js +1 -1
- package/dist/flows/engine.js.map +1 -1
- package/dist/flows/evaluate-condition.d.ts +19 -0
- package/dist/flows/evaluate-condition.d.ts.map +1 -0
- package/dist/flows/evaluate-condition.js +276 -0
- package/dist/flows/evaluate-condition.js.map +1 -0
- package/dist/flows/index.d.ts +1 -0
- package/dist/flows/index.d.ts.map +1 -1
- package/dist/flows/index.js +2 -0
- package/dist/flows/index.js.map +1 -1
- package/dist/flows/step-executor.d.ts.map +1 -1
- package/dist/flows/step-executor.js +15 -7
- package/dist/flows/step-executor.js.map +1 -1
- package/dist/governance/rules/architecture.d.ts.map +1 -1
- package/dist/governance/rules/architecture.js +2 -0
- package/dist/governance/rules/architecture.js.map +1 -1
- package/dist/governance/rules/index.d.ts +1 -0
- package/dist/governance/rules/index.d.ts.map +1 -1
- package/dist/governance/rules/index.js +1 -0
- package/dist/governance/rules/index.js.map +1 -1
- package/dist/governance/rules/mcp.d.ts +5 -0
- package/dist/governance/rules/mcp.d.ts.map +1 -0
- package/dist/governance/rules/mcp.js +34 -0
- package/dist/governance/rules/mcp.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/exposure.d.ts +4 -0
- package/dist/mcp/exposure.d.ts.map +1 -0
- package/dist/mcp/exposure.js +5 -0
- package/dist/mcp/exposure.js.map +1 -0
- package/dist/mcp/index.d.ts +4 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +6 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/manifest-generator.d.ts +19 -0
- package/dist/mcp/manifest-generator.d.ts.map +1 -0
- package/dist/mcp/manifest-generator.js +29 -0
- package/dist/mcp/manifest-generator.js.map +1 -0
- package/dist/mcp/skill-generator.d.ts +4 -0
- package/dist/mcp/skill-generator.d.ts.map +1 -0
- package/dist/mcp/skill-generator.js +59 -0
- package/dist/mcp/skill-generator.js.map +1 -0
- package/dist/schema/zod-input-to-json-schema.d.ts +4 -0
- package/dist/schema/zod-input-to-json-schema.d.ts.map +1 -0
- package/dist/schema/zod-input-to-json-schema.js +6 -0
- package/dist/schema/zod-input-to-json-schema.js.map +1 -0
- package/dist/server/bootstrap.d.ts.map +1 -1
- package/dist/server/bootstrap.js +8 -3
- package/dist/server/bootstrap.js.map +1 -1
- package/dist/testing/context.d.ts +2 -0
- package/dist/testing/context.d.ts.map +1 -1
- package/dist/testing/context.js +1 -0
- package/dist/testing/context.js.map +1 -1
- package/dist/testing/e2e.js +1 -1
- package/dist/testing/e2e.js.map +1 -1
- package/dist/types/capability.d.ts +13 -0
- package/dist/types/capability.d.ts.map +1 -1
- package/dist/types/config.d.ts +9 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/context.d.ts +31 -0
- package/dist/types/context.d.ts.map +1 -1
- package/dist/types/deep-freeze.d.ts +6 -0
- package/dist/types/deep-freeze.d.ts.map +1 -0
- package/dist/types/deep-freeze.js +38 -0
- package/dist/types/deep-freeze.js.map +1 -0
- package/dist/types/enums.d.ts +5 -0
- package/dist/types/enums.d.ts.map +1 -1
- package/dist/types/enums.js +5 -0
- package/dist/types/enums.js.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/security.d.ts +3 -1
- package/dist/types/security.d.ts.map +1 -1
- package/instructions/capabilities.md +36 -0
- package/instructions/cli.md +44 -0
- package/instructions/framework.md +20 -4
- package/instructions/mcp.md +20 -0
- package/package.json +21 -2
- package/dist/ai/__tests__/ai-service.test.d.ts +0 -2
- package/dist/ai/__tests__/ai-service.test.d.ts.map +0 -1
- package/dist/ai/__tests__/ai-service.test.js +0 -1078
- package/dist/ai/__tests__/ai-service.test.js.map +0 -1
- package/dist/ai/__tests__/chunking.test.d.ts +0 -2
- package/dist/ai/__tests__/chunking.test.d.ts.map +0 -1
- package/dist/ai/__tests__/chunking.test.js +0 -78
- package/dist/ai/__tests__/chunking.test.js.map +0 -1
- package/dist/ai/__tests__/cost-tracker.test.d.ts +0 -2
- package/dist/ai/__tests__/cost-tracker.test.d.ts.map +0 -1
- package/dist/ai/__tests__/cost-tracker.test.js +0 -201
- package/dist/ai/__tests__/cost-tracker.test.js.map +0 -1
- package/dist/ai/__tests__/explainability.test.d.ts +0 -2
- package/dist/ai/__tests__/explainability.test.d.ts.map +0 -1
- package/dist/ai/__tests__/explainability.test.js +0 -100
- package/dist/ai/__tests__/explainability.test.js.map +0 -1
- package/dist/ai/__tests__/model-pricing.test.d.ts +0 -2
- package/dist/ai/__tests__/model-pricing.test.d.ts.map +0 -1
- package/dist/ai/__tests__/model-pricing.test.js +0 -156
- package/dist/ai/__tests__/model-pricing.test.js.map +0 -1
- package/dist/ai/__tests__/prompt-registry.test.d.ts +0 -2
- package/dist/ai/__tests__/prompt-registry.test.d.ts.map +0 -1
- package/dist/ai/__tests__/prompt-registry.test.js +0 -56
- package/dist/ai/__tests__/prompt-registry.test.js.map +0 -1
- package/dist/ai/__tests__/provider.test.d.ts +0 -3
- package/dist/ai/__tests__/provider.test.d.ts.map +0 -1
- package/dist/ai/__tests__/provider.test.js +0 -979
- package/dist/ai/__tests__/provider.test.js.map +0 -1
- package/dist/ai/__tests__/rag-pipeline.test.d.ts +0 -2
- package/dist/ai/__tests__/rag-pipeline.test.d.ts.map +0 -1
- package/dist/ai/__tests__/rag-pipeline.test.js +0 -174
- package/dist/ai/__tests__/rag-pipeline.test.js.map +0 -1
- package/dist/ai/__tests__/security.test.d.ts +0 -2
- package/dist/ai/__tests__/security.test.d.ts.map +0 -1
- package/dist/ai/__tests__/security.test.js +0 -89
- package/dist/ai/__tests__/security.test.js.map +0 -1
- package/dist/ai/__tests__/usage-client.test.d.ts +0 -2
- package/dist/ai/__tests__/usage-client.test.d.ts.map +0 -1
- package/dist/ai/__tests__/usage-client.test.js +0 -120
- package/dist/ai/__tests__/usage-client.test.js.map +0 -1
- package/dist/ai/__tests__/validation.test.d.ts +0 -2
- package/dist/ai/__tests__/validation.test.d.ts.map +0 -1
- package/dist/ai/__tests__/validation.test.js +0 -384
- package/dist/ai/__tests__/validation.test.js.map +0 -1
- package/dist/ai/__tests__/zod-to-provider-schema.test.d.ts +0 -2
- package/dist/ai/__tests__/zod-to-provider-schema.test.d.ts.map +0 -1
- package/dist/ai/__tests__/zod-to-provider-schema.test.js +0 -60
- package/dist/ai/__tests__/zod-to-provider-schema.test.js.map +0 -1
- package/dist/api/__tests__/route-generator.test.d.ts +0 -2
- package/dist/api/__tests__/route-generator.test.d.ts.map +0 -1
- package/dist/api/__tests__/route-generator.test.js +0 -216
- package/dist/api/__tests__/route-generator.test.js.map +0 -1
- package/dist/audit/__tests__/service.test.d.ts +0 -2
- package/dist/audit/__tests__/service.test.d.ts.map +0 -1
- package/dist/audit/__tests__/service.test.js +0 -97
- package/dist/audit/__tests__/service.test.js.map +0 -1
- package/dist/auth/__tests__/adapter.test.d.ts +0 -2
- package/dist/auth/__tests__/adapter.test.d.ts.map +0 -1
- package/dist/auth/__tests__/adapter.test.js +0 -218
- package/dist/auth/__tests__/adapter.test.js.map +0 -1
- package/dist/auth/__tests__/oidc-adapter.test.d.ts +0 -2
- package/dist/auth/__tests__/oidc-adapter.test.d.ts.map +0 -1
- package/dist/auth/__tests__/oidc-adapter.test.js +0 -232
- package/dist/auth/__tests__/oidc-adapter.test.js.map +0 -1
- package/dist/auth/__tests__/password.test.d.ts +0 -2
- package/dist/auth/__tests__/password.test.d.ts.map +0 -1
- package/dist/auth/__tests__/password.test.js +0 -30
- package/dist/auth/__tests__/password.test.js.map +0 -1
- package/dist/auth/__tests__/saml-adapter.test.d.ts +0 -2
- package/dist/auth/__tests__/saml-adapter.test.d.ts.map +0 -1
- package/dist/auth/__tests__/saml-adapter.test.js +0 -188
- package/dist/auth/__tests__/saml-adapter.test.js.map +0 -1
- package/dist/auth/__tests__/scim.test.d.ts +0 -2
- package/dist/auth/__tests__/scim.test.d.ts.map +0 -1
- package/dist/auth/__tests__/scim.test.js +0 -306
- package/dist/auth/__tests__/scim.test.js.map +0 -1
- package/dist/cli/__tests__/agent.test.d.ts +0 -2
- package/dist/cli/__tests__/agent.test.d.ts.map +0 -1
- package/dist/cli/__tests__/agent.test.js +0 -63
- package/dist/cli/__tests__/agent.test.js.map +0 -1
- package/dist/cli/__tests__/certify.test.d.ts +0 -2
- package/dist/cli/__tests__/certify.test.d.ts.map +0 -1
- package/dist/cli/__tests__/certify.test.js +0 -97
- package/dist/cli/__tests__/certify.test.js.map +0 -1
- package/dist/cli/__tests__/cli.test.d.ts +0 -2
- package/dist/cli/__tests__/cli.test.d.ts.map +0 -1
- package/dist/cli/__tests__/cli.test.js +0 -123
- package/dist/cli/__tests__/cli.test.js.map +0 -1
- package/dist/cli/__tests__/create.test.d.ts +0 -2
- package/dist/cli/__tests__/create.test.d.ts.map +0 -1
- package/dist/cli/__tests__/create.test.js +0 -183
- package/dist/cli/__tests__/create.test.js.map +0 -1
- package/dist/cli/__tests__/dev.test.d.ts +0 -2
- package/dist/cli/__tests__/dev.test.d.ts.map +0 -1
- package/dist/cli/__tests__/dev.test.js +0 -132
- package/dist/cli/__tests__/dev.test.js.map +0 -1
- package/dist/cli/__tests__/discover.test.d.ts +0 -2
- package/dist/cli/__tests__/discover.test.d.ts.map +0 -1
- package/dist/cli/__tests__/discover.test.js +0 -48
- package/dist/cli/__tests__/discover.test.js.map +0 -1
- package/dist/cli/__tests__/doctor.test.d.ts +0 -2
- package/dist/cli/__tests__/doctor.test.d.ts.map +0 -1
- package/dist/cli/__tests__/doctor.test.js +0 -172
- package/dist/cli/__tests__/doctor.test.js.map +0 -1
- package/dist/cli/__tests__/generate.test.d.ts +0 -2
- package/dist/cli/__tests__/generate.test.d.ts.map +0 -1
- package/dist/cli/__tests__/generate.test.js +0 -478
- package/dist/cli/__tests__/generate.test.js.map +0 -1
- package/dist/cli/__tests__/init.test.d.ts +0 -2
- package/dist/cli/__tests__/init.test.d.ts.map +0 -1
- package/dist/cli/__tests__/init.test.js +0 -236
- package/dist/cli/__tests__/init.test.js.map +0 -1
- package/dist/cli/__tests__/migrate.test.d.ts +0 -2
- package/dist/cli/__tests__/migrate.test.d.ts.map +0 -1
- package/dist/cli/__tests__/migrate.test.js +0 -442
- package/dist/cli/__tests__/migrate.test.js.map +0 -1
- package/dist/cli/__tests__/start.test.d.ts +0 -2
- package/dist/cli/__tests__/start.test.d.ts.map +0 -1
- package/dist/cli/__tests__/start.test.js +0 -91
- package/dist/cli/__tests__/start.test.js.map +0 -1
- package/dist/cli/__tests__/templates.test.d.ts +0 -2
- package/dist/cli/__tests__/templates.test.d.ts.map +0 -1
- package/dist/cli/__tests__/templates.test.js +0 -49
- package/dist/cli/__tests__/templates.test.js.map +0 -1
- package/dist/cli/__tests__/test.test.d.ts +0 -2
- package/dist/cli/__tests__/test.test.d.ts.map +0 -1
- package/dist/cli/__tests__/test.test.js +0 -40
- package/dist/cli/__tests__/test.test.js.map +0 -1
- package/dist/cli/__tests__/ui.test.d.ts +0 -2
- package/dist/cli/__tests__/ui.test.d.ts.map +0 -1
- package/dist/cli/__tests__/ui.test.js +0 -76
- package/dist/cli/__tests__/ui.test.js.map +0 -1
- package/dist/cli/__tests__/upgrade.test.d.ts +0 -2
- package/dist/cli/__tests__/upgrade.test.d.ts.map +0 -1
- package/dist/cli/__tests__/upgrade.test.js +0 -20
- package/dist/cli/__tests__/upgrade.test.js.map +0 -1
- package/dist/cli/__tests__/utils.test.d.ts +0 -2
- package/dist/cli/__tests__/utils.test.d.ts.map +0 -1
- package/dist/cli/__tests__/utils.test.js +0 -255
- package/dist/cli/__tests__/utils.test.js.map +0 -1
- package/dist/cli/__tests__/verify.test.d.ts +0 -2
- package/dist/cli/__tests__/verify.test.d.ts.map +0 -1
- package/dist/cli/__tests__/verify.test.js +0 -138
- package/dist/cli/__tests__/verify.test.js.map +0 -1
- package/dist/cli/commands/__tests__/commands.test.d.ts +0 -2
- package/dist/cli/commands/__tests__/commands.test.d.ts.map +0 -1
- package/dist/cli/commands/__tests__/commands.test.js +0 -185
- package/dist/cli/commands/__tests__/commands.test.js.map +0 -1
- package/dist/config/__tests__/loader.test.d.ts +0 -2
- package/dist/config/__tests__/loader.test.d.ts.map +0 -1
- package/dist/config/__tests__/loader.test.js +0 -462
- package/dist/config/__tests__/loader.test.js.map +0 -1
- package/dist/data/__tests__/drift-inspector.test.d.ts +0 -2
- package/dist/data/__tests__/drift-inspector.test.d.ts.map +0 -1
- package/dist/data/__tests__/drift-inspector.test.js +0 -170
- package/dist/data/__tests__/drift-inspector.test.js.map +0 -1
- package/dist/data/__tests__/framework-migration-rewriter.test.d.ts +0 -2
- package/dist/data/__tests__/framework-migration-rewriter.test.d.ts.map +0 -1
- package/dist/data/__tests__/framework-migration-rewriter.test.js +0 -329
- package/dist/data/__tests__/framework-migration-rewriter.test.js.map +0 -1
- package/dist/data/__tests__/migration.test.d.ts +0 -2
- package/dist/data/__tests__/migration.test.d.ts.map +0 -1
- package/dist/data/__tests__/migration.test.js +0 -145
- package/dist/data/__tests__/migration.test.js.map +0 -1
- package/dist/data/__tests__/registry.test.d.ts +0 -2
- package/dist/data/__tests__/registry.test.d.ts.map +0 -1
- package/dist/data/__tests__/registry.test.js +0 -70
- package/dist/data/__tests__/registry.test.js.map +0 -1
- package/dist/data/__tests__/repository.test.d.ts +0 -2
- package/dist/data/__tests__/repository.test.d.ts.map +0 -1
- package/dist/data/__tests__/repository.test.js +0 -559
- package/dist/data/__tests__/repository.test.js.map +0 -1
- package/dist/data/__tests__/schema-generator.test.d.ts +0 -2
- package/dist/data/__tests__/schema-generator.test.d.ts.map +0 -1
- package/dist/data/__tests__/schema-generator.test.js +0 -112
- package/dist/data/__tests__/schema-generator.test.js.map +0 -1
- package/dist/define/__tests__/defineCapability.test.d.ts +0 -2
- package/dist/define/__tests__/defineCapability.test.d.ts.map +0 -1
- package/dist/define/__tests__/defineCapability.test.js +0 -66
- package/dist/define/__tests__/defineCapability.test.js.map +0 -1
- package/dist/define/__tests__/defineEntity.test.d.ts +0 -2
- package/dist/define/__tests__/defineEntity.test.d.ts.map +0 -1
- package/dist/define/__tests__/defineEntity.test.js +0 -50
- package/dist/define/__tests__/defineEntity.test.js.map +0 -1
- package/dist/define/__tests__/defineEvent.test.d.ts +0 -2
- package/dist/define/__tests__/defineEvent.test.d.ts.map +0 -1
- package/dist/define/__tests__/defineEvent.test.js +0 -35
- package/dist/define/__tests__/defineEvent.test.js.map +0 -1
- package/dist/define/__tests__/defineFlow.test.d.ts +0 -2
- package/dist/define/__tests__/defineFlow.test.d.ts.map +0 -1
- package/dist/define/__tests__/defineFlow.test.js +0 -49
- package/dist/define/__tests__/defineFlow.test.js.map +0 -1
- package/dist/define/__tests__/definePrompt.test.d.ts +0 -2
- package/dist/define/__tests__/definePrompt.test.d.ts.map +0 -1
- package/dist/define/__tests__/definePrompt.test.js +0 -77
- package/dist/define/__tests__/definePrompt.test.js.map +0 -1
- package/dist/define/__tests__/defineTranslation.test.d.ts +0 -2
- package/dist/define/__tests__/defineTranslation.test.d.ts.map +0 -1
- package/dist/define/__tests__/defineTranslation.test.js +0 -90
- package/dist/define/__tests__/defineTranslation.test.js.map +0 -1
- package/dist/errors/__tests__/errors.test.d.ts +0 -2
- package/dist/errors/__tests__/errors.test.d.ts.map +0 -1
- package/dist/errors/__tests__/errors.test.js +0 -70
- package/dist/errors/__tests__/errors.test.js.map +0 -1
- package/dist/errors/__tests__/http.test.d.ts +0 -2
- package/dist/errors/__tests__/http.test.d.ts.map +0 -1
- package/dist/errors/__tests__/http.test.js +0 -35
- package/dist/errors/__tests__/http.test.js.map +0 -1
- package/dist/events/__tests__/consumer-registry.test.d.ts +0 -2
- package/dist/events/__tests__/consumer-registry.test.d.ts.map +0 -1
- package/dist/events/__tests__/consumer-registry.test.js +0 -68
- package/dist/events/__tests__/consumer-registry.test.js.map +0 -1
- package/dist/events/__tests__/dispatcher.test.d.ts +0 -2
- package/dist/events/__tests__/dispatcher.test.d.ts.map +0 -1
- package/dist/events/__tests__/dispatcher.test.js +0 -168
- package/dist/events/__tests__/dispatcher.test.js.map +0 -1
- package/dist/events/__tests__/event-emitter.test.d.ts +0 -2
- package/dist/events/__tests__/event-emitter.test.d.ts.map +0 -1
- package/dist/events/__tests__/event-emitter.test.js +0 -172
- package/dist/events/__tests__/event-emitter.test.js.map +0 -1
- package/dist/events/__tests__/event-registry.test.d.ts +0 -2
- package/dist/events/__tests__/event-registry.test.d.ts.map +0 -1
- package/dist/events/__tests__/event-registry.test.js +0 -55
- package/dist/events/__tests__/event-registry.test.js.map +0 -1
- package/dist/events/__tests__/event-worker.test.d.ts +0 -2
- package/dist/events/__tests__/event-worker.test.d.ts.map +0 -1
- package/dist/events/__tests__/event-worker.test.js +0 -174
- package/dist/events/__tests__/event-worker.test.js.map +0 -1
- package/dist/events/__tests__/queue.test.d.ts +0 -2
- package/dist/events/__tests__/queue.test.d.ts.map +0 -1
- package/dist/events/__tests__/queue.test.js +0 -73
- package/dist/events/__tests__/queue.test.js.map +0 -1
- package/dist/execution/__tests__/authorization.test.d.ts +0 -2
- package/dist/execution/__tests__/authorization.test.d.ts.map +0 -1
- package/dist/execution/__tests__/authorization.test.js +0 -99
- package/dist/execution/__tests__/authorization.test.js.map +0 -1
- package/dist/execution/__tests__/capability-executor.test.d.ts +0 -2
- package/dist/execution/__tests__/capability-executor.test.d.ts.map +0 -1
- package/dist/execution/__tests__/capability-executor.test.js +0 -212
- package/dist/execution/__tests__/capability-executor.test.js.map +0 -1
- package/dist/execution/__tests__/capability-registry.test.d.ts +0 -2
- package/dist/execution/__tests__/capability-registry.test.d.ts.map +0 -1
- package/dist/execution/__tests__/capability-registry.test.js +0 -50
- package/dist/execution/__tests__/capability-registry.test.js.map +0 -1
- package/dist/execution/__tests__/context-factory.test.d.ts +0 -2
- package/dist/execution/__tests__/context-factory.test.d.ts.map +0 -1
- package/dist/execution/__tests__/context-factory.test.js +0 -133
- package/dist/execution/__tests__/context-factory.test.js.map +0 -1
- package/dist/explanation/__tests__/tracker.test.d.ts +0 -2
- package/dist/explanation/__tests__/tracker.test.d.ts.map +0 -1
- package/dist/explanation/__tests__/tracker.test.js +0 -169
- package/dist/explanation/__tests__/tracker.test.js.map +0 -1
- package/dist/fields/__tests__/fields.test.d.ts +0 -2
- package/dist/fields/__tests__/fields.test.d.ts.map +0 -1
- package/dist/fields/__tests__/fields.test.js +0 -60
- package/dist/fields/__tests__/fields.test.js.map +0 -1
- package/dist/flows/__tests__/flow-engine.test.d.ts +0 -2
- package/dist/flows/__tests__/flow-engine.test.d.ts.map +0 -1
- package/dist/flows/__tests__/flow-engine.test.js +0 -797
- package/dist/flows/__tests__/flow-engine.test.js.map +0 -1
- package/dist/flows/__tests__/flow-registry.test.d.ts +0 -2
- package/dist/flows/__tests__/flow-registry.test.d.ts.map +0 -1
- package/dist/flows/__tests__/flow-registry.test.js +0 -69
- package/dist/flows/__tests__/flow-registry.test.js.map +0 -1
- package/dist/flows/__tests__/flow-service.test.d.ts +0 -2
- package/dist/flows/__tests__/flow-service.test.d.ts.map +0 -1
- package/dist/flows/__tests__/flow-service.test.js +0 -55
- package/dist/flows/__tests__/flow-service.test.js.map +0 -1
- package/dist/flows/__tests__/scheduler.test.d.ts +0 -2
- package/dist/flows/__tests__/scheduler.test.d.ts.map +0 -1
- package/dist/flows/__tests__/scheduler.test.js +0 -120
- package/dist/flows/__tests__/scheduler.test.js.map +0 -1
- package/dist/flows/__tests__/state-machine.test.d.ts +0 -2
- package/dist/flows/__tests__/state-machine.test.d.ts.map +0 -1
- package/dist/flows/__tests__/state-machine.test.js +0 -62
- package/dist/flows/__tests__/state-machine.test.js.map +0 -1
- package/dist/flows/__tests__/step-executor.test.d.ts +0 -2
- package/dist/flows/__tests__/step-executor.test.d.ts.map +0 -1
- package/dist/flows/__tests__/step-executor.test.js +0 -201
- package/dist/flows/__tests__/step-executor.test.js.map +0 -1
- package/dist/flows/__tests__/triggers.test.d.ts +0 -2
- package/dist/flows/__tests__/triggers.test.d.ts.map +0 -1
- package/dist/flows/__tests__/triggers.test.js +0 -98
- package/dist/flows/__tests__/triggers.test.js.map +0 -1
- package/dist/governance/__tests__/overrides.test.d.ts +0 -2
- package/dist/governance/__tests__/overrides.test.d.ts.map +0 -1
- package/dist/governance/__tests__/overrides.test.js +0 -123
- package/dist/governance/__tests__/overrides.test.js.map +0 -1
- package/dist/governance/__tests__/policies.test.d.ts +0 -2
- package/dist/governance/__tests__/policies.test.d.ts.map +0 -1
- package/dist/governance/__tests__/policies.test.js +0 -203
- package/dist/governance/__tests__/policies.test.js.map +0 -1
- package/dist/governance/__tests__/rules.test.d.ts +0 -2
- package/dist/governance/__tests__/rules.test.d.ts.map +0 -1
- package/dist/governance/__tests__/rules.test.js +0 -342
- package/dist/governance/__tests__/rules.test.js.map +0 -1
- package/dist/observability/__tests__/metrics.test.d.ts +0 -2
- package/dist/observability/__tests__/metrics.test.d.ts.map +0 -1
- package/dist/observability/__tests__/metrics.test.js +0 -301
- package/dist/observability/__tests__/metrics.test.js.map +0 -1
- package/dist/server/__tests__/bootstrap.test.d.ts +0 -2
- package/dist/server/__tests__/bootstrap.test.d.ts.map +0 -1
- package/dist/server/__tests__/bootstrap.test.js +0 -381
- package/dist/server/__tests__/bootstrap.test.js.map +0 -1
- package/dist/testing/__tests__/context.test.d.ts +0 -2
- package/dist/testing/__tests__/context.test.d.ts.map +0 -1
- package/dist/testing/__tests__/context.test.js +0 -341
- package/dist/testing/__tests__/context.test.js.map +0 -1
- package/dist/testing/__tests__/e2e.test.d.ts +0 -2
- package/dist/testing/__tests__/e2e.test.d.ts.map +0 -1
- package/dist/testing/__tests__/e2e.test.js +0 -119
- package/dist/testing/__tests__/e2e.test.js.map +0 -1
- package/dist/testing/__tests__/field-validation.test.d.ts +0 -2
- package/dist/testing/__tests__/field-validation.test.d.ts.map +0 -1
- package/dist/testing/__tests__/field-validation.test.js +0 -178
- package/dist/testing/__tests__/field-validation.test.js.map +0 -1
- package/dist/testing/__tests__/governance.test.d.ts +0 -2
- package/dist/testing/__tests__/governance.test.d.ts.map +0 -1
- package/dist/testing/__tests__/governance.test.js +0 -222
- package/dist/testing/__tests__/governance.test.js.map +0 -1
- package/dist/testing/__tests__/run-capability.test.d.ts +0 -2
- package/dist/testing/__tests__/run-capability.test.d.ts.map +0 -1
- package/dist/testing/__tests__/run-capability.test.js +0 -114
- package/dist/testing/__tests__/run-capability.test.js.map +0 -1
- package/dist/testing/__tests__/scaffolding.test.d.ts +0 -2
- package/dist/testing/__tests__/scaffolding.test.d.ts.map +0 -1
- package/dist/testing/__tests__/scaffolding.test.js +0 -70
- package/dist/testing/__tests__/scaffolding.test.js.map +0 -1
- package/dist/testing/__tests__/security.test.d.ts +0 -2
- package/dist/testing/__tests__/security.test.d.ts.map +0 -1
- package/dist/testing/__tests__/security.test.js +0 -186
- package/dist/testing/__tests__/security.test.js.map +0 -1
- package/dist/testing/__tests__/simulate-flow.test.d.ts +0 -2
- package/dist/testing/__tests__/simulate-flow.test.d.ts.map +0 -1
- package/dist/testing/__tests__/simulate-flow.test.js +0 -230
- package/dist/testing/__tests__/simulate-flow.test.js.map +0 -1
- package/dist/translations/__tests__/translation-resolver.test.d.ts +0 -2
- package/dist/translations/__tests__/translation-resolver.test.d.ts.map +0 -1
- package/dist/translations/__tests__/translation-resolver.test.js +0 -140
- package/dist/translations/__tests__/translation-resolver.test.js.map +0 -1
- package/dist/types/__tests__/registry.test.d.ts +0 -2
- package/dist/types/__tests__/registry.test.d.ts.map +0 -1
- package/dist/types/__tests__/registry.test.js +0 -34
- package/dist/types/__tests__/registry.test.js.map +0 -1
- package/dist/worker/__tests__/bootstrap.test.d.ts +0 -2
- package/dist/worker/__tests__/bootstrap.test.d.ts.map +0 -1
- package/dist/worker/__tests__/bootstrap.test.js +0 -268
- package/dist/worker/__tests__/bootstrap.test.js.map +0 -1
- package/dist/worker/__tests__/flow-service-wiring.test.d.ts +0 -2
- package/dist/worker/__tests__/flow-service-wiring.test.d.ts.map +0 -1
- package/dist/worker/__tests__/flow-service-wiring.test.js +0 -64
- package/dist/worker/__tests__/flow-service-wiring.test.js.map +0 -1
|
@@ -1,1078 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { definePrompt } from '../../define/definePrompt.js';
|
|
4
|
-
import { createAIService, singleProviderConfig } from '../ai-service.js';
|
|
5
|
-
import { createCostTracker } from '../cost-tracker.js';
|
|
6
|
-
import { createExplainabilityTracker } from '../explainability.js';
|
|
7
|
-
import { PromptRegistry } from '../prompt-registry.js';
|
|
8
|
-
import { createInMemoryVectorStore, createRAGPipeline } from '../rag/pipeline.js';
|
|
9
|
-
import { AIIncompleteOutputError, AIRefusalError } from '../refusal.js';
|
|
10
|
-
import { createMockProvider } from './provider.test.js';
|
|
11
|
-
describe('AI Service (ctx.ai)', () => {
|
|
12
|
-
function setupService(opts) {
|
|
13
|
-
const provider = createMockProvider({
|
|
14
|
-
complete: vi.fn(async () => ({
|
|
15
|
-
content: '{"result":"hello"}',
|
|
16
|
-
model: 'mock-model',
|
|
17
|
-
usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
|
|
18
|
-
finishReason: 'stop',
|
|
19
|
-
})),
|
|
20
|
-
embed: vi.fn(async (req) => ({
|
|
21
|
-
embeddings: req.texts.map(() => [0.1, 0.2, 0.3]),
|
|
22
|
-
model: 'mock-embed',
|
|
23
|
-
usage: { totalTokens: 5 },
|
|
24
|
-
cost: 0,
|
|
25
|
-
})),
|
|
26
|
-
});
|
|
27
|
-
const promptRegistry = opts?.promptRegistry ? new PromptRegistry() : undefined;
|
|
28
|
-
const costTracker = opts?.costTracker ? createCostTracker() : undefined;
|
|
29
|
-
const explainability = opts?.explainability ? createExplainabilityTracker() : undefined;
|
|
30
|
-
let ragPipeline;
|
|
31
|
-
if (opts?.rag) {
|
|
32
|
-
const vectorStore = createInMemoryVectorStore();
|
|
33
|
-
ragPipeline = createRAGPipeline({ provider, vectorStore });
|
|
34
|
-
}
|
|
35
|
-
const service = createAIService(singleProviderConfig(provider, {
|
|
36
|
-
promptRegistry,
|
|
37
|
-
costTracker,
|
|
38
|
-
ragPipeline,
|
|
39
|
-
defaultModel: 'mock-model',
|
|
40
|
-
enableStrictStructuredOutputs: opts?.strictStructuredOutputs,
|
|
41
|
-
budget: { tenantId: 't1', actor: 'user1' },
|
|
42
|
-
}));
|
|
43
|
-
return { service, provider, promptRegistry, costTracker, explainability };
|
|
44
|
-
}
|
|
45
|
-
describe('generate', () => {
|
|
46
|
-
it('generates a response with raw prompt', async () => {
|
|
47
|
-
const { service } = setupService();
|
|
48
|
-
const result = await service.generate({
|
|
49
|
-
prompt: 'Say hello',
|
|
50
|
-
input: {},
|
|
51
|
-
});
|
|
52
|
-
expect(result).toBe('{"result":"hello"}');
|
|
53
|
-
});
|
|
54
|
-
it('uses prompt registry when available', async () => {
|
|
55
|
-
const { service, promptRegistry, provider } = setupService({ promptRegistry: true });
|
|
56
|
-
promptRegistry?.register(definePrompt({
|
|
57
|
-
name: 'greet',
|
|
58
|
-
description: 'Say hello to {{name}}',
|
|
59
|
-
input: z.object({ name: z.string() }),
|
|
60
|
-
output: z.object({ result: z.string(), language: z.string().optional() }),
|
|
61
|
-
}));
|
|
62
|
-
const result = await service.generate({
|
|
63
|
-
prompt: 'greet',
|
|
64
|
-
input: { name: 'Alice' },
|
|
65
|
-
});
|
|
66
|
-
expect(result).toEqual({ result: 'hello' });
|
|
67
|
-
expect(provider.complete).toHaveBeenCalled();
|
|
68
|
-
});
|
|
69
|
-
it('renders registered system prompts without duplicating substituted input', async () => {
|
|
70
|
-
const { service, promptRegistry, provider } = setupService({ promptRegistry: true });
|
|
71
|
-
promptRegistry?.register(definePrompt({
|
|
72
|
-
name: 'system-greet',
|
|
73
|
-
system: 'You are {{role}}.',
|
|
74
|
-
description: 'Data: {{text}}',
|
|
75
|
-
input: z.object({
|
|
76
|
-
role: z.string(),
|
|
77
|
-
text: z.string(),
|
|
78
|
-
locale: z.string(),
|
|
79
|
-
}),
|
|
80
|
-
output: z.object({ result: z.string(), language: z.string().optional() }),
|
|
81
|
-
}));
|
|
82
|
-
await service.generate({
|
|
83
|
-
prompt: 'system-greet',
|
|
84
|
-
input: { role: 'editor', text: 'hello', locale: 'en' },
|
|
85
|
-
});
|
|
86
|
-
expect(provider.complete).toHaveBeenCalledWith(expect.objectContaining({
|
|
87
|
-
system: 'You are editor.',
|
|
88
|
-
prompt: expect.stringContaining('Data: hello\n\nInput: {"locale":"en"}'),
|
|
89
|
-
}));
|
|
90
|
-
});
|
|
91
|
-
it('keeps system undefined for registered prompts without a system template', async () => {
|
|
92
|
-
const { service, promptRegistry, provider } = setupService({ promptRegistry: true });
|
|
93
|
-
promptRegistry?.register(definePrompt({
|
|
94
|
-
name: 'no-system',
|
|
95
|
-
description: 'Data: {{text}}',
|
|
96
|
-
input: z.object({ text: z.string() }),
|
|
97
|
-
output: z.object({ result: z.string(), language: z.string().optional() }),
|
|
98
|
-
}));
|
|
99
|
-
await service.generate({
|
|
100
|
-
prompt: 'no-system',
|
|
101
|
-
input: { text: 'hello' },
|
|
102
|
-
});
|
|
103
|
-
expect(provider.complete).toHaveBeenCalledWith(expect.objectContaining({
|
|
104
|
-
system: undefined,
|
|
105
|
-
prompt: expect.stringContaining('Data: hello'),
|
|
106
|
-
}));
|
|
107
|
-
});
|
|
108
|
-
it('can omit unsubstituted input when a prompt renders the complete user message', async () => {
|
|
109
|
-
const { service, promptRegistry, provider } = setupService({ promptRegistry: true });
|
|
110
|
-
promptRegistry?.register(definePrompt({
|
|
111
|
-
name: 'complete-user-message',
|
|
112
|
-
system: '{{systemInstructions}}',
|
|
113
|
-
description: '{{userPrompt}}',
|
|
114
|
-
input: z.object({
|
|
115
|
-
systemInstructions: z.string(),
|
|
116
|
-
userPrompt: z.string(),
|
|
117
|
-
sourceText: z.string(),
|
|
118
|
-
}),
|
|
119
|
-
output: z.object({ result: z.string(), language: z.string().optional() }),
|
|
120
|
-
appendUnsubstitutedInput: false,
|
|
121
|
-
}));
|
|
122
|
-
await service.generate({
|
|
123
|
-
prompt: 'complete-user-message',
|
|
124
|
-
input: {
|
|
125
|
-
systemInstructions: 'System text',
|
|
126
|
-
userPrompt: 'User text',
|
|
127
|
-
sourceText: 'raw source',
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
expect(provider.complete).toHaveBeenCalledWith(expect.objectContaining({
|
|
131
|
-
system: 'System text',
|
|
132
|
-
prompt: expect.stringContaining('User text'),
|
|
133
|
-
}));
|
|
134
|
-
});
|
|
135
|
-
it('passes a strict provider schema for registered JSON prompts when enabled', async () => {
|
|
136
|
-
const { service, promptRegistry, provider } = setupService({
|
|
137
|
-
promptRegistry: true,
|
|
138
|
-
strictStructuredOutputs: true,
|
|
139
|
-
});
|
|
140
|
-
promptRegistry?.register(definePrompt({
|
|
141
|
-
name: 'strict-greet',
|
|
142
|
-
description: 'Say hello to {{name}} as JSON.',
|
|
143
|
-
input: z.object({ name: z.string() }),
|
|
144
|
-
output: z.object({ result: z.string(), language: z.string().optional() }),
|
|
145
|
-
}));
|
|
146
|
-
await service.generate({
|
|
147
|
-
prompt: 'strict-greet',
|
|
148
|
-
input: { name: 'Alice' },
|
|
149
|
-
});
|
|
150
|
-
expect(provider.complete).toHaveBeenCalledWith(expect.objectContaining({
|
|
151
|
-
responseFormat: 'json',
|
|
152
|
-
responseSchema: expect.objectContaining({
|
|
153
|
-
type: 'object',
|
|
154
|
-
additionalProperties: false,
|
|
155
|
-
}),
|
|
156
|
-
}));
|
|
157
|
-
});
|
|
158
|
-
it('does not add a prose JSON hint when provider schema is active', async () => {
|
|
159
|
-
const { service, promptRegistry, provider } = setupService({
|
|
160
|
-
promptRegistry: true,
|
|
161
|
-
strictStructuredOutputs: true,
|
|
162
|
-
});
|
|
163
|
-
promptRegistry?.register(definePrompt({
|
|
164
|
-
name: 'schema-only-extract',
|
|
165
|
-
description: 'Extract names from {{text}}.',
|
|
166
|
-
input: z.object({ text: z.string() }),
|
|
167
|
-
output: z.object({ result: z.string(), language: z.string().optional() }),
|
|
168
|
-
requireStrictStructuredOutputs: true,
|
|
169
|
-
}));
|
|
170
|
-
await service.generate({
|
|
171
|
-
prompt: 'schema-only-extract',
|
|
172
|
-
input: { text: 'Alice and Bob' },
|
|
173
|
-
});
|
|
174
|
-
expect(provider.complete).toHaveBeenCalledWith(expect.objectContaining({
|
|
175
|
-
responseFormat: 'json',
|
|
176
|
-
responseSchema: expect.objectContaining({ type: 'object' }),
|
|
177
|
-
prompt: expect.not.stringContaining('Respond with a valid JSON object'),
|
|
178
|
-
}));
|
|
179
|
-
});
|
|
180
|
-
it('fails required strict structured prompts when provider schema is disabled', async () => {
|
|
181
|
-
const { service, promptRegistry, provider } = setupService({ promptRegistry: true });
|
|
182
|
-
promptRegistry?.register(definePrompt({
|
|
183
|
-
name: 'strict-required',
|
|
184
|
-
description: 'Extract names from {{text}}.',
|
|
185
|
-
input: z.object({ text: z.string() }),
|
|
186
|
-
output: z.object({ names: z.array(z.string()) }),
|
|
187
|
-
requireStrictStructuredOutputs: true,
|
|
188
|
-
}));
|
|
189
|
-
await expect(service.generate({
|
|
190
|
-
prompt: 'strict-required',
|
|
191
|
-
input: { text: 'Alice and Bob' },
|
|
192
|
-
})).rejects.toThrow('requires provider-side structured outputs');
|
|
193
|
-
expect(provider.complete).not.toHaveBeenCalled();
|
|
194
|
-
});
|
|
195
|
-
it('does not pass a strict provider schema for prompt-level opt outs', async () => {
|
|
196
|
-
const { service, promptRegistry, provider } = setupService({
|
|
197
|
-
promptRegistry: true,
|
|
198
|
-
strictStructuredOutputs: true,
|
|
199
|
-
});
|
|
200
|
-
promptRegistry?.register(definePrompt({
|
|
201
|
-
name: 'legacy-json',
|
|
202
|
-
description: 'Say hello as JSON.',
|
|
203
|
-
input: z.object({}),
|
|
204
|
-
output: z.object({ result: z.string() }),
|
|
205
|
-
disableStrictStructuredOutputs: true,
|
|
206
|
-
}));
|
|
207
|
-
await service.generate({ prompt: 'legacy-json', input: {} });
|
|
208
|
-
expect(provider.complete).toHaveBeenCalledWith(expect.objectContaining({
|
|
209
|
-
responseFormat: 'json',
|
|
210
|
-
responseSchema: undefined,
|
|
211
|
-
}));
|
|
212
|
-
});
|
|
213
|
-
it('tracks cost when cost tracker is present', async () => {
|
|
214
|
-
const { service, costTracker } = setupService({ costTracker: true });
|
|
215
|
-
await service.generate({ prompt: 'test', input: {} });
|
|
216
|
-
expect(costTracker?.getRecords()).toHaveLength(1);
|
|
217
|
-
expect(costTracker?.getRecords()[0]?.operation).toBe('generate');
|
|
218
|
-
});
|
|
219
|
-
it('records refused status and usage for provider refusals', async () => {
|
|
220
|
-
const provider = createMockProvider({
|
|
221
|
-
complete: vi.fn(async () => {
|
|
222
|
-
throw new AIRefusalError({
|
|
223
|
-
provider: 'mock',
|
|
224
|
-
model: 'mock-model',
|
|
225
|
-
refusalText: 'No.',
|
|
226
|
-
usage: { inputTokens: 3, outputTokens: 2, totalTokens: 5 },
|
|
227
|
-
});
|
|
228
|
-
}),
|
|
229
|
-
});
|
|
230
|
-
const promptRegistry = new PromptRegistry();
|
|
231
|
-
promptRegistry.register(definePrompt({
|
|
232
|
-
name: 'refused',
|
|
233
|
-
description: 'Return JSON.',
|
|
234
|
-
input: z.object({}),
|
|
235
|
-
output: z.object({ result: z.string() }),
|
|
236
|
-
}));
|
|
237
|
-
const costTracker = createCostTracker();
|
|
238
|
-
const service = createAIService(singleProviderConfig(provider, {
|
|
239
|
-
promptRegistry,
|
|
240
|
-
costTracker,
|
|
241
|
-
defaultModel: 'mock-model',
|
|
242
|
-
enableStrictStructuredOutputs: true,
|
|
243
|
-
}));
|
|
244
|
-
await expect(service.generate({ prompt: 'refused', input: {} })).rejects.toThrow(AIRefusalError);
|
|
245
|
-
expect(costTracker.getRecords()[0]).toMatchObject({
|
|
246
|
-
status: 'refused',
|
|
247
|
-
usage: { inputTokens: 3, outputTokens: 2, totalTokens: 5 },
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
it('records incomplete status and usage for truncated structured outputs', async () => {
|
|
251
|
-
const provider = createMockProvider({
|
|
252
|
-
complete: vi.fn(async () => {
|
|
253
|
-
throw new AIIncompleteOutputError({
|
|
254
|
-
provider: 'mock',
|
|
255
|
-
model: 'mock-model',
|
|
256
|
-
partialText: '{"result":',
|
|
257
|
-
usage: { inputTokens: 3, outputTokens: 8, totalTokens: 11 },
|
|
258
|
-
finishReason: 'length',
|
|
259
|
-
});
|
|
260
|
-
}),
|
|
261
|
-
});
|
|
262
|
-
const promptRegistry = new PromptRegistry();
|
|
263
|
-
promptRegistry.register(definePrompt({
|
|
264
|
-
name: 'truncated',
|
|
265
|
-
description: 'Return JSON.',
|
|
266
|
-
input: z.object({}),
|
|
267
|
-
output: z.object({ result: z.string() }),
|
|
268
|
-
}));
|
|
269
|
-
const costTracker = createCostTracker();
|
|
270
|
-
const service = createAIService(singleProviderConfig(provider, {
|
|
271
|
-
promptRegistry,
|
|
272
|
-
costTracker,
|
|
273
|
-
defaultModel: 'mock-model',
|
|
274
|
-
enableStrictStructuredOutputs: true,
|
|
275
|
-
}));
|
|
276
|
-
await expect(service.generate({ prompt: 'truncated', input: {} })).rejects.toThrow(AIIncompleteOutputError);
|
|
277
|
-
expect(costTracker.getRecords()[0]).toMatchObject({
|
|
278
|
-
status: 'incomplete',
|
|
279
|
-
usage: { inputTokens: 3, outputTokens: 8, totalTokens: 11 },
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
describe('prompt assembly', () => {
|
|
284
|
-
function getRequestPrompt(provider) {
|
|
285
|
-
const mock = provider.complete;
|
|
286
|
-
const firstCall = mock.mock.calls[0];
|
|
287
|
-
const req = firstCall?.[0];
|
|
288
|
-
return req?.prompt ?? '';
|
|
289
|
-
}
|
|
290
|
-
it('does NOT append `Input: {...}` when every input key is consumed by a {{placeholder}}', async () => {
|
|
291
|
-
const { service, promptRegistry, provider } = setupService({ promptRegistry: true });
|
|
292
|
-
promptRegistry?.register(definePrompt({
|
|
293
|
-
name: 'all-substituted',
|
|
294
|
-
description: 'Hello {{name}}, you are {{age}} years old. Respond as JSON.',
|
|
295
|
-
input: z.object({ name: z.string(), age: z.number() }),
|
|
296
|
-
output: z.object({ result: z.string() }),
|
|
297
|
-
}));
|
|
298
|
-
await service.generate({
|
|
299
|
-
prompt: 'all-substituted',
|
|
300
|
-
input: { name: 'Alice', age: 30 },
|
|
301
|
-
});
|
|
302
|
-
const sent = getRequestPrompt(provider);
|
|
303
|
-
expect(sent).toContain('Hello Alice, you are 30 years old');
|
|
304
|
-
expect(sent).not.toContain('Input: {');
|
|
305
|
-
// Each substituted value should appear exactly once (no duplication)
|
|
306
|
-
expect(sent.match(/Alice/g)?.length ?? 0).toBe(1);
|
|
307
|
-
expect(sent.match(/\b30\b/g)?.length ?? 0).toBe(1);
|
|
308
|
-
});
|
|
309
|
-
it('appends only the unsubstituted keys when input has extras beyond the placeholders', async () => {
|
|
310
|
-
const { service, promptRegistry, provider } = setupService({ promptRegistry: true });
|
|
311
|
-
promptRegistry?.register(definePrompt({
|
|
312
|
-
name: 'partial-substituted',
|
|
313
|
-
description: 'Greet {{name}}. Respond as JSON.',
|
|
314
|
-
input: z.object({ name: z.string(), context: z.string() }),
|
|
315
|
-
output: z.object({ result: z.string() }),
|
|
316
|
-
}));
|
|
317
|
-
await service.generate({
|
|
318
|
-
prompt: 'partial-substituted',
|
|
319
|
-
input: { name: 'Alice', context: 'morning meeting' },
|
|
320
|
-
});
|
|
321
|
-
const sent = getRequestPrompt(provider);
|
|
322
|
-
expect(sent).toContain('Greet Alice');
|
|
323
|
-
expect(sent).toContain('Input: {');
|
|
324
|
-
expect(sent).toContain('"context":"morning meeting"');
|
|
325
|
-
expect(sent).not.toContain('"name":"Alice"');
|
|
326
|
-
// Substituted value still appears only once (in the rendered template, not in Input blob)
|
|
327
|
-
expect(sent.match(/Alice/g)?.length ?? 0).toBe(1);
|
|
328
|
-
});
|
|
329
|
-
it('appends Input: {...} when prompt has no placeholders for any input key', async () => {
|
|
330
|
-
const { service, promptRegistry, provider } = setupService({ promptRegistry: true });
|
|
331
|
-
promptRegistry?.register(definePrompt({
|
|
332
|
-
name: 'no-placeholders',
|
|
333
|
-
description: 'Process the input. Respond as JSON.',
|
|
334
|
-
input: z.object({ name: z.string() }),
|
|
335
|
-
output: z.object({ result: z.string() }),
|
|
336
|
-
}));
|
|
337
|
-
await service.generate({
|
|
338
|
-
prompt: 'no-placeholders',
|
|
339
|
-
input: { name: 'Alice' },
|
|
340
|
-
});
|
|
341
|
-
const sent = getRequestPrompt(provider);
|
|
342
|
-
expect(sent).toContain('Process the input.');
|
|
343
|
-
expect(sent).toContain('Input: {"name":"Alice"}');
|
|
344
|
-
});
|
|
345
|
-
it('raw-prompt path (no registry def) is unchanged: no Input append, prompt sent as-is', async () => {
|
|
346
|
-
const { service, provider } = setupService();
|
|
347
|
-
await service.generate({ prompt: 'just say hi', input: {} });
|
|
348
|
-
const sent = getRequestPrompt(provider);
|
|
349
|
-
expect(sent).toBe('just say hi');
|
|
350
|
-
});
|
|
351
|
-
});
|
|
352
|
-
describe('generateWithUsage', () => {
|
|
353
|
-
it('returns data and usage for raw prompt', async () => {
|
|
354
|
-
const { service } = setupService();
|
|
355
|
-
const result = await service.generateWithUsage({
|
|
356
|
-
prompt: 'Say hello',
|
|
357
|
-
input: {},
|
|
358
|
-
});
|
|
359
|
-
expect(result.data).toBe('{"result":"hello"}');
|
|
360
|
-
expect(result.usage).toEqual({ inputTokens: 10, outputTokens: 20, totalTokens: 30 });
|
|
361
|
-
});
|
|
362
|
-
it('returns validated data and usage with prompt registry', async () => {
|
|
363
|
-
const { service, promptRegistry } = setupService({ promptRegistry: true });
|
|
364
|
-
promptRegistry?.register(definePrompt({
|
|
365
|
-
name: 'greet',
|
|
366
|
-
description: 'Say hello to {{name}}',
|
|
367
|
-
input: z.object({ name: z.string() }),
|
|
368
|
-
output: z.object({ result: z.string() }),
|
|
369
|
-
}));
|
|
370
|
-
const result = await service.generateWithUsage({
|
|
371
|
-
prompt: 'greet',
|
|
372
|
-
input: { name: 'Alice' },
|
|
373
|
-
});
|
|
374
|
-
expect(result.data).toEqual({ result: 'hello' });
|
|
375
|
-
expect(result.usage.inputTokens).toBe(10);
|
|
376
|
-
expect(result.usage.outputTokens).toBe(20);
|
|
377
|
-
});
|
|
378
|
-
it('allows per-request validation overrides', async () => {
|
|
379
|
-
let callCount = 0;
|
|
380
|
-
const provider = createMockProvider({
|
|
381
|
-
complete: vi.fn(async () => {
|
|
382
|
-
callCount += 1;
|
|
383
|
-
if (callCount === 1) {
|
|
384
|
-
return {
|
|
385
|
-
content: '{"wrong":"schema"}',
|
|
386
|
-
model: 'mock-model',
|
|
387
|
-
usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
|
|
388
|
-
finishReason: 'stop',
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
return {
|
|
392
|
-
content: '{"result":"hello"}',
|
|
393
|
-
model: 'mock-model',
|
|
394
|
-
usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
|
|
395
|
-
finishReason: 'stop',
|
|
396
|
-
};
|
|
397
|
-
}),
|
|
398
|
-
});
|
|
399
|
-
const promptRegistry = new PromptRegistry();
|
|
400
|
-
promptRegistry.register(definePrompt({
|
|
401
|
-
name: 'greet-once',
|
|
402
|
-
description: 'Say hello once',
|
|
403
|
-
input: z.object({}),
|
|
404
|
-
output: z.object({ result: z.string() }),
|
|
405
|
-
}));
|
|
406
|
-
const service = createAIService(singleProviderConfig(provider, {
|
|
407
|
-
promptRegistry,
|
|
408
|
-
defaultModel: 'mock-model',
|
|
409
|
-
}));
|
|
410
|
-
await expect(service.generateWithUsage({
|
|
411
|
-
prompt: 'greet-once',
|
|
412
|
-
input: {},
|
|
413
|
-
validation: { maxRetries: 0, feedbackOnError: false },
|
|
414
|
-
})).rejects.toThrow('AI output validation failed after 1 attempts');
|
|
415
|
-
expect(provider.complete).toHaveBeenCalledTimes(1);
|
|
416
|
-
});
|
|
417
|
-
});
|
|
418
|
-
describe('extract', () => {
|
|
419
|
-
it('extracts structured data from text', async () => {
|
|
420
|
-
const provider = createMockProvider({
|
|
421
|
-
complete: vi.fn(async () => ({
|
|
422
|
-
content: '{"name":"Alice","age":30}',
|
|
423
|
-
model: 'mock',
|
|
424
|
-
usage: { inputTokens: 20, outputTokens: 10, totalTokens: 30 },
|
|
425
|
-
finishReason: 'stop',
|
|
426
|
-
})),
|
|
427
|
-
});
|
|
428
|
-
const service = createAIService(singleProviderConfig(provider));
|
|
429
|
-
const schema = z.object({ name: z.string(), age: z.number() });
|
|
430
|
-
const result = await service.extract({ schema, text: 'Alice is 30 years old' });
|
|
431
|
-
expect(result).toEqual({ name: 'Alice', age: 30 });
|
|
432
|
-
});
|
|
433
|
-
});
|
|
434
|
-
describe('classify', () => {
|
|
435
|
-
it('classifies text into labels', async () => {
|
|
436
|
-
const provider = createMockProvider({
|
|
437
|
-
complete: vi.fn(async () => ({
|
|
438
|
-
content: '["urgent","billing"]',
|
|
439
|
-
model: 'mock',
|
|
440
|
-
usage: { inputTokens: 15, outputTokens: 5, totalTokens: 20 },
|
|
441
|
-
finishReason: 'stop',
|
|
442
|
-
})),
|
|
443
|
-
});
|
|
444
|
-
const service = createAIService(singleProviderConfig(provider));
|
|
445
|
-
const result = await service.classify({
|
|
446
|
-
labels: ['urgent', 'billing', 'support'],
|
|
447
|
-
text: 'My payment failed and I need help now!',
|
|
448
|
-
});
|
|
449
|
-
expect(result).toEqual(['urgent', 'billing']);
|
|
450
|
-
});
|
|
451
|
-
it('filters out invalid labels', async () => {
|
|
452
|
-
const provider = createMockProvider({
|
|
453
|
-
complete: vi.fn(async () => ({
|
|
454
|
-
content: '["urgent","invalid_label"]',
|
|
455
|
-
model: 'mock',
|
|
456
|
-
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
|
|
457
|
-
finishReason: 'stop',
|
|
458
|
-
})),
|
|
459
|
-
});
|
|
460
|
-
const service = createAIService(singleProviderConfig(provider));
|
|
461
|
-
const result = await service.classify({
|
|
462
|
-
labels: ['urgent', 'billing'],
|
|
463
|
-
text: 'test',
|
|
464
|
-
});
|
|
465
|
-
expect(result).toEqual(['urgent']);
|
|
466
|
-
});
|
|
467
|
-
});
|
|
468
|
-
describe('retrieve', () => {
|
|
469
|
-
it('throws when RAG pipeline not configured', async () => {
|
|
470
|
-
const { service } = setupService();
|
|
471
|
-
await expect(service.retrieve({ query: 'test' })).rejects.toThrow('RAG pipeline not configured');
|
|
472
|
-
});
|
|
473
|
-
it('retrieves documents when RAG is configured', async () => {
|
|
474
|
-
const { service } = setupService({ rag: true });
|
|
475
|
-
// The in-memory store is empty so results will be empty, but it should not throw
|
|
476
|
-
const results = await service.retrieve({ query: 'test' });
|
|
477
|
-
expect(Array.isArray(results)).toBe(true);
|
|
478
|
-
});
|
|
479
|
-
});
|
|
480
|
-
describe('budget enforcement', () => {
|
|
481
|
-
it('throws when budget is exceeded', async () => {
|
|
482
|
-
const costTracker = createCostTracker({ dailyCostLimit: 0.001 });
|
|
483
|
-
costTracker.record({
|
|
484
|
-
model: 'gpt-4o',
|
|
485
|
-
provider: 'mock',
|
|
486
|
-
operation: 'generate',
|
|
487
|
-
usage: { inputTokens: 1000, outputTokens: 500, totalTokens: 1500 },
|
|
488
|
-
cost: 0.001,
|
|
489
|
-
latencyMs: 200,
|
|
490
|
-
});
|
|
491
|
-
const provider = createMockProvider();
|
|
492
|
-
const service = createAIService(singleProviderConfig(provider, {
|
|
493
|
-
costTracker,
|
|
494
|
-
budget: { tenantId: 't1' },
|
|
495
|
-
}));
|
|
496
|
-
await expect(service.generate({ prompt: 'test', input: {} })).rejects.toThrow('AI budget exceeded');
|
|
497
|
-
});
|
|
498
|
-
});
|
|
499
|
-
describe('multi-provider routing', () => {
|
|
500
|
-
function createNamedProvider(name, content = '{"result":"ok"}') {
|
|
501
|
-
return createMockProvider({
|
|
502
|
-
name,
|
|
503
|
-
complete: vi.fn(async () => ({
|
|
504
|
-
content,
|
|
505
|
-
model: `${name}-model`,
|
|
506
|
-
usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
|
|
507
|
-
finishReason: 'stop',
|
|
508
|
-
})),
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
it('routes to the prompt-specified provider', async () => {
|
|
512
|
-
const openai = createNamedProvider('openai', '{"result":"from-openai"}');
|
|
513
|
-
const anthropic = createNamedProvider('anthropic', '{"result":"from-anthropic"}');
|
|
514
|
-
const promptRegistry = new PromptRegistry();
|
|
515
|
-
promptRegistry.register(definePrompt({
|
|
516
|
-
name: 'write-bio',
|
|
517
|
-
description: 'Write a bio for {{name}}',
|
|
518
|
-
input: z.object({ name: z.string() }),
|
|
519
|
-
output: z.object({ result: z.string() }),
|
|
520
|
-
model: { name: 'claude-sonnet', provider: 'anthropic' },
|
|
521
|
-
}));
|
|
522
|
-
const service = createAIService({
|
|
523
|
-
providers: { openai, anthropic },
|
|
524
|
-
defaultProvider: 'openai',
|
|
525
|
-
promptRegistry,
|
|
526
|
-
});
|
|
527
|
-
await service.generate({ prompt: 'write-bio', input: { name: 'Alice' } });
|
|
528
|
-
expect(anthropic.complete).toHaveBeenCalled();
|
|
529
|
-
expect(openai.complete).not.toHaveBeenCalled();
|
|
530
|
-
});
|
|
531
|
-
it('falls back to default provider when prompt has no provider', async () => {
|
|
532
|
-
const openai = createNamedProvider('openai');
|
|
533
|
-
const anthropic = createNamedProvider('anthropic');
|
|
534
|
-
const service = createAIService({
|
|
535
|
-
providers: { openai, anthropic },
|
|
536
|
-
defaultProvider: 'openai',
|
|
537
|
-
});
|
|
538
|
-
await service.generate({ prompt: 'raw prompt', input: {} });
|
|
539
|
-
expect(openai.complete).toHaveBeenCalled();
|
|
540
|
-
expect(anthropic.complete).not.toHaveBeenCalled();
|
|
541
|
-
});
|
|
542
|
-
it('throws when prompt specifies an unknown provider', async () => {
|
|
543
|
-
const openai = createNamedProvider('openai');
|
|
544
|
-
const promptRegistry = new PromptRegistry();
|
|
545
|
-
promptRegistry.register(definePrompt({
|
|
546
|
-
name: 'failing',
|
|
547
|
-
description: 'This will fail',
|
|
548
|
-
input: z.object({}),
|
|
549
|
-
output: z.object({ result: z.string() }),
|
|
550
|
-
model: { name: 'some-model', provider: 'nonexistent' },
|
|
551
|
-
}));
|
|
552
|
-
const service = createAIService({
|
|
553
|
-
providers: { openai },
|
|
554
|
-
defaultProvider: 'openai',
|
|
555
|
-
promptRegistry,
|
|
556
|
-
});
|
|
557
|
-
await expect(service.generate({ prompt: 'failing', input: {} })).rejects.toThrow('AI provider "nonexistent" not configured');
|
|
558
|
-
});
|
|
559
|
-
it('records provider name in cost tracker entries', async () => {
|
|
560
|
-
const anthropic = createNamedProvider('anthropic');
|
|
561
|
-
const costTracker = createCostTracker();
|
|
562
|
-
const promptRegistry = new PromptRegistry();
|
|
563
|
-
promptRegistry.register(definePrompt({
|
|
564
|
-
name: 'tracked',
|
|
565
|
-
description: 'Tracked prompt',
|
|
566
|
-
input: z.object({}),
|
|
567
|
-
output: z.object({ result: z.string() }),
|
|
568
|
-
model: { name: 'claude-sonnet', provider: 'anthropic' },
|
|
569
|
-
}));
|
|
570
|
-
const service = createAIService({
|
|
571
|
-
providers: { anthropic },
|
|
572
|
-
defaultProvider: 'anthropic',
|
|
573
|
-
promptRegistry,
|
|
574
|
-
costTracker,
|
|
575
|
-
});
|
|
576
|
-
await service.generate({ prompt: 'tracked', input: {} });
|
|
577
|
-
const records = costTracker.getRecords();
|
|
578
|
-
expect(records).toHaveLength(1);
|
|
579
|
-
expect(records[0]?.provider).toBe('anthropic');
|
|
580
|
-
});
|
|
581
|
-
it('extract and classify use default provider', async () => {
|
|
582
|
-
const openai = createNamedProvider('openai', '{"name":"Alice","age":30}');
|
|
583
|
-
const anthropic = createNamedProvider('anthropic');
|
|
584
|
-
const service = createAIService({
|
|
585
|
-
providers: { openai, anthropic },
|
|
586
|
-
defaultProvider: 'openai',
|
|
587
|
-
});
|
|
588
|
-
const schema = z.object({ name: z.string(), age: z.number() });
|
|
589
|
-
await service.extract({ schema, text: 'Alice is 30' });
|
|
590
|
-
expect(openai.complete).toHaveBeenCalled();
|
|
591
|
-
expect(anthropic.complete).not.toHaveBeenCalled();
|
|
592
|
-
});
|
|
593
|
-
});
|
|
594
|
-
describe('prompt override resolution', () => {
|
|
595
|
-
function createNamedProvider(name, content = '{"result":"ok"}') {
|
|
596
|
-
return createMockProvider({
|
|
597
|
-
name,
|
|
598
|
-
complete: vi.fn(async () => ({
|
|
599
|
-
content,
|
|
600
|
-
model: `${name}-model`,
|
|
601
|
-
usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
|
|
602
|
-
finishReason: 'stop',
|
|
603
|
-
})),
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
it('uses promptOverrides to route to a different provider', async () => {
|
|
607
|
-
const openai = createNamedProvider('openai');
|
|
608
|
-
const anthropic = createNamedProvider('anthropic', '{"result":"from-anthropic"}');
|
|
609
|
-
const promptRegistry = new PromptRegistry();
|
|
610
|
-
promptRegistry.register(definePrompt({
|
|
611
|
-
name: 'write-bio',
|
|
612
|
-
description: 'Write a bio for {{name}}',
|
|
613
|
-
input: z.object({ name: z.string() }),
|
|
614
|
-
output: z.object({ result: z.string() }),
|
|
615
|
-
model: { name: 'gpt-4o', provider: 'openai' },
|
|
616
|
-
}));
|
|
617
|
-
const service = createAIService({
|
|
618
|
-
providers: { openai, anthropic },
|
|
619
|
-
defaultProvider: 'openai',
|
|
620
|
-
promptRegistry,
|
|
621
|
-
promptOverrides: {
|
|
622
|
-
'write-bio': { provider: 'anthropic', model: 'claude-sonnet' },
|
|
623
|
-
},
|
|
624
|
-
});
|
|
625
|
-
await service.generate({ prompt: 'write-bio', input: { name: 'Alice' } });
|
|
626
|
-
expect(anthropic.complete).toHaveBeenCalled();
|
|
627
|
-
expect(openai.complete).not.toHaveBeenCalled();
|
|
628
|
-
});
|
|
629
|
-
it('uses promptOverrides model over prompt-defined model', async () => {
|
|
630
|
-
const openai = createNamedProvider('openai');
|
|
631
|
-
const promptRegistry = new PromptRegistry();
|
|
632
|
-
promptRegistry.register(definePrompt({
|
|
633
|
-
name: 'write-bio',
|
|
634
|
-
description: 'Write a bio for {{name}}',
|
|
635
|
-
input: z.object({ name: z.string() }),
|
|
636
|
-
output: z.object({ result: z.string() }),
|
|
637
|
-
model: { name: 'gpt-4o', provider: 'openai' },
|
|
638
|
-
}));
|
|
639
|
-
const service = createAIService({
|
|
640
|
-
providers: { openai },
|
|
641
|
-
defaultProvider: 'openai',
|
|
642
|
-
promptRegistry,
|
|
643
|
-
promptOverrides: {
|
|
644
|
-
'write-bio': { model: 'gpt-4o-mini' },
|
|
645
|
-
},
|
|
646
|
-
});
|
|
647
|
-
await service.generate({ prompt: 'write-bio', input: { name: 'Alice' } });
|
|
648
|
-
const call = openai.complete.mock.calls[0]?.[0];
|
|
649
|
-
expect(call?.model).toBe('gpt-4o-mini');
|
|
650
|
-
});
|
|
651
|
-
it('falls back to defaultModel when neither override nor prompt defines model', async () => {
|
|
652
|
-
const openai = createNamedProvider('openai');
|
|
653
|
-
const promptRegistry = new PromptRegistry();
|
|
654
|
-
promptRegistry.register(definePrompt({
|
|
655
|
-
name: 'simple',
|
|
656
|
-
description: 'A simple prompt',
|
|
657
|
-
input: z.object({}),
|
|
658
|
-
output: z.object({ result: z.string() }),
|
|
659
|
-
}));
|
|
660
|
-
const service = createAIService({
|
|
661
|
-
providers: { openai },
|
|
662
|
-
defaultProvider: 'openai',
|
|
663
|
-
promptRegistry,
|
|
664
|
-
defaultModel: 'gpt-4o-mini',
|
|
665
|
-
});
|
|
666
|
-
await service.generate({ prompt: 'simple', input: {} });
|
|
667
|
-
const call = openai.complete.mock.calls[0]?.[0];
|
|
668
|
-
expect(call?.model).toBe('gpt-4o-mini');
|
|
669
|
-
});
|
|
670
|
-
it('uses prompt model when no override is specified', async () => {
|
|
671
|
-
const openai = createNamedProvider('openai');
|
|
672
|
-
const promptRegistry = new PromptRegistry();
|
|
673
|
-
promptRegistry.register(definePrompt({
|
|
674
|
-
name: 'write-bio',
|
|
675
|
-
description: 'Write a bio for {{name}}',
|
|
676
|
-
input: z.object({ name: z.string() }),
|
|
677
|
-
output: z.object({ result: z.string() }),
|
|
678
|
-
model: { name: 'gpt-4o', provider: 'openai' },
|
|
679
|
-
}));
|
|
680
|
-
const service = createAIService({
|
|
681
|
-
providers: { openai },
|
|
682
|
-
defaultProvider: 'openai',
|
|
683
|
-
promptRegistry,
|
|
684
|
-
defaultModel: 'fallback-model',
|
|
685
|
-
});
|
|
686
|
-
await service.generate({ prompt: 'write-bio', input: { name: 'Alice' } });
|
|
687
|
-
const call = openai.complete.mock.calls[0]?.[0];
|
|
688
|
-
expect(call?.model).toBe('gpt-4o');
|
|
689
|
-
});
|
|
690
|
-
});
|
|
691
|
-
// ── Failure-path cost recording + onAICostRecorded hook ──
|
|
692
|
-
describe('failure-path cost recording', () => {
|
|
693
|
-
function makePromptRegistry() {
|
|
694
|
-
const reg = new PromptRegistry();
|
|
695
|
-
reg.register(definePrompt({
|
|
696
|
-
name: 'greet',
|
|
697
|
-
description: 'Say hello to {{name}}',
|
|
698
|
-
input: z.object({ name: z.string() }),
|
|
699
|
-
output: z.object({ result: z.string() }),
|
|
700
|
-
}));
|
|
701
|
-
return reg;
|
|
702
|
-
}
|
|
703
|
-
it('records status=success on the happy path with onAICostRecorded', async () => {
|
|
704
|
-
const provider = createMockProvider({
|
|
705
|
-
complete: vi.fn(async () => ({
|
|
706
|
-
content: '{"result":"hi"}',
|
|
707
|
-
model: 'mock',
|
|
708
|
-
usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
|
|
709
|
-
finishReason: 'stop',
|
|
710
|
-
})),
|
|
711
|
-
});
|
|
712
|
-
const costTracker = createCostTracker();
|
|
713
|
-
const hookCalls = [];
|
|
714
|
-
const service = createAIService(singleProviderConfig(provider, {
|
|
715
|
-
promptRegistry: makePromptRegistry(),
|
|
716
|
-
costTracker,
|
|
717
|
-
defaultModel: 'mock-model',
|
|
718
|
-
onAICostRecorded: (record, ctx) => {
|
|
719
|
-
hookCalls.push({ record, ctx });
|
|
720
|
-
},
|
|
721
|
-
}));
|
|
722
|
-
await service.generate({
|
|
723
|
-
prompt: 'greet',
|
|
724
|
-
input: { name: 'Alice' },
|
|
725
|
-
costContext: { projectId: 'p1', operationName: 'test' },
|
|
726
|
-
});
|
|
727
|
-
expect(costTracker.getRecords()).toHaveLength(1);
|
|
728
|
-
expect(costTracker.getRecords()[0]?.status).toBe('success');
|
|
729
|
-
expect(hookCalls).toHaveLength(1);
|
|
730
|
-
expect(hookCalls[0]?.record.status).toBe('success');
|
|
731
|
-
expect(hookCalls[0]?.ctx).toEqual({ projectId: 'p1', operationName: 'test' });
|
|
732
|
-
});
|
|
733
|
-
it('records status=failed with AIValidationError.usage when validation exhausts retries', async () => {
|
|
734
|
-
const provider = createMockProvider({
|
|
735
|
-
complete: vi.fn(async () => ({
|
|
736
|
-
content: '{"wrong":"schema"}',
|
|
737
|
-
model: 'mock',
|
|
738
|
-
usage: { inputTokens: 10, outputTokens: 10, totalTokens: 20 },
|
|
739
|
-
finishReason: 'stop',
|
|
740
|
-
})),
|
|
741
|
-
});
|
|
742
|
-
const costTracker = createCostTracker();
|
|
743
|
-
const hookCalls = [];
|
|
744
|
-
const service = createAIService(singleProviderConfig(provider, {
|
|
745
|
-
promptRegistry: makePromptRegistry(),
|
|
746
|
-
costTracker,
|
|
747
|
-
defaultModel: 'mock-model',
|
|
748
|
-
onAICostRecorded: (record, ctx) => {
|
|
749
|
-
hookCalls.push({ record, ctx });
|
|
750
|
-
},
|
|
751
|
-
}));
|
|
752
|
-
await expect(service.generate({
|
|
753
|
-
prompt: 'greet',
|
|
754
|
-
input: { name: 'Alice' },
|
|
755
|
-
validation: { maxRetries: 1, feedbackOnError: false },
|
|
756
|
-
costContext: { projectId: 'p2' },
|
|
757
|
-
})).rejects.toThrow(/AI output validation failed/);
|
|
758
|
-
expect(costTracker.getRecords()).toHaveLength(1);
|
|
759
|
-
const record = costTracker.getRecords()[0];
|
|
760
|
-
expect(record?.status).toBe('failed');
|
|
761
|
-
// Two attempts × 20 tokens each = 40 accumulated via AIValidationError.usage
|
|
762
|
-
expect(record?.usage.totalTokens).toBe(40);
|
|
763
|
-
expect(record?.errorMessage).toMatch(/validation/i);
|
|
764
|
-
expect(hookCalls).toHaveLength(1);
|
|
765
|
-
expect(hookCalls[0]?.record.status).toBe('failed');
|
|
766
|
-
expect(hookCalls[0]?.ctx).toEqual({ projectId: 'p2' });
|
|
767
|
-
});
|
|
768
|
-
it('records status=failed with zero usage when provider throws an opaque error', async () => {
|
|
769
|
-
const provider = createMockProvider({
|
|
770
|
-
complete: vi.fn(async () => {
|
|
771
|
-
throw new Error('Headers Timeout Error');
|
|
772
|
-
}),
|
|
773
|
-
});
|
|
774
|
-
const costTracker = createCostTracker();
|
|
775
|
-
const service = createAIService(singleProviderConfig(provider, {
|
|
776
|
-
costTracker,
|
|
777
|
-
defaultModel: 'mock-model',
|
|
778
|
-
}));
|
|
779
|
-
await expect(service.generate({ prompt: 'just a raw prompt', input: {} })).rejects.toThrow('Headers Timeout Error');
|
|
780
|
-
const records = costTracker.getRecords();
|
|
781
|
-
expect(records).toHaveLength(1);
|
|
782
|
-
expect(records[0]?.status).toBe('failed');
|
|
783
|
-
expect(records[0]?.usage.totalTokens).toBe(0);
|
|
784
|
-
expect(records[0]?.errorMessage).toContain('Headers Timeout Error');
|
|
785
|
-
});
|
|
786
|
-
it('streamGenerate records status=failed on mid-stream transport error', async () => {
|
|
787
|
-
const provider = {
|
|
788
|
-
name: 'mock',
|
|
789
|
-
complete: vi.fn(),
|
|
790
|
-
async *stream() {
|
|
791
|
-
yield { type: 'content_delta', delta: '' };
|
|
792
|
-
throw new Error('socket hang up');
|
|
793
|
-
},
|
|
794
|
-
embed: vi.fn(),
|
|
795
|
-
};
|
|
796
|
-
const costTracker = createCostTracker();
|
|
797
|
-
const service = createAIService(singleProviderConfig(provider, {
|
|
798
|
-
costTracker,
|
|
799
|
-
defaultModel: 'mock-model',
|
|
800
|
-
}));
|
|
801
|
-
const iterator = service.streamGenerate({ prompt: 'test', input: {} });
|
|
802
|
-
await expect(async () => {
|
|
803
|
-
for await (const _ of iterator) {
|
|
804
|
-
// drain
|
|
805
|
-
}
|
|
806
|
-
}).rejects.toThrow('socket hang up');
|
|
807
|
-
const records = costTracker.getRecords();
|
|
808
|
-
expect(records).toHaveLength(1);
|
|
809
|
-
expect(records[0]?.status).toBe('failed');
|
|
810
|
-
expect(records[0]?.errorMessage).toContain('socket hang up');
|
|
811
|
-
});
|
|
812
|
-
it('streamGenerate records status=failed with combined stream+fallback usage on fallback validation failure', async () => {
|
|
813
|
-
const reg = new PromptRegistry();
|
|
814
|
-
reg.register(definePrompt({
|
|
815
|
-
name: 'structured',
|
|
816
|
-
description: 'Return json',
|
|
817
|
-
input: z.object({}),
|
|
818
|
-
output: z.object({ ok: z.boolean(), name: z.string() }),
|
|
819
|
-
}));
|
|
820
|
-
const stream = vi.fn().mockImplementation(async function* () {
|
|
821
|
-
// Emit malformed JSON so local parse fails and we fall back
|
|
822
|
-
yield { type: 'content_delta', delta: '{"ok":' };
|
|
823
|
-
yield {
|
|
824
|
-
type: 'usage',
|
|
825
|
-
usage: { inputTokens: 100, outputTokens: 5, totalTokens: 105 },
|
|
826
|
-
};
|
|
827
|
-
yield { type: 'done', finishReason: 'stop' };
|
|
828
|
-
});
|
|
829
|
-
const complete = vi.fn(async () => ({
|
|
830
|
-
content: '{"still":"wrong"}',
|
|
831
|
-
model: 'mock',
|
|
832
|
-
usage: { inputTokens: 50, outputTokens: 5, totalTokens: 55 },
|
|
833
|
-
finishReason: 'stop',
|
|
834
|
-
}));
|
|
835
|
-
const provider = {
|
|
836
|
-
name: 'mock',
|
|
837
|
-
complete,
|
|
838
|
-
stream,
|
|
839
|
-
embed: vi.fn(),
|
|
840
|
-
};
|
|
841
|
-
const costTracker = createCostTracker();
|
|
842
|
-
const service = createAIService(singleProviderConfig(provider, {
|
|
843
|
-
promptRegistry: reg,
|
|
844
|
-
costTracker,
|
|
845
|
-
defaultModel: 'mock-model',
|
|
846
|
-
validation: { maxRetries: 0, feedbackOnError: false },
|
|
847
|
-
}));
|
|
848
|
-
const iterator = service.streamGenerate({ prompt: 'structured', input: {} });
|
|
849
|
-
await expect(async () => {
|
|
850
|
-
for await (const _ of iterator) {
|
|
851
|
-
// drain
|
|
852
|
-
}
|
|
853
|
-
}).rejects.toThrow(/AI output validation failed/);
|
|
854
|
-
const records = costTracker.getRecords();
|
|
855
|
-
expect(records).toHaveLength(1);
|
|
856
|
-
expect(records[0]?.status).toBe('failed');
|
|
857
|
-
// streamUsage (105) + fallback validation usage (55) = 160 total
|
|
858
|
-
expect(records[0]?.usage.totalTokens).toBe(160);
|
|
859
|
-
expect(records[0]?.usage.inputTokens).toBe(150);
|
|
860
|
-
expect(records[0]?.usage.outputTokens).toBe(10);
|
|
861
|
-
});
|
|
862
|
-
it('onAICostRecorded hook errors are swallowed and do not break the AI call', async () => {
|
|
863
|
-
const provider = createMockProvider({
|
|
864
|
-
complete: vi.fn(async () => ({
|
|
865
|
-
content: '{"result":"ok"}',
|
|
866
|
-
model: 'mock',
|
|
867
|
-
usage: { inputTokens: 5, outputTokens: 5, totalTokens: 10 },
|
|
868
|
-
finishReason: 'stop',
|
|
869
|
-
})),
|
|
870
|
-
});
|
|
871
|
-
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
872
|
-
const service = createAIService(singleProviderConfig(provider, {
|
|
873
|
-
promptRegistry: makePromptRegistry(),
|
|
874
|
-
defaultModel: 'mock-model',
|
|
875
|
-
onAICostRecorded: () => {
|
|
876
|
-
throw new Error('hook boom');
|
|
877
|
-
},
|
|
878
|
-
}));
|
|
879
|
-
const result = await service.generate({ prompt: 'greet', input: { name: 'Alice' } });
|
|
880
|
-
expect(result).toEqual({ result: 'ok' });
|
|
881
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('hook boom'));
|
|
882
|
-
consoleSpy.mockRestore();
|
|
883
|
-
});
|
|
884
|
-
});
|
|
885
|
-
// ── Text-mode brevity-hint suffix (and its opt-out) ──
|
|
886
|
-
// The ai-service auto-detects single-string-field output schemas
|
|
887
|
-
// (e.g. z.object({ content: z.string() })) and switches to plain text
|
|
888
|
-
// mode while appending a brevity-hint suffix that tells the model to
|
|
889
|
-
// "Respond with ONLY the plain text content...". That hint is the right
|
|
890
|
-
// default for short Q&A but collapses long-form prose into bullet points
|
|
891
|
-
// on some models — so prompts can opt out via
|
|
892
|
-
// `disableTextModeBrevityHint: true`.
|
|
893
|
-
describe('text-mode brevity hint', () => {
|
|
894
|
-
function makeStreamSpy() {
|
|
895
|
-
return vi.fn().mockImplementation(async function* () {
|
|
896
|
-
yield { type: 'content_delta', delta: 'short body' };
|
|
897
|
-
yield {
|
|
898
|
-
type: 'usage',
|
|
899
|
-
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
900
|
-
};
|
|
901
|
-
yield { type: 'done', finishReason: 'stop' };
|
|
902
|
-
});
|
|
903
|
-
}
|
|
904
|
-
function getStreamRequest(streamSpy) {
|
|
905
|
-
const firstCall = streamSpy.mock.calls[0];
|
|
906
|
-
const req = firstCall?.[0];
|
|
907
|
-
return req ?? {};
|
|
908
|
-
}
|
|
909
|
-
async function drain(iter) {
|
|
910
|
-
for await (const _ of iter) {
|
|
911
|
-
// drain
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
it('default text-mode (single string field, no opt-out): appends brevity-hint suffix', async () => {
|
|
915
|
-
const streamSpy = makeStreamSpy();
|
|
916
|
-
const provider = {
|
|
917
|
-
name: 'mock',
|
|
918
|
-
complete: vi.fn(),
|
|
919
|
-
stream: streamSpy,
|
|
920
|
-
embed: vi.fn(),
|
|
921
|
-
};
|
|
922
|
-
const reg = new PromptRegistry();
|
|
923
|
-
reg.register(definePrompt({
|
|
924
|
-
name: 'qa',
|
|
925
|
-
description: 'Answer the question.',
|
|
926
|
-
input: z.object({}),
|
|
927
|
-
output: z.object({ content: z.string() }),
|
|
928
|
-
}));
|
|
929
|
-
const service = createAIService(singleProviderConfig(provider, { promptRegistry: reg, defaultModel: 'mock-model' }));
|
|
930
|
-
await drain(service.streamGenerate({ prompt: 'qa', input: {} }));
|
|
931
|
-
const req = getStreamRequest(streamSpy);
|
|
932
|
-
expect(req.prompt ?? '').toContain('Respond with ONLY the plain text content');
|
|
933
|
-
expect(req.responseFormat).toBe('text');
|
|
934
|
-
});
|
|
935
|
-
it('forwards registered system prompts on streaming calls', async () => {
|
|
936
|
-
const streamSpy = makeStreamSpy();
|
|
937
|
-
const provider = {
|
|
938
|
-
name: 'mock',
|
|
939
|
-
complete: vi.fn(),
|
|
940
|
-
stream: streamSpy,
|
|
941
|
-
embed: vi.fn(),
|
|
942
|
-
};
|
|
943
|
-
const reg = new PromptRegistry();
|
|
944
|
-
reg.register(definePrompt({
|
|
945
|
-
name: 'stream-system',
|
|
946
|
-
system: 'You are {{role}}.',
|
|
947
|
-
description: 'Write about {{topic}}',
|
|
948
|
-
input: z.object({
|
|
949
|
-
role: z.string(),
|
|
950
|
-
topic: z.string(),
|
|
951
|
-
locale: z.string(),
|
|
952
|
-
}),
|
|
953
|
-
output: z.object({ content: z.string() }),
|
|
954
|
-
disableTextModeBrevityHint: true,
|
|
955
|
-
}));
|
|
956
|
-
const service = createAIService(singleProviderConfig(provider, { promptRegistry: reg, defaultModel: 'mock-model' }));
|
|
957
|
-
await drain(service.streamGenerate({
|
|
958
|
-
prompt: 'stream-system',
|
|
959
|
-
input: { role: 'editor', topic: 'Baghdad', locale: 'he' },
|
|
960
|
-
}));
|
|
961
|
-
const req = getStreamRequest(streamSpy);
|
|
962
|
-
expect(req.system).toBe('You are editor.');
|
|
963
|
-
expect(req.prompt).toBe('Write about Baghdad\n\nInput: {"locale":"he"}');
|
|
964
|
-
expect(req.responseFormat).toBe('text');
|
|
965
|
-
});
|
|
966
|
-
it('text-mode with disableTextModeBrevityHint=true: omits brevity-hint suffix but stays in text mode', async () => {
|
|
967
|
-
const streamSpy = makeStreamSpy();
|
|
968
|
-
const provider = {
|
|
969
|
-
name: 'mock',
|
|
970
|
-
complete: vi.fn(),
|
|
971
|
-
stream: streamSpy,
|
|
972
|
-
embed: vi.fn(),
|
|
973
|
-
};
|
|
974
|
-
const reg = new PromptRegistry();
|
|
975
|
-
reg.register(definePrompt({
|
|
976
|
-
name: 'long-form',
|
|
977
|
-
description: 'Write a long chapter. Output the title on the first line as a markdown H1.',
|
|
978
|
-
input: z.object({}),
|
|
979
|
-
output: z.object({ content: z.string() }),
|
|
980
|
-
disableTextModeBrevityHint: true,
|
|
981
|
-
}));
|
|
982
|
-
const service = createAIService(singleProviderConfig(provider, { promptRegistry: reg, defaultModel: 'mock-model' }));
|
|
983
|
-
await drain(service.streamGenerate({ prompt: 'long-form', input: {} }));
|
|
984
|
-
const req = getStreamRequest(streamSpy);
|
|
985
|
-
const sent = req.prompt ?? '';
|
|
986
|
-
expect(sent).not.toContain('Respond with ONLY the plain text content');
|
|
987
|
-
expect(sent).not.toContain('Do NOT wrap your response in JSON');
|
|
988
|
-
// Body must still be there — only the appended suffix is suppressed
|
|
989
|
-
expect(sent).toContain('Write a long chapter');
|
|
990
|
-
// And we are still in text mode (the whole point of the opt-out is to
|
|
991
|
-
// keep the streamed payload as raw text so JSON-escape rules never
|
|
992
|
-
// apply — e.g. for Hebrew content with embedded gershayim).
|
|
993
|
-
expect(req.responseFormat).toBe('text');
|
|
994
|
-
});
|
|
995
|
-
it('json-mode (multi-field schema) is unaffected by the opt-out flag: JSON instruction still appended', async () => {
|
|
996
|
-
const streamSpy = vi.fn().mockImplementation(async function* () {
|
|
997
|
-
yield { type: 'content_delta', delta: '{"a":"x","b":"y"}' };
|
|
998
|
-
yield {
|
|
999
|
-
type: 'usage',
|
|
1000
|
-
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
1001
|
-
};
|
|
1002
|
-
yield { type: 'done', finishReason: 'stop' };
|
|
1003
|
-
});
|
|
1004
|
-
const provider = {
|
|
1005
|
-
name: 'mock',
|
|
1006
|
-
complete: vi.fn(),
|
|
1007
|
-
stream: streamSpy,
|
|
1008
|
-
embed: vi.fn(),
|
|
1009
|
-
};
|
|
1010
|
-
const reg = new PromptRegistry();
|
|
1011
|
-
reg.register(definePrompt({
|
|
1012
|
-
name: 'multi',
|
|
1013
|
-
description: 'Return two fields.',
|
|
1014
|
-
input: z.object({}),
|
|
1015
|
-
output: z.object({ a: z.string(), b: z.string() }),
|
|
1016
|
-
// Even with the flag set, multi-field schemas are not text-mode
|
|
1017
|
-
// candidates, so this flag is a no-op for them.
|
|
1018
|
-
disableTextModeBrevityHint: true,
|
|
1019
|
-
}));
|
|
1020
|
-
const service = createAIService(singleProviderConfig(provider, { promptRegistry: reg, defaultModel: 'mock-model' }));
|
|
1021
|
-
await drain(service.streamGenerate({ prompt: 'multi', input: {} }));
|
|
1022
|
-
const req = getStreamRequest(streamSpy);
|
|
1023
|
-
expect(req.responseFormat).toBe('json');
|
|
1024
|
-
expect(req.prompt ?? '').toContain('Respond with a valid JSON object');
|
|
1025
|
-
// And the text-mode hint is definitely NOT in a JSON-mode call.
|
|
1026
|
-
expect(req.prompt ?? '').not.toContain('Respond with ONLY the plain text content');
|
|
1027
|
-
});
|
|
1028
|
-
});
|
|
1029
|
-
// ── Backward-compat: record() input type keeps `status` optional ──
|
|
1030
|
-
describe('CostTracker backward-compat', () => {
|
|
1031
|
-
it('record(entry) without status still compiles and defaults to success', () => {
|
|
1032
|
-
// This test's real value is type-level: if `status` stops being
|
|
1033
|
-
// optional on AICostRecordInput, this file fails to compile and
|
|
1034
|
-
// every external consumer of CostTracker.record breaks.
|
|
1035
|
-
const tracker = createCostTracker();
|
|
1036
|
-
const entry = {
|
|
1037
|
-
model: 'm',
|
|
1038
|
-
provider: 'p',
|
|
1039
|
-
operation: 'generate',
|
|
1040
|
-
usage: { inputTokens: 1, outputTokens: 2, totalTokens: 3 },
|
|
1041
|
-
cost: null,
|
|
1042
|
-
latencyMs: 0,
|
|
1043
|
-
};
|
|
1044
|
-
tracker.record(entry);
|
|
1045
|
-
expect(tracker.getRecords()).toHaveLength(1);
|
|
1046
|
-
expect(tracker.getRecords()[0]?.status).toBe('success');
|
|
1047
|
-
});
|
|
1048
|
-
it('a consumer-supplied CostTracker mock without status handling still satisfies the interface', () => {
|
|
1049
|
-
// Consumers writing their own tracker mocks (e.g. in framework tests)
|
|
1050
|
-
// should not have to know about the new `status` field.
|
|
1051
|
-
const seen = [];
|
|
1052
|
-
const tracker = {
|
|
1053
|
-
record(entry) {
|
|
1054
|
-
seen.push(entry);
|
|
1055
|
-
},
|
|
1056
|
-
checkBudget() {
|
|
1057
|
-
return { allowed: true };
|
|
1058
|
-
},
|
|
1059
|
-
getDailyUsage() {
|
|
1060
|
-
return { totalTokens: 0, totalCost: 0, costAvailable: false, requestCount: 0 };
|
|
1061
|
-
},
|
|
1062
|
-
getRecords() {
|
|
1063
|
-
return [];
|
|
1064
|
-
},
|
|
1065
|
-
};
|
|
1066
|
-
tracker.record({
|
|
1067
|
-
model: 'm',
|
|
1068
|
-
provider: 'p',
|
|
1069
|
-
operation: 'generate',
|
|
1070
|
-
usage: { inputTokens: 1, outputTokens: 2, totalTokens: 3 },
|
|
1071
|
-
cost: null,
|
|
1072
|
-
latencyMs: 0,
|
|
1073
|
-
});
|
|
1074
|
-
expect(seen).toHaveLength(1);
|
|
1075
|
-
});
|
|
1076
|
-
});
|
|
1077
|
-
});
|
|
1078
|
-
//# sourceMappingURL=ai-service.test.js.map
|