fix(coding): preserve conflict and unicode search results
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { execFile } from 'node:child_process';
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import type { AgentBrowserModule } from '../../electron/agent-browser';
|
||||
import {
|
||||
@@ -19,6 +21,7 @@ import { PiProductTools } from '../../electron/coding-runtime/pi/product-tools';
|
||||
|
||||
const roots: string[] = [];
|
||||
const conversationId = '11111111-1111-4111-8111-111111111111';
|
||||
const exec = promisify(execFile);
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
||||
@@ -62,6 +65,14 @@ function productTools(root: string): PiProductTools {
|
||||
});
|
||||
}
|
||||
|
||||
async function git(root: string, ...args: string[]): Promise<void> {
|
||||
await exec('git', ['-C', root, ...args], { windowsHide: true });
|
||||
}
|
||||
|
||||
async function gitOutput(root: string, ...args: string[]): Promise<string> {
|
||||
return (await exec('git', ['-C', root, ...args], { windowsHide: true })).stdout.trim();
|
||||
}
|
||||
|
||||
describe('PI-105 product Host composition', () => {
|
||||
it('projects managed skills and a stable command catalog without raw Pi fields', async () => {
|
||||
const root = await configuredProject();
|
||||
@@ -116,6 +127,40 @@ describe('PI-105 product Host composition', () => {
|
||||
expect(JSON.stringify(await host.getChanges(conversationId))).not.toContain(root);
|
||||
});
|
||||
|
||||
it('keeps a merge conflict created during the target run in its changes snapshot', async () => {
|
||||
const root = await configuredProject();
|
||||
await git(root, 'init');
|
||||
await git(root, 'config', 'user.email', 'pi-products@example.invalid');
|
||||
await git(root, 'config', 'user.name', 'PI Products');
|
||||
await writeFile(path.join(root, 'conflict.txt'), 'baseline\n', 'utf8');
|
||||
await git(root, 'add', '.');
|
||||
await git(root, 'commit', '-m', 'baseline');
|
||||
const baseBranch = await gitOutput(root, 'branch', '--show-current');
|
||||
await git(root, 'switch', '-c', 'conflicting-change');
|
||||
await writeFile(path.join(root, 'conflict.txt'), 'branch change\n', 'utf8');
|
||||
await git(root, 'commit', '-am', 'branch change');
|
||||
await git(root, 'switch', baseBranch);
|
||||
await writeFile(path.join(root, 'conflict.txt'), 'base change\n', 'utf8');
|
||||
await git(root, 'commit', '-am', 'base change');
|
||||
|
||||
const tools = productTools(root);
|
||||
const host = createCodingProductHost({
|
||||
getActiveProject: async () => ({ id: 'project-a', path: root }),
|
||||
productTools: tools,
|
||||
});
|
||||
await tools.beginRun({ conversationId, runId: 'run-conflict', projectPath: root });
|
||||
await expect(exec('git', ['-C', root, 'merge', 'conflicting-change'], { windowsHide: true }))
|
||||
.rejects.toThrow();
|
||||
await tools.markBash(conversationId, 'run-conflict');
|
||||
await tools.settleRun(conversationId, 'run-conflict');
|
||||
|
||||
expect(await host.getChanges(conversationId)).toMatchObject({
|
||||
conversationId,
|
||||
runId: 'run-conflict',
|
||||
files: [expect.objectContaining({ path: 'conflict.txt', status: 'conflicted' })],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns typed project, Agent, and Conversation errors', async () => {
|
||||
const root = await configuredProject();
|
||||
const tools = productTools(root);
|
||||
|
||||
@@ -25,6 +25,10 @@ async function git(root: string, ...args: string[]): Promise<void> {
|
||||
await exec('git', ['-C', root, ...args], { windowsHide: true });
|
||||
}
|
||||
|
||||
async function gitOutput(root: string, ...args: string[]): Promise<string> {
|
||||
return (await exec('git', ['-C', root, ...args], { windowsHide: true })).stdout.trim();
|
||||
}
|
||||
|
||||
async function initializeRepository(root: string): Promise<void> {
|
||||
await git(root, 'init');
|
||||
await git(root, 'config', 'user.email', 'pi-files@example.invalid');
|
||||
@@ -79,11 +83,33 @@ describe('PI-105 project file service', () => {
|
||||
await expect(service.content(root, 'binary.bin')).rejects.toThrow('Binary project files');
|
||||
});
|
||||
|
||||
it('keeps reachable Git merge conflicts visible in file status', async () => {
|
||||
const root = await temporaryRoot('makelore-pi-files-conflict-');
|
||||
await initializeRepository(root);
|
||||
const baseBranch = await gitOutput(root, 'branch', '--show-current');
|
||||
await writeFile(path.join(root, 'conflict.txt'), 'baseline\n', 'utf8');
|
||||
await git(root, 'add', 'conflict.txt');
|
||||
await git(root, 'commit', '-m', 'conflict baseline');
|
||||
await git(root, 'switch', '-c', 'conflicting-change');
|
||||
await writeFile(path.join(root, 'conflict.txt'), 'branch change\n', 'utf8');
|
||||
await git(root, 'commit', '-am', 'branch change');
|
||||
await git(root, 'switch', baseBranch);
|
||||
await writeFile(path.join(root, 'conflict.txt'), 'base change\n', 'utf8');
|
||||
await git(root, 'commit', '-am', 'base change');
|
||||
await expect(exec('git', ['-C', root, 'merge', 'conflicting-change'], { windowsHide: true }))
|
||||
.rejects.toThrow();
|
||||
|
||||
expect(await new CodingProjectFileService().status(root)).toContainEqual({
|
||||
path: 'conflict.txt', name: 'conflict.txt', type: 'file', status: 'conflicted',
|
||||
});
|
||||
});
|
||||
|
||||
it('searches literal text with bounded relative matches and falls back outside Git', async () => {
|
||||
const root = await temporaryRoot('makelore-pi-files-search-');
|
||||
await mkdir(path.join(root, 'docs'), { recursive: true });
|
||||
await mkdir(path.join(root, 'node_modules', 'private-package'), { recursive: true });
|
||||
await writeFile(path.join(root, 'docs', 'guide.md'), 'First line\nNeedle and needle again\n', 'utf8');
|
||||
await writeFile(path.join(root, 'docs', 'unicode.md'), 'İx needle and NEEDLE\n', 'utf8');
|
||||
await writeFile(path.join(root, 'node_modules', 'private-package', 'secret.txt'), 'needle\n', 'utf8');
|
||||
const service = new CodingProjectFileService({
|
||||
async run() {
|
||||
@@ -95,7 +121,7 @@ describe('PI-105 project file service', () => {
|
||||
'docs/guide.md',
|
||||
]);
|
||||
const matches = await service.search(root, 'needle');
|
||||
expect(matches).toEqual([{
|
||||
expect(matches).toContainEqual({
|
||||
path: 'docs/guide.md',
|
||||
name: 'guide.md',
|
||||
lineNumber: 2,
|
||||
@@ -104,8 +130,20 @@ describe('PI-105 project file service', () => {
|
||||
{ text: 'Needle', start: 0, end: 6 },
|
||||
{ text: 'needle', start: 11, end: 17 },
|
||||
],
|
||||
}]);
|
||||
});
|
||||
expect(JSON.stringify(matches)).not.toContain(root);
|
||||
expect(JSON.stringify(matches)).not.toContain('private-package');
|
||||
|
||||
expect(await service.search(root, 'i')).toContainEqual(expect.objectContaining({
|
||||
path: 'docs/unicode.md',
|
||||
submatches: [{ text: 'İ', start: 0, end: 1 }],
|
||||
}));
|
||||
expect(await service.search(root, 'needle')).toContainEqual(expect.objectContaining({
|
||||
path: 'docs/unicode.md',
|
||||
submatches: [
|
||||
{ text: 'needle', start: 3, end: 9 },
|
||||
{ text: 'NEEDLE', start: 14, end: 20 },
|
||||
],
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user