test(infra): shellcheck не-.sh скриптов, фикс BOM, регрессионный тест RAG

- workflow shellcheck теперь покрывает агентский CGI, hotplug WAN-монитор и
  точки входа usr/bin; в CGI перед shebang стоял UTF-8 BOM, из-за которого
  Linux вообще не мог исполнить скрипт (bad interpreter) - удалён вместе со
  случайными BOM в исходниках фронтенда
- новый rag_embeddings.sh фиксирует исправленный порядок аргументов push(),
  инвалидацию кэша по отпечатку индекса, нормализацию null model_override и
  порядок объявлений - ни один из починенных багов RAG не сможет вернуться
  незаметно
- hosts_li
This commit is contained in:
Dushnilin
2026-08-21 20:58:51 +03:00
parent 0b4edc4789
commit d097494b23
24 changed files with 96 additions and 26 deletions
+9
View File
@@ -10,6 +10,9 @@ on:
- 'build.sh'
- 'install.sh'
- 'tachyon/files/etc/init.d/**'
- 'tachyon/files/usr/bin/**'
- 'tachyon/files/usr/lib/cgi-bin/**'
- 'tachyon/files/etc/hotplug.d/iface/**'
- 'luci-app-tachyon/root/etc/uci-defaults/**'
- 'tests/**/*.sh'
- '.github/workflows/shellcheck.yml'
@@ -21,6 +24,9 @@ on:
- 'build.sh'
- 'install.sh'
- 'tachyon/files/etc/init.d/**'
- 'tachyon/files/usr/bin/**'
- 'tachyon/files/usr/lib/cgi-bin/**'
- 'tachyon/files/etc/hotplug.d/iface/**'
- 'luci-app-tachyon/root/etc/uci-defaults/**'
- 'tests/**/*.sh'
- '.github/workflows/shellcheck.yml'
@@ -49,6 +55,9 @@ jobs:
find . -type f \
\( -name '*.sh' -o -path './build.sh' -o -path './install.sh' \
-o -path './tachyon/files/etc/init.d/*' \
-o -path './tachyon/files/usr/bin/*' \
-o -path './tachyon/files/usr/lib/cgi-bin/*' \
-o -path './tachyon/files/etc/hotplug.d/iface/*' \
-o -path './luci-app-tachyon/root/etc/uci-defaults/*' \) \
-print0
)
@@ -1,4 +1,4 @@
import fs from 'node:fs';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { parse } from '@babel/parser';
@@ -1,4 +1,4 @@
import { getConfigSections } from './getConfigSections';
import { getConfigSections } from './getConfigSections';
import { getDashboardSections } from './getDashboardSections';
import { getClashApiSecret } from './getClashApiSecret';
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
executeShellCommand: vi.fn(),
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
executeShellCommand: vi.fn(),
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
executeShellCommand: vi.fn(),
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
executeShellCommand: vi.fn(),
@@ -1,4 +1,4 @@
import { TabServiceInstance } from './tab.service';
import { TabServiceInstance } from './tab.service';
import { store } from './store.service';
import { logger } from './logger.service';
import { TachyonLogWatcher } from './tachyonLogWatcher.service';
@@ -1,4 +1,4 @@
import { logger } from './logger.service';
import { logger } from './logger.service';
type LogFetcher = () => Promise<string> | string;
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it } from 'vitest';
import { TachyonLogWatcher } from '../tachyonLogWatcher.service';
@@ -1,4 +1,4 @@
import { insertIf } from '../../../../helpers';
import { insertIf } from '../../../../helpers';
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
import { TachyonShellMethods } from '../../../methods';
import type { IDiagnosticsChecksItem } from '../../../services';
@@ -1,4 +1,4 @@
import { insertIf } from '../../../../helpers';
import { insertIf } from '../../../../helpers';
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
import { TachyonShellMethods, RemoteFakeIPMethods } from '../../../methods';
import type { IDiagnosticsChecksItem } from '../../../services';
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
getZapretStatus: vi.fn(),
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
getDashboardSections: vi.fn(),
@@ -1,4 +1,4 @@
import type { StoreType } from '../../services';
import type { StoreType } from '../../services';
import type { DiagnosticsProviderOptions } from './diagnostic.store';
import { DIAGNOSTICS_CHECKS } from './checks/contstants';
@@ -1,4 +1,4 @@
type LoadingActionState = {
type LoadingActionState = {
loading: boolean;
};
@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import {
clearPersistedDiagnosticRun,
@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest';
import { describe, expect, it } from 'vitest';
import {
getAvailableActionsDisabledState,
getServiceTransition,
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, it, expect } from 'vitest';
import { validateUrl } from '../validateUrl';
const validUrls = [
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/sh
#!/bin/sh
# Tachyon AI Agent API — CGI handler for uhttpd
# Install at: /usr/lib/cgi-bin/tachyon-agent
# then symlink: ln -sf /usr/lib/cgi-bin/tachyon-agent /www/cgi-bin/tachyon-agent
+5 -3
View File
@@ -65,7 +65,9 @@ fi
SPAWN_UC="$(mktemp "${TMPDIR:-/tmp}/tachyon_spawn_fds.XXXXXX")"
COUNT_OUT="$(mktemp "${TMPDIR:-/tmp}/tachyon_spawn_count.XXXXXX")"
trap 'rm -f "$SPAWN_UC" "$COUNT_OUT"' EXIT
SPAWN_CLEANUP_FILES="$SPAWN_UC $COUNT_OUT"
add_cleanup() { SPAWN_CLEANUP_FILES="$SPAWN_CLEANUP_FILES $1"; }
trap 'rm -f $SPAWN_CLEANUP_FILES' EXIT
cat > "$SPAWN_UC" <<UCODE
let fs = require("fs");
@@ -144,7 +146,7 @@ done
if [ "$closes_high" = yes ]; then
LOCK_OUT="$(mktemp "${TMPDIR:-/tmp}/tachyon_spawn_lock.XXXXXX")"
LOCK_FILE="$(mktemp "${TMPDIR:-/tmp}/tachyon_spawn_lockfile.XXXXXX")"
trap 'rm -f "$SPAWN_UC" "$COUNT_OUT" "$LOCK_OUT" "$LOCK_FILE"' EXIT
add_cleanup "$LOCK_OUT"; add_cleanup "$LOCK_FILE"
cat > "$SPAWN_UC" <<UCODE
let common = require("core.common");
@@ -165,7 +167,7 @@ fi
# and the recorded pid belongs to nothing. Checked against a real spawn: the
# process the pid names must still be alive and must be the command itself.
PID_OUT="$(mktemp "${TMPDIR:-/tmp}/tachyon_spawn_pid.XXXXXX")"
trap 'rm -f "$SPAWN_UC" "$COUNT_OUT" "$PID_OUT"' EXIT
add_cleanup "$PID_OUT"
cat > "$SPAWN_UC" <<UCODE
let common = require("core.common");
+2 -2
View File
@@ -4,9 +4,9 @@ set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HELPERS_UC="$ROOT_DIR/tachyon/files/usr/lib/core/helpers.uc"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
TMP_DIR="$ROOT_DIR/tmp_file_usable_test"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
rm -rf "$TMP_DIR"
mkdir -p "$TMP_DIR"
ucode() {
+2 -2
View File
@@ -4,11 +4,11 @@ set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HOSTS_UC="$ROOT_DIR/tachyon/files/usr/lib/components/hosts.uc"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
TMP_DIR="$ROOT_DIR/tmp_hosts_list_test"
TMP_DIR="$(mktemp -d)"
PASS=0
FAIL=0
trap 'rm -rf "$TMP_DIR"' EXIT
rm -rf "$TMP_DIR"
mkdir -p "$TMP_DIR"
ucode() {
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
export TACHYON_LIB="$TACHYON_LIB"
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
RAG_UC="$TACHYON_LIB/diagnostics/rag.uc"
RUNTIME_UC="$TACHYON_LIB/diagnostics/runtime.uc"
# 1. Regression for the inverted push(): collecting vectors from an
# OpenAI-format response must append each embedding into the result array,
# not append the (still empty) result array into each source embedding -
# which silently made RAG return "" for every primary provider.
grep -Fq 'push(embeddings, data.data[i].embedding);' "$RAG_UC" ||
fail "rag.uc must collect embeddings into the result array"
grep -Fq 'push(data.data[i].embedding, embeddings);' "$RAG_UC" &&
fail "rag.uc still contains the inverted push(embedding, result) call"
# 2. The cached embeddings are keyed by an index fingerprint so a rebuilt
# index cannot serve stale vectors forever.
grep -Fq 'cached.index_stamp == stamp' "$RAG_UC" ||
fail "rag.uc cache validation must compare the index fingerprint"
grep -Fq 'function index_stamp()' "$RAG_UC" ||
fail "rag.uc must define index_stamp()"
grep -Fq 'index_stamp: index_stamp(),' "$RAG_UC" ||
fail "rag.uc must persist the index fingerprint alongside the cache"
# 3. A null/unset model override must be normalized before use; previously a
# use-before-declaration produced model:null payloads rejected by APIs.
grep -Fq 'model_override = trim(as_string(model_override));' "$RAG_UC" ||
fail "rag.uc get_embedding_model must normalize a null model override"
# 4. runtime.uc must declare model_override before the RAG call consumes it.
rag_line="$(grep -n 'rag_context = rag.retrieve' "$RUNTIME_UC" | cut -d: -f1)"
decl_line="$(grep -n 'let model_override = trim(cfg.ai_doctor_model' "$RUNTIME_UC" | cut -d: -f1)"
[ -n "$rag_line" ] && [ -n "$decl_line" ] ||
fail "runtime.uc must contain both the rag.retrieve call and its model_override declaration"
[ "$decl_line" -lt "$rag_line" ] ||
fail "runtime.uc declares model_override AFTER rag.retrieve uses it (ucode does not hoist)"
# 5. Pin the actual push() semantics of this ucode build so the fixed code
# above stays meaningful across toolchain updates.
push_check="$(ucode -e '
let source = [[1,2],[3,4]];
let out = [];
for (let i = 0; i < length(source); i++)
push(out, source[i]);
print((length(out) == 2 && length(out[0]) == 2 && out[1][1] == 4) ? "OK" : "BAD");
' 2>/dev/null || echo BAD)"
[ "$push_check" = "OK" ] ||
fail "ucode push(array, value) did not behave as expected (saw '$push_check')"
printf 'rag embeddings checks passed\n'