Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
179bbdc
fix(kb): render text-indexed docs in the preview instead of 'file not…
dishit-wednesday Jul 22, 2026
7c33831
feat(chat): scope a conversation to one recording via bounded per-tur…
dishit-wednesday Jul 23, 2026
bff2090
chore(llm): stop teeing llama.cpp native logs into the app log
dishit-wednesday Jul 23, 2026
7c71cd2
test(locket): rendered guards for pipeline %, setup focus/vocab, repa…
dishit-wednesday Jul 23, 2026
b720eb3
chore: bump pro submodule to the locket feed/detail + setup/settings …
dishit-wednesday Jul 23, 2026
b810055
chore(whisper): drop the experimental small.en-tdrz speaker-turns model
dishit-wednesday Jul 23, 2026
49a953d
fix(whisper): scroll the transcription model picker so all models are…
dishit-wednesday Jul 23, 2026
4d70880
chore: bump pro submodule to round-3 locket edits
dishit-wednesday Jul 24, 2026
6c0b44e
docs: update commit coordination
dishit-wednesday Jul 26, 2026
27ffa55
docs: record final commit plan for the transcript-visibility fix
dishit-wednesday Jul 26, 2026
02a15d5
docs: update commit coordination
dishit-wednesday Jul 26, 2026
b8ba620
docs: note branch rename and commit-1 status in coordination
dishit-wednesday Jul 26, 2026
2bbb251
docs: mark locket commits done and warn on shared-index commits
dishit-wednesday Jul 26, 2026
fdada9d
docs: log that 75c2ecc absorbed Agent A's copy-transcript hunk
dishit-wednesday Jul 26, 2026
8258aba
docs: record the coordinator sweep of remaining locket commits
dishit-wednesday Jul 26, 2026
c5a55ba
chore: bump pro submodule to round-4 locket edits
dishit-wednesday Jul 26, 2026
51e3c60
Merge pull request #614 from off-grid-ai/fix/locket-round-3
dishit-wednesday Aug 1, 2026
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
81 changes: 81 additions & 0 deletions __tests__/pro/locketPipelineLivePercent.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* The top pipeline bar reflects the CURRENT clip's live transcription percent (rendered integration).
*
* Before this, pipelineProgress filled from batch.done/total only, so while a single clip transcribed
* the bar sat frozen at the 0.03 indeterminate sliver for the whole clip, then jumped when it flipped
* done. transcriptProgressStore already carries the clip's live 0-100 percent (it's shown on the
* per-clip card); this folds it into the transcribe-phase fraction so the bar advances WITHIN a clip.
*
* Real Today feed, real queue state (a running transcribe job + its batch) - the sanctioned device
* leaf. Asserts the terminal visual: the fill View's width. Parameterized so both branches falsify -
* with a live 50% tick the bar reads 50%; with no tick it falls back to the 3% sliver.
*/
jest.mock('@react-navigation/native', () => jest.requireActual('@react-navigation/native'));

import { installNativeBoundary, requireRTL } from '../harness/nativeBoundary';
import { installPro } from '../harness/proHarness';

// The live case runs a MULTI-clip batch (clip 2 of 4) so the number can only be right if it is the
// CURRENT RECORDING's own percent (50%), not the batch-combined fraction (which would be (1+0.5)/4 = 38%).
const CASES: { name: string; livePct: number | null; done: number; total: number; width: string }[] = [
{ name: 'live 50% tick → bar shows the RECORDING percent (50%), not the batch fraction', livePct: 50, done: 1, total: 4, width: '50%' },
{ name: 'no live tick → falls back to the 3% sliver', livePct: null, done: 0, total: 1, width: '3%' },
];

describe('pipeline bar shows the current clip live transcription percent (rendered)', () => {
it.each(CASES)('$name', async ({ livePct, done, total, width }) => {
const boundary = installNativeBoundary({ fs: true });
const React = require('react');
const { StyleSheet } = require('react-native');
const { render, waitFor, act, within } = requireRTL();
await installPro();
const { NavigationContainer } = require('@react-navigation/native');
const { createNativeStackNavigator } = require('@react-navigation/native-stack');
const { getRegisteredScreens } = require('../../src/navigation/screenRegistry');
const { useRecordingsStore } = require('@offgrid/pro/locket/stores');
const { useTranscriptProgressStore } = require('@offgrid/pro/locket/stores/transcriptProgressStore');

useRecordingsStore.setState({ recordings: [], jobs: [] });
useTranscriptProgressStore.setState({ byId: {} });
const NOW = Date.now();
const midHour = new Date(NOW); midHour.setMinutes(30, 0, 0);
boundary.fs!.seedFile('/docs/rec.wav', 5_000_000);
useRecordingsStore.getState().addFinalized({
path: '/docs/rec.wav', startedAt: midHour.getTime(), endedAt: midHour.getTime(),
durationMs: 30 * 60 * 1000, sizeBytes: 5_000_000,
});
const id: string = useRecordingsStore.getState().recordings[0].id;
// The clip is transcribing right now: a running transcribe job + a batch pointing at it as the
// current clip. The bar/number must track THIS clip's live percent, not the batch position.
useRecordingsStore.getState().updateRecording(id, { transcriptStatus: 'running' });
useRecordingsStore.setState({
jobs: [{ recordingId: `transcribe:${id}`, kind: 'transcribe', state: 'running' }],
transcribeBatch: { running: true, done, total, currentId: id },
});
if (livePct != null) useTranscriptProgressStore.getState().setProgress(id, livePct);

const Stack = createNativeStackNavigator();
const screens = getRegisteredScreens();
const App = () => React.createElement(NavigationContainer, null,
React.createElement(Stack.Navigator, { initialRouteName: 'LocketFeed', screenOptions: { headerShown: false } },
...screens.map((sc: { name: string; component: React.ComponentType }) =>
React.createElement(Stack.Screen, { key: sc.name, name: sc.name, component: sc.component }))));
const ui = render(React.createElement(App));
await act(async () => { await Promise.resolve(); });

// The running bar shows in BOTH feed modes (an analyse/transcribe is one global queue job).
await waitFor(() => ui.getByTestId('today-analyse-running'));
const bar = ui.getByTestId('today-analyse-running');
const fill = ui.getByTestId('today-analyse-fill');
// The bar FILLS to the blended fraction...
expect(StyleSheet.flatten(fill.props.style).width).toBe(width);
// ...and shows EXACTLY ONE percentage - the standalone number, never a duplicate in the label.
if (livePct != null) {
expect(within(bar).getByTestId('today-analyse-pct')).toBeTruthy();
expect(within(bar).getAllByText(/50%/)).toHaveLength(1);
} else {
expect(within(bar).queryByTestId('today-analyse-pct')).toBeNull();
expect(within(bar).queryByText(/%/)).toBeNull();
}
});
});
71 changes: 71 additions & 0 deletions __tests__/pro/locketRepairState.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Repair progress shows on the FEED card and is store-owned (rendered integration).
*
* Repair used to hold its "repairing" flag in the detail screen's local state, so leaving the screen
* dropped it (looked broken) and the feed never showed it - even though the work ran to completion.
* It now lives on the recording as repairStatus ('repairing' -> 'pruning'), owned by recordingRepair,
* so BOTH the feed card and the detail read it and it survives navigation. This mounts the real feed
* and asserts the card reflects each phase. No mocking our own code.
*/
jest.mock('@react-navigation/native', () => jest.requireActual('@react-navigation/native'));

import { installNativeBoundary, requireRTL } from '../harness/nativeBoundary';
import { installPro } from '../harness/proHarness';

const CASES: { name: string; status: 'repairing' | 'pruning'; label: RegExp }[] = [
{ name: "repairing → 'Repairing' on the card", status: 'repairing', label: /^Repairing/ },
{ name: "pruning → 'Cleaning up' on the card", status: 'pruning', label: /^Cleaning up/ },
];

describe('repair progress shows on the feed card (rendered)', () => {
it.each(CASES)('$name', async ({ status, label }) => {
const boundary = installNativeBoundary({ fs: true });
const React = require('react');
const { StyleSheet } = require('react-native');
const { render, fireEvent, waitFor, act, within } = requireRTL();
await installPro();
const { NavigationContainer } = require('@react-navigation/native');
const { createNativeStackNavigator } = require('@react-navigation/native-stack');
const { getRegisteredScreens } = require('../../src/navigation/screenRegistry');
const { useRecordingsStore } = require('@offgrid/pro/locket/stores');

useRecordingsStore.setState({ recordings: [], jobs: [] });
const NOW = Date.now();
// Clearly in the past (today) so it's visible without a prunedAt - a mid-repair clip has none.
const startedAt = NOW - 2 * 60 * 60 * 1000;
boundary.fs!.seedFile('/docs/rec.wav', 5_000_000);
useRecordingsStore.getState().addFinalized({
path: '/docs/rec.wav', startedAt, endedAt: startedAt + 30 * 60 * 1000,
durationMs: 30 * 60 * 1000, sizeBytes: 5_000_000,
});
const id: string = useRecordingsStore.getState().recordings[0].id;

const Stack = createNativeStackNavigator();
const screens = getRegisteredScreens();
const App = () => React.createElement(NavigationContainer, null,
React.createElement(Stack.Navigator, { initialRouteName: 'LocketFeed', screenOptions: { headerShown: false } },
...screens.map((sc: { name: string; component: React.ComponentType }) =>
React.createElement(Stack.Screen, { key: sc.name, name: sc.name, component: sc.component }))));
const ui = render(React.createElement(App));
await act(async () => { await Promise.resolve(); });

await waitFor(() => ui.getByTestId('today-switcher'));
await act(async () => { fireEvent.press(ui.getByTestId('today-switcher')); await Promise.resolve(); });
await waitFor(() => ui.getByTestId('switcher-recordings'));
await act(async () => { fireEvent.press(ui.getByTestId('switcher-recordings')); await Promise.resolve(); });
await waitFor(() => ui.getByTestId(`today-clip-${id}`));

// Repair starts NOW (clip already displayed) - exactly what recordingRepair emits mid-run.
// The card must react to the store-owned status without any navigation.
await act(async () => {
useRecordingsStore.getState().updateRecording(id, { repairStatus: status });
await Promise.resolve();
});

const card = ui.getByTestId(`today-clip-${id}`);
// It's treated as processing (the green left-border, borderLeftWidth 3, from isRecordingProcessing).
expect(StyleSheet.flatten(card.props.style).borderLeftWidth).toBe(3);
// The phase label is on the card.
expect(within(card).getByText(label)).toBeTruthy();
});
});
60 changes: 60 additions & 0 deletions __tests__/pro/locketSetupFocusAndVocab.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* The setup flow saves BOTH vocabulary and Analysis focus (rendered integration).
*
* Analysis focus used to be a block appended AFTER the guided setup component; it's now a step
* INSIDE the same flow, right after Vocabulary (language -> priority -> vocab -> focus -> model).
* This walks the real screen through real taps and proves each Save writes the store (which persists
* the whole state, so both survive a restart). Fakes only at the device boundary; asserts on the
* store the UI writes through - the single source both onboarding and Settings read.
*/
jest.mock('@react-navigation/native', () => jest.requireActual('@react-navigation/native'));

import { installNativeBoundary, requireRTL } from '../harness/nativeBoundary';
import { installPro } from '../harness/proHarness';

describe('setup flow persists vocabulary and analysis focus (rendered)', () => {
it('saves both through the guided steps', async () => {
installNativeBoundary({ fs: true });
const React = require('react');
const { render, fireEvent, waitFor, act } = requireRTL();
await installPro();
const { NavigationContainer } = require('@react-navigation/native');
const { createNativeStackNavigator } = require('@react-navigation/native-stack');
const { getRegisteredScreens } = require('../../src/navigation/screenRegistry');
const { useAlwaysOnSettingsStore } = require('@offgrid/pro/locket/stores');

// Start from a clean slate so the asserts can't pass on stale values.
useAlwaysOnSettingsStore.setState({ transcribeVocabulary: '', insightsInstruction: '' });

const Stack = createNativeStackNavigator();
const screens = getRegisteredScreens();
const App = () => React.createElement(NavigationContainer, null,
React.createElement(Stack.Navigator, { initialRouteName: 'LocketTranscriptionSetup', screenOptions: { headerShown: false } },
...screens.map((sc: { name: string; component: React.ComponentType }) =>
React.createElement(Stack.Screen, { key: sc.name, name: sc.name, component: sc.component }))));
const ui = render(React.createElement(App));
await act(async () => { await Promise.resolve(); });

const tap = async (id: string) => {
await waitFor(() => ui.getByTestId(id));
await act(async () => { fireEvent.press(ui.getByTestId(id)); await Promise.resolve(); });
};
const type = async (id: string, text: string) => {
await waitFor(() => ui.getByTestId(id));
await act(async () => { fireEvent.changeText(ui.getByTestId(id), text); await Promise.resolve(); });
};

// Walk the guided flow: language -> priority -> vocab (type + Save) -> focus (type + Save).
await tap('setup-next-language');
await tap('setup-next-priority');
await type('setup-vocabulary-input', 'Off Grid, Locket, Kokoro');
await tap('setup-vocab-save');
await type('setup-focus-input', 'focus on decisions and deadlines');
await tap('setup-focus-save');

// Both landed on the single store the app reads (trimmed on Save).
const s = useAlwaysOnSettingsStore.getState();
expect(s.transcribeVocabulary).toBe('Off Grid, Locket, Kokoro');
expect(s.insightsInstruction).toBe('focus on decisions and deadlines');
});
});
Loading