scai 0.1.107 β 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/README.md +3 -2
- package/dist/CHANGELOG.md +10 -1
- package/dist/__tests__/example.test.js +10 -0
- package/dist/agent/agentManager.js +14 -2
- package/dist/agent/workflowManager.js +5 -0
- package/dist/github/repo.js +9 -2
- package/dist/pipeline/modules/cleanGeneratedTestsModule.js +17 -6
- package/dist/pipeline/modules/repairTestsModule.js +40 -0
- package/dist/pipeline/modules/runTestsModule.js +37 -0
- package/dist/pipeline/registry/moduleRegistry.js +17 -0
- package/package.json +1 -1
- package/dist/jest.config.js +0 -11
- package/dist/jest.setup.js +0 -14
- package/dist/utils/runTests.js +0 -11
package/README.md
CHANGED
|
@@ -559,7 +559,7 @@ You can run it in two ways:
|
|
|
559
559
|
|
|
560
560
|
---
|
|
561
561
|
|
|
562
|
-
### Backup only of `~/.scai
|
|
562
|
+
### Backup only of `~/.scai`:
|
|
563
563
|
|
|
564
564
|
Creates a backup of the .scai folder in the user root dir.
|
|
565
565
|
|
|
@@ -569,7 +569,8 @@ Creates a backup of the .scai folder in the user root dir.
|
|
|
569
569
|
|
|
570
570
|
Results in this:
|
|
571
571
|
~/.scai_backup_2025-08-12T06-58-00-227Z/
|
|
572
|
-
|
|
572
|
+
|
|
573
|
+
</br>
|
|
573
574
|
|
|
574
575
|
## π License & Fair Use
|
|
575
576
|
|
package/dist/CHANGELOG.md
CHANGED
|
@@ -162,4 +162,13 @@ Type handling with the module pipeline
|
|
|
162
162
|
## 2025-08-31
|
|
163
163
|
|
|
164
164
|
β’ Update Spinner class to correctly display text and frames
|
|
165
|
-
β’ Update CLI help to reflect new agent workflow examples.
|
|
165
|
+
β’ Update CLI help to reflect new agent workflow examples.
|
|
166
|
+
|
|
167
|
+
## 2025-09-01
|
|
168
|
+
|
|
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
|
-
|
|
22
|
-
|
|
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
|
package/dist/github/repo.js
CHANGED
|
@@ -15,12 +15,19 @@ function runGitCommand(cmd, cwd) {
|
|
|
15
15
|
*/
|
|
16
16
|
function getRepoOwnerAndNameFromGit(indexDir) {
|
|
17
17
|
try {
|
|
18
|
-
const originUrl = runGitCommand('git config --get remote.origin.url', indexDir);
|
|
18
|
+
const originUrl = runGitCommand('git config --get remote.origin.url', indexDir).trim();
|
|
19
19
|
console.log(`π Git origin URL from '${indexDir}': ${originUrl}`);
|
|
20
|
-
|
|
20
|
+
// Handle both SSH and HTTPS formats, with or without extra slashes
|
|
21
|
+
// Examples:
|
|
22
|
+
// - git@github.com:owner/repo.git
|
|
23
|
+
// - git@github.com:/owner/repo.git
|
|
24
|
+
// - https://github.com/owner/repo.git
|
|
25
|
+
const match = originUrl.match(/github[^:/]*[:/]\/?(.+?)(?:\.git)?$/);
|
|
21
26
|
if (!match)
|
|
22
27
|
throw new Error("β Could not parse GitHub repo from origin URL.");
|
|
23
28
|
const [owner, repo] = match[1].split('/');
|
|
29
|
+
if (!owner || !repo)
|
|
30
|
+
throw new Error("β Failed to extract owner or repo name from URL.");
|
|
24
31
|
console.log(`β
Parsed from Git: owner='${owner}', repo='${repo}'`);
|
|
25
32
|
return { owner, repo };
|
|
26
33
|
}
|
|
@@ -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
|
-
|
|
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,
|
|
18
|
-
filepath,
|
|
19
|
-
mode: "overwrite",
|
|
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
package/dist/jest.config.js
DELETED
|
@@ -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;
|
package/dist/jest.setup.js
DELETED
|
@@ -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
|
-
);
|
package/dist/utils/runTests.js
DELETED
|
@@ -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
|
-
}
|