From ab33272cac957ffed18dbf486befe290b819e2f1 Mon Sep 17 00:00:00 2001 From: yml Date: Mon, 4 May 2026 13:38:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=E8=85=BE=E8=AE=AF=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E7=BA=AF=E5=87=BD=E6=95=B0=E4=B8=8E=E5=BA=8F=E5=88=97?= =?UTF-8?q?=E5=8C=96=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../session/session-browser-config.js | 101 ++++++ .../src/services/session/session-payload.js | 126 ++++++++ .../src/services/session/session-state.js | 74 +++++ apps/backend/src/services/session/session.js | 302 ++---------------- .../src/services/session/session.test.js | 115 +++++++ 5 files changed, 446 insertions(+), 272 deletions(-) create mode 100644 apps/backend/src/services/session/session-browser-config.js create mode 100644 apps/backend/src/services/session/session-payload.js create mode 100644 apps/backend/src/services/session/session-state.js diff --git a/apps/backend/src/services/session/session-browser-config.js b/apps/backend/src/services/session/session-browser-config.js new file mode 100644 index 00000000..7ccbe485 --- /dev/null +++ b/apps/backend/src/services/session/session-browser-config.js @@ -0,0 +1,101 @@ +import process from 'node:process' + +export const DEFAULT_LOGIN_TYPE = 'qq' + +export function resolveBrowserLaunchOptions(options = {}) { + const browserConfig = options.browserConfig || {} + const chromePath = options.chromePath || '' + const nodeEnv = options.nodeEnv ?? process.env.NODE_ENV + const ci = options.ci ?? process.env.CI + const explicitHeadless = browserConfig.headless + const explicitDevtools = browserConfig.devtools + const explicitKeepAlive = browserConfig.keepAlive + const explicitPrewarm = browserConfig.prewarm + const explicitSlowMo = browserConfig.slowMoMs + const isProductionLike = nodeEnv === 'production' || ci === '1' + + const headless = typeof explicitHeadless === 'boolean' ? explicitHeadless : isProductionLike + + const devtools = typeof explicitDevtools === 'boolean' ? explicitDevtools : !headless + + const slowMoMs = Number.isFinite(explicitSlowMo) + ? Math.max(0, Math.min(explicitSlowMo, 2_000)) + : headless + ? 0 + : 150 + + const keepAlive = typeof explicitKeepAlive === 'boolean' ? explicitKeepAlive : !isProductionLike + const prewarm = typeof explicitPrewarm === 'boolean' ? explicitPrewarm : keepAlive + + return { + headless, + devtools, + usesBundledChromium: !chromePath, + keepAlive, + prewarm, + slowMoMs, + } +} + +export function buildChromiumLaunchOptions(launchOptions, { chromePath = '' } = {}) { + const options = { + headless: launchOptions.headless, + devtools: launchOptions.devtools, + slowMo: launchOptions.slowMoMs, + } + + if (chromePath) { + return { + ...options, + executablePath: chromePath, + } + } + + return options +} + +export function resolveDefaultViewport(rawValue = process.env.TENCENT_BROWSER_VNC_RESOLUTION) { + const fromVncResolution = parseViewportSpec(rawValue) + if (fromVncResolution) { + return fromVncResolution + } + + return { width: 1600, height: 900 } +} + +export function parseViewportSpec(rawValue) { + const text = String(rawValue || '').trim() + + if (!text) { + return null + } + + const match = text.match(/^(\d+)x(\d+)(?:x\d+)?$/i) + if (!match) { + return null + } + + const width = Number(match[1]) + const height = Number(match[2]) + + if (!Number.isFinite(width) || !Number.isFinite(height) || width < 800 || height < 600) { + return null + } + + return { width, height } +} + +export function normalizeLoginType(value, fallback = DEFAULT_LOGIN_TYPE) { + const normalized = String(value || fallback).trim().toLowerCase() + if (['qq', 'qc'].includes(normalized)) { + return 'qq' + } + if (['wx', 'vx', 'wechat', 'weixin'].includes(normalized)) { + return 'wx' + } + return fallback +} + +export function getLoginTypeLabel(loginType) { + return loginType === 'wx' ? '微信' : 'QQ' +} diff --git a/apps/backend/src/services/session/session-payload.js b/apps/backend/src/services/session/session-payload.js new file mode 100644 index 00000000..9df357e0 --- /dev/null +++ b/apps/backend/src/services/session/session-payload.js @@ -0,0 +1,126 @@ +export function buildSessionPayload( + session, + { + activityUrl = '', + includeQrImage = true, + sessionDebugEnabled = false, + browserDebug = null, + } = {}, +) { + const activityInfo = session.lastState?.activityInfo || null + const payload = { + sessionId: session.sessionId, + loginType: session.loginType, + activityUrl, + status: session.status, + notice: session.notice, + createdAt: session.createdAt, + updatedAt: session.updatedAt, + expiresAt: new Date(session.expiresAt).toISOString(), + qrUpdatedAt: session.qrUpdatedAt, + credentialReady: Boolean(session.lastState?.credentialReady), + activityInfo, + review: buildReviewPayload(session.lastReview), + redeem: buildRedeemPayload(session.lastRedeem), + artifacts: buildArtifactsPayload(session), + } + + if (includeQrImage) { + payload.qrImageBase64 = session.qrImageBase64 + } + + if (!sessionDebugEnabled) { + return payload + } + + return { + ...payload, + cookieKeys: Array.isArray(session.lastState?.cookieKeys) ? session.lastState.cookieKeys : [], + state: session.lastState, + browserDebug, + qrImagePath: session.qrImagePath, + review: buildReviewPayload(session.lastReview, { includeInternalPaths: true }), + redeem: buildRedeemPayload(session.lastRedeem, { includeInternalPaths: true }), + artifacts: buildArtifactsPayload(session, { includeInternalPaths: true }), + } +} + +export function buildReviewPayload(review, { includeInternalPaths = false } = {}) { + if (!review) { + return null + } + + const payload = { + capturedAt: review.capturedAt, + roleId: review.roleId || '', + roleName: review.roleName || '', + } + + if (!includeInternalPaths) { + return payload + } + + return { + ...payload, + screenshotPath: review.screenshotPath || '', + signature: review.signature || '', + } +} + +export function buildRedeemPayload(redeem, { includeInternalPaths = false } = {}) { + if (!redeem) { + return null + } + + const payload = { + code: redeem.code, + area: redeem.area, + proofMode: redeem.proofMode || 'full', + attempts: Array.isArray(redeem.attempts) + ? redeem.attempts.map((attempt) => ({ + attempt: attempt.attempt, + redeem: attempt.redeem || null, + })) + : [], + final: redeem.final + ? { + attempt: redeem.final.attempt, + redeem: redeem.final.redeem || null, + } + : null, + finishedAt: redeem.finishedAt, + } + + if (!includeInternalPaths) { + return payload + } + + return { + ...payload, + screenshotPath: redeem.screenshotPath || '', + htmlPath: redeem.htmlPath || '', + resultPath: redeem.resultPath || '', + } +} + +export function buildArtifactsPayload(session, { includeInternalPaths = false } = {}) { + const payload = { + hasQrImage: Boolean(session.qrImageBase64), + hasReviewScreenshot: Boolean(session.lastReview?.screenshotPath), + hasScreenshot: Boolean(session.lastRedeem?.screenshotPath), + } + + if (!includeInternalPaths) { + return payload + } + + return { + ...payload, + sessionDir: session.sessionDir, + qrImagePath: session.qrImagePath, + screenshotPath: session.lastRedeem?.screenshotPath || '', + htmlPath: session.lastRedeem?.htmlPath || '', + resultPath: session.lastRedeem?.resultPath || '', + reviewScreenshotPath: session.lastReview?.screenshotPath || '', + } +} diff --git a/apps/backend/src/services/session/session-state.js b/apps/backend/src/services/session/session-state.js new file mode 100644 index 00000000..da9e4638 --- /dev/null +++ b/apps/backend/src/services/session/session-state.js @@ -0,0 +1,74 @@ +import { getLoginTypeLabel } from './session-browser-config.js' + +export const SESSION_TTL_MS = 30 * 60 * 1000 + +export function resolveTencentSessionExpiresAt(session, nowMs = Date.now(), sessionTtlMs = SESSION_TTL_MS) { + const autoCloseAt = Number(session?.autoCloseAt || 0) + if (autoCloseAt > 0) { + return autoCloseAt + } + + return nowMs + sessionTtlMs +} + +export function shouldRefreshLoggedInPresentation(hostState) { + const loginText = String(hostState?.loginText || '').trim() + + return Boolean(hostState?.unloginVisible || !hostState?.loginedVisible || !loginText) +} + +export function resolveSessionStatus({ hostState, qrState, credentialReady, activityInfo }) { + if (hostState.loginedVisible || credentialReady) { + if (credentialReady && activityInfo?.role?.ready) { + return 'ready_to_redeem' + } + + return 'logged_in' + } + + if (qrState.expired) { + return 'expired' + } + + if (qrState.scanned) { + return 'scanned' + } + + return 'waiting_scan' +} + +export function resolveSessionNotice({ + hostState, + qrState, + credentialReady, + activityInfo, + loginType, +}) { + const loginTypeLabel = getLoginTypeLabel(loginType) + + if (credentialReady) { + if (activityInfo?.role?.ready && activityInfo.role.roleName) { + return `登录已完成,当前角色 ${activityInfo.role.roleName}` + } + + return '登录已完成,正在同步角色信息' + } + + if (hostState.loginedVisible) { + return '页面已识别登录态' + } + + if (qrState.expired) { + return '二维码已失效,准备刷新' + } + + if (qrState.scanned) { + return '二维码已扫描,请在手机上确认登录' + } + + return `请使用${loginTypeLabel}扫码登录` +} + +export function hasRedeemCredential(cookieMap) { + return Boolean(cookieMap.openid && cookieMap.access_token && cookieMap.appid) +} diff --git a/apps/backend/src/services/session/session.js b/apps/backend/src/services/session/session.js index 55ec9bb8..86122a74 100644 --- a/apps/backend/src/services/session/session.js +++ b/apps/backend/src/services/session/session.js @@ -3,16 +3,25 @@ import crypto from 'node:crypto' import fs from 'node:fs/promises' import path from 'node:path' -import process from 'node:process' import { fileURLToPath } from 'node:url' import { chromium } from 'playwright' import { runtimeConfig } from '../../config/runtime.js' +import { + buildChromiumLaunchOptions as buildChromiumLaunchOptionsBase, + getLoginTypeLabel, + normalizeLoginType, + resolveBrowserLaunchOptions as resolveBrowserLaunchOptionsBase, + resolveDefaultViewport, +} from './session-browser-config.js' import { isRetryableQrCaptureError, logBrowserSessionDebug, } from './session-login-shared.js' +import { + buildSessionPayload as buildSessionPayloadBase, +} from './session-payload.js' import { captureQqQrImage, ensureQqLoginReady, @@ -35,14 +44,20 @@ import { extractWxQrState, } from './session-wx.js' import { recognizeTencentCaptcha } from './ocr.js' +import { + hasRedeemCredential, + resolveSessionNotice, + resolveSessionStatus, + resolveTencentSessionExpiresAt, + SESSION_TTL_MS, + shouldRefreshLoggedInPresentation, +} from './session-state.js' const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url)) const PROJECT_ROOT = path.resolve(CURRENT_DIR, '../../..') const DATA_ROOT = path.resolve(PROJECT_ROOT, 'data/browser-sessions') const ACTIVITY_URL = 'https://df.qq.com/cp/a20240812cdk/index.html' const CHROME_PATH = String(runtimeConfig.browser.chromePath || '').trim() -const DEFAULT_LOGIN_TYPE = 'qq' -const SESSION_TTL_MS = 30 * 60 * 1000 export const REDEEM_SUCCESS_AUTO_CLOSE_MS = 3 * 60 * 1000 const DEFAULT_VIEWPORT = resolveDefaultViewport() const SESSION_DEBUG_ENABLED = Boolean(runtimeConfig.session.debug) @@ -51,6 +66,8 @@ const sessions = new Map() let browserPromise = null +export { resolveTencentSessionExpiresAt } + class SessionServiceError extends Error { constructor(message, { statusCode = 500, errorCode = 'session_error' } = {}) { super(message) @@ -396,83 +413,14 @@ async function ensureBrowser() { } function resolveBrowserLaunchOptions() { - const explicitHeadless = runtimeConfig.browser.headless - const explicitDevtools = runtimeConfig.browser.devtools - const explicitKeepAlive = runtimeConfig.browser.keepAlive - const explicitPrewarm = runtimeConfig.browser.prewarm - const explicitSlowMo = runtimeConfig.browser.slowMoMs - const isProductionLike = process.env.NODE_ENV === 'production' || process.env.CI === '1' - - const headless = typeof explicitHeadless === 'boolean' ? explicitHeadless : isProductionLike - - const devtools = typeof explicitDevtools === 'boolean' ? explicitDevtools : !headless - - const slowMoMs = Number.isFinite(explicitSlowMo) - ? Math.max(0, Math.min(explicitSlowMo, 2_000)) - : headless - ? 0 - : 150 - - const keepAlive = typeof explicitKeepAlive === 'boolean' ? explicitKeepAlive : !isProductionLike - - const prewarm = typeof explicitPrewarm === 'boolean' ? explicitPrewarm : keepAlive - - return { - headless, - devtools, - usesBundledChromium: !CHROME_PATH, - keepAlive, - prewarm, - slowMoMs, - } + return resolveBrowserLaunchOptionsBase({ + browserConfig: runtimeConfig.browser, + chromePath: CHROME_PATH, + }) } function buildChromiumLaunchOptions(launchOptions) { - const options = { - headless: launchOptions.headless, - devtools: launchOptions.devtools, - slowMo: launchOptions.slowMoMs, - } - - if (CHROME_PATH) { - return { - ...options, - executablePath: CHROME_PATH, - } - } - - return options -} - -function resolveDefaultViewport() { - const fromVncResolution = parseViewportSpec(process.env.TENCENT_BROWSER_VNC_RESOLUTION) - if (fromVncResolution) { - return fromVncResolution - } - - return { width: 1600, height: 900 } -} - -function parseViewportSpec(rawValue) { - const text = String(rawValue || '').trim() - - if (!text) { - return null - } - - const match = text.match(/^(\d+)x(\d+)(?:x\d+)?$/i) - if (!match) { - return null - } - - const width = Number(match[1]) - const height = Number(match[2]) - - if (!Number.isFinite(width) || !Number.isFinite(height) || width < 800 || height < 600) { - return null - } - - return { width, height } + return buildChromiumLaunchOptionsBase(launchOptions, { chromePath: CHROME_PATH }) } function createSessionId() { @@ -485,21 +433,6 @@ function createSessionId() { return sessionId } -function normalizeLoginType(value) { - const normalized = String(value || DEFAULT_LOGIN_TYPE).trim().toLowerCase() - if (['qq', 'qc'].includes(normalized)) { - return 'qq' - } - if (['wx', 'vx', 'wechat', 'weixin'].includes(normalized)) { - return 'wx' - } - return DEFAULT_LOGIN_TYPE -} - -function getLoginTypeLabel(loginType) { - return loginType === 'wx' ? '微信' : 'QQ' -} - async function getRequiredSession(sessionId) { const key = String(sessionId || '').trim() @@ -633,15 +566,6 @@ function applyInitialQrSessionState(session) { session.expiresAt = Date.now() + SESSION_TTL_MS } -export function resolveTencentSessionExpiresAt(session, nowMs = Date.now()) { - const autoCloseAt = Number(session?.autoCloseAt || 0) - if (autoCloseAt > 0) { - return autoCloseAt - } - - return nowMs + SESSION_TTL_MS -} - function armRedeemedSessionAutoClose(session) { resetTencentSessionAutoClose(session) @@ -719,12 +643,6 @@ async function ensureLoggedInPresentation( } } -function shouldRefreshLoggedInPresentation(hostState) { - const loginText = String(hostState?.loginText || '').trim() - - return Boolean(hostState?.unloginVisible || !hostState?.loginedVisible || !loginText) -} - async function reloadActivityPageForPresentation(page) { try { await page.reload({ waitUntil: 'domcontentloaded' }) @@ -920,52 +838,6 @@ async function extractActivityInfo(page) { } } -function resolveSessionStatus({ hostState, qrState, credentialReady, activityInfo }) { - if (hostState.loginedVisible || credentialReady) { - if (credentialReady && activityInfo?.role?.ready) { - return 'ready_to_redeem' - } - - return 'logged_in' - } - - if (qrState.expired) { - return 'expired' - } - - if (qrState.scanned) { - return 'scanned' - } - - return 'waiting_scan' -} - -function resolveSessionNotice({ hostState, qrState, credentialReady, activityInfo, loginType }) { - const loginTypeLabel = getLoginTypeLabel(loginType) - - if (credentialReady) { - if (activityInfo?.role?.ready && activityInfo.role.roleName) { - return `登录已完成,当前角色 ${activityInfo.role.roleName}` - } - - return '登录已完成,正在同步角色信息' - } - - if (hostState.loginedVisible) { - return '页面已识别登录态' - } - - if (qrState.expired) { - return '二维码已失效,准备刷新' - } - - if (qrState.scanned) { - return '二维码已扫描,请在手机上确认登录' - } - - return `请使用${loginTypeLabel}扫码登录` -} - async function cleanupExpiredTencentBrowserSessions() { const expired = [...sessions.values()].filter((session) => session.expiresAt <= Date.now()) @@ -1004,122 +876,12 @@ async function persistSessionState(session) { } function buildSessionPayload(session, { includeQrImage = true } = {}) { - const activityInfo = session.lastState?.activityInfo || null - const payload = { - sessionId: session.sessionId, - loginType: session.loginType, + return buildSessionPayloadBase(session, { activityUrl: ACTIVITY_URL, - status: session.status, - notice: session.notice, - createdAt: session.createdAt, - updatedAt: session.updatedAt, - expiresAt: new Date(session.expiresAt).toISOString(), - qrUpdatedAt: session.qrUpdatedAt, - credentialReady: Boolean(session.lastState?.credentialReady), - activityInfo, - review: buildReviewPayload(session.lastReview), - redeem: buildRedeemPayload(session.lastRedeem), - artifacts: buildArtifactsPayload(session), - } - - if (includeQrImage) { - payload.qrImageBase64 = session.qrImageBase64 - } - - if (!SESSION_DEBUG_ENABLED) { - return payload - } - - return { - ...payload, - cookieKeys: Array.isArray(session.lastState?.cookieKeys) ? session.lastState.cookieKeys : [], - state: session.lastState, - browserDebug: resolveBrowserLaunchOptions(), - qrImagePath: session.qrImagePath, - review: buildReviewPayload(session.lastReview, { includeInternalPaths: true }), - redeem: buildRedeemPayload(session.lastRedeem, { includeInternalPaths: true }), - artifacts: buildArtifactsPayload(session, { includeInternalPaths: true }), - } -} - -function buildReviewPayload(review, { includeInternalPaths = false } = {}) { - if (!review) { - return null - } - - const payload = { - capturedAt: review.capturedAt, - roleId: review.roleId || '', - roleName: review.roleName || '', - } - - if (!includeInternalPaths) { - return payload - } - - return { - ...payload, - screenshotPath: review.screenshotPath || '', - signature: review.signature || '', - } -} - -function buildRedeemPayload(redeem, { includeInternalPaths = false } = {}) { - if (!redeem) { - return null - } - - const payload = { - code: redeem.code, - area: redeem.area, - proofMode: redeem.proofMode || 'full', - attempts: Array.isArray(redeem.attempts) - ? redeem.attempts.map((attempt) => ({ - attempt: attempt.attempt, - redeem: attempt.redeem || null, - })) - : [], - final: redeem.final - ? { - attempt: redeem.final.attempt, - redeem: redeem.final.redeem || null, - } - : null, - finishedAt: redeem.finishedAt, - } - - if (!includeInternalPaths) { - return payload - } - - return { - ...payload, - screenshotPath: redeem.screenshotPath || '', - htmlPath: redeem.htmlPath || '', - resultPath: redeem.resultPath || '', - } -} - -function buildArtifactsPayload(session, { includeInternalPaths = false } = {}) { - const payload = { - hasQrImage: Boolean(session.qrImageBase64), - hasReviewScreenshot: Boolean(session.lastReview?.screenshotPath), - hasScreenshot: Boolean(session.lastRedeem?.screenshotPath), - } - - if (!includeInternalPaths) { - return payload - } - - return { - ...payload, - sessionDir: session.sessionDir, - qrImagePath: session.qrImagePath, - screenshotPath: session.lastRedeem?.screenshotPath || '', - htmlPath: session.lastRedeem?.htmlPath || '', - resultPath: session.lastRedeem?.resultPath || '', - reviewScreenshotPath: session.lastReview?.screenshotPath || '', - } + includeQrImage, + sessionDebugEnabled: SESSION_DEBUG_ENABLED, + browserDebug: SESSION_DEBUG_ENABLED ? resolveBrowserLaunchOptions() : null, + }) } async function ensureReviewScreenshot(session, activityInfo) { @@ -1187,10 +949,6 @@ async function loadCookieMapFromContext(browserContext) { return cookieMap } -function hasRedeemCredential(cookieMap) { - return Boolean(cookieMap.openid && cookieMap.access_token && cookieMap.appid) -} - async function fillRedeemCodeInBrowser(page, code) { await prepareRedeemCodeFill(page, code, { ensureActivityInfoReady }) } diff --git a/apps/backend/src/services/session/session.test.js b/apps/backend/src/services/session/session.test.js index b7cb40b2..fb6593e2 100644 --- a/apps/backend/src/services/session/session.test.js +++ b/apps/backend/src/services/session/session.test.js @@ -1,6 +1,14 @@ import test from 'node:test' import assert from 'node:assert/strict' +import { + normalizeLoginType, + parseViewportSpec, +} from './session-browser-config.js' +import { + buildArtifactsPayload, + buildRedeemPayload, +} from './session-payload.js' import { REDEEM_SUCCESS_AUTO_CLOSE_MS, resolveTencentSessionExpiresAt, @@ -23,3 +31,110 @@ test('resolveTencentSessionExpiresAt keeps redeemed auto-close deadline once arm autoCloseAt, ) }) + +test('normalizeLoginType collapses aliases and falls back to qq', () => { + assert.equal(normalizeLoginType(' qc '), 'qq') + assert.equal(normalizeLoginType('wechat'), 'wx') + assert.equal(normalizeLoginType('unknown'), 'qq') + assert.equal(normalizeLoginType(''), 'qq') +}) + +test('parseViewportSpec accepts valid viewport strings and rejects invalid values', () => { + assert.deepEqual(parseViewportSpec('1920x1080'), { width: 1920, height: 1080 }) + assert.deepEqual(parseViewportSpec('1920x1080x24'), { width: 1920, height: 1080 }) + assert.equal(parseViewportSpec('799x600'), null) + assert.equal(parseViewportSpec('broken'), null) +}) + +test('buildRedeemPayload strips internal paths unless explicitly requested', () => { + const redeem = { + code: 'ABC123', + area: '微信区', + proofMode: 'basic', + attempts: [ + { + attempt: 1, + redeem: { success: false, message: '验证码错误' }, + }, + ], + final: { + attempt: 2, + redeem: { success: true, message: '兑换成功' }, + }, + finishedAt: '2026-04-14T09:40:00.000Z', + screenshotPath: '/tmp/redeem.png', + htmlPath: '/tmp/redeem.html', + resultPath: '/tmp/redeem.json', + } + + assert.deepEqual(buildRedeemPayload(redeem), { + code: 'ABC123', + area: '微信区', + proofMode: 'basic', + attempts: [ + { + attempt: 1, + redeem: { success: false, message: '验证码错误' }, + }, + ], + final: { + attempt: 2, + redeem: { success: true, message: '兑换成功' }, + }, + finishedAt: '2026-04-14T09:40:00.000Z', + }) + + assert.deepEqual(buildRedeemPayload(redeem, { includeInternalPaths: true }), { + code: 'ABC123', + area: '微信区', + proofMode: 'basic', + attempts: [ + { + attempt: 1, + redeem: { success: false, message: '验证码错误' }, + }, + ], + final: { + attempt: 2, + redeem: { success: true, message: '兑换成功' }, + }, + finishedAt: '2026-04-14T09:40:00.000Z', + screenshotPath: '/tmp/redeem.png', + htmlPath: '/tmp/redeem.html', + resultPath: '/tmp/redeem.json', + }) +}) + +test('buildArtifactsPayload only exposes internal artifact paths in debug mode', () => { + const session = { + sessionDir: '/tmp/session', + qrImagePath: '/tmp/session/qq-qr.png', + qrImageBase64: 'base64data', + lastReview: { + screenshotPath: '/tmp/session/review.png', + }, + lastRedeem: { + screenshotPath: '/tmp/session/redeem.png', + htmlPath: '/tmp/session/redeem.html', + resultPath: '/tmp/session/redeem.json', + }, + } + + assert.deepEqual(buildArtifactsPayload(session), { + hasQrImage: true, + hasReviewScreenshot: true, + hasScreenshot: true, + }) + + assert.deepEqual(buildArtifactsPayload(session, { includeInternalPaths: true }), { + hasQrImage: true, + hasReviewScreenshot: true, + hasScreenshot: true, + sessionDir: '/tmp/session', + qrImagePath: '/tmp/session/qq-qr.png', + screenshotPath: '/tmp/session/redeem.png', + htmlPath: '/tmp/session/redeem.html', + resultPath: '/tmp/session/redeem.json', + reviewScreenshotPath: '/tmp/session/review.png', + }) +})