Skip to content

Commit

Permalink
test(agent): add test against full postprocess flow. (TabbyML#1029)
Browse files Browse the repository at this point in the history
* test(agent): add test against full postprocess flow.

* chore(agent): update yarn lock file.

* test(agent): add cases to postprocess tests.

* chore(agent): downgrade glob to 7.2.0.
  • Loading branch information
icycodes authored Dec 18, 2023
1 parent 8a3b62b commit 7eabfee
Show file tree
Hide file tree
Showing 25 changed files with 1,046 additions and 450 deletions.
3 changes: 1 addition & 2 deletions clients/tabby-agent/.mocha.env.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
process.env.NODE_ENV = "test";
process.env.IS_BROWSER = false;
process.env.IS_TEST = true;
process.env.IS_TEST = 1;
3 changes: 2 additions & 1 deletion clients/tabby-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
"build": "tsc --noEmit && tsup",
"test": "mocha",
"test:watch": "env TEST_LOG_DEBUG=1 mocha --watch",
"test:golden": "mocha --grep golden ./tests/golden.test.ts",
"lint": "eslint --fix --ext .ts ./src && prettier --write .",
"lint:check": "eslint --ext .ts ./src && prettier --check ."
},
Expand All @@ -22,6 +21,7 @@
"@types/deep-equal": "^1.0.4",
"@types/fast-levenshtein": "^0.0.4",
"@types/fs-extra": "^11.0.1",
"@types/glob": "^7.2.0",
"@types/mocha": "^10.0.1",
"@types/node": "18.x",
"@types/object-hash": "^3.0.0",
Expand All @@ -34,6 +34,7 @@
"esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^8.55.0",
"eslint-config-prettier": "^9.0.0",
"glob": "^7.2.0",
"mocha": "^10.2.0",
"openapi-typescript": "^6.6.1",
"prettier": "^3.0.0",
Expand Down
144 changes: 0 additions & 144 deletions clients/tabby-agent/src/AgentConfig.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
import { EventEmitter } from "events";
import path from "path";
import os from "os";
import fs from "fs-extra";
import toml from "toml";
import chokidar from "chokidar";
import deepEqual from "deep-equal";
import { getProperty, deleteProperty } from "dot-prop";
import { isBrowser } from "./env";
import { rootLogger } from "./logger";

export type AgentConfig = {
server: {
endpoint: string;
Expand Down Expand Up @@ -107,136 +96,3 @@ export const defaultAgentConfig: AgentConfig = {
disable: false,
},
};

const configTomlTemplate = `## Tabby agent configuration file
## Online documentation: https://tabby.tabbyml.com/docs/extensions/configuration
## You can uncomment and edit the values below to change the default settings.
## Configurations in this file have lower priority than the IDE settings.
## Server
## You can set the server endpoint here and an optional authentication token if required.
# [server]
# endpoint = "http://localhost:8080" # http or https URL
# token = "your-token-here" # if token is set, request header Authorization = "Bearer $token" will be added automatically
## You can add custom request headers.
# [server.requestHeaders]
# Header1 = "Value1" # list your custom headers here
# Header2 = "Value2" # values can be strings, numbers or booleans
## Completion
## (Since 1.1.0) You can set the completion request timeout here.
## Note that there is also a timeout config at the server side.
# [completion]
# timeout = 4000 # 4s
## Logs
## You can set the log level here. The log file is located at ~/.tabby-client/agent/logs/.
# [logs]
# level = "silent" # "silent" or "error" or "debug"
## Anonymous usage tracking
## Tabby collects anonymous usage data and sends it to the Tabby team to help improve our products.
## Your code, generated completions, or any sensitive information is never tracked or sent.
## For more details on data collection, see https://tabby.tabbyml.com/docs/extensions/configuration#usage-collection
## Your contribution is greatly appreciated. However, if you prefer not to participate, you can disable anonymous usage tracking here.
# [anonymousUsageTracking]
# disable = false # set to true to disable
`;

const typeCheckSchema: Record<string, string> = {
server: "object",
"server.endpoint": "string",
"server.token": "string",
"server.requestHeaders": "object",
"server.requestTimeout": "number",
completion: "object",
"completion.prompt": "object",
"completion.prompt.experimentalStripAutoClosingCharacters": "boolean",
"completion.prompt.maxPrefixLines": "number",
"completion.prompt.maxSuffixLines": "number",
"completion.prompt.clipboard": "object",
"completion.prompt.clipboard.minChars": "number",
"completion.prompt.clipboard.maxChars": "number",
"completion.debounce": "object",
"completion.debounce.mode": "string",
"completion.debounce.interval": "number",
"completion.timeout": "number",
postprocess: "object",
"postprocess.limitScopeByIndentation": "object",
"postprocess.limitScopeByIndentation.experimentalKeepBlockScopeWhenCompletingLine": "boolean",
logs: "object",
"logs.level": "string",
anonymousUsageTracking: "object",
"anonymousUsageTracking.disable": "boolean",
};

function validateConfig(config: PartialAgentConfig): PartialAgentConfig {
for (const [key, type] of Object.entries(typeCheckSchema)) {
if (typeof getProperty(config, key) !== type) {
deleteProperty(config, key);
}
}
return config;
}

class ConfigFile extends EventEmitter {
private data: PartialAgentConfig = {};
private watcher?: chokidar.FSWatcher;
private logger = rootLogger.child({ component: "ConfigFile" });

constructor(private readonly filepath: string) {
super();
}

get config(): PartialAgentConfig {
return this.data;
}

async load() {
try {
const fileContent = await fs.readFile(this.filepath, "utf8");
const data = toml.parse(fileContent);
// If the config file contains no value, overwrite it with the new template.
if (Object.keys(data).length === 0 && fileContent.trim() !== configTomlTemplate.trim()) {
await this.createTemplate();
return;
}
this.data = validateConfig(data);
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
await this.createTemplate();
} else {
this.logger.error({ error }, "Failed to load config file");
}
}
}

watch() {
this.watcher = chokidar.watch(this.filepath, {
interval: 1000,
});
const onChanged = async () => {
const oldData = this.data;
await this.load();
if (!deepEqual(oldData, this.data)) {
super.emit("updated", this.data);
}
};
this.watcher.on("add", onChanged);
this.watcher.on("change", onChanged);
}

private async createTemplate() {
try {
await fs.outputFile(this.filepath, configTomlTemplate);
} catch (error) {
this.logger.error({ error }, "Failed to create config template file");
}
}
}

const configFilePath = path.join(os.homedir(), ".tabby-client", "agent", "config.toml");
export const configFile = isBrowser ? undefined : new ConfigFile(configFilePath);
3 changes: 2 additions & 1 deletion clients/tabby-agent/src/TabbyAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import type {
import type { DataStore } from "./dataStore";
import { isBlank, abortSignalFromAnyOf, HttpError, isTimeoutError, isCanceledError, errorToString } from "./utils";
import { Auth } from "./Auth";
import { AgentConfig, PartialAgentConfig, defaultAgentConfig, configFile } from "./AgentConfig";
import { AgentConfig, PartialAgentConfig, defaultAgentConfig } from "./AgentConfig";
import { configFile } from "./configFile";
import { CompletionCache } from "./CompletionCache";
import { CompletionDebounce } from "./CompletionDebounce";
import { CompletionContext } from "./CompletionContext";
Expand Down
144 changes: 144 additions & 0 deletions clients/tabby-agent/src/configFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { EventEmitter } from "events";
import path from "path";
import os from "os";
import fs from "fs-extra";
import toml from "toml";
import chokidar from "chokidar";
import deepEqual from "deep-equal";
import { getProperty, deleteProperty } from "dot-prop";
import type { PartialAgentConfig } from "./AgentConfig";
import { isBrowser } from "./env";
import { rootLogger } from "./logger";

const configTomlTemplate = `## Tabby agent configuration file
## Online documentation: https://tabby.tabbyml.com/docs/extensions/configuration
## You can uncomment and edit the values below to change the default settings.
## Configurations in this file have lower priority than the IDE settings.
## Server
## You can set the server endpoint here and an optional authentication token if required.
# [server]
# endpoint = "http://localhost:8080" # http or https URL
# token = "your-token-here" # if token is set, request header Authorization = "Bearer $token" will be added automatically
## You can add custom request headers.
# [server.requestHeaders]
# Header1 = "Value1" # list your custom headers here
# Header2 = "Value2" # values can be strings, numbers or booleans
## Completion
## (Since 1.1.0) You can set the completion request timeout here.
## Note that there is also a timeout config at the server side.
# [completion]
# timeout = 4000 # 4s
## Logs
## You can set the log level here. The log file is located at ~/.tabby-client/agent/logs/.
# [logs]
# level = "silent" # "silent" or "error" or "debug"
## Anonymous usage tracking
## Tabby collects anonymous usage data and sends it to the Tabby team to help improve our products.
## Your code, generated completions, or any sensitive information is never tracked or sent.
## For more details on data collection, see https://tabby.tabbyml.com/docs/extensions/configuration#usage-collection
## Your contribution is greatly appreciated. However, if you prefer not to participate, you can disable anonymous usage tracking here.
# [anonymousUsageTracking]
# disable = false # set to true to disable
`;

const typeCheckSchema: Record<string, string> = {
server: "object",
"server.endpoint": "string",
"server.token": "string",
"server.requestHeaders": "object",
"server.requestTimeout": "number",
completion: "object",
"completion.prompt": "object",
"completion.prompt.experimentalStripAutoClosingCharacters": "boolean",
"completion.prompt.maxPrefixLines": "number",
"completion.prompt.maxSuffixLines": "number",
"completion.prompt.clipboard": "object",
"completion.prompt.clipboard.minChars": "number",
"completion.prompt.clipboard.maxChars": "number",
"completion.debounce": "object",
"completion.debounce.mode": "string",
"completion.debounce.interval": "number",
"completion.timeout": "number",
postprocess: "object",
"postprocess.limitScopeByIndentation": "object",
"postprocess.limitScopeByIndentation.experimentalKeepBlockScopeWhenCompletingLine": "boolean",
logs: "object",
"logs.level": "string",
anonymousUsageTracking: "object",
"anonymousUsageTracking.disable": "boolean",
};

function validateConfig(config: PartialAgentConfig): PartialAgentConfig {
for (const [key, type] of Object.entries(typeCheckSchema)) {
if (typeof getProperty(config, key) !== type) {
deleteProperty(config, key);
}
}
return config;
}

class ConfigFile extends EventEmitter {
private data: PartialAgentConfig = {};
private watcher?: chokidar.FSWatcher;
private logger = rootLogger.child({ component: "ConfigFile" });

constructor(private readonly filepath: string) {
super();
}

get config(): PartialAgentConfig {
return this.data;
}

async load() {
try {
const fileContent = await fs.readFile(this.filepath, "utf8");
const data = toml.parse(fileContent);
// If the config file contains no value, overwrite it with the new template.
if (Object.keys(data).length === 0 && fileContent.trim() !== configTomlTemplate.trim()) {
await this.createTemplate();
return;
}
this.data = validateConfig(data);
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
await this.createTemplate();
} else {
this.logger.error({ error }, "Failed to load config file");
}
}
}

watch() {
this.watcher = chokidar.watch(this.filepath, {
interval: 1000,
});
const onChanged = async () => {
const oldData = this.data;
await this.load();
if (!deepEqual(oldData, this.data)) {
super.emit("updated", this.data);
}
};
this.watcher.on("add", onChanged);
this.watcher.on("change", onChanged);
}

private async createTemplate() {
try {
await fs.outputFile(this.filepath, configTomlTemplate);
} catch (error) {
this.logger.error({ error }, "Failed to create config template file");
}
}
}

const configFilePath = path.join(os.homedir(), ".tabby-client", "agent", "config.toml");
export const configFile = isBrowser ? undefined : new ConfigFile(configFilePath);
1 change: 1 addition & 0 deletions clients/tabby-agent/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// FIXME: refactor env variables for running tests
export const isBrowser = !!process.env["IS_BROWSER"];
export const isTest = !!process.env["IS_TEST"];
export const testLogDebug = !!process.env["TEST_LOG_DEBUG"];
2 changes: 1 addition & 1 deletion clients/tabby-agent/src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ if (isTest && testLogDebug) {
}

export const allLoggers = [rootLogger];
rootLogger.onChild = (child) => {
rootLogger.onChild = (child: pino.Logger) => {
allLoggers.push(child);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
description = 'Basic python function fibonacci'

[config]
# use default config

[context]
filepath = 'fibonacci.py'
language = 'python'
# indentation = ' ' # not specified
text = '''
def fibonacci(n):
├if n == 0 or n == 1:
return n┤
return fibonacci(n - 1) + fibonacci(n - 2)
'''

[expected]
unchanged = true
Loading

0 comments on commit 7eabfee

Please sign in to comment.