mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-15 04:30:55 +08:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
430950570c | ||
|
|
f8bc149872 | ||
|
|
f3a8e82d29 | ||
|
|
e300f85987 | ||
|
|
dfa0e8a566 | ||
|
|
9b20a46eba | ||
|
|
8c8ae97400 | ||
|
|
149151919f | ||
|
|
5cdb720122 | ||
|
|
ae792126dc | ||
|
|
622b7490e0 | ||
|
|
b6e863c746 | ||
|
|
33078a7d35 |
@@ -45,8 +45,9 @@ jobs:
|
||||
# keeps the database state deterministic.
|
||||
run: go test -p 1 ./...
|
||||
- name: go test -race
|
||||
# Race detector on the packages that pass under -race today. ./server is
|
||||
# left out until it has been checked with -race against postgres.
|
||||
# Race detector on the packages that pass under -race today. ./server
|
||||
# joined once its suite was checked with -race against postgres and its
|
||||
# test teardown fixed (#227, #228).
|
||||
# Its own database: the plain run above leaves rows behind and the db tests
|
||||
# are not idempotent across runs (duplicate llm_profiles names).
|
||||
env:
|
||||
@@ -54,7 +55,8 @@ jobs:
|
||||
|
||||
run: >-
|
||||
go test -race -p 1 ./agent ./cmd/artex ./config ./db ./evidence ./guard
|
||||
./intercept ./llmpool ./llmrec ./mcphttp ./notify ./sidequestion ./traffic
|
||||
./intercept ./llmpool ./llmrec ./mcphttp ./notify ./server ./sidequestion
|
||||
./traffic
|
||||
|
||||
shell:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- **Untrusted-output boundary is now on by default**: Norma frames tool results for the worker, planner, chat and main agent unless `ARTEX_UNTRUSTED_BOUNDARY` is set to `0`, `false`, `off` or `no`. It was opt-in via `ARTEX_UNTRUSTED_BOUNDARY=1` in 0.6.0 (#166).
|
||||
|
||||
## [0.6.0] - 2026-11-12
|
||||
|
||||
Machine-checked finding verification (S1), intercept decisions traced to their actor, configurable notification limits, prompt hardening and goal guards from the #124 agentic audit, the target gate (S3b phase 1 and step 2.1, off by default and API-only), intent claim leases, and checksum-verified tool downloads in the Docker image.
|
||||
|
||||
+1
-1
@@ -178,7 +178,7 @@ COPY --from=build /out/artex /app/artex
|
||||
COPY start.sh /app/start.sh
|
||||
RUN chmod +x /app/artex /app/start.sh && mkdir -p /app/keys
|
||||
COPY skills/ /app/skills/
|
||||
# persistence points: data/ (SQLite, task files) and keys/ (jwt.key, via ARTEX_JWT_KEY_DIR)
|
||||
# persistence points: data/ (traffic index, task files, transcripts) and keys/ (jwt.key, secret.key, via ARTEX_JWT_KEY_DIR)
|
||||
VOLUME ["/app/data", "/app/keys"]
|
||||
EXPOSE 8787 8788
|
||||
ENTRYPOINT ["/app/start.sh"]
|
||||
|
||||
@@ -161,7 +161,7 @@ CGO_ENABLED=0 go build -tags embedui -o artex ./cmd/artex
|
||||
|
||||
```bash
|
||||
./build.sh --release
|
||||
# output: dist/artex-0.3.3-*.zip
|
||||
# output: dist/artex-<version>-*.zip
|
||||
```
|
||||
|
||||
UPX self-extracting binaries may be incompatible with some Linux kernels, virtualization environments, or security policies, so they are disabled by default. Use `ARTEX_TARGETS` to customize the targets; when you have confirmed the target runtime is compatible, explicitly pass `--upx` to further shrink the binary:
|
||||
@@ -207,7 +207,7 @@ Daily cron example (03:20, operator crontab):
|
||||
|
||||
## Updating and upgrading
|
||||
|
||||
> Upgrading only swaps the program and never touches data: the Postgres volume `pgdata`, `./data` (SQLite, task files, etc.), `./keys` (jwt.key), and `./skills` are all preserved. **No manual database migration is needed**: `artex` idempotently re-runs `schema.sql` on every start (with `ADD COLUMN` / `CREATE INDEX IF NOT EXISTS`), i.e. "restart equals migrate". It is still recommended to back up `./data` and the database before upgrading.
|
||||
> Upgrading only swaps the program and never touches data: the Postgres volume `pgdata`, `./data` (traffic index, task files, transcripts, etc.), `./keys` (jwt.key, secret.key), and `./skills` are all preserved. **No manual database migration is needed**: `artex` idempotently re-runs `schema.sql` on every start (with `ADD COLUMN` / `CREATE INDEX IF NOT EXISTS`), i.e. "restart equals migrate". It is still recommended to back up `./data` and the database before upgrading.
|
||||
|
||||
**Update banner (opt-in, off by default)**: start ARTEX with `ARTEX_UPDATE_CHECK=1` to have the backend ask the public GitHub releases API for the latest ARTEX tag, at most once per 24 hours (cached in memory; a failed check is not retried within that window). When the release is newer than the running version, the web UI shows a small dismissible strip linking to the Releases page. The request sends only the `User-Agent: artex-update-check` header. Unset, ARTEX makes no update request and shows no banner.
|
||||
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
ARTEX is an autonomous penetration-testing tool. Security fixes are provided only for the latest official release line. The `main` branch is not considered a stable release and may change at any time.
|
||||
|
||||
Only the latest release line receives security fixes. Older releases do not receive fixes; please upgrade to the latest release.
|
||||
|
||||
Notes:
|
||||
- Please upgrade to the latest release before reporting a vulnerability.
|
||||
- Forks, custom builds from arbitrary commits, and modified versions are not covered by official support.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
**Do not open a public issue for a security vulnerability.**
|
||||
|
||||
Report through **GitHub Security Advisories**: **Security** tab → **Report a vulnerability** in the `BaaRai/ARTEX` repository. This is the only reporting channel.
|
||||
|
||||
Please include:
|
||||
- A description of the vulnerability and its expected impact.
|
||||
- The ARTEX version and/or commit hash.
|
||||
- Runtime environment (OS, Docker/local, PostgreSQL, LLM provider, etc.).
|
||||
- Minimal reproduction steps (PoC).
|
||||
- Logs, screenshots, or sample traffic with **sensitive data removed**.
|
||||
- Suggested remediation, if available.
|
||||
|
||||
**Do not send** real third-party data, credentials, tokens, customer data, or unredacted sensitive content.
|
||||
|
||||
## Scope and Authorized Testing
|
||||
|
||||
ARTEX is an offensive-security tool. Test only against:
|
||||
- Your own ARTEX deployment in a lab or local environment;
|
||||
- Systems you own;
|
||||
- Targets explicitly authorized in writing (bug bounty, pentest contract, etc.).
|
||||
|
||||
Testing, scanning, or exploiting third-party systems without explicit authorization is strictly prohibited. Reports that violate this principle may be closed without action.
|
||||
|
||||
## What to Expect
|
||||
|
||||
This is an open-source project maintained on a best-effort basis. We aim to acknowledge a report within about a week and keep you updated, but we cannot guarantee specific response or fix timeframes.
|
||||
|
||||
## If a Report Is Accepted
|
||||
|
||||
- We will open a private **GitHub Security Advisory** for coordination.
|
||||
- We will identify the release containing the fix when it is published.
|
||||
- We may request a **CVE** if the vulnerability qualifies.
|
||||
- We will credit you or your organization in the advisory if you wish.
|
||||
- Default disclosure timing: **90 days** after a fix is available, or earlier by mutual agreement. If the vulnerability is being exploited in the wild, we may disclose earlier.
|
||||
|
||||
## If a Report Is Declined
|
||||
|
||||
We will explain the reason, for example:
|
||||
- It is not a security vulnerability but intended behavior.
|
||||
- It is outside ARTEX’s scope (e.g., an issue in an LLM provider, PostgreSQL, Docker, or a browser).
|
||||
- There is insufficient information to reproduce it.
|
||||
- The report only states that “the tool can be misused” without identifying a specific technical flaw in ARTEX.
|
||||
|
||||
Where appropriate, we may convert the report into a public hardening issue or feature request.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
The following are generally **not** considered ARTEX security vulnerabilities:
|
||||
- Attacks or scans against systems you do not own or are not authorized to test.
|
||||
- Self-XSS, spam, or brute-force without real impact.
|
||||
- Large-scale DoS/DDoS or resource-exhaustion attacks.
|
||||
- Social engineering targeting maintainers or users.
|
||||
- Issues that exist only in third-party dependencies and do not affect ARTEX.
|
||||
- Reports that “ARTEX can be used to attack systems” when the user has valid authorization — this is the nature of a penetration-testing tool, not a vulnerability.
|
||||
- Missing headers or hardening configuration without a concrete security impact.
|
||||
|
||||
## Safe Harbor
|
||||
|
||||
We welcome ethical security research. If you follow this policy and test only within authorized scope, we will not pursue legal action against you for good-faith research. This does not exempt you from applicable laws in your jurisdiction.
|
||||
|
||||
## Disclosure Policy
|
||||
|
||||
- Do not publicly disclose vulnerability details before a fix is available or before the coordinated disclosure period ends.
|
||||
- Do not publish a public PoC without maintainer consent.
|
||||
- If you do not receive a response within a reasonable time, please ping the contact channel above before disclosing.
|
||||
|
||||
Thank you for helping make ARTEX safer.
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// Package agent wires real LLM-driven planner and work agents (on top of the
|
||||
// agent-core SDK) to the dual SQLite graph. See the ARTEX architecture design doc
|
||||
// agent-core SDK) to the dual PostgreSQL graph. See the ARTEX architecture design doc
|
||||
// §4.3 (planner) and §4.4 (work agent).
|
||||
//
|
||||
// Provider configuration is read from the environment so the system runs with
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# ARTEX_TARGET_OS=linux One target OS in single-target mode.
|
||||
# ARTEX_TARGET_ARCH=amd64 One target arch in single-target mode.
|
||||
# ARTEX_TARGETS=linux/amd64,... Comma-separated targets for multi-target mode.
|
||||
# ARTEX_BUILD_VERSION=v0.3.3 Version embedded in the binary and archive name.
|
||||
# ARTEX_BUILD_VERSION=vX.Y.Z Version embedded in the binary and archive name.
|
||||
# ARTEX_OUTPUT=/path/to/artex Explicit binary path in single-target mode.
|
||||
# ARTEX_OUTPUT_DIR=dist Directory for default binary paths.
|
||||
# ARTEX_PACKAGE=1 Create a zip archive for each target.
|
||||
@@ -97,7 +97,7 @@ if [ -z "${ARTEX_BUILD_VERSION:-}" ]; then
|
||||
ARTEX_BUILD_VERSION="dev"
|
||||
fi
|
||||
fi
|
||||
# Release tags are commonly passed as v0.3.3; keep the binary version consistent.
|
||||
# Release tags are commonly passed with a v prefix; keep the binary version consistent.
|
||||
ARTEX_BUILD_VERSION="${ARTEX_BUILD_VERSION#v}"
|
||||
ARTEX_OUTPUT_DIR="${ARTEX_OUTPUT_DIR:-dist}"
|
||||
ARTEX_PACKAGE_DIR="${ARTEX_PACKAGE_DIR:-$ARTEX_OUTPUT_DIR}"
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
// Command artex runs the ARTEX backend: the dual SQLite graph stores,
|
||||
// Command artex runs the ARTEX backend: the dual PostgreSQL graph stores,
|
||||
// the event-driven exploration engine, and the JSON HTTP API consumed by the
|
||||
// shadcn/ui frontend.
|
||||
package main
|
||||
@@ -50,7 +50,7 @@ func main() {
|
||||
func run() int {
|
||||
var (
|
||||
addr = flag.String("addr", "127.0.0.1:8787", "HTTP listen address")
|
||||
dataDir = flag.String("data", filepath.Join(config.BaseDir(), "data"), "data directory for SQLite stores (default: data/ next to the executable)")
|
||||
dataDir = flag.String("data", filepath.Join(config.BaseDir(), "data"), "runtime data directory: traffic index, task files, transcripts (default: data/ next to the executable)")
|
||||
proxy = flag.String("proxy", "127.0.0.1:8788", "traffic recording proxy address (empty to disable)")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
+2
-2
@@ -54,10 +54,10 @@ services:
|
||||
- "${ARTEX_BIND_ADDR:-127.0.0.1}:8787:8787"
|
||||
- "127.0.0.1:8788:8788"
|
||||
volumes:
|
||||
# Bind the data directory (SQLite, task files, etc.) to data/ in the project root,
|
||||
# Bind the runtime data directory (traffic index, task files, transcripts, etc.) to data/ in the project root,
|
||||
# so it is easy to inspect/back up on the host and survives container rebuilds.
|
||||
- ./data:/app/data
|
||||
# JWT signing key (jwt.key), kept out of data/, which the file manager browses.
|
||||
# JWT signing key and secret key (jwt.key, secret.key), kept out of data/, which the file manager browses.
|
||||
# Persisting it keeps users signed in across container rebuilds.
|
||||
- ./keys:/app/keys
|
||||
# Mount skills/ from the project root into the container (overriding the image's built-in /app/skills);
|
||||
|
||||
@@ -18,7 +18,9 @@ func TestChatUnavailableReasonDistinguishesStates(t *testing.T) {
|
||||
}
|
||||
defer m.Close()
|
||||
td := t.TempDir()
|
||||
s := New(context.Background(), m, td, td, td)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := New(ctx, m, td, td, td)
|
||||
defer stopTestServer(t, s, cancel)
|
||||
|
||||
// Start from a clean profile table; other tests in the shared DB may have left rows.
|
||||
existing, _ := m.pg.ListProfiles()
|
||||
@@ -68,7 +70,9 @@ func TestResolveChatAgentHonoursConversationProfile(t *testing.T) {
|
||||
}
|
||||
defer m.Close()
|
||||
td := t.TempDir()
|
||||
s := New(context.Background(), m, td, td, td)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := New(ctx, m, td, td, td)
|
||||
defer stopTestServer(t, s, cancel)
|
||||
|
||||
// Force the global fallback to nil so a non-nil result can ONLY come from the
|
||||
// conversation's own profile — this is exactly the situation the user hit
|
||||
|
||||
+3
-1
@@ -24,7 +24,9 @@ func TestCoreTaskLifecyclePG(t *testing.T) {
|
||||
}
|
||||
defer m.Close()
|
||||
td := t.TempDir()
|
||||
s := New(context.Background(), m, td, td, td)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := New(ctx, m, td, td, td)
|
||||
defer stopTestServer(t, s, cancel)
|
||||
h := s.Handler()
|
||||
tok, err := signJWT(s.jwtKey)
|
||||
if err != nil {
|
||||
|
||||
@@ -67,7 +67,9 @@ func TestInheritedActivityDetailAndRelationDeletion(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
defer stopTestServer(t, s, cancel)
|
||||
token, err := signJWT(s.jwtKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -50,7 +50,9 @@ SELECT id,'completed','fixed','{}'::jsonb FROM findings WHERE task_id=$1 AND nam
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
defer stopTestServer(t, s, cancel)
|
||||
token, err := signJWT(s.jwtKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -144,7 +146,9 @@ VALUES ($1,'xss','Reflected XSS','reflected in search','high','confirmed')`, tas
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
defer stopTestServer(t, s, cancel)
|
||||
token, err := signJWT(s.jwtKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
+7
-1
@@ -18,8 +18,14 @@ func TestMgmtAPI(t *testing.T) {
|
||||
if m.pg == nil {
|
||||
t.Skip("postgres unavailable — skipping management API test")
|
||||
}
|
||||
// New starts the notifier and task-archive workers on context.Background();
|
||||
// closing the pool stops them from claiming later tests' digest batches
|
||||
// and archive jobs.
|
||||
defer m.Close()
|
||||
td := t.TempDir()
|
||||
s := New(context.Background(), m, td, td, td)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := New(ctx, m, td, td, td)
|
||||
defer stopTestServer(t, s, cancel)
|
||||
h := s.Handler()
|
||||
tok, err := signJWT(s.jwtKey)
|
||||
if err != nil {
|
||||
|
||||
@@ -33,7 +33,9 @@ INSERT INTO skill_usage(skill,agent_key,task_id,found) VALUES
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
defer stopTestServer(t, s, cancel)
|
||||
token, err := signJWT(s.jwtKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -21,7 +21,9 @@ func TestTaskCategoryBatchRoute(t *testing.T) {
|
||||
defer m.Close()
|
||||
|
||||
td := t.TempDir()
|
||||
s := New(context.Background(), m, td, td, td)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := New(ctx, m, td, td, td)
|
||||
defer stopTestServer(t, s, cancel)
|
||||
h := s.Handler()
|
||||
token, err := signJWT(s.jwtKey)
|
||||
if err != nil {
|
||||
|
||||
@@ -16,7 +16,9 @@ func TestWorkerControlRoutes(t *testing.T) {
|
||||
defer m.Close()
|
||||
|
||||
td := t.TempDir()
|
||||
s := New(context.Background(), m, td, td, td)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := New(ctx, m, td, td, td)
|
||||
defer stopTestServer(t, s, cancel)
|
||||
h := s.Handler()
|
||||
token, err := signJWT(s.jwtKey)
|
||||
if err != nil {
|
||||
|
||||
@@ -38,7 +38,9 @@ VALUES ($1,'goal','{}','met'), ($1,'goal','{}','open')`, task.ExpID); err != nil
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
defer stopTestServer(t, s, cancel)
|
||||
token, err := signJWT(s.jwtKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -24,7 +24,9 @@ func TestTaskMetadataPatchReturnsRenameAndPin(t *testing.T) {
|
||||
}
|
||||
taskID, _ := strconv.ParseInt(task.ID, 10, 64)
|
||||
defer func() { _ = m.pg.DeleteTask(taskID) }()
|
||||
s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
defer stopTestServer(t, s, cancel)
|
||||
token, err := signJWT(s.jwtKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -66,7 +68,9 @@ func TestConversationBatchDeleteReportsMissing(t *testing.T) {
|
||||
t.Skipf("postgres unavailable (%v) — skipping", err)
|
||||
}
|
||||
defer m.Close()
|
||||
s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
defer stopTestServer(t, s, cancel)
|
||||
first, err := m.pg.CreateConversation("mainagent", "batch-http-first", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -23,6 +23,9 @@ func TestTaskOutcomeCard(t *testing.T) {
|
||||
}
|
||||
taskID, _ := strconv.ParseInt(task.ID, 10, 64)
|
||||
defer func() { _ = m.pg.DeleteTask(taskID) }()
|
||||
// skill_usage has no foreign keys, so DeleteTask leaves these rows behind,
|
||||
// and their misses would outrank TestSkillGapsReport's fixture in later runs.
|
||||
defer func() { _, _ = m.pg.Exec(`DELETE FROM skill_usage WHERE task_id=$1`, taskID) }()
|
||||
|
||||
if _, err := m.pg.Exec(`
|
||||
INSERT INTO findings(task_id,vulnclass,name,severity,status) VALUES
|
||||
@@ -57,7 +60,9 @@ INSERT INTO skill_usage(skill,task_id,found) VALUES ('api-recon',$1,true),('grap
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
defer stopTestServer(t, s, cancel)
|
||||
token, err := signJWT(s.jwtKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -20,7 +20,9 @@ func TestTaskTemplateHTTPCRUD(t *testing.T) {
|
||||
t.Skipf("postgres unavailable (%v) — skipping", err)
|
||||
}
|
||||
defer m.Close()
|
||||
s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
defer stopTestServer(t, s, cancel)
|
||||
h := s.Handler()
|
||||
token, err := signJWT(s.jwtKey)
|
||||
if err != nil {
|
||||
@@ -90,7 +92,9 @@ func TestConversationPatchReturnsPinState(t *testing.T) {
|
||||
t.Skipf("postgres unavailable (%v) — skipping", err)
|
||||
}
|
||||
defer m.Close()
|
||||
s := New(context.Background(), m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir())
|
||||
defer stopTestServer(t, s, cancel)
|
||||
conversation, err := m.pg.CreateConversation("mainagent", "pin through http", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// stopTestServer cancels a test server's context and joins what New started.
|
||||
// New restores every unfinished task in the shared database and runs its
|
||||
// planner, worker and deadline loops, which write under m.dir. Call it with
|
||||
// defer right after New, so it runs before m.Close and the TempDir cleanups.
|
||||
func stopTestServer(t *testing.T, s *Server, cancel context.CancelFunc) {
|
||||
t.Helper()
|
||||
cancel()
|
||||
for _, task := range s.m.List() {
|
||||
s.engine.BeginDelete(task.ID)
|
||||
waitCtx, waitCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
if err := s.waitTaskQuiescent(waitCtx, task.ID); err != nil {
|
||||
t.Errorf("task %s still running at teardown: %v", task.ID, err)
|
||||
}
|
||||
waitCancel()
|
||||
s.engine.StopTask(task.ID)
|
||||
}
|
||||
s.archiveWG.Wait()
|
||||
<-s.side.done
|
||||
}
|
||||
Reference in New Issue
Block a user