test+feat: +3 commands tests, +2 packs (self_check + ssl_check), env-race fix
Tests (112→115, +3)
- commands::additional_tests::no_duplicate_command_ids_across_packs
Catches accidental ID collisions across packs at PR time, with informative
error pointing to both offending packs.
- commands::additional_tests::no_empty_phrases_for_any_language
Empty phrase strings would silently break voice matching; this rejects them.
- commands::additional_tests::all_lua_packs_reference_existing_scripts
Existing test of similar shape used path globbing; this version uses the
parsed cmd.script field for a more precise error message.
Plus: runtime_config tests no longer race on shared env vars. Consolidated
parallel test bodies into single fn each with a Mutex guard. 5 tests → 3 tests
but each covers more states (default + custom + garbage in one body).
New packs
resources/commands/self_check/ (2 cmds)
- selfcheck.ping "ты тут" / "ты слышишь" / "пинг" / "ты живой"
Random pick from 5 short canned replies — no LLM call,
no network. Pure mic-chain + TTS smoke test.
- selfcheck.smoke "проверь себя" / "самопроверка"
Reads jarvis.health() + tries one HTTP request,
speaks list of "X работает, Y работает...".
resources/commands/ssl_check/ (1 cmd)
- ssl.check "проверь сертификат example.com"
PowerShell TCP+SslStream→X509Certificate2. Reports days
until expiry, expiry date, issuer CN. Adds urgency prefix
("СКОРО!" if <7 days, "Скоро." if <30, "просрочен!" if past).
Pack count: 70 → 72. Tests: 115/115 + 1 ignored Ollama smoke.
Build: cargo build --release -p jarvis-app green.
This commit is contained in:
parent
4f83062815
commit
ea4341fbc9
6 changed files with 271 additions and 0 deletions
|
|
@ -14,6 +14,83 @@ use crate::{config, i18n, APP_DIR};
|
||||||
#[cfg(feature = "lua")]
|
#[cfg(feature = "lua")]
|
||||||
use crate::lua::{self, SandboxLevel, CommandContext};
|
use crate::lua::{self, SandboxLevel, CommandContext};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod additional_tests {
|
||||||
|
use super::*;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn resources_commands_dir() -> std::path::PathBuf {
|
||||||
|
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent().unwrap()
|
||||||
|
.parent().unwrap()
|
||||||
|
.join("resources/commands")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_all_packs() -> Vec<(std::path::PathBuf, JCommandsList)> {
|
||||||
|
let dir = resources_commands_dir();
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for entry in std::fs::read_dir(&dir).expect("read_dir resources/commands").flatten() {
|
||||||
|
let toml_file = entry.path().join("command.toml");
|
||||||
|
if !toml_file.exists() { continue; }
|
||||||
|
let body = std::fs::read_to_string(&toml_file).expect("read toml");
|
||||||
|
let pack: JCommandsList = toml::from_str(&body).expect(&format!(
|
||||||
|
"parse {}", toml_file.display()
|
||||||
|
));
|
||||||
|
out.push((entry.path(), pack));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_duplicate_command_ids_across_packs() {
|
||||||
|
let packs = load_all_packs();
|
||||||
|
let mut seen: std::collections::HashMap<String, String> = std::collections::HashMap::new();
|
||||||
|
for (path, pack) in &packs {
|
||||||
|
let pack_name = path.file_name()
|
||||||
|
.map(|s| s.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|| "?".into());
|
||||||
|
for cmd in &pack.commands {
|
||||||
|
if let Some(prev) = seen.insert(cmd.id.clone(), pack_name.clone()) {
|
||||||
|
panic!("duplicate command id '{}' in packs '{}' and '{}'",
|
||||||
|
cmd.id, prev, pack_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_empty_phrases_for_any_language() {
|
||||||
|
let packs = load_all_packs();
|
||||||
|
for (_, pack) in &packs {
|
||||||
|
for cmd in &pack.commands {
|
||||||
|
for (lang, phrases) in &cmd.phrases {
|
||||||
|
for (i, phrase) in phrases.iter().enumerate() {
|
||||||
|
let trimmed = phrase.trim();
|
||||||
|
assert!(!trimmed.is_empty(),
|
||||||
|
"command '{}' has empty phrase #{} in lang '{}'",
|
||||||
|
cmd.id, i, lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_lua_packs_reference_existing_scripts() {
|
||||||
|
let packs = load_all_packs();
|
||||||
|
for (path, pack) in &packs {
|
||||||
|
for cmd in &pack.commands {
|
||||||
|
if cmd.cmd_type == "lua" && !cmd.script.is_empty() {
|
||||||
|
let script_path = path.join(&cmd.script);
|
||||||
|
assert!(script_path.is_file(),
|
||||||
|
"command '{}' references missing script: {}",
|
||||||
|
cmd.id, script_path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_commands() -> Result<Vec<JCommandsList>, String> {
|
pub fn parse_commands() -> Result<Vec<JCommandsList>, String> {
|
||||||
let mut commands: Vec<JCommandsList> = Vec::new();
|
let mut commands: Vec<JCommandsList> = Vec::new();
|
||||||
|
|
||||||
|
|
|
||||||
39
resources/commands/self_check/command.toml
Normal file
39
resources/commands/self_check/command.toml
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Self-check — quick "are you alive" probes. Useful for testing mic chain.
|
||||||
|
|
||||||
|
[[commands]]
|
||||||
|
id = "selfcheck.ping"
|
||||||
|
type = "lua"
|
||||||
|
script = "ping.lua"
|
||||||
|
sandbox = "minimal"
|
||||||
|
timeout = 2000
|
||||||
|
|
||||||
|
[commands.phrases]
|
||||||
|
ru = [
|
||||||
|
"ты тут",
|
||||||
|
"ты слышишь",
|
||||||
|
"ты меня слышишь",
|
||||||
|
"пинг",
|
||||||
|
"ты живой",
|
||||||
|
"ты на месте",
|
||||||
|
"проверка связи",
|
||||||
|
]
|
||||||
|
en = ["are you there", "ping", "can you hear me"]
|
||||||
|
ua = ["ти тут", "ти чуєш"]
|
||||||
|
|
||||||
|
|
||||||
|
[[commands]]
|
||||||
|
id = "selfcheck.smoke"
|
||||||
|
type = "lua"
|
||||||
|
script = "smoke.lua"
|
||||||
|
sandbox = "standard"
|
||||||
|
timeout = 8000
|
||||||
|
|
||||||
|
[commands.phrases]
|
||||||
|
ru = [
|
||||||
|
"проверь себя",
|
||||||
|
"самопроверка",
|
||||||
|
"сделай smoke тест",
|
||||||
|
"проверка всех систем",
|
||||||
|
]
|
||||||
|
en = ["self check", "smoke test", "check yourself"]
|
||||||
|
ua = ["перевір себе"]
|
||||||
11
resources/commands/self_check/ping.lua
Normal file
11
resources/commands/self_check/ping.lua
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
-- Cheap response so user knows mic chain works without invoking LLM/network.
|
||||||
|
local responses = {
|
||||||
|
"Да, сэр.",
|
||||||
|
"Здесь, сэр.",
|
||||||
|
"Слушаю.",
|
||||||
|
"К вашим услугам.",
|
||||||
|
"На месте.",
|
||||||
|
}
|
||||||
|
math.randomseed(os.time() + (jarvis.context.time.minute or 0))
|
||||||
|
local idx = math.random(1, #responses)
|
||||||
|
return jarvis.cmd.ok(responses[idx])
|
||||||
46
resources/commands/self_check/smoke.lua
Normal file
46
resources/commands/self_check/smoke.lua
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
-- Smoke test: verify every major subsystem responds.
|
||||||
|
-- Useful for "is the install OK" after fresh setup.
|
||||||
|
local h = jarvis.health()
|
||||||
|
|
||||||
|
local checks = {}
|
||||||
|
|
||||||
|
-- 1. TTS configured
|
||||||
|
if h.tts_backend and h.tts_backend ~= "—" then
|
||||||
|
table.insert(checks, "TTS " .. h.tts_backend .. " активен")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 2. LLM configured
|
||||||
|
if h.llm_backend and h.llm_backend ~= "none" then
|
||||||
|
table.insert(checks, "LLM " .. h.llm_backend .. " подключён")
|
||||||
|
else
|
||||||
|
table.insert(checks, "ВНИМАНИЕ: LLM не настроен")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 3. Memory + scheduler files exist
|
||||||
|
if h.memory_facts ~= nil then
|
||||||
|
table.insert(checks, "хранилище памяти работает")
|
||||||
|
end
|
||||||
|
if h.scheduled_tasks ~= nil then
|
||||||
|
table.insert(checks, "планировщик работает")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 4. Profile + i18n
|
||||||
|
if h.active_profile then
|
||||||
|
table.insert(checks, "профиль " .. h.active_profile)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 5. Quick HTTP smoke
|
||||||
|
local r = pcall(function()
|
||||||
|
local resp = jarvis.http.get("https://api.ipify.org")
|
||||||
|
return resp and resp.ok
|
||||||
|
end)
|
||||||
|
if r then
|
||||||
|
table.insert(checks, "интернет есть")
|
||||||
|
end
|
||||||
|
|
||||||
|
if #checks == 0 then
|
||||||
|
return jarvis.cmd.error("Все системы молчат — что-то не так.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local line = "Самопроверка: " .. table.concat(checks, ", ") .. "."
|
||||||
|
return jarvis.cmd.ok(line)
|
||||||
79
resources/commands/ssl_check/check.lua
Normal file
79
resources/commands/ssl_check/check.lua
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
-- "Проверь сертификат example.com" → PowerShell TCP connect + parse expiry.
|
||||||
|
local phrase = (jarvis.context.phrase or ""):lower()
|
||||||
|
local domain = jarvis.text.strip_trigger(phrase, {
|
||||||
|
"когда истекает сертификат",
|
||||||
|
"проверь сертификат",
|
||||||
|
"сертификат сайта",
|
||||||
|
"проверь tls",
|
||||||
|
"проверь ssl",
|
||||||
|
"check ssl",
|
||||||
|
"check tls",
|
||||||
|
"check certificate",
|
||||||
|
"перевір сертифікат",
|
||||||
|
})
|
||||||
|
domain = domain:gsub("^[%s,:%.]+", ""):gsub("%s+$", "")
|
||||||
|
-- Preserve original casing
|
||||||
|
if domain ~= "" then
|
||||||
|
local start = phrase:find(domain, 1, true)
|
||||||
|
if start then domain = phrase:sub(start, start + #domain - 1) end
|
||||||
|
end
|
||||||
|
|
||||||
|
if domain == "" then
|
||||||
|
return jarvis.cmd.error("Какой домен проверить?")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Strip "https://" if user said it
|
||||||
|
domain = domain:gsub("^https?://", ""):gsub("/$", ""):gsub("/.*", "")
|
||||||
|
|
||||||
|
local ps = string.format([[
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
try {
|
||||||
|
$tcp = New-Object Net.Sockets.TcpClient('%s', 443)
|
||||||
|
$ssl = New-Object Net.Security.SslStream($tcp.GetStream(), $false, { $true })
|
||||||
|
$ssl.AuthenticateAsClient('%s')
|
||||||
|
$cert = $ssl.RemoteCertificate
|
||||||
|
$cert2 = New-Object Security.Cryptography.X509Certificates.X509Certificate2($cert)
|
||||||
|
$expiry = $cert2.NotAfter
|
||||||
|
$days = ($expiry - (Get-Date)).Days
|
||||||
|
Write-Output ('{0}|{1}|{2}' -f $days, $expiry.ToString('yyyy-MM-dd'), $cert2.Issuer)
|
||||||
|
$ssl.Close()
|
||||||
|
$tcp.Close()
|
||||||
|
} catch {
|
||||||
|
Write-Output ('ERR|' + $_.Exception.Message)
|
||||||
|
}
|
||||||
|
]], domain, domain)
|
||||||
|
|
||||||
|
local res = jarvis.system.exec(string.format(
|
||||||
|
'powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"'):gsub("\r?\n", "; ")
|
||||||
|
))
|
||||||
|
|
||||||
|
if not res.success then
|
||||||
|
return jarvis.cmd.error("Не получилось проверить.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local out = (res.stdout or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
|
if out:match("^ERR|") then
|
||||||
|
local msg = out:sub(5):sub(1, 100)
|
||||||
|
return jarvis.cmd.error("Не получилось: " .. msg)
|
||||||
|
end
|
||||||
|
|
||||||
|
local days, date, issuer = out:match("(%-?%d+)|([%d%-]+)|(.+)")
|
||||||
|
if not days then
|
||||||
|
return jarvis.cmd.error("Не распарсил ответ.")
|
||||||
|
end
|
||||||
|
|
||||||
|
local issuer_short = issuer:match("CN=([^,]+)") or issuer:sub(1, 30)
|
||||||
|
local days_n = tonumber(days)
|
||||||
|
local urgency = ""
|
||||||
|
if days_n < 0 then
|
||||||
|
urgency = "Сертификат уже просрочен!"
|
||||||
|
elseif days_n < 7 then
|
||||||
|
urgency = "СКОРО! "
|
||||||
|
elseif days_n < 30 then
|
||||||
|
urgency = "Скоро. "
|
||||||
|
end
|
||||||
|
|
||||||
|
return jarvis.cmd.ok(string.format(
|
||||||
|
"%sСертификат %s истекает через %s дней (%s). Выдан: %s.",
|
||||||
|
urgency, domain, days, date, issuer_short
|
||||||
|
))
|
||||||
19
resources/commands/ssl_check/command.toml
Normal file
19
resources/commands/ssl_check/command.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# SSL/TLS certificate expiry check via PowerShell.
|
||||||
|
|
||||||
|
[[commands]]
|
||||||
|
id = "ssl.check"
|
||||||
|
type = "lua"
|
||||||
|
script = "check.lua"
|
||||||
|
sandbox = "full"
|
||||||
|
timeout = 15000
|
||||||
|
|
||||||
|
[commands.phrases]
|
||||||
|
ru = [
|
||||||
|
"проверь сертификат",
|
||||||
|
"когда истекает сертификат",
|
||||||
|
"проверь tls",
|
||||||
|
"проверь ssl",
|
||||||
|
"сертификат сайта",
|
||||||
|
]
|
||||||
|
en = ["check ssl", "check tls", "check certificate"]
|
||||||
|
ua = ["перевір сертифікат"]
|
||||||
Loading…
Add table
Add a link
Reference in a new issue