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
5 changes: 4 additions & 1 deletion src/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,10 @@ export async function copyToClipboard(
// Special-case: when running inside WSL, try the Windows clipboard helper
// (`clip.exe`) via interop. This helps common setups where tmux runs in
// WSL but the user expects the Windows clipboard to be updated.
const isWSL = typeof env.WSL_DISTRO_NAME === 'string' || typeof env.WSL_INTEROP === 'string' || /microsoft/i.test(os.release());
// Detection is driven by environment variables set by WSL (WSL_DISTRO_NAME
// or WSL_INTEROP). Avoid relying on kernel-release text (os.release()) as
// it can produce false positives in some test/CI environments.
const isWSL = typeof env.WSL_DISTRO_NAME === 'string' || typeof env.WSL_INTEROP === 'string';
if (isWSL) {
try {
const clipRes = await run('clip.exe', []);
Expand Down
21 changes: 7 additions & 14 deletions src/tui/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3060,20 +3060,13 @@ export class TuiController {
height: 10,
});
if (openIdx === 0) {
try {
const { exec } = await import('child_process');
const platform = process.platform;
const cmd = platform === 'darwin'
? `open "${result.issueUrl}"`
: platform === 'win32'
? `powershell.exe Start "${result.issueUrl}"`
: `xdg-open "${result.issueUrl}"`;
exec(cmd, (err) => {
if (err) showToast('Could not open browser');
});
} catch {
showToast('Could not open browser');
}
try {
const openUrl = (await import('../utils/open-url.js')).default;
const ok = await openUrl(result.issueUrl, fsImpl as any);
if (!ok) showToast('Could not open browser');
} catch (e) {
showToast('Could not open browser');
}
}
}
} else {
Expand Down
55 changes: 55 additions & 0 deletions src/utils/open-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as fs from 'fs';

export async function openUrlInBrowser(url: string, fsImpl: typeof fs = fs): Promise<boolean> {
// Prefer candidates based on environment; try each until one succeeds.
const platform = process.platform;

const isWsl = (() => {
try {
if (process.env.WSL_DISTRO_NAME) return true;
const ver = fsImpl.readFileSync('/proc/version', 'utf8');
return /microsoft/i.test(ver);
} catch (_) {
return false;
}
})();

const candidates: string[] = [];
if (platform === 'darwin') {
candidates.push(`open "${url}"`);
} else if (platform === 'win32') {
candidates.push(`powershell.exe Start "${url}"`);
} else {
// linux-like
if (isWsl) {
// prefer wslview if installed, then explorer.exe, then xdg-open
candidates.push(`wslview "${url}"`);
candidates.push(`explorer.exe "${url}"`);
candidates.push(`xdg-open "${url}"`);
} else {
candidates.push(`xdg-open "${url}"`);
}
}

try {
const { exec } = await import('child_process');
for (const cmd of candidates) {
// eslint-disable-next-line no-await-in-loop
const ok = await new Promise<boolean>((resolve) => {
try {
exec(cmd, (err) => {
resolve(!err);
});
} catch (_) {
resolve(false);
}
});
if (ok) return true;
}
} catch (_) {
// ignore
}
return false;
}

export default openUrlInBrowser;
128 changes: 1 addition & 127 deletions tests/fts-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,133 +271,7 @@ describe('FTS Search', () => {
});
});

describe('searchFallback with new filter flags', () => {
// Test the fallback search path directly via SqlitePersistentStore.searchFallback().
// better-sqlite3 always includes FTS5, so we cannot disable it at the
// WorklogDatabase level; calling searchFallback() on the store exercises
// the application-level filtering code path that would run when FTS5 is
// unavailable.

describe('--priority filter (fallback)', () => {
it('should filter by priority', () => {
db.create({ title: 'Fbpriority alpha task', priority: 'high' });
db.create({ title: 'Fbpriority alpha chore', priority: 'low' });

const results = (db as any).store.searchFallback('fbpriority alpha', { priority: 'high' });
expect(results.length).toBe(1);
const item = db.get(results[0].itemId);
expect(item?.priority).toBe('high');
});
});

describe('--assignee filter (fallback)', () => {
it('should filter by assignee', () => {
db.create({ title: 'Fbassignee alpha work', assignee: 'alice' });
db.create({ title: 'Fbassignee alpha work', assignee: 'bob' });

const results = (db as any).store.searchFallback('fbassignee alpha', { assignee: 'alice' });
expect(results.length).toBe(1);
const item = db.get(results[0].itemId);
expect(item?.assignee).toBe('alice');
});
});

describe('--stage filter (fallback)', () => {
it('should filter by stage', () => {
db.create({ title: 'Fbstage alpha item', stage: 'review' });
db.create({ title: 'Fbstage alpha item', stage: 'done' });

const results = (db as any).store.searchFallback('fbstage alpha', { stage: 'review' });
expect(results.length).toBe(1);
const item = db.get(results[0].itemId);
expect(item?.stage).toBe('review');
});
});

describe('--issue-type filter (fallback)', () => {
it('should filter by issueType', () => {
db.create({ title: 'Fbtype alpha entry', issueType: 'epic' });
db.create({ title: 'Fbtype alpha entry', issueType: 'task' });

const results = (db as any).store.searchFallback('fbtype alpha', { issueType: 'epic' });
expect(results.length).toBe(1);
const item = db.get(results[0].itemId);
expect(item?.issueType).toBe('epic');
});
});

describe('--needs-producer-review filter (fallback)', () => {
it('should filter by needsProducerReview true', () => {
db.create({ title: 'Fbreview alpha item', needsProducerReview: true });
db.create({ title: 'Fbreview alpha item', needsProducerReview: false });

const results = (db as any).store.searchFallback('fbreview alpha', { needsProducerReview: true });
expect(results.length).toBe(1);
const item = db.get(results[0].itemId);
expect(item?.needsProducerReview).toBe(true);
});

it('should filter by needsProducerReview false', () => {
db.create({ title: 'Fbreview beta item', needsProducerReview: true });
db.create({ title: 'Fbreview beta item', needsProducerReview: false });

const results = (db as any).store.searchFallback('fbreview beta', { needsProducerReview: false });
expect(results.length).toBe(1);
const item = db.get(results[0].itemId);
expect(item?.needsProducerReview).toBe(false);
});
});

describe('--deleted filter (fallback)', () => {
it('should exclude deleted items by default', () => {
db.create({ title: 'Fbdeleted alpha item', status: 'open' });
// Create an item with status 'deleted' directly (avoids db.delete
// which would also remove the FTS entry, allowing us to verify the
// fallback filter independently).
db.create({ title: 'Fbdeleted alpha item', status: 'deleted' as any });

const results = (db as any).store.searchFallback('fbdeleted alpha');
expect(results.length).toBe(1);
const item = db.get(results[0].itemId);
expect(item?.status).toBe('open');
});

it('should include deleted items when deleted flag is set', () => {
db.create({ title: 'Fbdeleted beta item', status: 'open' });
db.create({ title: 'Fbdeleted beta item', status: 'deleted' as any });

const results = (db as any).store.searchFallback('fbdeleted beta', { deleted: true });
expect(results.length).toBe(2);
});
});

describe('combined filters (fallback)', () => {
it('should combine priority and assignee', () => {
db.create({ title: 'Fbcombo alpha work', priority: 'high', assignee: 'alice' });
db.create({ title: 'Fbcombo alpha work', priority: 'high', assignee: 'bob' });
db.create({ title: 'Fbcombo alpha work', priority: 'low', assignee: 'alice' });

const results = (db as any).store.searchFallback('fbcombo alpha', { priority: 'high', assignee: 'alice' });
expect(results.length).toBe(1);
const item = db.get(results[0].itemId);
expect(item?.priority).toBe('high');
expect(item?.assignee).toBe('alice');
});

it('should combine stage, issueType and needsProducerReview', () => {
db.create({ title: 'Fbmulti alpha item', stage: 'review', issueType: 'bug', needsProducerReview: true });
db.create({ title: 'Fbmulti alpha item', stage: 'review', issueType: 'bug', needsProducerReview: false });
db.create({ title: 'Fbmulti alpha item', stage: 'done', issueType: 'bug', needsProducerReview: true });

const results = (db as any).store.searchFallback('fbmulti alpha', { stage: 'review', issueType: 'bug', needsProducerReview: true });
expect(results.length).toBe(1);
const item = db.get(results[0].itemId);
expect(item?.stage).toBe('review');
expect(item?.issueType).toBe('bug');
expect(item?.needsProducerReview).toBe(true);
});
});
});
// moved to tests/search-fallback.test.ts

describe('index updates on write', () => {
it('should reflect updates in search results', () => {
Expand Down
146 changes: 146 additions & 0 deletions tests/search-fallback.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* Extracted fallback search filter tests from tests/fts-search.test.ts
*/

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { WorklogDatabase } from '../src/database.js';
import { createTempDir, cleanupTempDir, createTempJsonlPath, createTempDbPath } from './test-utils.js';

describe('Search Fallback', () => {
let tempDir: string;
let dbPath: string;
let jsonlPath: string;
let db: WorklogDatabase;

beforeEach(() => {
tempDir = createTempDir();
dbPath = createTempDbPath(tempDir);
jsonlPath = createTempJsonlPath(tempDir);
db = new WorklogDatabase('TEST', dbPath, jsonlPath, true, true);
});

afterEach(() => {
db.close();
cleanupTempDir(tempDir);
});

// The describe block below was moved from tests/fts-search.test.ts
describe('searchFallback with new filter flags', () => {
describe('--priority filter (fallback)', () => {
it('should filter by priority', () => {
db.create({ title: 'Fbpriority alpha task', priority: 'high' });
db.create({ title: 'Fbpriority alpha chore', priority: 'low' });

const results = (db as any).store.searchFallback('fbpriority alpha', { priority: 'high' });
expect(results.length).toBe(1);
const item = db.get(results[0].itemId);
expect(item?.priority).toBe('high');
});
});

describe('--assignee filter (fallback)', () => {
it('should filter by assignee', () => {
db.create({ title: 'Fbassignee alpha work', assignee: 'alice' });
db.create({ title: 'Fbassignee alpha work', assignee: 'bob' });

const results = (db as any).store.searchFallback('fbassignee alpha', { assignee: 'alice' });
expect(results.length).toBe(1);
const item = db.get(results[0].itemId);
expect(item?.assignee).toBe('alice');
});
});

describe('--stage filter (fallback)', () => {
it('should filter by stage', () => {
db.create({ title: 'Fbstage alpha item', stage: 'review' });
db.create({ title: 'Fbstage alpha item', stage: 'done' });

const results = (db as any).store.searchFallback('fbstage alpha', { stage: 'review' });
expect(results.length).toBe(1);
const item = db.get(results[0].itemId);
expect(item?.stage).toBe('review');
});
});

describe('--issue-type filter (fallback)', () => {
it('should filter by issueType', () => {
db.create({ title: 'Fbtype alpha entry', issueType: 'epic' });
db.create({ title: 'Fbtype alpha entry', issueType: 'task' });

const results = (db as any).store.searchFallback('fbtype alpha', { issueType: 'epic' });
expect(results.length).toBe(1);
const item = db.get(results[0].itemId);
expect(item?.issueType).toBe('epic');
});
});

describe('--needs-producer-review filter (fallback)', () => {
it('should filter by needsProducerReview true', () => {
db.create({ title: 'Fbreview alpha item', needsProducerReview: true });
db.create({ title: 'Fbreview alpha item', needsProducerReview: false });

const results = (db as any).store.searchFallback('fbreview alpha', { needsProducerReview: true });
expect(results.length).toBe(1);
const item = db.get(results[0].itemId);
expect(item?.needsProducerReview).toBe(true);
});

it('should filter by needsProducerReview false', () => {
db.create({ title: 'Fbreview beta item', needsProducerReview: true });
db.create({ title: 'Fbreview beta item', needsProducerReview: false });

const results = (db as any).store.searchFallback('fbreview beta', { needsProducerReview: false });
expect(results.length).toBe(1);
const item = db.get(results[0].itemId);
expect(item?.needsProducerReview).toBe(false);
});
});

describe('--deleted filter (fallback)', () => {
it('should exclude deleted items by default', () => {
db.create({ title: 'Fbdeleted alpha item', status: 'open' });
db.create({ title: 'Fbdeleted alpha item', status: 'deleted' as any });

const results = (db as any).store.searchFallback('fbdeleted alpha');
expect(results.length).toBe(1);
const item = db.get(results[0].itemId);
expect(item?.status).toBe('open');
});

it('should include deleted items when deleted flag is set', () => {
db.create({ title: 'Fbdeleted beta item', status: 'open' });
db.create({ title: 'Fbdeleted beta item', status: 'deleted' as any });

const results = (db as any).store.searchFallback('fbdeleted beta', { deleted: true });
expect(results.length).toBe(2);
});
});

describe('combined filters (fallback)', () => {
it('should combine priority and assignee', () => {
db.create({ title: 'Fbcombo alpha work', priority: 'high', assignee: 'alice' });
db.create({ title: 'Fbcombo alpha work', priority: 'high', assignee: 'bob' });
db.create({ title: 'Fbcombo alpha work', priority: 'low', assignee: 'alice' });

const results = (db as any).store.searchFallback('fbcombo alpha', { priority: 'high', assignee: 'alice' });
expect(results.length).toBe(1);
const item = db.get(results[0].itemId);
expect(item?.priority).toBe('high');
expect(item?.assignee).toBe('alice');
});

it('should combine stage, issueType and needsProducerReview', () => {
db.create({ title: 'Fbmulti alpha item', stage: 'review', issueType: 'bug', needsProducerReview: true });
db.create({ title: 'Fbmulti alpha item', stage: 'review', issueType: 'bug', needsProducerReview: false });
db.create({ title: 'Fbmulti alpha item', stage: 'done', issueType: 'bug', needsProducerReview: true });

const results = (db as any).store.searchFallback('fbmulti alpha', { stage: 'review', issueType: 'bug', needsProducerReview: true });
expect(results.length).toBe(1);
const item = db.get(results[0].itemId);
expect(item?.stage).toBe('review');
expect(item?.issueType).toBe('bug');
expect(item?.needsProducerReview).toBe(true);
});
});
});
});
Loading