mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
540 lines
20 KiB
TypeScript
540 lines
20 KiB
TypeScript
import { afterEach, describe, expect, it } from 'bun:test'
|
|
import * as fs from 'node:fs/promises'
|
|
import { execFileSync } from 'node:child_process'
|
|
import * as os from 'node:os'
|
|
import * as path from 'node:path'
|
|
import { WorkspaceService } from '../services/workspaceService.js'
|
|
|
|
const cleanupDirs = new Set<string>()
|
|
const ONE_MIB = 1024 * 1024
|
|
|
|
function trackDir(dir: string): string {
|
|
cleanupDirs.add(dir)
|
|
return dir
|
|
}
|
|
|
|
async function makeTempDir(prefix: string): Promise<string> {
|
|
return trackDir(await fs.mkdtemp(path.join(os.tmpdir(), prefix)))
|
|
}
|
|
|
|
function git(cwd: string, ...args: string[]): string {
|
|
return execFileSync('git', args, {
|
|
cwd,
|
|
encoding: 'utf8',
|
|
})
|
|
}
|
|
|
|
async function createGitWorkspace(): Promise<string> {
|
|
const repoDir = await makeTempDir('workspace-service-git-')
|
|
|
|
git(repoDir, 'init')
|
|
git(repoDir, 'config', 'user.email', 'workspace-service@example.com')
|
|
git(repoDir, 'config', 'user.name', 'Workspace Service')
|
|
|
|
await fs.writeFile(path.join(repoDir, 'tracked.txt'), 'before\n')
|
|
await fs.writeFile(path.join(repoDir, 'deleted.txt'), 'delete me\n')
|
|
await fs.writeFile(path.join(repoDir, 'clean.txt'), 'clean\n')
|
|
git(repoDir, 'add', 'tracked.txt', 'deleted.txt', 'clean.txt')
|
|
git(repoDir, 'commit', '-m', 'initial')
|
|
|
|
await fs.writeFile(path.join(repoDir, 'tracked.txt'), 'before\nafter\n')
|
|
await fs.writeFile(path.join(repoDir, 'new.txt'), 'new file\n')
|
|
git(repoDir, 'add', 'new.txt')
|
|
await fs.unlink(path.join(repoDir, 'deleted.txt'))
|
|
await fs.writeFile(path.join(repoDir, 'untracked.txt'), 'still untracked\n')
|
|
|
|
return repoDir
|
|
}
|
|
|
|
async function createNestedGitWorkspace(): Promise<{
|
|
repoDir: string
|
|
workDir: string
|
|
}> {
|
|
const repoDir = await makeTempDir('workspace-service-nested-git-')
|
|
const workDir = path.join(repoDir, 'subdir')
|
|
|
|
git(repoDir, 'init')
|
|
git(repoDir, 'config', 'user.email', 'workspace-service@example.com')
|
|
git(repoDir, 'config', 'user.name', 'Workspace Service')
|
|
|
|
await fs.mkdir(workDir)
|
|
await fs.writeFile(path.join(repoDir, 'root.txt'), 'root original\n')
|
|
await fs.writeFile(path.join(workDir, 'sub.txt'), 'sub original\n')
|
|
git(repoDir, 'add', 'root.txt', 'subdir/sub.txt')
|
|
git(repoDir, 'commit', '-m', 'initial')
|
|
|
|
await fs.writeFile(path.join(repoDir, 'root.txt'), 'root original\nroot changed\n')
|
|
await fs.writeFile(path.join(workDir, 'sub.txt'), 'sub original\nsub changed\n')
|
|
|
|
return { repoDir, workDir }
|
|
}
|
|
|
|
afterEach(async () => {
|
|
for (const dir of cleanupDirs) {
|
|
await fs.rm(dir, { recursive: true, force: true })
|
|
}
|
|
cleanupDirs.clear()
|
|
})
|
|
|
|
describe('WorkspaceService', () => {
|
|
it('returns git status for modified, added, deleted, and untracked files', async () => {
|
|
const repoDir = await createGitWorkspace()
|
|
const service = new WorkspaceService(async (sessionId) => sessionId === 'session-1' ? repoDir : null)
|
|
|
|
const result = await service.getStatus('session-1')
|
|
|
|
expect(result.state).toBe('ok')
|
|
expect(result.workDir).toBe(repoDir)
|
|
expect(result.isGitRepo).toBe(true)
|
|
expect(result.repoName).toBe(path.basename(repoDir))
|
|
expect(result.branch).toBeTruthy()
|
|
|
|
const files = new Map(result.changedFiles.map((file) => [file.path, file]))
|
|
expect(Array.from(files.keys()).sort()).toEqual([
|
|
'deleted.txt',
|
|
'new.txt',
|
|
'tracked.txt',
|
|
'untracked.txt',
|
|
])
|
|
expect(files.get('tracked.txt')?.status).toBe('modified')
|
|
expect(files.get('tracked.txt')?.additions).toBeGreaterThan(0)
|
|
expect(files.get('new.txt')?.status).toBe('added')
|
|
expect(files.get('new.txt')?.additions).toBeGreaterThan(0)
|
|
expect(files.get('deleted.txt')?.status).toBe('deleted')
|
|
expect(files.get('deleted.txt')?.deletions).toBeGreaterThan(0)
|
|
expect(files.get('untracked.txt')).toMatchObject({
|
|
status: 'untracked',
|
|
additions: 1,
|
|
deletions: 0,
|
|
})
|
|
})
|
|
|
|
it('scopes git status and diff paths to a nested workDir inside a repo', async () => {
|
|
const { repoDir, workDir } = await createNestedGitWorkspace()
|
|
const service = new WorkspaceService(async (sessionId) => sessionId === 'session-1' ? workDir : null)
|
|
|
|
const status = await service.getStatus('session-1')
|
|
|
|
expect(status.state).toBe('ok')
|
|
expect(status.workDir).toBe(workDir)
|
|
expect(status.repoName).toBe(path.basename(repoDir))
|
|
expect(status.changedFiles).toHaveLength(1)
|
|
expect(status.changedFiles[0]).toMatchObject({
|
|
path: 'sub.txt',
|
|
status: 'modified',
|
|
})
|
|
expect(status.changedFiles[0]?.additions).toBeGreaterThan(0)
|
|
expect(status.changedFiles[0]?.deletions).toBeGreaterThanOrEqual(0)
|
|
expect(status.changedFiles.some((file) => file.path === 'root.txt')).toBe(false)
|
|
|
|
const diff = await service.getDiff('session-1', 'sub.txt')
|
|
expect(diff.state).toBe('ok')
|
|
expect(diff.diff).toContain('subdir/sub.txt')
|
|
expect(diff.diff?.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('returns explicit non-git and missing-workdir states', async () => {
|
|
const nonGitDir = await makeTempDir('workspace-service-non-git-')
|
|
const missingDir = path.join(await makeTempDir('workspace-service-missing-parent-'), 'missing')
|
|
const service = new WorkspaceService(async (sessionId) => {
|
|
if (sessionId === 'non-git') return nonGitDir
|
|
if (sessionId === 'missing') return missingDir
|
|
return null
|
|
})
|
|
|
|
await expect(service.getStatus('unknown')).rejects.toThrow('Session not found: unknown')
|
|
|
|
await expect(service.getStatus('non-git')).resolves.toMatchObject({
|
|
state: 'ok',
|
|
workDir: nonGitDir,
|
|
repoName: path.basename(nonGitDir),
|
|
isGitRepo: false,
|
|
changedFiles: [],
|
|
})
|
|
|
|
await expect(service.getStatus('missing')).resolves.toMatchObject({
|
|
state: 'missing_workdir',
|
|
workDir: missingDir,
|
|
isGitRepo: false,
|
|
changedFiles: [],
|
|
})
|
|
})
|
|
|
|
it('reports session tool edits without requiring a git repository', async () => {
|
|
const nonGitDir = await makeTempDir('workspace-service-session-changes-')
|
|
await fs.mkdir(path.join(nonGitDir, 'src'))
|
|
await fs.writeFile(path.join(nonGitDir, 'src/App.jsx'), 'export default function App() { return <main>New</main> }\n')
|
|
|
|
const service = new WorkspaceService(
|
|
async () => nonGitDir,
|
|
async () => [{
|
|
id: 'assistant-1',
|
|
type: 'tool_use',
|
|
timestamp: new Date().toISOString(),
|
|
content: [{
|
|
type: 'tool_use',
|
|
name: 'Edit',
|
|
input: {
|
|
file_path: 'src/App.jsx',
|
|
old_string: 'export default function App() { return <main>Old</main> }\n',
|
|
new_string: 'export default function App() { return <main>New</main> }\n',
|
|
},
|
|
}],
|
|
}],
|
|
)
|
|
|
|
const status = await service.getStatus('session-1')
|
|
|
|
expect(status).toMatchObject({
|
|
state: 'ok',
|
|
workDir: nonGitDir,
|
|
isGitRepo: false,
|
|
changedFiles: [{
|
|
path: 'src/App.jsx',
|
|
status: 'modified',
|
|
additions: 1,
|
|
deletions: 1,
|
|
}],
|
|
})
|
|
|
|
const diff = await service.getDiff('session-1', 'src/App.jsx')
|
|
expect(diff.state).toBe('ok')
|
|
expect(diff.diff).toContain('diff --session a/src/App.jsx b/src/App.jsx')
|
|
expect(diff.diff).toContain('-export default function App() { return <main>Old</main> }')
|
|
expect(diff.diff).toContain('+export default function App() { return <main>New</main> }')
|
|
})
|
|
|
|
it('reports file-history changes without requiring a git repository', async () => {
|
|
const nonGitDir = await makeTempDir('workspace-service-file-history-')
|
|
const generatedFile = path.join(nonGitDir, 'aacc', 'src', 'App.tsx')
|
|
await fs.mkdir(path.dirname(generatedFile), { recursive: true })
|
|
await fs.writeFile(generatedFile, 'export default function App() { return <main>Tetris</main> }\n')
|
|
|
|
const service = new WorkspaceService(
|
|
async () => nonGitDir,
|
|
async () => [],
|
|
async () => [{
|
|
messageId: '11111111-1111-4111-8111-111111111111',
|
|
timestamp: new Date('2026-01-01T00:00:00.000Z'),
|
|
trackedFileBackups: {
|
|
'aacc/src/App.tsx': {
|
|
backupFileName: null,
|
|
version: 1,
|
|
backupTime: new Date('2026-01-01T00:00:00.000Z'),
|
|
},
|
|
},
|
|
}],
|
|
)
|
|
|
|
const status = await service.getStatus('session-1')
|
|
|
|
expect(status).toMatchObject({
|
|
state: 'ok',
|
|
workDir: nonGitDir,
|
|
isGitRepo: false,
|
|
changedFiles: [{
|
|
path: 'aacc/src/App.tsx',
|
|
status: 'added',
|
|
additions: 1,
|
|
deletions: 0,
|
|
}],
|
|
})
|
|
|
|
const diff = await service.getDiff('session-1', 'aacc/src/App.tsx')
|
|
expect(diff.state).toBe('ok')
|
|
expect(diff.diff).toContain('diff --session /dev/null b/aacc/src/App.tsx')
|
|
expect(diff.diff).toContain('+export default function App() { return <main>Tetris</main> }')
|
|
})
|
|
|
|
it('matches Windows file-history paths case-insensitively inside the workspace', async () => {
|
|
if (process.platform !== 'win32') return
|
|
|
|
const nonGitDir = await makeTempDir('workspace-service-windows-paths-')
|
|
const targetFile = path.join(nonGitDir, 'Child', 'index.ts')
|
|
await fs.mkdir(path.dirname(targetFile), { recursive: true })
|
|
await fs.writeFile(targetFile, 'export const value = 1\n')
|
|
|
|
const lowerDrivePath = targetFile[0]?.toLowerCase() + targetFile.slice(1)
|
|
const service = new WorkspaceService(
|
|
async () => nonGitDir,
|
|
async () => [],
|
|
async () => [{
|
|
messageId: '22222222-2222-4222-8222-222222222222',
|
|
timestamp: new Date('2026-01-01T00:00:00.000Z'),
|
|
trackedFileBackups: {
|
|
[lowerDrivePath]: {
|
|
backupFileName: null,
|
|
version: 1,
|
|
backupTime: new Date('2026-01-01T00:00:00.000Z'),
|
|
},
|
|
},
|
|
}],
|
|
)
|
|
|
|
const status = await service.getStatus('session-1')
|
|
|
|
expect(status.changedFiles).toEqual([{
|
|
path: 'Child/index.ts',
|
|
oldPath: undefined,
|
|
status: 'added',
|
|
additions: 1,
|
|
deletions: 0,
|
|
}])
|
|
})
|
|
|
|
it('rejects traversal attempts for file, diff, and tree access', async () => {
|
|
const repoDir = await createGitWorkspace()
|
|
const service = new WorkspaceService(async () => repoDir)
|
|
|
|
await expect(service.readFile('session-1', '../outside.txt')).rejects.toThrow(/outside workspace/)
|
|
await expect(service.getDiff('session-1', '../outside.txt')).resolves.toMatchObject({
|
|
state: 'error',
|
|
path: '../outside.txt',
|
|
})
|
|
await expect(service.readTree('session-1', '../outside')).rejects.toThrow(/outside workspace/)
|
|
})
|
|
|
|
it('rejects symlink targets that escape the workspace root', async () => {
|
|
const workDir = await makeTempDir('workspace-service-symlink-')
|
|
const outsideDir = await makeTempDir('workspace-service-symlink-outside-')
|
|
const outsideFile = path.join(outsideDir, 'secret.txt')
|
|
await fs.writeFile(outsideFile, 'top secret\n')
|
|
await fs.symlink(outsideFile, path.join(workDir, 'escape.txt'))
|
|
|
|
const service = new WorkspaceService(async () => workDir)
|
|
|
|
await expect(service.readFile('session-1', 'escape.txt')).rejects.toThrow(/outside workspace/)
|
|
})
|
|
|
|
it('returns error for an untracked symlink that escapes the workspace root', async () => {
|
|
const repoDir = await makeTempDir('workspace-service-symlink-git-')
|
|
const outsideDir = await makeTempDir('workspace-service-symlink-git-outside-')
|
|
const outsideFile = path.join(outsideDir, 'secret.txt')
|
|
|
|
git(repoDir, 'init')
|
|
git(repoDir, 'config', 'user.email', 'workspace-service@example.com')
|
|
git(repoDir, 'config', 'user.name', 'Workspace Service')
|
|
await fs.writeFile(path.join(repoDir, 'tracked.txt'), 'tracked\n')
|
|
git(repoDir, 'add', 'tracked.txt')
|
|
git(repoDir, 'commit', '-m', 'initial')
|
|
|
|
await fs.writeFile(outsideFile, 'top secret\n')
|
|
await fs.symlink(outsideFile, path.join(repoDir, 'escape.txt'))
|
|
|
|
const service = new WorkspaceService(async () => repoDir)
|
|
|
|
const status = await service.getStatus('session-1')
|
|
expect(status.state).toBe('error')
|
|
expect(status.error).toMatch(/outside workspace/)
|
|
|
|
await expect(service.getDiff('session-1', 'escape.txt')).resolves.toMatchObject({
|
|
state: 'error',
|
|
path: 'escape.txt',
|
|
})
|
|
const diffOutcome = await service.getDiff('session-1', 'escape.txt')
|
|
expect(diffOutcome.error).toMatch(/outside workspace/)
|
|
})
|
|
|
|
it('returns explicit readFile states for text, binary, large, and missing targets', async () => {
|
|
const workDir = await makeTempDir('workspace-service-files-')
|
|
const service = new WorkspaceService(async () => workDir)
|
|
|
|
await fs.writeFile(path.join(workDir, 'note.ts'), 'export const answer = 42\n')
|
|
await fs.writeFile(path.join(workDir, 'binary.bin'), Buffer.from([0, 1, 2, 3]))
|
|
await fs.writeFile(path.join(workDir, 'image.png'), Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00]))
|
|
await fs.writeFile(
|
|
path.join(workDir, 'large-image.png'),
|
|
Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47]), Buffer.alloc(ONE_MIB + 1, 0xff)]),
|
|
)
|
|
await fs.writeFile(path.join(workDir, 'large.txt'), Buffer.alloc(ONE_MIB + 1, 'a'))
|
|
await fs.mkdir(path.join(workDir, 'folder'))
|
|
|
|
await expect(service.readFile('session-1', 'note.ts')).resolves.toMatchObject({
|
|
state: 'ok',
|
|
language: 'typescript',
|
|
size: 25,
|
|
content: 'export const answer = 42\n',
|
|
})
|
|
await expect(service.readFile('session-1', 'binary.bin')).resolves.toMatchObject({
|
|
state: 'binary',
|
|
language: 'binary',
|
|
size: 4,
|
|
})
|
|
await expect(service.readFile('session-1', 'image.png')).resolves.toMatchObject({
|
|
state: 'ok',
|
|
previewType: 'image',
|
|
language: 'image',
|
|
mimeType: 'image/png',
|
|
dataUrl: 'data:image/png;base64,iVBORwA=',
|
|
size: 5,
|
|
})
|
|
const largeImage = await service.readFile('session-1', 'large-image.png')
|
|
expect(largeImage).toMatchObject({
|
|
state: 'ok',
|
|
previewType: 'image',
|
|
language: 'image',
|
|
mimeType: 'image/png',
|
|
size: ONE_MIB + 5,
|
|
})
|
|
expect(largeImage.dataUrl).toStartWith('data:image/png;base64,')
|
|
await expect(service.readFile('session-1', 'large.txt')).resolves.toMatchObject({
|
|
state: 'ok',
|
|
previewType: 'text',
|
|
language: 'text',
|
|
size: ONE_MIB + 1,
|
|
readBytes: ONE_MIB,
|
|
truncated: true,
|
|
content: 'a'.repeat(ONE_MIB),
|
|
})
|
|
await expect(service.readFile('session-1', 'missing.txt')).resolves.toMatchObject({
|
|
state: 'missing',
|
|
})
|
|
await expect(service.readFile('session-1', 'folder')).resolves.toMatchObject({
|
|
state: 'missing',
|
|
})
|
|
})
|
|
|
|
it('lists a single directory level with dotfiles excluded and directories first', async () => {
|
|
const workDir = await makeTempDir('workspace-service-tree-')
|
|
const service = new WorkspaceService(async () => workDir)
|
|
|
|
await fs.mkdir(path.join(workDir, 'b-dir'))
|
|
await fs.mkdir(path.join(workDir, 'a-dir'))
|
|
await fs.mkdir(path.join(workDir, 'a-dir', 'inner'))
|
|
await fs.writeFile(path.join(workDir, 'a-dir', 'note.txt'), 'nested\n')
|
|
await fs.writeFile(path.join(workDir, 'z-file.txt'), 'root file\n')
|
|
await fs.writeFile(path.join(workDir, '.hidden.txt'), 'ignore\n')
|
|
|
|
await expect(service.readTree('session-1')).resolves.toMatchObject({
|
|
state: 'ok',
|
|
path: '',
|
|
entries: [
|
|
{ name: 'a-dir', path: 'a-dir', isDirectory: true },
|
|
{ name: 'b-dir', path: 'b-dir', isDirectory: true },
|
|
{ name: 'z-file.txt', path: 'z-file.txt', isDirectory: false },
|
|
],
|
|
})
|
|
|
|
await expect(service.readTree('session-1', 'a-dir')).resolves.toMatchObject({
|
|
state: 'ok',
|
|
path: 'a-dir',
|
|
entries: [
|
|
{ name: 'inner', path: 'a-dir/inner', isDirectory: true },
|
|
{ name: 'note.txt', path: 'a-dir/note.txt', isDirectory: false },
|
|
],
|
|
})
|
|
})
|
|
|
|
it('returns diffs for modified, added, deleted, and untracked files', async () => {
|
|
const repoDir = await createGitWorkspace()
|
|
const service = new WorkspaceService(async (sessionId) => sessionId === 'session-1' ? repoDir : null)
|
|
|
|
const modified = await service.getDiff('session-1', 'tracked.txt')
|
|
expect(modified.state).toBe('ok')
|
|
expect(modified.diff).toContain('tracked.txt')
|
|
expect(modified.diff.length).toBeGreaterThan(0)
|
|
|
|
const added = await service.getDiff('session-1', 'new.txt')
|
|
expect(added.state).toBe('ok')
|
|
expect(added.diff).toContain('new.txt')
|
|
expect(added.diff.length).toBeGreaterThan(0)
|
|
|
|
const deleted = await service.getDiff('session-1', 'deleted.txt')
|
|
expect(deleted.state).toBe('ok')
|
|
expect(deleted.diff).toContain('deleted.txt')
|
|
expect(deleted.diff.length).toBeGreaterThan(0)
|
|
|
|
const untracked = await service.getDiff('session-1', 'untracked.txt')
|
|
expect(untracked.state).toBe('ok')
|
|
expect(untracked.diff).toContain('untracked.txt')
|
|
expect(untracked.diff.length).toBeGreaterThan(0)
|
|
|
|
await expect(service.getDiff('session-1', 'clean.txt')).resolves.toMatchObject({
|
|
state: 'missing',
|
|
path: 'clean.txt',
|
|
})
|
|
|
|
const nonGitDir = await makeTempDir('workspace-service-diff-non-git-')
|
|
const nonGitService = new WorkspaceService(async () => nonGitDir)
|
|
await expect(nonGitService.getDiff('session-1', 'whatever.txt')).resolves.toMatchObject({
|
|
state: 'not_git_repo',
|
|
path: 'whatever.txt',
|
|
})
|
|
})
|
|
|
|
it('returns explicit error state when git status fails instead of ok-empty', async () => {
|
|
const repoDir = await createGitWorkspace()
|
|
const service = new WorkspaceService(async () => repoDir) as WorkspaceService & {
|
|
runGit: (workDir: string, args: string[]) => Promise<{
|
|
stdout: string
|
|
stderr: string
|
|
code: number
|
|
}>
|
|
}
|
|
|
|
service.runGit = async (workDir, args) => {
|
|
if (args[0] === 'rev-parse' && args[1] === '--show-toplevel') {
|
|
return { stdout: `${workDir}\n`, stderr: '', code: 0 }
|
|
}
|
|
if (args[0] === 'rev-parse' && args[1] === '--abbrev-ref') {
|
|
return { stdout: 'main\n', stderr: '', code: 0 }
|
|
}
|
|
if (args[0] === 'status') {
|
|
return { stdout: '', stderr: 'fatal: synthetic git failure', code: 1 }
|
|
}
|
|
return { stdout: '', stderr: 'unexpected call', code: 1 }
|
|
}
|
|
|
|
await expect(service.getStatus('session-1')).resolves.toMatchObject({
|
|
state: 'error',
|
|
isGitRepo: true,
|
|
})
|
|
|
|
const result = await service.getStatus('session-1')
|
|
expect(result.state).toBe('error')
|
|
expect(result.changedFiles).toEqual([])
|
|
expect(result.error).toContain('Failed to read git status')
|
|
expect(result.error).toContain('synthetic git failure')
|
|
})
|
|
|
|
it('reads tracked diff stats in one bulk git call', async () => {
|
|
const repoDir = await makeTempDir('workspace-service-bulk-stats-')
|
|
await fs.writeFile(path.join(repoDir, 'a.txt'), 'a\n')
|
|
await fs.writeFile(path.join(repoDir, 'b.txt'), 'b\n')
|
|
const diffStatCalls: string[][] = []
|
|
const service = new WorkspaceService(async () => repoDir) as WorkspaceService & {
|
|
runGit: (workDir: string, args: string[]) => Promise<{
|
|
stdout: string
|
|
stderr: string
|
|
code: number
|
|
}>
|
|
}
|
|
|
|
service.runGit = async (_workDir, args) => {
|
|
if (args[0] === 'rev-parse' && args[1] === '--show-toplevel') {
|
|
return { stdout: `${repoDir}\n`, stderr: '', code: 0 }
|
|
}
|
|
if (args[0] === 'rev-parse' && args[1] === '--abbrev-ref') {
|
|
return { stdout: 'main\n', stderr: '', code: 0 }
|
|
}
|
|
if (args[0] === 'status') {
|
|
return { stdout: ' M a.txt\0 M b.txt\0', stderr: '', code: 0 }
|
|
}
|
|
if (args[0] === 'diff' && args.includes('--numstat')) {
|
|
diffStatCalls.push(args)
|
|
return { stdout: '1\t0\ta.txt\n2\t3\tb.txt\n', stderr: '', code: 0 }
|
|
}
|
|
return { stdout: '', stderr: `unexpected git call: ${args.join(' ')}`, code: 1 }
|
|
}
|
|
|
|
const result = await service.getStatus('session-1')
|
|
|
|
expect(result.state).toBe('ok')
|
|
expect(diffStatCalls).toHaveLength(1)
|
|
expect(result.changedFiles).toEqual([
|
|
{ path: 'a.txt', oldPath: undefined, status: 'modified', additions: 1, deletions: 0 },
|
|
{ path: 'b.txt', oldPath: undefined, status: 'modified', additions: 2, deletions: 3 },
|
|
])
|
|
})
|
|
})
|