Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(cli): app-generator's two tests pin a DIFFERENT generator branch depending on cwd — so CI never covers the standalone branch #7807

Closed

Description

@claude

由 objectui#7799 的实现析出(PR #7806),测量得出、未修 —— 那张卡的范围是修 cwd 依赖的缺陷,而这个文件不是缺陷,是覆盖缺口。

事实

packages/cli/src/__tests__/app-generator.test.ts 在两种 cwd 下都绿(各 41 passed),且经反向探针确认不是空过。但探针同时暴露出:它逐字节相同的两条测试,在两种调用下钉的是不同的分支 ——

调用 cwd 实际被钉的分支
pnpm exec vitest run packages/cli/…(仓根,CI 跑的形式) 仓根 IN-WORKSPACE
pnpm --filter @object-ui/cli test(包目录) packages/cli STANDALONE

探针读数:仓根 41p;包目录 1 failed | 40 passed,AssertionError: PROBE7799 ambient isMonorepo: expected false to be true。

⇒ CI 只跑仓根形式,所以 STANDALONE 分支在 CI 里从来没有被覆盖过。 它被覆盖,只发生在有人恰好从包目录敲命令的时候。

⛔ 为什么 #7799 没有顺手修它 —— 那个判断是对的

contextOfCurrentProcess() 有意地在比较的两侧都镜像 ambient cwd,而被测的真实 createTempApp 也用同样的方式派生自己的上下文。⇒ 这个镜像两种 cwd 下都自洽。

⚠️ 只把测试那一侧改成从文件派生,会让镜像失同步,把一条本来通过的测试变红。 ⇒ #7799 的席位据此判它 immune 并停手,本席采纳。

修法方向(不是命令)

加第三条用例,用文件派生的根显式钉住 IN-WORKSPACE 分支,保留原有的 ambient 镜像不动 ⇒ 两个分支在任何调用下都被覆盖。

⛔ 不要改现有那两条(它们的 ambient 镜像是有意的)。⛔ 不要改 packages/cli 的 test 脚本 —— 那是 objectui#7800 记录的 devx 决策,超出执行席权限。

定级 p3

无用户可绊之处、不产生错数据、不挡任何门。代价是一个分支的 CI 覆盖长期为零,而且从读代码上看不出来(两条测试看起来覆盖了两个分支,实际取决于你从哪敲命令)。

Refs: objectui#7799(析出它的那张卡)· PR #7806(探针读数)· objectui#7800(test 脚本 cwd 的 devx 决策,pm:awaiting-maintainer)


Generated by Claude Code

Activity

  1. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    Claim: PM loop, round 6 — all-opus lane (Clause-② no)
    Session: session_01KbJQ1y1J12nZxYzFWhP8Q3
    Branch: claude/issue-7807-app-generator-inworkspace-case
    Worktree: objectui-issue-7807
    Domain: domain:ui
    File surface: packages/cli/src/__tests__/app-generator.test.ts · 本卡自己的 .changeset/*.md. ⛔ Stop on breach: 任何非测试文件的改动(含 createTempApp / contextOfCurrentProcess 的实现)、任何 packages/cli/package.json 的 test 脚本改动、任何改动现有那两条测试的行为。Explain in the report.
    Container & model: S, mode:subagent, model: opus
    Clause-②: no —— 新增一条测试用例。不改变契约接受/拒绝行为,不扩大公开面。⚠️ Void condition: 任何 packages/*/src 下非测试文件的改动。
    Serial constraints cleared: **PR #7806(卡 #7799)**已交付,它判定 app-generator.test.ts immune 并未修改它(本卡正是从它的探针读数析出的);若 #7806 尚未落地也不阻塞(本卡加的是新用例,不碰它判过的东西)。在飞的 #7798 / #7795 / #7744 / #7745 都与 packages/cli/ 不相交。


    事实(#7799 的席位实测,⛔ 不必重测)

    app-generator.test.ts 在两种 cwd 下都绿(各 41 passed),且经反向探针确认非空过。但探针同时暴露:逐字节相同的那两条测试,在两种调用下钉的是不同分支 ——

    调用 cwd 实际被钉的分支
    pnpm exec vitest run packages/cli/…(仓根,CI 跑的形式) 仓根 IN-WORKSPACE
    pnpm --filter @object-ui/cli test packages/cli STANDALONE

    探针读数:仓根 41p;包目录 1 failed | 40 passed,AssertionError: PROBE7799 ambient isMonorepo: expected false to be true。

    ⇒ CI 只跑仓根形式 ⇒ STANDALONE 分支在 CI 里从来没被覆盖过,而且从读代码上看不出来(看起来两条测试覆盖了两个分支)。

    ⛔ 最要紧的一条:不要"修"现有那两条

    contextOfCurrentProcess() 有意在比较的两侧都镜像 ambient cwd,而被测的真实 createTempApp 也用同样方式派生自己的上下文 ⇒ 这个镜像两种 cwd 下都自洽。

    ⚠️ 只把测试那一侧改成从文件派生,会让镜像失同步,把一条本来通过的测试变红。 #7799 的席位据此判它 immune 并停手,本席采纳。⇒ ⛔ 现有两条一个字都不要动。

    交付物

    加第三条用例,用文件派生的根显式钉住 IN-WORKSPACE 分支,保留原有的 ambient 镜像不动 ⇒ 两个分支在任何调用下都被覆盖。

    ⭐ 路径派生用从 import.meta.url 逐段上溯(PR #7796 落地、#7806 沿用的同一个拼法)。⛔ 不要引入第三种拼法;⛔ 不要用 new URL(rel, import.meta.url) —— 它在本仓测试变换里被 Vite 重写成 http://localhost:3000/@fs/…,fileURLToPath 两种 cwd 下都抛。

    ⚠️ 若你判断同时该补一条显式钉住 STANDALONE 的用例,可以做 —— 但分开两条、各自显式,⛔ 不许再让"钉哪个分支"取决于 cwd。

    ⭐ 验收 —— 判据是「两种 cwd 下覆盖相同」

    • 两种 cwd 各跑一次,都绿,且报出文件数/用例数:仓根 pnpm exec vitest run packages/cli/src/__tests__/app-generator.test.ts;包目录 pnpm --filter @object-ui/cli test。⇒ 新用例在两边都跑到、都绿。
    • ⭐ 点亮控制:把新用例的根派生临时打回 ambient cwd 形式,确认它在包目录下变红并指名(因为那时它又会去钉 STANDALONE);然后在 trap … EXIT INT TERM 下用 git checkout HEAD -- <绝对路径> 还原,以 blob hash 相等 + 空 git diff HEAD 证明 —— ⛔ 证明靠状态,不靠 trap 是否触发;变异前先用 hash 确认落盘(空 hash 读作失败,不读作「没什么可比」)。
    • ⭐ 证明你没有动坏现有那两条:给出改动前后 app-generator.test.ts 在两种 cwd 下的用例数对照(改动前各 41p,改动后应为 41+N,且没有任何一条从通过变成不通过)。
    • pnpm --filter @object-ui/cli type-check(走 turbo),报出任务数与 exit code。

    变更集

    改动落在发版包 src/ 下(虽是测试文件)⇒ 按 AGENTS.md:161 大概率需要。⛔ 不要凭规则推断:跑 node scripts/check-changeset-presence.mjs,判词原样抄进报告;欠就写空 frontmatter。⛔ 永不写 major。

    ⚠️ 五个仪器陷阱

    ① 裸 tsc --noEmit 不是本仓的门(turbo type-check dependsOn ^build,到不了非包目录;构建前的 TS2882 是未满足的前提、不是结果)② 本容器没有「未认证的探测」,匿名样子的 POST 是真写入,探通道只用 GET ③ scripts/__tests__/check-doc-links.test.ts 共享盒上 15s 超时是已知 flake ④ ⛔ new URL(rel, import.meta.url) 被 Vite 重写(见上)⑤ ⛔ 不用两点式 origin/main..HEAD 报文件数(并发落地会虚报,实测 4 vs 真实 2),以记录的 base 或 GitHub 的 changed_files 为准。

    契约复审

    Clause-② 是 no ⇒ 不排在契约复审后面。


    Generated by Claude Code

  2. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 7807,
      "status": "done",
      "branch": "claude/issue-7807-app-generator-inworkspace-case",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7810",
      "premise_still_valid": true,
      "summary": "Added two cases to the 'generation onto disk' block of packages/cli/src/__tests__/app-generator.test.ts that NAME the generator context branch instead of inheriting it from the ambient cwd: one pins IN-WORKSPACE against the file-derived REPO_ROOT (the file's existing import.meta.url walk-up, PR 7796's spelling, not a third one), the other pins STANDALONE against the test's own mkdtemp directory, both sides naming the same path. The existing two cases are byte-identical: the only removed line in the entire diff is the vitest import, which gains vi. process.chdir() is not an available lever here - it throws ERR_WORKER_UNSUPPORTED_OPERATION under the unit project's pool: 'threads' (measured in this container with a worker_threads probe) - so process.cwd() is stubbed for exactly the one synchronous writer call and restored in a finally. I added the second, explicitly-STANDALONE case because one explicit IN-WORKSPACE case alone would not equalise coverage: from the repo root the ambient pair also pins IN-WORKSPACE, so STANDALONE would still be unreached in the only form CI runs. Two files changed (GitHub changed_files = 2, matching the recorded base a617bb8d3): the test file and an empty-frontmatter changeset. No non-test file under packages/*/src was touched, and packages/cli/package.json's test script is untouched - the Clause-2 void condition did not trigger.",
      "tests": "All readings below are at the final commit afa1722c9 (git rev-parse --short HEAD), working tree clean, and every exit code was captured by redirecting first (cmd > log 2>&1; EXIT=$?), never through a pipe. BOTH-CWD COUNTS, before -> after for the file: (A) repo root, 'pnpm exec vitest run packages/cli/src/__tests__/app-generator.test.ts': before 'Test Files 1 passed (1)' / 'Tests 41 passed (41)' exit 0 -> after 'Test Files 1 passed (1)' / 'Tests 43 passed (43)' exit 0. (B) cwd packages/cli, 'pnpm exec vitest run --root ../.. packages/cli/src/__tests__/app-generator.test.ts': before '1 passed (1)' / '41 passed (41)' exit 0 -> after '1 passed (1)' / '43 passed (43)' exit 0. DECLARED NARROWING: (B) is the criterion-named package-dir command narrowed to this one file. I also ran the unnarrowed criterion form, 'pnpm --filter @object-ui/cli test' (cwd packages/cli, whole package): before 'Test Files 16 passed (16)' / 'Tests 246 passed (246)' exit 0 -> after 'Test Files 16 passed (16)' / 'Tests 248 passed (248)' exit 0. The narrowing was necessary because an appended path argument cannot narrow that form - Vitest unions positional filters with the script's own 'packages/cli/', so all 16 files still ran (measured: 16 files / 246 tests with the path appended). +2 exactly in every column, and no case went from passing to failing; a --reporter=verbose run at afa1722c9 lists all five cases of the block, including the untouched original two, with a check mark. LIT CONTROL: mutation writingFrom(REPO_ROOT, ...) -> writingFrom(process.cwd(), ...), one occurrence, under trap '... git checkout HEAD -- ABSOLUTE_PATH' EXIT INT TERM with the absolute path taken from git rev-parse --show-toplevel. Mutation confirmed ON DISK before either leg ran: anchor counts flipped OLD=1 NEW=0 -> OLD=0 NEW=1, and the blob hash moved - HEAD_BLOB=f50502442602fdc45a8cf07df185283cc4060158, PRE_HASH=f50502442602fdc45a8cf07df185283cc4060158 (equal, so the tree really was at HEAD), MUT_HASH=f9c5acd8baa1096eed34f439a14b6656296b23ea (differs). Neither hash was empty; the script treats an empty hash as FAILURE and exits. Leg A (cwd packages/cli): RED AND NAMES ITSELF - 'FAIL |unit| packages/cli/src/__tests__/app-generator.test.ts > generation onto disk > writes the IN-WORKSPACE map when the generator runs at a workspace root', 'AssertionError: expected { ...(8) } to deeply equal { ...(8) }', 'Tests 1 failed | 42 passed (43)', exit 1. Leg B (repo root): green, '43 passed (43)', exit 0 - which is the finding restated: under the ambient form the only invocation CI runs stays green while the branch it pins has silently changed. RESTORATION PROVED BY STATE, not by whether the trap fired: POST_HASH=f50502442602fdc45a8cf07df185283cc4060158 (equal to HEAD_BLOB), 'git diff HEAD --stat' produced 0 bytes, 'git status --porcelain' empty. NO REBUILD LEG APPLIES and none was run: the test imports the subject as '../utils/app-generator.js', a relative source specifier Vite resolves to the .ts source, so no dist/ artifact sits between the mutation and the run. TYPE-CHECK THROUGH TURBO: 'pnpm exec turbo run type-check --filter=@object-ui/cli --concurrency=2' -> 'Tasks: 10 successful, 10 total', exit 0. (A bare tsc --noEmit is not this repo's gate; the turbo task dependsOn ^build.) CHANGESET GATE, verdict lines verbatim. Before: '❌  1 source file(s) of 1 released package(s) changed, and this change adds no changeset:'. So it was owed; I added .changeset/cli-app-generator-explicit-branch-cases.md with an EMPTY frontmatter. After: '✅  1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/cli-app-generator-explicit-branch-cases.md.' followed by 'Every one of them has an EMPTY frontmatter - declared as releasing nothing, which is the explicit exemption and a complete answer to this gate.', exit 0. 'node scripts/check-changeset-no-major.mjs' also exit 0: '✅  No changeset declares a `major` bump.' LINT: eslint on the changed file only - 1 file linted, 0 errors, 0 warnings, exit 0. Repo-wide 'pnpm lint' was NOT run; that is CI's run. CONTROL-CHARACTER SCAN: grep -naP over the changed test file and over the PR body found nothing (exit 1). No objectui gate script named check-nul-bytes.mjs exists in this repo, so the scan is the manual form.",
      "mcp_calls": "1 - a single search_issues, for the out-of-scope dedup check; it returned 'API rate limit already exceeded for user ID 318158314' and yielded no reading. Every other GitHub read and write in this run went through repo-scoped REST (issue body, comments, PR create, PR body read-back, this comment).",
      "open_questions": [],
      "out_of_scope_findings": [
        "NOT FILED - both dedup channels were unavailable, handing this to PM to file. Finding: a path argument appended to a package's test script silently WIDENS instead of narrowing. Measured: 'pnpm --filter @object-ui/cli test packages/cli/src/__tests__/app-generator.test.ts' ran 'Test Files 16 passed (16)' / 'Tests 246 passed (246)' - the whole package - because the script is 'vitest run --root ../.. packages/cli/' and Vitest UNIONS positional filters, so the script's own 'packages/cli/' filter re-admits every file. It exits 0 and reads as a green narrowed run, and an agent reporting 'the file is green, 246 tests' would be reporting the package's count as the file's. This is a third member of the family scripts/vitest-invocation-guard.mjs already documents as traps 1 and 2, and it is documented nowhere: I grepped AGENTS.md and the guard's docstring and neither mentions it. Related to objectui#7800 (the test script's cwd devx decision, still open) - a decision there could remove the baked-in filter and with it this hazard - so it may belong as a sub-issue of that card rather than standalone. Suggested shape: label 'finding', no assignee, no pm:queue. WHY I DID NOT FILE IT: the required search-before-filing could not be performed. The repo-scoped REST read channel works in this seat, but the REST search endpoint returned HTTP 403 for both the real query and a known-hit control query; the fallback single targeted MCP search_issues then failed with 'API rate limit already exceeded for user ID 318158314'. With no working dedup channel I did not file blind."
      ]
    }

    Generated by Claude Code

  3. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    落地记录 —— PR #7810 已合入(本 lane 今日第 23 次落地)

    ⭐ 三条值得留下来的读数

    1. ⭐⭐ 点灯对照把这张卡的论点变成了一句可复读的话。

    变异 writingFrom(REPO_ROOT, …) → writingFrom(process.cwd(), …) 后:

    运行位置 结果
    cwd = packages/cli 红,并自报其名:writes the IN-WORKSPACE map when the generator runs at a workspace root,Tests 1 failed | 42 passed (43)
    cwd = 仓库根(CI 唯一跑的形式) 绿,43 passed (43)

    ⇒ 这正是卡片的论点原样复述:在 CI 唯一会跑的那个调用下它保持绿色,而它所钉住的分支已经悄悄变了。 ⭐ 一个会在事故树上空过的钉子。

    2. process.chdir() 在本仓的单元 project 下不可用 —— 这是实测,不是推测。

    pool: 'threads' 下 process.chdir() 抛 ERR_WORKER_UNSUPPORTED_OPERATION(用 worker_threads 探针在本容器实测)。⇒ 只能对恰好一次同步写调用打桩 process.cwd(),并在 finally 里还原。⛔ 想用 chdir 写这类测试的下一位不必再试一遍。

    3. 为什么加了两个用例而不是一个 —— 一个不够,理由是覆盖而不是对称。

    只加一个显式 IN-WORKSPACE 用例不能把覆盖补平:从仓库根跑时,原有的那对 ambient 用例也钉 IN-WORKSPACE,于是 STANDALONE 在 CI 唯一会跑的形式下仍然无人触及。⇒ 必须同时加一个显式 STANDALONE 用例(对着测试自己的 mkdtemp 目录),两侧指向同一条路径。

    ⚠️ 另:原有两个用例逐字节未动,整个 diff 里唯一被删的行是 vitest import(因为要加 vi)——「不动既有断言」这件事被做成了可核对的事实。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions