diff --git a/app/main.ts b/app/main.ts index c2c7aa75c..adc56736f 100644 --- a/app/main.ts +++ b/app/main.ts @@ -1005,9 +1005,7 @@ async function createWindow() { mainWindow.webContents.send('ci:event', 'db-initialized', {}); const shouldShowWindow = - !app.getLoginItemSettings().wasOpenedAsHidden && - !startInTray && - !config.get('ciIsBackupIntegration'); + !app.getLoginItemSettings().wasOpenedAsHidden && !startInTray; if (shouldShowWindow) { getLogger().info('showing main window'); @@ -2748,12 +2746,11 @@ ipc.on('get-config', async event => { : getEnvironment(), isMockTestEnvironment: Boolean(process.env.MOCK_TEST), ciMode, + ciForceUnprocessed: config.get('ciForceUnprocessed'), devTools: defaultWebPrefs.devTools, // Should be already computed and cached at this point dnsFallback: await getDNSFallback(), disableIPv6: DISABLE_IPV6, - ciBackupPath: config.get('ciBackupPath') || undefined, - ciIsBackupIntegration: config.get('ciIsBackupIntegration'), nodeVersion: process.versions.node, hostname: os.hostname(), osRelease: os.release(), diff --git a/config/default.json b/config/default.json index d0061ab74..f16acde42 100644 --- a/config/default.json +++ b/config/default.json @@ -17,8 +17,7 @@ "registrationChallengeUrl": "https://signalcaptchas.org/staging/registration/generate.html", "updatesEnabled": false, "ciMode": false, - "ciBackupPath": null, - "ciIsBackupIntegration": false, + "ciForceUnprocessed": false, "forcePreloadBundle": false, "openDevTools": false, "buildCreation": 0, diff --git a/ts/CI.ts b/ts/CI.ts index 647415b32..411bb8b8a 100644 --- a/ts/CI.ts +++ b/ts/CI.ts @@ -42,13 +42,18 @@ export type CIType = { unlink: () => void; print: (...args: ReadonlyArray) => void; resetReleaseNotesFetcher(): void; + forceUnprocessed: boolean; }; export type GetCIOptionsType = Readonly<{ deviceName: string; + forceUnprocessed: boolean; }>; -export function getCI({ deviceName }: GetCIOptionsType): CIType { +export function getCI({ + deviceName, + forceUnprocessed, +}: GetCIOptionsType): CIType { const eventListeners = new Map>(); const completedEvents = new Map>(); @@ -208,5 +213,6 @@ export function getCI({ deviceName }: GetCIOptionsType): CIType { getPendingEventCount, print, resetReleaseNotesFetcher, + forceUnprocessed, }; } diff --git a/ts/test-mock/messaging/unprocessed_test.ts b/ts/test-mock/messaging/unprocessed_test.ts new file mode 100644 index 000000000..0176af7ea --- /dev/null +++ b/ts/test-mock/messaging/unprocessed_test.ts @@ -0,0 +1,96 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import createDebug from 'debug'; +import { StorageState } from '@signalapp/mock-server'; + +import * as durations from '../../util/durations'; +import type { App } from '../playwright'; +import { Bootstrap } from '../bootstrap'; + +export const debug = createDebug('mock:test:unprocessed'); + +describe('unprocessed', function (this: Mocha.Suite) { + this.timeout(durations.MINUTE); + + let bootstrap: Bootstrap; + let app: App; + + beforeEach(async () => { + bootstrap = new Bootstrap({ contactCount: 1 }); + + await bootstrap.init(); + + let state = StorageState.getEmpty(); + + const { + phone, + contacts: [alice], + } = bootstrap; + + state = state.addContact(alice, { + identityKey: alice.publicKey.serialize(), + profileKey: alice.profileKey.serialize(), + whitelisted: true, + }); + + state = state.pin(alice); + await phone.setStorageState(state); + + app = await bootstrap.link(); + }); + + afterEach(async function (this: Mocha.Context) { + if (!bootstrap) { + return; + } + + await bootstrap.maybeSaveLogs(this.currentTest, app); + await app.close(); + await bootstrap.teardown(); + }); + + it('generates and loads unprocessed envelopes', async () => { + const { + desktop, + contacts: [alice], + } = bootstrap; + + debug('closing'); + await app.close(); + + debug('queueing messages'); + const sends = new Array>(); + for (let i = 0; i < 100; i += 1) { + sends.push( + alice.sendText(desktop, `hello: ${i}`, { + timestamp: bootstrap.getTimestamp(), + }) + ); + } + + debug('starting app with unprocessed forced'); + [app] = await Promise.all([ + bootstrap.startApp({ + ciForceUnprocessed: true, + }), + ...sends, + ]); + + debug('waiting for the window'); + await app.getWindow(); + + debug('restarting normally'); + await app.close(); + app = await bootstrap.startApp(); + + const page = await app.getWindow(); + + debug('opening conversation'); + await page + .locator(`[data-testid="${alice.device.aci}"] >> "${alice.profileName}"`) + .click(); + + await page.locator('.module-message__text >> "hello: 5"').waitFor(); + }); +}); diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index 2cadffb0b..f8837deb0 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -1144,6 +1144,11 @@ export default class MessageReceiver return; } + // Force save of unprocessed envelopes for testing + if (window.SignalCI?.forceUnprocessed) { + return; + } + // Now, queue and process decrypted envelopes. We drop the promise so that the next // decryptAndCacheBatch batch does not have to wait for the decrypted envelopes to be // processed, which can be an asynchronous blocking operation diff --git a/ts/types/RendererConfig.ts b/ts/types/RendererConfig.ts index 3fad2c0a8..d15cf1877 100644 --- a/ts/types/RendererConfig.ts +++ b/ts/types/RendererConfig.ts @@ -40,11 +40,10 @@ export const rendererConfigSchema = z.object({ contentProxyUrl: configRequiredStringSchema, crashDumpsPath: configRequiredStringSchema, ciMode: z.enum(['full', 'benchmark']).or(z.literal(false)), + ciForceUnprocessed: z.boolean(), devTools: z.boolean(), disableIPv6: z.boolean(), dnsFallback: DNSFallbackSchema, - ciBackupPath: configOptionalStringSchema, - ciIsBackupIntegration: z.boolean(), environment: environmentSchema, isMockTestEnvironment: z.boolean(), homePath: configRequiredStringSchema, diff --git a/ts/windows/main/phase4-test.ts b/ts/windows/main/phase4-test.ts index cf38eea7b..2cda6457b 100644 --- a/ts/windows/main/phase4-test.ts +++ b/ts/windows/main/phase4-test.ts @@ -19,5 +19,6 @@ if (config.ciMode) { const { getCI } = require('../../CI'); window.SignalCI = getCI({ deviceName: window.getTitle(), + forceUnprocessed: config.ciForceUnprocessed, }); }