Files
Genarrative/apps/ai-game-creator-shell/tests/tauriEventSubscription.test.ts
T
lhk229 70271b408e
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 7m19s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 7m35s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 7m34s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 7m39s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m13s
Project CI / AI game creator shell Rust crates (push) Successful in 3m18s
Project CI / Backend tests (push) Successful in 9m16s
Project CI / Repository checks (push) Successful in 8m11s
Project CI / Native shell tests (push) Successful in 11m56s
Project CI / Frontend tests (push) Successful in 11m49s
Project CI / AI game creator shell web tests (push) Successful in 6m37s
删除策划agent v2,与退役功能解耦 (#355)
Reviewed-on: #355
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-09-20 14:49:57 +08:00

163 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @vitest-environment jsdom
/**
* 「注销竞态」回归用例(tauri-apps/tauri#15799):
* 运行模块切换版本时会重渲染并成对触发 AGC 的事件订阅/注销,`React.StrictMode`
* 更是挂载即注销。tauri 2.11 的库内注销脚本会先读监听注册表条目,而条目由注册
* eval 异步写入,于是「订阅后立刻注销」会抛
* `undefined is not an object (evaluating 'listeners[eventId].handlerId')`
* 且抛在 invoke 之前 → 后端订阅泄漏。这里用与 tauri 2.11 等价的替身锁死行为。
*/
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
canSubscribeTauriEvents,
subscribeTauriEvent,
} from '../src/services/tauriEventSubscription';
import { createTauriEventFake, type TauriEventFake } from './tauriEventFake';
let fake: TauriEventFake | null = null;
afterEach(() => {
fake?.restore();
fake = null;
vi.restoreAllMocks();
});
describe('subscribeTauriEvent', () => {
it('注册 eval 还没落地就注销:不抛错、不读注册表条目、不漏后端订阅', async () => {
fake = createTauriEventFake();
fake.install();
const handler = vi.fn();
const unsubscribe = await subscribeTauriEvent(
'game-creator-agent-progress',
handler,
);
expect(fake.pendingRegistrationEvals()).toBe(1);
// 注册 eval 还没落地(tauri 2.11 的竞态窗口):注销必须安全。
unsubscribe();
expect(fake.racyUnregisterViolations).toEqual([]);
expect(fake.racyUnregisterCalls).toBe(0);
expect(fake.listeners).toEqual([]);
expect(fake.jsCallbackCount()).toBe(0);
expect(fake.unlistenInvokeCounts.get('game-creator-agent-progress')).toBe(
1,
);
});
it('同一事件已有注册条目落地时,注销还没落地的订阅也不该抛', async () => {
fake = createTauriEventFake();
fake.install();
// 先让第一个订阅的注册 eval 落地:该事件的监听对象因此存在。
const firstUnsubscribe = await subscribeTauriEvent(
'game-creator-agent-runtime-update',
vi.fn(),
);
fake.flushRegistrationEvals();
// 重订阅(StrictMode 二次挂载 / 依赖变化都会重订阅),它的注册 eval 还没落地。
// 这就是报错现场:事件对象存在、新 eventId 的条目不存在。
const secondUnsubscribe = await subscribeTauriEvent(
'game-creator-agent-runtime-update',
vi.fn(),
);
secondUnsubscribe();
expect(fake.racyUnregisterViolations).toEqual([]);
expect(
fake.unlistenInvokeCounts.get('game-creator-agent-runtime-update'),
).toBe(1);
expect(fake.listeners).toHaveLength(1);
expect(fake.jsCallbackCount()).toBe(1);
firstUnsubscribe();
expect(fake.listeners).toEqual([]);
expect(fake.jsCallbackCount()).toBe(0);
});
it('重复注销只摘一次后端订阅', async () => {
fake = createTauriEventFake();
fake.install();
const unsubscribe = await subscribeTauriEvent(
'game-creator-manifest-invalidated',
vi.fn(),
);
unsubscribe();
unsubscribe();
unsubscribe();
expect(
fake.unlistenInvokeCounts.get('game-creator-manifest-invalidated'),
).toBe(1);
expect(fake.racyUnregisterViolations).toEqual([]);
});
it('注册 eval 落地后事件能投递,注销后不再投递', async () => {
fake = createTauriEventFake();
fake.install();
const handler = vi.fn();
const unsubscribe = await subscribeTauriEvent<{ message: string }>(
'game-creator-agent-progress',
handler,
);
fake.flushRegistrationEvals();
fake.emit('game-creator-agent-progress', { message: '正在生成' });
expect(handler).toHaveBeenCalledTimes(1);
expect(handler.mock.calls[0][0]).toMatchObject({
event: 'game-creator-agent-progress',
payload: { message: '正在生成' },
});
unsubscribe();
fake.emit('game-creator-agent-progress', { message: '不该再收到' });
expect(handler).toHaveBeenCalledTimes(1);
});
it('没有原生 internals 时回落到注入的事件桥接(jsdom 替身 / 浏览器预览)', async () => {
fake = createTauriEventFake();
fake.install();
// 只留全局桥,模拟各套件里 `event: { listen }` 的替身环境。
delete (window as unknown as Record<string, unknown>).__TAURI_INTERNALS__;
expect(canSubscribeTauriEvents()).toBe(true);
const handler = vi.fn();
const unsubscribe = await subscribeTauriEvent(
'game-creator-agent-progress',
handler,
);
expect(fake.bridgeListenCalls).toBe(1);
fake.emit('game-creator-agent-progress', { message: 'hi' });
fake.flushRegistrationEvals();
fake.emit('game-creator-agent-progress', { message: 'hi' });
expect(handler).toHaveBeenCalledTimes(1);
unsubscribe();
unsubscribe();
expect(fake.unlistenInvokeCounts.get('game-creator-agent-progress')).toBe(
1,
);
});
it('没有任何桥接时返回空操作', async () => {
delete (window as unknown as Record<string, unknown>).__TAURI__;
delete (window as unknown as Record<string, unknown>).__TAURI_INTERNALS__;
expect(canSubscribeTauriEvents()).toBe(false);
const unsubscribe = await subscribeTauriEvent(
'error-report-updated',
vi.fn(),
);
expect(typeof unsubscribe).toBe('function');
expect(() => unsubscribe()).not.toThrow();
});
});