mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-04 23:02:12 +00:00
fix: running tests and in ci (#536)
* fix: running tests and in ci * cr * cr * cr * exit 0 when no tests * cr
This commit is contained in:
parent
576d2553e3
commit
0923c8b47e
10 changed files with 86 additions and 34 deletions
40
.github/workflows/unit-tests.yml
vendored
Normal file
40
.github/workflows/unit-tests.yml
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# This workflow will run unit tests for the current project
|
||||||
|
|
||||||
|
name: Unit Tests
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch: # Allows triggering the workflow manually in GitHub UI
|
||||||
|
|
||||||
|
# If another push to the same PR or branch happens while this workflow is still running,
|
||||||
|
# cancel the earlier run in favor of the next run.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
unit-tests:
|
||||||
|
name: Unit Tests
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
node-version: [18.x, 20.x]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Enable Corepack
|
||||||
|
run: corepack enable
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: "yarn"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install --immutable --mode=skip-build
|
||||||
|
- name: Build project
|
||||||
|
run: yarn build
|
||||||
|
- name: Run tests
|
||||||
|
run: yarn test
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check .",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 0",
|
||||||
"dev": "tsx src/index.tsx"
|
"dev": "tsx src/index.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ export default {
|
||||||
preset: "ts-jest/presets/default-esm",
|
preset: "ts-jest/presets/default-esm",
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
"^(\\.{1,2}/.*)\\.js$": "$1",
|
"^(\\.{1,2}/.*)\\.js$": "$1",
|
||||||
|
"^@open-swe/shared$": "<rootDir>/../../packages/shared/src/index.ts",
|
||||||
|
"^@open-swe/shared/(.*)$": "<rootDir>/../../packages/shared/src/$1",
|
||||||
},
|
},
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.tsx?$": [
|
"^.+\\.tsx?$": [
|
||||||
|
|
@ -15,4 +17,5 @@ export default {
|
||||||
setupFiles: ["dotenv/config"],
|
setupFiles: ["dotenv/config"],
|
||||||
passWithNoTests: true,
|
passWithNoTests: true,
|
||||||
testTimeout: 20_000,
|
testTimeout: 20_000,
|
||||||
|
testMatch: ["<rootDir>/src/**/*.test.ts"],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@
|
||||||
"lint:fix": "eslint . --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check .",
|
||||||
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=\\.test\\.ts$ --testPathIgnorePatterns=\\.int\\.test\\.ts$",
|
"test": "NODE_OPTIONS=--experimental-vm-modules yarn run jest --config jest.config.js --testPathIgnorePatterns=int.test.ts",
|
||||||
"test:int": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=\\.int\\.test\\.ts$",
|
"test:int": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.config.js --testPathPattern=int.test.ts",
|
||||||
"test:single": "NODE_OPTIONS=--experimental-vm-modules yarn run jest --config jest.config.js --testTimeout 100000",
|
"test:single": "NODE_OPTIONS=--experimental-vm-modules yarn run jest --config jest.config.js --testTimeout 100000",
|
||||||
"eval:single": "NODE_OPTIONS=--experimental-vm-modules yarn run vitest --config ls.vitest.config.ts --run",
|
"eval:single": "NODE_OPTIONS=--experimental-vm-modules yarn run vitest --config ls.vitest.config.ts --run",
|
||||||
"postinstall": "turbo build"
|
"postinstall": "turbo build"
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,9 @@ describe("withRetry", () => {
|
||||||
.fn<() => Promise<string>>()
|
.fn<() => Promise<string>>()
|
||||||
.mockRejectedValue(new Error("always fails"));
|
.mockRejectedValue(new Error("always fails"));
|
||||||
|
|
||||||
await expect(withRetry(mockFn)).rejects.toThrow("always fails");
|
const result = await withRetry(mockFn);
|
||||||
|
expect(result).toBeInstanceOf(Error);
|
||||||
|
expect((result as Error).message).toBe("always fails");
|
||||||
expect(mockFn).toHaveBeenCalledTimes(4); // 1 initial + 3 retries
|
expect(mockFn).toHaveBeenCalledTimes(4); // 1 initial + 3 retries
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -40,9 +42,9 @@ describe("withRetry", () => {
|
||||||
.fn<() => Promise<string>>()
|
.fn<() => Promise<string>>()
|
||||||
.mockRejectedValue(new Error("always fails"));
|
.mockRejectedValue(new Error("always fails"));
|
||||||
|
|
||||||
await expect(withRetry(mockFn, { retries: 2 })).rejects.toThrow(
|
const result = await withRetry(mockFn, { retries: 2 });
|
||||||
"always fails",
|
expect(result).toBeInstanceOf(Error);
|
||||||
);
|
expect((result as Error).message).toBe("always fails");
|
||||||
expect(mockFn).toHaveBeenCalledTimes(3); // 1 initial + 2 retries
|
expect(mockFn).toHaveBeenCalledTimes(3); // 1 initial + 2 retries
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -52,11 +54,11 @@ describe("withRetry", () => {
|
||||||
.mockRejectedValue(new Error("always fails"));
|
.mockRejectedValue(new Error("always fails"));
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
await expect(withRetry(mockFn, { retries: 2, delay: 100 })).rejects.toThrow(
|
const result = await withRetry(mockFn, { retries: 2, delay: 100 });
|
||||||
"always fails",
|
|
||||||
);
|
|
||||||
const endTime = Date.now();
|
const endTime = Date.now();
|
||||||
|
|
||||||
|
expect(result).toBeInstanceOf(Error);
|
||||||
|
expect((result as Error).message).toBe("always fails");
|
||||||
expect(mockFn).toHaveBeenCalledTimes(3);
|
expect(mockFn).toHaveBeenCalledTimes(3);
|
||||||
expect(endTime - startTime).toBeGreaterThanOrEqual(200); // 2 delays of 100ms each
|
expect(endTime - startTime).toBeGreaterThanOrEqual(200); // 2 delays of 100ms each
|
||||||
});
|
});
|
||||||
|
|
@ -67,11 +69,11 @@ describe("withRetry", () => {
|
||||||
.mockRejectedValue(new Error("always fails"));
|
.mockRejectedValue(new Error("always fails"));
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
await expect(withRetry(mockFn, { retries: 2 })).rejects.toThrow(
|
const result = await withRetry(mockFn, { retries: 2 });
|
||||||
"always fails",
|
|
||||||
);
|
|
||||||
const endTime = Date.now();
|
const endTime = Date.now();
|
||||||
|
|
||||||
|
expect(result).toBeInstanceOf(Error);
|
||||||
|
expect((result as Error).message).toBe("always fails");
|
||||||
expect(mockFn).toHaveBeenCalledTimes(3);
|
expect(mockFn).toHaveBeenCalledTimes(3);
|
||||||
expect(endTime - startTime).toBeLessThan(50); // Should be very fast with no delay
|
expect(endTime - startTime).toBeLessThan(50); // Should be very fast with no delay
|
||||||
});
|
});
|
||||||
|
|
@ -81,13 +83,13 @@ describe("withRetry", () => {
|
||||||
.fn<() => Promise<string>>()
|
.fn<() => Promise<string>>()
|
||||||
.mockRejectedValue("string error");
|
.mockRejectedValue("string error");
|
||||||
|
|
||||||
await expect(withRetry(mockFn, { retries: 1 })).rejects.toThrow(
|
const result = await withRetry(mockFn, { retries: 1 });
|
||||||
"string error",
|
expect(result).toBeInstanceOf(Error);
|
||||||
);
|
expect((result as Error).message).toBe("string error");
|
||||||
expect(mockFn).toHaveBeenCalledTimes(2);
|
expect(mockFn).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw the last error after all retries", async () => {
|
it("should return the last error after all retries", async () => {
|
||||||
const error1 = new Error("first error");
|
const error1 = new Error("first error");
|
||||||
const error2 = new Error("second error");
|
const error2 = new Error("second error");
|
||||||
const lastError = new Error("last error");
|
const lastError = new Error("last error");
|
||||||
|
|
@ -98,9 +100,9 @@ describe("withRetry", () => {
|
||||||
.mockRejectedValueOnce(error2)
|
.mockRejectedValueOnce(error2)
|
||||||
.mockRejectedValue(lastError);
|
.mockRejectedValue(lastError);
|
||||||
|
|
||||||
await expect(withRetry(mockFn, { retries: 2 })).rejects.toThrow(
|
const result = await withRetry(mockFn, { retries: 2 });
|
||||||
"last error",
|
expect(result).toBeInstanceOf(Error);
|
||||||
);
|
expect((result as Error).message).toBe("last error");
|
||||||
expect(mockFn).toHaveBeenCalledTimes(3);
|
expect(mockFn).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -140,7 +142,9 @@ describe("createRetryWrapper", () => {
|
||||||
|
|
||||||
const wrappedFn = createRetryWrapper(originalFn, { retries: 1 });
|
const wrappedFn = createRetryWrapper(originalFn, { retries: 1 });
|
||||||
|
|
||||||
await expect(wrappedFn()).rejects.toThrow("always fails");
|
const result = await wrappedFn();
|
||||||
|
expect(result).toBeInstanceOf(Error);
|
||||||
|
expect((result as Error).message).toBe("always fails");
|
||||||
expect(originalFn).toHaveBeenCalledTimes(2); // 1 initial + 1 retry
|
expect(originalFn).toHaveBeenCalledTimes(2); // 1 initial + 1 retry
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
MAX_INTERNAL_TOKENS,
|
MAX_INTERNAL_TOKENS,
|
||||||
} from "../utils/tokens.js";
|
} from "../utils/tokens.js";
|
||||||
|
|
||||||
describe("calculateConversationHistoryTokenCount", async () => {
|
describe("calculateConversationHistoryTokenCount", () => {
|
||||||
it("should return 0 for empty messages array", async () => {
|
it("should return 0 for empty messages array", async () => {
|
||||||
const result = calculateConversationHistoryTokenCount([]);
|
const result = calculateConversationHistoryTokenCount([]);
|
||||||
expect(result).toBe(0);
|
expect(result).toBe(0);
|
||||||
|
|
@ -124,8 +124,8 @@ describe("calculateConversationHistoryTokenCount", async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(resultWithoutOption).toBeGreaterThan(resultWithOption);
|
expect(resultWithoutOption).toBeGreaterThan(resultWithOption);
|
||||||
// First two messages should be ~7 tokens
|
// First two messages should be ~8 tokens
|
||||||
expect(resultWithOption).toBe(7);
|
expect(resultWithOption).toBe(8);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not separate AI messages with tool calls from their tool messages when excluding from end", async () => {
|
it("should not separate AI messages with tool calls from their tool messages when excluding from end", async () => {
|
||||||
|
|
@ -214,7 +214,7 @@ describe("calculateConversationHistoryTokenCount", async () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getMessagesSinceLastSummary", async () => {
|
describe("getMessagesSinceLastSummary", () => {
|
||||||
it("should return all messages when there is no summary message", async () => {
|
it("should return all messages when there is no summary message", async () => {
|
||||||
const messages = [
|
const messages = [
|
||||||
new HumanMessage({ content: "Message 1" }),
|
new HumanMessage({ content: "Message 1" }),
|
||||||
|
|
@ -632,7 +632,7 @@ describe("getMessagesSinceLastSummary", async () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("MAX_INTERNAL_TOKENS constant", async () => {
|
describe("MAX_INTERNAL_TOKENS constant", () => {
|
||||||
it("should be defined as 60,000", async () => {
|
it("should be defined as 60,000", async () => {
|
||||||
expect(MAX_INTERNAL_TOKENS).toBe(60_000);
|
expect(MAX_INTERNAL_TOKENS).toBe(60_000);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export function calculateConversationHistoryTokenCount(
|
||||||
excludeCountFromEnd?: number;
|
excludeCountFromEnd?: number;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
let totalChars = 0;
|
let totalTokens = 0;
|
||||||
let messagesToCount = messages;
|
let messagesToCount = messages;
|
||||||
|
|
||||||
if (options?.excludeCountFromEnd && options.excludeCountFromEnd > 0) {
|
if (options?.excludeCountFromEnd && options.excludeCountFromEnd > 0) {
|
||||||
|
|
@ -33,25 +33,25 @@ export function calculateConversationHistoryTokenCount(
|
||||||
if (isHumanMessage(m) || isToolMessage(m)) {
|
if (isHumanMessage(m) || isToolMessage(m)) {
|
||||||
const contentString = getMessageContentString(m.content);
|
const contentString = getMessageContentString(m.content);
|
||||||
// Divide each char by 4 as it's roughly one token per 4 characters.
|
// Divide each char by 4 as it's roughly one token per 4 characters.
|
||||||
totalChars += contentString.length / 4;
|
totalTokens += Math.ceil(contentString.length / 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAIMessage(m)) {
|
if (isAIMessage(m)) {
|
||||||
const usageMetadata = m.usage_metadata;
|
const usageMetadata = m.usage_metadata;
|
||||||
if (usageMetadata) {
|
if (usageMetadata) {
|
||||||
totalChars += usageMetadata.output_tokens;
|
totalTokens += usageMetadata.total_tokens;
|
||||||
} else {
|
} else {
|
||||||
const contentString = getMessageContentString(m.content);
|
const contentString = getMessageContentString(m.content);
|
||||||
totalChars += contentString.length / 4;
|
totalTokens += Math.ceil(contentString.length / 4);
|
||||||
m.tool_calls?.forEach((tc) => {
|
m.tool_calls?.forEach((tc) => {
|
||||||
const nameAndArgs = tc.name + JSON.stringify(tc.args);
|
const nameAndArgs = tc.name + JSON.stringify(tc.args);
|
||||||
totalChars += nameAndArgs.length / 4;
|
totalTokens += Math.ceil(nameAndArgs.length / 4);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return totalChars;
|
return totalTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@
|
||||||
"strictFunctionTypes": false,
|
"strictFunctionTypes": false,
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"types": ["jest", "node"],
|
"types": ["jest", "node"],
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts", "**/*.js", "jest.setup.cjs"],
|
"include": ["**/*.ts", "**/*.js", "jest.setup.cjs"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@
|
||||||
"format": "turbo format",
|
"format": "turbo format",
|
||||||
"format:check": "turbo format:check",
|
"format:check": "turbo format:check",
|
||||||
"lint": "turbo lint",
|
"lint": "turbo lint",
|
||||||
"lint:fix": "turbo lint:fix"
|
"lint:fix": "turbo lint:fix",
|
||||||
|
"test": "turbo test"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"turbo": "^2.5.0",
|
"turbo": "^2.5.0",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@
|
||||||
},
|
},
|
||||||
"dev": {
|
"dev": {
|
||||||
"dependsOn": ["^dev"]
|
"dependsOn": ["^dev"]
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"dependsOn": ["^test"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue