Skip to content
308 changes: 304 additions & 4 deletions desktop/src/features/channels/mentionAdmissionJourney.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ const invoke = async (command, args) => {
if (command === "sync_agents_to_active_huddle") return null;
if (command === "list_relay_agents") {
state.directoryCalls += 1;
if (state.heldDirectory) return state.heldDirectory;
if (state.failDirectory) throw new Error("Directory unavailable");
return state.missingDirectory ? [] : [rawAgent()];
}
Expand Down Expand Up @@ -228,7 +229,13 @@ async function setup(overrides = {}) {
[["teams"], []],
[["archivedIdentities"], { archived: [] }],
])
client.setQueryData(key, data);
if (
!(
(state.heldDirectory || state.coldDirectory) &&
key[0] === "relay-agents"
)
)
client.setQueryData(key, data);
const container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
Expand Down Expand Up @@ -281,6 +288,11 @@ for (const change of [
"old actionable row must not establish intent",
);
assert.deepEqual(mention.knownNames, []);
assert.equal(
mention.isAgentPubkey(AGENT),
true,
"directory removal never turns a known agent into a human",
);
});
}

Expand Down Expand Up @@ -567,12 +579,12 @@ test("background membership/search updates leave visible same-name rows and Tab
client.invalidateQueries({ queryKey: ["user-search"] }),
);
await settle();
assert.equal(mention.suggestions, displayed);
assert.deepEqual(mention.suggestions, displayed);
let outcome, edit;
await act(async () => {
outcome = mention.handleMentionKeyDown(keyboard("Tab"));
});
assert.equal(outcome.suggestion, selected);
assert.deepEqual(outcome.suggestion, selected);
await act(async () => {
edit = mention.insertMention(outcome.suggestion, 6);
});
Expand Down Expand Up @@ -618,7 +630,7 @@ test("text changes load a new request; superseded and closed responses cannot in
releaseOld({ users: [person(VIEWER, "Old")], next_cursor: null }),
);
await settle();
assert.equal(mention.suggestions, displayed);
assert.deepEqual(mention.suggestions, displayed);
await act(async () => mention.updateMentionQuery("@closed", 7));
await settle();
await act(async () => mention.cancelMentionAutocomplete());
Expand Down Expand Up @@ -668,3 +680,291 @@ test("Space completes an exact name but remains literal for partial and same-nam
mention.suggestions[0].pubkey,
);
});

for (const condition of ["denied", "missing", "failed", "cold-failed"]) {
test(`disabled ${condition} member rejects pointer, keyboard and new pin intent`, async () => {
await setup({
owner: OTHER,
visible: true,
directoryVisible: true,
policy: condition === "denied" ? "owner-only" : "anyone",
missingDirectory: condition === "missing",
failDirectory: condition.endsWith("failed"),
coldDirectory: condition === "cold-failed",
searchUsers: [person(OTHER, "Remote Person")],
});
await act(async () => mention.updateMentionQuery("@Remote", 7));
await settle();
assert.equal(mention.isMentionLoading, false);
const row = rows()[0];
if (condition === "cold-failed") {
assert.equal(row.action, "unavailable");
assert.equal(typeof row.onRetry, "function");
assert.equal(
mention.suggestions.some((s) => s.pubkey === OTHER),
false,
);
}
assert.equal(mention.canSelectMention(row), false);
await act(async () => {
picker.selectMentionSuggestion(row);
picker.toggleAlwaysAddressAgent(row);
mention.handleMentionKeyDown(keyboard("ArrowDown"));
});
for (const key of ["Tab", "Enter", " "]) {
let outcome;
await act(async () => {
outcome = mention.handleMentionKeyDown(keyboard(key));
});
assert.equal(outcome.suggestion, undefined);
}
assert.deepEqual(effects, []);
assert.deepEqual(mention.knownNames, []);
});
}

test("checking resolves and retry refreshes without moving the selected identity", async () => {
await setup({
owner: OTHER,
visible: true,
directoryVisible: true,
missingDirectory: true,
});
const identity = rows()[0].pubkey;
assert.equal(rows()[0].action, "checking");
state.missingDirectory = false;
await act(async () =>
client.invalidateQueries({ queryKey: ["relay-agents"] }),
);
await settle();
assert.equal(rows()[0].pubkey, identity);
assert.equal(mention.mentionSelectedIndex, 0);
assert.equal(rows()[0].action, "mention");
assert.equal(mention.canSelectMention(rows()[0]), true);
state.failDirectory = true;
await act(async () =>
client.invalidateQueries({ queryKey: ["relay-agents"] }),
);
await settle();
assert.equal(rows()[0].action, "unavailable");
assert.equal(mention.canSelectMention(rows()[0]), false);
state.failDirectory = false;
await act(async () => rows()[0].onRetry());
await settle();
assert.equal(rows()[0].pubkey, identity);
assert.equal(rows()[0].action, "mention");
});

test("verification expiry never installs an unfinished people search", async () => {
await setup();
let release;
state.pendingSearch = {
slow: new Promise((resolve) => {
release = resolve;
}),
};
await act(async () => mention.updateMentionQuery("@slow", 5));
await act(async () => new Promise((resolve) => setTimeout(resolve, 5100)));
assert.equal(mention.isMentionLoading, true);
assert.deepEqual(mention.suggestions, []);
await act(async () =>
release({ users: [person(OTHER, "Slow")], next_cursor: null }),
);
await settle();
assert.equal(mention.isMentionLoading, false);
assert.equal(mention.suggestions[0].pubkey, OTHER);
});

test("cold directory expiry waits for required people search before installing choices", async () => {
let releaseDirectory, releaseSearch;
await setup({
heldDirectory: new Promise((resolve) => {
releaseDirectory = resolve;
}),
pendingSearch: {
slow: new Promise((resolve) => {
releaseSearch = resolve;
}),
},
});
await act(async () => mention.updateMentionQuery("@Slow", 5));
await act(async () => new Promise((resolve) => setTimeout(resolve, 5100)));
assert.equal(
mention.isMentionLoading,
true,
"directory expiry cannot complete a not-yet-enabled search",
);
assert.deepEqual(mention.suggestions, []);
state.heldDirectory = null;
await act(async () => releaseDirectory([]));
await settle();
assert.equal(
mention.isMentionLoading,
true,
"enabling search is not settlement",
);
assert.deepEqual(mention.suggestions, []);
await act(async () =>
releaseSearch({ users: [person(OTHER, "Slow Person")], next_cursor: null }),
);
await settle();
assert.equal(mention.isMentionLoading, false);
assert.deepEqual(
mention.suggestions.map((row) => row.pubkey),
[OTHER],
);
assert.equal(
mention.handleMentionKeyDown(keyboard("Tab")).suggestion.pubkey,
OTHER,
);
});

for (const visible of [true, false]) {
test(`cached allowed ${visible ? "member" : "relay-only nonmember"} expiry blocks every retained choice until fresh retry settles`, async () => {
await setup({
owner: OTHER,
visible,
directoryVisible: true,
policy: "anyone",
});
await act(async () => mention.updateMentionQuery("@Remote", 7));
await settle();
const retained = rows()[0];
assert.equal(retained.action, visible ? "mention" : "invite");
const identities = mention.suggestions.map((row) => [
row.pubkey,
row.personaId,
row.teamId,
row.displayName,
]);
await act(async () => new Promise((resolve) => setTimeout(resolve, 5100)));
assert.equal(rows()[0].pubkey, retained.pubkey);
assert.equal(rows()[0].action, "unavailable");
assert.equal(
rows()[0].unavailableReason,
"Could not verify access. Retry to check again.",
);
const assertBlocked = async () => {
assert.equal(mention.canSelectMention(retained), false);
await act(async () => {
picker.selectMentionSuggestion(retained);
picker.toggleAlwaysAddressAgent(retained);
});
for (const key of ["Tab", "Enter", " "]) {
let outcome;
await act(async () => {
outcome = mention.handleMentionKeyDown(keyboard(key));
});
assert.equal(outcome.suggestion, undefined);
}
assert.deepEqual(effects, []);
assert.deepEqual(mention.knownNames, []);
};
await assertBlocked();
let release;
state.heldDirectory = new Promise((resolve) => {
release = resolve;
});
await act(async () => rows()[0].onRetry());
assert.equal(rows()[0].action, "checking");
await assertBlocked();
state.heldDirectory = null;
await act(async () => release([rawAgent()]));
await settle();
assert.equal(rows()[0].action, visible ? "mention" : "invite");
assert.deepEqual(
mention.suggestions.map((row) => [
row.pubkey,
row.personaId,
row.teamId,
row.displayName,
]),
identities,
);
assert.equal(rows()[0].pubkey, retained.pubkey);
assert.equal(mention.mentionSelectedIndex, 0);
assert.equal(mention.canSelectMention(retained), true);
});
}

for (const failure of ["denied", "lookup-failed"]) {
test(`installed relay-only nonmember preserves ${failure} reason and held Retry`, async () => {
await setup({ owner: OTHER, visible: false, directoryVisible: true });
const action = "invite";
const retained = rows()[0];
assert.equal(retained.action, action);
const identities = mention.suggestions.map((row) => [
row.pubkey,
row.personaId,
row.teamId,
row.displayName,
]);
state.policy = failure === "denied" ? "owner-only" : "anyone";
state.failDirectory = failure === "lookup-failed";
await act(async () =>
client.invalidateQueries({ queryKey: ["relay-agents"] }),
);
await settle();
assert.equal(rows()[0].action, "unavailable");
assert.equal(
rows()[0].unavailableReason,
failure === "denied"
? "This agent does not permit you to mention it here."
: "Could not verify access. Retry to check again.",
);
assert.equal(mention.canSelectMention(retained), false);
let release, reject;
state.heldDirectory = new Promise((resolve, fail) => {
release = resolve;
reject = fail;
});
await act(async () => rows()[0].onRetry());
assert.equal(rows()[0].action, "checking");
assert.equal(rows()[0].onRetry, undefined);
assert.equal(mention.canSelectMention(retained), false);
if (failure === "lookup-failed") {
await act(async () => reject(new Error("Retry failed")));
await settle();
assert.equal(rows()[0].action, "unavailable");
assert.equal(
rows()[0].unavailableReason,
"Could not verify access. Retry to check again.",
);
assert.equal(mention.canSelectMention(retained), false);
state.heldDirectory = new Promise((resolve) => {
release = resolve;
});
await act(async () => rows()[0].onRetry());
assert.equal(rows()[0].action, "checking");
}
state.policy = "anyone";
state.failDirectory = false;
state.heldDirectory = null;
await act(async () => release([rawAgent()]));
await settle();
assert.equal(rows()[0].action, action);
assert.equal(mention.canSelectMention(rows()[0]), true);
assert.deepEqual(
mention.suggestions.map((row) => [
row.pubkey,
row.personaId,
row.teamId,
row.displayName,
]),
identities,
);
assert.equal(mention.mentionSelectedIndex, 0);
state.policy = "owner-only";
await act(async () =>
client.invalidateQueries({ queryKey: ["relay-agents"] }),
);
await settle();
await act(async () => mention.updateMentionQuery("@Remote", 7));
await settle();
assert.deepEqual(
rows(),
[],
"fresh discovery must still exclude denied nonmembers",
);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,44 @@ test("global search results join only while global search is enabled", () => {
assert.equal(searched[0].displayName, "Dana");
assert.equal(searched[0].isGlobalSearchResult, true);
});

test("cached allowed agents lose actionability on expiry and during retry", () => {
const cached = input({
members: [{ pubkey: AGENT_PUBKEY, displayName: "Scout", isAgent: true }],
relayAgents: [
{ pubkey: AGENT_PUBKEY, name: "Scout", ownerPubkey: MEMBER_PUBKEY },
],
mentionableAgentPubkeys: new Set([AGENT_PUBKEY]),
});
assert.equal(buildMentionCandidates(cached)[0].action, "mention");
const expired = buildMentionCandidates({
...cached,
verificationFailed: true,
})[0];
assert.equal(expired.action, "unavailable");
assert.equal(
expired.unavailableReason,
"Could not verify access. Retry to check again.",
);
assert.equal(
buildMentionCandidates({ ...cached, verificationPending: true })[0].action,
"checking",
);
assert.equal(buildMentionCandidates(cached)[0].action, "mention");
});

test("nonmembers promise Invite only with destination add authority", () => {
const nonmember = input({
canSearchGlobalUsers: true,
userSearchResults: [{ pubkey: SEARCHED_PUBKEY, displayName: "Visitor" }],
});
assert.equal(
buildMentionCandidates(nonmember)[0].action,
"mention-without-invite",
);
assert.equal(
buildMentionCandidates({ ...nonmember, canInviteNonMembers: true })[0]
.action,
"invite",
);
});
Loading
Loading