Implements Phases 1-8 of the TFTSR implementation plan. Rust backend (Tauri 2.x, src-tauri/): - Multi-provider AI: OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama - PII detection engine: 11 regex patterns with overlap resolution - SQLCipher AES-256 encrypted database with 10 versioned migrations - 28 Tauri IPC commands for triage, analysis, document, and system ops - Ollama: hardware probe, model recommendations, pull/delete with events - RCA and blameless post-mortem Markdown document generators - PDF export via printpdf - Audit log: SHA-256 hash of every external data send - Integration stubs for Confluence, ServiceNow, Azure DevOps (v0.2) Frontend (React 18 + TypeScript + Vite, src/): - 9 pages: full triage workflow NewIssue→LogUpload→Triage→Resolution→RCA→Postmortem→History+Settings - 7 components: ChatWindow, TriageProgress, PiiDiffViewer, DocEditor, HardwareReport, ModelSelector, UI primitives - 3 Zustand stores: session, settings (persisted), history - Type-safe tauriCommands.ts matching Rust backend types exactly - 8 IT domain system prompts (Linux, Windows, Network, K8s, DB, Virt, HW, Obs) DevOps: - .woodpecker/test.yml: rustfmt, clippy, cargo test, tsc, vitest on every push - .woodpecker/release.yml: linux/amd64 + linux/arm64 builds, Gogs release upload Verified: - cargo check: zero errors - tsc --noEmit: zero errors - vitest run: 13/13 unit tests passing Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
349 lines
12 KiB
JavaScript
349 lines
12 KiB
JavaScript
// src/index.ts
|
|
import url from "node:url";
|
|
import path from "node:path";
|
|
import Mocha from "mocha";
|
|
import { handleRequires } from "mocha/lib/cli/run-helpers.js";
|
|
import logger from "@wdio/logger";
|
|
import { executeHooksWithArgs } from "@wdio/utils";
|
|
|
|
// src/common.ts
|
|
import { wrapGlobalTestMethod } from "@wdio/utils";
|
|
|
|
// src/constants.ts
|
|
var INTERFACES = {
|
|
bdd: ["it", "specify", "before", "beforeEach", "after", "afterEach"],
|
|
tdd: ["test", "suiteSetup", "setup", "suiteTeardown", "teardown"],
|
|
qunit: ["test", "before", "beforeEach", "after", "afterEach"]
|
|
};
|
|
var TEST_INTERFACES = {
|
|
bdd: ["it", "specify"],
|
|
tdd: ["test"],
|
|
qunit: ["test"]
|
|
};
|
|
var EVENTS = {
|
|
"suite": "suite:start",
|
|
"suite end": "suite:end",
|
|
"test": "test:start",
|
|
"test end": "test:end",
|
|
"hook": "hook:start",
|
|
"hook end": "hook:end",
|
|
"pass": "test:pass",
|
|
"fail": "test:fail",
|
|
"retry": "test:retry",
|
|
"pending": "test:pending"
|
|
};
|
|
var NOOP = (
|
|
/* istanbul ignore next */
|
|
function() {
|
|
}
|
|
);
|
|
var MOCHA_TIMEOUT_MESSAGE = 'For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.';
|
|
|
|
// src/common.ts
|
|
var MOCHA_UI_TYPE_EXTRACTOR = /^(?:.*-)?([^-.]+)(?:.js)?$/;
|
|
var DEFAULT_INTERFACE_TYPE = "bdd";
|
|
function formatMessage(params) {
|
|
const message = {
|
|
type: params.type
|
|
};
|
|
const payload = params.payload;
|
|
const mochaAllHooksIfPresent = payload?.title?.match(/^"(before|after)( all| each)?" hook/);
|
|
if (params.err) {
|
|
const isSkipError = /^(sync|async) skip; aborting execution$/.test(params.err.message || "");
|
|
const isHookSkip = isSkipError && mochaAllHooksIfPresent;
|
|
if (isHookSkip) {
|
|
message.type = "hook:end";
|
|
} else {
|
|
if (params.err && params.err.message && params.err.message.includes(MOCHA_TIMEOUT_MESSAGE)) {
|
|
const testName = (payload?.parent?.title ? `${payload.parent.title} ${payload.title}` : payload?.title) || "unknown test";
|
|
const replacement = `The execution in the test "${testName}" took too long. Try to reduce the run time or increase your timeout for test specs (https://webdriver.io/docs/timeouts).`;
|
|
params.err.message = params.err.message.replace(MOCHA_TIMEOUT_MESSAGE, replacement);
|
|
params.err.stack = params.err.stack.replace(MOCHA_TIMEOUT_MESSAGE, replacement);
|
|
}
|
|
message.error = {
|
|
name: params.err.name,
|
|
message: params.err.message,
|
|
stack: params.err.stack,
|
|
type: params.err.type || params.err.name,
|
|
expected: params.err.expected,
|
|
actual: params.err.actual
|
|
};
|
|
if (mochaAllHooksIfPresent) {
|
|
message.type = "hook:end";
|
|
}
|
|
}
|
|
}
|
|
if (payload) {
|
|
message.title = payload.title;
|
|
message.parent = payload.parent ? payload.parent.title : void 0;
|
|
let fullTitle = message.title;
|
|
if (payload.parent) {
|
|
let parent = payload.parent;
|
|
while (parent && parent.title) {
|
|
fullTitle = parent.title + "." + fullTitle;
|
|
parent = parent.parent;
|
|
}
|
|
}
|
|
message.fullTitle = fullTitle;
|
|
message.pending = payload.pending || false;
|
|
message.file = payload.file;
|
|
message.duration = payload.duration;
|
|
message.body = payload.body;
|
|
if (payload.ctx && payload.ctx.currentTest) {
|
|
message.currentTest = payload.ctx.currentTest.title;
|
|
}
|
|
if (params.type.match(/Test/)) {
|
|
message.passed = payload.state === "passed";
|
|
}
|
|
if (payload.parent?.title && mochaAllHooksIfPresent) {
|
|
const hookName = mochaAllHooksIfPresent[0];
|
|
message.title = `${hookName} for ${payload.parent.title}`;
|
|
}
|
|
if (payload.context) {
|
|
message.context = payload.context;
|
|
}
|
|
}
|
|
return message;
|
|
}
|
|
function requireExternalModules(mods, loader = loadModule) {
|
|
return mods.map((mod) => {
|
|
if (!mod) {
|
|
return Promise.resolve();
|
|
}
|
|
mod = mod.includes(":") ? mod.substring(mod.lastIndexOf(":") + 1) : mod;
|
|
if (mod.startsWith("./") && globalThis.process) {
|
|
mod = `${globalThis.process.cwd()}/${mod.slice(2)}`;
|
|
}
|
|
return loader(mod);
|
|
});
|
|
}
|
|
function setupEnv(cid, options, beforeTest, beforeHook, afterTest, afterHook) {
|
|
const match = MOCHA_UI_TYPE_EXTRACTOR.exec(options.ui);
|
|
const type = match && INTERFACES[match[1]] && match[1] || DEFAULT_INTERFACE_TYPE;
|
|
const hookArgsFn = (context) => {
|
|
return [{ ...context.test, parent: context.test?.parent?.title }, context];
|
|
};
|
|
INTERFACES[type].forEach((fnName) => {
|
|
const isTest = TEST_INTERFACES[type].flatMap((testCommand) => [testCommand, testCommand + ".only"]).includes(fnName);
|
|
wrapGlobalTestMethod(
|
|
isTest,
|
|
isTest ? beforeTest : beforeHook,
|
|
// @ts-ignore
|
|
hookArgsFn,
|
|
isTest ? afterTest : afterHook,
|
|
hookArgsFn,
|
|
fnName,
|
|
cid
|
|
);
|
|
});
|
|
const { compilers = [] } = options;
|
|
return requireExternalModules([...compilers]);
|
|
}
|
|
async function loadModule(name) {
|
|
try {
|
|
return await import(
|
|
/* @vite-ignore */
|
|
name
|
|
);
|
|
} catch {
|
|
throw new Error(`Module ${name} can't get loaded. Are you sure you have installed it?
|
|
Note: if you've installed WebdriverIO globally you need to install these external modules globally too!`);
|
|
}
|
|
}
|
|
|
|
// src/types.ts
|
|
import { default as default2 } from "mocha";
|
|
|
|
// src/index.ts
|
|
var log = logger("@wdio/mocha-framework");
|
|
var FILE_PROTOCOL = "file://";
|
|
var MochaAdapter = class {
|
|
constructor(_cid, _config, _specs, _capabilities, _reporter) {
|
|
this._cid = _cid;
|
|
this._config = _config;
|
|
this._specs = _specs;
|
|
this._capabilities = _capabilities;
|
|
this._reporter = _reporter;
|
|
this._config = Object.assign({
|
|
mochaOpts: {}
|
|
}, _config);
|
|
}
|
|
_mocha;
|
|
_runner;
|
|
_specLoadError;
|
|
_level = 0;
|
|
_hasTests = true;
|
|
_suiteIds = ["0"];
|
|
_suiteCnt = /* @__PURE__ */ new Map();
|
|
_hookCnt = /* @__PURE__ */ new Map();
|
|
_testCnt = /* @__PURE__ */ new Map();
|
|
_suiteStartDate = Date.now();
|
|
async init() {
|
|
const { mochaOpts } = this._config;
|
|
if (Array.isArray(mochaOpts.require)) {
|
|
const plugins = await handleRequires(
|
|
mochaOpts.require.filter((p) => typeof p === "string").map((p) => path.resolve(this._config.rootDir, p))
|
|
);
|
|
Object.assign(mochaOpts, plugins);
|
|
}
|
|
const mocha = this._mocha = new Mocha(mochaOpts);
|
|
await mocha.loadFilesAsync({
|
|
esmDecorator: (file) => `${file}?invalidateCache=${Math.random()}`
|
|
});
|
|
mocha.reporter(NOOP);
|
|
mocha.fullTrace();
|
|
this._specs.forEach((spec) => mocha.addFile(
|
|
spec.startsWith(FILE_PROTOCOL) ? url.fileURLToPath(spec) : spec
|
|
));
|
|
const { beforeTest, beforeHook, afterTest, afterHook } = this._config;
|
|
mocha.suite.on("pre-require", () => setupEnv(this._cid, this._config.mochaOpts, beforeTest, beforeHook, afterTest, afterHook));
|
|
mocha.suite.on("require", () => mocha.unloadFiles());
|
|
await this._loadFiles(mochaOpts);
|
|
return this;
|
|
}
|
|
async _loadFiles(mochaOpts) {
|
|
try {
|
|
await this._mocha.loadFilesAsync({
|
|
esmDecorator: (file) => `${file}?invalidateCache=${Math.random()}`
|
|
});
|
|
const mochaRunner = new Mocha.Runner(this._mocha.suite, { delay: false });
|
|
if (mochaOpts.grep) {
|
|
mochaRunner.grep(this._mocha.options.grep, mochaOpts.invert);
|
|
}
|
|
this._hasTests = mochaRunner.total > 0;
|
|
} catch (err) {
|
|
const error = `Unable to load spec files quite likely because they rely on \`browser\` object that is not fully initialized.
|
|
\`browser\` object has only \`capabilities\` and some flags like \`isMobile\`.
|
|
Helper files that use other \`browser\` commands have to be moved to \`before\` hook.
|
|
Spec file(s): ${this._specs.join(",")}
|
|
Error: ${err.stack}`;
|
|
this._specLoadError = new Error(error);
|
|
log.warn(error);
|
|
}
|
|
}
|
|
hasTests() {
|
|
return this._hasTests;
|
|
}
|
|
async run() {
|
|
const mocha = this._mocha;
|
|
let runtimeError;
|
|
const result = await new Promise((resolve) => {
|
|
try {
|
|
this._runner = mocha.run(resolve);
|
|
} catch (err) {
|
|
runtimeError = err;
|
|
return resolve(1);
|
|
}
|
|
Object.keys(EVENTS).forEach((e) => this._runner.on(e, this.emit.bind(this, EVENTS[e])));
|
|
this._runner.suite.beforeAll(this.wrapHook("beforeSuite"));
|
|
this._runner.suite.afterAll(this.wrapHook("afterSuite"));
|
|
});
|
|
await executeHooksWithArgs("after", this._config.after, [runtimeError || this._specLoadError || result, this._capabilities, this._specs]);
|
|
if (runtimeError || this._specLoadError) {
|
|
throw runtimeError || this._specLoadError;
|
|
}
|
|
return result;
|
|
}
|
|
/**
|
|
* Hooks which are added as true Mocha hooks need to call done() to notify async
|
|
*/
|
|
wrapHook(hookName) {
|
|
return () => executeHooksWithArgs(
|
|
hookName,
|
|
this._config[hookName],
|
|
[this.prepareMessage(hookName)]
|
|
).catch((e) => {
|
|
log.error(`Error in ${hookName} hook: ${e.stack.slice(7)}`);
|
|
});
|
|
}
|
|
prepareMessage(hookName) {
|
|
const params = { type: hookName };
|
|
switch (hookName) {
|
|
case "beforeSuite":
|
|
this._suiteStartDate = Date.now();
|
|
params.payload = this._runner?.suite.suites[0];
|
|
break;
|
|
case "afterSuite":
|
|
params.payload = this._runner?.suite.suites[0];
|
|
if (params.payload) {
|
|
params.payload.duration = params.payload.duration || Date.now() - this._suiteStartDate;
|
|
}
|
|
break;
|
|
case "beforeTest":
|
|
case "afterTest":
|
|
params.payload = this._runner?.test;
|
|
break;
|
|
}
|
|
return formatMessage(params);
|
|
}
|
|
emit(event, payload, err) {
|
|
if (payload.root) {
|
|
return;
|
|
}
|
|
const message = formatMessage({ type: event, payload, err });
|
|
message.cid = this._cid;
|
|
message.specs = this._specs;
|
|
message.uid = this.getUID(message);
|
|
this._reporter.emit(message.type, message);
|
|
}
|
|
getSyncEventIdStart(type) {
|
|
const prop = `_${type}Cnt`;
|
|
const suiteId = this._suiteIds[this._suiteIds.length - 1];
|
|
const cnt = this[prop].has(suiteId) ? this[prop].get(suiteId) || 0 : 0;
|
|
this[prop].set(suiteId, cnt + 1);
|
|
return `${type}-${suiteId}-${cnt}`;
|
|
}
|
|
getSyncEventIdEnd(type) {
|
|
const prop = `_${type}Cnt`;
|
|
const suiteId = this._suiteIds[this._suiteIds.length - 1];
|
|
const cnt = this[prop].get(suiteId) - 1;
|
|
return `${type}-${suiteId}-${cnt}`;
|
|
}
|
|
getUID(message) {
|
|
if (message.type === "suite:start") {
|
|
const suiteCnt = this._suiteCnt.has(this._level.toString()) ? this._suiteCnt.get(this._level.toString()) : 0;
|
|
const suiteId = `suite-${this._level}-${suiteCnt}`;
|
|
if (this._suiteCnt.has(this._level.toString())) {
|
|
this._suiteCnt.set(this._level.toString(), this._suiteCnt.get(this._level.toString()) + 1);
|
|
} else {
|
|
this._suiteCnt.set(this._level.toString(), 1);
|
|
}
|
|
this._suiteIds.push(`${this._level}${suiteCnt}`);
|
|
this._level++;
|
|
return suiteId;
|
|
}
|
|
if (message.type === "suite:end") {
|
|
this._level--;
|
|
const suiteCnt = this._suiteCnt.get(this._level.toString()) - 1;
|
|
const suiteId = `suite-${this._level}-${suiteCnt}`;
|
|
this._suiteIds.pop();
|
|
return suiteId;
|
|
}
|
|
if (message.type === "hook:start") {
|
|
return this.getSyncEventIdStart("hook");
|
|
}
|
|
if (message.type === "hook:end") {
|
|
return this.getSyncEventIdEnd("hook");
|
|
}
|
|
if (["test:start", "test:pending"].includes(message.type)) {
|
|
return this.getSyncEventIdStart("test");
|
|
}
|
|
if (["test:end", "test:pass", "test:fail", "test:retry"].includes(message.type)) {
|
|
return this.getSyncEventIdEnd("test");
|
|
}
|
|
throw new Error(`Unknown message type : ${message.type}`);
|
|
}
|
|
};
|
|
var adapterFactory = {};
|
|
adapterFactory.init = async function(...args) {
|
|
const adapter = new MochaAdapter(...args);
|
|
const instance = await adapter.init();
|
|
return instance;
|
|
};
|
|
var index_default = adapterFactory;
|
|
export {
|
|
MochaAdapter,
|
|
adapterFactory,
|
|
index_default as default
|
|
};
|