mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-20 18:11:00 +00:00
refactor(ci): move the bot's repository briefing into versioned files a test pins
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
# Repository context for the Claude bot
|
||||
|
||||
Shared briefing for every job in `.github/workflows/claude-bot.yml`. It exists so
|
||||
these facts live in ONE place next to the code instead of being restated in five
|
||||
prompts, where they went stale silently.
|
||||
|
||||
**Read this from the workspace checkout, which is the base revision and is
|
||||
trusted. NEVER read it from `/tmp/head`** — a pull request controls that tree,
|
||||
and a fork that could supply this file could rewrite the rules it carries.
|
||||
|
||||
`CLAUDE.md`, `frontend/CLAUDE.md` and `docs/architecture.md` outrank this file.
|
||||
Where they disagree with it, they win and this file is the thing to fix.
|
||||
`docs/architecture.md` carries a "Symptom -> File" index and the cron-job table,
|
||||
which answer "which file owns X" in one hop; grepping blind wastes turns on a
|
||||
question it already answers.
|
||||
|
||||
## Stack
|
||||
|
||||
3x-ui is an open-source web control panel for managing Xray-core servers.
|
||||
|
||||
- Backend: Go 1.26, module `github.com/mhsanaei/3x-ui/v3`, Gin and GORM.
|
||||
- It runs Xray-core as a managed child process (`internal/xray/process.go`) and
|
||||
imports `github.com/xtls/xray-core` for config types and the gRPC
|
||||
stats/handler/router API. The release the panel BUNDLES is pinned in
|
||||
`DockerInit.sh`; the version it COMPILES against is pinned in `go.mod`, and
|
||||
the two are not always the same.
|
||||
- MTProto inbounds run a SECOND managed child, the `mtg-multi` binary (a
|
||||
multi-secret mtg fork, panel-side code in `internal/mtproto/`), one process
|
||||
per inbound. Client, ad-tag and quota/expiry edits are hot-applied through the
|
||||
fork's management API (`PUT /secrets`) so connections survive, with a process
|
||||
restart as the fallback on older binaries.
|
||||
- Storage: SQLite by default (`/etc/x-ui/x-ui.db` on Linux, the executable
|
||||
directory on Windows) or PostgreSQL (`XUI_DB_TYPE` / `XUI_DB_DSN`). The SQLite
|
||||
driver is CGo, so `CGO_ENABLED=0` builds fail.
|
||||
- Frontend: React 19 + Ant Design 6 + Vite 8 + TypeScript in `frontend/`, built
|
||||
into `internal/web/dist/` (gitignored) and embedded with `embed.FS`.
|
||||
|
||||
## Where things live
|
||||
|
||||
| area | path |
|
||||
| --- | --- |
|
||||
| entry point + `x-ui` CLI | `main.go` |
|
||||
| env parsing | `internal/config/` |
|
||||
| schema, migrations | `internal/database/`, `internal/database/model/` |
|
||||
| Xray child process + config | `internal/xray/` |
|
||||
| MTProto inbounds | `internal/mtproto/` |
|
||||
| subscription server | `internal/sub/` |
|
||||
| HTTP handlers | `internal/web/controller/` |
|
||||
| business logic | `internal/web/service/` |
|
||||
| cron jobs (schedules in `web.go startTask()`) | `internal/web/job/` |
|
||||
| master/sub-node over mTLS | `internal/web/runtime/` |
|
||||
| i18n | `internal/web/locale/`, `internal/web/translation/` |
|
||||
| UI source | `frontend/src/` |
|
||||
| install / upgrade | `install.sh`, `x-ui.sh`, `DockerInit.sh` |
|
||||
|
||||
## Hard rules a change must respect
|
||||
|
||||
- **Dispatch through `runtime.Runtime`.** Every state-changing inbound or client
|
||||
operation goes through the interface in `internal/web/runtime/`, never
|
||||
straight to `internal/xray/api.go`. A direct call passes every local test and
|
||||
silently breaks every multi-node deployment; it is invisible in a single-box
|
||||
reading of a diff.
|
||||
- **Layering.** Controllers are thin — bind, validate, respond — with no GORM
|
||||
queries, no Xray calls and no business rules. `internal/util/*` is leaf-only
|
||||
and must not import service, controller or database. `internal/web/dist/` and
|
||||
`frontend/src/generated/` are generated; a hand-edit is a violation.
|
||||
- **Comments in committed Go/TS/TSX: 2 lines MAX per block**, spent on the *why*
|
||||
a name cannot hold — an invariant, an issue number, a non-obvious constraint.
|
||||
Exempt, never flag: `//go:build`, `//go:generate`, `//nolint:`,
|
||||
`// Code generated ... DO NOT EDIT.`. HTML `<!-- -->` is fine.
|
||||
- **The route contract chain**, which breaks in four distinct places:
|
||||
1. a new `g.POST`/`g.GET` in `internal/web/controller/` needs a matching entry
|
||||
in `frontend/src/pages/api-docs/endpoints.ts` — pinned BOTH ways by
|
||||
`TestRouteRegistryContract` in `internal/web/routes_contract_test.go`, so a
|
||||
renamed or removed route that leaves a stale entry fails too;
|
||||
2. generated artefacts must be regenerated with `make gen`, or CI's `codegen`
|
||||
job fails on a dirty `frontend/src/generated` or
|
||||
`frontend/public/openapi.json`;
|
||||
3. a NEW struct crossing the API boundary must be added to the `StructAllow`
|
||||
allowlist in `tools/openapigen/main.go`, or it is SILENTLY dropped from the
|
||||
schemas and `frontend/scripts/build-openapi.mjs` then fails — a guaranteed
|
||||
CI break, not a style nit;
|
||||
4. the step NOTHING checks — `frontend/public/openapi.json` must be copied to
|
||||
`docs/public/openapi.json` and the MDX regenerated with
|
||||
`cd docs && pnpm gen:api`, because `docs-ci.yml` fires only on `docs/**`.
|
||||
Step 4 is the one that reaches production wrong.
|
||||
- **i18n.** A new English key goes in EVERY locale JSON in
|
||||
`internal/web/translation/` (13 files) AND must be referenced from
|
||||
`frontend/src` or Go in the SAME change.
|
||||
`frontend/src/test/i18n-dead-keys.test.ts` fails on a missing locale file and
|
||||
on an orphan key alike.
|
||||
- **Migrations.** Schema changes are GORM `AutoMigrate` PLUS hand-written
|
||||
migrations in `internal/database/db.go`. There are no migration files and no
|
||||
down-migrations, and everything has to work on SQLite AND PostgreSQL.
|
||||
- **Tests.** Stdlib `testing` only (no testify), table-driven with `t.Run`
|
||||
subtests and `t.Helper()` on helpers. An assertion must pin the exact value,
|
||||
typed error or emitted string — `err != nil` and `len(x) > 0` are findings,
|
||||
not nits. Prefer real dependencies: a throwaway DB via
|
||||
`database.InitDB(filepath.Join(t.TempDir(), "x-ui.db"))` with `t.Cleanup`, and
|
||||
`httptest` for HTTP. `internal/sub`'s `initSubDB(t)` is the template.
|
||||
A test must FAIL without its fix; one that passes either way certifies
|
||||
nothing and then gets cited as proof the fix works.
|
||||
|
||||
## The three link implementations
|
||||
|
||||
Link and subscription generation is implemented three times, independently:
|
||||
|
||||
| language | path | what it feeds |
|
||||
| --- | --- | --- |
|
||||
| Go | `internal/util/link/`, `internal/sub/` | what the panel serves |
|
||||
| TS | `frontend/src/lib/xray/` | what the panel UI shows |
|
||||
| TS | `docs/lib/xray/` | what the docs site shows |
|
||||
|
||||
A change to share-link or subscription output that touches one and not the
|
||||
others is how they drift apart.
|
||||
|
||||
## Downstream programs that must accept what the panel emits
|
||||
|
||||
- **XTLS/Xray-core** — the Xray config the panel generates, and the VLESS/VMess
|
||||
transport and security fields.
|
||||
- **MetaCubeX/mihomo** — consumes the Clash YAML from `internal/sub/`.
|
||||
- **SagerNet/sing-box** — parses the share links the panel emits.
|
||||
- **mhsanaei/mtg-multi** — the MTProto sidecar whose TOML (`[secrets]`,
|
||||
`[secret-ad-tags]`, `[secret-limits]`) and management API
|
||||
(`PUT /secrets`, `POST /secrets/{name}/reset-quota`) `internal/mtproto/`
|
||||
writes and calls.
|
||||
|
||||
## What CI runs
|
||||
|
||||
`.github/workflows/ci.yml`, on every pull request touching Go or frontend code.
|
||||
It is paths-filtered, so a docs-only or workflow-only change produces no run.
|
||||
|
||||
| job | what it proves |
|
||||
| --- | --- |
|
||||
| `go-test` | `go test -shuffle=on -count=1` over every package except `frontend/node_modules` |
|
||||
| `race` | the same set under `-race -shuffle=on` |
|
||||
| `postgres-durable-first` | live PostgreSQL 16: the `PostgresCommitFailure` tests plus `TestHostAutoMigrateCreatesColumns_Postgres` and `TestMigrate_Postgres`. Both steps COUNT passes rather than assert on SKIP, so a renamed or deleted test fails the job |
|
||||
| `govulncheck` | known vulnerabilities |
|
||||
| `golangci` | `golangci-lint` |
|
||||
| `fuzz-smoke` | 30s each on `FuzzParseLink` and `FuzzDecodeCertPin` |
|
||||
| `codegen` | `npm run gen` then `git diff --exit-code` on the generated files |
|
||||
| `frontend` | MSW worker drift, lint, format:check, typecheck, `npm test` (Vitest + headless-Chromium Storybook), build, build-storybook, `npm audit` |
|
||||
|
||||
**What CI does NOT prove.** These test families `t.Skip` unless an environment
|
||||
variable is set, and CI sets only the PostgreSQL ones above:
|
||||
|
||||
| gate | covers |
|
||||
| --- | --- |
|
||||
| `XUI_TEST_PG_DSN` | PostgreSQL-specific paths |
|
||||
| `XUI_DB_TYPE` + `XUI_DB_DSN` | dialect-dependent behaviour |
|
||||
| `XRAY_E2E_BINARY` | the Xray gRPC end-to-end tests in `internal/xray/` |
|
||||
| `XUI_SCALE_TEST` | scale tests in `internal/sub/`, `internal/web/job/`, `internal/web/service/` |
|
||||
|
||||
Mutation testing (`mutation.yml`) runs nightly and never on a pull request, so a
|
||||
test that cannot fail is invisible to CI. `make verify` is the local gate.
|
||||
|
||||
## Support facts reporters get wrong
|
||||
|
||||
- Linux install: `bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)`
|
||||
- Install generates a RANDOM username, password and web base path — never
|
||||
admin/admin. The `x-ui` menu on the server shows or resets them.
|
||||
- The installer service environment file is DISTRO-DEPENDENT:
|
||||
`/etc/default/x-ui` (Debian/Ubuntu), `/etc/conf.d/x-ui` (Arch),
|
||||
`/etc/sysconfig/x-ui` (RHEL/Fedora). Naming the wrong one means the reporter's
|
||||
edit is silently never read by systemd — a common cause of "I set the variable
|
||||
and nothing happened".
|
||||
- Windows is supported. There the database sits next to the executable, not in
|
||||
`/etc` — never quote the Linux path to a Windows user.
|
||||
- SQLite to PostgreSQL: `x-ui migrate-db --dsn "postgres://..."`, then set
|
||||
`XUI_DB_TYPE`/`XUI_DB_DSN` in that file and `systemctl restart x-ui`. The
|
||||
source SQLite file is left in place.
|
||||
- Docker image `ghcr.io/mhsanaei/3x-ui`; PostgreSQL profile
|
||||
`docker compose --profile postgres up -d`. Fail2ban IP-limit enforcement needs
|
||||
`NET_ADMIN` + `NET_RAW` (compose grants them; a bare `docker run` must add
|
||||
`--cap-add=NET_ADMIN --cap-add=NET_RAW`).
|
||||
- Never state that a `XUI_*` variable does not exist without grepping
|
||||
`internal/config/` and `internal/tunnelmonitor/` first. The
|
||||
`XUI_TUNNEL_HEALTH_*` family is the usual answer to "the panel restarts Xray
|
||||
every few minutes".
|
||||
- Security per inbound is none / tls / reality. XTLS is a VLESS *flow*
|
||||
(`xtls-rprx-vision`), not a security setting — never tell anyone to pick XTLS
|
||||
in the security dropdown.
|
||||
- Never hardcode a version. For "is this already fixed" use
|
||||
`gh release list -L 10`, `gh search commits`, and `git log -S`.
|
||||
@@ -0,0 +1,93 @@
|
||||
# Review rubric and lane map
|
||||
|
||||
Shared by the four pull-request review lanes in
|
||||
`.github/workflows/claude-bot.yml`. The lane map below is here so it exists
|
||||
ONCE: when each lane carried its own copy of "mine / not mine", the four copies
|
||||
could quietly contradict each other and the same defect got reported twice or
|
||||
not at all.
|
||||
|
||||
**Read this from the workspace checkout, which is the base revision and is
|
||||
trusted. NEVER read it from `/tmp/head`** — a pull request controls that tree,
|
||||
and a fork that could supply this file could rewrite the rubric it is judged by.
|
||||
|
||||
## Lane map — who owns what
|
||||
|
||||
Ownership is decided by WHAT YOU WOULD HAVE TO BE RIGHT ABOUT for the finding to
|
||||
be true, not by how bad the consequence would be.
|
||||
|
||||
| lane | owns |
|
||||
| --- | --- |
|
||||
| **Senior Developer** | Correctness, edge cases, nil and empty handling, regressions. Layering and the `runtime.Runtime` dispatch rule. Security in code: authn/authz, input validation, injection, XSS, CSRF, SSRF, path traversal, secrets, unsafe defaults — weighted at `internal/web/controller/`, session and middleware, the PUBLIC `internal/sub/` surface, and Xray config generation. Concurrency: races, deadlocks, goroutine and task leaks around the Xray and mtg-multi children, the cron jobs, the eventbus, the websockets. Performance. Maintainability and the 2-line comment cap. Frontend code quality. **Every client-facing field name, encoding and hash choice** the change emits. |
|
||||
| **Senior QA** | `internal/database/**`, `internal/database/model/**`, `internal/config/`, `internal/web/translation/**`, `tools/openapigen/`, `frontend/src/pages/api-docs/endpoints.ts`, `.github/workflows/**`, `Dockerfile*`, `docker-compose.yml`, `install.sh`, `x-ui.sh`, `DockerInit.sh`, `Makefile`, `CLAUDE.md`, `frontend/CLAUDE.md`, `docs/**`, `README*`, `SECURITY.md`. Plus intent, upgrade safety, blast radius, backward compatibility of those contracts, operational impact, and labels. |
|
||||
| **Senior Tester** | Test quality and coverage, what CI proved and what it did not, weak assertions, vacuous tests, snapshot and golden-fixture abuse. |
|
||||
| **Arbiter** | Reconciliation, upstream wire-format resolution, and divergence BETWEEN the three link implementations. |
|
||||
|
||||
### Boundaries that are easy to get wrong
|
||||
|
||||
- **Field names are the Developer's, never QA's** — a config key, JSON tag, URI
|
||||
parameter, YAML key, TOML key, value encoding, hash choice, or which of two
|
||||
variables a field is populated from. However large the blast radius. If your
|
||||
finding is only true when one of those is wrong, it is the Developer's.
|
||||
- **QA outside its own files** may report exactly ONE thing: *a configuration
|
||||
that works on the base branch today behaves differently after this ships, with
|
||||
no operator action* — and only when it can state (a) the concrete existing
|
||||
configuration, (b) what it does today, (c) what it does after. Otherwise drop
|
||||
it; the Developer has it.
|
||||
- **Destroying data IS QA's**, even outside its files: regenerating a live key or
|
||||
UUID, overwriting a stored secret, resetting a traffic counter or expiry. That
|
||||
is blast radius, not correctness.
|
||||
- **`docs/lib/xray/`**: QA reports the process omission ("it was not updated").
|
||||
The Arbiter reports semantic divergence between the three implementations. The
|
||||
Developer reports whether the one in front of it emits the right thing.
|
||||
- **The Tester never** opines on architecture, naming or what the code emits,
|
||||
and never restates a green CI job as a finding.
|
||||
|
||||
## Severity — exactly one per finding, plain text, no emoji
|
||||
|
||||
| level | means |
|
||||
| --- | --- |
|
||||
| Critical | security hole, data corruption or loss, crash, privilege escalation, authentication bypass, unrecoverable migration, or a fleet-wide outage path |
|
||||
| High | likely production bug, incorrect behaviour on a common path, a breaking API or subscription-format change, a missing migration, a guaranteed CI break, or a significant performance problem |
|
||||
| Medium | missing validation, an unhandled edge case, an undeclared behaviour change, documentation or OpenAPI drift, a maintainability problem, or an untested new code path |
|
||||
| Low | minor readability, consistency, operational or documentation improvement |
|
||||
| Suggestion | optional improvement with no correctness or release impact |
|
||||
|
||||
## Confidence — exactly one per finding
|
||||
|
||||
High, Medium, or Low. Reserve **High** for something CONFIRMED in the source and
|
||||
citable as `file:line`, or observed in real command output. Anything inferred,
|
||||
or resting on a detail you could not check, is Medium or Low.
|
||||
|
||||
## Verdict — exactly one
|
||||
|
||||
`Approve`, `Comment`, or `Request changes`.
|
||||
|
||||
## Finding block
|
||||
|
||||
Fields on their own lines:
|
||||
|
||||
```
|
||||
Severity / Confidence / Category
|
||||
Location: file:line as plain text, not a Markdown link
|
||||
Problem: what is wrong
|
||||
Why it matters: the practical runtime, security, operational or upgrade impact
|
||||
Recommendation: the preferred fix
|
||||
```
|
||||
|
||||
The Tester replaces `Why it matters` with `Evidence`: the command or CI job and
|
||||
the real output it read. A code example is optional and, if included, must be a
|
||||
plain fenced code block — never a ```suggestion``` block, since the Arbiter
|
||||
republishes the text.
|
||||
|
||||
## Reporting discipline
|
||||
|
||||
- Report every problem, including Low and Suggestion. Never drop a finding
|
||||
because you are unsure: report it at `Confidence: Low` and say what would
|
||||
confirm it. Severity and confidence ARE the filter.
|
||||
- Dropping a finding because it is not YOURS is different, and is exactly what
|
||||
the lane map asks for. A duplicate only costs the Arbiter a merge.
|
||||
- Do not report the same issue twice, do not bikeshed style, and ignore
|
||||
pure-formatting changes unless they reduce readability. Ignore lock files and
|
||||
true vendor code; do NOT ignore test fixtures or generated files.
|
||||
- If the diff is too large to cover completely, say so and name the files you
|
||||
did NOT review. A truncated review that does not admit it is worse than none.
|
||||
+1946
-788
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,150 @@
|
||||
package main
|
||||
|
||||
// The Claude bot prompts in .github/workflows/claude-bot.yml no longer restate
|
||||
// repository facts; they read .github/claude/repo-context.md instead. A stale
|
||||
// claim in that file is invisible until it produces a wrong review, so every
|
||||
// claim a machine can check is pinned here.
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
botContextPath = ".github/claude/repo-context.md"
|
||||
botRubricPath = ".github/claude/review-rubric.md"
|
||||
ciWorkflowPath = ".github/workflows/ci.yml"
|
||||
)
|
||||
|
||||
func readRepoFile(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// section returns the text between two markers, so a table is matched only
|
||||
// inside the heading that owns it.
|
||||
func section(t *testing.T, doc, from, to string) string {
|
||||
t.Helper()
|
||||
i := strings.Index(doc, from)
|
||||
if i < 0 {
|
||||
t.Fatalf("%s no longer contains the heading %q", botContextPath, from)
|
||||
}
|
||||
rest := doc[i+len(from):]
|
||||
if j := strings.Index(rest, to); j >= 0 {
|
||||
return rest[:j]
|
||||
}
|
||||
return rest
|
||||
}
|
||||
|
||||
func TestBotContextLocaleFileCount(t *testing.T) {
|
||||
doc := readRepoFile(t, botContextPath)
|
||||
m := regexp.MustCompile("`internal/web/translation/` \\((\\d+) files\\)").FindStringSubmatch(doc)
|
||||
if m == nil {
|
||||
t.Fatalf("%s no longer states the locale file count as \"`internal/web/translation/` (N files)\"", botContextPath)
|
||||
}
|
||||
files, err := filepath.Glob("internal/web/translation/*.json")
|
||||
if err != nil {
|
||||
t.Fatalf("glob locales: %v", err)
|
||||
}
|
||||
if got := len(files); m[1] != itoa(got) {
|
||||
t.Errorf("%s claims %s locale files, internal/web/translation/ holds %d; update the claim and every prompt that relies on it", botContextPath, m[1], got)
|
||||
}
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
var b []byte
|
||||
for n > 0 {
|
||||
b = append([]byte{byte('0' + n%10)}, b...)
|
||||
n /= 10
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func TestBotContextNamesRealCIJobs(t *testing.T) {
|
||||
doc := readRepoFile(t, botContextPath)
|
||||
ci := readRepoFile(t, ciWorkflowPath)
|
||||
table := section(t, doc, "## What CI runs", "**What CI does NOT prove.**")
|
||||
rows := regexp.MustCompile("(?m)^\\| `([a-z0-9-]+)` \\|").FindAllStringSubmatch(table, -1)
|
||||
if len(rows) < 5 {
|
||||
t.Fatalf("expected the CI table in %s to list at least 5 jobs, found %d", botContextPath, len(rows))
|
||||
}
|
||||
for _, r := range rows {
|
||||
t.Run(r[1], func(t *testing.T) {
|
||||
if !strings.Contains(ci, "\n "+r[1]+":\n") {
|
||||
t.Errorf("%s describes a CI job %q that %s does not define", botContextPath, r[1], ciWorkflowPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBotContextNamesRealPaths(t *testing.T) {
|
||||
doc := readRepoFile(t, botContextPath) + readRepoFile(t, botRubricPath)
|
||||
// internal/web/dist and frontend/node_modules are build output: absent from a
|
||||
// fresh clone, created by `make dist-stub` and `npm ci`.
|
||||
generated := map[string]bool{
|
||||
"internal/web/dist/": true,
|
||||
"frontend/node_modules": true,
|
||||
"frontend/src/generated/": true,
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, m := range regexp.MustCompile("`([^`]+)`").FindAllStringSubmatch(doc, -1) {
|
||||
p := m[1]
|
||||
if !regexp.MustCompile(`^(internal|frontend|docs|tools|\.github)/`).MatchString(p) ||
|
||||
strings.ContainsAny(p, "*{ ") || generated[p] || seen[p] {
|
||||
continue
|
||||
}
|
||||
seen[p] = true
|
||||
t.Run(p, func(t *testing.T) {
|
||||
if _, err := os.Stat(strings.TrimSuffix(p, "/")); err != nil {
|
||||
t.Errorf("%s names %q, which does not exist; the bot prompts trust this file", botContextPath, p)
|
||||
}
|
||||
})
|
||||
}
|
||||
if len(seen) < 20 {
|
||||
t.Errorf("expected the bot context to name at least 20 repository paths, found %d - has the file been gutted?", len(seen))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBotContextSkipGatesExist(t *testing.T) {
|
||||
doc := readRepoFile(t, botContextPath)
|
||||
table := section(t, doc, "**What CI does NOT prove.**", "Mutation testing")
|
||||
// [A-Z0-9_] and not [A-Z_]: XRAY_E2E_BINARY carries a digit, and excluding it
|
||||
// silently dropped that gate from the check instead of failing.
|
||||
gates := regexp.MustCompile("`((?:XUI|XRAY)_[A-Z0-9_]+)`").FindAllStringSubmatch(table, -1)
|
||||
if len(gates) < 5 {
|
||||
t.Fatalf("expected at least 5 skip-gate variables in %s, found %d", botContextPath, len(gates))
|
||||
}
|
||||
var sources []string
|
||||
err := filepath.WalkDir("internal", func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() && strings.HasSuffix(path, ".go") {
|
||||
sources = append(sources, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("walk internal: %v", err)
|
||||
}
|
||||
for _, g := range gates {
|
||||
t.Run(g[1], func(t *testing.T) {
|
||||
for _, f := range sources {
|
||||
if strings.Contains(readRepoFile(t, f), g[1]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("%s lists %s as a test skip gate, but no .go file under internal/ reads it", botContextPath, g[1])
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user