mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-06 05:27:48 +08:00
chore: 打包配置 测试用例
This commit is contained in:
@@ -55,7 +55,10 @@ describe('AgentHubConversationStore', () => {
|
||||
|
||||
const conversation = store.get('user-a')
|
||||
expect(conversation?.messages.map((item) => item.direction)).toEqual(['in', 'out'])
|
||||
expect(statSync(filePath).mode & 0o777).toBe(0o600)
|
||||
// Windows 没有 POSIX 权限位,chmod 0600 是空操作 ⇒ 只在 POSIX 上断言。
|
||||
if (process.platform !== 'win32') {
|
||||
expect(statSync(filePath).mode & 0o777).toBe(0o600)
|
||||
}
|
||||
})
|
||||
|
||||
it('保留发送失败的记录与错误码,便于在界面上对账', () => {
|
||||
|
||||
@@ -47,6 +47,16 @@ const readyCapability: PersonalWechatSendCapability = {
|
||||
|
||||
const NOW = 1_700_000_000_000
|
||||
|
||||
/** 本地时区的 `YYYY-MM-DD HH:mm:ss`:`{time}` 按本地时区渲染,写死偏移量只在东八区通过。 */
|
||||
function localStamp(epochMs: number): string {
|
||||
const date = new Date(epochMs)
|
||||
const pad = (value: number): string => String(value).padStart(2, '0')
|
||||
return (
|
||||
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
|
||||
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
)
|
||||
}
|
||||
|
||||
function exitEvent(overrides: Partial<GroupMemberExitedEvent> = {}): GroupMemberExitedEvent {
|
||||
return {
|
||||
eventId: 'room@chatroom:wxid_member:1700000000000:1',
|
||||
@@ -338,7 +348,10 @@ describe('AutomationService.handleGroupExit', () => {
|
||||
|
||||
it('发送能力缺失 → execution failed(退群事实不受影响)', async () => {
|
||||
const harness = createHarness()
|
||||
harness.setCapability({ ...readyCapability, capabilities: { text: false, image: false, voice: false } })
|
||||
harness.setCapability({
|
||||
...readyCapability,
|
||||
capabilities: { text: false, image: false, voice: false }
|
||||
})
|
||||
|
||||
await harness.service.handleGroupExit(exitEvent())
|
||||
|
||||
@@ -395,7 +408,7 @@ describe('AutomationService.handleGroupExit', () => {
|
||||
if (content.type === 'text') {
|
||||
// 群名({groupName})与成员群昵称({groupRemark})是两个值,不能混。
|
||||
expect(content.text).toBe(
|
||||
'产品测试群|微信昵称|群内备注|wxid_member|243->242|2023-11-15 06:13:20'
|
||||
`产品测试群|微信昵称|群内备注|wxid_member|243->242|${localStamp(NOW)}`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -60,7 +60,10 @@ const {
|
||||
pruneIntelMacKeyTool,
|
||||
pruneForeignArchConnectors,
|
||||
pruneForeignArchNativeRuntimes,
|
||||
validateRuntimeBinaryArchitecture
|
||||
validateRuntimeBinaryArchitecture,
|
||||
findSendRuntime,
|
||||
enforceSendRuntimeBoundary,
|
||||
default: afterPack
|
||||
} = nodeRequire('../../scripts/after-pack.cjs') as {
|
||||
pruneIntelMacKeyTool: (
|
||||
runtimeResources: string,
|
||||
@@ -83,6 +86,20 @@ const {
|
||||
arch: string,
|
||||
label: string
|
||||
) => void
|
||||
findSendRuntime: (runtimeResources: string) => string | null
|
||||
enforceSendRuntimeBoundary: (
|
||||
runtimeResources: string,
|
||||
platform: NodeJS.Platform,
|
||||
bundlesSendRuntime?: boolean
|
||||
) => string | null
|
||||
default: (context: AfterPackContext) => Promise<void>
|
||||
}
|
||||
|
||||
type AfterPackContext = {
|
||||
appOutDir: string
|
||||
electronPlatformName: NodeJS.Platform
|
||||
arch?: number
|
||||
packager: { appInfo: { productFilename: string } }
|
||||
}
|
||||
const { readBinaryArchitectures } = nodeRequire('../../scripts/binary-arch.cjs') as {
|
||||
readBinaryArchitectures: (filePath: string) => string[]
|
||||
@@ -435,6 +452,121 @@ describe('per-architecture macOS packaging', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('send runtime packaging boundary', () => {
|
||||
const boundaryRoot = mkdtempSync(join(tmpdir(), 'wxe-send-runtime-'))
|
||||
afterAll(() => rmSync(boundaryRoot, { recursive: true, force: true }))
|
||||
|
||||
function resourcesWithSendRuntime(name: string, unpacked = false): string {
|
||||
const resources = join(boundaryRoot, name, 'Contents', 'Resources')
|
||||
const runtime = unpacked
|
||||
? join(resources, 'app.asar.unpacked', 'resources', 'runtime', 'darwin-arm64')
|
||||
: join(resources, 'resources', 'runtime', 'darwin-arm64')
|
||||
mkdirSync(runtime, { recursive: true })
|
||||
writeFileSync(join(runtime, 'tm-wechat-host'), 'fixture')
|
||||
return resources
|
||||
}
|
||||
|
||||
it('accepts a macOS bundle without the send runtime', () => {
|
||||
const resources = join(boundaryRoot, 'clean', 'Contents', 'Resources')
|
||||
mkdirSync(resources, { recursive: true })
|
||||
|
||||
expect(findSendRuntime(resources)).toBeNull()
|
||||
expect(enforceSendRuntimeBoundary(resources, 'darwin', false)).toBeNull()
|
||||
})
|
||||
|
||||
it('fails a bundle that still carries the send runtime', () => {
|
||||
expect(() =>
|
||||
enforceSendRuntimeBoundary(resourcesWithSendRuntime('dirty'), 'darwin', false)
|
||||
).toThrow(/must not include the WeChat send runtime/)
|
||||
})
|
||||
|
||||
it('detects the send runtime when it lands under app.asar.unpacked', () => {
|
||||
const resources = resourcesWithSendRuntime('unpacked', true)
|
||||
|
||||
expect(findSendRuntime(resources)).toBe(
|
||||
join(resources, 'app.asar.unpacked', 'resources', 'runtime', 'darwin-arm64')
|
||||
)
|
||||
expect(() => enforceSendRuntimeBoundary(resources, 'darwin', false)).toThrow(
|
||||
/must not include the WeChat send runtime/
|
||||
)
|
||||
})
|
||||
|
||||
it('requires the send runtime when the build is configured to bundle it', () => {
|
||||
const missing = join(boundaryRoot, 'runtime-missing', 'Contents', 'Resources')
|
||||
mkdirSync(missing, { recursive: true })
|
||||
|
||||
expect(() => enforceSendRuntimeBoundary(missing, 'darwin', true)).toThrow(
|
||||
/requires the WeChat send runtime/
|
||||
)
|
||||
|
||||
const present = resourcesWithSendRuntime('runtime-ok')
|
||||
expect(enforceSendRuntimeBoundary(present, 'darwin', true)).toBe(
|
||||
join(present, 'resources', 'runtime', 'darwin-arm64')
|
||||
)
|
||||
})
|
||||
|
||||
it('ignores non-macOS bundles', () => {
|
||||
const resources = resourcesWithSendRuntime('win32')
|
||||
|
||||
expect(enforceSendRuntimeBoundary(resources, 'win32', false)).toBeNull()
|
||||
expect(enforceSendRuntimeBoundary(resources, 'win32', true)).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps the send runtime build off the release publishing path', () => {
|
||||
const pkg = JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf8')) as {
|
||||
scripts: Record<string, string>
|
||||
}
|
||||
|
||||
expect(Object.keys(pkg.scripts).filter((name) => name.includes('send-runtime'))).toEqual([
|
||||
'build:mac:arm64:send-runtime'
|
||||
])
|
||||
expect(pkg.scripts['build:mac:arm64:send-runtime']).toContain('TM_SEND_RUNTIME_BUILD=1')
|
||||
expect(pkg.scripts['build:mac:arm64:send-runtime']).toContain(
|
||||
'electron-builder.send-runtime.yml'
|
||||
)
|
||||
expect(pkg.scripts['build:mac:arm64:send-runtime']).not.toContain('--publish')
|
||||
|
||||
for (const [name, command] of Object.entries(pkg.scripts)) {
|
||||
if (!name.startsWith('release')) continue
|
||||
expect(command).not.toContain('electron-builder.send-runtime.yml')
|
||||
expect(command).not.toContain('TM_SEND_RUNTIME_BUILD')
|
||||
}
|
||||
})
|
||||
|
||||
it('excludes the send runtime in the default builder config', () => {
|
||||
const config = readFileSync(resolve(__dirname, '../../electron-builder.yml'), 'utf8')
|
||||
|
||||
expect(config).toContain("'!runtime/darwin-arm64/**'")
|
||||
})
|
||||
|
||||
// 只测函数是测不出「边界校验有没有被接上去」的。这个 bundle 故意不含 app.asar 与
|
||||
// Reader Skill:一旦 afterPack 把边界校验排到其它校验之后,抛出的就会是
|
||||
// "Missing packaged application archive" 而不是边界错误,用例即失败。
|
||||
it('applies the boundary check inside afterPack ahead of every other validation', async () => {
|
||||
const appOutDir = join(boundaryRoot, 'wired')
|
||||
const runtime = join(
|
||||
appOutDir,
|
||||
'TraceMemo.app',
|
||||
'Contents',
|
||||
'Resources',
|
||||
'resources',
|
||||
'runtime',
|
||||
'darwin-arm64'
|
||||
)
|
||||
mkdirSync(runtime, { recursive: true })
|
||||
writeFileSync(join(runtime, 'tm-wechat-host'), 'fixture')
|
||||
|
||||
await expect(
|
||||
afterPack({
|
||||
appOutDir,
|
||||
electronPlatformName: 'darwin',
|
||||
arch: 3,
|
||||
packager: { appInfo: { productFilename: 'TraceMemo' } }
|
||||
})
|
||||
).rejects.toThrow(/must not include the WeChat send runtime/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('release publishing policy', () => {
|
||||
it('uploads GitHub releases as drafts until the notes are reviewed', () => {
|
||||
for (const configFile of ['electron-builder.yml', 'electron-builder.win.yml']) {
|
||||
|
||||
@@ -36,15 +36,15 @@ const onlineAgentHubStatus = (): AgentHubStatus => ({
|
||||
})
|
||||
|
||||
const SCHEDULE_TIME = '18:30'
|
||||
|
||||
/** 按本地时区构造 ISO 串:调度走 `Date#setHours`,写死偏移量会让用例只在东八区通过。 */
|
||||
const localISO = (year: number, month: number, day: number, hour: number, minute: number): string =>
|
||||
new Date(year, month - 1, day, hour, minute, 0, 0).toISOString()
|
||||
|
||||
/** 一个"当天 18:30 之后"的时刻,保证当日槽位已到点。 */
|
||||
const AFTER_SLOT = new Date('2026-08-27T18:31:00+08:00')
|
||||
/**
|
||||
* 规则的创建时间:**固定**在 2026-08-20。
|
||||
*
|
||||
* 必须早于所有参与断言的槽位,否则「槽位晚于创建时间」这道闸会把一切挡住;
|
||||
* 也绝不能用 `Date.now()` —— 那样断言会随墙上时钟漂移。
|
||||
*/
|
||||
const CREATED_AT = Date.parse('2026-08-20T00:00:00+08:00')
|
||||
const AFTER_SLOT = new Date(2026, 7, 27, 18, 31)
|
||||
/** 必须早于所有参与断言的槽位,且固定,不能用 `Date.now()`。 */
|
||||
const CREATED_AT = new Date(2026, 7, 20, 0, 0).getTime()
|
||||
|
||||
function makeRule(overrides: Partial<AutomationRule> = {}): AutomationRule {
|
||||
return {
|
||||
@@ -114,24 +114,18 @@ describe('定时日报 · 时间计算', () => {
|
||||
expect(validateScheduleTime('09:05')).toBe(true)
|
||||
expect(validateScheduleTime('24:00')).toBe(false)
|
||||
expect(validateScheduleTime('9:5')).toBe(false)
|
||||
const from = new Date('2026-08-27T10:00:00+08:00')
|
||||
expect(calculateNextRunAt('18:30', from)).toBe(
|
||||
new Date('2026-08-27T18:30:00+08:00').toISOString()
|
||||
)
|
||||
const from = new Date(2026, 7, 27, 10, 0)
|
||||
expect(calculateNextRunAt('18:30', from)).toBe(localISO(2026, 8, 27, 18, 30))
|
||||
// 已经过了今天的点 ⇒ 顺延到明天。
|
||||
expect(calculateNextRunAt('09:00', from)).toBe(
|
||||
new Date('2026-08-28T09:00:00+08:00').toISOString()
|
||||
)
|
||||
expect(calculateNextRunAt('09:00', from)).toBe(localISO(2026, 8, 28, 9, 0))
|
||||
})
|
||||
|
||||
it('resolveDueScheduledSlot 给出"已经到点、最近的那个"槽位', () => {
|
||||
// 18:31 时,18:30 已经到点 ⇒ 槽位是今天 18:30。
|
||||
expect(resolveDueScheduledSlot(SCHEDULE_TIME, AFTER_SLOT)).toBe(
|
||||
new Date('2026-08-27T18:30:00+08:00').toISOString()
|
||||
)
|
||||
expect(resolveDueScheduledSlot(SCHEDULE_TIME, AFTER_SLOT)).toBe(localISO(2026, 8, 27, 18, 30))
|
||||
// 18:29 时,今天的点还没到 ⇒ 最近一个槽位是昨天。
|
||||
expect(resolveDueScheduledSlot(SCHEDULE_TIME, new Date('2026-08-27T18:29:00+08:00'))).toBe(
|
||||
new Date('2026-08-26T18:30:00+08:00').toISOString()
|
||||
expect(resolveDueScheduledSlot(SCHEDULE_TIME, new Date(2026, 7, 27, 18, 29))).toBe(
|
||||
localISO(2026, 8, 26, 18, 30)
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -148,7 +142,7 @@ describe('定时日报 · 调度器', () => {
|
||||
expect(ruleId).toBe('rule-1')
|
||||
expect(options).toEqual({
|
||||
trigger: 'schedule',
|
||||
scheduledSlot: new Date('2026-08-27T18:30:00+08:00').toISOString()
|
||||
scheduledSlot: localISO(2026, 8, 27, 18, 30)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -159,7 +153,7 @@ describe('定时日报 · 调度器', () => {
|
||||
schedule: { time: SCHEDULE_TIME },
|
||||
report: { sourceConversationId: 'g1@chatroom', messageTypes: ['text'] }
|
||||
}),
|
||||
lastScheduledSlot: new Date('2026-08-27T18:30:00+08:00').toISOString()
|
||||
lastScheduledSlot: localISO(2026, 8, 27, 18, 30)
|
||||
}
|
||||
})
|
||||
const { service, executeRule } = await makeHarness({}, [rule])
|
||||
@@ -209,12 +203,12 @@ describe('定时日报 · 调度器', () => {
|
||||
schedule: { time: SCHEDULE_TIME },
|
||||
report: { sourceConversationId: 'g1@chatroom', messageTypes: ['text'] }
|
||||
}),
|
||||
lastScheduledSlot: new Date('2026-08-26T18:30:00+08:00').toISOString()
|
||||
lastScheduledSlot: localISO(2026, 8, 26, 18, 30)
|
||||
}
|
||||
})
|
||||
const { service, executeRule } = await makeHarness({}, [rule])
|
||||
|
||||
await service.tick(new Date('2026-08-27T18:29:00+08:00'))
|
||||
await service.tick(new Date(2026, 7, 27, 18, 29))
|
||||
await service.settle()
|
||||
|
||||
expect(executeRule).not.toHaveBeenCalled()
|
||||
@@ -224,10 +218,10 @@ describe('定时日报 · 调度器', () => {
|
||||
// 今天 12:00 建的规则、执行时间 18:30。今天 18:30 还没到,
|
||||
// 最近的槽位是**昨天 18:30** —— 它早于 createdAt,不属于这条规则。
|
||||
// 少了这道闸,新建规则会在下一次 15 秒 tick 里立刻发一份昨天的报告。
|
||||
const rule = makeRule({ createdAt: Date.parse('2026-08-27T12:00:00+08:00') })
|
||||
const rule = makeRule({ createdAt: new Date(2026, 7, 27, 12, 0).getTime() })
|
||||
const { service, executeRule } = await makeHarness({}, [rule])
|
||||
|
||||
await service.tick(new Date('2026-08-27T18:29:00+08:00'))
|
||||
await service.tick(new Date(2026, 7, 27, 18, 29))
|
||||
await service.settle()
|
||||
|
||||
expect(executeRule).not.toHaveBeenCalled()
|
||||
@@ -242,18 +236,18 @@ describe('定时日报 · 调度器', () => {
|
||||
schedule: { time: SCHEDULE_TIME },
|
||||
report: { sourceConversationId: 'g1@chatroom', messageTypes: ['text'] }
|
||||
}),
|
||||
lastScheduledSlot: new Date('2026-08-25T18:30:00+08:00').toISOString()
|
||||
lastScheduledSlot: localISO(2026, 8, 25, 18, 30)
|
||||
}
|
||||
})
|
||||
const { service, executeRule } = await makeHarness({}, [rule])
|
||||
|
||||
await service.tick(new Date('2026-08-27T18:29:00+08:00'))
|
||||
await service.tick(new Date(2026, 7, 27, 18, 29))
|
||||
await service.settle()
|
||||
|
||||
expect(executeRule).toHaveBeenCalledTimes(1)
|
||||
expect(executeRule.mock.calls[0][1]).toEqual({
|
||||
trigger: 'schedule',
|
||||
scheduledSlot: new Date('2026-08-26T18:30:00+08:00').toISOString()
|
||||
scheduledSlot: localISO(2026, 8, 26, 18, 30)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -264,7 +258,7 @@ describe('定时日报 · 调度器', () => {
|
||||
schedule: { time: SCHEDULE_TIME },
|
||||
report: { sourceConversationId: 'g1@chatroom', messageTypes: ['text'] }
|
||||
}),
|
||||
lastScheduledSlot: new Date('2026-08-27T18:30:00+08:00').toISOString()
|
||||
lastScheduledSlot: localISO(2026, 8, 27, 18, 30)
|
||||
}
|
||||
})
|
||||
const { service, executeRule } = await makeHarness({}, [rule])
|
||||
@@ -272,7 +266,7 @@ describe('定时日报 · 调度器', () => {
|
||||
// 18:31 时最近槽位正好等于游标 ⇒ 不触发;再把时间回拨到 18:29,
|
||||
// 槽位退化成 08-26 18:30,仍然**比游标旧** ⇒ 同样不触发。
|
||||
await service.tick(AFTER_SLOT)
|
||||
await service.tick(new Date('2026-08-27T18:29:00+08:00'))
|
||||
await service.tick(new Date(2026, 7, 27, 18, 29))
|
||||
await service.settle()
|
||||
|
||||
expect(executeRule).not.toHaveBeenCalled()
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('sticker HTTP failures', () => {
|
||||
})
|
||||
|
||||
describe('personal WeChat runtime security invariants', () => {
|
||||
it('waits for the macOS native runtime to detach during application shutdown', () => {
|
||||
it('keeps the macOS native runtime alive across application shutdown', () => {
|
||||
const mainSource = readFileSync(resolve('src/main/index.ts'), 'utf8')
|
||||
const shutdownStart = mainSource.indexOf("app.on('before-quit'")
|
||||
const shutdownEnd = mainSource.indexOf('function showMainWindow', shutdownStart)
|
||||
@@ -62,7 +62,10 @@ describe('personal WeChat runtime security invariants', () => {
|
||||
expect(shutdownStart).toBeGreaterThanOrEqual(0)
|
||||
expect(shutdownEnd).toBeGreaterThan(shutdownStart)
|
||||
expect(shutdownSource).toContain('await Promise.all([')
|
||||
expect(shutdownSource).toContain('macWechatRuntimeManager.shutdown()')
|
||||
// host 要活过 TraceMemo(保留发送能力进程,保住已绑定的 frida 会话),
|
||||
// 所以退出流程里**不得**关掉它 —— 否则每次重启都要重新绑定微信。
|
||||
// 它有自己的生命周期:微信消失后自判 stale 并自退,也可从设置卡片手动重载。
|
||||
expect(shutdownSource).not.toContain('macWechatRuntimeManager.shutdown()')
|
||||
})
|
||||
|
||||
it('does not install Python packages while preparing the native runtime', () => {
|
||||
|
||||
@@ -58,7 +58,10 @@ describe('wechat-ilink account store', () => {
|
||||
expect(path).toBe(
|
||||
join(home, '.tracememo', 'wechat-connector', 'accounts', 'bot_abc-im-bot.json')
|
||||
)
|
||||
expect(statSync(path).mode & 0o777).toBe(0o600)
|
||||
// Windows 没有 POSIX 权限位,chmod 0600 是空操作 ⇒ 只在 POSIX 上断言。
|
||||
if (process.platform !== 'win32') {
|
||||
expect(statSync(path).mode & 0o777).toBe(0o600)
|
||||
}
|
||||
expect(loadAllCredentials(() => home)).toEqual([credentials])
|
||||
})
|
||||
|
||||
@@ -150,7 +153,10 @@ describe('wechat-ilink account store', () => {
|
||||
accountsDirectory(() => home),
|
||||
`${accountId}.context.json`
|
||||
)
|
||||
expect(statSync(path).mode & 0o777).toBe(0o600)
|
||||
// Windows 没有 POSIX 权限位,chmod 0600 是空操作 ⇒ 只在 POSIX 上断言。
|
||||
if (process.platform !== 'win32') {
|
||||
expect(statSync(path).mode & 0o777).toBe(0o600)
|
||||
}
|
||||
|
||||
const stored = JSON.parse(readFileSync(path, 'utf8')) as {
|
||||
tokens: Record<string, { context_token: string }>
|
||||
|
||||
@@ -45,7 +45,10 @@ describe('WechatInboundInbox', () => {
|
||||
|
||||
expect(accepted.map((entry) => entry.messageId)).toEqual(['1', '2'])
|
||||
expect(accepted[0].contextToken).toBe('ctx-1')
|
||||
expect(statSync(filePath).mode & 0o777).toBe(0o600)
|
||||
// Windows 没有 POSIX 权限位,chmod 0600 是空操作 ⇒ 只在 POSIX 上断言。
|
||||
if (process.platform !== 'win32') {
|
||||
expect(statSync(filePath).mode & 0o777).toBe(0o600)
|
||||
}
|
||||
expect(inbox.size()).toBe(2)
|
||||
})
|
||||
|
||||
@@ -111,6 +114,9 @@ describe('WechatInboundInbox', () => {
|
||||
|
||||
const raw = readFileSync(filePath, 'utf8')
|
||||
expect(raw).toContain('机密内容')
|
||||
expect(statSync(filePath).mode & 0o777).toBe(0o600)
|
||||
// Windows 没有 POSIX 权限位,chmod 0600 是空操作 ⇒ 只在 POSIX 上断言。
|
||||
if (process.platform !== 'win32') {
|
||||
expect(statSync(filePath).mode & 0o777).toBe(0o600)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user