Merge origin/main: supersede the PR #317 minimal fix with the unified resolver
Why: main landed fix-svc-dsn-reuse-dbref (reuse-path dbref restore, env-DSN
aware precheck, DSN_ENV scoping) for the same restart-DSN incident; the
approved recovery plan is its superset and replaces those mechanisms with
resolve_db_config + recoverable svc.env keys + refuse-on-invalid semantics.
Evidence: tests/test_svc_precheck.py ported to the new units (18 cases green,
same scenario matrix; broken-dbref expectation updated to refusal per the
approved plan); tests/test_svc.py 11 cases green; bash -n OK.
Impact: conflict hunks resolved toward the unified resolver; main's source
guard at the script tail restored; agent_listen_rejected kept as a pure
predicate (used by the start precheck and the governance tests).
Refs: main 7acf318 (PR #317); task card .task/feature-svc-db-config-recover.md.
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -93,7 +93,8 @@ frontend: contracts
|
||||
|
||||
build: frontend
|
||||
cd $(BACKEND) && CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=$(VERSION)" -o ../../$(BIN) ./cmd/agent-system
|
||||
@echo "OK: $(BIN)(版本 $(VERSION);随附插件目录 $(PLUGINS)/)"
|
||||
cd $(BACKEND) && CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o ../../bin/storage-migrate ./cmd/storage-migrate
|
||||
@echo "OK: $(BIN)(版本 $(VERSION);随附插件目录 $(PLUGINS)/)+ bin/storage-migrate(协作存储 U4 迁移工具)"
|
||||
|
||||
test-core:
|
||||
cd $(CORE) && go test ./...
|
||||
|
||||
+1
-1
Submodule modules/backend updated: c8d66ca3cd...a466838440
+1
-1
Submodule modules/core updated: a253a40b8e...452daf3ffa
+1
-1
Submodule modules/documents updated: 5ac58dcd7e...99a54ed340
@@ -1,80 +1,82 @@
|
||||
[
|
||||
"backend/app backend/bus",
|
||||
"backend/app backend/cardsvc",
|
||||
"backend/app backend/cmdbus",
|
||||
"backend/app backend/config",
|
||||
"backend/app backend/hostapi",
|
||||
"backend/app backend/ingest",
|
||||
"backend/app backend/intentgate",
|
||||
"backend/app backend/intentsem",
|
||||
"backend/app backend/pgbus",
|
||||
"backend/app backend/plugins",
|
||||
"backend/app backend/profiles",
|
||||
"backend/app backend/prompts",
|
||||
"backend/app backend/rag",
|
||||
"backend/app backend/ratelimit",
|
||||
"backend/app backend/runmgr",
|
||||
"backend/app backend/runtime",
|
||||
"backend/app backend/server",
|
||||
"backend/app backend/settings",
|
||||
"backend/app backend/skills",
|
||||
"backend/app backend/squadadapter",
|
||||
"backend/app backend/store",
|
||||
"backend/app backend/tools",
|
||||
"backend/app backend/workflow",
|
||||
"backend/cardsvc backend/store",
|
||||
"backend/hostapi backend/store",
|
||||
"backend/pgbus backend/bus",
|
||||
"backend/pgbus backend/store",
|
||||
"backend/plugins backend/settings",
|
||||
"backend/plugins backend/store",
|
||||
"backend/plugins backend/skills",
|
||||
"backend/plugins backend/tools",
|
||||
"backend/plugins backend/workflow",
|
||||
"backend/profiles backend/store",
|
||||
"backend/prompts backend/store",
|
||||
"backend/rag backend/settings",
|
||||
"backend/rag backend/store",
|
||||
"backend/route backend/intentgate",
|
||||
"backend/runmgr backend/bus",
|
||||
"backend/runmgr backend/cardsvc",
|
||||
"backend/runmgr backend/cmdbus",
|
||||
"backend/runmgr backend/ingest",
|
||||
"backend/runmgr backend/intentgate",
|
||||
"backend/runmgr backend/profiles",
|
||||
"backend/runmgr backend/route",
|
||||
"backend/runmgr backend/runtime",
|
||||
"backend/runmgr backend/settings",
|
||||
"backend/runmgr backend/store",
|
||||
"backend/runmgr backend/tools",
|
||||
"backend/runmgr backend/workflow",
|
||||
"backend/runtime backend/ctxasm",
|
||||
"backend/runtime backend/docgen",
|
||||
"backend/runtime backend/prompts",
|
||||
"backend/runtime backend/quality",
|
||||
"backend/runtime backend/squadadapter",
|
||||
"backend/runtime backend/wfcompile",
|
||||
"backend/runtime backend/workflow",
|
||||
"backend/server backend/cardsvc",
|
||||
"backend/server backend/cmdbus",
|
||||
"backend/server backend/ingest",
|
||||
"backend/server backend/intentgate",
|
||||
"backend/server backend/plugins",
|
||||
"backend/server backend/profiles",
|
||||
"backend/server backend/prompts",
|
||||
"backend/server backend/ratelimit",
|
||||
"backend/server backend/route",
|
||||
"backend/server backend/runmgr",
|
||||
"backend/server backend/settings",
|
||||
"backend/server backend/store",
|
||||
"backend/server backend/tools",
|
||||
"backend/server backend/workflow",
|
||||
"backend/settings backend/store",
|
||||
"backend/squadadapter backend/quality",
|
||||
"backend/store backend/blobstore",
|
||||
"backend/tools backend/quality",
|
||||
"backend/wfcompile backend/tools",
|
||||
"backend/wfcompile backend/workflow",
|
||||
"frontend/app frontend/components",
|
||||
"frontend/components frontend/app"
|
||||
"backend/app backend/bus",
|
||||
"backend/app backend/cardsvc",
|
||||
"backend/app backend/cmdbus",
|
||||
"backend/app backend/config",
|
||||
"backend/app backend/hostapi",
|
||||
"backend/app backend/ingest",
|
||||
"backend/app backend/intentgate",
|
||||
"backend/app backend/intentsem",
|
||||
"backend/app backend/pgbus",
|
||||
"backend/app backend/plugins",
|
||||
"backend/app backend/profiles",
|
||||
"backend/app backend/prompts",
|
||||
"backend/app backend/rag",
|
||||
"backend/app backend/ratelimit",
|
||||
"backend/app backend/runmgr",
|
||||
"backend/app backend/runtime",
|
||||
"backend/app backend/server",
|
||||
"backend/app backend/settings",
|
||||
"backend/app backend/skills",
|
||||
"backend/app backend/squadadapter",
|
||||
"backend/app backend/store",
|
||||
"backend/app backend/surfacesvc",
|
||||
"backend/app backend/tools",
|
||||
"backend/app backend/workflow",
|
||||
"backend/cardsvc backend/store",
|
||||
"backend/hostapi backend/store",
|
||||
"backend/pgbus backend/bus",
|
||||
"backend/pgbus backend/store",
|
||||
"backend/plugins backend/settings",
|
||||
"backend/plugins backend/skills",
|
||||
"backend/plugins backend/store",
|
||||
"backend/plugins backend/tools",
|
||||
"backend/plugins backend/workflow",
|
||||
"backend/profiles backend/store",
|
||||
"backend/prompts backend/store",
|
||||
"backend/rag backend/settings",
|
||||
"backend/rag backend/store",
|
||||
"backend/route backend/intentgate",
|
||||
"backend/runmgr backend/bus",
|
||||
"backend/runmgr backend/cardsvc",
|
||||
"backend/runmgr backend/ingest",
|
||||
"backend/runmgr backend/intentgate",
|
||||
"backend/runmgr backend/profiles",
|
||||
"backend/runmgr backend/route",
|
||||
"backend/runmgr backend/runtime",
|
||||
"backend/runmgr backend/settings",
|
||||
"backend/runmgr backend/store",
|
||||
"backend/runmgr backend/tools",
|
||||
"backend/runmgr backend/workflow",
|
||||
"backend/runtime backend/ctxasm",
|
||||
"backend/runtime backend/docgen",
|
||||
"backend/runtime backend/prompts",
|
||||
"backend/runtime backend/quality",
|
||||
"backend/runtime backend/squadadapter",
|
||||
"backend/runtime backend/wfcompile",
|
||||
"backend/runtime backend/workflow",
|
||||
"backend/server backend/cardsvc",
|
||||
"backend/server backend/cmdbus",
|
||||
"backend/server backend/ingest",
|
||||
"backend/server backend/intentgate",
|
||||
"backend/server backend/plugins",
|
||||
"backend/server backend/profiles",
|
||||
"backend/server backend/prompts",
|
||||
"backend/server backend/ratelimit",
|
||||
"backend/server backend/route",
|
||||
"backend/server backend/runmgr",
|
||||
"backend/server backend/settings",
|
||||
"backend/server backend/store",
|
||||
"backend/server backend/surfacesvc",
|
||||
"backend/server backend/tools",
|
||||
"backend/server backend/workflow",
|
||||
"backend/settings backend/store",
|
||||
"backend/squadadapter backend/quality",
|
||||
"backend/store backend/blobstore",
|
||||
"backend/surfacesvc backend/store",
|
||||
"backend/tools backend/quality",
|
||||
"backend/wfcompile backend/tools",
|
||||
"backend/wfcompile backend/workflow",
|
||||
"frontend/app frontend/components",
|
||||
"frontend/components frontend/app"
|
||||
]
|
||||
|
||||
+21
-5
@@ -1476,6 +1476,19 @@ db_auth_enabled() {
|
||||
esac
|
||||
}
|
||||
|
||||
# agent_listen_rejected EXEC_ARGS DEFAULT_ADDR INSECURE:启动预检整体判定(0=放行,
|
||||
# 1=拒绝)。与二进制 listen guard 同口径:回环或显式放行(insecure)不拦;认证开
|
||||
# (db_auth_enabled,依赖解析出的 DB_AUTH/DB_DSN)不拦;仅非回环 + 未放行 + 认证关
|
||||
# 才拦。抽出纯判定便于治理测试覆盖(tests/test_svc_precheck.py)。
|
||||
agent_listen_rejected() {
|
||||
local addr; addr="$(printf '%s' "$1" | sed -n 's/.*--addr \([^ ]*\).*/\1/p')"
|
||||
addr="${addr:-$2}"
|
||||
addr_is_loopback "${addr%:*}" && return 1
|
||||
[ -n "$3" ] && return 1
|
||||
db_auth_enabled && return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
# bin_env_dsn_supported BIN:二进制是否消费环境层 AGENT_SYSTEM_DSN(经 config explain
|
||||
# 探测:explain 对 DSN 值脱敏但用户名保留,探测标记用户即可)。旧产物不支持环境层时
|
||||
# 返回 1——调用方拒绝以环境传递 DSN(防静默按内嵌 SQLite 运行)。
|
||||
@@ -1559,10 +1572,10 @@ agent_do_start() {
|
||||
# 不等二进制 listen guard 以"进程即退"暴露;二进制守卫仍是权威。
|
||||
# 认证判定依据统一解析的最终有效 DSN 与 auth(含环境变量与配置文件来源),
|
||||
# 与二进制 AuthEnabled 口径一致(2026-10-11 修复:此前只看命令行 --dsn,
|
||||
# DSN 迁入环境变量后 auto 被误判为未开认证)。
|
||||
local pf_addr; pf_addr="$(printf '%s' "$exec_args" | sed -n 's/.*--addr \([^ ]*\).*/\1/p')"
|
||||
pf_addr="${pf_addr:-$ENV_ADDR}"
|
||||
if ! addr_is_loopback "${pf_addr%:*}" && [ -z "$insecure" ] && ! db_auth_enabled; then
|
||||
# DSN 迁入环境变量后 auto 被误判为未开认证;PR #317 同因,本方案为其超集)。
|
||||
if agent_listen_rejected "$exec_args" "$ENV_ADDR" "$insecure"; then
|
||||
local pf_addr; pf_addr="$(printf '%s' "$exec_args" | sed -n 's/.*--addr \([^ ]*\).*/\1/p')"
|
||||
pf_addr="${pf_addr:-$ENV_ADDR}"
|
||||
c_err "[$ENV_NAME] 拒绝启动:非回环地址 $pf_addr 且认证判定为关(auto 无 DSN 即关)。"
|
||||
c_err " 任选其一:--addr 127.0.0.1:${pf_addr##*:};数据库绑定 scripts/svc.sh $ENV_NAME db <pg实例>;"
|
||||
c_err " 显式 --auth on;或环境 SVC_$(printf '%s' "$ENV_NAME" | tr 'a-z-' 'A-Z_')_INSECURE_LISTEN=1 显式放行。"
|
||||
@@ -3395,4 +3408,7 @@ main() {
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
# 仅作为脚本直接执行时进入主流程;被 source(治理单测 fixtures)时只定义函数。
|
||||
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
||||
main "$@"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
"""svc.sh 启动预检与数据库配置统一解析的治理回归。
|
||||
|
||||
背景(2026-10-10/10-11 dev 事故,PR #317 最小修复 → feature/svc-db-config-recover
|
||||
方案超集):G1-7 把 DSN 从命令行迁到 AGENT_SYSTEM_DSN 环境变量后——
|
||||
|
||||
1. 预检只看参数串,「非回环 + auth auto + env DSN」被误判认证关而拒启;二进制
|
||||
AuthEnabled(config.go:显式 on,或非显式 off 且 DSN 非空——env 为合法 DSN
|
||||
来源)会放行。现由 db_auth_enabled(依赖 resolve_db_config 产出的 DB_AUTH/
|
||||
DB_DSN)统一判定,覆盖 on/off/auto 与命令行/环境/配置文件来源。
|
||||
2. 复用启动路径(无显式参数、复用 exec=)不再有 DSN:dbref 兜底只在重建分支,
|
||||
纯 start/restart 会静默回落 SQLite。现 start/restart/deploy/update/db 共用
|
||||
resolve_db_config,且存量 exec= 的 --dsn 迁移为 dbref=/dsn= 专键(幂等)。
|
||||
3. DSN 跨服务串用(同进程连续启动):现 env_config 按实例重置解析态、DSN 经
|
||||
局部 guard_env 交付,无跨服务全局通道。
|
||||
|
||||
行为变化(经用户 2026-10-11 方案批准,取代 PR #317 的对应语义):dbref 无法重建
|
||||
DSN 时从「告警后按无绑定启动」改为**拒绝启动**;本测试按新语义断言。
|
||||
|
||||
测试通过 source scripts/svc.sh 调用纯函数/fixture 函数(文件尾有 source 守卫),
|
||||
不启动真实服务、不触碰真实 runtime/。端到端生命周期回归见 tests/test_svc.py。
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
SVC = REPO / "scripts" / "svc.sh"
|
||||
|
||||
PG_ADDR = "10.1.2.3:8899"
|
||||
PG_USER = "svc_test"
|
||||
PG_PASS = "fixture-only-pass"
|
||||
PG_DSN = f"postgresql://{PG_USER}:{PG_PASS}@{PG_ADDR}/agentdb"
|
||||
NONLOOP_EXEC = "--addr 0.0.0.0:8788 --data /tmp/x --auth auto"
|
||||
|
||||
|
||||
def _bash(script: str, *args: str) -> str:
|
||||
"""source svc.sh 后在 bash 内执行脚本,返回 stdout(stderr 丢弃)。"""
|
||||
proc = subprocess.run(
|
||||
["bash", "-c", script, "svc-test", str(SVC), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(REPO),
|
||||
env={**os.environ, "LC_ALL": "C"},
|
||||
timeout=30,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise AssertionError(
|
||||
f"bash 脚本失败 rc={proc.returncode}\nstdout:\n{proc.stdout}\nstderr:\n{proc.stderr}"
|
||||
)
|
||||
return proc.stdout
|
||||
|
||||
|
||||
def _authed(auth_mode: str, dsn: str = "", exec_args: str = "") -> bool:
|
||||
"""db_auth_enabled 判定:auth 值 + 有效 DSN(模拟 resolve_db_config 产出)。"""
|
||||
out = _bash(
|
||||
'source "$1"; shift; DB_AUTH="$1"; DB_DSN="$2"; '
|
||||
'if db_auth_enabled; then echo authed; else echo denied; fi',
|
||||
auth_mode or "auto",
|
||||
dsn,
|
||||
)
|
||||
return out.strip() == "authed"
|
||||
|
||||
|
||||
def _rejected(exec_args: str, default_addr: str = "0.0.0.0:8788",
|
||||
insecure: str = "", auth_mode: str = "auto", dsn: str = "") -> bool:
|
||||
"""agent_listen_rejected 判定(认证部分经 DB_AUTH/DB_DSN 全局)。"""
|
||||
out = _bash(
|
||||
"source \"$1\"; shift; DB_AUTH=\"$4\"; DB_DSN=\"$5\"; "
|
||||
"if agent_listen_rejected \"$1\" \"$2\" \"$3\"; then echo rejected; else echo allowed; fi",
|
||||
exec_args,
|
||||
default_addr,
|
||||
insecure,
|
||||
auth_mode or "auto",
|
||||
dsn,
|
||||
)
|
||||
return out.strip() == "rejected"
|
||||
|
||||
|
||||
class DbAuthEnabledMatrix(unittest.TestCase):
|
||||
"""认证判定与二进制 AuthEnabled 口径一致(config.go,默认 auth=auto)。"""
|
||||
|
||||
def test_auth_on_explicit(self):
|
||||
self.assertTrue(_authed("on"))
|
||||
|
||||
def test_auth_off_beats_dsn(self):
|
||||
# 显式 off 优先:与二进制 AuthMode=="off" → AuthEnabled=false 一致。
|
||||
self.assertFalse(_authed("off", PG_DSN))
|
||||
|
||||
def test_auto_with_effective_dsn_is_authed(self):
|
||||
# 2026-10-10 误拒主场景:dbref 重建/环境交付后 DSN 只在 AGENT_SYSTEM_DSN。
|
||||
self.assertTrue(_authed("auto", PG_DSN))
|
||||
|
||||
def test_auto_without_dsn_is_denied(self):
|
||||
self.assertFalse(_authed("auto"))
|
||||
self.assertFalse(_authed("", ""))
|
||||
|
||||
def test_no_auth_no_dsn_denied(self):
|
||||
self.assertFalse(_authed("auto", "", "--mock"))
|
||||
|
||||
|
||||
class AgentListenRejectedPrecheck(unittest.TestCase):
|
||||
"""预检整体判定:非回环 + 未放行 + 认证关才拒绝。"""
|
||||
|
||||
def test_incident_combo_not_rejected(self):
|
||||
# 原事故组合:非回环 + auto + DSN 仅在环境变量 → 必须放行(曾误拒)。
|
||||
self.assertFalse(_rejected(NONLOOP_EXEC, dsn=PG_DSN))
|
||||
|
||||
def test_nonloop_no_dsn_rejected(self):
|
||||
self.assertTrue(_rejected(NONLOOP_EXEC))
|
||||
|
||||
def test_nonloop_auth_off_with_dsn_rejected(self):
|
||||
self.assertTrue(_rejected("--addr 0.0.0.0:8788 --auth off",
|
||||
dsn=PG_DSN, auth_mode="off"))
|
||||
|
||||
def test_loopback_not_rejected(self):
|
||||
self.assertFalse(_rejected("--addr 127.0.0.1:8788"))
|
||||
|
||||
def test_insecure_not_rejected(self):
|
||||
self.assertFalse(_rejected(NONLOOP_EXEC, insecure="1"))
|
||||
|
||||
def test_addr_defaults_to_default_addr(self):
|
||||
# exec 参数里没有 --addr 时按 DEFAULT_ADDR 判。
|
||||
self.assertTrue(_rejected("--data /tmp/x", default_addr="0.0.0.0:8788"))
|
||||
self.assertFalse(_rejected("--data /tmp/x", default_addr="127.0.0.1:8788"))
|
||||
|
||||
|
||||
class ResolveDbConfigFixtures(unittest.TestCase):
|
||||
"""统一解析:复用/迁移语义(防静默回落 SQLite;坏绑定拒绝)。"""
|
||||
|
||||
def setUp(self):
|
||||
self._tmp = tempfile.TemporaryDirectory()
|
||||
rt = Path(self._tmp.name) / "runtime"
|
||||
(rt / "pg1").mkdir(parents=True)
|
||||
(rt / "pg1" / "svc.env").write_text(
|
||||
f"kind=postgres\naddr={PG_ADDR}\nuser={PG_USER}\npassword={PG_PASS}\n"
|
||||
)
|
||||
(rt / "dev").mkdir()
|
||||
self.rt = rt
|
||||
self.dev_dir = rt / "dev"
|
||||
|
||||
def tearDown(self):
|
||||
self._tmp.cleanup()
|
||||
|
||||
def _resolve(self, exec_line: str, dbref: str = "", extra: str = "",
|
||||
rewrite: bool = True) -> tuple:
|
||||
"""写 fixture svc.env 后 resolve_db_config;回 (rc, DB_MODE/DSN/REF/SAVE)。
|
||||
rewrite=False 时不覆写登记(幂等用例:在同一登记上二次解析)。"""
|
||||
if rewrite:
|
||||
content = f"kind=agent\nenv=dev\nexec={exec_line}\n"
|
||||
if dbref:
|
||||
content += f"dbref={dbref}\n"
|
||||
if extra:
|
||||
content += extra + "\n"
|
||||
(self.dev_dir / "svc.env").write_text(content)
|
||||
out = _bash(
|
||||
'source "$1"; shift; RUNTIME="$1"; ENV_NAME=dev; ENV_DIR="$2"; '
|
||||
'if resolve_db_config; then rc=ok; else rc=fail; fi; '
|
||||
'printf "RC=%s\\nMODE=%s\\nDSN=%s\\nREF=%s\\nSAVE=%s\\n" '
|
||||
'"$rc" "$DB_MODE" "$DB_DSN" "$DB_REF" "$DB_DSN_SAVE"',
|
||||
str(self.rt),
|
||||
str(self.dev_dir),
|
||||
)
|
||||
return dict(part.split("=", 1) for part in out.strip().splitlines())
|
||||
|
||||
def test_post_g17_exec_restores_dsn_from_dbref(self):
|
||||
# G1-7 后 exec= 不含 DSN:解析必须从 dbref= 重建(否则回落 SQLite)。
|
||||
got = self._resolve("--addr 0.0.0.0:8788 --auth auto", dbref="pg1/agentdb")
|
||||
self.assertEqual(got["RC"], "ok")
|
||||
self.assertEqual(got["MODE"], "pg")
|
||||
self.assertEqual(got["DSN"], PG_DSN)
|
||||
self.assertEqual(got["REF"], "pg1/agentdb")
|
||||
self.assertEqual(got["SAVE"], "")
|
||||
|
||||
def test_legacy_exec_dsn_migrates_to_recoverable_keys(self):
|
||||
# 存量 exec= 的明文 --dsn:迁移为可恢复配置(命中实例 → dbref=),
|
||||
# exec= 不再携带 DSN。
|
||||
legacy = f"--addr 0.0.0.0:8788 --dsn {PG_DSN}"
|
||||
got = self._resolve(legacy)
|
||||
self.assertEqual(got["RC"], "ok")
|
||||
self.assertEqual(got["DSN"], PG_DSN)
|
||||
self.assertEqual(got["REF"], "pg1/agentdb")
|
||||
envfile = (self.dev_dir / "svc.env").read_text()
|
||||
self.assertNotIn("--dsn", envfile)
|
||||
self.assertIn("dbref=pg1/agentdb", envfile)
|
||||
|
||||
def test_legacy_exec_dsn_without_instance_goes_to_dsn_key(self):
|
||||
# 未命中本机实例:迁移到受保护 dsn= 字段。
|
||||
external = "postgresql://u:pw@10.9.9.9:5432/otherdb"
|
||||
got = self._resolve(f"--addr 127.0.0.1:8788 --dsn {external}")
|
||||
self.assertEqual(got["RC"], "ok")
|
||||
self.assertEqual(got["DSN"], external)
|
||||
self.assertEqual(got["SAVE"], external)
|
||||
self.assertIn(f"dsn={external}", (self.dev_dir / "svc.env").read_text())
|
||||
|
||||
def test_migration_is_idempotent(self):
|
||||
# 第二次解析:exec 已无 --dsn,登记保持稳定。
|
||||
self._resolve(f"--addr 0.0.0.0:8788 --dsn {PG_DSN}")
|
||||
first = (self.dev_dir / "svc.env").read_text()
|
||||
got = self._resolve("", rewrite=False)
|
||||
self.assertEqual(got["RC"], "ok")
|
||||
self.assertEqual(got["DSN"], PG_DSN)
|
||||
self.assertEqual((self.dev_dir / "svc.env").read_text(), first)
|
||||
|
||||
def test_no_dsn_no_dbref_is_sqlite(self):
|
||||
got = self._resolve("--addr 127.0.0.1:8788")
|
||||
self.assertEqual(got["RC"], "ok")
|
||||
self.assertEqual(got["MODE"], "sqlite")
|
||||
self.assertEqual(got["DSN"], "")
|
||||
|
||||
def test_broken_dbref_refuses(self):
|
||||
# 行为变化(方案批准):实例登记不全 → 拒绝启动,不静默回落 SQLite。
|
||||
got = self._resolve("--addr 127.0.0.1:8788", dbref="missing/agentdb")
|
||||
self.assertEqual(got["RC"], "fail")
|
||||
self.assertEqual(got["DSN"], "")
|
||||
|
||||
|
||||
class ResolverStateIsolation(unittest.TestCase):
|
||||
"""解析态按实例隔离(env_config 重置,防 update --all 串库)。"""
|
||||
|
||||
def test_env_config_resets_resolver_state(self):
|
||||
# 棘轮:env_config 必须重置 SVC_DB_*(删除即回归「同进程连续启动跨服务
|
||||
# 泄漏数据库配置」——DSN_ENV 全局时代的事故形态)。
|
||||
out = _bash(
|
||||
'source "$1"; declare -f env_config | grep -c \'SVC_DB_MODE=""\''
|
||||
)
|
||||
self.assertEqual(out.strip(), "1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user