teams-cli 0.2.4__tar.gz → 0.2.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {teams_cli-0.2.4 → teams_cli-0.2.5}/CHANGELOG.md +10 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/Cargo.lock +5 -5
- {teams_cli-0.2.4 → teams_cli-0.2.5}/Cargo.toml +1 -1
- {teams_cli-0.2.4 → teams_cli-0.2.5}/PKG-INFO +1 -1
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop.js +32 -7
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop.rs +3 -2
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop_cache.js +9 -2
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/tui.rs +91 -2
- {teams_cli-0.2.4 → teams_cli-0.2.5}/tests/desktop_scripts.test.cjs +67 -7
- {teams_cli-0.2.4 → teams_cli-0.2.5}/.github/workflows/ci.yml +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/.github/workflows/dependencies.yml +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/.github/workflows/release.yml +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/.gitignore +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/.mise.toml +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/LICENSE +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/Makefile +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/README.md +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/build.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/docs/releases.md +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/examples/validate_schema.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/pyproject.toml +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/scripts/smoke-release.py +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/auth/pass.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/auth/process.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/auth/wsl.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/auth/wsl_credentials.ps1 +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/auth.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/backend.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/cli.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/config.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/delivery.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop/avatars.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop/browser.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop/browser_children.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop/browser_core.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop/browser_install.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop/browser_lifetime.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop/browser_setup.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop/cdp.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop/connection.ps1 +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop/connection.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop/helper.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop/install.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop/macos.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop/owner.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop/session.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop/windows.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop_core.js +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop_people.js +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/desktop_send.js +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/error.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/graph.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/lib.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/main.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/output.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/render.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/schema.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/setup/capabilities.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/setup.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/src/teams_cli_alias.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/tests/browser_lifetime.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/tests/cli.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/tests/connection.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/tests/connection.test.ps1 +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/tests/credentials.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/tests/desktop.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/tests/desktop_scripts.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/tests/fixtures/clispec-v0.3.json +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/tests/js/people.test.mjs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/tests/package_alias.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/tests/pass_credentials.sh +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/tests/release_smoke.rs +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/tests/wsl_credentials.test.ps1 +0 -0
- {teams_cli-0.2.4 → teams_cli-0.2.5}/vership.toml +0 -0
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.5](https://github.com/rvben/teams-cli/compare/v0.2.4...v0.2.5) - 2026-10-21
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Update TLS certificate verification and bundled root certificates to address RUSTSEC-2026-0382 and RUSTSEC-2026-0383.
|
|
15
|
+
- Desktop chats whose messages Teams keeps in a secondary reply-chain store (`replychains-2`, ...) no longer look empty. All such stores are now read and merged, which also fills in member names for those 1:1 chats.
|
|
16
|
+
- Desktop chat messages missing from the current cache are filled in from caches the same user and tenant left under earlier display languages. Those caches no longer update, so messages edited or deleted since can appear as they were.
|
|
17
|
+
- In the TUI, a chat whose messages are missing now shows its latest message from the chat list and why, instead of "No messages in this chat yet." With the desktop backend it says no messages for the chat were found in the Teams cache on this PC.
|
|
18
|
+
- **deps**: update TLS certificate trust dependencies ([435b11c](https://github.com/rvben/teams-cli/commit/435b11c39113e165e843d73579bc28505ef05620))
|
|
19
|
+
|
|
10
20
|
## [0.2.4](https://github.com/rvben/teams-cli/compare/v0.2.3...v0.2.4) - 2026-10-20
|
|
11
21
|
|
|
12
22
|
### Fixed
|
|
@@ -2815,9 +2815,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
|
|
2815
2815
|
|
|
2816
2816
|
[[package]]
|
|
2817
2817
|
name = "rustls-webpki"
|
|
2818
|
-
version = "0.103.
|
|
2818
|
+
version = "0.103.16"
|
|
2819
2819
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2820
|
-
checksum = "
|
|
2820
|
+
checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281"
|
|
2821
2821
|
dependencies = [
|
|
2822
2822
|
"aws-lc-rs",
|
|
2823
2823
|
"ring",
|
|
@@ -3187,7 +3187,7 @@ dependencies = [
|
|
|
3187
3187
|
|
|
3188
3188
|
[[package]]
|
|
3189
3189
|
name = "teams-cli"
|
|
3190
|
-
version = "0.2.
|
|
3190
|
+
version = "0.2.5"
|
|
3191
3191
|
dependencies = [
|
|
3192
3192
|
"assert_cmd",
|
|
3193
3193
|
"base64 0.23.1",
|
|
@@ -3843,9 +3843,9 @@ dependencies = [
|
|
|
3843
3843
|
|
|
3844
3844
|
[[package]]
|
|
3845
3845
|
name = "webpki-root-certs"
|
|
3846
|
-
version = "1.0.
|
|
3846
|
+
version = "1.0.10"
|
|
3847
3847
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3848
|
-
checksum = "
|
|
3848
|
+
checksum = "58ec80e73fd8baf57bf0d9615554c75348bda990d6d5bbeab964dd068319fecb"
|
|
3849
3849
|
dependencies = [
|
|
3850
3850
|
"rustls-pki-types",
|
|
3851
3851
|
]
|
|
@@ -7,13 +7,16 @@ async (p, select) => {
|
|
|
7
7
|
q.onsuccess = () => resolve(q.result);
|
|
8
8
|
q.onerror = () => reject(q.error);
|
|
9
9
|
});
|
|
10
|
-
const
|
|
11
|
-
if (!cache[k]) throw Error('layout');
|
|
10
|
+
const openName = name => {
|
|
12
11
|
// If the cache disappeared after enumeration, abort rather than create a database.
|
|
13
|
-
const q = indexedDB.open(
|
|
12
|
+
const q = indexedDB.open(name);
|
|
14
13
|
q.onupgradeneeded = () => q.transaction.abort();
|
|
15
14
|
return req(q);
|
|
16
15
|
};
|
|
16
|
+
const open = k => {
|
|
17
|
+
if (!cache[k]) throw Error('layout');
|
|
18
|
+
return openName(cache[k]);
|
|
19
|
+
};
|
|
17
20
|
// Missing cache timestamps can be -1. Keep sort keys valid for CLI cursors.
|
|
18
21
|
const time = t => Number.isFinite(+t) && +t > 0 && +t <= 8640000000000000 ? Math.trunc(+t) : 0;
|
|
19
22
|
const iso = t => time(t) ? new Date(time(t)).toISOString() : null;
|
|
@@ -64,8 +67,16 @@ async (p, select) => {
|
|
|
64
67
|
}
|
|
65
68
|
return { names, me };
|
|
66
69
|
};
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
// Teams can split reply chains across several stores (replychains, replychains-2, ...).
|
|
71
|
+
// Numeric order keeps the primary store's copy of a message found in more than one.
|
|
72
|
+
const chain = async (db, id) => {
|
|
73
|
+
const rank = n => +(n.split('-')[1] || 1);
|
|
74
|
+
const names = [...db.objectStoreNames].filter(n => /^replychains(-\d+)?$/.test(n)).sort((a, b) => rank(a) - rank(b));
|
|
75
|
+
if (!names.length) throw Error('layout');
|
|
76
|
+
const tx = db.transaction(names);
|
|
77
|
+
const lists = await Promise.all(names.map(n => req(tx.objectStore(n).getAll(IDBKeyRange.bound([id], [id, []])))));
|
|
78
|
+
return lists.flat().flatMap(rc => Object.values((rc && rc.messageMap) || {}));
|
|
79
|
+
};
|
|
69
80
|
|
|
70
81
|
const cdb = await open('conversation');
|
|
71
82
|
let pg;
|
|
@@ -124,10 +135,24 @@ async (p, select) => {
|
|
|
124
135
|
}
|
|
125
136
|
return { me, items: chats.map(x => x.value), next: pg.next };
|
|
126
137
|
}
|
|
138
|
+
// The current cache wins; older caches of the same account only add messages it lacks.
|
|
139
|
+
const messages = await chain(rdb, p.chat);
|
|
140
|
+
for (const name of cache.older || []) {
|
|
141
|
+
let db;
|
|
142
|
+
try {
|
|
143
|
+
db = await openName(name);
|
|
144
|
+
messages.push(...await chain(db, p.chat));
|
|
145
|
+
} catch (_) {
|
|
146
|
+
// Older caches are optional; one that vanished or changed layout is skipped.
|
|
147
|
+
} finally {
|
|
148
|
+
if (db) db.close();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
127
151
|
const rows = [], seen = new Set();
|
|
128
|
-
for (const m of
|
|
129
|
-
if (
|
|
152
|
+
for (const m of messages) {
|
|
153
|
+
if (seen.has(m.id)) continue;
|
|
130
154
|
seen.add(m.id);
|
|
155
|
+
if (system(m.messageType)) continue;
|
|
131
156
|
rows.push({ t: time(m.originalArrivalTime), id: m.id, value: {
|
|
132
157
|
id: m.id, createdDateTime: iso(m.originalArrivalTime),
|
|
133
158
|
clientMessageId: typeof m.clientMessageId === 'string' ? m.clientMessageId : null,
|
|
@@ -1271,12 +1271,13 @@ mod tests {
|
|
|
1271
1271
|
.collect::<String>()
|
|
1272
1272
|
});
|
|
1273
1273
|
// Protocol 5 selects the signed-in cache and reports remaining ambiguity apart from surface selection.
|
|
1274
|
+
// Older same-account caches are optional; a helper without them reads as before.
|
|
1274
1275
|
assert_eq!(
|
|
1275
1276
|
scripts,
|
|
1276
1277
|
[
|
|
1277
|
-
"
|
|
1278
|
+
"9c000776f04a",
|
|
1278
1279
|
"baa0bc29c44b",
|
|
1279
|
-
"
|
|
1280
|
+
"7d2b70751d6a",
|
|
1280
1281
|
"23b1a0e4b293"
|
|
1281
1282
|
]
|
|
1282
1283
|
);
|
|
@@ -29,7 +29,7 @@ all => {
|
|
|
29
29
|
}
|
|
30
30
|
if (conversations.length !== 1) return { error: 'layout' };
|
|
31
31
|
const conversation = conversations[0].name;
|
|
32
|
-
const account = conversation.split(':')
|
|
32
|
+
const [, , , tenant, account] = conversation.split(':');
|
|
33
33
|
if (!account) return { error: 'layout' };
|
|
34
34
|
const related = kind => {
|
|
35
35
|
const sibling = conversation.replace(':conversation-manager:', ':' + kind + ':');
|
|
@@ -37,5 +37,12 @@ all => {
|
|
|
37
37
|
const matches = candidates(kind).filter(db => db.name.split(':')[4] === account);
|
|
38
38
|
return matches.length === 1 ? matches[0].name : null;
|
|
39
39
|
};
|
|
40
|
-
|
|
40
|
+
const replies = related('replychain-manager');
|
|
41
|
+
// Caches the same user and tenant left under earlier display languages. Teams stopped
|
|
42
|
+
// updating them, so they may only fill in messages the current cache lacks.
|
|
43
|
+
const older = candidates('replychain-manager').map(db => db.name).filter(name => {
|
|
44
|
+
const part = name.split(':');
|
|
45
|
+
return name !== replies && part[3] === tenant && part[4] === account;
|
|
46
|
+
}).sort();
|
|
47
|
+
return { account, conversation, replies, profiles: related('profiles'), older };
|
|
41
48
|
}
|
|
@@ -532,6 +532,16 @@ impl App {
|
|
|
532
532
|
}
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
/// Why a chat shows only its latest message, said for where messages come from.
|
|
536
|
+
fn missing_history_note(&self) -> &'static str {
|
|
537
|
+
match self.backend().as_deref() {
|
|
538
|
+
Some(TeamsBackend::Desktop(_)) => {
|
|
539
|
+
"No messages for this chat were found in the Teams cache on this PC. Latest message:"
|
|
540
|
+
}
|
|
541
|
+
_ => "Only the latest message is available.",
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
535
545
|
fn can_send(&self) -> bool {
|
|
536
546
|
match &self.source {
|
|
537
547
|
Source::Live { can_send, .. } => *can_send,
|
|
@@ -2030,7 +2040,21 @@ impl App {
|
|
|
2030
2040
|
}
|
|
2031
2041
|
Load::Ready => {}
|
|
2032
2042
|
}
|
|
2033
|
-
let entries = render::conversation(&thread.items, &self.view(), self.viewer.as_deref());
|
|
2043
|
+
let mut entries = render::conversation(&thread.items, &self.view(), self.viewer.as_deref());
|
|
2044
|
+
// The chat list can carry a latest message whose history was never read.
|
|
2045
|
+
let mut missing = false;
|
|
2046
|
+
if entries.is_empty()
|
|
2047
|
+
&& thread.cursor.is_none()
|
|
2048
|
+
&& let Some(chat) = self.chats.iter().find(|chat| chat["id"] == id.as_str())
|
|
2049
|
+
&& chat["lastMessagePreview"].is_object()
|
|
2050
|
+
{
|
|
2051
|
+
entries = render::conversation(
|
|
2052
|
+
std::slice::from_ref(&chat["lastMessagePreview"]),
|
|
2053
|
+
&self.view(),
|
|
2054
|
+
self.viewer.as_deref(),
|
|
2055
|
+
);
|
|
2056
|
+
missing = !entries.is_empty();
|
|
2057
|
+
}
|
|
2034
2058
|
let mut lines = Vec::new();
|
|
2035
2059
|
if thread.loading_older {
|
|
2036
2060
|
lines.push(
|
|
@@ -2050,7 +2074,12 @@ impl App {
|
|
|
2050
2074
|
.alignment(Alignment::Center),
|
|
2051
2075
|
);
|
|
2052
2076
|
}
|
|
2053
|
-
if
|
|
2077
|
+
if missing {
|
|
2078
|
+
lines.push(Line::raw(""));
|
|
2079
|
+
for note in render::wrap(self.missing_history_note(), usize::from(body.width)) {
|
|
2080
|
+
lines.push(Line::styled(note, theme.muted()).centered());
|
|
2081
|
+
}
|
|
2082
|
+
} else if entries.is_empty() {
|
|
2054
2083
|
lines.push(Line::raw(""));
|
|
2055
2084
|
lines.push(Line::styled("No messages in this chat yet.", theme.muted()).centered());
|
|
2056
2085
|
}
|
|
@@ -3706,6 +3735,66 @@ mod tests {
|
|
|
3706
3735
|
App::new(source, Theme { color: false }, now)
|
|
3707
3736
|
}
|
|
3708
3737
|
|
|
3738
|
+
#[test]
|
|
3739
|
+
fn a_chat_without_history_shows_its_latest_message_and_why() {
|
|
3740
|
+
let empty = |app: &mut App| {
|
|
3741
|
+
let id = app.selected_row().unwrap().id.clone();
|
|
3742
|
+
app.threads.insert(
|
|
3743
|
+
id,
|
|
3744
|
+
Thread {
|
|
3745
|
+
items: Vec::new(),
|
|
3746
|
+
cursor: None,
|
|
3747
|
+
load: Load::Ready,
|
|
3748
|
+
loading_older: false,
|
|
3749
|
+
fetching: false,
|
|
3750
|
+
},
|
|
3751
|
+
);
|
|
3752
|
+
frame_text(app)
|
|
3753
|
+
};
|
|
3754
|
+
let mut app = demo_app();
|
|
3755
|
+
let text = empty(&mut app);
|
|
3756
|
+
assert!(
|
|
3757
|
+
text.contains("Only the latest message is available."),
|
|
3758
|
+
"{text}"
|
|
3759
|
+
);
|
|
3760
|
+
assert!(text.contains("release candidate is green"), "{text}");
|
|
3761
|
+
assert!(!text.contains("No messages in this chat yet."), "{text}");
|
|
3762
|
+
|
|
3763
|
+
let now = app.now.clone();
|
|
3764
|
+
let mut app = live_app(false, "work");
|
|
3765
|
+
app.source = Source::Live {
|
|
3766
|
+
backend: Arc::new(TeamsBackend::Desktop(Box::new(
|
|
3767
|
+
crate::desktop::DesktopClient::new(19224).unwrap(),
|
|
3768
|
+
))),
|
|
3769
|
+
profile: "work".into(),
|
|
3770
|
+
can_send: false,
|
|
3771
|
+
};
|
|
3772
|
+
app.apply(Outcome::Chats {
|
|
3773
|
+
more: false,
|
|
3774
|
+
result: Ok(demo_chats(&now)),
|
|
3775
|
+
});
|
|
3776
|
+
let text = empty(&mut app);
|
|
3777
|
+
assert!(text.contains("were found in the Teams cache"), "{text}");
|
|
3778
|
+
// The note wraps with the pane rather than being cut off.
|
|
3779
|
+
let mut terminal = Terminal::new(TestBackend::new(60, 24)).unwrap();
|
|
3780
|
+
app.focus = Focus::Thread;
|
|
3781
|
+
terminal.draw(|frame| app.draw(frame)).unwrap();
|
|
3782
|
+
let narrow = buffer_text(terminal.backend().buffer());
|
|
3783
|
+
assert!(narrow.contains("Latest message:"), "{narrow}");
|
|
3784
|
+
assert!(text.contains("release candidate is green"), "{text}");
|
|
3785
|
+
|
|
3786
|
+
// Older pages may still hold messages, so nothing is claimed missing.
|
|
3787
|
+
let id = app.selected_row().unwrap().id.clone();
|
|
3788
|
+
app.threads.get_mut(&id).unwrap().cursor = Some("older".into());
|
|
3789
|
+
let text = frame_text(&mut app);
|
|
3790
|
+
assert!(text.contains("No messages in this chat yet."), "{text}");
|
|
3791
|
+
|
|
3792
|
+
// Without a preview there is nothing to show.
|
|
3793
|
+
app.chats[0]["lastMessagePreview"] = Value::Null;
|
|
3794
|
+
let text = empty(&mut app);
|
|
3795
|
+
assert!(text.contains("No messages in this chat yet."), "{text}");
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3709
3798
|
#[test]
|
|
3710
3799
|
fn the_header_shows_the_profile_send_mode_freshness_and_counts() {
|
|
3711
3800
|
let mut app = demo_app();
|
|
@@ -29,7 +29,7 @@ function cursor(records) {
|
|
|
29
29
|
return q;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function fixture({ ambiguous = false, foreignOnly = false, mutate, timeout = false, core = false } = {}) {
|
|
32
|
+
function fixture({ ambiguous = false, foreignOnly = false, older = [], stores = {}, mutate, timeout = false, core = false } = {}) {
|
|
33
33
|
const databases = new Map([
|
|
34
34
|
[name('conversation-manager', 'account-a'), [conversation]],
|
|
35
35
|
[name('replychain-manager', 'account-b'), [{ chat, messageMap: { b: { ...message, content: 'Wrong account' } } }]],
|
|
@@ -40,6 +40,9 @@ function fixture({ ambiguous = false, foreignOnly = false, mutate, timeout = fal
|
|
|
40
40
|
databases.set(name('profiles', 'account-a'), [{ mri: '8:orgid:sender', displayName: 'Correct Name' }]);
|
|
41
41
|
}
|
|
42
42
|
if (ambiguous) databases.set(name('conversation-manager', 'account-b'), [conversation]);
|
|
43
|
+
for (const [locale, records] of older) databases.set(name('replychain-manager', 'account-a') + ':' + locale, records);
|
|
44
|
+
// Extra object stores of the current account's reply-chain database, beside its default store.
|
|
45
|
+
const extra = new Map([[name('replychain-manager', 'account-a'), stores]]);
|
|
43
46
|
const opened = [], mutations = [];
|
|
44
47
|
let inspections = 0;
|
|
45
48
|
const client = { mutate: options => {
|
|
@@ -55,11 +58,15 @@ function fixture({ ambiguous = false, foreignOnly = false, mutate, timeout = fal
|
|
|
55
58
|
open: database => {
|
|
56
59
|
assert.ok(databases.has(database), `Unknown database: ${database}`);
|
|
57
60
|
opened.push(database);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
const named = extra.get(database) || {};
|
|
62
|
+
const records = store => store in named ? named[store] : databases.get(database);
|
|
63
|
+
return request({ name: database, close() {},
|
|
64
|
+
objectStoreNames: database.includes(':replychain-manager:') ? ['replychains', ...Object.keys(named)] : [],
|
|
65
|
+
transaction: () => ({ objectStore: store => ({
|
|
66
|
+
get: id => request(records(store).find(record => (record.mri || record.id) === id)),
|
|
67
|
+
getAll: range => request(records(store).filter(record => !range || record.chat === range.chat)),
|
|
68
|
+
openCursor: () => cursor(records(store)),
|
|
69
|
+
}) }) });
|
|
63
70
|
},
|
|
64
71
|
},
|
|
65
72
|
IDBKeyRange: { bound: a => ({ chat: a[0] }) },
|
|
@@ -225,6 +232,57 @@ for (const query of ['chats', 'messages']) {
|
|
|
225
232
|
});
|
|
226
233
|
}
|
|
227
234
|
|
|
235
|
+
test('older caches of the same account only add messages the current cache lacks', async () => {
|
|
236
|
+
const earlier = { ...message, id: '1690000000000', originalArrivalTime: 1690000000000, content: 'Earlier' };
|
|
237
|
+
const f = fixture({ older: [
|
|
238
|
+
['nl-nl', [{ chat, messageMap: { stale: { ...message, content: 'Stale' }, earlier } }]],
|
|
239
|
+
['de-de', [{ chat: '19:other@thread.v2', messageMap: { other: { ...message, id: '1680000000000', content: 'Other chat' } } }]],
|
|
240
|
+
] });
|
|
241
|
+
const result = await f.read();
|
|
242
|
+
assert.deepEqual([...result.items.map(item => item.body.content)], [message.content, 'Earlier']);
|
|
243
|
+
assert.ok(f.opened.every(database => !database.includes(':account-b')));
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('messages filed only in a later reply-chain store are read', async () => {
|
|
247
|
+
const f = fixture({ stores: {
|
|
248
|
+
'replychains-2': [{ chat, messageMap: { a: message } }],
|
|
249
|
+
'unrelated': [{ chat, messageMap: { x: { ...message, id: '1710000000000', content: 'Unrelated store' } } }],
|
|
250
|
+
} });
|
|
251
|
+
f.databases.set(name('replychain-manager', 'account-a'), []);
|
|
252
|
+
const result = await f.read();
|
|
253
|
+
assert.deepEqual([...result.items.map(item => item.body.content)], [message.content]);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('messages split across reply-chain stores are merged newest first without duplicates', async () => {
|
|
257
|
+
const earlier = { ...message, id: '1690000000000', originalArrivalTime: 1690000000000, content: 'Earlier' };
|
|
258
|
+
const later = { ...message, id: '1710000000000', originalArrivalTime: 1710000000000, content: 'Later' };
|
|
259
|
+
const f = fixture({ stores: { 'replychains-2': [{ chat, messageMap: { a: message, earlier, later } }] } });
|
|
260
|
+
const result = await f.read();
|
|
261
|
+
assert.deepEqual([...result.items.map(item => item.body.content)], ['Later', message.content, 'Earlier']);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('reply-chain stores are read in numeric order, so the primary copy of a message wins', async () => {
|
|
265
|
+
const f = fixture({ stores: {
|
|
266
|
+
'replychains-10': [{ chat, messageMap: { a: { ...message, content: 'Tenth store' } } }],
|
|
267
|
+
'replychains-2': [{ chat, messageMap: { a: { ...message, content: 'Second store' } } }],
|
|
268
|
+
} });
|
|
269
|
+
assert.deepEqual([...(await f.read()).items.map(item => item.body.content)], [message.content]);
|
|
270
|
+
f.databases.set(name('replychain-manager', 'account-a'), []);
|
|
271
|
+
assert.deepEqual([...(await f.read()).items.map(item => item.body.content)], ['Second store']);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('1:1 member names come from messages in a later reply-chain store', async () => {
|
|
275
|
+
const one = '19:account-a_peer@unq.gbl.spaces';
|
|
276
|
+
const f = fixture({ stores: { 'replychains-2': [{ chat: one, messageMap: {
|
|
277
|
+
p: { ...message, creator: '8:orgid:peer', imDisplayName: 'Peer Name' },
|
|
278
|
+
} }] } });
|
|
279
|
+
f.databases.set(name('conversation-manager', 'account-a'), [{ ...conversation, id: one, members: [] }]);
|
|
280
|
+
f.databases.set(name('replychain-manager', 'account-a'), []);
|
|
281
|
+
const result = await f.read({ query: 'chats', chat: '' });
|
|
282
|
+
assert.equal(result.me, 'account-a');
|
|
283
|
+
assert.equal(result.items[0].members.find(m => m.userId === 'peer').displayName, 'Peer Name');
|
|
284
|
+
});
|
|
285
|
+
|
|
228
286
|
test('a missing account database never falls back to another account', async () => {
|
|
229
287
|
const f = fixture({ foreignOnly: true });
|
|
230
288
|
await assert.rejects(f.read(), /layout/);
|
|
@@ -251,10 +309,12 @@ test('the signed-in user, tenant and display language select among coexisting ca
|
|
|
251
309
|
...caches('home', 'me', 'en-us'), ...caches('home', 'me', 'nl-nl'),
|
|
252
310
|
...caches('guest', 'me-as-guest'), ...caches('home', 'colleague'),
|
|
253
311
|
];
|
|
254
|
-
for (const [tenant, user, locale] of [['home', 'me', 'nl-nl'], ['home', 'me', 'en-us'], ['guest', 'me-as-guest', 'en-us']]) {
|
|
312
|
+
for (const [tenant, user, locale, older] of [['home', 'me', 'nl-nl', ['en-us']], ['home', 'me', 'en-us', ['nl-nl']], ['guest', 'me-as-guest', 'en-us', []]]) {
|
|
255
313
|
assert.deepEqual(selectWith(names, signedIn(tenant, user, locale)), {
|
|
256
314
|
account: user, conversation: teams('conversation-manager', tenant, user, locale),
|
|
257
315
|
replies: teams('replychain-manager', tenant, user, locale), profiles: teams('profiles', tenant, user, locale),
|
|
316
|
+
// Only the same user in the same tenant may fill in messages.
|
|
317
|
+
older: older.map(other => teams('replychain-manager', tenant, user, other)),
|
|
258
318
|
});
|
|
259
319
|
}
|
|
260
320
|
// Only one cache for the signed-in account needs no display language.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|