Skip to content

Commit 62c36b3

Browse files
authored
Merge branch 'main' into feature/ecs-migration
2 parents 5002fae + a2603c5 commit 62c36b3

21 files changed

Lines changed: 1421 additions & 276 deletions

File tree

.github/risk.json

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -83,22 +83,42 @@
8383
{
8484
"class": "billing",
8585
"tier": "R3",
86-
"why": "billing / credits — money moves",
86+
"why": "billing / credits — money moves. Scoped to the application source tree: an unscoped **/ token glob matched the marketing site, e2e specs, PNG snapshots, stories, locale files and docs on filename characters alone",
8787
"paths": [
88-
"**/billing/**",
89-
"**/pricing/**",
90-
"**/credits/**",
91-
"**/subscription/**",
92-
"**/*billing*",
93-
"**/*Billing*",
94-
"**/*subscription*",
95-
"**/*Subscription*",
96-
"**/*credit*",
97-
"**/*Credit*",
98-
"**/*price*",
99-
"**/*Price*",
100-
"**/*pricing*",
101-
"**/*Pricing*"
88+
"src/**/billing/**",
89+
"src/**/pricing/**",
90+
"src/**/credit/**",
91+
"src/**/credits/**",
92+
"src/**/subscription/**",
93+
"src/**/*TopUp*.vue",
94+
"src/platform/**/*billing*",
95+
"src/platform/**/*Billing*",
96+
"src/platform/**/*subscription*",
97+
"src/platform/**/*Subscription*",
98+
"src/platform/**/*credit*",
99+
"src/platform/**/*Credit*",
100+
"src/platform/**/*price*",
101+
"src/platform/**/*Price*",
102+
"src/platform/**/*pricing*",
103+
"src/platform/**/*Pricing*",
104+
"src/stores/*billing*",
105+
"src/stores/*Billing*",
106+
"src/stores/*credit*",
107+
"src/stores/*Credit*",
108+
"src/services/*billing*",
109+
"src/services/*Billing*",
110+
"src/services/*credit*",
111+
"src/services/*Credit*",
112+
"src/config/*billing*",
113+
"src/config/*Billing*",
114+
"src/config/*credit*",
115+
"src/config/*Credit*",
116+
"src/config/*price*",
117+
"src/config/*Price*",
118+
"src/config/*pricing*",
119+
"src/config/*Pricing*",
120+
"src/config/*subscription*",
121+
"src/config/*Subscription*"
102122
]
103123
},
104124
{
@@ -198,6 +218,12 @@
198218
"packages/**"
199219
]
200220
},
221+
{
222+
"class": "website",
223+
"tier": "R2",
224+
"why": "the marketing site is production and had no rule at all — but it is copy and layout, so it cannot corrupt a saved workflow or move money",
225+
"paths": ["apps/website/**"]
226+
},
201227

202228
{
203229
"class": "docs",

apps/website/e2e/events.spec.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,9 @@ test.describe('Events page — desktop @smoke', () => {
223223
page
224224
}) => {
225225
const event = upcomingEvents.find((entry) => eventVideoId(entry))
226-
test.skip(!event, 'needs an upcoming event with a video')
226+
const videoId = event && eventVideoId(event)
227+
test.skip(!videoId, 'needs an upcoming event with a video')
228+
if (!event || !videoId) return
227229

228230
for (const [path, locale] of LOCALES) {
229231
await page.goto(path)
@@ -232,21 +234,21 @@ test.describe('Events page — desktop @smoke', () => {
232234

233235
await section
234236
.getByRole('link', {
235-
name: `${event!.title[locale]}${t('events.upcoming.livestream', locale)}`
237+
name: `${event.title[locale]}${t('events.upcoming.livestream', locale)}`
236238
})
237239
.click()
238240

239241
await expect(page).toHaveURL(
240-
new RegExp(`${localizeHref(eventPath(event!), locale)}/?$`)
242+
new RegExp(`${localizeHref(eventPath(event), locale)}/?$`)
241243
)
242-
const dialog = page.getByRole('dialog', { name: event!.title[locale] })
244+
const dialog = page.getByRole('dialog', { name: event.title[locale] })
243245
await expect(dialog).toBeVisible()
244246
await expect(
245-
dialog.getByRole('heading', { level: 1, name: event!.title[locale] })
247+
dialog.getByRole('heading', { level: 1, name: event.title[locale] })
246248
).toBeVisible()
247249
await expect(dialog.locator('iframe')).toHaveAttribute(
248250
'src',
249-
new RegExp(eventVideoId(event!)!)
251+
new RegExp(videoId)
250252
)
251253

252254
// Future events offer adding the stream to the visitor's calendar; the

apps/website/src/data/events.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,26 @@ const events: readonly ComfyEvent[] = [
317317
'zh-CN': 'UCAN:智能体商务——设计下一代商业基础设施'
318318
})
319319
},
320+
{
321+
id: 'local-mcp',
322+
category: 'livestream',
323+
title: {
324+
en: 'Local MCP: Run ComfyUI with Your Agent & Hardware',
325+
'zh-CN': '本地 MCP:用你的智能体与硬件运行 ComfyUI'
326+
},
327+
description: {
328+
en: 'Run ComfyUI locally through MCP — a live walkthrough of driving your own agent and hardware to build and run workflows from the tools you already use.',
329+
'zh-CN':
330+
'通过 MCP 在本地运行 ComfyUI——现场演示如何驱动你自己的智能体与硬件,用你已经在使用的工具来构建并运行工作流。'
331+
},
332+
location: { en: 'Online', 'zh-CN': '线上' },
333+
dateLabel: {
334+
en: 'August 26, 2026 · 10AM PT',
335+
'zh-CN': '2026年8月26日 · 上午10点(PT)'
336+
},
337+
startDateTime: '2026-08-26T10:00:00-07:00',
338+
liveVideoId: '6yH_15XSd0w'
339+
},
320340
{
321341
id: 'beyond-the-models',
322342
category: 'livestream',

apps/website/src/layouts/BaseLayout.astro

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,18 @@ const structuredData = noindex
151151

152152
<ClientRouter />
153153

154+
<!-- Chrome initializes a video's muted state from the `muted` attribute
155+
only during document parsing, not for the DOMParser-built pages the
156+
ClientRouter swaps in — re-assert it before paint or autoplaying
157+
videos come back with sound. -->
158+
<script>
159+
document.addEventListener('astro:after-swap', () => {
160+
document.querySelectorAll('video').forEach((video) => {
161+
if (video.hasAttribute('muted')) video.muted = true
162+
})
163+
})
164+
</script>
165+
154166
<!-- Hide an already-dismissed announcement banner before first paint (no flash/shift). -->
155167
{bannerVisible && (
156168
<script

browser_tests/fixtures/helpers/CloudWorkspaceMockHelper.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const jsonRoute = (body: unknown) => ({
4646
export class CloudWorkspaceMockHelper {
4747
constructor(private readonly page: Page) {}
4848

49-
async openWorkspaceSettings(): Promise<Locator> {
49+
async openPlanAndCreditsSettings(): Promise<Locator> {
5050
await this.page.goto(
5151
process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
5252
)
@@ -65,7 +65,7 @@ export class CloudWorkspaceMockHelper {
6565
await dialog.waitFor({ state: 'visible' })
6666
await dialog
6767
.locator('nav')
68-
.getByRole('button', { name: 'Workspace', exact: true })
68+
.getByRole('button', { name: 'Plan & Credits', exact: true })
6969
.click()
7070
return dialog.getByRole('main')
7171
}

browser_tests/tests/dialogs/creditsTile.spec.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ test.describe('Credits tile (Plan & Credits)', { tag: '@cloud' }, () => {
309309
await expect(page.getByText('Cancel plan', { exact: true })).toBeVisible()
310310
})
311311

312-
test('keeps the legacy Workspace UX when billing controls are disabled', async ({
312+
test('keeps V1 workspace navigation when billing controls are disabled', async ({
313313
page
314314
}) => {
315315
test.setTimeout(60_000)
@@ -320,23 +320,21 @@ test.describe('Credits tile (Plan & Credits)', { tag: '@cloud' }, () => {
320320

321321
await expect(
322322
nav.getByRole('button', { name: 'Workspace', exact: true })
323-
).toBeVisible()
323+
).toHaveCount(0)
324324
await expect(
325325
nav.getByRole('button', { name: 'Plan & Credits', exact: true })
326-
).toHaveCount(0)
326+
).toBeVisible()
327327
await expect(
328328
nav.getByRole('button', { name: 'Members', exact: true })
329-
).toHaveCount(0)
329+
).toBeVisible()
330330

331-
await nav.getByRole('button', { name: 'Workspace', exact: true }).click()
331+
await nav
332+
.getByRole('button', { name: 'Plan & Credits', exact: true })
333+
.click()
332334
const content = dialog.getByRole('main')
333-
await expect(
334-
content.getByRole('tab', { name: 'Plan & Credits' })
335-
).toBeVisible()
336-
await expect(content.getByRole('tab', { name: 'Members' })).toBeVisible()
337335
await expect(
338336
content.getByRole('button', { name: 'Activity', exact: true })
339-
).toHaveCount(0)
337+
).toBeVisible()
340338
})
341339

342340
test('renders the unified tile with breakdown and add-credits', async ({

browser_tests/tests/dialogs/endedSubscription.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ test.describe('Ended workspace subscription', { tag: '@cloud' }, () => {
2929
TEAM_WORKSPACE,
3030
ENDED_STANDARD_BILLING_STATUS
3131
)
32-
content = await workspace.openWorkspaceSettings()
32+
content = await workspace.openPlanAndCreditsSettings()
3333
})
3434

3535
test('shows subscribe prompt instead of stale paid plan metadata', async ({
@@ -74,7 +74,7 @@ test.describe('Inactive Team subscription billing', { tag: '@cloud' }, () => {
7474
TEAM_WORKSPACE,
7575
INACTIVE_TEAM_BILLING_STATUS
7676
)
77-
content = await workspace.openWorkspaceSettings()
77+
content = await workspace.openPlanAndCreditsSettings()
7878
})
7979

8080
test('keeps the owner billing portal available', async ({ page }) => {
@@ -99,7 +99,7 @@ test.describe('Team member billing permissions', { tag: '@cloud' }, () => {
9999
TEAM_MEMBER_WORKSPACE,
100100
TEAM_BILLING_STATUS
101101
)
102-
content = await workspace.openWorkspaceSettings()
102+
content = await workspace.openPlanAndCreditsSettings()
103103
})
104104

105105
test('does not expose owner billing actions', async () => {

browser_tests/tests/dialogs/memberRoleChange.spec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,9 @@ async function openMembersTab(page: Page): Promise<Locator> {
3232
.click()
3333
const dialog = page.getByTestId('settings-dialog')
3434
await expect(dialog).toBeVisible()
35-
await dialog.locator('nav').getByRole('button', { name: 'Workspace' }).click()
35+
await dialog.locator('nav').getByRole('button', { name: 'Members' }).click()
3636

3737
const content = dialog.getByRole('main')
38-
await content.getByRole('tab', { name: /Members/ }).click()
3938
await expect(content.getByText('4 of 30 members')).toBeVisible()
4039
return content
4140
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { render, screen } from '@testing-library/vue'
2+
import userEvent from '@testing-library/user-event'
3+
import { beforeEach, describe, expect, it, vi } from 'vitest'
4+
import { defineComponent, h, nextTick } from 'vue'
5+
import { createI18n } from 'vue-i18n'
6+
7+
import type { BalanceInfo } from '@/composables/billing/types'
8+
9+
import CreditsPanel from './CreditsPanel.vue'
10+
11+
const billingMocks = vi.hoisted(() => ({
12+
balance: { value: null as BalanceInfo | null },
13+
manageSubscription: vi.fn()
14+
}))
15+
vi.mock('@/composables/billing/useBillingContext', async () => {
16+
const { ref } = await import('vue')
17+
const balance = ref<BalanceInfo | null>(null)
18+
Object.defineProperty(billingMocks, 'balance', { get: () => balance })
19+
return {
20+
useBillingContext: () => ({
21+
balance,
22+
manageSubscription: billingMocks.manageSubscription
23+
})
24+
}
25+
})
26+
27+
const refreshActivity = vi.hoisted(() => vi.fn())
28+
vi.mock('./UsageLogsTable.vue', async () => {
29+
const { defineComponent, h } = await import('vue')
30+
return {
31+
default: defineComponent({
32+
setup(_props, { expose }) {
33+
expose({ refresh: refreshActivity })
34+
return () => h('div', { 'data-testid': 'usage-logs-table' })
35+
}
36+
})
37+
}
38+
})
39+
40+
vi.mock('@/platform/cloud/subscription/components/CreditsTile.vue', () => ({
41+
default: defineComponent({ setup: () => () => h('div') })
42+
}))
43+
44+
vi.mock('@/platform/telemetry', () => ({
45+
useTelemetry: () => ({ trackHelpResourceClicked: vi.fn() })
46+
}))
47+
48+
vi.mock('@/stores/commandStore', () => ({
49+
useCommandStore: () => ({ execute: vi.fn() })
50+
}))
51+
52+
vi.mock('@/composables/useExternalLink', () => ({
53+
useExternalLink: () => ({
54+
buildDocsUrl: () => 'https://docs.comfy.org',
55+
docsPaths: { partnerNodesPricing: '/partner-nodes' }
56+
})
57+
}))
58+
59+
const i18n = createI18n({
60+
legacy: false,
61+
locale: 'en',
62+
messages: {
63+
en: {
64+
credits: {
65+
credits: 'Credits',
66+
activity: 'Activity',
67+
invoiceHistory: 'Invoice History',
68+
faqs: 'FAQs',
69+
messageSupport: 'Message Support'
70+
},
71+
subscription: { partnerNodesCredits: 'Partner Nodes Credits' }
72+
}
73+
}
74+
})
75+
76+
function makeBalance(amountMicros: number): BalanceInfo {
77+
return {
78+
amountMicros,
79+
currency: 'usd',
80+
effectiveBalanceMicros: amountMicros,
81+
prepaidBalanceMicros: 0,
82+
cloudCreditBalanceMicros: 0
83+
}
84+
}
85+
86+
describe('CreditsPanel', () => {
87+
beforeEach(() => {
88+
billingMocks.balance.value = null
89+
})
90+
91+
function renderComponent() {
92+
return render(CreditsPanel, {
93+
global: { plugins: [i18n], stubs: { Divider: true } }
94+
})
95+
}
96+
97+
it('opens the billing portal for the active billing rail', async () => {
98+
const user = userEvent.setup()
99+
renderComponent()
100+
101+
await user.click(screen.getByRole('button', { name: /Invoice History/ }))
102+
103+
expect(billingMocks.manageSubscription).toHaveBeenCalledOnce()
104+
})
105+
106+
it('refreshes activity on a balance change but not on first hydration', async () => {
107+
renderComponent()
108+
109+
billingMocks.balance.value = makeBalance(5000)
110+
await nextTick()
111+
expect(refreshActivity).not.toHaveBeenCalled()
112+
113+
billingMocks.balance.value = makeBalance(9000)
114+
await nextTick()
115+
expect(refreshActivity).toHaveBeenCalledOnce()
116+
})
117+
})

0 commit comments

Comments
 (0)