feat: integrate custom features for smart context management
Some checks failed
CI / test (map[image:macos-latest name:macos], 1, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 2, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 3, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 4, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 1, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 2, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 3, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 4, 4) (push) Has been cancelled
CI / merge-reports (push) Has been cancelled
Some checks failed
CI / test (map[image:macos-latest name:macos], 1, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 2, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 3, 4) (push) Has been cancelled
CI / test (map[image:macos-latest name:macos], 4, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 1, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 2, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 3, 4) (push) Has been cancelled
CI / test (map[image:windows-latest name:windows], 4, 4) (push) Has been cancelled
CI / merge-reports (push) Has been cancelled
- Added a new integration script to manage custom features related to smart context. - Implemented handlers for smart context operations (get, update, clear, stats) in ipc. - Created a SmartContextStore class to manage context snippets and summaries. - Developed hooks for React to interact with smart context (useSmartContext, useUpdateSmartContext, useClearSmartContext, useSmartContextStats). - Included backup and restore functionality in the integration script. - Validated integration by checking for custom modifications and file existence.
This commit is contained in:
77
backups/backup-20251218-094212/src/__tests__/README.md
Normal file
77
backups/backup-20251218-094212/src/__tests__/README.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Test Documentation
|
||||
|
||||
This directory contains unit tests for the Dyad application.
|
||||
|
||||
## Testing Setup
|
||||
|
||||
We use [Vitest](https://vitest.dev/) as our testing framework, which is designed to work well with Vite and modern JavaScript.
|
||||
|
||||
### Test Commands
|
||||
|
||||
Add these commands to your `package.json`:
|
||||
|
||||
```json
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:ui": "vitest --ui"
|
||||
```
|
||||
|
||||
- `npm run test` - Run tests once
|
||||
- `npm run test:watch` - Run tests in watch mode (rerun when files change)
|
||||
- `npm run test:ui` - Run tests with UI reporter
|
||||
|
||||
## Mocking Guidelines
|
||||
|
||||
### Mocking fs module
|
||||
|
||||
When mocking the `node:fs` module, use a default export in the mock:
|
||||
|
||||
```typescript
|
||||
vi.mock("node:fs", async () => {
|
||||
return {
|
||||
default: {
|
||||
mkdirSync: vi.fn(),
|
||||
writeFileSync: vi.fn(),
|
||||
// Add other fs methods as needed
|
||||
},
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### Mocking isomorphic-git
|
||||
|
||||
When mocking isomorphic-git, provide a default export:
|
||||
|
||||
```typescript
|
||||
vi.mock("isomorphic-git", () => ({
|
||||
default: {
|
||||
add: vi.fn().mockResolvedValue(undefined),
|
||||
commit: vi.fn().mockResolvedValue(undefined),
|
||||
// Add other git methods as needed
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
### Testing IPC Handlers
|
||||
|
||||
When testing IPC handlers, mock the Electron IPC system:
|
||||
|
||||
```typescript
|
||||
vi.mock("electron", () => ({
|
||||
ipcMain: {
|
||||
handle: vi.fn(),
|
||||
on: vi.fn(),
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
1. Create a new file with the `.test.ts` or `.spec.ts` extension
|
||||
2. Import the functions you want to test
|
||||
3. Mock any dependencies using `vi.mock()`
|
||||
4. Write your test cases using `describe()` and `it()`
|
||||
|
||||
## Example
|
||||
|
||||
See `chat_stream_handlers.test.ts` for an example of testing IPC handlers with proper mocking.
|
||||
@@ -0,0 +1,127 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`problem_prompt > createConciseProblemFixPrompt > should format a concise prompt for multiple errors 1`] = `
|
||||
"Fix these 2 TypeScript compile-time errors:
|
||||
|
||||
1. src/main.ts:5:12 - Cannot find module 'react-dom/client' or its corresponding type declarations. (TS2307)
|
||||
\`\`\`
|
||||
SNIPPET
|
||||
\`\`\`
|
||||
|
||||
2. src/components/Modal.tsx:35:20 - Property 'isOpen' does not exist on type 'IntrinsicAttributes & ModalProps'. (TS2339)
|
||||
\`\`\`
|
||||
SNIPPET
|
||||
\`\`\`
|
||||
|
||||
|
||||
Please fix all errors in a concise way."
|
||||
`;
|
||||
|
||||
exports[`problem_prompt > createConciseProblemFixPrompt > should format a concise prompt for single error 1`] = `
|
||||
"Fix these 1 TypeScript compile-time error:
|
||||
|
||||
1. src/App.tsx:10:5 - Cannot find name 'consol'. Did you mean 'console'? (TS2552)
|
||||
\`\`\`
|
||||
SNIPPET
|
||||
\`\`\`
|
||||
|
||||
|
||||
Please fix all errors in a concise way."
|
||||
`;
|
||||
|
||||
exports[`problem_prompt > createConciseProblemFixPrompt > should return a short message when no problems exist 1`] = `"No TypeScript problems detected."`;
|
||||
|
||||
exports[`problem_prompt > createProblemFixPrompt > should format a single error correctly 1`] = `
|
||||
"Fix these 1 TypeScript compile-time error:
|
||||
|
||||
1. src/components/Button.tsx:15:23 - Property 'onClick' does not exist on type 'ButtonProps'. (TS2339)
|
||||
\`\`\`
|
||||
SNIPPET
|
||||
\`\`\`
|
||||
|
||||
|
||||
Please fix all errors in a concise way."
|
||||
`;
|
||||
|
||||
exports[`problem_prompt > createProblemFixPrompt > should format multiple errors across multiple files 1`] = `
|
||||
"Fix these 4 TypeScript compile-time errors:
|
||||
|
||||
1. src/components/Button.tsx:15:23 - Property 'onClick' does not exist on type 'ButtonProps'. (TS2339)
|
||||
\`\`\`
|
||||
SNIPPET
|
||||
\`\`\`
|
||||
|
||||
2. src/components/Button.tsx:8:12 - Type 'string | undefined' is not assignable to type 'string'. (TS2322)
|
||||
\`\`\`
|
||||
SNIPPET
|
||||
\`\`\`
|
||||
|
||||
3. src/hooks/useApi.ts:42:5 - Argument of type 'unknown' is not assignable to parameter of type 'string'. (TS2345)
|
||||
\`\`\`
|
||||
SNIPPET
|
||||
\`\`\`
|
||||
|
||||
4. src/utils/helpers.ts:45:8 - Function lacks ending return statement and return type does not include 'undefined'. (TS2366)
|
||||
\`\`\`
|
||||
SNIPPET
|
||||
\`\`\`
|
||||
|
||||
|
||||
Please fix all errors in a concise way."
|
||||
`;
|
||||
|
||||
exports[`problem_prompt > createProblemFixPrompt > should handle realistic React TypeScript errors 1`] = `
|
||||
"Fix these 4 TypeScript compile-time errors:
|
||||
|
||||
1. src/components/UserProfile.tsx:12:35 - Type '{ children: string; }' is missing the following properties from type 'UserProfileProps': user, onEdit (TS2739)
|
||||
\`\`\`
|
||||
SNIPPET
|
||||
\`\`\`
|
||||
|
||||
2. src/components/UserProfile.tsx:25:15 - Object is possibly 'null'. (TS2531)
|
||||
\`\`\`
|
||||
SNIPPET
|
||||
\`\`\`
|
||||
|
||||
3. src/hooks/useLocalStorage.ts:18:12 - Type 'string | null' is not assignable to type 'T'. (TS2322)
|
||||
\`\`\`
|
||||
SNIPPET
|
||||
\`\`\`
|
||||
|
||||
4. src/types/api.ts:45:3 - Duplicate identifier 'UserRole'. (TS2300)
|
||||
\`\`\`
|
||||
SNIPPET
|
||||
\`\`\`
|
||||
|
||||
|
||||
Please fix all errors in a concise way."
|
||||
`;
|
||||
|
||||
exports[`problem_prompt > createProblemFixPrompt > should return a message when no problems exist 1`] = `"No TypeScript problems detected."`;
|
||||
|
||||
exports[`problem_prompt > realistic TypeScript error scenarios > should handle common React + TypeScript errors 1`] = `
|
||||
"Fix these 4 TypeScript compile-time errors:
|
||||
|
||||
1. src/components/ProductCard.tsx:22:18 - Property 'price' is missing in type '{ name: string; description: string; }' but required in type 'Product'. (TS2741)
|
||||
\`\`\`
|
||||
SNIPPET
|
||||
\`\`\`
|
||||
|
||||
2. src/components/SearchInput.tsx:15:45 - Type '(value: string) => void' is not assignable to type 'ChangeEventHandler<HTMLInputElement>'. (TS2322)
|
||||
\`\`\`
|
||||
SNIPPET
|
||||
\`\`\`
|
||||
|
||||
3. src/api/userService.ts:8:1 - Function lacks ending return statement and return type does not include 'undefined'. (TS2366)
|
||||
\`\`\`
|
||||
SNIPPET
|
||||
\`\`\`
|
||||
|
||||
4. src/utils/dataProcessor.ts:34:25 - Object is possibly 'undefined'. (TS2532)
|
||||
\`\`\`
|
||||
SNIPPET
|
||||
\`\`\`
|
||||
|
||||
|
||||
Please fix all errors in a concise way."
|
||||
`;
|
||||
@@ -0,0 +1,534 @@
|
||||
import { parseEnvFile, serializeEnvFile } from "@/ipc/utils/app_env_var_utils";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("parseEnvFile", () => {
|
||||
it("should parse basic key=value pairs", () => {
|
||||
const content = `API_KEY=abc123
|
||||
DATABASE_URL=postgres://localhost:5432/mydb
|
||||
PORT=3000`;
|
||||
|
||||
const result = parseEnvFile(content);
|
||||
expect(result).toEqual([
|
||||
{ key: "API_KEY", value: "abc123" },
|
||||
{ key: "DATABASE_URL", value: "postgres://localhost:5432/mydb" },
|
||||
{ key: "PORT", value: "3000" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle quoted values and remove quotes", () => {
|
||||
const content = `API_KEY="abc123"
|
||||
DATABASE_URL='postgres://localhost:5432/mydb'
|
||||
MESSAGE="Hello World"`;
|
||||
|
||||
const result = parseEnvFile(content);
|
||||
expect(result).toEqual([
|
||||
{ key: "API_KEY", value: "abc123" },
|
||||
{ key: "DATABASE_URL", value: "postgres://localhost:5432/mydb" },
|
||||
{ key: "MESSAGE", value: "Hello World" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should skip empty lines", () => {
|
||||
const content = `API_KEY=abc123
|
||||
|
||||
DATABASE_URL=postgres://localhost:5432/mydb
|
||||
|
||||
|
||||
PORT=3000`;
|
||||
|
||||
const result = parseEnvFile(content);
|
||||
expect(result).toEqual([
|
||||
{ key: "API_KEY", value: "abc123" },
|
||||
{ key: "DATABASE_URL", value: "postgres://localhost:5432/mydb" },
|
||||
{ key: "PORT", value: "3000" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should skip comment lines", () => {
|
||||
const content = `# This is a comment
|
||||
API_KEY=abc123
|
||||
# Another comment
|
||||
DATABASE_URL=postgres://localhost:5432/mydb
|
||||
# PORT=3000 (commented out)
|
||||
DEBUG=true`;
|
||||
|
||||
const result = parseEnvFile(content);
|
||||
expect(result).toEqual([
|
||||
{ key: "API_KEY", value: "abc123" },
|
||||
{ key: "DATABASE_URL", value: "postgres://localhost:5432/mydb" },
|
||||
{ key: "DEBUG", value: "true" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle values with spaces", () => {
|
||||
const content = `MESSAGE="Hello World"
|
||||
DESCRIPTION='This is a long description'
|
||||
TITLE=My App Title`;
|
||||
|
||||
const result = parseEnvFile(content);
|
||||
expect(result).toEqual([
|
||||
{ key: "MESSAGE", value: "Hello World" },
|
||||
{ key: "DESCRIPTION", value: "This is a long description" },
|
||||
{ key: "TITLE", value: "My App Title" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle values with special characters", () => {
|
||||
const content = `PASSWORD="p@ssw0rd!#$%"
|
||||
URL="https://example.com/api?key=123&secret=456"
|
||||
REGEX="^[a-zA-Z0-9]+$"`;
|
||||
|
||||
const result = parseEnvFile(content);
|
||||
expect(result).toEqual([
|
||||
{ key: "PASSWORD", value: "p@ssw0rd!#$%" },
|
||||
{ key: "URL", value: "https://example.com/api?key=123&secret=456" },
|
||||
{ key: "REGEX", value: "^[a-zA-Z0-9]+$" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle empty values", () => {
|
||||
const content = `EMPTY_VAR=
|
||||
QUOTED_EMPTY=""
|
||||
ANOTHER_VAR=value`;
|
||||
|
||||
const result = parseEnvFile(content);
|
||||
expect(result).toEqual([
|
||||
{ key: "EMPTY_VAR", value: "" },
|
||||
{ key: "QUOTED_EMPTY", value: "" },
|
||||
{ key: "ANOTHER_VAR", value: "value" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle values with equals signs", () => {
|
||||
const content = `EQUATION="2+2=4"
|
||||
CONNECTION_STRING="server=localhost;user=admin;password=secret"`;
|
||||
|
||||
const result = parseEnvFile(content);
|
||||
expect(result).toEqual([
|
||||
{ key: "EQUATION", value: "2+2=4" },
|
||||
{
|
||||
key: "CONNECTION_STRING",
|
||||
value: "server=localhost;user=admin;password=secret",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should trim whitespace around keys and values", () => {
|
||||
const content = ` API_KEY = abc123
|
||||
DATABASE_URL = "postgres://localhost:5432/mydb"
|
||||
PORT = 3000 `;
|
||||
|
||||
const result = parseEnvFile(content);
|
||||
expect(result).toEqual([
|
||||
{ key: "API_KEY", value: "abc123" },
|
||||
{ key: "DATABASE_URL", value: "postgres://localhost:5432/mydb" },
|
||||
{ key: "PORT", value: "3000" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should skip malformed lines without equals sign", () => {
|
||||
const content = `API_KEY=abc123
|
||||
MALFORMED_LINE
|
||||
DATABASE_URL=postgres://localhost:5432/mydb
|
||||
ANOTHER_MALFORMED
|
||||
PORT=3000`;
|
||||
|
||||
const result = parseEnvFile(content);
|
||||
expect(result).toEqual([
|
||||
{ key: "API_KEY", value: "abc123" },
|
||||
{ key: "DATABASE_URL", value: "postgres://localhost:5432/mydb" },
|
||||
{ key: "PORT", value: "3000" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should skip lines with equals sign at the beginning", () => {
|
||||
const content = `API_KEY=abc123
|
||||
=invalid_line
|
||||
DATABASE_URL=postgres://localhost:5432/mydb`;
|
||||
|
||||
const result = parseEnvFile(content);
|
||||
expect(result).toEqual([
|
||||
{ key: "API_KEY", value: "abc123" },
|
||||
{ key: "DATABASE_URL", value: "postgres://localhost:5432/mydb" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle mixed quote types in values", () => {
|
||||
const content = `MESSAGE="He said 'Hello World'"
|
||||
COMMAND='echo "Hello World"'`;
|
||||
|
||||
const result = parseEnvFile(content);
|
||||
expect(result).toEqual([
|
||||
{ key: "MESSAGE", value: "He said 'Hello World'" },
|
||||
{ key: "COMMAND", value: 'echo "Hello World"' },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle empty content", () => {
|
||||
const result = parseEnvFile("");
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle content with only comments and empty lines", () => {
|
||||
const content = `# Comment 1
|
||||
|
||||
# Comment 2
|
||||
|
||||
# Comment 3`;
|
||||
|
||||
const result = parseEnvFile(content);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle values that start with hash symbol when quoted", () => {
|
||||
const content = `HASH_VALUE="#hashtag"
|
||||
COMMENT_LIKE="# This looks like a comment but it's a value"
|
||||
ACTUAL_COMMENT=value
|
||||
# This is an actual comment`;
|
||||
|
||||
const result = parseEnvFile(content);
|
||||
expect(result).toEqual([
|
||||
{ key: "HASH_VALUE", value: "#hashtag" },
|
||||
{
|
||||
key: "COMMENT_LIKE",
|
||||
value: "# This looks like a comment but it's a value",
|
||||
},
|
||||
{ key: "ACTUAL_COMMENT", value: "value" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should skip comments that look like key=value pairs", () => {
|
||||
const content = `API_KEY=abc123
|
||||
# SECRET_KEY=should_be_ignored
|
||||
DATABASE_URL=postgres://localhost:5432/mydb
|
||||
# PORT=3000
|
||||
DEBUG=true`;
|
||||
|
||||
const result = parseEnvFile(content);
|
||||
expect(result).toEqual([
|
||||
{ key: "API_KEY", value: "abc123" },
|
||||
{ key: "DATABASE_URL", value: "postgres://localhost:5432/mydb" },
|
||||
{ key: "DEBUG", value: "true" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle values containing comment symbols", () => {
|
||||
const content = `GIT_COMMIT_MSG="feat: add new feature # closes #123"
|
||||
SQL_QUERY="SELECT * FROM users WHERE id = 1 # Get user by ID"
|
||||
MARKDOWN_HEADING="# Main Title"
|
||||
SHELL_COMMENT="echo 'hello' # prints hello"`;
|
||||
|
||||
const result = parseEnvFile(content);
|
||||
expect(result).toEqual([
|
||||
{ key: "GIT_COMMIT_MSG", value: "feat: add new feature # closes #123" },
|
||||
{
|
||||
key: "SQL_QUERY",
|
||||
value: "SELECT * FROM users WHERE id = 1 # Get user by ID",
|
||||
},
|
||||
{ key: "MARKDOWN_HEADING", value: "# Main Title" },
|
||||
{ key: "SHELL_COMMENT", value: "echo 'hello' # prints hello" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle inline comments after key=value pairs", () => {
|
||||
const content = `API_KEY=abc123 # This is the API key
|
||||
DATABASE_URL=postgres://localhost:5432/mydb # Database connection
|
||||
PORT=3000 # Server port
|
||||
DEBUG=true # Enable debug mode`;
|
||||
|
||||
const result = parseEnvFile(content);
|
||||
expect(result).toEqual([
|
||||
{ key: "API_KEY", value: "abc123 # This is the API key" },
|
||||
{
|
||||
key: "DATABASE_URL",
|
||||
value: "postgres://localhost:5432/mydb # Database connection",
|
||||
},
|
||||
{ key: "PORT", value: "3000 # Server port" },
|
||||
{ key: "DEBUG", value: "true # Enable debug mode" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle quoted values with inline comments", () => {
|
||||
const content = `MESSAGE="Hello World" # Greeting message
|
||||
PASSWORD="secret#123" # Password with hash
|
||||
URL="https://example.com#section" # URL with fragment`;
|
||||
|
||||
const result = parseEnvFile(content);
|
||||
expect(result).toEqual([
|
||||
{ key: "MESSAGE", value: "Hello World" },
|
||||
{ key: "PASSWORD", value: "secret#123" },
|
||||
{ key: "URL", value: "https://example.com#section" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should handle complex mixed comment scenarios", () => {
|
||||
const content = `# Configuration file
|
||||
API_KEY=abc123
|
||||
# Database settings
|
||||
DATABASE_URL="postgres://localhost:5432/mydb"
|
||||
# PORT=5432 (commented out)
|
||||
DATABASE_NAME=myapp
|
||||
|
||||
# Feature flags
|
||||
FEATURE_A=true # Enable feature A
|
||||
FEATURE_B="false" # Disable feature B
|
||||
# FEATURE_C=true (disabled)
|
||||
|
||||
# URLs with fragments
|
||||
HOMEPAGE="https://example.com#home"
|
||||
DOCS_URL=https://docs.example.com#getting-started # Documentation link`;
|
||||
|
||||
const result = parseEnvFile(content);
|
||||
expect(result).toEqual([
|
||||
{ key: "API_KEY", value: "abc123" },
|
||||
{ key: "DATABASE_URL", value: "postgres://localhost:5432/mydb" },
|
||||
{ key: "DATABASE_NAME", value: "myapp" },
|
||||
{ key: "FEATURE_A", value: "true # Enable feature A" },
|
||||
{ key: "FEATURE_B", value: "false" },
|
||||
{ key: "HOMEPAGE", value: "https://example.com#home" },
|
||||
{
|
||||
key: "DOCS_URL",
|
||||
value: "https://docs.example.com#getting-started # Documentation link",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("serializeEnvFile", () => {
|
||||
it("should serialize basic key=value pairs", () => {
|
||||
const envVars = [
|
||||
{ key: "API_KEY", value: "abc123" },
|
||||
{ key: "DATABASE_URL", value: "postgres://localhost:5432/mydb" },
|
||||
{ key: "PORT", value: "3000" },
|
||||
];
|
||||
|
||||
const result = serializeEnvFile(envVars);
|
||||
expect(result).toBe(`API_KEY=abc123
|
||||
DATABASE_URL=postgres://localhost:5432/mydb
|
||||
PORT=3000`);
|
||||
});
|
||||
|
||||
it("should quote values with spaces", () => {
|
||||
const envVars = [
|
||||
{ key: "MESSAGE", value: "Hello World" },
|
||||
{ key: "DESCRIPTION", value: "This is a long description" },
|
||||
{ key: "SIMPLE", value: "no_spaces" },
|
||||
];
|
||||
|
||||
const result = serializeEnvFile(envVars);
|
||||
expect(result).toBe(`MESSAGE="Hello World"
|
||||
DESCRIPTION="This is a long description"
|
||||
SIMPLE=no_spaces`);
|
||||
});
|
||||
|
||||
it("should quote values with special characters", () => {
|
||||
const envVars = [
|
||||
{ key: "PASSWORD", value: "p@ssw0rd!#$%" },
|
||||
{ key: "URL", value: "https://example.com/api?key=123&secret=456" },
|
||||
{ key: "SIMPLE", value: "simple123" },
|
||||
];
|
||||
|
||||
const result = serializeEnvFile(envVars);
|
||||
expect(result).toBe(`PASSWORD="p@ssw0rd!#$%"
|
||||
URL="https://example.com/api?key=123&secret=456"
|
||||
SIMPLE=simple123`);
|
||||
});
|
||||
|
||||
it("should escape quotes in values", () => {
|
||||
const envVars = [
|
||||
{ key: "MESSAGE", value: 'He said "Hello World"' },
|
||||
{ key: "COMMAND", value: 'echo "test"' },
|
||||
];
|
||||
|
||||
const result = serializeEnvFile(envVars);
|
||||
expect(result).toBe(`MESSAGE="He said \\"Hello World\\""
|
||||
COMMAND="echo \\"test\\""`);
|
||||
});
|
||||
|
||||
it("should handle empty values", () => {
|
||||
const envVars = [
|
||||
{ key: "EMPTY_VAR", value: "" },
|
||||
{ key: "ANOTHER_VAR", value: "value" },
|
||||
{ key: "ALSO_EMPTY", value: "" },
|
||||
];
|
||||
|
||||
const result = serializeEnvFile(envVars);
|
||||
expect(result).toBe(`EMPTY_VAR=
|
||||
ANOTHER_VAR=value
|
||||
ALSO_EMPTY=`);
|
||||
});
|
||||
|
||||
it("should quote values with hash symbols", () => {
|
||||
const envVars = [
|
||||
{ key: "PASSWORD", value: "secret#123" },
|
||||
{ key: "COMMENT", value: "This has # in it" },
|
||||
];
|
||||
|
||||
const result = serializeEnvFile(envVars);
|
||||
expect(result).toBe(`PASSWORD="secret#123"
|
||||
COMMENT="This has # in it"`);
|
||||
});
|
||||
|
||||
it("should quote values with single quotes", () => {
|
||||
const envVars = [
|
||||
{ key: "MESSAGE", value: "Don't worry" },
|
||||
{ key: "SQL", value: "SELECT * FROM 'users'" },
|
||||
];
|
||||
|
||||
const result = serializeEnvFile(envVars);
|
||||
expect(result).toBe(`MESSAGE="Don't worry"
|
||||
SQL="SELECT * FROM 'users'"`);
|
||||
});
|
||||
|
||||
it("should handle values with equals signs", () => {
|
||||
const envVars = [
|
||||
{ key: "EQUATION", value: "2+2=4" },
|
||||
{
|
||||
key: "CONNECTION_STRING",
|
||||
value: "server=localhost;user=admin;password=secret",
|
||||
},
|
||||
];
|
||||
|
||||
const result = serializeEnvFile(envVars);
|
||||
expect(result).toBe(`EQUATION="2+2=4"
|
||||
CONNECTION_STRING="server=localhost;user=admin;password=secret"`);
|
||||
});
|
||||
|
||||
it("should handle mixed scenarios", () => {
|
||||
const envVars = [
|
||||
{ key: "SIMPLE", value: "value" },
|
||||
{ key: "WITH_SPACES", value: "hello world" },
|
||||
{ key: "WITH_QUOTES", value: 'say "hello"' },
|
||||
{ key: "EMPTY", value: "" },
|
||||
{ key: "SPECIAL_CHARS", value: "p@ssw0rd!#$%" },
|
||||
];
|
||||
|
||||
const result = serializeEnvFile(envVars);
|
||||
expect(result).toBe(`SIMPLE=value
|
||||
WITH_SPACES="hello world"
|
||||
WITH_QUOTES="say \\"hello\\""
|
||||
EMPTY=
|
||||
SPECIAL_CHARS="p@ssw0rd!#$%"`);
|
||||
});
|
||||
|
||||
it("should handle empty array", () => {
|
||||
const result = serializeEnvFile([]);
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it("should handle complex escaped quotes", () => {
|
||||
const envVars = [
|
||||
{ key: "COMPLEX", value: "This is \"complex\" with 'mixed' quotes" },
|
||||
];
|
||||
|
||||
const result = serializeEnvFile(envVars);
|
||||
expect(result).toBe(`COMPLEX="This is \\"complex\\" with 'mixed' quotes"`);
|
||||
});
|
||||
|
||||
it("should handle values that start with hash symbol", () => {
|
||||
const envVars = [
|
||||
{ key: "HASHTAG", value: "#trending" },
|
||||
{ key: "COMMENT_LIKE", value: "# This looks like a comment" },
|
||||
{ key: "MARKDOWN_HEADING", value: "# Main Title" },
|
||||
{ key: "NORMAL_VALUE", value: "no_hash_here" },
|
||||
];
|
||||
|
||||
const result = serializeEnvFile(envVars);
|
||||
expect(result).toBe(`HASHTAG="#trending"
|
||||
COMMENT_LIKE="# This looks like a comment"
|
||||
MARKDOWN_HEADING="# Main Title"
|
||||
NORMAL_VALUE=no_hash_here`);
|
||||
});
|
||||
|
||||
it("should handle values containing comment symbols", () => {
|
||||
const envVars = [
|
||||
{ key: "GIT_COMMIT", value: "feat: add feature # closes #123" },
|
||||
{ key: "SQL_QUERY", value: "SELECT * FROM users # Get all users" },
|
||||
{ key: "SHELL_CMD", value: "echo 'hello' # prints hello" },
|
||||
];
|
||||
|
||||
const result = serializeEnvFile(envVars);
|
||||
expect(result).toBe(`GIT_COMMIT="feat: add feature # closes #123"
|
||||
SQL_QUERY="SELECT * FROM users # Get all users"
|
||||
SHELL_CMD="echo 'hello' # prints hello"`);
|
||||
});
|
||||
|
||||
it("should handle URLs with fragments that contain hash symbols", () => {
|
||||
const envVars = [
|
||||
{ key: "HOMEPAGE", value: "https://example.com#home" },
|
||||
{ key: "DOCS_URL", value: "https://docs.example.com#getting-started" },
|
||||
{ key: "API_ENDPOINT", value: "https://api.example.com/v1#section" },
|
||||
];
|
||||
|
||||
const result = serializeEnvFile(envVars);
|
||||
expect(result).toBe(`HOMEPAGE="https://example.com#home"
|
||||
DOCS_URL="https://docs.example.com#getting-started"
|
||||
API_ENDPOINT="https://api.example.com/v1#section"`);
|
||||
});
|
||||
|
||||
it("should handle values with hash symbols and other special characters", () => {
|
||||
const envVars = [
|
||||
{ key: "COMPLEX_PASSWORD", value: "p@ssw0rd#123!&" },
|
||||
{ key: "REGEX_PATTERN", value: "^[a-zA-Z0-9#]+$" },
|
||||
{
|
||||
key: "MARKDOWN_CONTENT",
|
||||
value: "# Title\n\nSome content with = and & symbols",
|
||||
},
|
||||
];
|
||||
|
||||
const result = serializeEnvFile(envVars);
|
||||
expect(result).toBe(`COMPLEX_PASSWORD="p@ssw0rd#123!&"
|
||||
REGEX_PATTERN="^[a-zA-Z0-9#]+$"
|
||||
MARKDOWN_CONTENT="# Title\n\nSome content with = and & symbols"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseEnvFile and serializeEnvFile integration", () => {
|
||||
it("should be able to parse what it serializes", () => {
|
||||
const originalEnvVars = [
|
||||
{ key: "API_KEY", value: "abc123" },
|
||||
{ key: "MESSAGE", value: "Hello World" },
|
||||
{ key: "PASSWORD", value: 'secret"123' },
|
||||
{ key: "EMPTY", value: "" },
|
||||
{ key: "SPECIAL", value: "p@ssw0rd!#$%" },
|
||||
];
|
||||
|
||||
const serialized = serializeEnvFile(originalEnvVars);
|
||||
const parsed = parseEnvFile(serialized);
|
||||
|
||||
expect(parsed).toEqual(originalEnvVars);
|
||||
});
|
||||
|
||||
it("should handle round-trip with complex values", () => {
|
||||
const originalEnvVars = [
|
||||
{ key: "URL", value: "https://example.com/api?key=123&secret=456" },
|
||||
{ key: "REGEX", value: "^[a-zA-Z0-9]+$" },
|
||||
{ key: "COMMAND", value: 'echo "Hello World"' },
|
||||
{ key: "EQUATION", value: "2+2=4" },
|
||||
];
|
||||
|
||||
const serialized = serializeEnvFile(originalEnvVars);
|
||||
const parsed = parseEnvFile(serialized);
|
||||
|
||||
expect(parsed).toEqual(originalEnvVars);
|
||||
});
|
||||
|
||||
it("should handle round-trip with comment-like values", () => {
|
||||
const originalEnvVars = [
|
||||
{ key: "HASHTAG", value: "#trending" },
|
||||
{
|
||||
key: "COMMENT_LIKE",
|
||||
value: "# This looks like a comment but it's a value",
|
||||
},
|
||||
{ key: "GIT_COMMIT", value: "feat: add feature # closes #123" },
|
||||
{ key: "URL_WITH_FRAGMENT", value: "https://example.com#section" },
|
||||
{ key: "MARKDOWN_HEADING", value: "# Main Title" },
|
||||
{ key: "COMPLEX_VALUE", value: "password#123=secret&token=abc" },
|
||||
];
|
||||
|
||||
const serialized = serializeEnvFile(originalEnvVars);
|
||||
const parsed = parseEnvFile(serialized);
|
||||
|
||||
expect(parsed).toEqual(originalEnvVars);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
||||
import { cleanFullResponse } from "@/ipc/utils/cleanFullResponse";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("cleanFullResponse", () => {
|
||||
it("should replace < characters in dyad-write attributes", () => {
|
||||
const input = `<dyad-write path="src/file.tsx" description="Testing <a> tags.">content</dyad-write>`;
|
||||
const expected = `<dyad-write path="src/file.tsx" description="Testing <a> tags.">content</dyad-write>`;
|
||||
|
||||
const result = cleanFullResponse(input);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it("should replace < characters in multiple attributes", () => {
|
||||
const input = `<dyad-write path="src/<component>.tsx" description="Testing <div> tags.">content</dyad-write>`;
|
||||
const expected = `<dyad-write path="src/<component>.tsx" description="Testing <div> tags.">content</dyad-write>`;
|
||||
|
||||
const result = cleanFullResponse(input);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it("should handle multiple nested HTML tags in a single attribute", () => {
|
||||
const input = `<dyad-write path="src/file.tsx" description="Testing <div> and <span> and <a> tags.">content</dyad-write>`;
|
||||
const expected = `<dyad-write path="src/file.tsx" description="Testing <div> and <span> and <a> tags.">content</dyad-write>`;
|
||||
|
||||
const result = cleanFullResponse(input);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it("should handle complex example with mixed content", () => {
|
||||
const input = `
|
||||
BEFORE TAG
|
||||
<dyad-write path="src/pages/locations/neighborhoods/louisville/Highlands.tsx" description="Updating Highlands neighborhood page to use <a> tags.">
|
||||
import React from 'react';
|
||||
</dyad-write>
|
||||
AFTER TAG
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
BEFORE TAG
|
||||
<dyad-write path="src/pages/locations/neighborhoods/louisville/Highlands.tsx" description="Updating Highlands neighborhood page to use <a> tags.">
|
||||
import React from 'react';
|
||||
</dyad-write>
|
||||
AFTER TAG
|
||||
`;
|
||||
|
||||
const result = cleanFullResponse(input);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it("should handle other dyad tag types", () => {
|
||||
const input = `<dyad-rename from="src/<old>.tsx" to="src/<new>.tsx"></dyad-rename>`;
|
||||
const expected = `<dyad-rename from="src/<old>.tsx" to="src/<new>.tsx"></dyad-rename>`;
|
||||
|
||||
const result = cleanFullResponse(input);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it("should handle dyad-delete tags", () => {
|
||||
const input = `<dyad-delete path="src/<component>.tsx"></dyad-delete>`;
|
||||
const expected = `<dyad-delete path="src/<component>.tsx"></dyad-delete>`;
|
||||
|
||||
const result = cleanFullResponse(input);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it("should not affect content outside dyad tags", () => {
|
||||
const input = `Some text with <regular> HTML tags. <dyad-write path="test.tsx" description="With <nested> tags.">content</dyad-write> More <html> here.`;
|
||||
const expected = `Some text with <regular> HTML tags. <dyad-write path="test.tsx" description="With <nested> tags.">content</dyad-write> More <html> here.`;
|
||||
|
||||
const result = cleanFullResponse(input);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it("should handle empty attributes", () => {
|
||||
const input = `<dyad-write path="src/file.tsx">content</dyad-write>`;
|
||||
const expected = `<dyad-write path="src/file.tsx">content</dyad-write>`;
|
||||
|
||||
const result = cleanFullResponse(input);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it("should handle attributes without < characters", () => {
|
||||
const input = `<dyad-write path="src/file.tsx" description="Normal description">content</dyad-write>`;
|
||||
const expected = `<dyad-write path="src/file.tsx" description="Normal description">content</dyad-write>`;
|
||||
|
||||
const result = cleanFullResponse(input);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,167 @@
|
||||
import { formatMessagesForSummary } from "../ipc/handlers/chat_stream_handlers";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("formatMessagesForSummary", () => {
|
||||
it("should return all messages when there are 8 or fewer messages", () => {
|
||||
const messages = [
|
||||
{ role: "user", content: "Hello" },
|
||||
{ role: "assistant", content: "Hi there!" },
|
||||
{ role: "user", content: "How are you?" },
|
||||
{ role: "assistant", content: "I'm doing well, thanks!" },
|
||||
];
|
||||
|
||||
const result = formatMessagesForSummary(messages);
|
||||
const expected = [
|
||||
'<message role="user">Hello</message>',
|
||||
'<message role="assistant">Hi there!</message>',
|
||||
'<message role="user">How are you?</message>',
|
||||
'<message role="assistant">I\'m doing well, thanks!</message>',
|
||||
].join("\n");
|
||||
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it("should return all messages when there are exactly 8 messages", () => {
|
||||
const messages = Array.from({ length: 8 }, (_, i) => ({
|
||||
role: i % 2 === 0 ? "user" : "assistant",
|
||||
content: `Message ${i + 1}`,
|
||||
}));
|
||||
|
||||
const result = formatMessagesForSummary(messages);
|
||||
const expected = messages
|
||||
.map((m) => `<message role="${m.role}">${m.content}</message>`)
|
||||
.join("\n");
|
||||
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it("should truncate messages when there are more than 8 messages", () => {
|
||||
const messages = Array.from({ length: 12 }, (_, i) => ({
|
||||
role: i % 2 === 0 ? "user" : "assistant",
|
||||
content: `Message ${i + 1}`,
|
||||
}));
|
||||
|
||||
const result = formatMessagesForSummary(messages);
|
||||
|
||||
// Should contain first 2 messages
|
||||
expect(result).toContain('<message role="user">Message 1</message>');
|
||||
expect(result).toContain('<message role="assistant">Message 2</message>');
|
||||
|
||||
// Should contain omission indicator
|
||||
expect(result).toContain(
|
||||
'<message role="system">[... 4 messages omitted ...]</message>',
|
||||
);
|
||||
|
||||
// Should contain last 6 messages
|
||||
expect(result).toContain('<message role="user">Message 7</message>');
|
||||
expect(result).toContain('<message role="assistant">Message 8</message>');
|
||||
expect(result).toContain('<message role="user">Message 9</message>');
|
||||
expect(result).toContain('<message role="assistant">Message 10</message>');
|
||||
expect(result).toContain('<message role="user">Message 11</message>');
|
||||
expect(result).toContain('<message role="assistant">Message 12</message>');
|
||||
|
||||
// Should not contain middle messages
|
||||
expect(result).not.toContain('<message role="user">Message 3</message>');
|
||||
expect(result).not.toContain(
|
||||
'<message role="assistant">Message 4</message>',
|
||||
);
|
||||
expect(result).not.toContain('<message role="user">Message 5</message>');
|
||||
expect(result).not.toContain(
|
||||
'<message role="assistant">Message 6</message>',
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle messages with undefined content", () => {
|
||||
const messages = [
|
||||
{ role: "user", content: "Hello" },
|
||||
{ role: "assistant", content: undefined },
|
||||
{ role: "user", content: "Are you there?" },
|
||||
];
|
||||
|
||||
const result = formatMessagesForSummary(messages);
|
||||
const expected = [
|
||||
'<message role="user">Hello</message>',
|
||||
'<message role="assistant">undefined</message>',
|
||||
'<message role="user">Are you there?</message>',
|
||||
].join("\n");
|
||||
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it("should handle empty messages array", () => {
|
||||
const messages: { role: string; content: string | undefined }[] = [];
|
||||
const result = formatMessagesForSummary(messages);
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it("should handle single message", () => {
|
||||
const messages = [{ role: "user", content: "Hello world" }];
|
||||
const result = formatMessagesForSummary(messages);
|
||||
expect(result).toBe('<message role="user">Hello world</message>');
|
||||
});
|
||||
|
||||
it("should correctly calculate omitted messages count", () => {
|
||||
const messages = Array.from({ length: 20 }, (_, i) => ({
|
||||
role: i % 2 === 0 ? "user" : "assistant",
|
||||
content: `Message ${i + 1}`,
|
||||
}));
|
||||
|
||||
const result = formatMessagesForSummary(messages);
|
||||
|
||||
// Should indicate 12 messages omitted (20 total - 2 first - 6 last = 12)
|
||||
expect(result).toContain(
|
||||
'<message role="system">[... 12 messages omitted ...]</message>',
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle messages with special characters in content", () => {
|
||||
const messages = [
|
||||
{ role: "user", content: 'Hello <world> & "friends"' },
|
||||
{ role: "assistant", content: "Hi there! <tag>content</tag>" },
|
||||
];
|
||||
|
||||
const result = formatMessagesForSummary(messages);
|
||||
|
||||
// Should preserve special characters as-is (no HTML escaping)
|
||||
expect(result).toContain(
|
||||
'<message role="user">Hello <world> & "friends"</message>',
|
||||
);
|
||||
expect(result).toContain(
|
||||
'<message role="assistant">Hi there! <tag>content</tag></message>',
|
||||
);
|
||||
});
|
||||
|
||||
it("should maintain message order in truncated output", () => {
|
||||
const messages = Array.from({ length: 15 }, (_, i) => ({
|
||||
role: i % 2 === 0 ? "user" : "assistant",
|
||||
content: `Message ${i + 1}`,
|
||||
}));
|
||||
|
||||
const result = formatMessagesForSummary(messages);
|
||||
const lines = result.split("\n");
|
||||
|
||||
// Should have exactly 9 lines (2 first + 1 omission + 6 last)
|
||||
expect(lines).toHaveLength(9);
|
||||
|
||||
// Check order: first 2, then omission, then last 6
|
||||
expect(lines[0]).toBe('<message role="user">Message 1</message>');
|
||||
expect(lines[1]).toBe('<message role="assistant">Message 2</message>');
|
||||
expect(lines[2]).toBe(
|
||||
'<message role="system">[... 7 messages omitted ...]</message>',
|
||||
);
|
||||
|
||||
// Last 6 messages are messages 10-15 (indices 9-14)
|
||||
// Message 10 (index 9): 9 % 2 === 1, so "assistant"
|
||||
// Message 11 (index 10): 10 % 2 === 0, so "user"
|
||||
// Message 12 (index 11): 11 % 2 === 1, so "assistant"
|
||||
// Message 13 (index 12): 12 % 2 === 0, so "user"
|
||||
// Message 14 (index 13): 13 % 2 === 1, so "assistant"
|
||||
// Message 15 (index 14): 14 % 2 === 0, so "user"
|
||||
expect(lines[3]).toBe('<message role="assistant">Message 10</message>');
|
||||
expect(lines[4]).toBe('<message role="user">Message 11</message>');
|
||||
expect(lines[5]).toBe('<message role="assistant">Message 12</message>');
|
||||
expect(lines[6]).toBe('<message role="user">Message 13</message>');
|
||||
expect(lines[7]).toBe('<message role="assistant">Message 14</message>');
|
||||
expect(lines[8]).toBe('<message role="user">Message 15</message>');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,227 @@
|
||||
import { parseAppMentions } from "@/shared/parse_mention_apps";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("parseAppMentions", () => {
|
||||
it("should parse basic app mentions", () => {
|
||||
const prompt = "Can you help me with @app:MyApp and @app:AnotherApp?";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["MyApp", "AnotherApp"]);
|
||||
});
|
||||
|
||||
it("should parse app mentions with underscores", () => {
|
||||
const prompt = "I need help with @app:my_app and @app:another_app_name";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["my_app", "another_app_name"]);
|
||||
});
|
||||
|
||||
it("should parse app mentions with hyphens", () => {
|
||||
const prompt = "Check @app:my-app and @app:another-app-name";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["my-app", "another-app-name"]);
|
||||
});
|
||||
|
||||
it("should parse app mentions with numbers", () => {
|
||||
const prompt = "Update @app:app1 and @app:app2023 please";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["app1", "app2023"]);
|
||||
});
|
||||
|
||||
it("should not parse mentions without app: prefix", () => {
|
||||
const prompt = "Can you work on @MyApp and @AnotherApp?";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should require exact 'app:' prefix (case sensitive)", () => {
|
||||
const prompt = "Check @App:MyApp and @APP:AnotherApp vs @app:ValidApp";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["ValidApp"]);
|
||||
});
|
||||
|
||||
it("should parse mixed case app mentions", () => {
|
||||
const prompt = "Help with @app:MyApp, @app:myapp, and @app:MYAPP";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["MyApp", "myapp", "MYAPP"]);
|
||||
});
|
||||
|
||||
it("should parse app mentions with mixed characters (no spaces)", () => {
|
||||
const prompt = "Check @app:My_App-2023 and @app:Another_App_Name-v2";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["My_App-2023", "Another_App_Name-v2"]);
|
||||
});
|
||||
|
||||
it("should not handle spaces in app names (spaces break app names)", () => {
|
||||
const prompt = "Work on @app:My_App_Name with underscores";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["My_App_Name"]);
|
||||
});
|
||||
|
||||
it("should handle empty string", () => {
|
||||
const result = parseAppMentions("");
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle string with no mentions", () => {
|
||||
const prompt = "This is just a regular message without any mentions";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle standalone @ symbol", () => {
|
||||
const prompt = "This has @ symbol but no valid mention";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should ignore @ followed by special characters", () => {
|
||||
const prompt = "Check @# and @! and @$ symbols";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should ignore @ at the end of string", () => {
|
||||
const prompt = "This ends with @";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should parse mentions at different positions", () => {
|
||||
const prompt =
|
||||
"@app:StartApp in the beginning, @app:MiddleApp in middle, and @app:EndApp at end";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["StartApp", "MiddleApp", "EndApp"]);
|
||||
});
|
||||
|
||||
it("should handle mentions with punctuation around them", () => {
|
||||
const prompt = "Check (@app:MyApp), @app:AnotherApp! and @app:ThirdApp?";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["MyApp", "AnotherApp", "ThirdApp"]);
|
||||
});
|
||||
|
||||
it("should parse mentions in different sentence structures", () => {
|
||||
const prompt = `
|
||||
Can you help me with @app:WebApp?
|
||||
I also need @app:MobileApp updated.
|
||||
Don't forget about @app:DesktopApp.
|
||||
`;
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["WebApp", "MobileApp", "DesktopApp"]);
|
||||
});
|
||||
|
||||
it("should handle duplicate mentions", () => {
|
||||
const prompt = "Update @app:MyApp and also check @app:MyApp again";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["MyApp", "MyApp"]);
|
||||
});
|
||||
|
||||
it("should parse mentions in multiline text", () => {
|
||||
const prompt = `Line 1 has @app:App1
|
||||
Line 2 has @app:App2
|
||||
Line 3 has @app:App3`;
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["App1", "App2", "App3"]);
|
||||
});
|
||||
|
||||
it("should handle mentions with tabs and other whitespace", () => {
|
||||
const prompt = "Check\t@app:TabApp\nand\r@app:NewlineApp";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["TabApp", "NewlineApp"]);
|
||||
});
|
||||
|
||||
it("should parse single character app names", () => {
|
||||
const prompt = "Check @app:A and @app:B and @app:1";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["A", "B", "1"]);
|
||||
});
|
||||
|
||||
it("should handle very long app names", () => {
|
||||
const longAppName = "VeryLongAppNameWithManyCharacters123_test-app";
|
||||
const prompt = `Check @app:${longAppName}`;
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual([longAppName]);
|
||||
});
|
||||
|
||||
it("should stop parsing at invalid characters", () => {
|
||||
const prompt =
|
||||
"Check @app:MyApp@InvalidPart and @app:AnotherApp.InvalidPart";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["MyApp", "AnotherApp"]);
|
||||
});
|
||||
|
||||
it("should handle mentions with numbers and underscores mixed", () => {
|
||||
const prompt = "Update @app:app_v1_2023 and @app:test_app_123";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["app_v1_2023", "test_app_123"]);
|
||||
});
|
||||
|
||||
it("should handle mentions with hyphens and numbers mixed", () => {
|
||||
const prompt = "Check @app:app-v1-2023 and @app:test-app-123";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["app-v1-2023", "test-app-123"]);
|
||||
});
|
||||
|
||||
it("should parse mentions in URLs and complex text", () => {
|
||||
const prompt =
|
||||
"Visit https://example.com and check @app:WebApp for updates. Email admin@company.com about @app:MobileApp";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["WebApp", "MobileApp"]);
|
||||
});
|
||||
|
||||
it("should not handle spaces in app names (spaces break app names)", () => {
|
||||
const prompt = "Check @app:My_App_Name with underscores";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["My_App_Name"]);
|
||||
});
|
||||
|
||||
it("should parse mentions in JSON-like strings", () => {
|
||||
const prompt = '{"app": "@app:MyApp", "another": "@app:SecondApp"}';
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["MyApp", "SecondApp"]);
|
||||
});
|
||||
|
||||
it("should handle complex real-world scenarios (no spaces in app names)", () => {
|
||||
const prompt = `
|
||||
Hi there! I need help with @app:My_Web_App and @app:Mobile_App_v2.
|
||||
Could you also check the status of @app:backend-service-2023?
|
||||
Don't forget about @app:legacy_app and @app:NEW_PROJECT.
|
||||
|
||||
Thanks!
|
||||
@app:user_mention should not be confused with @app:ActualApp.
|
||||
`;
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual([
|
||||
"My_Web_App",
|
||||
"Mobile_App_v2",
|
||||
"backend-service-2023",
|
||||
"legacy_app",
|
||||
"NEW_PROJECT",
|
||||
"user_mention",
|
||||
"ActualApp",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should preserve order of mentions", () => {
|
||||
const prompt = "@app:Third @app:First @app:Second @app:Third @app:First";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["Third", "First", "Second", "Third", "First"]);
|
||||
});
|
||||
|
||||
it("should handle edge case with @ followed by space", () => {
|
||||
const prompt = "This has @ space but @app:ValidApp is here";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["ValidApp"]);
|
||||
});
|
||||
|
||||
it("should handle unicode characters after @", () => {
|
||||
const prompt = "Check @app:AppName and @app:测试 and @app:café-app";
|
||||
const result = parseAppMentions(prompt);
|
||||
// Based on the regex, unicode characters like 测试 and é should not match
|
||||
expect(result).toEqual(["AppName", "caf"]);
|
||||
});
|
||||
|
||||
it("should handle nested mentions pattern", () => {
|
||||
const prompt = "Check @app:App1 @app:App2 @app:App3 test";
|
||||
const result = parseAppMentions(prompt);
|
||||
expect(result).toEqual(["App1", "App2", "App3"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
import { parseOllamaHost } from "@/ipc/handlers/local_model_ollama_handler";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("parseOllamaHost", () => {
|
||||
it("should return default URL when no host is provided", () => {
|
||||
const result = parseOllamaHost();
|
||||
expect(result).toBe("http://localhost:11434");
|
||||
});
|
||||
|
||||
it("should return default URL when host is undefined", () => {
|
||||
const result = parseOllamaHost(undefined);
|
||||
expect(result).toBe("http://localhost:11434");
|
||||
});
|
||||
|
||||
it("should return default URL when host is empty string", () => {
|
||||
const result = parseOllamaHost("");
|
||||
expect(result).toBe("http://localhost:11434");
|
||||
});
|
||||
|
||||
describe("full URLs with protocol", () => {
|
||||
it("should return http URLs as-is", () => {
|
||||
const input = "http://localhost:11434";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("http://localhost:11434");
|
||||
});
|
||||
|
||||
it("should return https URLs as-is", () => {
|
||||
const input = "https://example.com:11434";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("https://example.com:11434");
|
||||
});
|
||||
|
||||
it("should return http URLs with custom ports as-is", () => {
|
||||
const input = "http://192.168.1.100:8080";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("http://192.168.1.100:8080");
|
||||
});
|
||||
|
||||
it("should return https URLs with paths as-is", () => {
|
||||
const input = "https://api.example.com:443/ollama";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("https://api.example.com:443/ollama");
|
||||
});
|
||||
});
|
||||
|
||||
describe("hostname with port", () => {
|
||||
it("should add http protocol to IPv4 host with port", () => {
|
||||
const input = "192.168.1.100:8080";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("http://192.168.1.100:8080");
|
||||
});
|
||||
|
||||
it("should add http protocol to localhost with custom port", () => {
|
||||
const input = "localhost:8080";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("http://localhost:8080");
|
||||
});
|
||||
|
||||
it("should add http protocol to domain with port", () => {
|
||||
const input = "ollama.example.com:11434";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("http://ollama.example.com:11434");
|
||||
});
|
||||
|
||||
it("should add http protocol to 0.0.0.0 with port", () => {
|
||||
const input = "0.0.0.0:1234";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("http://0.0.0.0:1234");
|
||||
});
|
||||
|
||||
it("should handle IPv6 with port", () => {
|
||||
const input = "[::1]:8080";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("http://[::1]:8080");
|
||||
});
|
||||
});
|
||||
|
||||
describe("hostname only", () => {
|
||||
it("should add http protocol and default port to IPv4 host", () => {
|
||||
const input = "192.168.1.100";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("http://192.168.1.100:11434");
|
||||
});
|
||||
|
||||
it("should add http protocol and default port to localhost", () => {
|
||||
const input = "localhost";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("http://localhost:11434");
|
||||
});
|
||||
|
||||
it("should add http protocol and default port to domain", () => {
|
||||
const input = "ollama.example.com";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("http://ollama.example.com:11434");
|
||||
});
|
||||
|
||||
it("should add http protocol and default port to 0.0.0.0", () => {
|
||||
const input = "0.0.0.0";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("http://0.0.0.0:11434");
|
||||
});
|
||||
|
||||
it("should handle IPv6 hostname", () => {
|
||||
const input = "::1";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("http://[::1]:11434");
|
||||
});
|
||||
|
||||
it("should handle full IPv6 hostname", () => {
|
||||
const input = "2001:db8:85a3:0:0:8a2e:370:7334";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("http://[2001:db8:85a3:0:0:8a2e:370:7334]:11434");
|
||||
});
|
||||
|
||||
it("should handle compressed IPv6 hostname", () => {
|
||||
const input = "2001:db8::1";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("http://[2001:db8::1]:11434");
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle hostname with unusual characters", () => {
|
||||
const input = "my-ollama-server";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("http://my-ollama-server:11434");
|
||||
});
|
||||
|
||||
it("should handle hostname with dots", () => {
|
||||
const input = "my.ollama.server";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("http://my.ollama.server:11434");
|
||||
});
|
||||
|
||||
it("should handle port 80", () => {
|
||||
const input = "example.com:80";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("http://example.com:80");
|
||||
});
|
||||
|
||||
it("should handle port 443", () => {
|
||||
const input = "example.com:443";
|
||||
const result = parseOllamaHost(input);
|
||||
expect(result).toBe("http://example.com:443");
|
||||
});
|
||||
});
|
||||
});
|
||||
227
backups/backup-20251218-094212/src/__tests__/path_utils.test.ts
Normal file
227
backups/backup-20251218-094212/src/__tests__/path_utils.test.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import { safeJoin } from "@/ipc/utils/path_utils";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
|
||||
describe("safeJoin", () => {
|
||||
const testBaseDir = "/app/workspace";
|
||||
const testBaseDirWindows = "C:\\app\\workspace";
|
||||
|
||||
describe("safe paths", () => {
|
||||
it("should join simple relative paths", () => {
|
||||
const result = safeJoin(testBaseDir, "src", "components", "Button.tsx");
|
||||
expect(result).toBe(
|
||||
path.join(testBaseDir, "src", "components", "Button.tsx"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle single file names", () => {
|
||||
const result = safeJoin(testBaseDir, "package.json");
|
||||
expect(result).toBe(path.join(testBaseDir, "package.json"));
|
||||
});
|
||||
|
||||
it("should handle nested directories", () => {
|
||||
const result = safeJoin(testBaseDir, "src/pages/home/index.tsx");
|
||||
expect(result).toBe(path.join(testBaseDir, "src/pages/home/index.tsx"));
|
||||
});
|
||||
|
||||
it("should handle paths with dots in filename", () => {
|
||||
const result = safeJoin(testBaseDir, "config.test.js");
|
||||
expect(result).toBe(path.join(testBaseDir, "config.test.js"));
|
||||
});
|
||||
|
||||
it("should handle empty path segments", () => {
|
||||
const result = safeJoin(testBaseDir, "", "src", "", "file.ts");
|
||||
expect(result).toBe(path.join(testBaseDir, "", "src", "", "file.ts"));
|
||||
});
|
||||
|
||||
it("should handle multiple path segments", () => {
|
||||
const result = safeJoin(testBaseDir, "a", "b", "c", "d", "file.txt");
|
||||
expect(result).toBe(
|
||||
path.join(testBaseDir, "a", "b", "c", "d", "file.txt"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should work with actual temp directory", () => {
|
||||
const tempDir = os.tmpdir();
|
||||
const result = safeJoin(tempDir, "test", "file.txt");
|
||||
expect(result).toBe(path.join(tempDir, "test", "file.txt"));
|
||||
});
|
||||
|
||||
it("should handle Windows-style relative paths with backslashes", () => {
|
||||
const result = safeJoin(testBaseDir, "src\\components\\Button.tsx");
|
||||
expect(result).toBe(
|
||||
path.join(testBaseDir, "src\\components\\Button.tsx"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle mixed forward/backslashes in relative paths", () => {
|
||||
const result = safeJoin(testBaseDir, "src/components\\ui/button.tsx");
|
||||
expect(result).toBe(
|
||||
path.join(testBaseDir, "src/components\\ui/button.tsx"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle Windows-style nested directories", () => {
|
||||
const result = safeJoin(
|
||||
testBaseDir,
|
||||
"pages\\home\\components\\index.tsx",
|
||||
);
|
||||
expect(result).toBe(
|
||||
path.join(testBaseDir, "pages\\home\\components\\index.tsx"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle relative paths starting with dot and backslash", () => {
|
||||
const result = safeJoin(testBaseDir, ".\\src\\file.txt");
|
||||
expect(result).toBe(path.join(testBaseDir, ".\\src\\file.txt"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("unsafe paths - directory traversal", () => {
|
||||
it("should throw on simple parent directory traversal", () => {
|
||||
expect(() => safeJoin(testBaseDir, "../outside.txt")).toThrow(
|
||||
/would escape the base directory/,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw on multiple parent directory traversals", () => {
|
||||
expect(() => safeJoin(testBaseDir, "../../etc/passwd")).toThrow(
|
||||
/would escape the base directory/,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw on complex traversal paths", () => {
|
||||
expect(() => safeJoin(testBaseDir, "src/../../../etc/passwd")).toThrow(
|
||||
/would escape the base directory/,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw on mixed traversal with valid components", () => {
|
||||
expect(() =>
|
||||
safeJoin(
|
||||
testBaseDir,
|
||||
"src",
|
||||
"components",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"outside.txt",
|
||||
),
|
||||
).toThrow(/would escape the base directory/);
|
||||
});
|
||||
|
||||
it("should throw on absolute Unix paths", () => {
|
||||
expect(() => safeJoin(testBaseDir, "/etc/passwd")).toThrow(
|
||||
/would escape the base directory/,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw on absolute Windows paths", () => {
|
||||
expect(() =>
|
||||
safeJoin(testBaseDir, "C:\\Windows\\System32\\config"),
|
||||
).toThrow(/would escape the base directory/);
|
||||
});
|
||||
|
||||
it("should throw on Windows UNC paths", () => {
|
||||
expect(() =>
|
||||
safeJoin(testBaseDir, "\\\\server\\share\\file.txt"),
|
||||
).toThrow(/would escape the base directory/);
|
||||
});
|
||||
|
||||
it("should throw on home directory shortcuts", () => {
|
||||
expect(() => safeJoin(testBaseDir, "~/secrets.txt")).toThrow(
|
||||
/would escape the base directory/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle Windows-style base paths", () => {
|
||||
const result = safeJoin(testBaseDirWindows, "src", "file.txt");
|
||||
expect(result).toBe(path.join(testBaseDirWindows, "src", "file.txt"));
|
||||
});
|
||||
|
||||
it("should throw on Windows traversal from Unix base", () => {
|
||||
expect(() => safeJoin(testBaseDir, "..\\..\\file.txt")).toThrow(
|
||||
/would escape the base directory/,
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle current directory references safely", () => {
|
||||
const result = safeJoin(testBaseDir, "./src/file.txt");
|
||||
expect(result).toBe(path.join(testBaseDir, "./src/file.txt"));
|
||||
});
|
||||
|
||||
it("should handle nested current directory references", () => {
|
||||
const result = safeJoin(testBaseDir, "src/./components/./Button.tsx");
|
||||
expect(result).toBe(
|
||||
path.join(testBaseDir, "src/./components/./Button.tsx"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw when current dir plus traversal escapes", () => {
|
||||
expect(() => safeJoin(testBaseDir, "./../../outside.txt")).toThrow(
|
||||
/would escape the base directory/,
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle very long paths safely", () => {
|
||||
const longPath = Array(50).fill("subdir").join("/") + "/file.txt";
|
||||
const result = safeJoin(testBaseDir, longPath);
|
||||
expect(result).toBe(path.join(testBaseDir, longPath));
|
||||
});
|
||||
|
||||
it("should allow Windows-style paths that look like drive letters but aren't", () => {
|
||||
// These look like they could be problematic but are actually safe relative paths
|
||||
const result1 = safeJoin(testBaseDir, "C_drive\\file.txt");
|
||||
expect(result1).toBe(path.join(testBaseDir, "C_drive\\file.txt"));
|
||||
|
||||
const result2 = safeJoin(testBaseDir, "src\\C-file.txt");
|
||||
expect(result2).toBe(path.join(testBaseDir, "src\\C-file.txt"));
|
||||
});
|
||||
|
||||
it("should handle Windows paths with multiple backslashes (not UNC)", () => {
|
||||
// Single backslashes in the middle are fine - it's only \\ at the start that's UNC
|
||||
const result = safeJoin(testBaseDir, "src\\\\components\\\\Button.tsx");
|
||||
expect(result).toBe(
|
||||
path.join(testBaseDir, "src\\\\components\\\\Button.tsx"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should provide descriptive error messages", () => {
|
||||
expect(() => safeJoin("/base", "../outside.txt")).toThrow(
|
||||
'Unsafe path: joining "../outside.txt" with base "/base" would escape the base directory',
|
||||
);
|
||||
});
|
||||
|
||||
it("should provide descriptive error for multiple segments", () => {
|
||||
expect(() => safeJoin("/base", "src", "..", "..", "outside.txt")).toThrow(
|
||||
'Unsafe path: joining "src, .., .., outside.txt" with base "/base" would escape the base directory',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("boundary conditions", () => {
|
||||
it("should allow paths at the exact boundary", () => {
|
||||
const result = safeJoin(testBaseDir, ".");
|
||||
expect(result).toBe(path.join(testBaseDir, "."));
|
||||
});
|
||||
|
||||
it("should handle paths that approach but don't cross boundary", () => {
|
||||
const result = safeJoin(testBaseDir, "deep/nested/../file.txt");
|
||||
expect(result).toBe(path.join(testBaseDir, "deep/nested/../file.txt"));
|
||||
});
|
||||
|
||||
it("should handle root directory as base", () => {
|
||||
const result = safeJoin("/", "tmp/file.txt");
|
||||
expect(result).toBe(path.join("/", "tmp/file.txt"));
|
||||
});
|
||||
|
||||
it("should throw when trying to escape root", () => {
|
||||
expect(() => safeJoin("/tmp", "../etc/passwd")).toThrow(
|
||||
/would escape the base directory/,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,232 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { createProblemFixPrompt } from "../shared/problem_prompt";
|
||||
import type { ProblemReport } from "../ipc/ipc_types";
|
||||
|
||||
const snippet = `SNIPPET`;
|
||||
|
||||
describe("problem_prompt", () => {
|
||||
describe("createProblemFixPrompt", () => {
|
||||
it("should return a message when no problems exist", () => {
|
||||
const problemReport: ProblemReport = {
|
||||
problems: [],
|
||||
};
|
||||
|
||||
const result = createProblemFixPrompt(problemReport);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should format a single error correctly", () => {
|
||||
const problemReport: ProblemReport = {
|
||||
problems: [
|
||||
{
|
||||
file: "src/components/Button.tsx",
|
||||
line: 15,
|
||||
column: 23,
|
||||
message: "Property 'onClick' does not exist on type 'ButtonProps'.",
|
||||
code: 2339,
|
||||
snippet,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = createProblemFixPrompt(problemReport);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should format multiple errors across multiple files", () => {
|
||||
const problemReport: ProblemReport = {
|
||||
problems: [
|
||||
{
|
||||
file: "src/components/Button.tsx",
|
||||
line: 15,
|
||||
column: 23,
|
||||
message: "Property 'onClick' does not exist on type 'ButtonProps'.",
|
||||
code: 2339,
|
||||
snippet,
|
||||
},
|
||||
{
|
||||
file: "src/components/Button.tsx",
|
||||
line: 8,
|
||||
column: 12,
|
||||
message:
|
||||
"Type 'string | undefined' is not assignable to type 'string'.",
|
||||
code: 2322,
|
||||
snippet,
|
||||
},
|
||||
{
|
||||
file: "src/hooks/useApi.ts",
|
||||
line: 42,
|
||||
column: 5,
|
||||
message:
|
||||
"Argument of type 'unknown' is not assignable to parameter of type 'string'.",
|
||||
code: 2345,
|
||||
snippet,
|
||||
},
|
||||
{
|
||||
file: "src/utils/helpers.ts",
|
||||
line: 45,
|
||||
column: 8,
|
||||
message:
|
||||
"Function lacks ending return statement and return type does not include 'undefined'.",
|
||||
code: 2366,
|
||||
snippet,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = createProblemFixPrompt(problemReport);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should handle realistic React TypeScript errors", () => {
|
||||
const problemReport: ProblemReport = {
|
||||
problems: [
|
||||
{
|
||||
file: "src/components/UserProfile.tsx",
|
||||
line: 12,
|
||||
column: 35,
|
||||
message:
|
||||
"Type '{ children: string; }' is missing the following properties from type 'UserProfileProps': user, onEdit",
|
||||
code: 2739,
|
||||
snippet,
|
||||
},
|
||||
{
|
||||
file: "src/components/UserProfile.tsx",
|
||||
line: 25,
|
||||
column: 15,
|
||||
message: "Object is possibly 'null'.",
|
||||
code: 2531,
|
||||
snippet,
|
||||
},
|
||||
{
|
||||
file: "src/hooks/useLocalStorage.ts",
|
||||
line: 18,
|
||||
column: 12,
|
||||
message: "Type 'string | null' is not assignable to type 'T'.",
|
||||
code: 2322,
|
||||
snippet,
|
||||
},
|
||||
{
|
||||
file: "src/types/api.ts",
|
||||
line: 45,
|
||||
column: 3,
|
||||
message: "Duplicate identifier 'UserRole'.",
|
||||
code: 2300,
|
||||
snippet,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = createProblemFixPrompt(problemReport);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createConciseProblemFixPrompt", () => {
|
||||
it("should return a short message when no problems exist", () => {
|
||||
const problemReport: ProblemReport = {
|
||||
problems: [],
|
||||
};
|
||||
|
||||
const result = createProblemFixPrompt(problemReport);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should format a concise prompt for single error", () => {
|
||||
const problemReport: ProblemReport = {
|
||||
problems: [
|
||||
{
|
||||
file: "src/App.tsx",
|
||||
line: 10,
|
||||
column: 5,
|
||||
message: "Cannot find name 'consol'. Did you mean 'console'?",
|
||||
code: 2552,
|
||||
snippet,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = createProblemFixPrompt(problemReport);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should format a concise prompt for multiple errors", () => {
|
||||
const problemReport: ProblemReport = {
|
||||
problems: [
|
||||
{
|
||||
file: "src/main.ts",
|
||||
line: 5,
|
||||
column: 12,
|
||||
message:
|
||||
"Cannot find module 'react-dom/client' or its corresponding type declarations.",
|
||||
code: 2307,
|
||||
snippet,
|
||||
},
|
||||
{
|
||||
file: "src/components/Modal.tsx",
|
||||
line: 35,
|
||||
column: 20,
|
||||
message:
|
||||
"Property 'isOpen' does not exist on type 'IntrinsicAttributes & ModalProps'.",
|
||||
code: 2339,
|
||||
snippet,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = createProblemFixPrompt(problemReport);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("realistic TypeScript error scenarios", () => {
|
||||
it("should handle common React + TypeScript errors", () => {
|
||||
const problemReport: ProblemReport = {
|
||||
problems: [
|
||||
// Missing interface property
|
||||
{
|
||||
file: "src/components/ProductCard.tsx",
|
||||
line: 22,
|
||||
column: 18,
|
||||
message:
|
||||
"Property 'price' is missing in type '{ name: string; description: string; }' but required in type 'Product'.",
|
||||
code: 2741,
|
||||
snippet,
|
||||
},
|
||||
// Incorrect event handler type
|
||||
{
|
||||
file: "src/components/SearchInput.tsx",
|
||||
line: 15,
|
||||
column: 45,
|
||||
message:
|
||||
"Type '(value: string) => void' is not assignable to type 'ChangeEventHandler<HTMLInputElement>'.",
|
||||
code: 2322,
|
||||
snippet,
|
||||
},
|
||||
// Async/await without Promise return type
|
||||
{
|
||||
file: "src/api/userService.ts",
|
||||
line: 8,
|
||||
column: 1,
|
||||
message:
|
||||
"Function lacks ending return statement and return type does not include 'undefined'.",
|
||||
code: 2366,
|
||||
snippet,
|
||||
},
|
||||
// Strict null check
|
||||
{
|
||||
file: "src/utils/dataProcessor.ts",
|
||||
line: 34,
|
||||
column: 25,
|
||||
message: "Object is possibly 'undefined'.",
|
||||
code: 2532,
|
||||
snippet,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = createProblemFixPrompt(problemReport);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,409 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { safeStorage } from "electron";
|
||||
import { readSettings, getSettingsFilePath } from "@/main/settings";
|
||||
import { getUserDataPath } from "@/paths/paths";
|
||||
import { UserSettings } from "@/lib/schemas";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("node:fs");
|
||||
vi.mock("node:path");
|
||||
vi.mock("electron", () => ({
|
||||
safeStorage: {
|
||||
isEncryptionAvailable: vi.fn(),
|
||||
decryptString: vi.fn(),
|
||||
},
|
||||
}));
|
||||
vi.mock("@/paths/paths", () => ({
|
||||
getUserDataPath: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockFs = vi.mocked(fs);
|
||||
const mockPath = vi.mocked(path);
|
||||
const mockSafeStorage = vi.mocked(safeStorage);
|
||||
const mockGetUserDataPath = vi.mocked(getUserDataPath);
|
||||
|
||||
describe("readSettings", () => {
|
||||
const mockUserDataPath = "/mock/user/data";
|
||||
const mockSettingsPath = "/mock/user/data/user-settings.json";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockGetUserDataPath.mockReturnValue(mockUserDataPath);
|
||||
mockPath.join.mockReturnValue(mockSettingsPath);
|
||||
mockSafeStorage.isEncryptionAvailable.mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("when settings file does not exist", () => {
|
||||
it("should create default settings file and return default settings", () => {
|
||||
mockFs.existsSync.mockReturnValue(false);
|
||||
mockFs.writeFileSync.mockImplementation(() => {});
|
||||
|
||||
const result = readSettings();
|
||||
|
||||
expect(mockFs.existsSync).toHaveBeenCalledWith(mockSettingsPath);
|
||||
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
||||
mockSettingsPath,
|
||||
expect.stringContaining('"selectedModel"'),
|
||||
);
|
||||
expect(scrubSettings(result)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"enableAutoFixProblems": false,
|
||||
"enableAutoUpdate": true,
|
||||
"enableProLazyEditsMode": true,
|
||||
"enableProSmartFilesContextMode": true,
|
||||
"experiments": {},
|
||||
"hasRunBefore": false,
|
||||
"isRunning": false,
|
||||
"lastKnownPerformance": undefined,
|
||||
"providerSettings": {},
|
||||
"releaseChannel": "stable",
|
||||
"selectedChatMode": "build",
|
||||
"selectedModel": {
|
||||
"name": "auto",
|
||||
"provider": "auto",
|
||||
},
|
||||
"selectedTemplateId": "react",
|
||||
"telemetryConsent": "unset",
|
||||
"telemetryUserId": "[scrubbed]",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when settings file exists", () => {
|
||||
it("should read and merge settings with defaults", () => {
|
||||
const mockFileContent = {
|
||||
selectedModel: {
|
||||
name: "gpt-4",
|
||||
provider: "openai",
|
||||
},
|
||||
telemetryConsent: "opted_in",
|
||||
hasRunBefore: true,
|
||||
};
|
||||
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
|
||||
|
||||
const result = readSettings();
|
||||
|
||||
expect(mockFs.readFileSync).toHaveBeenCalledWith(
|
||||
mockSettingsPath,
|
||||
"utf-8",
|
||||
);
|
||||
expect(result.selectedModel).toEqual({
|
||||
name: "gpt-4",
|
||||
provider: "openai",
|
||||
});
|
||||
expect(result.telemetryConsent).toBe("opted_in");
|
||||
expect(result.hasRunBefore).toBe(true);
|
||||
// Should still have defaults for missing properties
|
||||
expect(result.enableAutoUpdate).toBe(true);
|
||||
expect(result.releaseChannel).toBe("stable");
|
||||
});
|
||||
|
||||
it("should decrypt encrypted provider API keys", () => {
|
||||
const mockFileContent = {
|
||||
providerSettings: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
value: "encrypted-api-key",
|
||||
encryptionType: "electron-safe-storage",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
|
||||
mockSafeStorage.decryptString.mockReturnValue("decrypted-api-key");
|
||||
|
||||
const result = readSettings();
|
||||
|
||||
expect(mockSafeStorage.decryptString).toHaveBeenCalledWith(
|
||||
Buffer.from("encrypted-api-key", "base64"),
|
||||
);
|
||||
expect(result.providerSettings.openai.apiKey).toEqual({
|
||||
value: "decrypted-api-key",
|
||||
encryptionType: "electron-safe-storage",
|
||||
});
|
||||
});
|
||||
|
||||
it("should decrypt encrypted GitHub access token", () => {
|
||||
const mockFileContent = {
|
||||
githubAccessToken: {
|
||||
value: "encrypted-github-token",
|
||||
encryptionType: "electron-safe-storage",
|
||||
},
|
||||
};
|
||||
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
|
||||
mockSafeStorage.decryptString.mockReturnValue("decrypted-github-token");
|
||||
|
||||
const result = readSettings();
|
||||
|
||||
expect(mockSafeStorage.decryptString).toHaveBeenCalledWith(
|
||||
Buffer.from("encrypted-github-token", "base64"),
|
||||
);
|
||||
expect(result.githubAccessToken).toEqual({
|
||||
value: "decrypted-github-token",
|
||||
encryptionType: "electron-safe-storage",
|
||||
});
|
||||
});
|
||||
|
||||
it("should decrypt encrypted Supabase tokens", () => {
|
||||
const mockFileContent = {
|
||||
supabase: {
|
||||
accessToken: {
|
||||
value: "encrypted-access-token",
|
||||
encryptionType: "electron-safe-storage",
|
||||
},
|
||||
refreshToken: {
|
||||
value: "encrypted-refresh-token",
|
||||
encryptionType: "electron-safe-storage",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
|
||||
mockSafeStorage.decryptString
|
||||
.mockReturnValueOnce("decrypted-refresh-token")
|
||||
.mockReturnValueOnce("decrypted-access-token");
|
||||
|
||||
const result = readSettings();
|
||||
|
||||
expect(mockSafeStorage.decryptString).toHaveBeenCalledTimes(2);
|
||||
expect(result.supabase?.refreshToken).toEqual({
|
||||
value: "decrypted-refresh-token",
|
||||
encryptionType: "electron-safe-storage",
|
||||
});
|
||||
expect(result.supabase?.accessToken).toEqual({
|
||||
value: "decrypted-access-token",
|
||||
encryptionType: "electron-safe-storage",
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle plaintext secrets without decryption", () => {
|
||||
const mockFileContent = {
|
||||
githubAccessToken: {
|
||||
value: "plaintext-token",
|
||||
encryptionType: "plaintext",
|
||||
},
|
||||
providerSettings: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
value: "plaintext-api-key",
|
||||
encryptionType: "plaintext",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
|
||||
|
||||
const result = readSettings();
|
||||
|
||||
expect(mockSafeStorage.decryptString).not.toHaveBeenCalled();
|
||||
expect(result.githubAccessToken?.value).toBe("plaintext-token");
|
||||
expect(result.providerSettings.openai.apiKey?.value).toBe(
|
||||
"plaintext-api-key",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle secrets without encryptionType", () => {
|
||||
const mockFileContent = {
|
||||
githubAccessToken: {
|
||||
value: "token-without-encryption-type",
|
||||
},
|
||||
providerSettings: {
|
||||
openai: {
|
||||
apiKey: {
|
||||
value: "api-key-without-encryption-type",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
|
||||
|
||||
const result = readSettings();
|
||||
|
||||
expect(mockSafeStorage.decryptString).not.toHaveBeenCalled();
|
||||
expect(result.githubAccessToken?.value).toBe(
|
||||
"token-without-encryption-type",
|
||||
);
|
||||
expect(result.providerSettings.openai.apiKey?.value).toBe(
|
||||
"api-key-without-encryption-type",
|
||||
);
|
||||
});
|
||||
|
||||
it("should strip extra fields not recognized by the schema", () => {
|
||||
const mockFileContent = {
|
||||
selectedModel: {
|
||||
name: "gpt-4",
|
||||
provider: "openai",
|
||||
},
|
||||
telemetryConsent: "opted_in",
|
||||
hasRunBefore: true,
|
||||
// Extra fields that are not in the schema
|
||||
unknownField: "should be removed",
|
||||
deprecatedSetting: true,
|
||||
extraConfig: {
|
||||
someValue: 123,
|
||||
anotherValue: "test",
|
||||
},
|
||||
};
|
||||
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
|
||||
|
||||
const result = readSettings();
|
||||
|
||||
expect(mockFs.readFileSync).toHaveBeenCalledWith(
|
||||
mockSettingsPath,
|
||||
"utf-8",
|
||||
);
|
||||
expect(result.selectedModel).toEqual({
|
||||
name: "gpt-4",
|
||||
provider: "openai",
|
||||
});
|
||||
expect(result.telemetryConsent).toBe("opted_in");
|
||||
expect(result.hasRunBefore).toBe(true);
|
||||
|
||||
// Extra fields should be stripped by schema validation
|
||||
expect(result).not.toHaveProperty("unknownField");
|
||||
expect(result).not.toHaveProperty("deprecatedSetting");
|
||||
expect(result).not.toHaveProperty("extraConfig");
|
||||
|
||||
// Should still have defaults for missing properties
|
||||
expect(result.enableAutoUpdate).toBe(true);
|
||||
expect(result.releaseChannel).toBe("stable");
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should return default settings when file read fails", () => {
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
mockFs.readFileSync.mockImplementation(() => {
|
||||
throw new Error("File read error");
|
||||
});
|
||||
|
||||
const result = readSettings();
|
||||
|
||||
expect(scrubSettings(result)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"enableAutoFixProblems": false,
|
||||
"enableAutoUpdate": true,
|
||||
"enableProLazyEditsMode": true,
|
||||
"enableProSmartFilesContextMode": true,
|
||||
"experiments": {},
|
||||
"hasRunBefore": false,
|
||||
"isRunning": false,
|
||||
"lastKnownPerformance": undefined,
|
||||
"providerSettings": {},
|
||||
"releaseChannel": "stable",
|
||||
"selectedChatMode": "build",
|
||||
"selectedModel": {
|
||||
"name": "auto",
|
||||
"provider": "auto",
|
||||
},
|
||||
"selectedTemplateId": "react",
|
||||
"telemetryConsent": "unset",
|
||||
"telemetryUserId": "[scrubbed]",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it("should return default settings when JSON parsing fails", () => {
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
mockFs.readFileSync.mockReturnValue("invalid json");
|
||||
|
||||
const result = readSettings();
|
||||
|
||||
expect(result).toMatchObject({
|
||||
selectedModel: {
|
||||
name: "auto",
|
||||
provider: "auto",
|
||||
},
|
||||
releaseChannel: "stable",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return default settings when schema validation fails", () => {
|
||||
const mockFileContent = {
|
||||
selectedModel: {
|
||||
name: "gpt-4",
|
||||
// Missing required 'provider' field
|
||||
},
|
||||
releaseChannel: "invalid-channel", // Invalid enum value
|
||||
};
|
||||
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
|
||||
|
||||
const result = readSettings();
|
||||
|
||||
expect(result).toMatchObject({
|
||||
selectedModel: {
|
||||
name: "auto",
|
||||
provider: "auto",
|
||||
},
|
||||
releaseChannel: "stable",
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle decryption errors gracefully", () => {
|
||||
const mockFileContent = {
|
||||
githubAccessToken: {
|
||||
value: "corrupted-encrypted-data",
|
||||
encryptionType: "electron-safe-storage",
|
||||
},
|
||||
};
|
||||
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockFileContent));
|
||||
mockSafeStorage.decryptString.mockImplementation(() => {
|
||||
throw new Error("Decryption failed");
|
||||
});
|
||||
|
||||
const result = readSettings();
|
||||
|
||||
expect(result).toMatchObject({
|
||||
selectedModel: {
|
||||
name: "auto",
|
||||
provider: "auto",
|
||||
},
|
||||
releaseChannel: "stable",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSettingsFilePath", () => {
|
||||
it("should return correct settings file path", () => {
|
||||
const result = getSettingsFilePath();
|
||||
|
||||
expect(mockGetUserDataPath).toHaveBeenCalled();
|
||||
expect(mockPath.join).toHaveBeenCalledWith(
|
||||
mockUserDataPath,
|
||||
"user-settings.json",
|
||||
);
|
||||
expect(result).toBe(mockSettingsPath);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function scrubSettings(result: UserSettings) {
|
||||
return {
|
||||
...result,
|
||||
telemetryUserId: "[scrubbed]",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { replacePromptReference } from "@/ipc/utils/replacePromptReference";
|
||||
|
||||
describe("replacePromptReference", () => {
|
||||
it("returns original when no references present", () => {
|
||||
const input = "Hello world";
|
||||
const output = replacePromptReference(input, {});
|
||||
expect(output).toBe(input);
|
||||
});
|
||||
|
||||
it("replaces a single @prompt:id with content", () => {
|
||||
const input = "Use this: @prompt:42";
|
||||
const prompts = { 42: "Meaning of life" };
|
||||
const output = replacePromptReference(input, prompts);
|
||||
expect(output).toBe("Use this: Meaning of life");
|
||||
});
|
||||
|
||||
it("replaces multiple occurrences and keeps surrounding text", () => {
|
||||
const input = "A @prompt:1 and B @prompt:2 end";
|
||||
const prompts = { 1: "One", 2: "Two" };
|
||||
const output = replacePromptReference(input, prompts);
|
||||
expect(output).toBe("A One and B Two end");
|
||||
});
|
||||
|
||||
it("leaves unknown references intact", () => {
|
||||
const input = "Unknown @prompt:99 here";
|
||||
const prompts = { 1: "One" };
|
||||
const output = replacePromptReference(input, prompts);
|
||||
expect(output).toBe("Unknown @prompt:99 here");
|
||||
});
|
||||
|
||||
it("supports string keys in map as well as numeric", () => {
|
||||
const input = "Mix @prompt:7 and @prompt:8";
|
||||
const prompts = { "7": "Seven", 8: "Eight" } as Record<
|
||||
string | number,
|
||||
string
|
||||
>;
|
||||
const output = replacePromptReference(input, prompts);
|
||||
expect(output).toBe("Mix Seven and Eight");
|
||||
});
|
||||
});
|
||||
118
backups/backup-20251218-094212/src/__tests__/style-utils.test.ts
Normal file
118
backups/backup-20251218-094212/src/__tests__/style-utils.test.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { stylesToTailwind } from "../utils/style-utils";
|
||||
|
||||
describe("convertSpacingToTailwind", () => {
|
||||
describe("margin conversion", () => {
|
||||
it("should convert equal margins on all sides", () => {
|
||||
const result = stylesToTailwind({
|
||||
margin: { left: "16px", right: "16px", top: "16px", bottom: "16px" },
|
||||
});
|
||||
expect(result).toEqual(["m-[16px]"]);
|
||||
});
|
||||
|
||||
it("should convert equal horizontal margins", () => {
|
||||
const result = stylesToTailwind({
|
||||
margin: { left: "16px", right: "16px" },
|
||||
});
|
||||
expect(result).toEqual(["mx-[16px]"]);
|
||||
});
|
||||
|
||||
it("should convert equal vertical margins", () => {
|
||||
const result = stylesToTailwind({
|
||||
margin: { top: "16px", bottom: "16px" },
|
||||
});
|
||||
expect(result).toEqual(["my-[16px]"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("padding conversion", () => {
|
||||
it("should convert equal padding on all sides", () => {
|
||||
const result = stylesToTailwind({
|
||||
padding: { left: "20px", right: "20px", top: "20px", bottom: "20px" },
|
||||
});
|
||||
expect(result).toEqual(["p-[20px]"]);
|
||||
});
|
||||
|
||||
it("should convert equal horizontal padding", () => {
|
||||
const result = stylesToTailwind({
|
||||
padding: { left: "12px", right: "12px" },
|
||||
});
|
||||
expect(result).toEqual(["px-[12px]"]);
|
||||
});
|
||||
|
||||
it("should convert equal vertical padding", () => {
|
||||
const result = stylesToTailwind({
|
||||
padding: { top: "8px", bottom: "8px" },
|
||||
});
|
||||
expect(result).toEqual(["py-[8px]"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("combined margin and padding", () => {
|
||||
it("should handle both margin and padding", () => {
|
||||
const result = stylesToTailwind({
|
||||
margin: { left: "16px", right: "16px" },
|
||||
padding: { top: "8px", bottom: "8px" },
|
||||
});
|
||||
expect(result).toContain("mx-[16px]");
|
||||
expect(result).toContain("py-[8px]");
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases: equal horizontal and vertical spacing", () => {
|
||||
it("should consolidate px = py to p when values match", () => {
|
||||
const result = stylesToTailwind({
|
||||
padding: { left: "16px", right: "16px", top: "16px", bottom: "16px" },
|
||||
});
|
||||
// When all four sides are equal, should use p-[]
|
||||
expect(result).toEqual(["p-[16px]"]);
|
||||
});
|
||||
|
||||
it("should consolidate mx = my to m when values match (but not all four sides)", () => {
|
||||
const result = stylesToTailwind({
|
||||
margin: { left: "20px", right: "20px", top: "20px", bottom: "20px" },
|
||||
});
|
||||
// When all four sides are equal, should use m-[]
|
||||
expect(result).toEqual(["m-[20px]"]);
|
||||
});
|
||||
|
||||
it("should not consolidate when px != py", () => {
|
||||
const result = stylesToTailwind({
|
||||
padding: { left: "16px", right: "16px", top: "8px", bottom: "8px" },
|
||||
});
|
||||
expect(result).toContain("px-[16px]");
|
||||
expect(result).toContain("py-[8px]");
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should not consolidate when mx != my", () => {
|
||||
const result = stylesToTailwind({
|
||||
margin: { left: "20px", right: "20px", top: "10px", bottom: "10px" },
|
||||
});
|
||||
expect(result).toContain("mx-[20px]");
|
||||
expect(result).toContain("my-[10px]");
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should handle case where left != right", () => {
|
||||
const result = stylesToTailwind({
|
||||
padding: { left: "16px", right: "12px", top: "8px", bottom: "8px" },
|
||||
});
|
||||
expect(result).toContain("pl-[16px]");
|
||||
expect(result).toContain("pr-[12px]");
|
||||
expect(result).toContain("py-[8px]");
|
||||
expect(result).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("should handle case where top != bottom", () => {
|
||||
const result = stylesToTailwind({
|
||||
margin: { left: "20px", right: "20px", top: "10px", bottom: "15px" },
|
||||
});
|
||||
expect(result).toContain("mx-[20px]");
|
||||
expect(result).toContain("mt-[10px]");
|
||||
expect(result).toContain("mb-[15px]");
|
||||
expect(result).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,352 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
isServerFunction,
|
||||
isSharedServerModule,
|
||||
extractFunctionNameFromPath,
|
||||
} from "@/supabase_admin/supabase_utils";
|
||||
import {
|
||||
toPosixPath,
|
||||
stripSupabaseFunctionsPrefix,
|
||||
buildSignature,
|
||||
type FileStatEntry,
|
||||
} from "@/supabase_admin/supabase_management_client";
|
||||
|
||||
describe("isServerFunction", () => {
|
||||
describe("returns true for valid function paths", () => {
|
||||
it("should return true for function index.ts", () => {
|
||||
expect(isServerFunction("supabase/functions/hello/index.ts")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for nested function files", () => {
|
||||
expect(isServerFunction("supabase/functions/hello/lib/utils.ts")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return true for function with complex name", () => {
|
||||
expect(isServerFunction("supabase/functions/send-email/index.ts")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("returns false for non-function paths", () => {
|
||||
it("should return false for shared modules", () => {
|
||||
expect(isServerFunction("supabase/functions/_shared/utils.ts")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return false for regular source files", () => {
|
||||
expect(isServerFunction("src/components/Button.tsx")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for root supabase files", () => {
|
||||
expect(isServerFunction("supabase/config.toml")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for non-supabase paths", () => {
|
||||
expect(isServerFunction("package.json")).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isSharedServerModule", () => {
|
||||
describe("returns true for _shared paths", () => {
|
||||
it("should return true for files in _shared", () => {
|
||||
expect(isSharedServerModule("supabase/functions/_shared/utils.ts")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return true for nested _shared files", () => {
|
||||
expect(
|
||||
isSharedServerModule("supabase/functions/_shared/lib/helpers.ts"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for _shared directory itself", () => {
|
||||
expect(isSharedServerModule("supabase/functions/_shared/")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("returns false for non-_shared paths", () => {
|
||||
it("should return false for regular functions", () => {
|
||||
expect(isSharedServerModule("supabase/functions/hello/index.ts")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return false for similar but different paths", () => {
|
||||
expect(isSharedServerModule("supabase/functions/shared/utils.ts")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return false for _shared in wrong location", () => {
|
||||
expect(isSharedServerModule("src/_shared/utils.ts")).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractFunctionNameFromPath", () => {
|
||||
describe("extracts function name correctly from nested paths", () => {
|
||||
it("should extract function name from index.ts path", () => {
|
||||
expect(
|
||||
extractFunctionNameFromPath("supabase/functions/hello/index.ts"),
|
||||
).toBe("hello");
|
||||
});
|
||||
|
||||
it("should extract function name from deeply nested path", () => {
|
||||
expect(
|
||||
extractFunctionNameFromPath("supabase/functions/hello/lib/utils.ts"),
|
||||
).toBe("hello");
|
||||
});
|
||||
|
||||
it("should extract function name from very deeply nested path", () => {
|
||||
expect(
|
||||
extractFunctionNameFromPath(
|
||||
"supabase/functions/hello/src/helpers/format.ts",
|
||||
),
|
||||
).toBe("hello");
|
||||
});
|
||||
|
||||
it("should extract function name with dashes", () => {
|
||||
expect(
|
||||
extractFunctionNameFromPath("supabase/functions/send-email/index.ts"),
|
||||
).toBe("send-email");
|
||||
});
|
||||
|
||||
it("should extract function name with underscores", () => {
|
||||
expect(
|
||||
extractFunctionNameFromPath("supabase/functions/my_function/index.ts"),
|
||||
).toBe("my_function");
|
||||
});
|
||||
});
|
||||
|
||||
describe("throws for invalid paths", () => {
|
||||
it("should throw for _shared paths", () => {
|
||||
expect(() =>
|
||||
extractFunctionNameFromPath("supabase/functions/_shared/utils.ts"),
|
||||
).toThrow(/Function names starting with "_" are reserved/);
|
||||
});
|
||||
|
||||
it("should throw for other _ prefixed directories", () => {
|
||||
expect(() =>
|
||||
extractFunctionNameFromPath("supabase/functions/_internal/utils.ts"),
|
||||
).toThrow(/Function names starting with "_" are reserved/);
|
||||
});
|
||||
|
||||
it("should throw for non-supabase paths", () => {
|
||||
expect(() =>
|
||||
extractFunctionNameFromPath("src/components/Button.tsx"),
|
||||
).toThrow(/Invalid Supabase function path/);
|
||||
});
|
||||
|
||||
it("should throw for supabase root files", () => {
|
||||
expect(() => extractFunctionNameFromPath("supabase/config.toml")).toThrow(
|
||||
/Invalid Supabase function path/,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw for partial matches", () => {
|
||||
expect(() => extractFunctionNameFromPath("supabase/functions")).toThrow(
|
||||
/Invalid Supabase function path/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handles edge cases", () => {
|
||||
it("should handle backslashes (Windows paths)", () => {
|
||||
expect(
|
||||
extractFunctionNameFromPath(
|
||||
"supabase\\functions\\hello\\lib\\utils.ts",
|
||||
),
|
||||
).toBe("hello");
|
||||
});
|
||||
|
||||
it("should handle mixed slashes", () => {
|
||||
expect(
|
||||
extractFunctionNameFromPath("supabase/functions\\hello/lib\\utils.ts"),
|
||||
).toBe("hello");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("toPosixPath", () => {
|
||||
it("should keep forward slashes unchanged", () => {
|
||||
expect(toPosixPath("supabase/functions/hello/index.ts")).toBe(
|
||||
"supabase/functions/hello/index.ts",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle empty string", () => {
|
||||
expect(toPosixPath("")).toBe("");
|
||||
});
|
||||
|
||||
it("should handle single filename", () => {
|
||||
expect(toPosixPath("index.ts")).toBe("index.ts");
|
||||
});
|
||||
|
||||
// Note: On Unix, path.sep is "/", so backslashes won't be converted
|
||||
// This test is for documentation - actual behavior depends on platform
|
||||
it("should handle path with no separators", () => {
|
||||
expect(toPosixPath("filename")).toBe("filename");
|
||||
});
|
||||
});
|
||||
|
||||
describe("stripSupabaseFunctionsPrefix", () => {
|
||||
describe("strips prefix correctly", () => {
|
||||
it("should strip full prefix from index.ts", () => {
|
||||
expect(
|
||||
stripSupabaseFunctionsPrefix(
|
||||
"supabase/functions/hello/index.ts",
|
||||
"hello",
|
||||
),
|
||||
).toBe("index.ts");
|
||||
});
|
||||
|
||||
it("should strip prefix from nested file", () => {
|
||||
expect(
|
||||
stripSupabaseFunctionsPrefix(
|
||||
"supabase/functions/hello/lib/utils.ts",
|
||||
"hello",
|
||||
),
|
||||
).toBe("lib/utils.ts");
|
||||
});
|
||||
|
||||
it("should handle leading slash", () => {
|
||||
expect(
|
||||
stripSupabaseFunctionsPrefix(
|
||||
"/supabase/functions/hello/index.ts",
|
||||
"hello",
|
||||
),
|
||||
).toBe("index.ts");
|
||||
});
|
||||
});
|
||||
|
||||
describe("handles edge cases", () => {
|
||||
it("should return filename when no prefix match", () => {
|
||||
const result = stripSupabaseFunctionsPrefix("just-a-file.ts", "hello");
|
||||
expect(result).toBe("just-a-file.ts");
|
||||
});
|
||||
|
||||
it("should handle paths without function name", () => {
|
||||
const result = stripSupabaseFunctionsPrefix(
|
||||
"supabase/functions/other/index.ts",
|
||||
"hello",
|
||||
);
|
||||
// Should strip base prefix and return the rest
|
||||
expect(result).toBe("other/index.ts");
|
||||
});
|
||||
|
||||
it("should handle empty relative path after prefix", () => {
|
||||
// When the path is exactly the function directory
|
||||
const result = stripSupabaseFunctionsPrefix(
|
||||
"supabase/functions/hello",
|
||||
"hello",
|
||||
);
|
||||
expect(result).toBe("hello");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildSignature", () => {
|
||||
it("should build signature from single entry", () => {
|
||||
const entries: FileStatEntry[] = [
|
||||
{
|
||||
absolutePath: "/app/file.ts",
|
||||
relativePath: "file.ts",
|
||||
mtimeMs: 1000,
|
||||
size: 100,
|
||||
},
|
||||
];
|
||||
const result = buildSignature(entries);
|
||||
expect(result).toBe("file.ts:3e8:64");
|
||||
});
|
||||
|
||||
it("should build signature from multiple entries sorted by relativePath", () => {
|
||||
const entries: FileStatEntry[] = [
|
||||
{
|
||||
absolutePath: "/app/b.ts",
|
||||
relativePath: "b.ts",
|
||||
mtimeMs: 2000,
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
absolutePath: "/app/a.ts",
|
||||
relativePath: "a.ts",
|
||||
mtimeMs: 1000,
|
||||
size: 100,
|
||||
},
|
||||
];
|
||||
const result = buildSignature(entries);
|
||||
// Should be sorted by relativePath
|
||||
expect(result).toBe("a.ts:3e8:64|b.ts:7d0:c8");
|
||||
});
|
||||
|
||||
it("should return empty string for empty array", () => {
|
||||
const result = buildSignature([]);
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it("should produce different signatures for different mtimes", () => {
|
||||
const entries1: FileStatEntry[] = [
|
||||
{
|
||||
absolutePath: "/app/file.ts",
|
||||
relativePath: "file.ts",
|
||||
mtimeMs: 1000,
|
||||
size: 100,
|
||||
},
|
||||
];
|
||||
const entries2: FileStatEntry[] = [
|
||||
{
|
||||
absolutePath: "/app/file.ts",
|
||||
relativePath: "file.ts",
|
||||
mtimeMs: 2000,
|
||||
size: 100,
|
||||
},
|
||||
];
|
||||
expect(buildSignature(entries1)).not.toBe(buildSignature(entries2));
|
||||
});
|
||||
|
||||
it("should produce different signatures for different sizes", () => {
|
||||
const entries1: FileStatEntry[] = [
|
||||
{
|
||||
absolutePath: "/app/file.ts",
|
||||
relativePath: "file.ts",
|
||||
mtimeMs: 1000,
|
||||
size: 100,
|
||||
},
|
||||
];
|
||||
const entries2: FileStatEntry[] = [
|
||||
{
|
||||
absolutePath: "/app/file.ts",
|
||||
relativePath: "file.ts",
|
||||
mtimeMs: 1000,
|
||||
size: 200,
|
||||
},
|
||||
];
|
||||
expect(buildSignature(entries1)).not.toBe(buildSignature(entries2));
|
||||
});
|
||||
|
||||
it("should include path in signature for cache invalidation", () => {
|
||||
const entries1: FileStatEntry[] = [
|
||||
{
|
||||
absolutePath: "/app/a.ts",
|
||||
relativePath: "a.ts",
|
||||
mtimeMs: 1000,
|
||||
size: 100,
|
||||
},
|
||||
];
|
||||
const entries2: FileStatEntry[] = [
|
||||
{
|
||||
absolutePath: "/app/b.ts",
|
||||
relativePath: "b.ts",
|
||||
mtimeMs: 1000,
|
||||
size: 100,
|
||||
},
|
||||
];
|
||||
expect(buildSignature(entries1)).not.toBe(buildSignature(entries2));
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user