diff --git a/.github/actions/deploy/deploy.mjs b/.github/actions/deploy/deploy.mjs
index e5a35a18bf070..f371bfa573c17 100644
--- a/.github/actions/deploy/deploy.mjs
+++ b/.github/actions/deploy/deploy.mjs
@@ -19,6 +19,8 @@ const {
COPILOT_FAL_API_KEY,
COPILOT_PERPLEXITY_API_KEY,
COPILOT_UNSPLASH_API_KEY,
+ SLACK_BOT_TOKEN,
+ RELEASE_SLACK_CHANNEL_ID,
MAILER_SENDER,
MAILER_USER,
MAILER_PASSWORD,
@@ -150,6 +152,8 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-string graphql.app.copilot.fal.key="${COPILOT_FAL_API_KEY}"`,
`--set-string graphql.app.copilot.perplexity.key="${COPILOT_PERPLEXITY_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 5e08f72619559..1199fcdb2f227 100644
--- a/.github/helm/affine/charts/graphql/templates/copilot-secret.yaml
+++ b/.github/helm/affine/charts/graphql/templates/copilot-secret.yaml
@@ -9,4 +9,6 @@ data:
falSecret: {{ .Values.app.copilot.fal.key | b64enc }}
perplexitySecret: {{ .Values.app.copilot.perplexity.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 a1cbbee9752f0..8bec387068c8a 100644
--- a/.github/workflows/copilot-test.yml
+++ b/.github/workflows/copilot-test.yml
@@ -181,7 +181,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 }}
@@ -191,7 +191,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 }}
@@ -201,7 +201,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 95b3067851d9c..b8aa4d5da5d05 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -100,6 +100,9 @@ jobs:
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_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 }}
@@ -162,7 +165,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 }}
@@ -178,7 +181,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
@@ -193,7 +196,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/server/package.json b/packages/backend/server/package.json
index edc2b48af2668..1c2f46142266f 100644
--- a/packages/backend/server/package.json
+++ b/packages/backend/server/package.json
@@ -16,6 +16,7 @@
"test:coverage": "c8 ava --concurrency 1 --serial",
"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",
"data-migration": "cross-env NODE_ENV=script r ./src/data/index.ts",
"predeploy": "yarn prisma migrate deploy && NODE_ENV=script node --import ./scripts/register.js ./dist/data/index.js run",
"postinstall": "prisma generate"
@@ -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",
"eventsource-parser": "^3.0.0",
@@ -71,8 +74,10 @@
"html-validate": "^9.0.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",
@@ -89,6 +94,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",
"winston": "^3.17.0",
@@ -112,12 +119,10 @@
"@types/on-headers": "^1.0.3",
"@types/sinon": "^17.0.3",
"@types/supertest": "^6.0.2",
- "ava": "^6.2.0",
"c8": "^10.1.3",
"cross-env": "^7.0.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..c5332f8cb3cbd
--- /dev/null
+++ b/packages/backend/server/scripts/copilot-cron-test.js
@@ -0,0 +1,95 @@
+// 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 `
+
+
+
+ `;
+ }
+
+ paragraph({ tokens }) {
+ return ``;
+ }
+
+ list(token) {
+ return ``;
+ }
+
+ hr() {
+ return ``;
+ }
+ })(),
+ });
+ return jsxslack([`${rendered}`]);
+}
+
+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');
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..08a05f011401f
--- /dev/null
+++ b/packages/backend/server/tests/ava.docker.config.js
@@ -0,0 +1,15 @@
+import packageJson from '../package.json' with { type: 'json' };
+
+export default {
+ ...packageJson.ava,
+ nodeArguments: [
+ '--trace-sigint',
+ '--loader',
+ 'ts-node/esm/transpile-only.mjs',
+ '--es-module-specifier-resolution=node',
+ ],
+ 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 de01ad0328a07..f506603573588 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({
@@ -219,7 +218,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({
@@ -249,7 +248,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({
@@ -403,7 +402,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 4d99d1fc90bf1..332ec9e1570e0 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';
export async function inviteUser(
app: INestApplication,
@@ -12,7 +12,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({
@@ -37,7 +37,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({
@@ -98,7 +98,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({
@@ -149,7 +149,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({
@@ -201,7 +201,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({
@@ -225,7 +225,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({
@@ -250,7 +250,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 71a11489b0743..ef2baed9fef7b 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';
@@ -156,35 +154,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 401df040e98e2..bf25b76411af9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -790,6 +790,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"
@@ -803,7 +804,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"
cross-env: "npm:^7.0.3"
@@ -818,8 +819,10 @@ __metadata:
html-validate: "npm:^9.0.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"
@@ -839,6 +842,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"
winston: "npm:^3.17.0"
@@ -12643,7 +12647,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:
@@ -16686,7 +16690,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:
@@ -21152,6 +21156,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"
@@ -24760,7 +24771,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:
@@ -25757,7 +25768,7 @@ __metadata:
languageName: node
linkType: hard
-"marked@npm:^15.0.3":
+"marked@npm:^15.0.0, marked@npm:^15.0.3":
version: 15.0.6
resolution: "marked@npm:15.0.6"
bin:
@@ -32562,6 +32573,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"
@@ -35006,6 +35039,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"
@@ -35013,7 +35055,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: