Skip to content
Merged
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
195 changes: 36 additions & 159 deletions plug/tests/tui/discover.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,117 +93,80 @@ describe('DiscoverScreen', () => {
expect(lastFrame()).toContain('Fetching packages');
});

it('shows zero-search-results message when filter has no matches', async () => {
it('renders community and official packages in list', () => {
vi.mocked(usePackages).mockReturnValue({
packages: [...MOCK_PACKAGES, ...MOCK_COMMUNITY_PACKAGES],
loading: false,
error: null,
warning: null,
});
const { lastFrame } = render(<DiscoverScreen onInputCapture={() => {}} />);
const frame = lastFrame();
expect(frame).toContain('code-review');
expect(frame).toContain('agent-fleet');
});

it('shows empty message when search has no matches', async () => {
vi.mocked(usePackages).mockReturnValue({
packages: MOCK_PACKAGES,
loading: false,
error: null,
warning: null,
});
const { lastFrame, stdin } = render(<DiscoverScreen onInputCapture={() => {}} />);

// Wait for useEffect to register stdin listeners
await new Promise((r) => setTimeout(r, 100));

// Activate search with '/' and wait for focus state to propagate
stdin.write('/');
await new Promise((r) => setTimeout(r, 100));

// Type a query that won't match anything
stdin.write('z');
stdin.write('z');
stdin.write('z');
await new Promise((r) => setTimeout(r, 100));

const frame = lastFrame();
// Should show the no-results empty message
expect(frame).toContain('No results for');
expect(lastFrame()).toContain('No results for');
});

// ── Community package rendering ─────────────────────────────────────────
// ── PackageDetail ─────────────────────────────────────────────────────────

it('renders community packages without error', () => {
vi.mocked(usePackages).mockReturnValue({
packages: MOCK_COMMUNITY_PACKAGES,
loading: false,
error: null,
warning: null,
});
const { lastFrame } = render(<DiscoverScreen onInputCapture={() => {}} />);
it('detail view shows metadata and install hint', () => {
const { lastFrame } = render(
<PackageDetail
pkg={MOCK_COMMUNITY_PACKAGES[0]}
onBack={() => {}}
onInstall={() => {}}
installedNames={new Set()}
/>
);
const frame = lastFrame();
expect(frame).toContain('agent-fleet');
expect(frame).toContain('simple-tool');
});

it('shows "★ 3 deps" for a package with depCount: 3', () => {
vi.mocked(usePackages).mockReturnValue({
packages: [MOCK_COMMUNITY_PACKAGES[0]],
loading: false,
error: null,
warning: null,
});
const { lastFrame } = render(<DiscoverScreen onInputCapture={() => {}} />);
expect(lastFrame()).toContain('★ 3 deps');
expect(frame).toContain('[agent]');
expect(frame).toContain('[i]');
});

it('shows "no deps" for a package with depCount: 0', () => {
vi.mocked(usePackages).mockReturnValue({
packages: [MOCK_COMMUNITY_PACKAGES[1]],
loading: false,
error: null,
warning: null,
});
const { lastFrame } = render(<DiscoverScreen onInputCapture={() => {}} />);
expect(lastFrame()).toContain('no deps');
});

it('official packages still visible when community fetch would fail (community failure isolated)', () => {
// Simulates usePackages state after an isolated community fetch failure:
// community catch block swallows the error; only official packages in state.
vi.mocked(usePackages).mockReturnValue({
packages: MOCK_PACKAGES,
loading: false,
error: null,
warning: null,
});
const { lastFrame } = render(<DiscoverScreen onInputCapture={() => {}} />);
const frame = lastFrame();
expect(frame).toContain('code-review');
expect(frame).toContain('test-gen');
});

// ── PackageDetail — dep list rendering ──────────────────────────────────

it('detail view: renders ✓ installed for installed deps, "not installed" for absent', () => {
const pkg = MOCK_COMMUNITY_PACKAGES[0]; // agent-fleet with 3 deps
const installedNames = new Set(['dep-one']); // only dep-one is installed
it('detail view shows installed badge when already installed', () => {
const { lastFrame } = render(
<PackageDetail
pkg={pkg}
pkg={MOCK_COMMUNITY_PACKAGES[0]}
onBack={() => {}}
onInstall={() => {}}
installedNames={installedNames}
installedNames={new Set(['agent-fleet'])}
/>
);
const frame = lastFrame();
// dep-one is installed
expect(frame).toContain('dep-one');
expect(frame).toContain('✓ installed');
// dep-two and dep-three are not installed
expect(frame).toContain('dep-two');
expect(frame).toContain('not installed');
expect(frame).not.toContain('[i]');
});

// ── Plan view ──────────────────────────────────────────────────────────────
// ── Plan view ─────────────────────────────────────────────────────────────

it('shows plan view when package has deps (toInstall.length > 1)', async () => {
it('shows plan view with scope selector on install', async () => {
vi.mocked(usePackages).mockReturnValue({
packages: [MOCK_COMMUNITY_PACKAGES[0]], // agent-fleet with deps
packages: [MOCK_COMMUNITY_PACKAGES[0]],
loading: false,
error: null,
warning: null,
});
// Resolver returns multiple packages → plan screen should show
vi.mocked(resolve).mockResolvedValueOnce({
toInstall: ['dep-one', 'agent-fleet'],
alreadySatisfied: [],
Expand All @@ -213,44 +176,15 @@ describe('DiscoverScreen', () => {
const { lastFrame, stdin } = render(<DiscoverScreen onInputCapture={() => {}} />);
await new Promise((r) => setTimeout(r, 50));

// Press 'i' to start install
stdin.write('i');
await new Promise((r) => setTimeout(r, 200));

const frame = lastFrame();
expect(frame).toContain('Will install');
expect(frame).toContain('◉ Project');
});

it('skips plan view for a no-dep package (installs immediately)', async () => {
vi.mocked(usePackages).mockReturnValue({
packages: [MOCK_COMMUNITY_PACKAGES[1]], // simple-tool with no deps
loading: false,
error: null,
warning: null,
});
// Resolver returns only root → skip plan screen
vi.mocked(resolve).mockResolvedValueOnce({
toInstall: ['simple-tool'],
alreadySatisfied: [],
cycles: [],
});

const { runInstall } = await import('../../src/commands/install.js');
vi.mocked(runInstall).mockResolvedValue(undefined);

const { lastFrame, stdin } = render(<DiscoverScreen onInputCapture={() => {}} />);
await new Promise((r) => setTimeout(r, 50));

stdin.write('i');
await new Promise((r) => setTimeout(r, 200));

// Should now show plan view with scope selector even for single package
const frame = lastFrame();
expect(frame).toContain('Will install');
expect(frame).toContain('Project');
});

it('Tab key toggles scope between Project and Global in plan view', async () => {
it('Tab toggles scope, Esc cancels plan view', async () => {
vi.mocked(usePackages).mockReturnValue({
packages: [MOCK_COMMUNITY_PACKAGES[0]],
loading: false,
Expand All @@ -269,70 +203,13 @@ describe('DiscoverScreen', () => {
stdin.write('i');
await new Promise((r) => setTimeout(r, 200));

// Default scope is project
expect(lastFrame()).toContain('◉ Project');

// Tab toggles to global
stdin.write('\t');
await new Promise((r) => setTimeout(r, 50));

expect(lastFrame()).toContain('◉ Global');
});

it('Esc in plan view returns to list', async () => {
vi.mocked(usePackages).mockReturnValue({
packages: [MOCK_COMMUNITY_PACKAGES[0]],
loading: false,
error: null,
warning: null,
});
vi.mocked(resolve).mockResolvedValueOnce({
toInstall: ['dep-one', 'agent-fleet'],
alreadySatisfied: [],
cycles: [],
});

const { lastFrame, stdin } = render(<DiscoverScreen onInputCapture={() => {}} />);
await new Promise((r) => setTimeout(r, 50));

stdin.write('i');
await new Promise((r) => setTimeout(r, 200));

// Plan view is showing
expect(lastFrame()).toContain('Will install');

// Esc cancels back to list
stdin.write('\x1B');
await new Promise((r) => setTimeout(r, 50));

expect(lastFrame()).toContain('agent-fleet');
expect(lastFrame()).not.toContain('Will install');
});

it('"Installing this will also install" shows only uninstalled required deps', () => {
const pkg = {
...MOCK_COMMUNITY_PACKAGES[0],
// dep-one (required), dep-two (required), dep-three (optional)
// dep-one is already installed → only dep-two should appear in the notice
};
const installedNames = new Set(['dep-one']); // dep-one already installed
const { lastFrame } = render(
<PackageDetail
pkg={pkg}
onBack={() => {}}
onInstall={() => {}}
installedNames={installedNames}
/>
);
const frame = lastFrame();
expect(frame).toContain('Installing this will also install');
// Find the specific "also install" line and verify its exact contents
const alsoInstallLine = frame.split('\n').find(l => l.includes('Installing this will also install'));
// dep-two is required and not installed → must appear
expect(alsoInstallLine).toContain('dep-two');
// dep-one is already installed → must NOT appear in the "also install" notice
expect(alsoInstallLine).not.toContain('dep-one');
// dep-three is optional (required: false) → must NOT appear in the "also install" notice
expect(alsoInstallLine).not.toContain('dep-three');
});
});
Loading