tests, pr comments

This commit is contained in:
Martin McKeaveney 2024-10-14 18:38:34 +01:00
parent cf8f7db36d
commit 30215ea9be
7 changed files with 187 additions and 21 deletions

View File

@ -28,6 +28,7 @@ export enum Config {
OIDC = "oidc", OIDC = "oidc",
OIDC_LOGOS = "logos_oidc", OIDC_LOGOS = "logos_oidc",
SCIM = "scim", SCIM = "scim",
AI = "AI",
} }
export const MIN_VALID_DATE = new Date(-2147483647000) export const MIN_VALID_DATE = new Date(-2147483647000)

View File

@ -102,6 +102,14 @@ export const useAppBuilders = () => {
return useFeature(Feature.APP_BUILDERS) return useFeature(Feature.APP_BUILDERS)
} }
export const useBudibaseAI = () => {
return useFeature(Feature.BUDIBASE_AI)
}
export const useAICustomConfigs = () => {
return useFeature(Feature.AI_CUSTOM_CONFIGS)
}
// QUOTAS // QUOTAS
export const setAutomationLogsQuota = (value: number) => { export const setAutomationLogsQuota = (value: number) => {

View File

@ -26,7 +26,9 @@
$: style = makeStyle($menu) $: style = makeStyle($menu)
$: isNewRow = $focusedRowId === NewRowID $: isNewRow = $focusedRowId === NewRowID
$: budibaseAIEnabled = $config.licensing?.budibaseAIEnabled || $config.licensing?.customAIConfigsEnabled $: budibaseAIEnabled =
$config.licensing?.budibaseAIEnabled ||
$config.licensing?.customAIConfigsEnabled
const makeStyle = menu => { const makeStyle = menu => {
return `left:${menu.left}px; top:${menu.top}px;` return `left:${menu.left}px; top:${menu.top}px;`

@ -1 +1 @@
Subproject commit fc4c7f4925139af078480217965c3d6338dc0a7f Subproject commit a55a58fc96291ae021e61ad369a2077992fddfcd

View File

@ -1,10 +1,6 @@
import * as setup from "./utilities" import * as setup from "./utilities"
import { import { DatabaseName, knexClient } from "../../../integrations/tests/utils"
DatabaseName,
getDatasource,
knexClient,
} from "../../../integrations/tests/utils"
import tk from "timekeeper" import tk from "timekeeper"
import emitter from "../../../../src/events" import emitter from "../../../../src/events"
@ -12,36 +8,37 @@ import { outputProcessing } from "../../../utilities/rowProcessor"
import { import {
context, context,
InternalTable, InternalTable,
setEnv as setCoreEnv,
tenancy, tenancy,
withEnv as withCoreEnv, withEnv as withCoreEnv,
setEnv as setCoreEnv,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { import {
AIOperationEnum,
AttachmentFieldMetadata, AttachmentFieldMetadata,
AutoFieldSubType, AutoFieldSubType,
BBReferenceFieldSubType,
Datasource, Datasource,
DateFieldMetadata, DateFieldMetadata,
DeleteRow, DeleteRow,
FeatureFlag,
FieldSchema, FieldSchema,
FieldType, FieldType,
BBReferenceFieldSubType,
FormulaType, FormulaType,
INTERNAL_TABLE_SOURCE_ID, INTERNAL_TABLE_SOURCE_ID,
JsonFieldSubType,
NumberFieldMetadata, NumberFieldMetadata,
QuotaUsageType, QuotaUsageType,
RelationSchemaField,
RelationshipType, RelationshipType,
Row, Row,
RowExportFormat,
SaveTableRequest, SaveTableRequest,
StaticQuotaName, StaticQuotaName,
Table, Table,
TableSchema,
TableSourceType, TableSourceType,
UpdatedRowEventEmitter, UpdatedRowEventEmitter,
TableSchema,
JsonFieldSubType,
RowExportFormat,
FeatureFlag,
RelationSchemaField,
} from "@budibase/types" } from "@budibase/types"
import { generator, mocks } from "@budibase/backend-core/tests" import { generator, mocks } from "@budibase/backend-core/tests"
import _, { merge } from "lodash" import _, { merge } from "lodash"
@ -51,6 +48,18 @@ import { InternalTables } from "../../../db/utils"
import { withEnv } from "../../../environment" import { withEnv } from "../../../environment"
import { JsTimeoutError } from "@budibase/string-templates" import { JsTimeoutError } from "@budibase/string-templates"
jest.mock("@budibase/pro", () => ({
...jest.requireActual("@budibase/pro"),
ai: {
LargeLanguageModel: {
forCurrentTenant: async () => ({
run: jest.fn(() => `Mock LLM Response`),
buildPromptFromAIOperation: jest.fn(),
}),
},
},
}))
const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString() const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString()
tk.freeze(timestamp) tk.freeze(timestamp)
interface WaitOptions { interface WaitOptions {
@ -77,13 +86,13 @@ async function waitForEvent(
} }
describe.each([ describe.each([
["lucene", undefined], // ["lucene", undefined],
["sqs", undefined], ["sqs", undefined],
[DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], // [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)],
[DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], // [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)],
[DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], // [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)],
[DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], // [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)],
[DatabaseName.ORACLE, getDatasource(DatabaseName.ORACLE)], // [DatabaseName.ORACLE, getDatasource(DatabaseName.ORACLE)],
])("/rows (%s)", (providerType, dsProvider) => { ])("/rows (%s)", (providerType, dsProvider) => {
const isInternal = dsProvider === undefined const isInternal = dsProvider === undefined
const isLucene = providerType === "lucene" const isLucene = providerType === "lucene"
@ -1986,6 +1995,7 @@ describe.each([
[FieldType.ATTACHMENT_SINGLE]: setup.structures.basicAttachment(), [FieldType.ATTACHMENT_SINGLE]: setup.structures.basicAttachment(),
[FieldType.FORMULA]: undefined, // generated field [FieldType.FORMULA]: undefined, // generated field
[FieldType.AUTO]: undefined, // generated field [FieldType.AUTO]: undefined, // generated field
[FieldType.AI]: undefined, // generated field
[FieldType.JSON]: { name: generator.guid() }, [FieldType.JSON]: { name: generator.guid() },
[FieldType.INTERNAL]: generator.guid(), [FieldType.INTERNAL]: generator.guid(),
[FieldType.BARCODEQR]: generator.guid(), [FieldType.BARCODEQR]: generator.guid(),
@ -2016,6 +2026,7 @@ describe.each([
url: expect.any(String), url: expect.any(String),
}), }),
[FieldType.FORMULA]: fullSchema[FieldType.FORMULA].formula, [FieldType.FORMULA]: fullSchema[FieldType.FORMULA].formula,
[FieldType.AI]: fullSchema[FieldType.AI].prompt,
[FieldType.AUTO]: expect.any(Number), [FieldType.AUTO]: expect.any(Number),
[FieldType.JSON]: rowValues[FieldType.JSON], [FieldType.JSON]: rowValues[FieldType.JSON],
[FieldType.INTERNAL]: rowValues[FieldType.INTERNAL], [FieldType.INTERNAL]: rowValues[FieldType.INTERNAL],
@ -2880,6 +2891,57 @@ describe.each([
) )
}) })
isSqs &&
describe("AI fields", () => {
let table: Table
beforeAll(async () => {
mocks.licenses.useBudibaseAI()
mocks.licenses.useAICustomConfigs()
table = await config.api.table.save(
saveTableRequest({
schema: {
ai: {
name: "ai",
type: FieldType.AI,
operation: AIOperationEnum.PROMPT,
prompt: "Convert the following to German: '{{ product }}'",
},
product: {
name: "product",
type: FieldType.STRING,
},
},
})
)
await config.api.row.save(table._id!, {
product: generator.word(),
})
})
afterAll(() => {
jest.unmock("@budibase/pro")
})
it("should be able to save a row with an AI column", async () => {
const { rows } = await config.api.row.search(table._id!)
expect(rows.length).toBe(1)
expect(rows[0].ai).toEqual("Mock LLM Response")
})
it("should be able to update a row with an AI column", async () => {
const { rows } = await config.api.row.search(table._id!)
expect(rows.length).toBe(1)
await config.api.row.save(table._id!, {
product: generator.word(),
...rows[0],
})
expect(rows.length).toBe(1)
expect(rows[0].ai).toEqual("Mock LLM Response")
})
})
describe("Formula fields", () => { describe("Formula fields", () => {
let table: Table let table: Table
let otherTable: Table let otherTable: Table

View File

@ -17,6 +17,7 @@ import {
import * as setup from "./utilities" import * as setup from "./utilities"
import { import {
AIOperationEnum,
AutoFieldSubType, AutoFieldSubType,
BBReferenceFieldSubType, BBReferenceFieldSubType,
Datasource, Datasource,
@ -41,11 +42,23 @@ import tk from "timekeeper"
import { encodeJSBinding } from "@budibase/string-templates" import { encodeJSBinding } from "@budibase/string-templates"
import { dataFilters } from "@budibase/shared-core" import { dataFilters } from "@budibase/shared-core"
import { Knex } from "knex" import { Knex } from "knex"
import { generator, structures } from "@budibase/backend-core/tests" import { generator, structures, mocks } from "@budibase/backend-core/tests"
import { DEFAULT_EMPLOYEE_TABLE_SCHEMA } from "../../../db/defaultData/datasource_bb_default" import { DEFAULT_EMPLOYEE_TABLE_SCHEMA } from "../../../db/defaultData/datasource_bb_default"
import { generateRowIdField } from "../../../integrations/utils" import { generateRowIdField } from "../../../integrations/utils"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
jest.mock("@budibase/pro", () => ({
...jest.requireActual("@budibase/pro"),
ai: {
LargeLanguageModel: {
forCurrentTenant: async () => ({
run: jest.fn(() => `Mock LLM Response`),
buildPromptFromAIOperation: jest.fn(),
}),
},
},
}))
describe.each([ describe.each([
["in-memory", undefined], ["in-memory", undefined],
["lucene", undefined], ["lucene", undefined],
@ -1606,6 +1619,79 @@ describe.each([
}) })
}) })
isSqs &&
describe("AI Column", () => {
const UNEXISTING_AI_COLUMN = "Real LLM Response"
beforeAll(async () => {
mocks.licenses.useBudibaseAI()
mocks.licenses.useAICustomConfigs()
tableOrViewId = await createTableOrView({
product: { name: "product", type: FieldType.STRING },
ai: {
name: "AI",
type: FieldType.AI,
operation: AIOperationEnum.PROMPT,
prompt: "Translate '{{ product }}' into German",
},
})
await createRows([{ product: "Big Mac" }, { product: "McCrispy" }])
})
describe("equal", () => {
it("successfully finds rows based on AI column", async () => {
await expectQuery({
equal: { ai: "Mock LLM Response" },
}).toContainExactly([
{ product: "Big Mac" },
{ product: "McCrispy" },
])
})
it("fails to find nonexistent row", async () => {
await expectQuery({
equal: { ai: UNEXISTING_AI_COLUMN },
}).toFindNothing()
})
})
describe("notEqual", () => {
it("Returns nothing when searching notEqual on the mock AI response", async () => {
await expectQuery({
notEqual: { ai: "Mock LLM Response" },
}).toContainExactly([])
})
it("return all when requesting non-existing response", async () => {
await expectQuery({
notEqual: { ai: "Real LLM Response" },
}).toContainExactly([
{ product: "Big Mac" },
{ product: "McCrispy" },
])
})
})
describe("oneOf", () => {
it("successfully finds a row", async () => {
await expectQuery({
oneOf: { ai: ["Mock LLM Response", "Other LLM Response"] },
}).toContainExactly([
{ product: "Big Mac" },
{ product: "McCrispy" },
])
})
it("fails to find nonexistent row", async () => {
await expectQuery({
oneOf: { ai: ["Whopper"] },
}).toFindNothing()
})
})
})
describe.each([FieldType.ARRAY, FieldType.OPTIONS])("%s", () => { describe.each([FieldType.ARRAY, FieldType.OPTIONS])("%s", () => {
beforeAll(async () => { beforeAll(async () => {
tableOrViewId = await createTableOrView({ tableOrViewId = await createTableOrView({

View File

@ -7,6 +7,7 @@ import {
TRIGGER_DEFINITIONS, TRIGGER_DEFINITIONS,
} from "../../automations" } from "../../automations"
import { import {
AIOperationEnum,
Automation, Automation,
AutomationActionStepId, AutomationActionStepId,
AutomationResults, AutomationResults,
@ -666,6 +667,12 @@ export function fullSchemaWithoutLinks({
presence: allRequired, presence: allRequired,
}, },
}, },
[FieldType.AI]: {
name: "ai",
type: FieldType.AI,
operation: AIOperationEnum.PROMPT,
prompt: "Translate this into German :'{{ product }}'",
},
[FieldType.BARCODEQR]: { [FieldType.BARCODEQR]: {
name: "barcodeqr", name: "barcodeqr",
type: FieldType.BARCODEQR, type: FieldType.BARCODEQR,