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:
@@ -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,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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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'
|
||||
Reference in New Issue
Block a user