scai 0.1.108 → 0.1.109

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/CHANGELOG.md CHANGED
@@ -166,4 +166,9 @@ Type handling with the module pipeline
166
166
 
167
167
  ## 2025-09-01
168
168
 
169
- * Improve handling of GitHub repository URLs and paths by extracting the owner and name separately
169
+ * Improve handling of GitHub repository URLs and paths by extracting the owner and name separately
170
+
171
+ ## 2025-09-02
172
+
173
+ • Added test configuration for project and generated tests
174
+ • Add runTestsModule and repairTestsModule for testing pipeline
@@ -0,0 +1,10 @@
1
+ import { SCAI_HOME } from "../constants"; // example constant
2
+ // cli/src/__tests__/example.test.ts
3
+ describe("CLI src basic test", () => {
4
+ it("should pass a simple truthy test", () => {
5
+ expect(true).toBe(true);
6
+ });
7
+ it("should import a constant from cli/src/constants.ts", () => {
8
+ expect(typeof SCAI_HOME).not.toBe("undefined");
9
+ });
10
+ });
@@ -18,8 +18,20 @@ export class Agent {
18
18
  // Resolve modules (with before/after dependencies)
19
19
  const modules = this.resolveModules(this.goals);
20
20
  console.log(chalk.green("📋 Modules to run:"), modules.map((m) => m.name).join(" → "));
21
- // Read file content (optional, could be used by modules in workflow)
22
- await fs.readFile(filepath, "utf-8");
21
+ try {
22
+ // Check that the file exists before trying to read it
23
+ await fs.access(filepath);
24
+ // Read file content (optional, could be used by modules in workflow)
25
+ await fs.readFile(filepath, "utf-8");
26
+ }
27
+ catch (err) {
28
+ if (err.code === "ENOENT") {
29
+ console.error(chalk.redBright("❌ Error:"), `File not found: ${chalk.yellow(filepath)}`);
30
+ console.error(`Make sure the path is correct. (cwd: ${chalk.gray(process.cwd())})`);
31
+ process.exit(1);
32
+ }
33
+ throw err; // rethrow for unexpected errors
34
+ }
23
35
  // Delegate everything to handleAgentRun (like CLI commands do)
24
36
  await handleAgentRun(filepath, modules);
25
37
  console.log(chalk.green("✅ Agent finished!"));
@@ -76,6 +76,11 @@ export async function handleAgentRun(filepath, modules) {
76
76
  baseChunks.length = 0;
77
77
  baseChunks.push(...reset);
78
78
  break;
79
+ case 'skip':
80
+ console.log(chalk.gray(`⏭️ Skipped writing for module ${mod.name}`));
81
+ // don’t touch files, but keep chunks flowing
82
+ workingChunks = processed;
83
+ break;
79
84
  default:
80
85
  console.log(chalk.yellow(`⚠️ Unknown mode; skipping write`));
81
86
  // still move pipeline forward with processed
@@ -7,16 +7,27 @@ export const cleanGeneratedTestsModule = {
7
7
  // normalize + strip markdown
8
8
  const normalized = normalizeText(content);
9
9
  const stripped = stripMarkdownFences(normalized);
10
- // filter non-code lines
11
- const lines = stripped.split("\n");
12
10
  // filter non-code lines, but keep blank ones
11
+ const lines = stripped.split("\n");
13
12
  const codeLines = lines.filter(line => line.trim() === "" || isCodeLike(line));
14
- const cleanedCode = codeLines.join("\n");
13
+ // remove duplicate imports (normalize spacing/semicolon)
14
+ const seenImports = new Set();
15
+ const dedupedLines = codeLines.filter(line => {
16
+ if (line.trim().startsWith("import")) {
17
+ const key = line.trim().replace(/;$/, "");
18
+ if (seenImports.has(key)) {
19
+ return false;
20
+ }
21
+ seenImports.add(key);
22
+ }
23
+ return true;
24
+ });
25
+ const cleanedCode = dedupedLines.join("\n").trimEnd();
15
26
  return {
16
27
  originalContent: content,
17
- content: cleanedCode, // cleaned code for pipeline
18
- filepath, // original file path
19
- mode: "overwrite", // indicates overwrite existing file
28
+ content: cleanedCode,
29
+ filepath,
30
+ mode: "overwrite",
20
31
  };
21
32
  }
22
33
  };
@@ -0,0 +1,40 @@
1
+ import { generate } from "../../lib/generate.js";
2
+ import { Config } from "../../config.js";
3
+ export const repairTestsModule = {
4
+ name: "repairTestsModule",
5
+ description: "Fix failing Jest tests using AI",
6
+ async run({ content, filepath, summary }) {
7
+ const model = Config.getModel();
8
+ const prompt = `
9
+ You are a senior engineer tasked with repairing Jest tests.
10
+
11
+ The following test file failed:
12
+
13
+ --- BEGIN TEST FILE ---
14
+ ${content}
15
+ --- END TEST FILE ---
16
+
17
+ Failure summary:
18
+ ${summary || "No summary provided."}
19
+
20
+ Instructions:
21
+ - Keep the overall structure, imports, and test cases.
22
+ - Only fix syntax errors, invalid Jest matchers, or broken references.
23
+ - Do NOT remove or replace entire test suites unless strictly necessary.
24
+ - Do NOT generate trivial placeholder tests (like add(2,3) examples).
25
+ - Only return valid Jest code, no explanations, no markdown fences.
26
+
27
+ Output the repaired test file:
28
+ `.trim();
29
+ const response = await generate({ content: prompt }, model);
30
+ if (!response)
31
+ throw new Error("⚠️ No repaired test code returned from model");
32
+ return {
33
+ originalContent: content,
34
+ content: response.content, // repaired test code
35
+ filepath, // repair in-place
36
+ summary: `Repaired tests based on failure: ${summary || "unknown error"}`,
37
+ mode: "overwrite" // signal to overwrite existing test file
38
+ };
39
+ }
40
+ };
@@ -0,0 +1,37 @@
1
+ import { execSync } from "child_process";
2
+ import path from "path";
3
+ export const runTestsModule = {
4
+ name: "runTestsModule",
5
+ description: "Runs generated Jest tests with safety checks for a single file",
6
+ async run(input) {
7
+ const { filepath } = input;
8
+ if (!filepath) {
9
+ throw new Error("No filepath provided to runTestsModule.");
10
+ }
11
+ const absoluteFilePath = path.resolve(filepath);
12
+ try {
13
+ // Step 1: TypeScript syntax check for this file only using tsconfig.test.json
14
+ // Step 1: TypeScript syntax check for this file only
15
+ execSync(`npx tsc --noEmit --project tsconfig.test.json`, { stdio: "inherit" });
16
+ // Step 2: Dry-run Jest for this file only
17
+ execSync(`npx jest --config ${path.resolve("jest.config.ts")} --dryRun ${absoluteFilePath}`, { stdio: "inherit" });
18
+ // Step 3: Full Jest run for this file only
19
+ execSync(`npx jest --config ${path.resolve("jest.config.ts")} ${absoluteFilePath}`, { stdio: "inherit" });
20
+ return {
21
+ content: "✅ Tests ran successfully",
22
+ filepath,
23
+ mode: "skip",
24
+ summary: "All tests passed successfully."
25
+ };
26
+ }
27
+ catch (error) {
28
+ const errorMessage = error?.message || String(error);
29
+ return {
30
+ content: `❌ Test run failed:\n${errorMessage}`,
31
+ filepath,
32
+ mode: "skip",
33
+ summary: errorMessage // provides failure context for repair
34
+ };
35
+ }
36
+ }
37
+ };
@@ -6,6 +6,8 @@ import { commitSuggesterModule } from '../modules/commitSuggesterModule.js';
6
6
  import { changelogModule } from '../modules/changeLogModule.js';
7
7
  import { cleanGeneratedTestsModule } from '../modules/cleanGeneratedTestsModule.js';
8
8
  import { preserveCodeModule } from '../modules/preserveCodeModule.js';
9
+ import { runTestsModule } from '../modules/runTestsModule.js';
10
+ import { repairTestsModule } from '../modules/repairTestsModule.js';
9
11
  // Built-in modules with metadata
10
12
  export const builtInModules = {
11
13
  comments: {
@@ -38,6 +40,21 @@ export const builtInModules = {
38
40
  ...cleanGeneratedTestsModule,
39
41
  group: 'testing',
40
42
  },
43
+ runTests: {
44
+ ...runTestsModule,
45
+ group: 'testing',
46
+ dependencies: {
47
+ before: ['tests'], // must exist after tests are generated
48
+ after: ['cleanTests'], // run after cleaning
49
+ },
50
+ },
51
+ repairTests: {
52
+ ...repairTestsModule,
53
+ group: 'testing',
54
+ dependencies: {
55
+ after: ['runTests'], // repair runs after tests have been executed
56
+ },
57
+ },
41
58
  suggest: {
42
59
  ...commitSuggesterModule,
43
60
  group: 'git',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.108",
3
+ "version": "0.1.109",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"
@@ -1,11 +0,0 @@
1
- const config = {
2
- preset: 'ts-jest',
3
- testEnvironment: 'node',
4
- setupFilesAfterEnv: ['./jest.setup.ts'],
5
- transform: {
6
- '^.+\\.ts$': 'ts-jest',
7
- },
8
- moduleFileExtensions: ['ts', 'js', 'json', 'node'],
9
- testPathIgnorePatterns: ['/node_modules/', '/dist/'],
10
- };
11
- export default config;
@@ -1,14 +0,0 @@
1
- import { jest } from '@jest/globals';
2
- // jest.setup.ts
3
- // Mock the global fetch function in Jest for Node.js 18+
4
- global.fetch = jest.fn(() => Promise.resolve({
5
- json: () => Promise.resolve({ response: 'Mocked Commit Message' }),
6
- ok: true,
7
- status: 200,
8
- statusText: 'OK',
9
- headers: new Headers(),
10
- redirected: false,
11
- type: 'default',
12
- url: 'https://mocked-url.com',
13
- }) // Type assertion for `Response` object
14
- );
@@ -1,11 +0,0 @@
1
- import { execSync } from 'child_process';
2
- export function runJestTest(testFile) {
3
- try {
4
- execSync(`npx jest ${testFile} --silent`, { stdio: 'inherit' });
5
- return true;
6
- }
7
- catch (err) {
8
- console.error(`❌ Tests failed for ${testFile}`);
9
- return false;
10
- }
11
- }