Skip to content

Commit

Permalink
feat: online ci
Browse files Browse the repository at this point in the history
  • Loading branch information
darkskygit committed Dec 13, 2024
1 parent 0e73737 commit f426901
Show file tree
Hide file tree
Showing 28 changed files with 624 additions and 241 deletions.
2 changes: 1 addition & 1 deletion .github/actions/copilot-test/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: 'Run Copilot E2E Test'
inputs:
script:
description: 'Script to run'
default: 'yarn workspace @affine-test/affine-cloud-copilot e2e --forbid-only'
default: 'yarn workspace @affine-test/affine-cloud-copilot test:e2e --forbid-only'
required: false
openai-key:
description: 'OpenAI secret key'
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ jobs:
changed:
- 'packages/backend/server/src/plugins/copilot/**'
- 'packages/backend/server/tests/copilot.*'
- 'tests/affine-cloud-copilot/**'
- name: Setup Node.js
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
Expand All @@ -448,7 +449,7 @@ jobs:

- name: Run server tests
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
run: yarn workspace @affine/server test:copilot:coverage --forbid-only
run: yarn workspace @affine/server test:copilot:spec:coverage --forbid-only
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
Expand Down Expand Up @@ -537,7 +538,7 @@ jobs:
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
uses: ./.github/actions/copilot-test
with:
script: yarn workspace @affine-test/affine-cloud-copilot e2e --forbid-only --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
script: yarn workspace @affine-test/affine-cloud-copilot test:e2e --forbid-only --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
openai-key: ${{ secrets.COPILOT_OPENAI_API_KEY }}
fal-key: ${{ secrets.COPILOT_FAL_API_KEY }}

Expand Down
17 changes: 14 additions & 3 deletions .github/workflows/copilot-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ jobs:
DISTRIBUTION: web
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
REDIS_SERVER_HOST: localhost
strategy:
fail-fast: false
matrix:
spec:
- {
name: e2e,
package: '@affine-test/affine-cloud-copilot',
type: e2e,
}
- { name: spec, package: '@affine/server', type: copilot:spec }
services:
postgres:
image: postgres
Expand Down Expand Up @@ -83,12 +93,13 @@ jobs:
- name: Prepare Server Test Environment
uses: ./.github/actions/server-test-env

- name: Run server tests
run: yarn workspace @affine/server test:copilot:coverage --forbid-only
- name: Run copilot api ${{ matrix.spec.name }} tests
run: yarn workspace ${{ matrix.spec.package }} test:${{ matrix.spec.type }}:coverage --forbid-only
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_E2E_ENDPOINT: ${{ secrets.COPILOT_E2E_ENDPOINT }}

- name: Upload server test coverage results
uses: codecov/codecov-action@v5
Expand Down Expand Up @@ -149,7 +160,7 @@ jobs:
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
uses: ./.github/actions/copilot-test
with:
script: yarn workspace @affine-test/affine-cloud-copilot e2e --forbid-only --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
script: yarn workspace @affine-test/affine-cloud-copilot test:e2e --forbid-only --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
openai-key: ${{ secrets.COPILOT_OPENAI_API_KEY }}
fal-key: ${{ secrets.COPILOT_FAL_API_KEY }}

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"docs/reference",
"tools/@types/*",
"tests/*",
"tests/affine-cloud/*",
"tests/affine-legacy/*"
],
"engines": {
Expand Down
6 changes: 4 additions & 2 deletions packages/backend/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
"start": "node --loader ts-node/esm/transpile-only.mjs ./src/index.ts",
"dev": "nodemon ./src/index.ts",
"test": "ava --concurrency 1 --serial",
"test:copilot": "ava \"tests/**/copilot-*.spec.ts\"",
"test:copilot:e2e": "ava \"e2e/copilot.e2e.ts\"",
"test:copilot:spec": "ava \"tests/**/copilot-*.spec.ts\"",
"test:coverage": "c8 ava --concurrency 1 --serial",
"test:copilot:coverage": "c8 ava --timeout=5m \"tests/**/copilot-*.spec.ts\"",
"test:copilot:e2e:coverage": "c8 ava --timeout=5m \"e2e/copilot.e2e.ts\"",
"test:copilot:spec:coverage": "c8 ava --timeout=5m \"tests/**/copilot-*.spec.ts\"",
"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"
Expand Down
214 changes: 214 additions & 0 deletions packages/backend/server/tests/copilot-provider.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { randomUUID } from 'node:crypto';

import { createRandomAIUser } from '@affine-test/kit/utils/cloud';
import type { ExecutionContext, TestFn } from 'ava';
import ava from 'ava';

import { createWorkspace } from './utils';
import {
chatWithImages,
chatWithText,
chatWithWorkflow,
createCopilotMessage,
createCopilotSession,
ProviderActionTestCase,
ProviderWorkflowTestCase,
sse2array,
} from './utils/copilot';

type Tester = {
app: any;
userEmail: string;
userToken: string;
workspaceId: string;
};
const test = ava as TestFn<Tester>;

const e2eConfig = {
endpoint: process.env.COPILOT_E2E_ENDPOINT || 'http://localhost:3010',
};

const isCopilotConfigured =
!!process.env.COPILOT_OPENAI_API_KEY &&
!!process.env.COPILOT_FAL_API_KEY &&
process.env.COPILOT_OPENAI_API_KEY !== '1' &&
process.env.COPILOT_FAL_API_KEY !== '1';
const runIfCopilotConfigured = test.macro(
async (
t,
callback: (t: ExecutionContext<Tester>) => Promise<void> | void
) => {
if (isCopilotConfigured) {
await callback(t);
} else {
t.log('Skip test because copilot is not configured');
t.pass();
}
}
);

export const runPrisma = async <T>(
cb: (
prisma: InstanceType<
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
typeof import('../../../../packages/backend/server/node_modules/@prisma/client').PrismaClient
>
) => Promise<T>
): Promise<T> => {
const {
PrismaClient,
// eslint-disable-next-line @typescript-eslint/no-var-requires
} = await import(
'../../../../packages/backend/server/node_modules/@prisma/client'
);
const client = new PrismaClient();
await client.$connect();
try {
return await cb(client);
} finally {
await client.$disconnect();
}
};

test.before(async t => {
if (!isCopilotConfigured) return;
const { endpoint } = e2eConfig;

const { email, sessionId: token } = await createRandomAIUser(
'affine.fail',
runPrisma
);
const app = { getHttpServer: () => endpoint } as any;
const { id } = await createWorkspace(app, token);

t.context.app = app;
t.context.userEmail = email;
t.context.userToken = token;
t.context.workspaceId = id;
});

test.after(async t => {
if (!isCopilotConfigured) return;
await runPrisma(async client => {
await client.user.delete({
where: {
email: t.context.userEmail,
},
});
});
});

const retry = async (
action: string,
t: ExecutionContext<Tester>,
callback: (t: ExecutionContext<Tester>) => void
) => {
let i = 3;
while (i--) {
const ret = await t.try(callback);
if (ret.passed) {
return ret.commit();
} else {
ret.discard();
t.log(ret.errors.map(e => e.message).join('\n'));
t.log(`retrying ${action} ${3 - i}/3 ...`);
}
}
t.fail(`failed to run ${action}`);
};

const makeCopilotChat = async (
t: ExecutionContext<Tester>,
promptName: string,
{ content, attachments, params }: any
) => {
const { app, userToken, workspaceId } = t.context;
const sessionId = await createCopilotSession(
app,
userToken,
workspaceId,
randomUUID(),
promptName
);
const messageId = await createCopilotMessage(
app,
userToken,
sessionId,
content,
attachments,
undefined,
params
);
return { sessionId, messageId };
};

// ==================== action ====================

for (const { promptName, messages, verifier, type } of ProviderActionTestCase) {
const prompts = Array.isArray(promptName) ? promptName : [promptName];
for (const promptName of prompts) {
test(
`should be able to run action: ${promptName}`,
runIfCopilotConfigured,
async t => {
await retry(`action: ${promptName}`, t, async t => {
const { app, userToken } = t.context;
const { sessionId, messageId } = await makeCopilotChat(
t,
promptName,
messages[0]
);

if (type === 'text') {
const result = await chatWithText(
app,
userToken,
sessionId,
messageId
);
t.truthy(result, 'should return result');
verifier?.(t, result);
} else if (type === 'image') {
const result = sse2array(
await chatWithImages(app, userToken, sessionId, messageId)
)
.filter(e => e.event !== 'event')
.map(e => e.data)
.filter(Boolean);
t.truthy(result.length, 'should return result');
for (const r of result) {
verifier?.(t, r);
}
} else {
t.fail('unsupported provider type');
}
});
}
);
}
}

// ==================== workflow ====================

for (const { name, content, verifier } of ProviderWorkflowTestCase) {
test(
`should be able to run workflow: ${name}`,
runIfCopilotConfigured,
async t => {
await retry(`workflow: ${name}`, t, async t => {
const { app, userToken } = t.context;
const { sessionId, messageId } = await makeCopilotChat(
t,
`workflow:${name}`,
{ content }
);
const r = await chatWithWorkflow(app, userToken, sessionId, messageId);
const result = sse2array(r)
.filter(e => e.event !== 'event' && e.data)
.reduce((p, c) => p + c.data, '');
t.truthy(result, 'should return result');
verifier?.(t, result);
});
}
);
}
Loading

0 comments on commit f426901

Please sign in to comment.