mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
feat: COD-151 add searchable case picker
This commit is contained in:
committed by
Aamer Akhter
parent
1fa88cd187
commit
e29721046c
@@ -144,6 +144,173 @@ describe('Codex quick start settings', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('case selector refresh', () => {
|
||||
it('sorts case picker options alphabetically and filters by case or host label', () => {
|
||||
const CodemanApp = function CodemanApp(this: any) {};
|
||||
const context = vm.createContext({
|
||||
CodemanApp,
|
||||
localStorage: { getItem: () => null, setItem: () => {} },
|
||||
document: { getElementById: () => null },
|
||||
console,
|
||||
});
|
||||
|
||||
const sessionUi = readFileSync(resolve(import.meta.dirname, '../src/web/public/session-ui.js'), 'utf8');
|
||||
vm.runInContext(sessionUi, context, { filename: 'session-ui.js' });
|
||||
|
||||
const app = new (CodemanApp as any)();
|
||||
const cases = [
|
||||
{ name: 'zeta' },
|
||||
{ name: 'moneytrove', location: 'remote', remote: { hostId: 'mac-mini', path: '/Users/saqeb/moneytrove' } },
|
||||
{ name: 'Alpha' },
|
||||
{ name: 'plex-previews' },
|
||||
];
|
||||
|
||||
const options = app.buildCasePickerOptions(cases);
|
||||
|
||||
expect(options.map((option: any) => option.name)).toEqual([
|
||||
'Alpha',
|
||||
'moneytrove',
|
||||
'plex-previews',
|
||||
'testcase',
|
||||
'zeta',
|
||||
]);
|
||||
expect(options.find((option: any) => option.name === 'moneytrove')?.label).toBe('moneytrove @ mac-mini');
|
||||
expect(app.filterCasePickerOptions(options, 'MAC').map((option: any) => option.name)).toEqual(['moneytrove']);
|
||||
expect(app.filterCasePickerOptions(options, 'plex').map((option: any) => option.name)).toEqual(['plex-previews']);
|
||||
});
|
||||
|
||||
it('creates remote shell sessions by caseName instead of remote display path', async () => {
|
||||
const elements: Record<string, any> = {
|
||||
quickStartCase: { value: 'gpu-work' },
|
||||
shellCount: { value: '1' },
|
||||
};
|
||||
const requests: Array<{ url: string; body?: any }> = [];
|
||||
const CodemanApp = function CodemanApp(this: any) {};
|
||||
|
||||
const context = vm.createContext({
|
||||
CodemanApp,
|
||||
localStorage: {
|
||||
getItem: () => null,
|
||||
setItem: () => {},
|
||||
},
|
||||
document: {
|
||||
getElementById: (id: string) => elements[id] ?? null,
|
||||
},
|
||||
fetch: async (url: string, init?: { body?: string }) => {
|
||||
requests.push({ url, body: init?.body ? JSON.parse(init.body) : undefined });
|
||||
if (url === '/api/cases/gpu-work') {
|
||||
return {
|
||||
json: async () => ({
|
||||
success: true,
|
||||
data: {
|
||||
name: 'gpu-work',
|
||||
path: 'ubuntu@10.0.0.42:/home/ubuntu/work',
|
||||
location: 'remote',
|
||||
remote: { hostId: 'gpu-box', path: '/home/ubuntu/work' },
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
if (url === '/api/sessions') {
|
||||
return { json: async () => ({ success: true, data: { session: { id: 'sess-1' } } }) };
|
||||
}
|
||||
if (url === '/api/sessions/sess-1/shell') return { json: async () => ({ success: true }) };
|
||||
throw new Error(`unexpected fetch: ${url}`);
|
||||
},
|
||||
console,
|
||||
});
|
||||
|
||||
const sessionUi = readFileSync(resolve(import.meta.dirname, '../src/web/public/session-ui.js'), 'utf8');
|
||||
vm.runInContext(sessionUi, context, { filename: 'session-ui.js' });
|
||||
|
||||
const app = new (CodemanApp as any)();
|
||||
app.terminal = { clear: () => {}, writeln: () => {}, focus: () => {} };
|
||||
app.sessions = new Map();
|
||||
app.cases = [{ name: 'gpu-work', path: 'ubuntu@10.0.0.42:/home/ubuntu/work', location: 'remote' }];
|
||||
app.getTerminalDimensions = () => null;
|
||||
app.selectSession = async () => {};
|
||||
|
||||
await app.runShell();
|
||||
|
||||
expect(requests.find((req) => req.url === '/api/sessions')?.body).toMatchObject({
|
||||
caseName: 'gpu-work',
|
||||
mode: 'shell',
|
||||
});
|
||||
expect(requests.find((req) => req.url === '/api/sessions')?.body).not.toHaveProperty('workingDir');
|
||||
});
|
||||
|
||||
it('removes a deleted selected case from the dropdown and blurs the native picker', async () => {
|
||||
const elements: Record<string, any> = {};
|
||||
const requests: Array<{ url: string; method: string; body?: any }> = [];
|
||||
const CodemanApp = function CodemanApp(this: any) {};
|
||||
const quickStartCase = {
|
||||
value: 'deleted-case',
|
||||
innerHTML: '<option value="deleted-case">deleted-case</option><option value="kept-case">kept-case</option>',
|
||||
dataset: {},
|
||||
blur: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
};
|
||||
|
||||
elements.quickStartCase = quickStartCase;
|
||||
elements.caseManageList = { innerHTML: '' };
|
||||
elements.mobileCaseName = { textContent: '' };
|
||||
elements.dirDisplay = { textContent: '' };
|
||||
elements.dirInput = { value: '' };
|
||||
|
||||
const context = vm.createContext({
|
||||
CodemanApp,
|
||||
MobileDetection: { getDeviceType: () => 'desktop' },
|
||||
localStorage: {
|
||||
getItem: () => null,
|
||||
setItem: () => {},
|
||||
},
|
||||
document: {
|
||||
getElementById: (id: string) => elements[id] ?? null,
|
||||
},
|
||||
confirm: () => true,
|
||||
fetch: async (url: string, init?: { method?: string; body?: string }) => {
|
||||
requests.push({ url, method: init?.method ?? 'GET', body: init?.body ? JSON.parse(init.body) : undefined });
|
||||
if (url === '/api/cases/deleted-case')
|
||||
return { json: async () => ({ success: true, data: { name: 'deleted-case' } }) };
|
||||
// The server's preSerialization hook wraps bare payloads as { success, data },
|
||||
// so the frontend reads `.data` off every JSON response — mirror that here.
|
||||
if (url === '/api/settings')
|
||||
return { ok: true, json: async () => ({ success: true, data: { lastUsedCase: 'deleted-case' } }) };
|
||||
if (url === '/api/cases')
|
||||
return { json: async () => ({ success: true, data: [{ name: 'kept-case', path: '/tmp/kept-case' }] }) };
|
||||
if (url === '/api/cases/kept-case')
|
||||
return { json: async () => ({ success: true, data: { path: '/tmp/kept-case' } }) };
|
||||
if (url === '/api/settings' && init?.method === 'PUT') return { json: async () => ({ success: true }) };
|
||||
throw new Error(`unexpected fetch: ${url}`);
|
||||
},
|
||||
console,
|
||||
escapeHtml: (s: string) => s,
|
||||
});
|
||||
|
||||
const sessionUi = readFileSync(resolve(import.meta.dirname, '../src/web/public/session-ui.js'), 'utf8');
|
||||
vm.runInContext(sessionUi, context, { filename: 'session-ui.js' });
|
||||
|
||||
const app = new (CodemanApp as any)();
|
||||
app.cases = [
|
||||
{ name: 'deleted-case', path: '/tmp/deleted-case' },
|
||||
{ name: 'kept-case', path: '/tmp/kept-case' },
|
||||
];
|
||||
app.showToast = vi.fn();
|
||||
|
||||
await app.deleteCase('deleted-case');
|
||||
|
||||
expect(quickStartCase.blur).toHaveBeenCalled();
|
||||
expect(quickStartCase.innerHTML).not.toContain('deleted-case');
|
||||
expect(quickStartCase.innerHTML).toContain('kept-case');
|
||||
expect(elements.mobileCaseName.textContent).toBe('kept-case');
|
||||
expect(requests).toContainEqual({
|
||||
url: '/api/settings',
|
||||
method: 'PUT',
|
||||
body: { lastUsedCase: 'kept-case' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Gemini quick start', () => {
|
||||
// Regression guard for the ApiResponse-envelope unwrap in runGemini(): the
|
||||
// status check must read `.data.available` and the quick-start response must
|
||||
|
||||
Reference in New Issue
Block a user