Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion .github/workflows/component-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,37 @@ jobs:
- name: Run component tests
id: component-tests
continue-on-error: true
run: npm run test:component -- --reporter spec
env:
DEBUG: cypress:webpack-dev-server:*,cypress:server:specs,cypress:server:request,cypress:network:agent,cypress:server:runner-ct,cypress:server:controllers:spec,cypress:server:iframes,cypress:server:socket-ct,cypress:server:socket-base,cypress:server:open_project,cypress:server:project,cypress:server:preprocessor
HOST: localhost
run: |
mkdir -p cypress
npm run test:component -- --reporter spec 2>&1 | tee cypress/cypress-run.log
exit ${PIPESTATUS[0]}

- name: Analyze CT debug markers
if: always()
run: |
if [ ! -f "cypress/cypress-run.log" ]; then
echo "### CT Debug Markers" >> $GITHUB_STEP_SUMMARY
echo "- cypress/cypress-run.log not found" >> $GITHUB_STEP_SUMMARY
exit 0
fi

echo "### CT Debug Markers" >> $GITHUB_STEP_SUMMARY

for pattern in "Invalid Host/Origin header" "[webpack-dev-server] Disconnected!" "[ct-debug] zero-tests" "[ct-debug] uncaught:exception (propagated)" "cypress:server:project received runnables null" "cypress:server:project received runnables {" "[ct-debug] runtime-config" "[ct-debug] runnables-probe" "[ct-debug] first-test" "[ct-debug] window-error" "[ct-debug] unhandled-rejection" "cypress:server:socket-base socket-disconnect" "cypress:server:socket-base socket-error"; do
count=$(grep -F -c "$pattern" cypress/cypress-run.log || true)
echo "- \`$pattern\`: $count" >> $GITHUB_STEP_SUMMARY
done

if grep -F -q "Invalid Host/Origin header" cypress/cypress-run.log; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "#### Invalid Host/Origin Samples" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
grep -F "Invalid Host/Origin header" cypress/cypress-run.log | head -20 >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
fi

- name: Generate test summary
if: always()
Expand Down Expand Up @@ -73,4 +103,5 @@ jobs:
path: |
cypress/screenshots
cypress/videos
cypress/cypress-run.log
retention-days: 30
63 changes: 63 additions & 0 deletions CI_CT_DEBUG_INSIGHTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# CI Component Tests Debug Insights
_Last updated: 2026-02-17_

## Confirmed
- Проблема воспроизводится только в GitHub Actions (CT), локально не воспроизводится.
- Симптом нестабилен: случайные spec-файлы дают `Tests: 0`.
- В проблемных spec-файлах тесты реально есть (`describe/it` присутствуют).
- При части `0 tests` webpack-компиляция успешна и JS-ассеты отдаются с `200`.
- `Cannot stat .../Cache/Cache_Data/...` встречается и в успешных спеках; считаем шумом.
- Есть полный прогон с `zero-tests = 11` при `Invalid Host/Origin header = 0`, `Disconnected = 0`, `uncaught:exception (propagated) = 0`.
- В падающих спеках есть `cypress:server:project received runnables null` прямо перед `0 passing`.
- В успешных спеках есть `cypress:server:project received runnables { ... }`.
- В Cypress 15.9.0 `normalizeAll(...)` возвращает `undefined`, если в `runner.suite` нет тестов (`hasTests === false`).
- Следовательно, `runnables null` — это следствие пустого Mocha suite на момент нормализации, а не само по себе сетевой сбой.

## Instrumentation Added
- CI пишет полный лог в `cypress/cypress-run.log` и загружает его артефактом.
- Добавлены маркеры `[ct-debug] before:spec`, `[ct-debug] after:spec`, `[ct-debug] zero-tests`.
- Добавлен шаг `Analyze CT debug markers` в `GITHUB_STEP_SUMMARY`.
- Расширены DEBUG namespace: webpack/server/network + `cypress:server:socket-base`.
- Добавлен CI-only probe в support:
- `[ct-debug] runtime-config`
- `[ct-debug] runnables-probe` (immediate / t+0ms / t+200ms)
- `[ct-debug] first-test`
- `[ct-debug] window-error`
- `[ct-debug] unhandled-rejection`

## Hypotheses Log
_Statuses: `in_progress` | `confirmed` | `rejected`_

- `rejected` `H-001`: проблема в пустых/невалидных spec-файлах.
- Проверка: в проблемных файлах есть валидные `describe/it`.

- `rejected` `H-002`: проблема из-за глобального подавления ошибок `uncaught:exception`.
- Проверка: подавляются только `ResizeObserver*`; остальные ошибки не подавляются.

- `rejected` `H-003`: корень в `Invalid Host/Origin header` + reconnect.
- Проверка: был прогон с `zero-tests > 0` и нулем по этим маркерам.

- `in_progress` `H-004`: сбой в стадии регистрации/инициализации spec после компиляции.
- Сигнал: compile/network OK, но `tests=0`.

- `in_progress` `H-005`: сбой между `before:spec` и фактической регистрацией Mocha runnables.
- Сигнал: `before:spec` есть, `after:spec tests=0`.

- `confirmed` `H-006`: ключевой симптом — `runnables null` из runner слоя.

- `in_progress` `H-007`: upstream-причина до `normalizeAll` (suite пустой к моменту нормализации).

- `in_progress` `H-008`: spec не исполняется/падает до деклараций тестов.

- `in_progress` `H-009`: spec сначала регистрируется, но suite очищается/фильтруется до `set:runnables:and:maybe:record:tests`.

## Next Run Checklist
- Сравнить для одних и тех же namespace:
- проблемный spec (`runnables null`)
- успешный spec (`runnables { ... }`)
- Проверить в проблемных спеках последовательность:
- `[ct-debug] runtime-config`
- `[ct-debug] runnables-probe ... tests=...`
- `[ct-debug] first-test` (есть/нет)
- `cypress:server:project received runnables null`
- Проверить `socket-base socket-disconnect/socket-error` рядом по времени с `runnables null`.
34 changes: 34 additions & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ export default defineConfig({
bundler: 'webpack',
webpackConfig: {
devtool: false,
// Reduce host/origin mismatch noise in CI websocket handshake.
devServer: {
allowedHosts: 'all',
host: 'localhost',
client: {
webSocketURL: {
hostname: 'localhost',
},
},
},
},
},
specPattern: 'cypress/component/**/*.cy.{js,jsx,ts,tsx}',
Expand All @@ -16,6 +26,30 @@ export default defineConfig({
// Add code coverage for component tests
require('@cypress/code-coverage/task')(on, config)

const isCi = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true'

if (isCi) {
on('before:spec', (spec) => {
console.log(`[ct-debug] before:spec ${spec.relative}`)
})

on('after:spec', (spec, results) => {
if (!results) {
console.log(`[ct-debug] after:spec ${spec.relative} results=undefined`)
return
}

const { tests, passes, failures, pending, skipped, duration } = results.stats
console.log(
`[ct-debug] after:spec ${spec.relative} tests=${tests} passes=${passes} failures=${failures} pending=${pending} skipped=${skipped} durationMs=${duration}`,
)

if (tests === 0) {
console.warn(`[ct-debug] zero-tests ${spec.relative}`)
}
})
}

return config
},
viewportWidth: 1280,
Expand Down
2 changes: 2 additions & 0 deletions cypress/support/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import './commands'
import '@testing-library/cypress/add-commands'
import '@cypress/code-coverage/support'
import { registerGlobalErrorHandling } from './setup/error-handling'
import { registerCtRunnablesDebug } from './setup/ct-runnables-debug'

// Define a minimal Stub type based on usage to avoid 'any'
type SinonStub = {
Expand Down Expand Up @@ -59,6 +60,7 @@ const createSupabaseStub = (): SupabaseStub => {
}

registerGlobalErrorHandling()
registerCtRunnablesDebug()

Cypress.on('window:before:load', (win) => {
const appWindow = win as unknown as ComponentTestWindow
Expand Down
78 changes: 78 additions & 0 deletions cypress/support/setup/ct-runnables-debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
type MinimalMochaSuite = {
tests?: unknown[]
suites?: MinimalMochaSuite[]
}

const toBool = (value: unknown): boolean => value === true || value === 'true' || value === 1 || value === '1'

const countTests = (suite?: MinimalMochaSuite): number => {
if (!suite) return 0
const own = Array.isArray(suite.tests) ? suite.tests.length : 0
const nested = Array.isArray(suite.suites)
? suite.suites.reduce((acc, child) => acc + countTests(child), 0)
: 0
return own + nested
}

const countSuites = (suite?: MinimalMochaSuite): number => {
if (!suite || !Array.isArray(suite.suites)) return 0
return suite.suites.reduce((acc, child) => acc + 1 + countSuites(child), 0)
}

const logRunnerSnapshot = (stage: string, spec: string): void => {
try {
// `Cypress.mocha` is internal but stable enough for temporary CI diagnostics.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const runner = (Cypress as any).mocha?.getRunner?.()
const suite = runner?.suite as MinimalMochaSuite | undefined
const tests = countTests(suite)
const suites = countSuites(suite)

// eslint-disable-next-line no-console
console.error(
`[ct-debug] runnables-probe spec=${spec} stage=${stage} hasRunner=${Boolean(runner)} tests=${tests} suites=${suites}`,
)
} catch (error) {
// eslint-disable-next-line no-console
console.error(`[ct-debug] runnables-probe-error spec=${spec} stage=${stage}`, error)
}
}

export const registerCtRunnablesDebug = (): void => {
const isCi = toBool(Cypress.env('CI')) || toBool(Cypress.env('GITHUB_ACTIONS'))
if (!isCi) return

const spec = Cypress.spec?.relative ?? 'unknown-spec'

// eslint-disable-next-line no-console
console.error(
`[ct-debug] runtime-config spec=${spec} isInteractive=${String(Cypress.config('isInteractive'))} isTextTerminal=${String(Cypress.config('isTextTerminal'))}`,
)

let firstTestLogged = false

Cypress.on('test:before:run', (test) => {
if (firstTestLogged) return
firstTestLogged = true

const titlePath = typeof test?.titlePath === 'function' ? test.titlePath() : [test?.title]
// eslint-disable-next-line no-console
console.error(`[ct-debug] first-test spec=${spec} title="${titlePath.join(' > ')}"`)
})

window.addEventListener('error', (event) => {
// eslint-disable-next-line no-console
console.error(
`[ct-debug] window-error spec=${spec} message="${event.message}" filename="${event.filename}" lineno=${event.lineno}`,
)
})

window.addEventListener('unhandledrejection', (event) => {
// eslint-disable-next-line no-console
console.error(`[ct-debug] unhandled-rejection spec=${spec}`, event.reason)
})

logRunnerSnapshot('immediate', spec)
setTimeout(() => logRunnerSnapshot('t+0ms', spec), 0)
setTimeout(() => logRunnerSnapshot('t+200ms', spec), 200)
}
8 changes: 8 additions & 0 deletions cypress/support/setup/error-handling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ export const registerGlobalErrorHandling = (): void => {
return false
}

// Keep non-ignored exceptions visible in CI logs; do not suppress them.
// eslint-disable-next-line no-console
console.error('[ct-debug] uncaught:exception (propagated)', {
message,
name: err?.name,
stack: err?.stack,
})

return undefined
})
}
Loading