Skip to content

Commit

Permalink
Use deepseek for fixing too
Browse files Browse the repository at this point in the history
  • Loading branch information
mohsen1 committed Jan 26, 2025
1 parent 6304217 commit 5428f3c
Show file tree
Hide file tree
Showing 17 changed files with 376 additions and 98 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test-and-build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
lfs: true

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"

- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 9

- name: Install dependencies
run: pnpm install

- name: Build
run: pnpm build

- name: Run tests
run: pnpm test:unit
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
dist/
node_modules/

api-response.txt
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,13 @@ askai npm test
askds --fix npm test
```

You will need to have a Fireworks AI API key set under `FIREWORKS_API_KEY`.
`--fix` uses Ollama to fix the code. You will need to have Ollama installed and running.

You also need to pull the `fastapply` model and add it to Ollama.

```bash
./scripts/ollama.sh
```

> [!NOTE]
> `fast-apply` is a 7B model, and requires a lot of memory.
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"dev": "tsx src/index.ts",
"debug": "tsx --inspect src/index.ts",
"test": "vitest --watch=false",
"test:unit": "vitest run src/__tests__/fix-parser.test.ts",
"test:unit": "vitest run src/",
"prepublish": "npm run build"
},
"keywords": [],
Expand All @@ -35,13 +35,14 @@
},
"dependencies": {
"chalk": "^5.4.1",
"prompts": "^2.4.2",
"commander": "^13.1.0",
"diff": "^5.1.0",
"fast-glob": "^3.3.3",
"ink": "^4.4.1",
"openai": "^4.79.4",
"prompts": "^2.4.2",
"react": "^18.3.1",
"react-reconciler": "^0.29.2"
"react-reconciler": "^0.29.2",
"word-wrap": "^1.2.5"
}
}
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 70 additions & 0 deletions src/__tests__/commands.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { executeCommand, findTestFiles } from "../commands.js";
import { Config } from "../types.js";
import { ui } from "../ui.js";

const mockSync = vi.hoisted(() => vi.fn());

vi.mock("fast-glob", () => ({
default: { sync: mockSync },
}));

describe("Command Utilities", () => {
beforeEach(() => {
vi.restoreAllMocks();
ui.destroy();
mockSync.mockReset();
});

describe("executeCommand", () => {
it("should resolve with command output", async () => {
const mockSpawn = vi.fn(() => ({
stdout: { on: vi.fn() },
stderr: { on: vi.fn() },
on: vi.fn((event, cb) => cb(0)),
}));
vi.stubGlobal("child_process", { spawn: mockSpawn });
const result = await executeCommand("echo", ["test"]);
expect(result).toContain("test");
});

it("should reject on non-zero exit code", async () => {
const mockSpawn = vi.fn(() => ({
stdout: { on: vi.fn() },
stderr: { on: vi.fn() },
on: vi.fn((event, cb) => cb(1)),
}));
vi.stubGlobal("child_process", { spawn: mockSpawn });
await expect(executeCommand("false", [])).rejects.toThrow();
});

it("should call onData callback with output", async () => {
const onData = vi.fn();
const mockSpawn = vi.fn(() => ({
stdout: { on: vi.fn((event, cb) => cb(Buffer.from("test"))) },
stderr: { on: vi.fn() },
on: vi.fn((event, cb) => cb(0)),
}));
vi.stubGlobal("child_process", { spawn: mockSpawn });
await executeCommand("echo", ["test"], { onData } as any);
expect(onData).toHaveBeenCalledWith(expect.stringContaining("test"));
});
});

describe("findTestFiles", () => {
const mockConfig: Config = { testFilePattern: ["**/*.test.ts"] } as Config;

it("should find test files in output", () => {
const testOutput = ["FAIL src/test.test.ts", "PASS src/other.ts"].join(
"\n"
);
mockSync.mockReturnValue(["src/test.test.ts"]);
const result = findTestFiles(testOutput, mockConfig);
expect(result).toEqual(["src/test.test.ts"]);
expect(mockSync).toHaveBeenCalledWith(
mockConfig.testFilePattern,
expect.any(Object)
);
});
});
});
16 changes: 16 additions & 0 deletions src/__tests__/fix.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { extractFixedCode, identifyFixableFiles } from "../fix.js";
import { FILE_PATH_TAG, FIX_END_TAG, FIX_START_TAG } from "../constants.js";

describe("Fix Utilities", () => {
describe("extractFixedCode", () => {
it("should return null when tags are missing", () => {
expect(extractFixedCode("no tags here")).toBeNull();
});

it("should extract content between tags", () => {
const content = `prefix\n<updated-code>\nconst a = 1;\n</updated-code>\nsuffix`;
expect(extractFixedCode(content)).toBe("const a = 1;");
});
});
});
66 changes: 66 additions & 0 deletions src/__tests__/log-store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { logStore } from '../log-store.js';

describe('LogStore', () => {
beforeEach(() => {
logStore.clear();
});

it('should notify subscribers when logs are updated', async () => {
const listener = vi.fn();
const unsubscribe = logStore.subscribe(listener);
logStore.appendOutput('test');
await new Promise((r) => setTimeout(r, 20));
expect(listener).toHaveBeenCalledTimes(1);
unsubscribe();
});

it('should debounce multiple notifications', async () => {
const listener = vi.fn();
logStore.subscribe(listener);
logStore.appendOutput('test1');
logStore.appendOutput('test2');
logStore.appendReasoning('reason1');
await new Promise((r) => setTimeout(r, 5));
expect(listener).toHaveBeenCalledTimes(0);
await new Promise((r) => setTimeout(r, 20));
expect(listener).toHaveBeenCalledTimes(1);
});

it('should maintain separate buffers for output and reasoning', () => {
logStore.appendOutput('output');
logStore.appendReasoning('reasoning');
expect(logStore.getOutput()).toBe('output');
expect(logStore.getReasoning()).toBe('reasoning');
expect(logStore.getOutputBuffer()).toBe('output');
expect(logStore.getReasoningBuffer()).toBe('reasoning');
});

it('should clear logs properly', () => {
logStore.appendOutput('test');
logStore.appendReasoning('test');
logStore.clear();
expect(logStore.getOutput()).toBe('');
expect(logStore.getReasoning()).toBe('');
});

it('should handle multiple subscribers', async () => {
const listener1 = vi.fn();
const listener2 = vi.fn();
logStore.subscribe(listener1);
logStore.subscribe(listener2);
logStore.appendOutput('test');
await new Promise((r) => setTimeout(r, 20));
expect(listener1).toHaveBeenCalledTimes(1);
expect(listener2).toHaveBeenCalledTimes(1);
});

it('should unsubscribe properly', async () => {
const listener = vi.fn();
const unsubscribe = logStore.subscribe(listener);
unsubscribe();
logStore.appendOutput('test');
await new Promise((r) => setTimeout(r, 20));
expect(listener).not.toHaveBeenCalled();
});
});
63 changes: 63 additions & 0 deletions src/__tests__/ui.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { ui } from "../ui.js";
import { render } from "ink";
import { logStore } from "../log-store.js";

// Mock process.stdin.isTTY to true for tests
Object.defineProperty(process.stdin, "isTTY", { value: true });

vi.mock("ink", () => ({
render: vi.fn(() => ({
unmount: vi.fn(),
waitUntilExit: vi.fn(),
rerender: vi.fn(),
cleanup: vi.fn(),
clear: vi.fn(),
})),
}));

describe("UI Manager", () => {
let mockUnmount: ReturnType<typeof vi.fn>;

beforeEach(() => {
vi.resetAllMocks();
mockUnmount = vi.fn();
vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
const mockRender = render as unknown as ReturnType<typeof vi.fn>;
mockRender.mockReturnValue({ unmount: mockUnmount });
ui.destroy();
logStore.clear();
});

it("should initialize UI only once", () => {
ui.initialize();
ui.initialize(); // Second call should be ignored
expect(render).toHaveBeenCalledTimes(1);
});

it("should handle output logging correctly", () => {
const consoleSpy = vi.spyOn(process.stdout, "write");

// Test without UI initialized
ui.appendOutputLog("test output");
expect(consoleSpy).toHaveBeenCalledWith("test output");

// Initialize UI and test again
ui.initialize();
ui.appendOutputLog("ui output");
expect(logStore.getOutput()).toBe("ui output");
});

it("should clean up resources on destroy", () => {
ui.initialize();
ui.destroy();
expect(mockUnmount).toHaveBeenCalled();
});

it("should handle SIGINT cleanup", () => {
ui.initialize();
process.emit("SIGINT");
expect(mockUnmount).toHaveBeenCalled();
expect(process.exit).toHaveBeenCalledWith(0);
});
});
24 changes: 6 additions & 18 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,16 @@ import OpenAI from "openai";

import { Config, Message } from "./types.js";
import { ui } from "./ui.js";

import fs from "fs";
export const apis = {
DEEPSEEK: {
name: "DeepSeek",
provider: "DeepSeek",
endpoint: "https://api.deepseek.com",
model: "deepseek-reasoner",
apiKey: process.env.DEEPSEEK_API_KEY,
maxTokens: 4096,
temperature: 0.01,
},
FIREWORKS: {
name: "Fireworks",
endpoint: "https://api.fireworks.ai/inference/v1",
model: "accounts/me-63642b/deployedModels/fast-apply-de5bd7ca",
apiKey: process.env.FIREWORKS_AI_API_KEY,
maxTokens: 4096,
temperature: 0.6,
},
} as const;

export async function streamAIResponse({
Expand All @@ -34,7 +26,7 @@ export async function streamAIResponse({
appendReasoningMessages?: boolean;
}): Promise<string> {
if (!api.apiKey) {
throw new Error(`API key for ${api.name} is not set`);
throw new Error(`API key for ${api.provider} is not set`);
}

const client = new OpenAI({
Expand All @@ -47,7 +39,7 @@ export async function streamAIResponse({

try {
if (config.debug) {
ui.appendOutputLog(`Sending messages to ${api.name}...`);
ui.appendOutputLog(`Sending messages to ${api.provider}...`);
}

if (appendReasoningMessages) {
Expand Down Expand Up @@ -75,11 +67,7 @@ export async function streamAIResponse({
(chunk as any).choices[0]?.delta?.reasoning_content || "";

if (config.debug && chunkCount % 25 === 0) {
ui.appendOutputLog(
`[${api.name}] Received ${chunkCount} chunks. ` +
`Content: ${contentChunk.length}b, ` +
`Reasoning: ${reasoningChunk.length}b\n`
);
fs.appendFileSync("api-response.txt", JSON.stringify(chunk) + "\n\n");
}

fullContent += contentChunk;
Expand Down Expand Up @@ -114,7 +102,7 @@ export async function streamAIResponse({

if (config.debug) {
ui.appendOutputLog(
`[${api.name}] API Error: ${
`[${api.provider}] API Error: ${
error instanceof Error ? error.message : String(error)
}`
);
Expand Down
Loading

0 comments on commit 5428f3c

Please sign in to comment.