Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
30 changes: 26 additions & 4 deletions scripts/api/checkInState.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ function streak(updatedAt = '2026-10-23T17:26:19.6377348+00:00') {
}
}
const recordedLegacy = legacy({ hidden: 'True', last_updated: '2024-10-18T08:21:27.1990138+00:00' })
// 签到推广自身记录了今天(北京时间 10-24)的签到
const checkedInToday = legacy({ last_updated: '2026-10-23T17:26:19.6377348+00:00' })

test('Main leaves the App remaining total unknown when only stale check-in data is available', async t => {
t.mock.timers.enable({ apis: ['Date'], now: now.getTime() })
Expand All @@ -53,7 +55,9 @@ test('Main leaves the App remaining total unknown when only stale check-in data
})

for (const [name, promotions, status, remaining] of [
['recorded current check-in overrides stale hidden promotion', [recordedLegacy, streak()], 'complete', 0],
// 连胜计数器会被每日任务等活动刷新到今天,签到前不能据此判定已签
['streak updated today by other activities is not a check-in', [recordedLegacy, streak()], 'unknown', null],
['streak alone is not a check-in', [streak()], 'unknown', null],
['stale hidden promotion alone does not invent five points', [recordedLegacy], 'unknown', null],
['missing timestamp remains unknown', [legacy({ last_updated: '' })], 'unknown', null],
['invalid timestamp remains unknown', [legacy({ last_updated: 'invalid' })], 'unknown', null],
Expand All @@ -69,7 +73,7 @@ for (const [name, promotions, status, remaining] of [
'pending',
5
],
['China date uses the date after UTC midnight offset', [streak()], 'complete', 0],
['China date uses the date after UTC midnight offset', [checkedInToday], 'complete', 0],
['previous China date is not already checked in', [legacy(), streak('2026-10-23T15:59:00Z')], 'pending', 5],
['seventh day reads its explicit reward', [legacy({ progress: '6', day_7_points: '20' })], 'pending', 20],
['new seven-day cycle returns to the first day', [legacy({ progress: '7' })], 'pending', 5],
Expand All @@ -96,7 +100,7 @@ for (const completed of [true, false]) {
bot.http.request = t.mock.fn(async () => ({
data: {
response: {
promotions: completed ? [recordedLegacy, streak()] : [recordedLegacy]
promotions: completed ? [checkedInToday] : [recordedLegacy]
}
}
}))
Expand All @@ -116,14 +120,32 @@ function checkInBot(t, promotions = [legacy()]) {
}

test('already checked-in account skips the repeated request', async t => {
const bot = checkInBot(t, [recordedLegacy, streak()])
const bot = checkInBot(t, [checkedInToday])
const activity = new DailyCheckIn(bot)
await activity.doDailyCheckIn()

assert.equal(bot.http.request.mock.callCount(), 0)
assert.equal(bot.userData.gainedPoints, 0)
})

test('streak refreshed by the daily set still submits the check-in and credits it', async t => {
const bot = checkInBot(t, [recordedLegacy, streak()])
bot.http.request = t.mock.fn(async () => ({ status: 200, data: { code: 0, response: { balance: 110 } } }))
await new DailyCheckIn(bot).doDailyCheckIn()

assert.equal(bot.http.request.mock.callCount(), 1)
assert.equal(bot.userData.gainedPoints, 10)
})

test('a repeated check-in with zero gain is confirmed by the streak record', async t => {
const bot = checkInBot(t, [recordedLegacy, streak()])
await new DailyCheckIn(bot).doDailyCheckIn()

assert.equal(bot.http.request.mock.callCount(), 1)
assert.equal(bot.userData.gainedPoints, 0)
assert.ok(bot.logger.info.mock.calls.some(call => /本次无新增积分/.test(call.arguments[2])))
})

test('a pending check-in credits only the observed balance change', async t => {
const bot = checkInBot(t)
bot.http.request = t.mock.fn(async () => ({ status: 200, data: { code: 0, response: { balance: 105 } } }))
Expand Down
9 changes: 7 additions & 2 deletions src/functions/activities/app/CheckInState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@ function dateKey(value: string | Date, country: string): string | null {
export function resolveCheckInState(
promotions: Pick<Promotion, 'name' | 'attributes'>[],
country: string,
now = new Date()
now = new Date(),
// 连胜计数器在完成每日任务等活动时也会刷新到今天,不能据此判断"签到前已签";
// 只在签到请求之后作为到账佐证使用
trustStreak = false
): CheckInState {
const today = dateKey(now, country)
const checkIn = promotions.find(item => item.attributes.offerid === 'Gamification_Sapphire_DailyCheckIn')
const streak = promotions.find(item => item.name === 'Gamification_Streak_Counter_Promotion')
const streak = trustStreak
? promotions.find(item => item.name === 'Gamification_Streak_Counter_Promotion')
: undefined
for (const item of [streak, checkIn]) {
const updatedAt = item?.attributes.last_updated
if (updatedAt && dateKey(updatedAt, country) === today) {
Expand Down
11 changes: 8 additions & 3 deletions src/functions/activities/app/DailyCheckIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export class DailyCheckIn extends BaseActivity {
'green'
)
} else {
const after = await this.getCheckInState()
const after = await this.getCheckInState(true)
if (after.status !== 'complete') {
throw new Error(
`签到未确认到账 | pointsGained=${this.gainedPoints} | currentBalance=${newBalance} | state=${after.status}`
Expand All @@ -99,10 +99,15 @@ export class DailyCheckIn extends BaseActivity {
}
}

private async getCheckInState(): Promise<CheckInState> {
private async getCheckInState(trustStreak = false): Promise<CheckInState> {
try {
const data = await this.bot.browser.func.getAppDashboardData('SAAndroid')
return resolveCheckInState(data.response?.promotions ?? [], this.bot.userData.geoLocale)
return resolveCheckInState(
data.response?.promotions ?? [],
this.bot.userData.geoLocale,
new Date(),
trustStreak
)
} catch (error) {
this.bot.logger.warn(
this.bot.isMobile,
Expand Down