fix: run ci properly on pr (#37233)

This commit is contained in:
Yunlu Wen 2026-06-09 18:06:55 +08:00 committed by GitHub
parent 28cc3fc10d
commit 19d2a4d7a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 120 additions and 108 deletions

View File

@ -37,12 +37,17 @@ jobs:
- name: Setup web environment
uses: ./.github/actions/setup-web
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Validate release manifest
if: matrix.os == 'depot-ubuntu-24.04'
run: scripts/release-validate-manifest.sh
- name: CI pipeline (typecheck, lint, coverage, build)
run: pnpm ci
run: pnpm run ci
- name: Report coverage
if: ${{ env.CODECOV_TOKEN != '' && matrix.os == 'depot-ubuntu-24.04' }}

View File

@ -74,7 +74,7 @@ describe('runLogin', () => {
})
const active = reg.resolveActive()
expect(active?.ctx.account.email).toBe('tester@dify.ai')
expect(active?.ctx.workspace?.id).toBe('ws-1')
expect(active?.ctx.workspace?.id).toBe('550e8400-e29b-41d4-a716-446655440000')
expect(active?.ctx.available_workspaces).toHaveLength(2)
expect(store.get(tokenKey(active!.host, 'tester@dify.ai'))).toBe('dfoa_test')

View File

@ -10,8 +10,8 @@ function active(): ActiveContext {
email: 'inviter@example.com',
ctx: {
account: { id: 'acct-1', email: 'inviter@example.com', name: 'Inviter' },
workspace: { id: 'ws-1', name: 'Default', role: 'owner' },
available_workspaces: [{ id: 'ws-1', name: 'Default', role: 'owner' }],
workspace: { id: '550e8400-e29b-41d4-a716-446655440000', name: 'Default', role: 'owner' },
available_workspaces: [{ id: '550e8400-e29b-41d4-a716-446655440000', name: 'Default', role: 'owner' }],
},
}
}
@ -25,7 +25,7 @@ function fakeClient() {
role: body.role,
member_id: 'acct-new',
invite_url: 'https://console.example.com/activate?email=x&token=tok',
tenant_id: 'ws-1',
tenant_id: '550e8400-e29b-41d4-a716-446655440000',
})),
}
}
@ -42,7 +42,7 @@ describe('runCreateMember', () => {
membersFactory: () => client as never,
},
)
expect(client.invite).toHaveBeenCalledWith('ws-1', { email: 'new@example.com', role: 'normal' })
expect(client.invite).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000', { email: 'new@example.com', role: 'normal' })
expect(result.data.text()).toMatch(/Invited new@example\.com as normal/)
expect(result.data.name()).toBe('acct-new')
expect(result.data.json()).toMatchObject({
@ -50,9 +50,9 @@ describe('runCreateMember', () => {
role: 'normal',
member_id: 'acct-new',
invite_url: 'https://console.example.com/activate?email=x&token=tok',
tenant_id: 'ws-1',
tenant_id: '550e8400-e29b-41d4-a716-446655440000',
})
expect(result.workspaceId).toBe('ws-1')
expect(result.workspaceId).toBe('550e8400-e29b-41d4-a716-446655440000')
})
it('rejects unknown role before any HTTP call', async () => {
@ -90,7 +90,7 @@ describe('runCreateMember', () => {
it('-w flag overrides resolved workspace', async () => {
const client = fakeClient()
await runCreateMember(
{ email: 'new@example.com', role: 'admin', workspace: 'ws-9' },
{ email: 'new@example.com', role: 'admin', workspace: '550e8400-e29b-41d4-a716-446655440008' },
{
active: active(),
http: {} as HttpClient,
@ -98,6 +98,6 @@ describe('runCreateMember', () => {
membersFactory: () => client as never,
},
)
expect(client.invite).toHaveBeenCalledWith('ws-9', { email: 'new@example.com', role: 'admin' })
expect(client.invite).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440008', { email: 'new@example.com', role: 'admin' })
})
})

View File

@ -10,8 +10,8 @@ function active(): ActiveContext {
email: 'me@example.com',
ctx: {
account: { id: 'acct-1', email: 'me@example.com', name: 'Me' },
workspace: { id: 'ws-1', name: 'Default', role: 'owner' },
available_workspaces: [{ id: 'ws-1', name: 'Default', role: 'owner' }],
workspace: { id: '550e8400-e29b-41d4-a716-446655440000', name: 'Default', role: 'owner' },
available_workspaces: [{ id: '550e8400-e29b-41d4-a716-446655440000', name: 'Default', role: 'owner' }],
},
}
}
@ -34,17 +34,17 @@ describe('runDeleteMember', () => {
membersFactory: () => client as never,
},
)
expect(client.remove).toHaveBeenCalledExactlyOnceWith('ws-1', 'acct-2')
expect(client.remove).toHaveBeenCalledExactlyOnceWith('550e8400-e29b-41d4-a716-446655440000', 'acct-2')
expect(result.data.text()).toMatch(/Removed acct-2/)
expect(result.data.name()).toBe('acct-2')
expect(result.data.json()).toEqual({ id: 'acct-2', deleted: true })
expect(result.workspaceId).toBe('ws-1')
expect(result.workspaceId).toBe('550e8400-e29b-41d4-a716-446655440000')
})
it('-w flag overrides resolved workspace', async () => {
const client = fakeClient()
await runDeleteMember(
{ memberId: 'acct-2', workspace: 'ws-9' },
{ memberId: 'acct-2', workspace: '550e8400-e29b-41d4-a716-446655440008' },
{
active: active(),
http: {} as HttpClient,
@ -52,7 +52,7 @@ describe('runDeleteMember', () => {
membersFactory: () => client as never,
},
)
expect(client.remove).toHaveBeenCalledWith('ws-9', 'acct-2')
expect(client.remove).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440008', 'acct-2')
})
it('rejects empty member id before any HTTP call', async () => {

View File

@ -12,10 +12,10 @@ const baseActive: ActiveContext = {
email: 'tester@dify.ai',
ctx: {
account: { id: 'acct-1', email: 'tester@dify.ai', name: 'Test Tester' },
workspace: { id: 'ws-1', name: 'Default', role: 'owner' },
workspace: { id: '550e8400-e29b-41d4-a716-446655440000', name: 'Default', role: 'owner' },
available_workspaces: [
{ id: 'ws-1', name: 'Default', role: 'owner' },
{ id: 'ws-2', name: 'Other', role: 'normal' },
{ id: '550e8400-e29b-41d4-a716-446655440000', name: 'Default', role: 'owner' },
{ id: '550e8400-e29b-41d4-a716-446655440001', name: 'Other', role: 'normal' },
],
},
scheme: 'http',
@ -128,7 +128,7 @@ describe('runGetApp', () => {
})
it('--workspace flag overrides bundle default', async () => {
const out = await render({ workspace: 'ws-2' })
const out = await render({ workspace: '550e8400-e29b-41d4-a716-446655440001' })
expect(out).toContain('app-3')
expect(out).toContain('OtherWS Bot')
expect(out).not.toContain('Greeter')

View File

@ -11,8 +11,8 @@ function active(): ActiveContext {
email: 'me@example.com',
ctx: {
account: { id: 'acct-1', email: 'me@example.com', name: 'Me' },
workspace: { id: 'ws-1', name: 'Default', role: 'owner' },
available_workspaces: [{ id: 'ws-1', name: 'Default', role: 'owner' }],
workspace: { id: '550e8400-e29b-41d4-a716-446655440000', name: 'Default', role: 'owner' },
available_workspaces: [{ id: '550e8400-e29b-41d4-a716-446655440000', name: 'Default', role: 'owner' }],
},
}
}
@ -44,8 +44,8 @@ describe('runGetMember', () => {
membersFactory: () => client as never,
},
)
expect(client.list).toHaveBeenCalledExactlyOnceWith('ws-1', { page: 1, limit: 20 })
expect(r.workspaceId).toBe('ws-1')
expect(client.list).toHaveBeenCalledExactlyOnceWith('550e8400-e29b-41d4-a716-446655440000', { page: 1, limit: 20 })
expect(r.workspaceId).toBe('550e8400-e29b-41d4-a716-446655440000')
expect(r.data.rows.map(row => row.current)).toEqual([true, false])
expect(r.data.rows.map(row => row.id)).toEqual(['acct-1', 'acct-2'])
})
@ -53,7 +53,7 @@ describe('runGetMember', () => {
it('-w flag overrides resolved workspace', async () => {
const client = fakeClient(env)
const r = await runGetMember(
{ workspace: 'ws-9' },
{ workspace: '550e8400-e29b-41d4-a716-446655440008' },
{
active: active(),
http: {} as HttpClient,
@ -61,8 +61,8 @@ describe('runGetMember', () => {
membersFactory: () => client as never,
},
)
expect(client.list).toHaveBeenCalledWith('ws-9', { page: 1, limit: 20 })
expect(r.workspaceId).toBe('ws-9')
expect(client.list).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440008', { page: 1, limit: 20 })
expect(r.workspaceId).toBe('550e8400-e29b-41d4-a716-446655440008')
})
it('--page/--limit are forwarded to the client', async () => {
@ -76,7 +76,7 @@ describe('runGetMember', () => {
membersFactory: () => client as never,
},
)
expect(client.list).toHaveBeenCalledWith('ws-1', { page: 3, limit: 50 })
expect(client.list).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440000', { page: 3, limit: 50 })
})
it('marks no row when active context has no account id', async () => {
@ -86,7 +86,7 @@ describe('runGetMember', () => {
email: 'me@example.com',
ctx: {
account: { id: '', email: '', name: '' },
workspace: { id: 'ws-1', name: 'Default', role: 'owner' },
workspace: { id: '550e8400-e29b-41d4-a716-446655440000', name: 'Default', role: 'owner' },
},
}
const r = await runGetMember(

View File

@ -12,10 +12,10 @@ const baseActive: ActiveContext = {
email: 'tester@dify.ai',
ctx: {
account: { id: 'acct-1', email: 'tester@dify.ai', name: 'Test Tester' },
workspace: { id: 'ws-1', name: 'Default', role: 'owner' },
workspace: { id: '550e8400-e29b-41d4-a716-446655440000', name: 'Default', role: 'owner' },
available_workspaces: [
{ id: 'ws-1', name: 'Default', role: 'owner' },
{ id: 'ws-2', name: 'Other', role: 'normal' },
{ id: '550e8400-e29b-41d4-a716-446655440000', name: 'Default', role: 'owner' },
{ id: '550e8400-e29b-41d4-a716-446655440001', name: 'Other', role: 'normal' },
],
},
scheme: 'http',
@ -49,8 +49,8 @@ describe('runGetWorkspace', () => {
it('default format renders ID NAME ROLE STATUS CURRENT table', async () => {
const out = await render()
expect(out).toMatch(/^ID\s+NAME\s+ROLE\s+STATUS\s+CURRENT/)
expect(out).toContain('ws-1')
expect(out).toContain('ws-2')
expect(out).toContain('550e8400-e29b-41d4-a716-446655440000')
expect(out).toContain('550e8400-e29b-41d4-a716-446655440001')
expect(out).toContain('Default')
expect(out).toContain('owner')
expect(out).toContain('normal')
@ -69,18 +69,18 @@ describe('runGetWorkspace', () => {
it('marks the current workspace with *', async () => {
const out = await render()
for (const line of out.split('\n')) {
if (line.includes('ws-1'))
if (line.includes('550e8400-e29b-41d4-a716-446655440000'))
expect(line).toContain('*')
else if (line.includes('ws-2'))
else if (line.includes('550e8400-e29b-41d4-a716-446655440001'))
expect(line).not.toContain('*')
}
})
it('falls back to active context workspace.id when server current=false', async () => {
const overridden: ActiveContext = { ...baseActive, ctx: { ...baseActive.ctx, workspace: { id: 'ws-2', name: 'Other', role: 'normal' } } }
const overridden: ActiveContext = { ...baseActive, ctx: { ...baseActive.ctx, workspace: { id: '550e8400-e29b-41d4-a716-446655440001', name: 'Other', role: 'normal' } } }
const out = await render('', overridden)
for (const line of out.split('\n')) {
if (line.includes('ws-2'))
if (line.includes('550e8400-e29b-41d4-a716-446655440001'))
expect(line).toContain('*')
}
})
@ -89,7 +89,7 @@ describe('runGetWorkspace', () => {
const out = await render('json')
const parsed = JSON.parse(out) as { workspaces: Array<{ id: string, status: string, current: boolean }> }
expect(parsed.workspaces).toHaveLength(2)
expect(parsed.workspaces.map(w => w.id).sort()).toEqual(['ws-1', 'ws-2'])
expect(parsed.workspaces.map(w => w.id).sort()).toEqual(['550e8400-e29b-41d4-a716-446655440000', '550e8400-e29b-41d4-a716-446655440001'])
expect(parsed.workspaces[0]?.status).toBe('normal')
expect(parsed.workspaces[0]?.current).toBe(true)
})
@ -97,12 +97,12 @@ describe('runGetWorkspace', () => {
it('-o yaml emits "workspaces:" header', async () => {
const out = await render('yaml')
expect(out).toContain('workspaces:')
expect(out).toContain('ws-1')
expect(out).toContain('550e8400-e29b-41d4-a716-446655440000')
})
it('-o name emits ids joined by newline', async () => {
const out = await render('name')
expect(out.trim().split('\n').sort()).toEqual(['ws-1', 'ws-2'])
expect(out.trim().split('\n').sort()).toEqual(['550e8400-e29b-41d4-a716-446655440000', '550e8400-e29b-41d4-a716-446655440001'])
})
it('empty workspaces (sso scenario) prints external-SSO message regardless of format', async () => {

View File

@ -10,8 +10,8 @@ function active(): ActiveContext {
email: 'me@example.com',
ctx: {
account: { id: 'acct-1', email: 'me@example.com', name: 'Me' },
workspace: { id: 'ws-1', name: 'Default', role: 'owner' },
available_workspaces: [{ id: 'ws-1', name: 'Default', role: 'owner' }],
workspace: { id: '550e8400-e29b-41d4-a716-446655440000', name: 'Default', role: 'owner' },
available_workspaces: [{ id: '550e8400-e29b-41d4-a716-446655440000', name: 'Default', role: 'owner' }],
},
}
}
@ -34,11 +34,11 @@ describe('runSetMember', () => {
membersFactory: () => client as never,
},
)
expect(client.updateRole).toHaveBeenCalledExactlyOnceWith('ws-1', 'acct-2', { role: 'admin' })
expect(client.updateRole).toHaveBeenCalledExactlyOnceWith('550e8400-e29b-41d4-a716-446655440000', 'acct-2', { role: 'admin' })
expect(result.data.text()).toMatch(/Set acct-2 role to admin/)
expect(result.data.name()).toBe('acct-2')
expect(result.data.json()).toEqual({ id: 'acct-2', role: 'admin' })
expect(result.workspaceId).toBe('ws-1')
expect(result.workspaceId).toBe('550e8400-e29b-41d4-a716-446655440000')
})
it('rejects unknown role before any HTTP call', async () => {
@ -75,7 +75,7 @@ describe('runSetMember', () => {
it('-w flag overrides resolved workspace', async () => {
const client = fakeClient()
await runSetMember(
{ memberId: 'acct-2', role: 'normal', workspace: 'ws-9' },
{ memberId: 'acct-2', role: 'normal', workspace: '550e8400-e29b-41d4-a716-446655440008' },
{
active: active(),
http: {} as HttpClient,
@ -83,6 +83,6 @@ describe('runSetMember', () => {
membersFactory: () => client as never,
},
)
expect(client.updateRole).toHaveBeenCalledWith('ws-9', 'acct-2', { role: 'normal' })
expect(client.updateRole).toHaveBeenCalledWith('550e8400-e29b-41d4-a716-446655440008', 'acct-2', { role: 'normal' })
})
})

View File

@ -41,28 +41,30 @@ describe('detectAgents', () => {
})
describe('agent registry paths', () => {
const home = join('home', 'dev')
it('installs Codex skills under ~/.agents/skills, detected via ~/.codex', () => {
const codex = AGENTS.find(a => a.name === 'codex')
expect(codex?.probeDir('/home/dev')).toBe('/home/dev/.codex')
expect(codex?.skillDir('/home/dev')).toBe('/home/dev/.agents/skills/difyctl')
expect(codex?.probeDir(home)).toBe(join(home, '.codex'))
expect(codex?.skillDir(home)).toBe(join(home, '.agents', 'skills', 'difyctl'))
})
it('installs Claude Code and opencode skills under their native dirs', () => {
const claude = AGENTS.find(a => a.name === 'claude-code')
const opencode = AGENTS.find(a => a.name === 'opencode')
expect(claude?.skillDir('/home/dev')).toBe('/home/dev/.claude/skills/difyctl')
expect(opencode?.skillDir('/home/dev')).toBe('/home/dev/.config/opencode/skills/difyctl')
expect(claude?.skillDir(home)).toBe(join(home, '.claude', 'skills', 'difyctl'))
expect(opencode?.skillDir(home)).toBe(join(home, '.config', 'opencode', 'skills', 'difyctl'))
})
it('installs Cursor under ~/.cursor/skills, detected via ~/.cursor', () => {
const cursor = AGENTS.find(a => a.name === 'cursor')
expect(cursor?.probeDir('/home/dev')).toBe('/home/dev/.cursor')
expect(cursor?.skillDir('/home/dev')).toBe('/home/dev/.cursor/skills/difyctl')
expect(cursor?.probeDir(home)).toBe(join(home, '.cursor'))
expect(cursor?.skillDir(home)).toBe(join(home, '.cursor', 'skills', 'difyctl'))
})
it('installs pi under ~/.pi/agent/skills, detected via ~/.pi', () => {
const pi = AGENTS.find(a => a.name === 'pi')
expect(pi?.probeDir('/home/dev')).toBe('/home/dev/.pi')
expect(pi?.skillDir('/home/dev')).toBe('/home/dev/.pi/agent/skills/difyctl')
expect(pi?.probeDir(home)).toBe(join(home, '.pi'))
expect(pi?.skillDir(home)).toBe(join(home, '.pi', 'agent', 'skills', 'difyctl'))
})
})

View File

@ -32,6 +32,9 @@ abstract class FileBasedStore implements Store {
constructor(filePath: string) {
this.filePath = filePath
this.platform = resolvePlatform()
}
private ensureDir(): void {
fs.mkdirSync(dirname(this.filePath), { recursive: true, mode: DIR_PERM })
}
@ -50,6 +53,7 @@ abstract class FileBasedStore implements Store {
}
if (this.rawContent !== undefined) {
this.ensureDir()
const tmp = `${this.filePath}.tmp.${pid()}.${Date.now()}`
try {
fs.writeFileSync(tmp, this.rawContent, { mode: FILE_PERM })
@ -68,6 +72,7 @@ abstract class FileBasedStore implements Store {
}
lock(): void {
this.ensureDir()
try {
lockfile.lockSync(`${this.filePath}.lock`, {
stale: 30_000,

View File

@ -52,7 +52,7 @@ export async function cleanupRegisteredConversations(): Promise<void> {
if (_conversations.length === 0)
return
console.log(`[E2E teardown] Cleaning up ${_conversations.length} staged conversation(s)…`)
console.warn(`[E2E teardown] Cleaning up ${_conversations.length} staged conversation(s)…`)
const results = await Promise.allSettled(
_conversations.map(({ host, token, appId, conversationId }) =>
@ -68,7 +68,7 @@ export async function cleanupRegisteredConversations(): Promise<void> {
)
}
else {
console.log(`[E2E teardown] All conversations cleaned up.`)
console.warn(`[E2E teardown] All conversations cleaned up.`)
}
_conversations.length = 0

View File

@ -278,15 +278,15 @@ export async function setup(project: TestProject): Promise<void> {
workspaceId: primaryWsId,
workspaceName: primaryWsName,
ws2Id: secondaryWsId,
chatAppId: provisionedIds.DIFY_E2E_CHAT_APP_ID || E.chatAppId,
workflowAppId: provisionedIds.DIFY_E2E_WORKFLOW_APP_ID || E.workflowAppId,
fileAppId: provisionedIds.DIFY_E2E_FILE_APP_ID || E.fileAppId,
fileChatAppId: provisionedIds.DIFY_E2E_FILE_CHAT_APP_ID || E.fileChatAppId,
hitlAppId: provisionedIds.DIFY_E2E_HITL_APP_ID || E.hitlAppId,
hitlExternalAppId: provisionedIds.DIFY_E2E_HITL_EXTERNAL_APP_ID || E.hitlExternalAppId,
chatAppId: provisionedIds.DIFY_E2E_CHAT_APP_ID || E.chatAppId,
workflowAppId: provisionedIds.DIFY_E2E_WORKFLOW_APP_ID || E.workflowAppId,
fileAppId: provisionedIds.DIFY_E2E_FILE_APP_ID || E.fileAppId,
fileChatAppId: provisionedIds.DIFY_E2E_FILE_CHAT_APP_ID || E.fileChatAppId,
hitlAppId: provisionedIds.DIFY_E2E_HITL_APP_ID || E.hitlAppId,
hitlExternalAppId: provisionedIds.DIFY_E2E_HITL_EXTERNAL_APP_ID || E.hitlExternalAppId,
hitlSingleActionAppId: provisionedIds.DIFY_E2E_HITL_SINGLE_ACTION_APP_ID || E.hitlSingleActionAppId,
hitlMultiNodeAppId: provisionedIds.DIFY_E2E_HITL_MULTI_NODE_APP_ID || E.hitlMultiNodeAppId,
ws2AppId: provisionedIds.DIFY_E2E_WS2_APP_ID || E.ws2AppId,
hitlMultiNodeAppId: provisionedIds.DIFY_E2E_HITL_MULTI_NODE_APP_ID || E.hitlMultiNodeAppId,
ws2AppId: provisionedIds.DIFY_E2E_WS2_APP_ID || E.ws2AppId,
}
// @ts-expect-error — ProvidedContext augmentation cannot be expressed without

View File

@ -64,18 +64,18 @@ export const ACCOUNT: AccountFixture = {
email: 'tester@dify.ai',
name: 'Test Tester',
is_external: false,
current_workspace_id: 'ws-1',
current_workspace_id: '550e8400-e29b-41d4-a716-446655440000',
}
export const WORKSPACES: WorkspaceFixture[] = [
{ id: 'ws-1', name: 'Default', role: 'owner', status: 'normal', is_current: true },
{ id: 'ws-2', name: 'Other', role: 'normal', status: 'normal', is_current: false },
{ id: '550e8400-e29b-41d4-a716-446655440000', name: 'Default', role: 'owner', status: 'normal', is_current: true },
{ id: '550e8400-e29b-41d4-a716-446655440001', name: 'Other', role: 'normal', status: 'normal', is_current: false },
]
export const APPS: AppFixture[] = [
{
id: 'app-1',
workspace_id: 'ws-1',
workspace_id: '550e8400-e29b-41d4-a716-446655440000',
workspace_name: 'Default',
name: 'Greeter',
mode: 'chat',
@ -98,7 +98,7 @@ export const APPS: AppFixture[] = [
},
{
id: 'app-4',
workspace_id: 'ws-2',
workspace_id: '550e8400-e29b-41d4-a716-446655440001',
workspace_name: 'Other',
name: 'Researcher',
mode: 'agent-chat',
@ -113,7 +113,7 @@ export const APPS: AppFixture[] = [
},
{
id: 'app-2',
workspace_id: 'ws-1',
workspace_id: '550e8400-e29b-41d4-a716-446655440000',
workspace_name: 'Default',
name: 'Workflow',
mode: 'workflow',
@ -127,7 +127,7 @@ export const APPS: AppFixture[] = [
},
{
id: 'app-3',
workspace_id: 'ws-2',
workspace_id: '550e8400-e29b-41d4-a716-446655440001',
workspace_name: 'Other',
name: 'OtherWS Bot',
mode: 'chat',

View File

@ -59,7 +59,7 @@ describe('dify-mock fixture server', () => {
workspaces: Array<{ id: string, name: string, role: string, status: string, current: boolean }>
}
expect(body.workspaces).toHaveLength(2)
expect(body.workspaces[0]?.id).toBe('ws-1')
expect(body.workspaces[0]?.id).toBe('550e8400-e29b-41d4-a716-446655440000')
expect(body.workspaces[0]?.status).toBe('normal')
expect(body.workspaces[0]?.current).toBe(true)
expect(body.workspaces[1]?.current).toBe(false)
@ -89,11 +89,11 @@ describe('dify-mock fixture server', () => {
expect(body.subject_type).toBe('account')
expect(body.account?.email).toBe('tester@dify.ai')
expect(body.workspaces).toHaveLength(2)
expect(body.default_workspace_id).toBe('ws-1')
expect(body.default_workspace_id).toBe('550e8400-e29b-41d4-a716-446655440000')
})
it('GET /openapi/v1/apps respects ?mode filter', async () => {
const r = await fetch(`${mock.url}/openapi/v1/apps?workspace_id=ws-1&mode=workflow`, {
const r = await fetch(`${mock.url}/openapi/v1/apps?workspace_id=550e8400-e29b-41d4-a716-446655440000&mode=workflow`, {
headers: { Authorization: 'Bearer dfoa_test' },
})
const body = await r.json() as { data: Array<{ mode: string }>, total: number }
@ -103,7 +103,7 @@ describe('dify-mock fixture server', () => {
})
it('GET /openapi/v1/apps scopes by workspace_id', async () => {
const r = await fetch(`${mock.url}/openapi/v1/apps?workspace_id=ws-2`, {
const r = await fetch(`${mock.url}/openapi/v1/apps?workspace_id=550e8400-e29b-41d4-a716-446655440001`, {
headers: { Authorization: 'Bearer dfoa_test' },
})
const body = await r.json() as { data: Array<{ id: string }> }
@ -112,14 +112,14 @@ describe('dify-mock fixture server', () => {
})
it('GET /openapi/v1/apps/:id/describe returns 404 for unknown id', async () => {
const r = await fetch(`${mock.url}/openapi/v1/apps/nope/describe?workspace_id=ws-1`, {
const r = await fetch(`${mock.url}/openapi/v1/apps/nope/describe?workspace_id=550e8400-e29b-41d4-a716-446655440000`, {
headers: { Authorization: 'Bearer dfoa_test' },
})
expect(r.status).toBe(404)
})
it('GET /openapi/v1/apps/:id/describe returns the app for known id', async () => {
const r = await fetch(`${mock.url}/openapi/v1/apps/app-1/describe?workspace_id=ws-1`, {
const r = await fetch(`${mock.url}/openapi/v1/apps/app-1/describe?workspace_id=550e8400-e29b-41d4-a716-446655440000`, {
headers: { Authorization: 'Bearer dfoa_test' },
})
expect(r.status).toBe(200)
@ -158,7 +158,7 @@ describe('dify-mock fixture server', () => {
})
it('GET /openapi/v1/apps/:id/describe?fields=info returns slim payload', async () => {
const r = await fetch(`${mock.url}/openapi/v1/apps/app-1/describe?workspace_id=ws-1&fields=info`, {
const r = await fetch(`${mock.url}/openapi/v1/apps/app-1/describe?workspace_id=550e8400-e29b-41d4-a716-446655440000&fields=info`, {
headers: { Authorization: 'Bearer dfoa_test' },
})
expect(r.status).toBe(200)
@ -169,7 +169,7 @@ describe('dify-mock fixture server', () => {
})
it('GET /openapi/v1/apps/:id/describe full returns parameters when present', async () => {
const r = await fetch(`${mock.url}/openapi/v1/apps/app-1/describe?workspace_id=ws-1`, {
const r = await fetch(`${mock.url}/openapi/v1/apps/app-1/describe?workspace_id=550e8400-e29b-41d4-a716-446655440000`, {
headers: { Authorization: 'Bearer dfoa_test' },
})
expect(r.status).toBe(200)

View File

@ -178,7 +178,7 @@ export function buildApp(getScenario: () => Scenario, state?: MockState): Hono {
subject_email: ACCOUNT.email,
account: { id: ACCOUNT.id, email: ACCOUNT.email, name: ACCOUNT.name },
workspaces: WORKSPACES.map(w => ({ id: w.id, name: w.name, role: w.role })),
default_workspace_id: 'ws-1',
default_workspace_id: ACCOUNT.current_workspace_id,
})
})
@ -227,7 +227,7 @@ export function buildApp(getScenario: () => Scenario, state?: MockState): Hono {
const mode = c.req.query('mode')
const tag = c.req.query('tag')
const name = c.req.query('name')
const workspaceId = c.req.query('workspace_id') ?? 'ws-1'
const workspaceId = c.req.query('workspace_id') ?? ACCOUNT.current_workspace_id
let filtered = APPS.filter(a => a.workspace_id === workspaceId)
if (mode !== undefined && mode !== '')
filtered = filtered.filter(a => a.mode === mode)
@ -374,7 +374,7 @@ export function buildApp(getScenario: () => Scenario, state?: MockState): Hono {
subject_type: 'account',
account: { id: ACCOUNT.id, email: '', name: '' },
workspaces: WORKSPACES.map(w => ({ id: w.id, name: w.name, role: w.role })),
default_workspace_id: 'ws-1',
default_workspace_id: ACCOUNT.current_workspace_id,
token_id: 'tok-1',
})
}
@ -383,7 +383,7 @@ export function buildApp(getScenario: () => Scenario, state?: MockState): Hono {
subject_type: 'account',
account: ACCOUNT,
workspaces: WORKSPACES.map(w => ({ id: w.id, name: w.name, role: w.role })),
default_workspace_id: 'ws-1',
default_workspace_id: ACCOUNT.current_workspace_id,
token_id: 'tok-1',
})
})

View File

@ -67,31 +67,31 @@ export default defineConfig({
return raw.split(',').map(s => s.trim()).filter(Boolean)
return undefined
})()
?? (process.env.DIFY_E2E_MODE === 'local'
? ['test/e2e/suites/framework/help.e2e.ts', 'test/e2e/suites/agent/**/*.e2e.ts']
: [
?? (process.env.DIFY_E2E_MODE === 'local'
? ['test/e2e/suites/framework/help.e2e.ts', 'test/e2e/suites/agent/**/*.e2e.ts']
: [
// auth tests first (most others depend on a valid session)
'test/e2e/suites/auth/login.e2e.ts',
'test/e2e/suites/auth/status.e2e.ts',
'test/e2e/suites/auth/use.e2e.ts',
'test/e2e/suites/auth/whoami.e2e.ts',
// help (no network, no auth — runs first)
'test/e2e/suites/framework/help.e2e.ts',
// output format (table / cross-cutting)
'test/e2e/suites/output/**/*.e2e.ts',
// error handling (cross-cutting error message spec)
'test/e2e/suites/error-handling/**/*.e2e.ts',
// framework (global flags, non-interactive, debug)
'test/e2e/suites/framework/**/*.e2e.ts',
// discovery (get app / describe app)
'test/e2e/suites/discovery/**/*.e2e.ts',
// run tests (require valid token)
'test/e2e/suites/run/**/*.e2e.ts',
'test/e2e/suites/agent/**/*.e2e.ts',
// devices + logout LAST — both can revoke tokens
'test/e2e/suites/auth/devices.e2e.ts',
'test/e2e/suites/auth/logout.e2e.ts',
]),
'test/e2e/suites/auth/login.e2e.ts',
'test/e2e/suites/auth/status.e2e.ts',
'test/e2e/suites/auth/use.e2e.ts',
'test/e2e/suites/auth/whoami.e2e.ts',
// help (no network, no auth — runs first)
'test/e2e/suites/framework/help.e2e.ts',
// output format (table / cross-cutting)
'test/e2e/suites/output/**/*.e2e.ts',
// error handling (cross-cutting error message spec)
'test/e2e/suites/error-handling/**/*.e2e.ts',
// framework (global flags, non-interactive, debug)
'test/e2e/suites/framework/**/*.e2e.ts',
// discovery (get app / describe app)
'test/e2e/suites/discovery/**/*.e2e.ts',
// run tests (require valid token)
'test/e2e/suites/run/**/*.e2e.ts',
'test/e2e/suites/agent/**/*.e2e.ts',
// devices + logout LAST — both can revoke tokens
'test/e2e/suites/auth/devices.e2e.ts',
'test/e2e/suites/auth/logout.e2e.ts',
]),
// E2E calls a real staging server — allow plenty of time per test.
testTimeout: 120_000,
hookTimeout: 30_000,