mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-28 11:25:00 +00:00
265 lines
7.8 KiB
TypeScript
265 lines
7.8 KiB
TypeScript
import type { Browser, Locator, Page, TestInfo } from '@playwright/test'
|
|
import { createFreshSession, loginWithCredentials, registerSession } from './session'
|
|
import { E2eTestDataBuilder, type SeededNamespace, type SeededSkill } from './test-data-builder'
|
|
|
|
export const DEFAULT_SEARCH_KEYWORD = 'agent'
|
|
|
|
export interface SearchSeedContext {
|
|
builder: E2eTestDataBuilder
|
|
keyword: string
|
|
namespace: SeededNamespace
|
|
skills: SeededSkill[]
|
|
skillNames: string[]
|
|
}
|
|
|
|
export interface PreparedSearchSeed extends SearchSeedContext {
|
|
dispose: () => Promise<void>
|
|
}
|
|
|
|
interface PublisherSession {
|
|
builder: E2eTestDataBuilder
|
|
context: Awaited<ReturnType<Browser['newContext']>>
|
|
namespace: SeededNamespace
|
|
page: Page
|
|
}
|
|
|
|
function requireEnv(name: string): string {
|
|
const value = process.env[name]
|
|
if (!value) {
|
|
throw new Error(`Missing required E2E env: ${name}`)
|
|
}
|
|
return value
|
|
}
|
|
|
|
function getOptionalEnv(name: string): string | undefined {
|
|
const value = process.env[name]?.trim()
|
|
return value ? value : undefined
|
|
}
|
|
|
|
function publisherCredentials() {
|
|
return {
|
|
username: requireEnv('E2E_PUBLISH_USERNAME'),
|
|
password: requireEnv('E2E_PUBLISH_PASSWORD'),
|
|
}
|
|
}
|
|
|
|
function adminCredentials() {
|
|
return {
|
|
username: getOptionalEnv('E2E_ADMIN_USERNAME') ?? getOptionalEnv('BOOTSTRAP_ADMIN_USERNAME') ?? 'admin',
|
|
password: getOptionalEnv('E2E_ADMIN_PASSWORD') ?? getOptionalEnv('BOOTSTRAP_ADMIN_PASSWORD') ?? 'ChangeMe!2026',
|
|
}
|
|
}
|
|
|
|
function hasPublisherCredentials() {
|
|
return Boolean(getOptionalEnv('E2E_PUBLISH_USERNAME') && getOptionalEnv('E2E_PUBLISH_PASSWORD'))
|
|
}
|
|
|
|
async function openProvidedPublisherSession(browser: Browser, testInfo: TestInfo): Promise<PublisherSession> {
|
|
const context = await browser.newContext()
|
|
const page = await context.newPage()
|
|
const builder = new E2eTestDataBuilder(page, testInfo)
|
|
|
|
await loginWithCredentials(page, publisherCredentials(), testInfo)
|
|
await builder.init()
|
|
|
|
return {
|
|
builder,
|
|
context,
|
|
namespace: await builder.ensureWritableNamespace(),
|
|
page,
|
|
}
|
|
}
|
|
|
|
async function openAdhocPublisherSession(browser: Browser, testInfo: TestInfo): Promise<PublisherSession> {
|
|
const context = await browser.newContext()
|
|
const page = await context.newPage()
|
|
const builder = new E2eTestDataBuilder(page, testInfo)
|
|
|
|
try {
|
|
await createFreshSession(page, testInfo)
|
|
} catch {
|
|
// Fall back to a regular worker session when transient registration issues happen
|
|
// after Playwright restarts the worker following an earlier test failure.
|
|
await registerSession(page, testInfo)
|
|
}
|
|
await builder.init()
|
|
|
|
return {
|
|
builder,
|
|
context,
|
|
namespace: await builder.ensureWritableNamespace(),
|
|
page,
|
|
}
|
|
}
|
|
|
|
async function publishSearchSkillsChunk(
|
|
session: PublisherSession,
|
|
keyword: string,
|
|
description: string,
|
|
seedSuffix: string,
|
|
startIndex: number,
|
|
count: number,
|
|
) {
|
|
const skills: SeededSkill[] = []
|
|
const skillNames: string[] = []
|
|
|
|
for (let offset = 0; offset < count; offset += 1) {
|
|
const skillIndex = startIndex + offset + 1
|
|
const skillName = `${keyword}-search-${skillIndex}-${seedSuffix}`.slice(0, 48)
|
|
const skill = await session.builder.publishSkill(session.namespace.slug, {
|
|
name: skillName,
|
|
description,
|
|
})
|
|
skills.push(skill)
|
|
skillNames.push(skillName)
|
|
}
|
|
|
|
return { skillNames, skills }
|
|
}
|
|
|
|
export async function seedPublicSearchSkills(
|
|
page: Page,
|
|
testInfo: TestInfo,
|
|
options?: {
|
|
awaitSearchIndexed?: boolean
|
|
count?: number
|
|
keyword?: string
|
|
description?: string
|
|
},
|
|
): Promise<SearchSeedContext> {
|
|
const count = options?.count ?? 1
|
|
const builder = new E2eTestDataBuilder(page, testInfo)
|
|
const seedSuffix = `${testInfo.parallelIndex ?? 0}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`
|
|
const keyword = options?.keyword || `agent-${seedSuffix}`.slice(0, 32)
|
|
|
|
await loginWithCredentials(page, publisherCredentials(), testInfo)
|
|
await builder.init()
|
|
|
|
const namespace = await builder.ensureWritableNamespace()
|
|
const skills: SeededSkill[] = []
|
|
const skillNames: string[] = []
|
|
|
|
for (let index = 0; index < count; index += 1) {
|
|
const skillName = `${keyword}-search-${index + 1}-${seedSuffix}`.slice(0, 48)
|
|
const skill = await builder.publishSkill(namespace.slug, {
|
|
name: skillName,
|
|
description: options?.description || `Searchable ${keyword} skill ${index + 1} for Playwright E2E coverage.`,
|
|
})
|
|
skills.push(skill)
|
|
skillNames.push(skillName)
|
|
}
|
|
|
|
if (options?.awaitSearchIndexed ?? true) {
|
|
await builder.waitForSearchResults(keyword, skills.map((skill) => skill.slug))
|
|
}
|
|
|
|
return {
|
|
builder,
|
|
keyword,
|
|
namespace,
|
|
skills,
|
|
skillNames,
|
|
}
|
|
}
|
|
|
|
export async function cleanupSearchSeed(seed?: SearchSeedContext) {
|
|
if (seed) {
|
|
await seed.builder.cleanup()
|
|
}
|
|
}
|
|
|
|
export async function prepareSearchSeed(
|
|
browser: Browser,
|
|
testInfo: TestInfo,
|
|
options?: {
|
|
awaitSearchIndexed?: boolean
|
|
count?: number
|
|
keyword?: string
|
|
description?: string
|
|
},
|
|
): Promise<PreparedSearchSeed> {
|
|
const count = options?.count ?? 1
|
|
const seedSuffix = `${testInfo.parallelIndex ?? 0}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`
|
|
const keyword = options?.keyword || `agent-${seedSuffix}`.slice(0, 32)
|
|
const description = options?.description || `Searchable ${keyword} skill for Playwright E2E coverage.`
|
|
const useProvidedPublisher = count <= 3 && hasPublisherCredentials()
|
|
const publisherSessions: PublisherSession[] = [
|
|
useProvidedPublisher
|
|
? await openProvidedPublisherSession(browser, testInfo)
|
|
: await openAdhocPublisherSession(browser, testInfo),
|
|
]
|
|
const skills: SeededSkill[] = []
|
|
const skillNames: string[] = []
|
|
let publishedCount = 0
|
|
|
|
while (publishedCount < count) {
|
|
if (publishedCount >= 10 && publisherSessions.length === 1) {
|
|
publisherSessions.push(await openAdhocPublisherSession(browser, testInfo))
|
|
}
|
|
|
|
const activeSession = publishedCount < 10 ? publisherSessions[0] : publisherSessions[publisherSessions.length - 1]
|
|
const chunkSize = publishedCount < 10 ? Math.min(10 - publishedCount, count - publishedCount) : count - publishedCount
|
|
const chunk = await publishSearchSkillsChunk(
|
|
activeSession,
|
|
keyword,
|
|
description,
|
|
seedSuffix,
|
|
publishedCount,
|
|
chunkSize,
|
|
)
|
|
skills.push(...chunk.skills)
|
|
skillNames.push(...chunk.skillNames)
|
|
publishedCount += chunkSize
|
|
}
|
|
|
|
const seed: SearchSeedContext = {
|
|
builder: publisherSessions[0].builder,
|
|
keyword,
|
|
namespace: publisherSessions[0].namespace,
|
|
skills,
|
|
skillNames,
|
|
}
|
|
|
|
const adminContext = await browser.newContext()
|
|
const adminPage = await adminContext.newPage()
|
|
const adminBuilder = new E2eTestDataBuilder(adminPage, testInfo)
|
|
|
|
await loginWithCredentials(adminPage, adminCredentials(), testInfo)
|
|
await adminBuilder.init()
|
|
|
|
for (const skill of seed.skills) {
|
|
const reviewTaskId = await adminBuilder.waitForPendingReview(skill.namespace, skill.slug, skill.version)
|
|
await adminBuilder.approveReview(reviewTaskId)
|
|
}
|
|
|
|
await seed.builder.waitForSearchResults(seed.keyword, seed.skills.map((skill) => skill.slug))
|
|
|
|
return {
|
|
...seed,
|
|
dispose: async () => {
|
|
await adminContext.close()
|
|
for (let index = publisherSessions.length - 1; index >= 0; index -= 1) {
|
|
await cleanupSearchSeed({
|
|
builder: publisherSessions[index].builder,
|
|
keyword: seed.keyword,
|
|
namespace: publisherSessions[index].namespace,
|
|
skills: [],
|
|
skillNames: [],
|
|
})
|
|
await publisherSessions[index].context.close()
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
export function getSearchCard(page: Page, skillName: string): Locator {
|
|
return getSearchCards(page).filter({
|
|
has: page.getByRole('heading', { name: skillName, exact: true }),
|
|
}).first()
|
|
}
|
|
|
|
export function getSearchCards(page: Page): Locator {
|
|
return page.getByRole('link').filter({
|
|
has: page.locator('h3'),
|
|
})
|
|
}
|