diff --git a/.github/actions/deploy/deploy.mjs b/.github/actions/deploy/deploy.mjs index 3059feabbf0fb..c78af288654d9 100644 --- a/.github/actions/deploy/deploy.mjs +++ b/.github/actions/deploy/deploy.mjs @@ -18,6 +18,8 @@ const { COPILOT_OPENAI_API_KEY, COPILOT_FAL_API_KEY, COPILOT_UNSPLASH_API_KEY, + SLACK_BOT_TOKEN, + RELEASE_SLACK_CHANNEL_ID, MAILER_SENDER, MAILER_USER, MAILER_PASSWORD, @@ -148,6 +150,8 @@ const createHelmCommand = ({ isDryRun }) => { `--set-string graphql.app.copilot.openai.key="${COPILOT_OPENAI_API_KEY}"`, `--set-string graphql.app.copilot.fal.key="${COPILOT_FAL_API_KEY}"`, `--set-string graphql.app.copilot.unsplash.key="${COPILOT_UNSPLASH_API_KEY}"`, + `--set-string graphql.app.copilot.slack.botToken="${SLACK_BOT_TOKEN}"`, + `--set-string graphql.app.copilot.slack.channelId="${RELEASE_SLACK_CHANNEL_ID}"`, `--set-string graphql.app.mailer.sender="${MAILER_SENDER}"`, `--set-string graphql.app.mailer.user="${MAILER_USER}"`, `--set-string graphql.app.mailer.password="${MAILER_PASSWORD}"`, diff --git a/.github/helm/affine/charts/graphql/templates/copilot-secret.yaml b/.github/helm/affine/charts/graphql/templates/copilot-secret.yaml index c4d93dc773d23..14c0a2fcebca8 100644 --- a/.github/helm/affine/charts/graphql/templates/copilot-secret.yaml +++ b/.github/helm/affine/charts/graphql/templates/copilot-secret.yaml @@ -8,4 +8,6 @@ data: openaiSecret: {{ .Values.app.copilot.openai.key | b64enc }} falSecret: {{ .Values.app.copilot.fal.key | b64enc }} unsplashSecret: {{ .Values.app.copilot.unsplash.key | b64enc }} + slackBotToken: {{ .Values.app.copilot.slack.botToken | b64enc }} + slackChannelId: {{ .Values.app.copilot.slack.channelId | b64enc }} {{- end }} diff --git a/.github/helm/affine/charts/graphql/templates/copilot-test.yaml b/.github/helm/affine/charts/graphql/templates/copilot-test.yaml new file mode 100644 index 0000000000000..64fa3c1754ede --- /dev/null +++ b/.github/helm/affine/charts/graphql/templates/copilot-test.yaml @@ -0,0 +1,66 @@ +{{ if .Values.app.copilot.enabled }} +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{ include "graphql.fullname" . }}-copilot-test + labels: + {{- include "graphql.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-weight": "1" + "helm.sh/hook-delete-policy": before-hook-creation +spec: + schedule: "0 8 * * *" + jobTemplate: + spec: + template: + spec: + serviceAccountName: {{ include "graphql.serviceAccountName" . }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + command: ["yarn", "test:copilot:e2e:cron"] + env: + - name: AFFINE_ENV + value: "{{ .Release.Namespace }}" + - name: SLACK_BOT_TOKEN + valueFrom: + secretKeyRef: + name: "{{ .Values.app.copilot.secretName }}" + key: slackBotToken + - name: CHANNEL_ID + valueFrom: + secretKeyRef: + name: "{{ .Values.app.copilot.secretName }}" + key: slackChannelId + - name: COPILOT_E2E_ENDPOINT + value: "http://{{ include "graphql.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:3000" + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: pg-postgresql + key: postgres-password + - name: DATABASE_URL + value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.url }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }} + - name: COPILOT_OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: "{{ .Values.app.copilot.secretName }}" + key: openaiSecret + - name: COPILOT_FAL_API_KEY + valueFrom: + secretKeyRef: + name: "{{ .Values.app.copilot.secretName }}" + key: falSecret + - name: COPILOT_UNSPLASH_API_KEY + valueFrom: + secretKeyRef: + name: "{{ .Values.app.copilot.secretName }}" + key: unsplashSecret + resources: + requests: + cpu: '100m' + memory: '200Mi' + restartPolicy: Never + backoffLimit: 1 +{{ end }} diff --git a/.github/workflows/copilot-test.yml b/.github/workflows/copilot-test.yml index e24df7193623d..2a98bf1ed8e40 100644 --- a/.github/workflows/copilot-test.yml +++ b/.github/workflows/copilot-test.yml @@ -182,7 +182,7 @@ jobs: if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} run: node ./tools/copilot-result/index.js env: - CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }} + CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHANNEL_ID }} SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} BRANCH_SHA: ${{ github.sha }} BRANCH_NAME: ${{ github.ref }} @@ -192,7 +192,7 @@ jobs: if: ${{ always() && contains(needs.*.result, 'failure') }} run: node ./tools/copilot-result/index.js env: - CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }} + CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHANNEL_ID }} SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} BRANCH_SHA: ${{ github.sha }} BRANCH_NAME: ${{ github.ref }} @@ -202,7 +202,7 @@ jobs: if: ${{ always() && contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'failure') }} run: node ./tools/copilot-result/index.js env: - CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }} + CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHANNEL_ID }} SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} BRANCH_SHA: ${{ github.sha }} BRANCH_NAME: ${{ github.ref }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7198f08179878..527c7b2b6888f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -99,6 +99,9 @@ jobs: COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }} COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }} COPILOT_UNSPLASH_API_KEY: ${{ secrets.COPILOT_UNSPLASH_API_KEY }} + # used for slack notifications + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + RELEASE_SLACK_CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHANNEL_ID }} METRICS_CUSTOMER_IO_TOKEN: ${{ secrets.METRICS_CUSTOMER_IO_TOKEN }} MAILER_SENDER: ${{ secrets.OAUTH_EMAIL_SENDER }} MAILER_USER: ${{ secrets.OAUTH_EMAIL_LOGIN }} @@ -161,7 +164,7 @@ jobs: if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} run: node ./tools/changelog/index.js env: - CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }} + CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHANNEL_ID }} SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} DEPLOYED_URL: ${{ steps.set_info.outputs.deployed_url }} PREV_VERSION: ${{ needs.output-prev-version.outputs.prev }} @@ -177,7 +180,7 @@ jobs: method: chat.postMessage token: ${{ secrets.SLACK_BOT_TOKEN }} payload: | - channel: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }} + channel: ${{ secrets.RELEASE_SLACK_CHANNEL_ID }} text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy failed `${{ github.event.inputs.flavor }}`>" blocks: - type: section @@ -192,7 +195,7 @@ jobs: token: ${{ secrets.SLACK_BOT_TOKEN }} method: chat.postMessage payload: | - channel: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }} + channel: ${{ secrets.RELEASE_SLACK_CHANNEL_ID }} text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy cancelled `${{ github.event.inputs.flavor }}`>" blocks: - type: section diff --git a/packages/backend/native/index.d.ts b/packages/backend/native/index.d.ts index de691213ec201..c5d2e4e1d0cb1 100644 --- a/packages/backend/native/index.d.ts +++ b/packages/backend/native/index.d.ts @@ -19,4 +19,3 @@ export declare function mergeUpdatesInApplyWay(updates: Array): Buffer export declare function mintChallengeResponse(resource: string, bits?: number | undefined | null): Promise export declare function verifyChallengeResponse(response: string, bits: number, resource: string): Promise - diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index cfa3fff70549c..29cf4df04eba2 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -12,11 +12,12 @@ "start": "node --loader ts-node/esm/transpile-only.mjs ./src/index.ts", "dev": "nodemon ./src/index.ts", "test": "ava --concurrency 1 --serial", - "test:copilot:e2e": "ava \"e2e/copilot.e2e.ts\"", + "test:copilot:e2e": "ava \"tests/**/copilot-*.e2e.ts\"", "test:copilot:spec": "ava \"tests/**/copilot-*.spec.ts\"", "test:coverage": "c8 ava --concurrency 1 --serial", - "test:copilot:e2e:coverage": "c8 ava --timeout=5m \"e2e/copilot.e2e.ts\"", + "test:copilot:e2e:coverage": "c8 ava --timeout=5m \"tests/**/copilot-*.e2e.ts\"", "test:copilot:spec:coverage": "c8 ava --timeout=5m \"tests/**/copilot-*.spec.ts\"", + "test:copilot:e2e:cron": "node ./scripts/copilot-cron-test.js", "postinstall": "prisma generate", "data-migration": "NODE_ENV=script node --loader ts-node/esm/transpile-only.mjs ./src/data/index.ts", "predeploy": "yarn prisma migrate deploy && node --import ./scripts/register.js ./dist/data/index.js run" @@ -58,7 +59,9 @@ "@opentelemetry/semantic-conventions": "^1.28.0", "@prisma/client": "^5.22.0", "@prisma/instrumentation": "^5.22.0", + "@slack/web-api": "^7.3.4", "@socket.io/redis-adapter": "^8.3.0", + "ava": "^6.1.2", "cookie-parser": "^1.4.7", "dotenv": "^16.4.7", "express": "^4.21.2", @@ -70,8 +73,10 @@ "html-validate": "^8.27.0", "ioredis": "^5.4.1", "is-mobile": "^5.0.0", + "jsx-slack": "^6.1.1", "keyv": "^5.2.2", "lodash-es": "^4.17.21", + "marked": "^15.0.0", "mixpanel": "^0.18.0", "mustache": "^4.2.0", "nanoid": "^5.0.9", @@ -87,6 +92,8 @@ "ses": "^1.10.0", "socket.io": "^4.8.1", "stripe": "^17.4.0", + "supertest": "^7.0.0", + "tap-parser": "^18.0.0", "ts-node": "^10.9.2", "typescript": "^5.7.2", "yjs": "patch:yjs@npm%3A13.6.18#~/.yarn/patches/yjs-npm-13.6.18-ad0d5f7c43.patch", @@ -108,11 +115,9 @@ "@types/on-headers": "^1.0.3", "@types/sinon": "^17.0.3", "@types/supertest": "^6.0.2", - "ava": "^6.2.0", "c8": "^10.1.3", "nodemon": "^3.1.7", - "sinon": "^19.0.2", - "supertest": "^7.0.0" + "sinon": "^19.0.2" }, "ava": { "timeout": "1m", diff --git a/packages/backend/server/scripts/copilot-cron-test.js b/packages/backend/server/scripts/copilot-cron-test.js new file mode 100644 index 0000000000000..915b68bd79d13 --- /dev/null +++ b/packages/backend/server/scripts/copilot-cron-test.js @@ -0,0 +1,99 @@ +// start process + +import { spawn } from 'node:child_process'; + +import { WebClient } from '@slack/web-api'; +import { jsxslack } from 'jsx-slack'; +import { marked, Renderer } from 'marked'; +import { Parser } from 'tap-parser'; + +async function runTest() { + const tester = new Promise(resolve => { + const test = spawn( + 'npx', + [ + 'ava', + '--config', + 'tests/ava.docker.config.js', + 'tests/**/copilot-*.e2e.ts', + '--tap', + ], + { env: { ...process.env, NODE_NO_WARNINGS: 1 } } + ); + + const parser = new Parser(); + test.stdout.on('data', data => { + console.log(data.toString()); + parser.write(data); + }); + + test.on('close', _ => { + const failures = parser?.failures.filter(f => !!f.fullname); + const timeouts = parser?.failures.filter(f => !f.fullname); + const result = [ + `${parser.results.pass} passed`, + `${parser.results.fail - timeouts.length} failed`, + `${timeouts.length} timeouts`, + `${parser.results.skip} skipped`, + ]; + const report = [ + `Test finished with ${result.join(', ')}.`, + failures?.length > 0 + ? `Failed tests: \n\n${failures.map(failure => `- ${failure.fullname}`).join('\n')}` + : '', + ]; + resolve(report.join('\n\n')); + }); + }); + + try { + return await tester; + } catch (e) { + return e.message; + } +} + +function render(markdown) { + const rendered = marked(markdown, { + renderer: new (class CustomRenderer extends Renderer { + heading({ tokens }) { + return ` + +
${tokens[0].text}
+ +
`; + } + + paragraph({ tokens }) { + return `

${tokens[0].text}

`; + } + + list(token) { + return `
${super.list(token)}
`; + } + + hr() { + return ``; + } + })(), + }); + return jsxslack([`${rendered}`]); +} + +async function main() { + const { CHANNEL_ID, SLACK_BOT_TOKEN, AFFINE_ENV } = process.env; + + const report = await runTest(); + const blocks = render( + [`# AFFiNE Copilot Test ${AFFINE_ENV} Env Test Result`, report].join('\n\n') + ); + const { ok } = await new WebClient(SLACK_BOT_TOKEN).chat.postMessage({ + channel: CHANNEL_ID, + text: `AFFiNE Copilot Test ${AFFINE_ENV} Env Test Result`, + blocks, + }); + + console.assert(ok, 'Failed to send a message to Slack'); +} + +await main(); diff --git a/packages/backend/server/tests/ava.docker.config.js b/packages/backend/server/tests/ava.docker.config.js new file mode 100644 index 0000000000000..832ff81f3b290 --- /dev/null +++ b/packages/backend/server/tests/ava.docker.config.js @@ -0,0 +1,9 @@ +import packageJson from '../package.json' with { type: 'json' }; + +export default { + ...packageJson.ava, + environmentVariables: { + ...packageJson.ava.environmentVariables, + TS_NODE_PROJECT: './tests/tsconfig.docker.json', + }, +}; diff --git a/packages/backend/server/tests/copilot-provider.e2e.ts b/packages/backend/server/tests/copilot-provider.e2e.ts index 530db68d3e8e1..ae10f645e0ab9 100644 --- a/packages/backend/server/tests/copilot-provider.e2e.ts +++ b/packages/backend/server/tests/copilot-provider.e2e.ts @@ -1,10 +1,10 @@ -import { randomUUID } from 'node:crypto'; +import { randomInt, randomUUID } from 'node:crypto'; -import { createRandomAIUser } from '@affine-test/kit/utils/cloud'; +import { hash } from '@node-rs/argon2'; import type { ExecutionContext, TestFn } from 'ava'; import ava from 'ava'; +import { z } from 'zod'; -import { createWorkspace } from './utils'; import { chatWithImages, chatWithText, @@ -15,6 +15,7 @@ import { ProviderWorkflowTestCase, sse2array, } from './utils/copilot'; +import { createWorkspace } from './utils/workspace'; type Tester = { app: any; @@ -47,20 +48,14 @@ const runIfCopilotConfigured = test.macro( } ); -export const runPrisma = async ( +const runPrisma = async ( cb: ( prisma: InstanceType< - // eslint-disable-next-line @typescript-eslint/consistent-type-imports - typeof import('../../../../packages/backend/server/node_modules/@prisma/client').PrismaClient + typeof import('../node_modules/@prisma/client').PrismaClient > ) => Promise ): Promise => { - const { - PrismaClient, - // eslint-disable-next-line @typescript-eslint/no-var-requires - } = await import( - '../../../../packages/backend/server/node_modules/@prisma/client' - ); + const { PrismaClient } = await import('../node_modules/@prisma/client'); const client = new PrismaClient(); await client.$connect(); try { @@ -70,14 +65,99 @@ export const runPrisma = async ( } }; +const cloudUserSchema = z.object({ + id: z.string(), + name: z.string(), + email: z.string().email(), + password: z.string(), +}); + +function randomName() { + return Array.from({ length: 10 }, () => + String.fromCharCode(randomInt(65, 90)) + ) + .join('') + .toLowerCase(); +} + +async function createRandomAIUser(): Promise<{ + name: string; + email: string; + password: string; + id: string; + sessionId: string; +}> { + const name = randomName(); + const email = `${name}@affine.fail`; + const user = { name, email, password: '123456' }; + const result = await runPrisma(async client => { + const freeFeatureId = await client.feature + .findFirst({ + where: { feature: 'free_plan_v1' }, + select: { id: true }, + orderBy: { version: 'desc' }, + }) + .then(f => f!.id); + const aiFeatureId = await client.feature + .findFirst({ + where: { feature: 'unlimited_copilot' }, + select: { id: true }, + orderBy: { version: 'desc' }, + }) + .then(f => f!.id); + + const { id: userId } = await client.user.create({ + data: { + ...user, + emailVerifiedAt: new Date(), + password: await hash(user.password), + features: { + create: [ + { + reason: 'created by test case', + activated: true, + featureId: freeFeatureId, + }, + { + reason: 'created by test case', + activated: true, + featureId: aiFeatureId, + }, + ], + }, + }, + }); + + const { id: sessionId } = await client.session.create({ data: {} }); + await client.userSession.create({ + data: { + sessionId, + userId, + // half an hour + expiresAt: new Date(Date.now() + 60 * 30 * 1000), + }, + }); + + return await client.user + .findUnique({ + where: { + email: user.email, + }, + }) + .then(r => ({ ...r, sessionId })); + }); + cloudUserSchema.parse(result); + return { + ...result, + password: user.password, + } as any; +} + test.before(async t => { if (!isCopilotConfigured) return; const { endpoint } = e2eConfig; - const { email, sessionId: token } = await createRandomAIUser( - 'affine.fail', - runPrisma - ); + const { email, sessionId: token } = await createRandomAIUser(); const app = { getHttpServer: () => endpoint } as any; const { id } = await createWorkspace(app, token); diff --git a/packages/backend/server/tests/tsconfig.docker.json b/packages/backend/server/tests/tsconfig.docker.json new file mode 100644 index 0000000000000..023ed91e8add1 --- /dev/null +++ b/packages/backend/server/tests/tsconfig.docker.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "rootDir": ".", + "outDir": "../lib/tests", + "verbatimModuleSyntax": false, + "tsBuildInfoFile": "../lib/tests/.tsbuildinfo" + }, + "include": [".", "utils"], + "exclude": [], + "ts-node": { + "esm": true, + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Node" + } + } +} diff --git a/packages/backend/server/tests/user/user.e2e.ts b/packages/backend/server/tests/user/user.e2e.ts index ae3b430d0a534..e8a0691b402af 100644 --- a/packages/backend/server/tests/user/user.e2e.ts +++ b/packages/backend/server/tests/user/user.e2e.ts @@ -5,7 +5,8 @@ import request from 'supertest'; import { AppModule } from '../../src/app.module'; import { AuthService, CurrentUser } from '../../src/core/auth'; -import { createTestingApp, gql, internalSignIn } from '../utils'; +import { createTestingApp, internalSignIn } from '../utils'; +import { gql } from '../utils/common'; const test = ava as TestFn<{ app: INestApplication; diff --git a/packages/backend/server/tests/utils/blobs.ts b/packages/backend/server/tests/utils/blobs.ts index f6832c41c401d..4fd302be738a1 100644 --- a/packages/backend/server/tests/utils/blobs.ts +++ b/packages/backend/server/tests/utils/blobs.ts @@ -1,7 +1,7 @@ import type { INestApplication } from '@nestjs/common'; import request from 'supertest'; -import { gql } from './common'; +import { gqlEndpoint } from './common'; export async function listBlobs( app: INestApplication, @@ -9,7 +9,7 @@ export async function listBlobs( workspaceId: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -29,7 +29,7 @@ export async function getWorkspaceBlobsSize( workspaceId: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .send({ query: ` @@ -49,7 +49,7 @@ export async function collectAllBlobSizes( token: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .send({ query: ` @@ -73,7 +73,7 @@ export async function setBlob( buffer: Buffer ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .field( diff --git a/packages/backend/server/tests/utils/common.ts b/packages/backend/server/tests/utils/common.ts index 772922367612e..d8e27f72cc99f 100644 --- a/packages/backend/server/tests/utils/common.ts +++ b/packages/backend/server/tests/utils/common.ts @@ -1 +1,37 @@ -export const gql = '/graphql'; +import { INestApplication } from '@nestjs/common'; +import type { Response } from 'supertest'; +import supertest from 'supertest'; + +export function handleGraphQLError(resp: Response) { + const { errors } = resp.body; + if (errors) { + const cause = errors[0]; + const stacktrace = cause.extensions?.stacktrace; + throw new Error( + stacktrace + ? Array.isArray(stacktrace) + ? stacktrace.join('\n') + : String(stacktrace) + : cause.message, + cause + ); + } +} + +export const gqlEndpoint = '/graphql'; + +export function gql(app: INestApplication, query?: string) { + const req = supertest(app.getHttpServer()) + .post(gqlEndpoint) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }); + + if (query) { + return req.send({ query }); + } + + return req; +} + +export async function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/packages/backend/server/tests/utils/copilot.ts b/packages/backend/server/tests/utils/copilot.ts index a5b84299d165d..054d60e594574 100644 --- a/packages/backend/server/tests/utils/copilot.ts +++ b/packages/backend/server/tests/utils/copilot.ts @@ -27,8 +27,7 @@ import { WorkflowNodeType, WorkflowParams, } from '../../src/plugins/copilot/workflow/types'; -import { gql } from './common'; -import { handleGraphQLError, sleep } from './utils'; +import { gqlEndpoint, handleGraphQLError, sleep } from './common'; // @ts-expect-error no error export class MockCopilotTestProvider @@ -167,7 +166,7 @@ export async function createCopilotSession( promptName: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(userToken, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -194,7 +193,7 @@ export async function forkCopilotSession( latestMessageId: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(userToken, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -224,7 +223,7 @@ export async function createCopilotMessage( params?: Record ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(userToken, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -378,7 +377,7 @@ export async function getHistories( } ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(userToken, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ diff --git a/packages/backend/server/tests/utils/index.ts b/packages/backend/server/tests/utils/index.ts index 92904d3c35d7e..6755347ca7824 100644 --- a/packages/backend/server/tests/utils/index.ts +++ b/packages/backend/server/tests/utils/index.ts @@ -1,4 +1,5 @@ export * from './blobs'; +export * from './common'; export * from './invite'; export * from './user'; export * from './utils'; diff --git a/packages/backend/server/tests/utils/invite.ts b/packages/backend/server/tests/utils/invite.ts index 218ddad371709..62f28f98a0c26 100644 --- a/packages/backend/server/tests/utils/invite.ts +++ b/packages/backend/server/tests/utils/invite.ts @@ -2,7 +2,7 @@ import type { INestApplication } from '@nestjs/common'; import request from 'supertest'; import type { InvitationType } from '../../src/core/workspaces'; -import { gql } from './common'; +import { gqlEndpoint } from './common'; import { PermissionEnum } from './utils'; export async function inviteUser( @@ -14,7 +14,7 @@ export async function inviteUser( sendInviteMail = false ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -39,7 +39,7 @@ export async function inviteUsers( sendInviteMail = false ): Promise> { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -100,7 +100,7 @@ export async function createInviteLink( expireTime: 'OneDay' | 'ThreeDays' | 'OneWeek' | 'OneMonth' ): Promise<{ link: string; expireTime: string }> { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -151,7 +151,7 @@ export async function acceptInviteById( token: string = '' ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .auth(token, { type: 'bearer' }) .send({ @@ -203,7 +203,7 @@ export async function leaveWorkspace( sendLeaveMail = false ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -227,7 +227,7 @@ export async function revokeUser( userId: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -252,7 +252,7 @@ export async function getInviteInfo( inviteId: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ diff --git a/packages/backend/server/tests/utils/user.ts b/packages/backend/server/tests/utils/user.ts index 79b793a87394f..2a5eb438b67a2 100644 --- a/packages/backend/server/tests/utils/user.ts +++ b/packages/backend/server/tests/utils/user.ts @@ -8,7 +8,7 @@ import { } from '../../src/core/auth'; import { sessionUser } from '../../src/core/auth/service'; import { UserService, type UserType } from '../../src/core/user'; -import { gql } from './common'; +import { gqlEndpoint } from './common'; export type UserAuthedType = UserType & { token: ClientTokenType }; @@ -68,7 +68,7 @@ export async function signUp( export async function currentUser(app: INestApplication, token: string) { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -92,7 +92,7 @@ export async function sendChangeEmail( callbackUrl: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(userToken, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -114,7 +114,7 @@ export async function sendSetPasswordEmail( callbackUrl: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(userToken, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -136,7 +136,7 @@ export async function changePassword( password: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ query: ` @@ -158,7 +158,7 @@ export async function sendVerifyChangeEmail( callbackUrl: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(userToken, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -180,7 +180,7 @@ export async function changeEmail( email: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(userToken, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ diff --git a/packages/backend/server/tests/utils/utils.ts b/packages/backend/server/tests/utils/utils.ts index 07298ea4bdec3..2471956e88def 100644 --- a/packages/backend/server/tests/utils/utils.ts +++ b/packages/backend/server/tests/utils/utils.ts @@ -5,8 +5,6 @@ import { Test, TestingModuleBuilder } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; import cookieParser from 'cookie-parser'; import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; -import type { Response } from 'supertest'; -import supertest from 'supertest'; import { AppModule, FunctionalityModules } from '../../src/app.module'; import { GlobalExceptionFilter, Runtime } from '../../src/base'; @@ -154,35 +152,3 @@ export async function createTestingApp(moduleDef: TestingModuleMeatdata = {}) { app, }; } - -export function handleGraphQLError(resp: Response) { - const { errors } = resp.body; - if (errors) { - const cause = errors[0]; - const stacktrace = cause.extensions?.stacktrace; - throw new Error( - stacktrace - ? Array.isArray(stacktrace) - ? stacktrace.join('\n') - : String(stacktrace) - : cause.message, - cause - ); - } -} - -export function gql(app: INestApplication, query?: string) { - const req = supertest(app.getHttpServer()) - .post('/graphql') - .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }); - - if (query) { - return req.send({ query }); - } - - return req; -} - -export async function sleep(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); -} diff --git a/packages/backend/server/tests/utils/workspace.ts b/packages/backend/server/tests/utils/workspace.ts index 8fcdde1553afa..23bc9c1c46b84 100644 --- a/packages/backend/server/tests/utils/workspace.ts +++ b/packages/backend/server/tests/utils/workspace.ts @@ -2,7 +2,7 @@ import type { INestApplication } from '@nestjs/common'; import request from 'supertest'; import type { WorkspaceType } from '../../src/core/workspaces'; -import { gql } from './common'; +import { gqlEndpoint } from './common'; import { PermissionEnum } from './utils'; export async function createWorkspace( @@ -10,7 +10,7 @@ export async function createWorkspace( token: string ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .field( @@ -37,7 +37,7 @@ export async function getWorkspacePublicPages( workspaceId: string ) { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -64,7 +64,7 @@ export async function getWorkspace( take = 8 ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -87,7 +87,7 @@ export async function updateWorkspace( isPublic: boolean ): Promise { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -110,7 +110,7 @@ export async function publishPage( pageId: string ) { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -134,7 +134,7 @@ export async function revokePublicPage( pageId: string ) { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ @@ -160,7 +160,7 @@ export async function grantMember( permission: PermissionEnum ) { const res = await request(app.getHttpServer()) - .post(gql) + .post(gqlEndpoint) .auth(token, { type: 'bearer' }) .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) .send({ diff --git a/tools/copilot-result/index.js b/tools/copilot-result/index.js index 77d8e26bcc17a..c25501448dc09 100644 --- a/tools/copilot-result/index.js +++ b/tools/copilot-result/index.js @@ -2,8 +2,16 @@ import { WebClient } from '@slack/web-api'; import { render } from './markdown.js'; -const { CHANNEL_ID, SLACK_BOT_TOKEN, COPILOT_RESULT, BRANCH_SHA, BRANCH_NAME } = - process.env; +const { + CHANNEL_ID, + SLACK_BOT_TOKEN, + COPILOT_RESULT, + BRANCH_SHA, + BRANCH_NAME, + GITHUB_SERVER_URL, + GITHUB_REPOSITORY, + GITHUB_RUN_ID, +} = process.env; const { ok } = await new WebClient(SLACK_BOT_TOKEN).chat.postMessage({ channel: CHANNEL_ID, @@ -11,7 +19,8 @@ const { ok } = await new WebClient(SLACK_BOT_TOKEN).chat.postMessage({ blocks: render( `# AFFiNE Copilot Test ${COPILOT_RESULT} -- [${BRANCH_NAME?.replace('refs/heads/', '') || BRANCH_SHA}](https://github.com/toeverything/AFFiNE/commit/${BRANCH_SHA}) +- Branch: [${BRANCH_NAME?.replace('refs/heads/', '') || BRANCH_SHA}](https://github.com/toeverything/AFFiNE/commit/${BRANCH_SHA}) +- Job: [${GITHUB_RUN_ID}](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) ` ), }); diff --git a/yarn.lock b/yarn.lock index a7cb5331528b7..5692bf9c373a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -767,6 +767,7 @@ __metadata: "@opentelemetry/semantic-conventions": "npm:^1.28.0" "@prisma/client": "npm:^5.22.0" "@prisma/instrumentation": "npm:^5.22.0" + "@slack/web-api": "npm:^7.3.4" "@socket.io/redis-adapter": "npm:^8.3.0" "@types/cookie-parser": "npm:^1.4.8" "@types/express": "npm:^4.17.21" @@ -780,7 +781,7 @@ __metadata: "@types/on-headers": "npm:^1.0.3" "@types/sinon": "npm:^17.0.3" "@types/supertest": "npm:^6.0.2" - ava: "npm:^6.2.0" + ava: "npm:^6.1.2" c8: "npm:^10.1.3" cookie-parser: "npm:^1.4.7" dotenv: "npm:^16.4.7" @@ -793,8 +794,10 @@ __metadata: html-validate: "npm:^8.27.0" ioredis: "npm:^5.4.1" is-mobile: "npm:^5.0.0" + jsx-slack: "npm:^6.1.1" keyv: "npm:^5.2.2" lodash-es: "npm:^4.17.21" + marked: "npm:^15.0.0" mixpanel: "npm:^0.18.0" mustache: "npm:^4.2.0" nanoid: "npm:^5.0.9" @@ -813,6 +816,7 @@ __metadata: socket.io: "npm:^4.8.1" stripe: "npm:^17.4.0" supertest: "npm:^7.0.0" + tap-parser: "npm:^18.0.0" ts-node: "npm:^10.9.2" typescript: "npm:^5.7.2" yjs: "patch:yjs@npm%3A13.6.18#~/.yarn/patches/yjs-npm-13.6.18-ad0d5f7c43.patch" @@ -12338,7 +12342,7 @@ __metadata: languageName: node linkType: hard -"@slack/web-api@npm:^7.8.0": +"@slack/web-api@npm:^7.3.4, @slack/web-api@npm:^7.8.0": version: 7.8.0 resolution: "@slack/web-api@npm:7.8.0" dependencies: @@ -16188,7 +16192,7 @@ __metadata: languageName: node linkType: hard -"ava@npm:^6.2.0": +"ava@npm:^6.1.2, ava@npm:^6.2.0": version: 6.2.0 resolution: "ava@npm:6.2.0" dependencies: @@ -20550,6 +20554,13 @@ __metadata: languageName: node linkType: hard +"events-to-array@npm:^2.0.3": + version: 2.0.3 + resolution: "events-to-array@npm:2.0.3" + checksum: 10/d392eb0013013c3dfa66710a017902760edb2a588f6b1a3f1c92219563ba1c24bcb99c48e3754423a3538ebfd70318c3536d30bfd80c00e7fec77fdd088540d0 + languageName: node + linkType: hard + "events@npm:^3.2.0": version: 3.3.0 resolution: "events@npm:3.3.0" @@ -24073,7 +24084,7 @@ __metadata: languageName: node linkType: hard -"jsx-slack@npm:^6.1.2": +"jsx-slack@npm:^6.1.1, jsx-slack@npm:^6.1.2": version: 6.1.2 resolution: "jsx-slack@npm:6.1.2" dependencies: @@ -25042,7 +25053,7 @@ __metadata: languageName: node linkType: hard -"marked@npm:^15.0.3": +"marked@npm:^15.0.0, marked@npm:^15.0.3": version: 15.0.4 resolution: "marked@npm:15.0.4" bin: @@ -31741,6 +31752,28 @@ __metadata: languageName: node linkType: hard +"tap-parser@npm:^18.0.0": + version: 18.0.0 + resolution: "tap-parser@npm:18.0.0" + dependencies: + events-to-array: "npm:^2.0.3" + tap-yaml: "npm:4.0.0" + bin: + tap-parser: bin/cmd.cjs + checksum: 10/6e13dc475bfdc880307cc935b9917d43255f7e2b0902d171b7f51b41b029aae9dce5b8683aa48170bb720819b3551d793663c0a11f0660ef78d4a0fe87228ad0 + languageName: node + linkType: hard + +"tap-yaml@npm:4.0.0": + version: 4.0.0 + resolution: "tap-yaml@npm:4.0.0" + dependencies: + yaml: "npm:^2.4.1" + yaml-types: "npm:^0.4.0" + checksum: 10/21d3a27328aa419bb90249357689446488351d350a4ba56c5b1afce9dfe33f60db49014170ea7d16eb90381d3f9f31b59d9637bda632e55167f33c834eb02171 + languageName: node + linkType: hard + "tapable@npm:^2.0.0, tapable@npm:^2.1.1, tapable@npm:^2.2.0, tapable@npm:^2.2.1": version: 2.2.1 resolution: "tapable@npm:2.2.1" @@ -34034,6 +34067,15 @@ __metadata: languageName: node linkType: hard +"yaml-types@npm:^0.4.0": + version: 0.4.0 + resolution: "yaml-types@npm:0.4.0" + peerDependencies: + yaml: ^2.3.0 + checksum: 10/8a3cd3a0420d5d09981e3e1add46d7482336531e3bdc02192d26caa915c7d0795ad28dd8766e357234d6bfa3a2bd986687f967079e47aecfd4b191250f041cec + languageName: node + linkType: hard + "yaml@npm:^1.10.0": version: 1.10.2 resolution: "yaml@npm:1.10.2" @@ -34041,7 +34083,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.3.1, yaml@npm:^2.3.4, yaml@npm:^2.6.1, yaml@npm:~2.6.1": +"yaml@npm:^2.3.1, yaml@npm:^2.3.4, yaml@npm:^2.4.1, yaml@npm:^2.6.1, yaml@npm:~2.6.1": version: 2.6.1 resolution: "yaml@npm:2.6.1" bin: