ea791c9f30
The plan's one irreversible risk is the worst-case test itself. It deliberately lets the script fall through to the shared table, and the account in .env is a superuser, so running it before the sandbox role exists would delete every expired row in there. All eleven rows on that table predate any cutoff the tests use. That forces the order: factory, then the retention tests move onto an owner role, and only then does the case get written. Review caught that the original order also made the --table integration cases impossible to fail first, since the tool would already be implemented by the time they were written. Same fix resolves both. The worst-case case has no red-first path at all. Turning it red means running it as the superuser, which is the thing being prevented, so its evidence is the probe in the finding instead, and the plan says so rather than calling it verified. One acceptance criterion in the design turned out to be unrunnable: the hint line only prints on the Postgres branch, so no unit test that never connects can assert it. Corrected in place.
298 lines
21 KiB
Markdown
298 lines
21 KiB
Markdown
---
|
||
type: plan
|
||
node_id: plan:2026-08-26-issue18-pg-test-isolation
|
||
title: "issue #18 实现计划: 权限边界替代行数快照 + --table 锁死目标"
|
||
date: 2026-08-26
|
||
---
|
||
|
||
# issue #18 实现计划
|
||
|
||
> 类型:plan|日期:2026-08-26|分支 `fix/issue-18-pg-test-isolation`
|
||
> 实现设计 `designs/2026-08-26-issue18-pg-test-isolation-design.md`(已过人类门)。设计的节号在下文直接引用;本计划只负责"动哪些文件、按什么顺序、怎么拿到证据"。
|
||
> **本计划不涉及参考实现迁移,保真校验不适用。**
|
||
|
||
> [!CAUTION]
|
||
> **执行期唯一的不可逆风险,写在最前面。** 设计 §5.3 的"最坏情况"用例故意让脚本以裸 `search_path` 跑到共享表上。它**只有在沙箱角色就位之后才可以跑**——若在角色化之前用 `.env` 的 `app`(实测 superuser)跑它,`--older-than-days 7 --apply` 会真的删掉共享表里的过期行(实测那 11 行 2026-07-22 的数据全部早于任何截止线)。
|
||
> 这条风险决定了下面的任务顺序:**沙箱工厂(Task 1)→ retention 全面角色化(Task 2)→ 才写这条用例**。它没有常规意义上的"先红"路径,见 Task 2 的说明。
|
||
|
||
## 目标
|
||
|
||
让 `tests/integration` 不再依赖也不再污染共享表 `llm_calls`,并把"清理脚本删错表"从事后可观测改成物理上做不到,随后发布 1.3.2。
|
||
|
||
## 方案概述
|
||
|
||
先建 `tests/integration/conftest.py` 的一次性沙箱工厂(独立 schema + 可选独占登录角色),把 retention 测试全面切到对真表无任何权限的角色上并删除行数快照;再给 `telemetry_retention.py` 加 `--table SCHEMA.llm_calls`(目标由参数精确解析、绕开 `search_path`,表名段锁死);随后把 `test_postgres_telemetry.py` 的 7 条用例迁出真表、拆分 `_RUN_PREFIX` 的两个职责;最后加一道 lint 门防字面量回归,发布 1.3.2。
|
||
|
||
## 涉及技术
|
||
|
||
Python 3.12 / pytest + pytest-asyncio(auto) / asyncpg / PostgreSQL 16 权限与 `search_path` 语义 / argparse。
|
||
|
||
## 文件结构
|
||
|
||
| 文件 | 动作 | 职责 |
|
||
|---|---|---|
|
||
| `tests/integration/conftest.py` | **新建** | `PgSandbox` 与 `pg_sandbox` 工厂;admin DSN 私有化 |
|
||
| `tests/integration/test_pg_sandbox.py` | **新建** | 工厂自身的行为测试(含 setup 中途失败不留残留) |
|
||
| `tests/integration/test_retention_tool_pg.py` | 修改 | 全部用例角色化;删行数快照;补 `--table` 与最坏情况用例 |
|
||
| `tools/telemetry_retention.py` | 修改 | 新增 `--table`;PG 分支目标解析改为"显式限定名优先" |
|
||
| `tests/unit/test_retention_tool.py` | 修改 | `--table` 的参数分类用例(不连库) |
|
||
| `tests/integration/test_postgres_telemetry.py` | 修改 | 7 条用例迁出真表;`_RUN_PREFIX` 双职责拆分;其余 fixture 收敛到工厂 |
|
||
| `Makefile` | 修改 | `lint` / `check` 各加一道字面量门 |
|
||
| `README.md` / `CHANGELOG.md` / `pyproject.toml` / `src/polygateway/__init__.py` | 修改 | `--table` 用法与 1.3.2 定版 |
|
||
|
||
---
|
||
|
||
## 跨任务共享接口(Task 1 产出,Task 2/4/5 消费)
|
||
|
||
`tests/integration/conftest.py` 对外只有一个 fixture 与一个返回类型:
|
||
|
||
```python
|
||
@dataclass(frozen=True)
|
||
class PgSandbox:
|
||
"""一次性 PG 沙箱: 独立 schema + 可选独占登录角色。"""
|
||
|
||
schema: str
|
||
role: str | None
|
||
dsn: str # 已挂 options=-csearch_path=<schema>
|
||
bare_dsn: str | None # 同角色但不挂 search_path;role is None 时为 None
|
||
```
|
||
|
||
```python
|
||
async def pg_sandbox(
|
||
*,
|
||
ddl: str | None = None,
|
||
extra: Sequence[str] = (),
|
||
role: Literal["none", "owner", "grantee"] = "none",
|
||
grants: Sequence[str] = ("SELECT", "INSERT"),
|
||
) -> PgSandbox: ...
|
||
```
|
||
|
||
### 三种 `role` 的语义
|
||
|
||
覆盖现有全部六个 fixture 的需求,**不得再加第四种**:
|
||
|
||
| `role` | schema 属主 | `ddl`/`extra` 由谁执行 | 返回 DSN 的身份 | 对应今天的 fixture |
|
||
|---|---|---|---|---|
|
||
| `"none"` | admin | admin | admin | `fresh_schema` / `legacy_schema` / `pre_tenant_schema` / `partitioned_schema` |
|
||
| `"owner"` | 临时角色 | **临时角色自己**(故表属主 = 该角色) | 临时角色 | 无(本次新增,retention 全部用例用) |
|
||
| `"grantee"` | admin | **admin**(故表属主 = admin,与最小权限现场一致) | 临时角色(只被 `GRANT USAGE ON SCHEMA` + 表级 `grants`,**绝不 GRANT CREATE**) | `least_privilege_dsn` / `least_privilege_pre_tenant_dsn` |
|
||
|
||
### `ddl` / `extra` 的执行契约
|
||
|
||
1. **调用方传的 DDL 一律不带 schema 限定**(`CREATE TABLE llm_calls (...)`,不是 `CREATE TABLE {schema}.llm_calls`)。工厂在执行前对该连接 `SET search_path = <schema>`,由 search_path 定位。这条统一了两种今天并存的写法——`PG_DDL` 本就是裸表名,而 `_LEGACY_DDL` / `_PRE_TENANT_DDL` 今天带 `{schema}` 占位,**Task 5 要把这两个常量的 `{schema}.` 前缀去掉**。
|
||
2. `extra` 在**同一连接、同一 search_path** 下按给定顺序逐条执行,不包事务(分区子表这类 DDL 各自提交即可)。
|
||
3. `ddl is None` 时只建空 schema,不执行任何建表语句。
|
||
|
||
### 临时角色的 DSN 构造
|
||
|
||
- 密码:模块级常量(测试专用,非机密),沿用今天 `_PROBE_PASSWORD` 的做法。
|
||
- `bare_dsn`:把 admin DSN 里的 `//user:pass@` 段整体替换为 `//<role>:<密码>@`(`re.sub(r"//[^@/]+@", ...)`,`count=1`),**不追加任何 `options` 参数**——它的用途就是让 `search_path` 回落到 `"$user", public`。
|
||
- `dsn`:在 `bare_dsn` 基础上追加 `options=-csearch_path%3D<schema>`,分隔符按 DSN 里是否已有 `?` 选 `?` 或 `&`。
|
||
- `role="none"` 时 `dsn` 用 admin 身份加同样的 options,`bare_dsn` 为 `None`——admin 的裸 DSN 不对用例开放(设计 §7.1 约束 3)。
|
||
|
||
### 三条硬约束(设计 §5.1、§7.1,逐条都是验收点)
|
||
|
||
1. schema 名 `pgw_s_<12 位 hex>`、角色名 `pgw_r_<12 位 hex>`,**两者前缀有意不同**——同名会让 `"$user"` 遮蔽真表,最坏情况用例就测不到真现场。
|
||
2. 资源逐步登记:每建成一个对象就把它的清理动作入栈,`except BaseException` 时**逆序**执行并 re-raise;`yield` 之后的 teardown 走同一条清理路径。单个沙箱的清理顺序固定为 `DROP SCHEMA IF EXISTS <s> CASCADE` → `DROP OWNED BY <r>` → `DROP ROLE IF EXISTS <r>`(`DROP OWNED BY` 必须在 `DROP ROLE` 之前,否则角色仍持有对象无法删除)。一次用例内建多个沙箱时,沙箱之间也按 LIFO 清理。
|
||
3. `role != "none"` 时先查 `rolcreaterole OR rolsuper`,**在建任何对象之前** `pytest.skip`(`production_template` 的教训:`pytest.skip` 抛的是 `BaseException`,若在清理块内触发会去 DROP 从未建过的对象,把 skip 盖掉)。
|
||
|
||
---
|
||
|
||
## Task 1:沙箱工厂
|
||
|
||
- [ ] **文件**:`tests/integration/conftest.py`(新建)、`tests/integration/test_pg_sandbox.py`(新建)
|
||
|
||
**行为**:实现上文《跨任务共享接口》全部内容。DSN 读取沿用今天两个文件里的做法(`dotenv_values(".env")` 合并 `os.environ`,剥掉 `+driver`,缺则 `skip`,库名不以 `/polygateway` 结尾则 `pytest.fail`)——这段逻辑今天重复两份,本任务收敛为一份私有函数。
|
||
|
||
**测试要求(先红后绿的路径明确)**:先写 `test_pg_sandbox.py` 再写 `conftest.py`——此时 `pg_sandbox` fixture 不存在,pytest 报 `fixture 'pg_sandbox' not found`,六条用例全红,这就是本任务的先失败证据。随后实现工厂使其转绿。
|
||
|
||
| 用例 | 断言 |
|
||
|---|---|
|
||
| `role="none"` 建表 | 表落在 `sandbox.schema` 下;`sandbox.bare_dsn is None` |
|
||
| `role="owner"` 建表 | 表属主 = `sandbox.role`;`sandbox.role != sandbox.schema` 且两者前缀不同 |
|
||
| `role="owner"` 的 `bare_dsn` | `SHOW search_path` 为 `"$user", public`;用它解析 `llm_calls` 得到的**不是**沙箱里那张表 |
|
||
| `role="grantee"` | 该角色 `CREATE TABLE` 被拒(`asyncpg.exceptions.InsufficientPrivilegeError`),`INSERT` 正常 |
|
||
| **setup 中途失败** | 传一段必然报错的 `ddl`(如 `CREATE TABLE llm_calls (bad_type NOT_A_TYPE)`),捕获异常后查 `pg_namespace` / `pg_roles`:本次 uuid 对应的 schema 与角色**都不存在** |
|
||
| teardown 后无残留 | 在用例内部记下 `sandbox.schema` / `sandbox.role`,用一个**更外层**的 fixture(在 `pg_sandbox` 之后销毁)回查两者均已消失 |
|
||
|
||
**验证**:
|
||
```
|
||
conda run -n PolyGateway pytest tests/integration/test_pg_sandbox.py -v
|
||
```
|
||
预期全绿;随后手工查实例:`SELECT nspname FROM pg_namespace WHERE nspname LIKE 'pgw%'` 与 `pg_roles` 同款查询均为空。
|
||
|
||
---
|
||
|
||
## Task 2:retention 测试角色化,删除行数快照
|
||
|
||
- [ ] **文件**:`tests/integration/test_retention_tool_pg.py`(修改)
|
||
|
||
**必须在 Task 3 之前完成**——见文首 CAUTION。
|
||
|
||
**行为**:
|
||
|
||
1. 删除 `_public_count`、`before_public` 与那条行数断言;删除本地的 `_make_schema` / `_drop_schema` / `_search_path_dsn` / `dsn` fixture,全部改用 `pg_sandbox`。
|
||
2. **凡启动脚本的用例一律 `role="owner"`**(设计 §5.1,无一例外,含 dry-run 与分区让路两条)。
|
||
3. 现有三条用例的其余断言逐条保留:`将删除行数: 5`、`'acme': 3`、批次 1/3 存在而批次 4 不存在、`已删除 5 行`、剩余 `fresh-1`/`fresh-2`、分区表退出 3 且含 `DROP PARTITION`/`DETACH`、缺 asyncpg 退出 2。
|
||
4. 新增设计 §5.3 的**最坏情况**用例:用 `sandbox.bare_dsn`、不给 `--table`、`--older-than-days 7 --apply`。断言退出 **2**、stderr 非空且含 `llm_calls`、沙箱表一行不少。**不断言 PG 的英文错误原文**(`lc_messages` 不由测试掌握),**测试代码里不得出现 `public.llm_calls` 字面量**。
|
||
|
||
**测试证据(这条用例没有常规先红路径,如实记录)**:让它变红的唯一方式是把角色换回 admin superuser——那会真删共享表的行,绝不执行。它的证伪由 `findings/2026-08-26-issue18-shared-pg-test-isolation.md` §7 的探针 6/7 提供:同款临时角色对真表的 `COUNT` 与 `DELETE` 均返回 `InsufficientPrivilegeError`。**提交说明里必须写明这一点**,不得含糊成"已验证"。
|
||
|
||
其余改动的先红路径正常:删掉 `_public_count` 之前,先把三条既有用例切到沙箱并跑通(此时它们仍带旧断言),再删断言——若沙箱切换有问题,旧断言会先报出来。
|
||
|
||
**验证**:
|
||
```
|
||
conda run -n PolyGateway pytest tests/integration/test_retention_tool_pg.py -v
|
||
```
|
||
预期全绿;连跑三次结果一致。
|
||
|
||
---
|
||
|
||
## Task 3:`--table` 参数与精确解析
|
||
|
||
- [ ] **文件**:`tools/telemetry_retention.py`(修改)、`tests/unit/test_retention_tool.py`(修改)、`tests/integration/test_retention_tool_pg.py`(追加用例)
|
||
|
||
**顺序**:**先写测试再改脚本**——四条集成用例与五条单测在脚本未改时全部先红(`--table` 未定义,argparse 直接以退出码 1 拒绝,而用例期望的是别的码/别的 stdout),实现后转绿。这就是本任务的先失败证据;Task 2 已先行完成,故这些用例从第一次运行起就跑在沙箱角色之下。
|
||
|
||
**脚本行为**(设计 §4):
|
||
|
||
| 项 | 要求 |
|
||
|---|---|
|
||
| 参数 | `--table SCHEMA.NAME`,仅 `--backend postgres` 接受 |
|
||
| 校验(全部退出 **1**) | sqlite 给了它;不是恰好两段;任一段为空;任一段含 `.` 或 `"`;**表名段不等于 `llm_calls`** |
|
||
| 解析 | 给了 `--table` 时用 `to_regclass($1)` 传 `"<schema>"."llm_calls"`(`_quote` 包裹),绕开 `search_path`;未给时维持今天的裸 `TABLE` 解析 |
|
||
| 解析不到 | 退出 **2**,消息点名显式指定的表,并附一句"PG 中未加引号建的标识符在 catalog 里是小写" |
|
||
| 无权限 | 后续 `COUNT` 抛 `PostgresError`,走既有 except → 退出 **2**(不新增分支) |
|
||
| 分区表 | 仍退出 **3**,逻辑不动 |
|
||
| 提示行 | `--apply` 且**未**给 `--table` 时,在"目标表: x.y"之后打印一行,指出目标由 `search_path` 推断、可用 `--table` 钉死;dry-run 不打 |
|
||
|
||
`--help` 的 epilog 补两句:本脚本只清理 `llm_calls`;含点或引号的复杂标识符不支持,此时退回不给 `--table` 的路径。
|
||
|
||
**单测**(`tests/unit/test_retention_tool.py`,不连库):`TestUsageErrors` 加五条,对应上表五种退出 1 的情形,逐条断言 stderr 含 `--table`;`TestHelp` 加一条断言 epilog 点明表名固定为 `llm_calls`。
|
||
|
||
**集成用例**(`test_retention_tool_pg.py`,全部 `role="owner"`):
|
||
|
||
| 用例 | 构造 | 预期 |
|
||
|---|---|---|
|
||
| 显式指定成功 | `--table <sandbox.schema>.llm_calls` + `--apply` | 退出 0,删除结果与不给 `--table` 时逐条一致 |
|
||
| 指向不存在的 schema | `--table pgw_s_nosuchxxxxxxxx.llm_calls` | 退出 **2**,stderr 点名该表;沙箱表一行不少 |
|
||
| 指向无权的表 | 建两个 `role="owner"` 沙箱,用 A 的 DSN 指 B 的表 | 退出 **2**;A、B 两张表都不变 |
|
||
| 指向分区表 | 分区沙箱 + `--table` | 仍退出 **3**,含 `DROP PARTITION` / `DETACH` 字样 |
|
||
| 提示行(设计验收 #2) | 沙箱 DSN + `--apply`,**不给** `--table` | stdout 含推断提示。设计原写"单测断言 stdout",但该行只在 PG 分支打印、不连库触发不到,故落在集成层;设计 §11 判据 2 已同步更正 |
|
||
|
||
**验证**:
|
||
```
|
||
conda run -n PolyGateway pytest tests/unit/test_retention_tool.py tests/integration/test_retention_tool_pg.py -v
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4:7 条用例迁出真表,`_RUN_PREFIX` 拆职责
|
||
|
||
- [ ] **文件**:`tests/integration/test_postgres_telemetry.py`(修改)
|
||
|
||
**行为**:
|
||
|
||
1. 七条用例改用 `pg_sandbox(role="none")`:`TestObservabilityColumns::test_values_round_trip`、`TestSchema` 三条、`TestDegradation::test_row_failure_does_not_poison_later_rows` 与 `test_aclose_idempotent`、`TestPoolFootprint::test_pool_does_not_preconnect_and_stays_within_pool_max`。
|
||
2. `test_schema_has_frozen_columns_in_order` 的 `information_schema` 查询补 `table_schema = $1`(设计 §6.2;仓库注释已记载该隐患)。
|
||
3. `TestPoolFootprint` **保留唯一 `application_name`**,就地生成 uuid(设计 §6.1)——这是实例级资源,schema 隔离对它无效。
|
||
4. 删除 `_RUN_PREFIX` 的行隔离用途:`_cid()` 的 63 处调用机械替换为字面量(`_cid("c1")` → `"c1"`);5 处 `LIKE` 逐条处置——`dsn` fixture teardown 的 `DELETE` 整条删除,`test_concurrent_writes_all_land` 的计数改 `COUNT(*)`,其余三处(legacy / least_privilege / manual-lp)改为不带前缀的精确条件。
|
||
5. 删除已无引用的本地 `dsn` fixture 与其 teardown。
|
||
|
||
**测试证据**:判据 6c 有明确先红路径——先在库里手工留一个残留同名表(`CREATE SCHEMA pgw_s_leftover; CREATE TABLE pgw_s_leftover.llm_calls (call_id TEXT)`),此时 `test_schema_has_frozen_columns_in_order` 因少了 `table_schema` 过滤而红;补上过滤后转绿;用完删掉该残留 schema。其余六条属迁移,证据形式是迁移前后断言逐条对照(设计 §11 判据 6),差异只允许出现在"表在哪"与"查询是否带 schema 过滤"两处——**这是回归门不是先红门,提交说明里如实这么写**。
|
||
|
||
**验证**:
|
||
```
|
||
conda run -n PolyGateway pytest tests/integration/test_postgres_telemetry.py -v
|
||
```
|
||
判据 6b 另做:两个 shell 同时跑 `TestPoolFootprint` 那一条,两边都绿。
|
||
|
||
---
|
||
|
||
## Task 5:其余 fixture 收敛到工厂
|
||
|
||
- [ ] **文件**:`tests/integration/test_postgres_telemetry.py`(修改)
|
||
|
||
**行为**:
|
||
|
||
1. `legacy_schema`、`pre_tenant_schema`、`fresh_schema`、`partitioned_schema` 改为 `role="none"`;`least_privilege_dsn`、`least_privilege_pre_tenant_dsn` 改为 `role="grantee"`。
|
||
2. 按接口契约,`_LEGACY_DDL` 与 `_PRE_TENANT_DDL` 两个常量去掉 `{schema}.` 前缀与 `.format(schema=...)` 调用,改为裸表名由工厂的 search_path 定位。
|
||
3. `production_template` **不收敛**:它要建三个角色、跑 README 解析出的整套模板 SQL、按月建分区,权限语义与失败期清理都是它自己的(设计 §7.1 末段与 Codex 意见 3)。工厂强行接管会把这些语义压扁。本任务只把它内部的 `_cid()` 调用一并处理掉。
|
||
|
||
**测试证据**:这些 fixture 的既有用例断言**一行不改**——它们是这次收敛的验收器,改了就失去验收意义。这是回归门。
|
||
|
||
**验证**:同 Task 4 的命令,预期全绿;在无 CREATEROLE 的账号下 `least_privilege` 系列仍能正确 skip。
|
||
|
||
---
|
||
|
||
## Task 6:lint 门与字面量清理
|
||
|
||
- [ ] **文件**:`Makefile`(修改)、`tests/integration/*.py`(注释措辞)
|
||
|
||
**行为**:`lint` 与 `check` 各加一步——`tests/` 下命中字面量 `public.llm_calls` 即 `exit 1` 并打印命中行。注释与 docstring **同样不豁免**,现有"共享的 public.llm_calls"改写为"共享表 `llm_calls`"。
|
||
|
||
Makefile 里这道门的注释必须写明它的定位(设计 §7.2):**烟雾报警器,不是隔离证明**——它拦不住 `f"{schema}.{table}"` 拼接与参数化查询,真正的隔离来自工厂不交出 admin DSN、脚本以无权角色运行。
|
||
|
||
**测试证据**:故意加一行含该字面量的注释 → `make lint` 失败并打印该行;移除后 → 通过。
|
||
|
||
**验证**:
|
||
```
|
||
make lint && make check
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7:独立验证(合并前硬门)
|
||
|
||
- [ ] 派**全新上下文**的 verifier subagent(`verification-before-completion`),交给它设计 §11 的判据表逐条核对,重点:
|
||
- 判据 3(最坏情况删不掉任何行)是否真由权限拒绝达成,而非碰巧——它没有先红证据,须由 verifier 独立复核 findings §7 的探针与用例断言是否真的对应同一条防线
|
||
- 判据 4:整套 `tests/integration` 连跑三次,**其间由 verifier 手工改动真表行数**(插入若干行再删掉),全程应无任何用例受影响
|
||
- 判据 6:7 条用例迁移前后断言逐条对照
|
||
- `--table` 的五种退出 1 与三种退出 2/3 是否都有用例覆盖
|
||
- `tests/` 与实例上是否留下任何 `pgw_%` 残留
|
||
|
||
---
|
||
|
||
## Task 8:文档与版本号
|
||
|
||
- [ ] **文件**:`README.md`、`CHANGELOG.md`、`pyproject.toml`、`src/polygateway/__init__.py`
|
||
|
||
- README:`--table` 用法落在两处——"存量兜底"表格行与 SQLite 侧段落之后的脚本说明段;写明表名固定为 `llm_calls`。安装约束是 `>=1.3.0,<2` 范围式,**本版无需改**(已核)。
|
||
- CHANGELOG:按设计 §10 如实写明 `tools/` 与 `tests/` 都不在 pip 包内,**1.3.2 的 wheel 与 1.3.1 在库代码上逐字节相同**,本版内容是运维脚本的契约扩展与测试确定性,不得包装成库能力更新。
|
||
- 版本号两处一致改 `1.3.2`。
|
||
- `make wiki-check WIKI=<路径>` 跑过(公共行为变更须同步用户文档站,`docs-convention.md` §2)。
|
||
|
||
---
|
||
|
||
## Task 9:发布 1.3.2
|
||
|
||
- [ ] 按 CLAUDE.md §4.4.1 九步执行,一步不跳:合并 main(`--no-ff`)→ 在 main 上重跑 `make lint` 与全套件 → **显式跑 `pytest -m slow`** → 打 tag 并 push → `rm -rf dist && python -m build && twine check` → 上传 registry(token 走 `TWINE_PASSWORD`,不进命令行)→ `pip download` 验证并解包确认 → 建 Release + 挂仓库 → 以下游视角打开包页面与 Releases 页核对。
|
||
- [ ] 关闭 issue #18,正文指向本计划与设计。
|
||
|
||
---
|
||
|
||
## 审查留痕(Codex,2026-08-26)
|
||
|
||
报 5 项,**全部采纳**:
|
||
|
||
| # | 意见 | 处置 |
|
||
|---|---|---|
|
||
| 1 | `ddl`/`extra` 的执行身份、search_path、顺序、schema 占位、失败清理顺序都没写成契约 | 新增《`ddl`/`extra` 的执行契约》一节;并据此在 Task 5 追加"去掉两个 DDL 常量的 `{schema}` 占位"这一步 |
|
||
| 2 | 临时角色的密码来源与 DSN 构造规则缺失 | 新增《临时角色的 DSN 构造》一节 |
|
||
| 3 | **Task 1 先实现 `--table`、Task 3 才写集成用例,先红路径不可能成立** | 采纳,任务重排:沙箱工厂 → retention 角色化 → `--table`(测试先写)。重排同时让 `--table` 的集成用例从第一次运行起就在沙箱角色之下,与文首 CAUTION 一致 |
|
||
| 4 | 工厂测试缺"先写失败测试"的明确步骤 | Task 1 写明:先写 `test_pg_sandbox.py`,此时 `fixture 'pg_sandbox' not found` 全红 |
|
||
| 5 | 设计验收 #2 说"单测断言 stdout",计划却放在集成层 | 核实后确认是**设计写错了**——该提示行只在 PG 分支打印,不连库的单测触发不到。已就地更正设计 §11 判据 2,并在 Task 3 注明 |
|
||
|
||
另外据 Codex 对 Task 4/5 的观察,两处证据形式(回归门而非先红门)已在任务里如实标注,不含糊成"已验证"。
|
||
|
||
## Wiki 注册
|
||
|
||
```bash
|
||
.claude/tools/research_wiki.py add_entity research-wiki/ --type plan \
|
||
--id 2026-08-26-issue18-pg-test-isolation --title "issue #18 实现计划"
|
||
.claude/tools/research_wiki.py add_edge research-wiki/ \
|
||
--from "plan:2026-08-26-issue18-pg-test-isolation" \
|
||
--to "design:2026-08-26-issue18-pg-test-isolation" --type implements
|
||
.claude/tools/research_wiki.py rebuild_index research-wiki/
|
||
```
|