Files
upstream-zulip/web/tests/message_list_data_cache.test.cjs
upstream-mirror 64898b2c79
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
2026-09-15 15:05:30 +05:00

284 lines
11 KiB
JavaScript

"use strict";
const assert = require("node:assert/strict");
const {make_realm} = require("./lib/example_realm.cjs");
const {make_stream} = require("./lib/example_stream.cjs");
const {make_user} = require("./lib/example_user.cjs");
const {zrequire} = require("./lib/namespace.cjs");
const {run_test} = require("./lib/test.cjs");
const muted_users = zrequire("muted_users");
const people = zrequire("people");
const stream_data = zrequire("stream_data");
const state_data = zrequire("state_data");
const {initialize_user_settings} = zrequire("user_settings");
state_data.set_realm(make_realm());
const me = make_user({email: "me@example.com", user_id: 1, full_name: "Me"});
const alice = make_user({email: "alice@example.com", user_id: 2, full_name: "Alice"});
people.add_active_user(me, "server_events");
people.add_active_user(alice, "server_events");
people.initialize_current_user(me.user_id);
state_data.set_current_user(me);
muted_users.set_muted_users([]);
initialize_user_settings({user_settings: {}});
const foo_stream = make_stream({name: "Foo", stream_id: 100});
stream_data.add_sub_for_tests(foo_stream);
const {Filter} = zrequire("filter");
const {MessageListData} = zrequire("../src/message_list_data");
const message_list_data_cache = zrequire("../src/message_list_data_cache");
const recent_view_messages_data = zrequire("../src/recent_view_messages_data");
const recent = recent_view_messages_data.recent_view_messages_data;
function make_channel_message(id) {
return {
id,
type: "stream",
stream_id: foo_stream.stream_id,
topic: "bar",
sender_id: me.user_id,
unread: false,
};
}
function make_dm_message(id) {
return {
id,
type: "private",
sender_id: alice.user_id,
display_recipient: [{id: me.user_id}, {id: alice.user_id}],
unread: false,
};
}
function make_data(filter_terms, message_ids = [], make_message = make_channel_message) {
const data = new MessageListData({
excludes_muted_topics: false,
excludes_muted_users: false,
filter: new Filter(filter_terms),
});
if (message_ids.length > 0) {
data.add_messages(
message_ids.map((id) => make_message(id)),
true,
);
}
return data;
}
function reset_cache() {
message_list_data_cache.clear();
recent.clear();
}
function test(label, f) {
run_test(label, () => {
reset_cache();
f();
});
}
const channel_topic_terms = [
{operator: "channel", operand: foo_stream.stream_id.toString()},
{operator: "topic", operand: "bar"},
];
const channel_topic_near_terms = (msg_id) => [
...channel_topic_terms,
{operator: "near", operand: msg_id.toString()},
];
const channel_topic_with_terms = (msg_id) => [
...channel_topic_terms,
{operator: "with", operand: msg_id.toString()},
];
const dm_terms = [{operator: "dm", operand: [alice.user_id]}];
const dm_near_terms = (msg_id) => [...dm_terms, {operator: "near", operand: msg_id.toString()}];
const dm_with_terms = (msg_id) => [...dm_terms, {operator: "with", operand: msg_id.toString()}];
test("cache miss returns only recent_view_messages_data", () => {
const supersets = message_list_data_cache.get_superset_datasets(
new Filter(channel_topic_terms),
);
assert.deepEqual(supersets, [recent]);
});
test("exact match returned alongside fallback", () => {
const exact = make_data(channel_topic_terms);
message_list_data_cache.add(exact);
const supersets = message_list_data_cache.get_superset_datasets(
new Filter(channel_topic_terms),
);
assert.deepEqual(supersets, [exact, recent]);
});
test("conversation cache used to populate `near` view", () => {
// We have a cached `channel:foo topic:bar` view that contains message 42;
// target `channel:foo topic:bar near:42` should reuse it.
const conversation = make_data(channel_topic_terms, [40, 42, 44]);
message_list_data_cache.add(conversation);
const supersets = message_list_data_cache.get_superset_datasets(
new Filter(channel_topic_near_terms(42)),
);
assert.deepEqual(supersets, [conversation, recent]);
});
test("conversation cache used to populate `with` view", () => {
const conversation = make_data(channel_topic_terms, [40, 42, 44]);
message_list_data_cache.add(conversation);
const supersets = message_list_data_cache.get_superset_datasets(
new Filter(channel_topic_with_terms(42)),
);
assert.deepEqual(supersets, [conversation, recent]);
});
test("conversation cache skipped when it lacks the `near` message", () => {
const conversation = make_data(channel_topic_terms, [10, 11, 12]);
message_list_data_cache.add(conversation);
const supersets = message_list_data_cache.get_superset_datasets(
new Filter(channel_topic_near_terms(42)),
);
assert.deepEqual(supersets, [recent]);
});
test("two `near` views differing only in message share cached data", () => {
// Cached `near:42` view; target `near:99` should still consume it
// when the cached dataset already includes message 99.
const cached_near = make_data(channel_topic_near_terms(42), [40, 42, 99]);
message_list_data_cache.add(cached_near);
const supersets = message_list_data_cache.get_superset_datasets(
new Filter(channel_topic_near_terms(99)),
);
assert.deepEqual(supersets, [cached_near, recent]);
});
test("exact match for `near` view is not duplicated by helper", () => {
const cached_near = make_data(channel_topic_near_terms(42), [40, 42, 44]);
message_list_data_cache.add(cached_near);
const supersets = message_list_data_cache.get_superset_datasets(
new Filter(channel_topic_near_terms(42)),
);
// cached_near matches both the exact-match check and the helper's
// criteria; make sure it only appears once.
assert.deepEqual(supersets, [cached_near, recent]);
});
test("DM `near` view uses cached DM conversation", () => {
const dm_cached = make_data(dm_terms, [5, 7, 9], make_dm_message);
message_list_data_cache.add(dm_cached);
const supersets = message_list_data_cache.get_superset_datasets(new Filter(dm_near_terms(7)));
assert.deepEqual(supersets, [dm_cached, recent]);
});
test("DM `with` view uses cached DM conversation", () => {
const dm_cached = make_data(dm_terms, [5, 7, 9], make_dm_message);
message_list_data_cache.add(dm_cached);
const supersets = message_list_data_cache.get_superset_datasets(new Filter(dm_with_terms(7)));
assert.deepEqual(supersets, [dm_cached, recent]);
});
test("non-conversation cached views (e.g. is:starred) are not used as supersets", () => {
const starred = make_data([{operator: "is", operand: "starred"}]);
starred.add_messages([{...make_channel_message(42), starred: true}], true);
message_list_data_cache.add(starred);
const supersets = message_list_data_cache.get_superset_datasets(
new Filter(channel_topic_near_terms(42)),
);
// The starred view contains the message but isn't a conversation view,
// so muting semantics may differ — we must not use it as a superset.
assert.deepEqual(supersets, [recent]);
});
test("non-conversation target (e.g. a channel feed) uses no conversation cache", () => {
const conversation = make_data(channel_topic_terms, [40, 42, 44]);
message_list_data_cache.add(conversation);
// A channel feed anchored at message 42 needs every topic in the
// channel, so a cached single-topic dataset can't populate it.
const supersets = message_list_data_cache.get_superset_datasets(
new Filter([
{operator: "channel", operand: foo_stream.stream_id.toString()},
{operator: "near", operand: "42"},
]),
);
assert.deepEqual(supersets, [recent]);
});
test("non-near/with target ignores the helper entirely", () => {
const cached_near = make_data(channel_topic_near_terms(42), [40, 42, 44]);
message_list_data_cache.add(cached_near);
// Target lacks both near and with; even though a cached conversation
// view exists with overlapping messages, the helper is a no-op.
const supersets = message_list_data_cache.get_superset_datasets(
new Filter(channel_topic_terms),
);
assert.deepEqual(supersets, [recent]);
});
test("recent_view_messages_data containing message is not double-listed", () => {
recent.add_messages([make_channel_message(42)], true);
const supersets = message_list_data_cache.get_superset_datasets(
new Filter(channel_topic_near_terms(42)),
);
// `recent` is empty-filter, so it's not a conversation view; the helper
// shouldn't include it. The fallback at the end adds it exactly once.
assert.deepEqual(supersets, [recent]);
});
test("multiple conversation caches are all considered", () => {
const conversation = make_data(channel_topic_terms, [40, 42, 44]);
const cached_near = make_data(channel_topic_near_terms(60), [58, 60, 62]);
message_list_data_cache.add(conversation);
message_list_data_cache.add(cached_near);
// Add a second message to `conversation` so it also contains 62.
conversation.add_messages([make_channel_message(62)], true);
const supersets = message_list_data_cache.get_superset_datasets(
new Filter(channel_topic_near_terms(62)),
);
// Both cached conversation datasets contain message 62, so both should
// be candidates. recent_view_messages_data is always last.
assert.equal(supersets.length, 3);
assert.ok(supersets.includes(conversation));
assert.ok(supersets.includes(cached_near));
assert.equal(supersets.at(-1), recent);
});
test("conversation-view muting flags keep candidate reuse safe", () => {
// `get_supersets_containing_near_or_with_message` trusts that a
// conversation-view candidate has not silently dropped a message the
// target needs. That relies on the muting flags below; pin them so a
// future filter.ts change forces this reasoning to be re-examined.
const cases = [
{terms: channel_topic_terms, excludes_muted_users: true},
{terms: channel_topic_near_terms(42), excludes_muted_users: true},
{terms: channel_topic_with_terms(42), excludes_muted_users: true},
{terms: dm_terms, excludes_muted_users: false},
{terms: dm_near_terms(7), excludes_muted_users: false},
{terms: dm_with_terms(7), excludes_muted_users: false},
];
for (const {terms, excludes_muted_users} of cases) {
const filter = new Filter(terms);
assert.ok(filter.is_conversation_view());
// Muted-topic filtering is disabled for every conversation view, so
// a candidate cannot have dropped a message on that basis.
assert.equal(filter.excludes_muted_topics(), false);
// Muted-user filtering may apply, but only ever identically across
// views of the same shape (and channel and DM messages never share a
// dataset), so a candidate drops only what the target would too.
assert.equal(filter.excludes_muted_users(), excludes_muted_users);
}
});