excludes, include --dst for glob extensions
This commit is contained in:
parent
976dea2757
commit
d4712949be
File diff suppressed because one or more lines are too long
2
packages/kbot/dist-in/glob.d.ts
vendored
2
packages/kbot/dist-in/glob.d.ts
vendored
@ -6,7 +6,7 @@ export declare const default_filters: {
|
||||
size: (filePath: string) => boolean;
|
||||
};
|
||||
export declare const isWebUrl: (str: string) => boolean;
|
||||
export declare const glob: (projectPath: string, include?: string[], exclude?: string[], options?: IKBotTask) => {
|
||||
export declare const glob: (projectPath: string, include?: string[], rawExcludeOptions?: string[], options?: IKBotTask) => {
|
||||
files: string[];
|
||||
webUrls: Set<string>;
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
3
packages/kbot/dist-in/source.d.ts
vendored
3
packages/kbot/dist-in/source.d.ts
vendored
@ -8,5 +8,6 @@ import { IKBotTask } from '@polymech/ai-tools';
|
||||
export declare const isPathOutsideSafe: (pathA: string, pathB: string) => boolean;
|
||||
export declare const base64: (filePath: string) => string | null;
|
||||
export declare const images: (files: string[]) => ChatCompletionContentPartImage[];
|
||||
export declare function get(projectPath: string, include: string[], options: IKBotTask): Promise<Array<IHandlerResult>>;
|
||||
export declare function get(projectPath: string, // This is already an absolute path from processRun/complete_messages
|
||||
include: string[], options: IKBotTask): Promise<Array<IHandlerResult>>;
|
||||
export declare function vectorize(file: string, options: IKBotTask): Promise<string>;
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
packages/kbot/dist-in/variables.d.ts
vendored
2
packages/kbot/dist-in/variables.d.ts
vendored
@ -1,3 +1,3 @@
|
||||
import { IKBotTask } from '@polymech/ai-tools';
|
||||
export declare const generateSingleFileVariables: (filePath: string, projectPath: string) => Record<string, string>;
|
||||
export declare const sourceVariables: (filePath: string, projectPath: string) => Record<string, string>;
|
||||
export declare const variables: (options: IKBotTask) => any;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -111,17 +111,19 @@ export const complete_messages = async (
|
||||
messages: Array<ChatCompletionMessageParam>,
|
||||
files: any[]
|
||||
}> => {
|
||||
let messages: Array<ChatCompletionMessageParam> = []
|
||||
let chatMessages: Array<ChatCompletionMessageParam> = []
|
||||
|
||||
const promptMessage = await prompt(opts)
|
||||
if (!promptMessage?.content) {
|
||||
return { messages: [], files: [] }
|
||||
}
|
||||
messages.push(promptMessage as ChatCompletionMessageParam)
|
||||
messages.push((await preferences(opts)) as ChatCompletionMessageParam)
|
||||
// Get content from files and web URLs
|
||||
let files = await get(path.resolve(options.path || '.'), options.include, options) || []
|
||||
files = files.map(f => {
|
||||
chatMessages.push(promptMessage as ChatCompletionMessageParam)
|
||||
chatMessages.push((await preferences(opts)) as ChatCompletionMessageParam)
|
||||
|
||||
const projectPath = path.resolve(options.path || '.')
|
||||
let sourceFiles = await get(projectPath, options.include, options) || []
|
||||
|
||||
const processedFileMessages: ChatCompletionMessageParam[] = sourceFiles.map(f => {
|
||||
if (f.path && f.content && typeof f.content === 'string') {
|
||||
const mimeType = lookup(f.path)
|
||||
// Check if the mime type is not binary (heuristic: starts with 'text/' or is a common non-binary type)
|
||||
@ -147,8 +149,9 @@ Original Content:
|
||||
}
|
||||
return { ...f, role: 'user' }
|
||||
})
|
||||
messages = [...messages as Array<ChatCompletionMessageParam>, ...files]
|
||||
return { messages, files }
|
||||
|
||||
chatMessages = [...chatMessages, ...processedFileMessages]
|
||||
return { messages: chatMessages, files: sourceFiles }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -223,6 +226,7 @@ export const execute_request = async (
|
||||
* @returns - The result of the task execution
|
||||
*/
|
||||
export const processRun = async (opts: IKBotTask): Promise<ProcessRunResult> => {
|
||||
const intialIncludes = [...opts.include]
|
||||
let options = await complete_options(opts)
|
||||
if (!options) {
|
||||
return null
|
||||
@ -231,10 +235,12 @@ export const processRun = async (opts: IKBotTask): Promise<ProcessRunResult> =>
|
||||
const client = options.client
|
||||
|
||||
const { messages, files } = await complete_messages(opts, options)
|
||||
if(intialIncludes.length > 0 && files.length === 0) {
|
||||
return ""
|
||||
}
|
||||
if (messages.length === 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const params = await complete_params(options, messages)
|
||||
|
||||
const logDir = path.resolve(resolve(opts.logs || './logs'))
|
||||
|
||||
@ -1,15 +1,13 @@
|
||||
import * as path from 'node:path'
|
||||
import { sync as dir } from '@polymech/fs/dir'
|
||||
|
||||
import { createItem as toNode } from '@polymech/fs/inspect'
|
||||
import { sync as exists } from '@polymech/fs/exists'
|
||||
import { isFile, forward_slash } from '@polymech/commons'
|
||||
import { createItem as toNode } from '@polymech/fs/inspect'
|
||||
import { isFile, forward_slash, resolveVariables } from '@polymech/commons'
|
||||
import { globSync, hasMagic } from 'glob'
|
||||
import { EXCLUDE_GLOB, MAX_FILE_SIZE } from './constants.js'
|
||||
import { IKBotTask} from '@polymech/ai-tools'
|
||||
import { resolveVariables } from '@polymech/commons'
|
||||
import { generateSingleFileVariables } from './variables.js'
|
||||
import { E_GlobExtensionType } from './zod_schema.js'
|
||||
import { IKBotTask } from '@polymech/ai-tools'
|
||||
import { sourceVariables } from './variables.js'
|
||||
import { E_GlobExtensionType, E_Mode } from './zod_schema.js'
|
||||
|
||||
export const default_filters = {
|
||||
isFile,
|
||||
@ -19,123 +17,121 @@ export const default_filters = {
|
||||
|
||||
const isPathInside = (childPath: string, parentPath: string): boolean => {
|
||||
const relation = path.relative(parentPath, childPath);
|
||||
return Boolean(
|
||||
relation &&
|
||||
!relation.startsWith('..') &&
|
||||
!relation.startsWith('..' + path.sep)
|
||||
);
|
||||
return Boolean(relation && !relation.startsWith('..') && !relation.startsWith('..' + path.sep));
|
||||
};
|
||||
|
||||
export const isWebUrl = (str: string): boolean => {
|
||||
return /^https?:\/\//.test(str);
|
||||
}
|
||||
export const isWebUrl = (str: string): boolean => /^https?:\/\//.test(str);
|
||||
|
||||
const globExtensionPresets: Map<E_GlobExtensionType, string> = new Map([
|
||||
['match-cpp', '${SRC_DIR}/${SRC_NAME}*.cpp']
|
||||
]);
|
||||
|
||||
const resolveAndGlobExtensionPattern = (
|
||||
resolvedPatternString: string,
|
||||
): string[] => {
|
||||
const resolveAndGlobExtensionPattern = (resolvedPatternString: string): string[] => {
|
||||
try {
|
||||
if (!hasMagic(resolvedPatternString)) {
|
||||
// No magic characters, treat as a literal path
|
||||
if (default_filters.exists(resolvedPatternString) && default_filters.isFile(resolvedPatternString)) {
|
||||
return [forward_slash(resolvedPatternString)];
|
||||
}
|
||||
return []; // Literal path does not exist or is not a file
|
||||
return [];
|
||||
} else {
|
||||
// Has magic characters, use globSync to expand
|
||||
const foundFiles = globSync(resolvedPatternString, {
|
||||
absolute: true, // Expecting resolvedPatternString to be absolute or glob to handle it
|
||||
nodir: true
|
||||
});
|
||||
const foundFiles = globSync(resolvedPatternString, { absolute: true, nodir: true });
|
||||
return foundFiles.map(f => forward_slash(f));
|
||||
}
|
||||
} catch (e) {
|
||||
// console.warn(`Error processing globExtension pattern "${resolvedPatternString}": ${e.message}`);
|
||||
return [];
|
||||
}
|
||||
} catch (e) { return []; }
|
||||
};
|
||||
|
||||
export const glob = (
|
||||
projectPath: string,
|
||||
include: string[] = [],
|
||||
exclude: string[] = [],
|
||||
rawExcludeOptions: string[] = [],
|
||||
options?: IKBotTask
|
||||
): { files: string[], webUrls: Set<string> } => {
|
||||
if (!exists(projectPath)) {
|
||||
dir(projectPath)
|
||||
return { files: [], webUrls: new Set<string>() }
|
||||
dir(projectPath);
|
||||
return { files: [], webUrls: new Set<string>() };
|
||||
}
|
||||
|
||||
const filters = new Set<string>()
|
||||
const absolutePathsFromInclude = new Set<string>()
|
||||
const webUrls = new Set<string>()
|
||||
const ignorePatterns = new Set<string>(EXCLUDE_GLOB)
|
||||
const includeFilters = new Set<string>(); // Renamed from 'filters' to be specific
|
||||
const absolutePathsFromInclude = new Set<string>();
|
||||
const webUrls = new Set<string>();
|
||||
const staticIgnorePatterns = new Set<string>(EXCLUDE_GLOB);
|
||||
const dynamicExcludePatterns: string[] = [];
|
||||
|
||||
include.forEach(pattern => {
|
||||
if (isWebUrl(pattern)) {
|
||||
webUrls.add(pattern)
|
||||
return
|
||||
}
|
||||
if (path.isAbsolute(pattern)) {
|
||||
absolutePathsFromInclude.add(path.resolve(pattern));
|
||||
} else {
|
||||
filters.add(pattern)
|
||||
}
|
||||
})
|
||||
|
||||
exclude.forEach(pattern => {
|
||||
if (isWebUrl(pattern)) {
|
||||
return;
|
||||
}
|
||||
ignorePatterns.add(pattern);
|
||||
(rawExcludeOptions || []).forEach(pattern => {
|
||||
if (pattern.includes('${')) { dynamicExcludePatterns.push(pattern); }
|
||||
else { staticIgnorePatterns.add(pattern); }
|
||||
});
|
||||
|
||||
const initialRelativeGlobResults = globSync([...filters], {
|
||||
cwd: projectPath,
|
||||
absolute: false,
|
||||
ignore: [...ignorePatterns],
|
||||
nodir: true
|
||||
})
|
||||
include.forEach(pattern => {
|
||||
if (isWebUrl(pattern)) { webUrls.add(pattern); return; }
|
||||
if (path.isAbsolute(pattern)) { absolutePathsFromInclude.add(path.resolve(pattern)); }
|
||||
else { includeFilters.add(pattern); }
|
||||
});
|
||||
|
||||
const initialAbsoluteFiles = new Set<string>([
|
||||
const initialRelativeGlobResults = globSync([...includeFilters], {
|
||||
cwd: projectPath, absolute: false, ignore: [...staticIgnorePatterns], nodir: true
|
||||
});
|
||||
const allInitialInputFiles = new Set<string>([
|
||||
...initialRelativeGlobResults.map(file => path.resolve(projectPath, file)),
|
||||
...Array.from(absolutePathsFromInclude)
|
||||
]);
|
||||
|
||||
const allFilesToConsider = new Set<string>(initialAbsoluteFiles);
|
||||
let filesSurvivingPrimaryDynamicExclude = new Set<string>();
|
||||
if (dynamicExcludePatterns.length > 0 && allInitialInputFiles.size > 0) {
|
||||
for (const inputFile of allInitialInputFiles) {
|
||||
let isDynamicallyExcluded = false;
|
||||
const fileVars = sourceVariables(inputFile, projectPath);
|
||||
for (const dynamicPattern of dynamicExcludePatterns) {
|
||||
const resolvedDynamicExcludeGlob = resolveVariables(dynamicPattern, false, fileVars, false);
|
||||
let foundExcludedMatches: string[] = [];
|
||||
try {
|
||||
if (path.isAbsolute(resolvedDynamicExcludeGlob)) {
|
||||
foundExcludedMatches = globSync(resolvedDynamicExcludeGlob, { absolute: true, nodir: true, cwd: process.cwd() });
|
||||
} else {
|
||||
foundExcludedMatches = globSync(resolvedDynamicExcludeGlob, { cwd: projectPath, absolute: true, nodir: true });
|
||||
}
|
||||
} catch (e) { options?.logger?.warn(`[Dynamic Exclude] Error globbing exclude pattern ${resolvedDynamicExcludeGlob}: ${e.message}`); }
|
||||
|
||||
if (foundExcludedMatches.length > 0) {
|
||||
//options?.logger?.info(`[Dynamic Exclude - Pass 1] Input file ${path.relative(projectPath, inputFile)} excluded by pattern ${resolvedDynamicExcludeGlob}`);
|
||||
isDynamicallyExcluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isDynamicallyExcluded) {
|
||||
filesSurvivingPrimaryDynamicExclude.add(inputFile);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filesSurvivingPrimaryDynamicExclude = new Set<string>(allInitialInputFiles);
|
||||
}
|
||||
|
||||
const finalCandidates = new Set<string>(filesSurvivingPrimaryDynamicExclude);
|
||||
|
||||
if (options && typeof options.globExtension === 'string' && options.globExtension.trim() !== '') {
|
||||
let rawPatternString = options.globExtension;
|
||||
let patternFromPresetOrCustom = options.globExtension;
|
||||
if (globExtensionPresets.has(options.globExtension as E_GlobExtensionType)) {
|
||||
rawPatternString = globExtensionPresets.get(options.globExtension as E_GlobExtensionType)!;
|
||||
patternFromPresetOrCustom = globExtensionPresets.get(options.globExtension as E_GlobExtensionType)!;
|
||||
}
|
||||
|
||||
for (const initialFile of [...initialAbsoluteFiles]) {
|
||||
const fileVars = generateSingleFileVariables(initialFile, projectPath);
|
||||
const fullyResolvedPattern = resolveVariables(rawPatternString, false, fileVars, false);
|
||||
|
||||
for (const sourceFileForExtension of filesSurvivingPrimaryDynamicExclude) {
|
||||
const fileVars = sourceVariables(sourceFileForExtension, projectPath);
|
||||
const fullyResolvedPattern = resolveVariables(patternFromPresetOrCustom, false, fileVars, false);
|
||||
const additionalFiles = resolveAndGlobExtensionPattern(fullyResolvedPattern);
|
||||
additionalFiles.forEach(f => allFilesToConsider.add(f));
|
||||
additionalFiles.forEach(f => finalCandidates.add(f));
|
||||
}
|
||||
}
|
||||
const finalFiles = Array.from(allFilesToConsider).filter(absoluteFilePath => {
|
||||
|
||||
const trulyFinalFiles = Array.from(finalCandidates).filter(absoluteFilePath => {
|
||||
if (!Object.keys(default_filters).every((key) => default_filters[key](absoluteFilePath))) {
|
||||
return false;
|
||||
}
|
||||
const relativeFilePath = path.relative(projectPath, absoluteFilePath);
|
||||
const checkResult = globSync([forward_slash(relativeFilePath)], {
|
||||
cwd: projectPath,
|
||||
ignore: [...ignorePatterns],
|
||||
nodir: true,
|
||||
absolute: false
|
||||
const staticCheckResult = globSync([forward_slash(relativeFilePath)], {
|
||||
cwd: projectPath, ignore: [...staticIgnorePatterns], nodir: true, absolute: false
|
||||
});
|
||||
if (checkResult.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (staticCheckResult.length === 0) { return false; }
|
||||
return true;
|
||||
});
|
||||
return { files: finalFiles.map(f => forward_slash(f)), webUrls }
|
||||
}
|
||||
|
||||
return { files: trulyFinalFiles.map(f => forward_slash(f)), webUrls };
|
||||
};
|
||||
|
||||
@ -5,7 +5,7 @@ import { sync as read } from '@polymech/fs/read'
|
||||
|
||||
// import { createItem as toNode } from '@polymech/fs/inspect' // Moved to glob.ts
|
||||
import { sync as exists } from '@polymech/fs/exists' // Still needed for vectorize
|
||||
import { isFile, forward_slash } from '@polymech/commons' // isFile potentially still needed for vectorize
|
||||
import { isFile, forward_slash, resolve as resolvePath } from '@polymech/commons' // Renamed resolve to resolvePath to avoid conflict
|
||||
import { logger } from './index.js'
|
||||
import { lookup } from 'mime-types'
|
||||
// import { globSync } from 'glob' // Moved to glob.ts
|
||||
@ -16,6 +16,8 @@ import { IKBotTask, ICollector } from '@polymech/ai-tools'
|
||||
import { supported } from './commands/run-assistant.js'
|
||||
import { handleWebUrl } from './http.js'
|
||||
import { glob } from './glob.js' // Import glob from glob.ts
|
||||
import { sourceVariables } from './variables.js' // Import for dynamic exclusion
|
||||
import { E_Mode } from './zod_schema.js' // Import E_Mode for the check
|
||||
|
||||
/**
|
||||
* @todos
|
||||
@ -65,21 +67,48 @@ export const images = (files: string[]): ChatCompletionContentPartImage[] => {
|
||||
// glob function definition removed from here
|
||||
|
||||
export async function get(
|
||||
projectPath: string,
|
||||
projectPath: string, // This is already an absolute path from processRun/complete_messages
|
||||
include: string[] = [],
|
||||
options: IKBotTask
|
||||
): Promise<Array<IHandlerResult>> {
|
||||
const { files, webUrls } = glob(projectPath, include, options.exclude, options)
|
||||
const { files: initialAbsoluteFilePaths, webUrls } = glob(projectPath, include, options.exclude, options)
|
||||
|
||||
const fileResults = files.map((fullPath) => {
|
||||
let filesToProcess = initialAbsoluteFilePaths;
|
||||
|
||||
// --- Dynamic Exclusion based on --dst existence (for completion mode) ---
|
||||
if (options.dst && options.mode === E_Mode.COMPLETION && filesToProcess.length > 0) {
|
||||
const filesToKeepAfterDstCheck = [];
|
||||
for (const absoluteSrcFilePath of filesToProcess) {
|
||||
// No need to check fileObj.path, as these are already absolute string paths
|
||||
const fileSpecificVars = sourceVariables(absoluteSrcFilePath, projectPath)
|
||||
const fullVarsForDst = {
|
||||
...options.variables, // Global variables from complete_options
|
||||
...fileSpecificVars, // File-specific variables
|
||||
MODEL: options.model ? path.parse(options.model).name : 'unknown_model',
|
||||
ROUTER: options.router || 'unknown_router'
|
||||
}
|
||||
const potentialDstPath = path.resolve(resolvePath(options.dst, false, fullVarsForDst));
|
||||
if (exists(potentialDstPath)) {
|
||||
options.logger?.info(`Skipping source file ${path.relative(projectPath, absoluteSrcFilePath)} as output ${potentialDstPath} already exists.`);
|
||||
} else {
|
||||
filesToKeepAfterDstCheck.push(absoluteSrcFilePath);
|
||||
}
|
||||
}
|
||||
filesToProcess = filesToKeepAfterDstCheck;
|
||||
}
|
||||
// --- End Dynamic Exclusion ---
|
||||
|
||||
// Process file contents from the final list of files
|
||||
const fileResults = filesToProcess.map((fullPath) => { // fullPath is an absolute path
|
||||
try {
|
||||
const relativePath = forward_slash(path.relative(projectPath, fullPath))
|
||||
const relativePath = forward_slash(path.relative(projectPath, fullPath)) // This is correct for mime handlers and message construction
|
||||
if (isFile(fullPath) && exists(fullPath)) {
|
||||
const mimeType = lookup(fullPath) || 'text/plain'
|
||||
const mimeType = lookup(fullPath) || 'text/plain' // Use fullPath for lookup
|
||||
const handler = defaultMimeRegistry.getHandler(mimeType)
|
||||
if (handler) {
|
||||
return handler.handle(fullPath, relativePath)
|
||||
return handler.handle(fullPath, relativePath) // Pass absolute and relative paths to handler
|
||||
}
|
||||
// Fallback for text/* if specific handler not found
|
||||
return defaultMimeRegistry.getHandler('text/*')?.handle(fullPath, relativePath) || null
|
||||
}
|
||||
return null
|
||||
|
||||
@ -3,7 +3,7 @@ import { pathInfoEx } from '@polymech/commons'
|
||||
import { DEFAULT_ROOTS, DEFAULT_VARS } from '@polymech/commons'
|
||||
import { IKBotTask } from '@polymech/ai-tools'
|
||||
|
||||
export const generateSingleFileVariables = (filePath: string, projectPath: string): Record<string, string> => {
|
||||
export const sourceVariables = (filePath: string, projectPath: string): Record<string, string> => {
|
||||
const fileSpecificVariables: Record<string, string> = {};
|
||||
const srcParts = path.parse(filePath);
|
||||
|
||||
|
||||
279
packages/kbot/tests/test-data/glob/PHApp.md
Normal file
279
packages/kbot/tests/test-data/glob/PHApp.md
Normal file
@ -0,0 +1,279 @@
|
||||
#ifndef PHAPP_H
|
||||
#define PHAPP_H
|
||||
|
||||
#include "config.h"
|
||||
#include "config-modbus.h"
|
||||
#include "features.h"
|
||||
|
||||
#include <enums.h>
|
||||
#include <vector>
|
||||
#include <xmath.h>
|
||||
#include <macros.h>
|
||||
#include <App.h>
|
||||
#include <Component.h>
|
||||
#include <Bridge.h>
|
||||
#include <SerialMessage.h>
|
||||
#include <ArduinoLog.h>
|
||||
#include <Logger.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <LittleFS.h>
|
||||
#include <xstatistics.h>
|
||||
|
||||
#include <modbus/ModbusTCP.h>
|
||||
#include <modbus/ModbusTypes.h>
|
||||
#include <profiles/SignalPlot.h>
|
||||
#include <profiles/WiFiNetworkSettings.h>
|
||||
|
||||
#include <components/OmronE5.h>
|
||||
|
||||
|
||||
class POT;
|
||||
class Relay;
|
||||
class RS485;
|
||||
class Pos3Analog;
|
||||
class StatusLight;
|
||||
class RESTServer;
|
||||
class PIDController;
|
||||
class TemperatureProfile;
|
||||
class SignalPlot;
|
||||
class SAKO_VFD;
|
||||
class MB_GPIO;
|
||||
class AnalogLevelSwitch;
|
||||
class LEDFeedback;
|
||||
class Extruder;
|
||||
class Plunger;
|
||||
class Joystick;
|
||||
class PHApp;
|
||||
class AmperageBudgetManager;
|
||||
|
||||
class AsyncWebServerRequest;
|
||||
|
||||
|
||||
class PHApp : public App
|
||||
{
|
||||
public:
|
||||
//////////////////////////////////////////////////////////////
|
||||
// Enums
|
||||
//////////////////////////////////////////////////////////////
|
||||
enum CONTROLLER_STATE
|
||||
{
|
||||
E_CS_OK = 0,
|
||||
E_CS_ERROR = 10
|
||||
};
|
||||
enum APP_STATE
|
||||
{
|
||||
RESET = 0,
|
||||
EXTRUDING = 1,
|
||||
STANDBY = 2,
|
||||
ERROR = 5,
|
||||
PID_TIMEOUT = 11,
|
||||
FEED_TIMEOUT = 12,
|
||||
CONTROL_PANEL_INVALID = 13,
|
||||
PID_ERROR = 20,
|
||||
FEED_ERROR = 40,
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// Constructor / Destructor
|
||||
//////////////////////////////////////////////////////////////
|
||||
PHApp();
|
||||
~PHApp() override;
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// Core Application Logic
|
||||
//////////////////////////////////////////////////////////////
|
||||
virtual short setup();
|
||||
virtual short onRun();
|
||||
short loop() override;
|
||||
short load(short val0 = 0, short val1 = 0);
|
||||
virtual short serial_register(Bridge *bridge);
|
||||
virtual Component *byId(ushort id);
|
||||
// App States & Error Handling
|
||||
short _state;
|
||||
short _cstate;
|
||||
short _error;
|
||||
short setAppState(short newState);
|
||||
short getAppState(short val);
|
||||
short getLastError() { return _error; }
|
||||
short setLastError(short val = 0) { _error = val; return _error; }
|
||||
short onError(short id, short code);
|
||||
short clearError();
|
||||
short reset(short arg1, short arg2); // Related to resetting state?
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// Components
|
||||
//////////////////////////////////////////////////////////////
|
||||
SerialMessage *com_serial;
|
||||
POT *pot_0;
|
||||
POT *pot_1;
|
||||
POT *pot_2;
|
||||
StatusLight *statusLight_0;
|
||||
StatusLight *statusLight_1;
|
||||
Relay *relay_0;
|
||||
Relay *relay_1;
|
||||
Relay *relay_2;
|
||||
Relay *relay_3;
|
||||
Relay *relay_4;
|
||||
Relay *relay_5;
|
||||
Relay *relay_6;
|
||||
Relay *relay_7;
|
||||
Pos3Analog *pos3Analog_0;
|
||||
Pos3Analog *pos3Analog_1;
|
||||
PIDController *pidController_0;
|
||||
SAKO_VFD *vfd_0;
|
||||
Extruder *extruder_0;
|
||||
Plunger *plunger_0;
|
||||
MB_GPIO *gpio_0;
|
||||
AnalogLevelSwitch *analogLevelSwitch_0;
|
||||
LEDFeedback *ledFeedback_0;
|
||||
Joystick *joystick_0;
|
||||
AmperageBudgetManager *pidManagerAmperage;
|
||||
|
||||
// Component Callbacks/Control
|
||||
short onStop(short code = 0);
|
||||
short onWarning(short code);
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// Logging
|
||||
//////////////////////////////////////////////////////////////
|
||||
std::vector<String> logBuffer;
|
||||
size_t currentLogIndex = 0;
|
||||
CircularLogPrinter *logPrinter = nullptr;
|
||||
std::vector<String> getLogSnapshot()
|
||||
{
|
||||
std::vector<String> snapshot;
|
||||
snapshot.reserve(logBuffer.size());
|
||||
if (logBuffer.size() < LOG_BUFFER_LINES)
|
||||
{
|
||||
for (size_t i = 0; i < logBuffer.size(); ++i)
|
||||
{
|
||||
snapshot.push_back(logBuffer[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Buffer is full and circular
|
||||
size_t startIndex = (currentLogIndex + 1) % LOG_BUFFER_LINES; // <-- Note: LOG_BUFFER_LINES is now defined in Logger.h
|
||||
for (size_t i = 0; i < LOG_BUFFER_LINES; ++i)
|
||||
{
|
||||
snapshot.push_back(logBuffer[(startIndex + i) % LOG_BUFFER_LINES]);
|
||||
}
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// Network Management
|
||||
//////////////////////////////////////////////////////////////
|
||||
short setupNetwork();
|
||||
short loadNetworkSettings();
|
||||
short saveNetworkSettings(JsonObject& doc);
|
||||
WiFiNetworkSettings wifiSettings;
|
||||
//////////////////////////////////////////////////////////////
|
||||
// Modbus TCP
|
||||
//////////////////////////////////////////////////////////////
|
||||
ModbusTCP *modbusManager;
|
||||
short loopModbus();
|
||||
#ifdef ENABLE_MODBUS_TCP
|
||||
short setupModbus();
|
||||
|
||||
short mb_tcp_write(short address, short value) override;
|
||||
short mb_tcp_write(MB_Registers *reg, short networkValue) override;
|
||||
short mb_tcp_read(short address) override;
|
||||
|
||||
void mb_tcp_register(ModbusTCP *manager) const override;
|
||||
ModbusBlockView *mb_tcp_blocks() const override;
|
||||
|
||||
int client_count;
|
||||
int client_max;
|
||||
int client_total;
|
||||
millis_t client_track_ts;
|
||||
|
||||
short updateClientCount(short val0, short val1);
|
||||
short resetClientStats(short val0, short val1);
|
||||
short getClientStats(short val0, short val1);
|
||||
|
||||
// Modbus PID Specific (Conditional)
|
||||
short getConnectedClients() const; // Returns number of currently connected Modbus TCP clients
|
||||
|
||||
#ifdef ENABLE_PID
|
||||
short getPid2Register(short offset, short unused);
|
||||
short setPid2Register(short offset, short value);
|
||||
#endif // ENABLE_PID
|
||||
#endif // ENABLE_MODBUS_TCP
|
||||
RESTServer *webServer;
|
||||
short loopWeb();
|
||||
|
||||
#ifdef ENABLE_RS485
|
||||
friend class RS485Devices;
|
||||
#endif // ENABLE_RS485
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// Component Overrides / Message Handling
|
||||
/////////////////////////////////////////////////////////////
|
||||
/**
|
||||
* @brief Handles incoming messages, including RTU updates via void*.
|
||||
*/
|
||||
short onMessage(int id, E_CALLS verb, E_MessageFlags flags, void* user, Component *src) override;
|
||||
//////////////////////////////////////////////////////////////
|
||||
// Debugging & Utility Methods
|
||||
//////////////////////////////////////////////////////////////
|
||||
void printRegisters();
|
||||
short list(short val0, short val1);
|
||||
short print(short arg1, short arg2);
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// Profiling & Feature Specific (Conditional)
|
||||
//////////////////////////////////////////////////////////////
|
||||
#ifdef ENABLE_PROFILER
|
||||
static uint32_t initialFreeHeap;
|
||||
static uint64_t initialCpuTicks;
|
||||
#endif // ENABLE_PROFILER
|
||||
|
||||
#ifdef ENABLE_PROFILE_TEMPERATURE
|
||||
TemperatureProfile* tempProfiles[PROFILE_TEMPERATURE_COUNT]; // Array to hold multiple temperature profiles
|
||||
void getProfilesHandler(AsyncWebServerRequest *request);
|
||||
void setProfilesHandler(AsyncWebServerRequest *request, JsonVariant &json, int slot); // Adjusted for body handling
|
||||
bool saveProfilesToJson();
|
||||
#endif // ENABLE_PROCESS_PROFILE
|
||||
|
||||
#ifdef ENABLE_PROFILE_SIGNAL_PLOT
|
||||
SignalPlot* signalPlots[PROFILE_SIGNAL_PLOT_COUNT]; // Array to hold multiple signal plot profiles
|
||||
void getSignalPlotsHandler(AsyncWebServerRequest *request);
|
||||
void setSignalPlotsHandler(AsyncWebServerRequest *request, JsonVariant &json, int slot);
|
||||
bool saveSignalPlotsToJson();
|
||||
// Methods to control SignalPlot from TemperatureProfile
|
||||
void startSignalPlot(short slotId);
|
||||
void stopSignalPlot(short slotId);
|
||||
void enableSignalPlot(short slotId, bool enable);
|
||||
void pauseSignalPlot(short slotId);
|
||||
void resumeSignalPlot(short slotId);
|
||||
#endif // ENABLE_PROFILE_SIGNAL_PLOT
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// Web Server
|
||||
//////////////////////////////////////////////////////////////
|
||||
/**
|
||||
* @brief Register routes with the RESTServer. This will be called upon built-in RESTServer initialization.
|
||||
*
|
||||
* @param server The RESTServer instance to register routes with.
|
||||
* @return short The result of the operation.
|
||||
*/
|
||||
short registerRoutes(RESTServer *instance);
|
||||
// Network settings handlers
|
||||
#ifdef ENABLE_WEBSERVER_WIFI_SETTINGS
|
||||
void handleGetNetworkSettings(AsyncWebServerRequest *request);
|
||||
void handleSetNetworkSettings(AsyncWebServerRequest *request, JsonVariant &json);
|
||||
#endif
|
||||
void getSystemLogsHandler(AsyncWebServerRequest *request);
|
||||
void getBridgeMethodsHandler(AsyncWebServerRequest *request);
|
||||
|
||||
private:
|
||||
//////////////////////////////////////////////////////////////
|
||||
// Private Methods
|
||||
//////////////////////////////////////////////////////////////
|
||||
void handleSerialCommand(const String &command); // Moved here as it's private impl detail
|
||||
void cleanupComponents(); // Moved here as it's private impl detail
|
||||
};
|
||||
|
||||
#endif
|
||||
@ -1,11 +1,12 @@
|
||||
import * as path from 'node:path'
|
||||
import * as fsSync from 'node:fs' // For reading test file content directly
|
||||
// import * as fs from 'node:fs/promises' // No longer needed for this test
|
||||
import { describe, it, expect } from 'vitest' // Removed afterAll, beforeAll
|
||||
import { E_Mode, run, IKBotTask, complete_options, complete_messages, complete_params } from '../../src/index'
|
||||
import { E_Mode, run, IKBotTask, complete_options, complete_messages, complete_params, E_WrapMode } from '../../src/index'
|
||||
// import { LOGGING_DIRECTORY } from '../../src/constants.js' // No longer needed for this test
|
||||
// import { sync as rimrafSync } from 'rimraf' // No longer needed for this test
|
||||
|
||||
describe('globExtension with complete_params output', () => {
|
||||
describe('globExtension with collected files verification', () => {
|
||||
const testDataBaseDir = path.resolve(__dirname, '../test-data');
|
||||
const testDataRoot = path.resolve(testDataBaseDir, 'glob');
|
||||
// const defaultLogsDir = path.resolve(LOGGING_DIRECTORY); // No longer needed
|
||||
@ -29,54 +30,149 @@ describe('globExtension with complete_params output', () => {
|
||||
debug: () => {}, info: () => {}, warn: () => {}, error: () => {}, fatal: () => {},
|
||||
} as any;
|
||||
|
||||
it('should include .h, related .cpp files, and .md file using brace expansion in globExtension', async () => {
|
||||
it('should collect .h, related .cpp, and .md files when using brace expansion in globExtension', async () => {
|
||||
const initialOpts: IKBotTask = {
|
||||
path: testDataRoot,
|
||||
include: ['*.h'],
|
||||
globExtension: '${SRC_DIR}/${SRC_NAME}*.{cpp,md}',
|
||||
mode: E_Mode.COMPLETION,
|
||||
prompt: 'test-prompt-brace-expansion',
|
||||
prompt: 'test-prompt-direct-file-check',
|
||||
logger: mockLogger,
|
||||
// wrap: 'none' // Default is 'none', explicitly test this or remove for default
|
||||
};
|
||||
|
||||
// 1. Complete Options
|
||||
const completedOptions = await complete_options(initialOpts);
|
||||
expect(completedOptions).not.toBeNull();
|
||||
if (!completedOptions) return;
|
||||
if (!completedOptions) return;
|
||||
|
||||
// 2. Complete Messages
|
||||
const { messages: gatheredMessages } = await complete_messages(initialOpts, completedOptions);
|
||||
expect(gatheredMessages).toBeInstanceOf(Array);
|
||||
// 2. Complete Messages - and get the raw `files` array
|
||||
const { files: collectedFileObjects } = await complete_messages(initialOpts, completedOptions);
|
||||
expect(collectedFileObjects).toBeInstanceOf(Array);
|
||||
|
||||
// 3. Complete Params
|
||||
const finalParams = await complete_params(completedOptions, gatheredMessages);
|
||||
expect(finalParams.messages).toBeInstanceOf(Array);
|
||||
|
||||
// 4. Assert on finalParams.messages
|
||||
const collectedPathsFromFinalParams: string[] = finalParams.messages
|
||||
.filter((msg: any) => msg.role === 'user' && (typeof msg.content === 'string' || msg.path))
|
||||
.map((msg: any) => {
|
||||
const contentStr = typeof msg.content === 'string' ? msg.content : '';
|
||||
const metaPathMatch = contentStr.match(/^File: (.*)$/m);
|
||||
if (metaPathMatch && metaPathMatch[1]) {
|
||||
const metaRelPath = metaPathMatch[1];
|
||||
return path.normalize(path.resolve(testDataRoot, metaRelPath));
|
||||
}
|
||||
if (msg.path && typeof msg.path === 'string') {
|
||||
return path.normalize(path.resolve(testDataRoot, msg.path));
|
||||
// 3. Assert directly on the paths from collectedFileObjects
|
||||
// These paths should be relative to `completedOptions.path` (which is testDataRoot)
|
||||
const actualCollectedPaths: string[] = collectedFileObjects
|
||||
.map((fileObj: any) => {
|
||||
if (fileObj && typeof fileObj.path === 'string') {
|
||||
return path.normalize(path.resolve(testDataRoot, fileObj.path));
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((p: string | null) => p !== null) as string[];
|
||||
|
||||
const collectedPathsSet = new Set(collectedPathsFromFinalParams);
|
||||
const collectedPathsSet = new Set(actualCollectedPaths);
|
||||
|
||||
// console.log("Expected absolute paths:", expectedAbsoluteFilePaths);
|
||||
// console.log("Actual collected absolute paths:", actualCollectedPaths);
|
||||
|
||||
expectedAbsoluteFilePaths.forEach(expectedFile => {
|
||||
expect(collectedPathsSet.has(expectedFile), `Expected file ${path.basename(expectedFile)} (${expectedFile}) to be in finalParams.messages.`).toBe(true);
|
||||
expect(collectedPathsSet.has(expectedFile), `Expected file ${path.basename(expectedFile)} (${expectedFile}) to be collected.`).toBe(true);
|
||||
});
|
||||
|
||||
expect(collectedPathsSet.size).toBe(expectedAbsoluteFilePaths.length);
|
||||
expect(collectedPathsSet.size, "Number of unique collected files should match expected").toBe(expectedAbsoluteFilePaths.length);
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
describe('globExtension and wrap modes with complete_params output', () => {
|
||||
const testDataBaseDir = path.resolve(__dirname, '../test-data');
|
||||
const testDataRoot = path.resolve(testDataBaseDir, 'glob');
|
||||
|
||||
const expectedFileNamesDefaultTest = [
|
||||
'PHApp.h',
|
||||
'PHApp.cpp',
|
||||
'PHApp-Modbus.cpp',
|
||||
'PHApp-Profiles.cpp',
|
||||
'PHAppNetwork.cpp',
|
||||
'PHAppSettings.cpp',
|
||||
'PHAppWeb.cpp',
|
||||
'PHApp.md'
|
||||
];
|
||||
const expectedAbsoluteFilePathsDefaultTest = expectedFileNamesDefaultTest.map(f => path.normalize(path.resolve(testDataRoot, f)));
|
||||
|
||||
const mockLogger = {
|
||||
debug: () => {}, info: () => {}, warn: () => {}, error: () => {}, fatal: () => {},
|
||||
} as any;
|
||||
|
||||
it('should collect .h, related .cpp, and .md files when using brace expansion in globExtension (default wrap:none)', async () => {
|
||||
const initialOpts: IKBotTask = {
|
||||
path: testDataRoot,
|
||||
include: ['*.h'],
|
||||
globExtension: '${SRC_DIR}/${SRC_NAME}*.{cpp,md}',
|
||||
mode: E_Mode.COMPLETION,
|
||||
prompt: 'test-prompt-direct-file-check',
|
||||
logger: mockLogger,
|
||||
};
|
||||
// ... (existing test logic for wrap:none - this test remains the same)
|
||||
// 1. Complete Options
|
||||
const completedOptions = await complete_options(initialOpts);
|
||||
expect(completedOptions).not.toBeNull();
|
||||
if (!completedOptions) return;
|
||||
|
||||
// 2. Complete Messages - and get the raw `files` array
|
||||
const { files: collectedFileObjects } = await complete_messages(initialOpts, completedOptions);
|
||||
expect(collectedFileObjects).toBeInstanceOf(Array);
|
||||
|
||||
const actualCollectedPaths: string[] = collectedFileObjects
|
||||
.map((fileObj: any) => {
|
||||
if (fileObj && typeof fileObj.path === 'string') {
|
||||
return path.normalize(path.resolve(testDataRoot, fileObj.path));
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((p: string | null) => p !== null) as string[];
|
||||
|
||||
const collectedPathsSet = new Set(actualCollectedPaths);
|
||||
expectedAbsoluteFilePathsDefaultTest.forEach(expectedFile => {
|
||||
expect(collectedPathsSet.has(expectedFile), `Expected file ${path.basename(expectedFile)} (${expectedFile}) to be collected.`).toBe(true);
|
||||
});
|
||||
expect(collectedPathsSet.size, "Number of unique collected files should match expected").toBe(expectedAbsoluteFilePathsDefaultTest.length);
|
||||
}, 10000);
|
||||
|
||||
// New test case for wrap: 'meta'
|
||||
it('should correctly wrap content with metadata when options.wrap is \'meta\'', async () => {
|
||||
const targetFileName = 'PHApp.h';
|
||||
const targetFileAbsolutePath = path.normalize(path.resolve(testDataRoot, targetFileName));
|
||||
const originalFileContent = fsSync.readFileSync(targetFileAbsolutePath, 'utf-8');
|
||||
|
||||
const initialOptsMeta: IKBotTask = {
|
||||
path: testDataRoot,
|
||||
include: [targetFileName], // Focus on a single known text file
|
||||
wrap: E_WrapMode.enum.meta, // Explicitly set wrap mode to meta
|
||||
mode: E_Mode.COMPLETION,
|
||||
prompt: 'test-prompt-wrap-meta',
|
||||
logger: mockLogger,
|
||||
};
|
||||
|
||||
const completedOptionsMeta = await complete_options(initialOptsMeta);
|
||||
expect(completedOptionsMeta).not.toBeNull();
|
||||
if (!completedOptionsMeta) return;
|
||||
|
||||
const { messages: gatheredMessagesMeta } = await complete_messages(initialOptsMeta, completedOptionsMeta);
|
||||
const finalParamsMeta = await complete_params(completedOptionsMeta, gatheredMessagesMeta);
|
||||
|
||||
const targetFileMessage = finalParamsMeta.messages.find((msg: any) => {
|
||||
if (msg.role === 'user' && typeof msg.content === 'string') {
|
||||
// Check if the content contains the file path, good indicator for meta-wrapped file
|
||||
// More robust: check if msg.content includes `File: ${targetFileName}`
|
||||
return msg.content.includes(`File: ${targetFileName}`);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
expect(targetFileMessage, `Message for ${targetFileName} should be found`).toBeDefined();
|
||||
if (!targetFileMessage) return;
|
||||
|
||||
const messageContent = targetFileMessage.content as string;
|
||||
expect(messageContent).toContain(`File: ${targetFileName}`);
|
||||
expect(messageContent).toContain(`Absolute Path: ${targetFileAbsolutePath}`);
|
||||
// expect(messageContent).toContain(`CWD: ${process.cwd()}`); // CWD can vary based on test runner, more fragile
|
||||
expect(messageContent).toContain('\nOriginal Content:\n' + originalFileContent);
|
||||
expect(messageContent.startsWith('IMPORTANT: The following information is file metadata.')).toBe(true);
|
||||
expect(messageContent.includes('METADATA_START')).toBe(true);
|
||||
expect(messageContent.includes('METADATA_END')).toBe(true);
|
||||
}, 10000);
|
||||
|
||||
});
|
||||
|
||||
// To run this test, you would typically use your npm script, e.g., `npm run vi-test` or `npx vitest`
|
||||
Loading…
Reference in New Issue
Block a user