feat(app): add @ workspace file autocomplete in agent chat
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+1083
-1159
File diff suppressed because it is too large
Load Diff
+2486
-2707
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
AgentStreamMessageSchema,
|
||||
@@ -6,172 +6,176 @@ import {
|
||||
SessionInboundMessageSchema,
|
||||
SessionOutboundMessageSchema,
|
||||
WSOutboundMessageSchema,
|
||||
} from "./messages.js";
|
||||
} from './messages.js'
|
||||
|
||||
describe("shared messages stream parsing", () => {
|
||||
it("parses representative fetch_agent_timeline_response payload", () => {
|
||||
describe('shared messages stream parsing', () => {
|
||||
it('parses representative fetch_agent_timeline_response payload', () => {
|
||||
const parsed = FetchAgentTimelineResponseMessageSchema.parse({
|
||||
type: "fetch_agent_timeline_response",
|
||||
type: 'fetch_agent_timeline_response',
|
||||
payload: {
|
||||
requestId: "req-1",
|
||||
agentId: "agent_live",
|
||||
direction: "tail",
|
||||
projection: "projected",
|
||||
epoch: "epoch-1",
|
||||
requestId: 'req-1',
|
||||
agentId: 'agent_live',
|
||||
direction: 'tail',
|
||||
projection: 'projected',
|
||||
epoch: 'epoch-1',
|
||||
reset: false,
|
||||
staleCursor: false,
|
||||
gap: false,
|
||||
window: { minSeq: 1, maxSeq: 2, nextSeq: 3 },
|
||||
startCursor: { epoch: "epoch-1", seq: 1 },
|
||||
endCursor: { epoch: "epoch-1", seq: 2 },
|
||||
startCursor: { epoch: 'epoch-1', seq: 1 },
|
||||
endCursor: { epoch: 'epoch-1', seq: 2 },
|
||||
hasOlder: false,
|
||||
hasNewer: false,
|
||||
entries: [
|
||||
{
|
||||
provider: "codex",
|
||||
item: { type: "assistant_message", text: "hello" },
|
||||
timestamp: "2026-02-08T20:10:00.000Z",
|
||||
provider: 'codex',
|
||||
item: { type: 'assistant_message', text: 'hello' },
|
||||
timestamp: '2026-02-08T20:10:00.000Z',
|
||||
seqStart: 1,
|
||||
seqEnd: 2,
|
||||
sourceSeqRanges: [{ startSeq: 1, endSeq: 2 }],
|
||||
collapsed: ["assistant_merge"],
|
||||
collapsed: ['assistant_merge'],
|
||||
},
|
||||
],
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
expect(parsed.payload.entries).toHaveLength(1);
|
||||
expect(parsed.payload.entries[0]?.item.type).toBe("assistant_message");
|
||||
});
|
||||
expect(parsed.payload.entries).toHaveLength(1)
|
||||
expect(parsed.payload.entries[0]?.item.type).toBe('assistant_message')
|
||||
})
|
||||
|
||||
it("parses representative agent_stream tool_call event", () => {
|
||||
it('parses representative agent_stream tool_call event', () => {
|
||||
const parsed = AgentStreamMessageSchema.parse({
|
||||
type: "agent_stream",
|
||||
type: 'agent_stream',
|
||||
payload: {
|
||||
agentId: "agent_live",
|
||||
timestamp: "2026-02-08T20:10:00.000Z",
|
||||
agentId: 'agent_live',
|
||||
timestamp: '2026-02-08T20:10:00.000Z',
|
||||
event: {
|
||||
type: "timeline",
|
||||
provider: "claude",
|
||||
type: 'timeline',
|
||||
provider: 'claude',
|
||||
item: {
|
||||
type: "tool_call",
|
||||
callId: "call_live",
|
||||
name: "shell",
|
||||
status: "running",
|
||||
type: 'tool_call',
|
||||
callId: 'call_live',
|
||||
name: 'shell',
|
||||
status: 'running',
|
||||
detail: {
|
||||
type: "shell",
|
||||
command: "ls",
|
||||
type: 'shell',
|
||||
command: 'ls',
|
||||
},
|
||||
error: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
expect(parsed.payload.event.type).toBe("timeline");
|
||||
if (parsed.payload.event.type === "timeline") {
|
||||
expect(parsed.payload.event.item.type).toBe("tool_call");
|
||||
if (parsed.payload.event.item.type === "tool_call") {
|
||||
expect(parsed.payload.event.item.status).toBe("running");
|
||||
expect(parsed.payload.event.type).toBe('timeline')
|
||||
if (parsed.payload.event.type === 'timeline') {
|
||||
expect(parsed.payload.event.item.type).toBe('tool_call')
|
||||
if (parsed.payload.event.item.type === 'tool_call') {
|
||||
expect(parsed.payload.event.item.status).toBe('running')
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
it("rejects removed initialize_agent_request inbound payload", () => {
|
||||
it('rejects removed initialize_agent_request inbound payload', () => {
|
||||
const parsed = SessionInboundMessageSchema.safeParse({
|
||||
type: "initialize_agent_request",
|
||||
agentId: "agent-legacy",
|
||||
requestId: "req-legacy-1",
|
||||
});
|
||||
expect(parsed.success).toBe(false);
|
||||
});
|
||||
type: 'initialize_agent_request',
|
||||
agentId: 'agent-legacy',
|
||||
requestId: 'req-legacy-1',
|
||||
})
|
||||
expect(parsed.success).toBe(false)
|
||||
})
|
||||
|
||||
it("rejects removed initialize_agent_request outbound payload", () => {
|
||||
it('rejects removed initialize_agent_request outbound payload', () => {
|
||||
const parsed = SessionOutboundMessageSchema.safeParse({
|
||||
type: "initialize_agent_request",
|
||||
type: 'initialize_agent_request',
|
||||
payload: {
|
||||
requestId: "req-legacy-1",
|
||||
agentId: "agent-legacy",
|
||||
agentStatus: "running",
|
||||
requestId: 'req-legacy-1',
|
||||
agentId: 'agent-legacy',
|
||||
agentStatus: 'running',
|
||||
timelineSize: 12,
|
||||
},
|
||||
});
|
||||
expect(parsed.success).toBe(false);
|
||||
});
|
||||
})
|
||||
expect(parsed.success).toBe(false)
|
||||
})
|
||||
|
||||
it("parses directory suggestions request and response payloads", () => {
|
||||
it('parses directory suggestions request and response payloads', () => {
|
||||
const requestParsed = SessionInboundMessageSchema.safeParse({
|
||||
type: "directory_suggestions_request",
|
||||
query: "proj",
|
||||
type: 'directory_suggestions_request',
|
||||
query: 'proj',
|
||||
cwd: '/tmp/project',
|
||||
includeFiles: true,
|
||||
includeDirectories: true,
|
||||
limit: 20,
|
||||
requestId: "req-dir-1",
|
||||
});
|
||||
expect(requestParsed.success).toBe(true);
|
||||
requestId: 'req-dir-1',
|
||||
})
|
||||
expect(requestParsed.success).toBe(true)
|
||||
|
||||
const responseParsed = SessionOutboundMessageSchema.safeParse({
|
||||
type: "directory_suggestions_response",
|
||||
type: 'directory_suggestions_response',
|
||||
payload: {
|
||||
directories: ["/Users/test/projects/paseo"],
|
||||
directories: ['/Users/test/projects/paseo'],
|
||||
entries: [{ path: '/Users/test/projects/paseo', kind: 'directory' }],
|
||||
error: null,
|
||||
requestId: "req-dir-1",
|
||||
requestId: 'req-dir-1',
|
||||
},
|
||||
});
|
||||
expect(responseParsed.success).toBe(true);
|
||||
});
|
||||
})
|
||||
expect(responseParsed.success).toBe(true)
|
||||
})
|
||||
|
||||
it("rejects websocket envelope for removed agent_stream_snapshot message type", () => {
|
||||
it('rejects websocket envelope for removed agent_stream_snapshot message type', () => {
|
||||
const fixture = {
|
||||
type: "agent_stream_snapshot",
|
||||
type: 'agent_stream_snapshot',
|
||||
payload: {
|
||||
agentId: "agent-legacy",
|
||||
agentId: 'agent-legacy',
|
||||
events: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
const wrapped = WSOutboundMessageSchema.safeParse({
|
||||
type: "session",
|
||||
type: 'session',
|
||||
message: fixture,
|
||||
});
|
||||
expect(wrapped.success).toBe(false);
|
||||
});
|
||||
})
|
||||
expect(wrapped.success).toBe(false)
|
||||
})
|
||||
|
||||
it("rejects removed legacy git diff request messages", () => {
|
||||
it('rejects removed legacy git diff request messages', () => {
|
||||
const gitDiffParsed = SessionInboundMessageSchema.safeParse({
|
||||
type: "git_diff_request",
|
||||
agentId: "agent-1",
|
||||
requestId: "req-1",
|
||||
});
|
||||
expect(gitDiffParsed.success).toBe(false);
|
||||
type: 'git_diff_request',
|
||||
agentId: 'agent-1',
|
||||
requestId: 'req-1',
|
||||
})
|
||||
expect(gitDiffParsed.success).toBe(false)
|
||||
|
||||
const highlightedParsed = SessionInboundMessageSchema.safeParse({
|
||||
type: "highlighted_diff_request",
|
||||
agentId: "agent-1",
|
||||
requestId: "req-2",
|
||||
});
|
||||
expect(highlightedParsed.success).toBe(false);
|
||||
});
|
||||
type: 'highlighted_diff_request',
|
||||
agentId: 'agent-1',
|
||||
requestId: 'req-2',
|
||||
})
|
||||
expect(highlightedParsed.success).toBe(false)
|
||||
})
|
||||
|
||||
it("rejects removed legacy git diff response messages", () => {
|
||||
it('rejects removed legacy git diff response messages', () => {
|
||||
const gitDiffParsed = SessionOutboundMessageSchema.safeParse({
|
||||
type: "git_diff_response",
|
||||
type: 'git_diff_response',
|
||||
payload: {
|
||||
agentId: "agent-1",
|
||||
diff: "",
|
||||
agentId: 'agent-1',
|
||||
diff: '',
|
||||
error: null,
|
||||
requestId: "req-1",
|
||||
requestId: 'req-1',
|
||||
},
|
||||
});
|
||||
expect(gitDiffParsed.success).toBe(false);
|
||||
})
|
||||
expect(gitDiffParsed.success).toBe(false)
|
||||
|
||||
const highlightedParsed = SessionOutboundMessageSchema.safeParse({
|
||||
type: "highlighted_diff_response",
|
||||
type: 'highlighted_diff_response',
|
||||
payload: {
|
||||
agentId: "agent-1",
|
||||
agentId: 'agent-1',
|
||||
files: [],
|
||||
error: null,
|
||||
requestId: "req-2",
|
||||
requestId: 'req-2',
|
||||
},
|
||||
});
|
||||
expect(highlightedParsed.success).toBe(false);
|
||||
});
|
||||
});
|
||||
})
|
||||
expect(highlightedParsed.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,165 +1,237 @@
|
||||
import {
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
realpathSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { searchHomeDirectories } from "./directory-suggestions.js";
|
||||
import { mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { searchHomeDirectories, searchWorkspaceEntries } from './directory-suggestions.js'
|
||||
|
||||
describe("searchHomeDirectories", () => {
|
||||
let tempRoot: string;
|
||||
let homeDir: string;
|
||||
let outsideDir: string;
|
||||
describe('searchHomeDirectories', () => {
|
||||
let tempRoot: string
|
||||
let homeDir: string
|
||||
let outsideDir: string
|
||||
|
||||
beforeEach(() => {
|
||||
tempRoot = realpathSync(mkdtempSync(path.join(tmpdir(), "directory-suggestions-")));
|
||||
homeDir = path.join(tempRoot, "home");
|
||||
outsideDir = path.join(tempRoot, "outside");
|
||||
tempRoot = realpathSync(mkdtempSync(path.join(tmpdir(), 'directory-suggestions-')))
|
||||
homeDir = path.join(tempRoot, 'home')
|
||||
outsideDir = path.join(tempRoot, 'outside')
|
||||
|
||||
mkdirSync(homeDir, { recursive: true });
|
||||
mkdirSync(outsideDir, { recursive: true });
|
||||
mkdirSync(homeDir, { recursive: true })
|
||||
mkdirSync(outsideDir, { recursive: true })
|
||||
|
||||
mkdirSync(path.join(homeDir, "projects", "paseo"), { recursive: true });
|
||||
mkdirSync(path.join(homeDir, "projects", "playground"), { recursive: true });
|
||||
mkdirSync(path.join(homeDir, "documents", "plans"), { recursive: true });
|
||||
mkdirSync(path.join(homeDir, ".hidden", "cache"), { recursive: true });
|
||||
writeFileSync(path.join(homeDir, "projects", "README.md"), "not a directory\n");
|
||||
mkdirSync(path.join(homeDir, 'projects', 'paseo'), { recursive: true })
|
||||
mkdirSync(path.join(homeDir, 'projects', 'playground'), { recursive: true })
|
||||
mkdirSync(path.join(homeDir, 'documents', 'plans'), { recursive: true })
|
||||
mkdirSync(path.join(homeDir, '.hidden', 'cache'), { recursive: true })
|
||||
writeFileSync(path.join(homeDir, 'projects', 'README.md'), 'not a directory\n')
|
||||
|
||||
mkdirSync(path.join(outsideDir, "outside-match"), { recursive: true });
|
||||
symlinkSync(path.join(outsideDir, "outside-match"), path.join(homeDir, "outside-link"));
|
||||
});
|
||||
mkdirSync(path.join(outsideDir, 'outside-match'), { recursive: true })
|
||||
symlinkSync(path.join(outsideDir, 'outside-match'), path.join(homeDir, 'outside-link'))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
});
|
||||
rmSync(tempRoot, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it("returns an empty list for blank queries", async () => {
|
||||
it('returns an empty list for blank queries', async () => {
|
||||
await expect(
|
||||
searchHomeDirectories({
|
||||
homeDir,
|
||||
query: " ",
|
||||
query: ' ',
|
||||
limit: 10,
|
||||
})
|
||||
).resolves.toEqual([]);
|
||||
});
|
||||
).resolves.toEqual([])
|
||||
})
|
||||
|
||||
it("returns only existing directories", async () => {
|
||||
it('returns only existing directories', async () => {
|
||||
const results = await searchHomeDirectories({
|
||||
homeDir,
|
||||
query: "proj",
|
||||
query: 'proj',
|
||||
limit: 10,
|
||||
});
|
||||
})
|
||||
|
||||
expect(results).toContain(path.join(homeDir, "projects"));
|
||||
expect(results).toContain(path.join(homeDir, "projects", "paseo"));
|
||||
expect(results).not.toContain(path.join(homeDir, "projects", "README.md"));
|
||||
});
|
||||
expect(results).toContain(path.join(homeDir, 'projects'))
|
||||
expect(results).toContain(path.join(homeDir, 'projects', 'paseo'))
|
||||
expect(results).not.toContain(path.join(homeDir, 'projects', 'README.md'))
|
||||
})
|
||||
|
||||
it("supports home-relative path query syntax", async () => {
|
||||
it('supports home-relative path query syntax', async () => {
|
||||
const results = await searchHomeDirectories({
|
||||
homeDir,
|
||||
query: "~/projects/pa",
|
||||
query: '~/projects/pa',
|
||||
limit: 10,
|
||||
});
|
||||
})
|
||||
|
||||
expect(results).toEqual([
|
||||
path.join(homeDir, "projects", "paseo"),
|
||||
]);
|
||||
});
|
||||
expect(results).toEqual([path.join(homeDir, 'projects', 'paseo')])
|
||||
})
|
||||
|
||||
it("prioritizes exact segment matches before segment-prefix matches", async () => {
|
||||
const exactSegmentPath = path.join(
|
||||
homeDir,
|
||||
"something",
|
||||
"faro",
|
||||
"something-else"
|
||||
);
|
||||
const prefixSegmentPath = path.join(
|
||||
homeDir,
|
||||
"something",
|
||||
"somethingelse",
|
||||
"faro-bla"
|
||||
);
|
||||
mkdirSync(exactSegmentPath, { recursive: true });
|
||||
mkdirSync(prefixSegmentPath, { recursive: true });
|
||||
it('prioritizes exact segment matches before segment-prefix matches', async () => {
|
||||
const exactSegmentPath = path.join(homeDir, 'something', 'faro', 'something-else')
|
||||
const prefixSegmentPath = path.join(homeDir, 'something', 'somethingelse', 'faro-bla')
|
||||
mkdirSync(exactSegmentPath, { recursive: true })
|
||||
mkdirSync(prefixSegmentPath, { recursive: true })
|
||||
|
||||
const results = await searchHomeDirectories({
|
||||
homeDir,
|
||||
query: "faro",
|
||||
query: 'faro',
|
||||
limit: 30,
|
||||
});
|
||||
})
|
||||
|
||||
const exactIndex = results.indexOf(exactSegmentPath);
|
||||
const prefixIndex = results.indexOf(prefixSegmentPath);
|
||||
expect(exactIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(prefixIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(exactIndex).toBeLessThan(prefixIndex);
|
||||
});
|
||||
const exactIndex = results.indexOf(exactSegmentPath)
|
||||
const prefixIndex = results.indexOf(prefixSegmentPath)
|
||||
expect(exactIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(prefixIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(exactIndex).toBeLessThan(prefixIndex)
|
||||
})
|
||||
|
||||
it("prioritizes partial matches that appear earlier in the path", async () => {
|
||||
const earlierPath = path.join(homeDir, "farofoo");
|
||||
const laterPath = path.join(homeDir, "x", "y", "farofoo");
|
||||
mkdirSync(earlierPath, { recursive: true });
|
||||
mkdirSync(laterPath, { recursive: true });
|
||||
it('prioritizes partial matches that appear earlier in the path', async () => {
|
||||
const earlierPath = path.join(homeDir, 'farofoo')
|
||||
const laterPath = path.join(homeDir, 'x', 'y', 'farofoo')
|
||||
mkdirSync(earlierPath, { recursive: true })
|
||||
mkdirSync(laterPath, { recursive: true })
|
||||
|
||||
const results = await searchHomeDirectories({
|
||||
homeDir,
|
||||
query: "arofo",
|
||||
query: 'arofo',
|
||||
limit: 30,
|
||||
});
|
||||
})
|
||||
|
||||
const earlierIndex = results.indexOf(earlierPath);
|
||||
const laterIndex = results.indexOf(laterPath);
|
||||
expect(earlierIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(laterIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(earlierIndex).toBeLessThan(laterIndex);
|
||||
});
|
||||
const earlierIndex = results.indexOf(earlierPath)
|
||||
const laterIndex = results.indexOf(laterPath)
|
||||
expect(earlierIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(laterIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(earlierIndex).toBeLessThan(laterIndex)
|
||||
})
|
||||
|
||||
it("returns home-root suggestions when query is '~'", async () => {
|
||||
const results = await searchHomeDirectories({
|
||||
homeDir,
|
||||
query: "~",
|
||||
query: '~',
|
||||
limit: 20,
|
||||
});
|
||||
})
|
||||
|
||||
expect(results).toContain(path.join(homeDir, "projects"));
|
||||
expect(results).toContain(path.join(homeDir, "documents"));
|
||||
expect(results).not.toContain(path.join(homeDir, ".hidden"));
|
||||
});
|
||||
expect(results).toContain(path.join(homeDir, 'projects'))
|
||||
expect(results).toContain(path.join(homeDir, 'documents'))
|
||||
expect(results).not.toContain(path.join(homeDir, '.hidden'))
|
||||
})
|
||||
|
||||
it("does not return hidden directories during tree search", async () => {
|
||||
it('does not return hidden directories during tree search', async () => {
|
||||
const results = await searchHomeDirectories({
|
||||
homeDir,
|
||||
query: "cache",
|
||||
query: 'cache',
|
||||
limit: 20,
|
||||
});
|
||||
})
|
||||
|
||||
expect(results).not.toContain(path.join(homeDir, ".hidden", "cache"));
|
||||
});
|
||||
expect(results).not.toContain(path.join(homeDir, '.hidden', 'cache'))
|
||||
})
|
||||
|
||||
it("does not return paths that escape home through symlinks", async () => {
|
||||
it('does not return paths that escape home through symlinks', async () => {
|
||||
const results = await searchHomeDirectories({
|
||||
homeDir,
|
||||
query: "outside",
|
||||
query: 'outside',
|
||||
limit: 20,
|
||||
});
|
||||
})
|
||||
|
||||
expect(results).not.toContain(path.join(homeDir, "outside-link"));
|
||||
expect(results).not.toContain(path.join(outsideDir, "outside-match"));
|
||||
});
|
||||
expect(results).not.toContain(path.join(homeDir, 'outside-link'))
|
||||
expect(results).not.toContain(path.join(outsideDir, 'outside-match'))
|
||||
})
|
||||
|
||||
it("respects the result limit", async () => {
|
||||
it('respects the result limit', async () => {
|
||||
const results = await searchHomeDirectories({
|
||||
homeDir,
|
||||
query: "p",
|
||||
query: 'p',
|
||||
limit: 1,
|
||||
});
|
||||
})
|
||||
|
||||
expect(results).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
expect(results).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('searchWorkspaceEntries', () => {
|
||||
let tempRoot: string
|
||||
let workspaceDir: string
|
||||
let outsideDir: string
|
||||
|
||||
beforeEach(() => {
|
||||
tempRoot = realpathSync(mkdtempSync(path.join(tmpdir(), 'workspace-suggestions-')))
|
||||
workspaceDir = path.join(tempRoot, 'workspace')
|
||||
outsideDir = path.join(tempRoot, 'outside')
|
||||
|
||||
mkdirSync(path.join(workspaceDir, 'src', 'components'), {
|
||||
recursive: true,
|
||||
})
|
||||
mkdirSync(path.join(workspaceDir, 'docs'), { recursive: true })
|
||||
mkdirSync(path.join(outsideDir, 'escaped'), { recursive: true })
|
||||
|
||||
writeFileSync(path.join(workspaceDir, 'README.md'), '# paseo\n')
|
||||
writeFileSync(
|
||||
path.join(workspaceDir, 'src', 'components', 'chat-input.tsx'),
|
||||
'export const ChatInput = null;\n'
|
||||
)
|
||||
writeFileSync(path.join(workspaceDir, 'docs', 'notes.md'), 'notes\n')
|
||||
|
||||
symlinkSync(path.join(outsideDir, 'escaped'), path.join(workspaceDir, 'escaped-link'))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tempRoot, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('returns relative file and directory suggestions for workspace queries', async () => {
|
||||
const results = await searchWorkspaceEntries({
|
||||
cwd: workspaceDir,
|
||||
query: 'chat',
|
||||
limit: 20,
|
||||
includeFiles: true,
|
||||
includeDirectories: true,
|
||||
})
|
||||
|
||||
expect(results).toContainEqual({
|
||||
path: 'src/components/chat-input.tsx',
|
||||
kind: 'file',
|
||||
})
|
||||
expect(results.some((entry) => entry.path === path.join(workspaceDir, 'src'))).toBe(false)
|
||||
})
|
||||
|
||||
it('filters entries by kind', async () => {
|
||||
const dirsOnly = await searchWorkspaceEntries({
|
||||
cwd: workspaceDir,
|
||||
query: 'src',
|
||||
limit: 20,
|
||||
includeFiles: false,
|
||||
includeDirectories: true,
|
||||
})
|
||||
expect(dirsOnly.some((entry) => entry.kind === 'file')).toBe(false)
|
||||
expect(dirsOnly.some((entry) => entry.path === 'src')).toBe(true)
|
||||
|
||||
const filesOnly = await searchWorkspaceEntries({
|
||||
cwd: workspaceDir,
|
||||
query: 'readme',
|
||||
limit: 20,
|
||||
includeFiles: true,
|
||||
includeDirectories: false,
|
||||
})
|
||||
expect(filesOnly).toEqual([{ path: 'README.md', kind: 'file' }])
|
||||
})
|
||||
|
||||
it('supports path-style queries and does not escape cwd through symlinks', async () => {
|
||||
const pathResults = await searchWorkspaceEntries({
|
||||
cwd: workspaceDir,
|
||||
query: 'src/co',
|
||||
limit: 20,
|
||||
includeFiles: true,
|
||||
includeDirectories: true,
|
||||
})
|
||||
expect(pathResults).toContainEqual({
|
||||
path: 'src/components',
|
||||
kind: 'directory',
|
||||
})
|
||||
|
||||
const escapedResults = await searchWorkspaceEntries({
|
||||
cwd: workspaceDir,
|
||||
query: 'escaped',
|
||||
limit: 20,
|
||||
includeFiles: true,
|
||||
includeDirectories: true,
|
||||
})
|
||||
expect(escapedResults.some((entry) => entry.path.includes('escaped-link'))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user