Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions web/src/components/EmojiPicker.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
>
</div>
<div class="emoji-categories">
{#each categories as cat}
{#each categories as cat (cat.id)}
<button
class="emoji-cat"
class:active={activeCategory === cat.id}
Expand All @@ -110,7 +110,7 @@
</div>
<div class="emoji-grid-label">Frequently Used</div>
<div class="emoji-grid">
{#each frequentEmojis as emojiData}
{#each frequentEmojis as emojiData (emojiData.code)}
<button
class="emoji-item"
onclick={() => selectEmoji(emojiData)}
Expand Down
9 changes: 1 addition & 8 deletions web/src/components/ReadReceipt.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,6 @@
let { count = 0, readers = [] } = $props();

let showTooltip = $state(false);

let tooltipText = $derived(() => {
if (readers.length > 0) {
return readers.join(', ');
}
return `${count} ${count === 1 ? 'person' : 'people'}`;
});
</script>

<div
Expand All @@ -36,7 +29,7 @@
<div class="tooltip-arrow"></div>
<span class="tooltip-text">
{#if readers.length > 0}
{#each readers as reader, i}
{#each readers as reader, i (i)}
<span class="reader-name">{reader}{i < readers.length - 1 ? ',' : ''}</span>
{/each}
{:else}
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/SearchPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
data-testid="search-panel-input"
>
<div class="search-panel-filters">
{#each filters as filter}
{#each filters as filter (filter)}
<button
class="search-filter"
class:active={activeFilter === filter.toLowerCase()}
Expand Down
21 changes: 17 additions & 4 deletions web/tests/history-status-normalization.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ function mockHistory(messages) {
}));
}

// Poll for the async ``#fetchHistory`` result instead of racing a single
// fixed delay: ``switchChannel`` kicks off the history fetch fire-and-forget,
// so under suite load a one-shot 10ms wait was occasionally too short and the
// message had not landed yet (intermittent "expected undefined to be truthy").
// This waits up to a generous deadline but returns as soon as the row appears.
async function waitForMessage(store, id, timeout = 1000) {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
const m = store.messages.find((x) => x.id === id);
if (m) return m;
await new Promise((r) => setTimeout(r, 5));
}
return store.messages.find((x) => x.id === id);
}

describe('#fetchHistory status normalization', () => {
let origFetch;
beforeEach(() => {
Expand All @@ -63,9 +78,8 @@ describe('#fetchHistory status normalization', () => {
]);

store.switchChannel('testchan');
await new Promise((r) => setTimeout(r, 10));
const m = await waitForMessage(store, 'h1');

const m = store.messages.find((x) => x.id === 'h1');
expect(m).toBeTruthy();
expect(m.status).toBe('sent');
});
Expand All @@ -84,9 +98,8 @@ describe('#fetchHistory status normalization', () => {
]);

store.switchChannel('testchan');
await new Promise((r) => setTimeout(r, 10));
const m = await waitForMessage(store, 'h2');

const m = store.messages.find((x) => x.id === 'h2');
expect(m).toBeTruthy();
expect(m.status).toBeUndefined();
});
Expand Down
28 changes: 27 additions & 1 deletion web/tests/mqtt-store-bootstrap.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
// without needing a live broker. We mock `lib/api.js`'s `apiGet` so the
// daemon does not need to be running.

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
Expand All @@ -41,8 +41,17 @@ const STORE_SRC_PATH = resolve(HERE, '..', 'src', 'lib', 'mqtt-store.svelte.js')
describe('MqttChatStore — v0.4.0 Step 2.5 bootstrap from /api/conversations', () => {
/** @type {InstanceType<typeof MqttChatStore>} */
let store;
/** @type {import('vitest').MockInstance} */
let warnSpy;

beforeEach(() => {
// The error-path bootstrap tests below deliberately exercise
// ``#bootstrapChannels``'s 404/500/network branch, which logs a
// ``[claude-comms] /api/conversations bootstrap failed`` warning by
// design. Spy on (and silence) console.warn so the expected diagnostic
// does not pollute the suite's console; the error-path tests assert the
// warning fired so the suppression never hides a regression.
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
apiGetMock.mockReset();
store = new MqttChatStore();
// The store ships with an empty channels array now — assert the
Expand All @@ -51,6 +60,10 @@ describe('MqttChatStore — v0.4.0 Step 2.5 bootstrap from /api/conversations',
expect(store.serverUnreachable).toBe(false);
});

afterEach(() => {
warnSpy.mockRestore();
});

it('starts with an empty channels array (no hardcoded seed)', () => {
// No `general`, `random`, `project-alpha`, `lora-training`, etc.
expect(store.channels).toHaveLength(0);
Expand Down Expand Up @@ -159,6 +172,11 @@ describe('MqttChatStore — v0.4.0 Step 2.5 bootstrap from /api/conversations',

expect(store.channels).toEqual([]);
expect(store.serverUnreachable).toBe(true);
// The failure is surfaced via a single diagnostic warning.
expect(warnSpy).toHaveBeenCalledWith(
'[claude-comms] /api/conversations bootstrap failed',
expect.objectContaining({ status: 404 }),
);
// No fallback to hardcoded seeds.
const ids = store.channels.map((c) => c.id);
expect(ids).not.toContain('general');
Expand All @@ -172,6 +190,10 @@ describe('MqttChatStore — v0.4.0 Step 2.5 bootstrap from /api/conversations',

expect(store.channels).toEqual([]);
expect(store.serverUnreachable).toBe(true);
expect(warnSpy).toHaveBeenCalledWith(
'[claude-comms] /api/conversations bootstrap failed',
expect.objectContaining({ status: 500 }),
);
});

it('network error → serverUnreachable=true, channels stays empty', async () => {
Expand All @@ -181,6 +203,10 @@ describe('MqttChatStore — v0.4.0 Step 2.5 bootstrap from /api/conversations',

expect(store.channels).toEqual([]);
expect(store.serverUnreachable).toBe(true);
expect(warnSpy).toHaveBeenCalledWith(
'[claude-comms] /api/conversations bootstrap failed',
expect.objectContaining({ message: 'NetworkError: fetch failed' }),
);
});

it('successful bootstrap after a failure clears the serverUnreachable flag', async () => {
Expand Down