test(boot): isolate plugin-manager Git children from host command-line config
The self-hosted Windows serial lane failed 12 cases from one cause: the host exported Git's indexed command-line configuration (`GIT_CONFIG_COUNT` with `GIT_CONFIG_KEY_0` / `GIT_CONFIG_VALUE_0`), and the shared subprocess scrub removes credential-shaped names, so a child inherited the counter without its key and died with `error: missing config key GIT_CONFIG_KEY_0` before it read any configuration file. Both spec files now delete that group from their own environment before they spawn Git, because they judge profile behavior rather than host state. The product-side handling stays out of this change; it is preserved on the branch `deferred/git-command-line-config-scrub-20260927` for a later product PR. GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.bare GIT_CONFIG_VALUE_0=true pnpm exec vitest run packages/boot/plugin-manager/tests/github-connection.spec.ts packages/boot/plugin-manager/tests/manager.spec.ts -> 12 failed at the parent revision, 95/95 passed with the isolation.
This commit is contained in:
1 parent
1badc22b1b
commit
1c0234e7b1
6 files changed
+122
-2
No files matched your search
+22
@@ -0,0 +1,22 @@
|
||||
# Bilingual-pair consistency record for 2026-09-27-plugin-manager-git-child-environment.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-09-27-plugin-manager-git-child-environment.md
|
||||
/agent-note-plugin-manager-git-children-isolate-the-host-s-command-line-configuration:
|
||||
en: ebb8f6a97dcc126a
|
||||
zh: 4f7fbb487136f218
|
||||
/agent-note-plugin-manager-git-children-isolate-the-host-s-command-line-configuration/problem:
|
||||
en: 67f7316995be5f48
|
||||
zh: fbb8dda3946ff1f3
|
||||
/agent-note-plugin-manager-git-children-isolate-the-host-s-command-line-configuration/decision:
|
||||
en: 7723360996b8b09e
|
||||
zh: 78d8dfa61b1711ec
|
||||
/agent-note-plugin-manager-git-children-isolate-the-host-s-command-line-configuration/alternatives-considered:
|
||||
en: 3f4e5b0bc9a8a961
|
||||
zh: 5f39ffd20e076914
|
||||
/agent-note-plugin-manager-git-children-isolate-the-host-s-command-line-configuration/verification:
|
||||
en: 7c8b5b8f4002c71a
|
||||
zh: 59493bd968ba34bf
|
||||
/agent-note-plugin-manager-git-children-isolate-the-host-s-command-line-configuration/consequences:
|
||||
en: b81437323c8ae6e2
|
||||
zh: 79f8d0926218ae2e
|
||||
@@ -0,0 +1,29 @@
|
||||
# Agent Note: Plugin-manager Git children isolate the host's command-line configuration
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-09-27-plugin-manager-git-child-environment.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The self-hosted Windows serial lane failed 13 cases in run 36227311113 (job 108363684565); 12 share one cause: [github-connection.spec.ts](../../../../packages/boot/plugin-manager/tests/github-connection.spec.ts) 9 (the six install-address forms and three proxy cases) and [manager.spec.ts](../../../../packages/boot/plugin-manager/tests/manager.spec.ts) 3 real-Git install cases. The host carried Git's indexed command-line configuration — `GIT_CONFIG_COUNT` with `GIT_CONFIG_KEY_0` / `GIT_CONFIG_VALUE_0`, the group Git hands to `git -c` calls and their hook children. The shared subprocess scrub (`scrubbedParentEnv`) removes credential-shaped names, so it stripped the key and kept the counter; Git reads the counter first, and every child died with `error: missing config key GIT_CONFIG_KEY_0` / `fatal: unable to parse command-line config` (exit 128) before opening any configuration file.
|
||||
|
||||
## Decision
|
||||
|
||||
Both spec files delete Git's command-line configuration group from their own process environment before they spawn Git, through [isolateGitCommandLineConfig](../../../../packages/boot/plugin-manager/tests/git-environment.ts). These specs judge profile behavior — profile Git configuration, real installs — rather than host state, so a host that exports the group no longer changes their outcome. The helper restores the removed entries after the file with `vi.stubEnv(name, undefined)` and `vi.unstubAllEnvs()`.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Remove the group whole in `scrubbedParentEnv`.** The product-side fix treats the counter and its keys as one unit, but it changes shared subprocess behavior for every consumer, which the review directive for this change excludes. It is preserved on the branch `deferred/git-command-line-config-scrub-20260927` for a future product PR with its own justification.
|
||||
|
||||
**Pin `GIT_CONFIG_COUNT: '0'` at the plugin-manager call sites.** `packages/deliverables/workspace-changes/src/git.ts` does exactly that for its children, so the same two lines in `github-connection.ts` and `operations.ts` would cover this class. That is still a product behavior change, so it stays out of scope here.
|
||||
|
||||
**Clear the group in the lane's workflow.** The group reaches the job from the host image rather than from a step this repository owns, and other Git-spawning specs would stay exposed.
|
||||
|
||||
## Verification
|
||||
|
||||
`GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.bare GIT_CONFIG_VALUE_0=true pnpm exec vitest run packages/boot/plugin-manager/tests/github-connection.spec.ts packages/boot/plugin-manager/tests/manager.spec.ts` fails 12 cases at the parent revision with the lane's assertion set and passes 95/95 with the isolation in place.
|
||||
|
||||
## Consequences
|
||||
|
||||
The two specs pass on a host that exports Git command-line configuration. The product's treatment of such hosts is unchanged, so the same environment still breaks any other consumer of the shared scrub; a future product change to `scrubbedParentEnv` must revisit this note.
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
# Agent Note: plugin-manager 的 Git 子进程隔离宿主机的命令行配置
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-27-plugin-manager-git-child-environment.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
自托管 Windows 串行 lane 在运行 36227311113(作业 108363684565)中失败 13 个用例,其中 12 个同一成因:[github-connection.spec.ts](../../../../packages/boot/plugin-manager/tests/github-connection.spec.ts) 9 个(六种安装地址形式与三个代理用例)与 [manager.spec.ts](../../../../packages/boot/plugin-manager/tests/manager.spec.ts) 3 个真实 Git 安装用例。该宿主机携带 Git 的索引式命令行配置——`GIT_CONFIG_COUNT` 加上 `GIT_CONFIG_KEY_0` / `GIT_CONFIG_VALUE_0`,即 Git 交给 `git -c` 调用及其 hook 子进程的那一组变量。共享的子进程清洗(`scrubbedParentEnv`)按凭据形态的名字移除变量,于是移除了键却保留了计数;Git 先读计数,每个子进程都在打开任何配置文件之前以 `error: missing config key GIT_CONFIG_KEY_0` / `fatal: unable to parse command-line config`(退出码 128)失败。
|
||||
|
||||
## Decision
|
||||
|
||||
这两个 spec 文件在派生 Git 之前,从自身进程环境里删除 Git 的命令行配置组,由 [isolateGitCommandLineConfig](../../../../packages/boot/plugin-manager/tests/git-environment.ts) 完成。这些 spec 判定的是 profile 行为——profile 的 Git 配置、真实安装——而不是宿主机状态,因此导出该组的宿主机不再改变它们的结果。辅助函数在文件结束后用 `vi.stubEnv(name, undefined)` 与 `vi.unstubAllEnvs()` 还原被移除的条目。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**在 `scrubbedParentEnv` 中整体移除该组。** 产品侧修复把计数与它的键当作一个整体,但它会改变所有消费者共享的子进程行为,而本次改动的 review 指令排除了这一点。该修复保留在分支 `deferred/git-command-line-config-scrub-20260927`,留待将来带自身论证的产品 PR。
|
||||
|
||||
**在 plugin-manager 的调用点固定 `GIT_CONFIG_COUNT: '0'`。** `packages/deliverables/workspace-changes/src/git.ts` 正是这样对待自己的子进程,因此在 `github-connection.ts` 与 `operations.ts` 加同样两行即可覆盖这一类。但那仍属产品行为改动,故不在此范围内。
|
||||
|
||||
**在 lane 的工作流里清除该组。** 该组来自宿主机镜像,而不是本仓库拥有的某个步骤,且其他会派生 Git 的 spec 仍会暴露。
|
||||
|
||||
## Verification
|
||||
|
||||
`GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.bare GIT_CONFIG_VALUE_0=true pnpm exec vitest run packages/boot/plugin-manager/tests/github-connection.spec.ts packages/boot/plugin-manager/tests/manager.spec.ts` 在父修订上以 lane 的断言集合失败 12 个用例,加入隔离后 95/95 通过。
|
||||
|
||||
## Consequences
|
||||
|
||||
这两个 spec 在导出 Git 命令行配置的宿主机上通过。产品对此类宿主机的处理未变,因此同一环境仍会破坏共享清洗的其他消费者;将来若改动 `scrubbedParentEnv`,必须回头审视本记录。
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Keep host Git command-line configuration out of the Git children these specs spawn.
|
||||
*
|
||||
* Git exports its indexed command-line configuration (`GIT_CONFIG_COUNT` with
|
||||
* `GIT_CONFIG_KEY_n` / `GIT_CONFIG_VALUE_n`, or the older `GIT_CONFIG_PARAMETERS`)
|
||||
* to `git -c` calls and to hook children. The shared subprocess scrub removes
|
||||
* credential-shaped names, so a child that inherits the counter without its key
|
||||
* fails before it reads any configuration file (`error: missing config key
|
||||
* GIT_CONFIG_KEY_0`). These specs judge profile behavior rather than host state,
|
||||
* so they run Git without the host's group.
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/** Names Git uses for its indexed command-line configuration. */
|
||||
const COMMAND_LINE_CONFIG_NAME = /^GIT_CONFIG_(?:COUNT|PARAMETERS|KEY_\d+|VALUE_\d+)$/u
|
||||
|
||||
/**
|
||||
* Delete Git's command-line configuration group from this process's environment.
|
||||
* @returns a disposer restoring every environment entry the call removed.
|
||||
*/
|
||||
export function isolateGitCommandLineConfig(): () => void {
|
||||
for (const name of Object.keys(process.env)) {
|
||||
if (COMMAND_LINE_CONFIG_NAME.test(name)) vi.stubEnv(name, undefined)
|
||||
}
|
||||
// `unstubAllEnvs` is the documented inverse of `stubEnv`; neither spec stubs another name.
|
||||
return () => { vi.unstubAllEnvs() }
|
||||
}
|
||||
@@ -8,15 +8,21 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import * as subprocess from 'execa'
|
||||
import { expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { afterAll, beforeAll, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { checkGithubConnection } from '../src/github-connection.ts'
|
||||
import { parseInstallSpec } from '../src/install-spec.ts'
|
||||
import { isolateGitCommandLineConfig } from './git-environment.ts'
|
||||
|
||||
vi.mock('execa', async (original) => {
|
||||
const module = await original<typeof import('execa')>()
|
||||
return { ...module, execa: vi.fn(module.execa) }
|
||||
})
|
||||
|
||||
// These cases spawn real Git, so the host's command-line configuration group must not reach its children.
|
||||
let restoreGitCommandLineConfig: () => void
|
||||
beforeAll(() => { restoreGitCommandLineConfig = isolateGitCommandLineConfig() })
|
||||
afterAll(() => { restoreGitCommandLineConfig() })
|
||||
|
||||
async function fixture() {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'dsh-github-check-'))
|
||||
onTestFinished(() => rm(dir, { recursive: true, force: true }))
|
||||
|
||||
@@ -9,7 +9,7 @@ import { tmpdir } from 'node:os'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { execa } from 'execa'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { expect, it, onTestFinished, vi } from 'vitest'
|
||||
import { afterAll, beforeAll, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import {
|
||||
boot, composeEntries, initProfile, loadProfileDirectory, readProfilePatches, readProfileManifest,
|
||||
reconcileProfilePatches, OPTIONAL_BUNDLES, PluginPackages, readPluginMeta, getDshRuntimeVersion,
|
||||
@@ -23,6 +23,12 @@ import { Group } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import * as operations from '../src/operations.ts'
|
||||
import * as githubConnection from '../src/github-connection.ts'
|
||||
import { parse, parseDocument } from 'yaml'
|
||||
import { isolateGitCommandLineConfig } from './git-environment.ts'
|
||||
|
||||
// These cases install through real Git and pnpm, so the host's command-line configuration group must not reach their children.
|
||||
let restoreGitCommandLineConfig: () => void
|
||||
beforeAll(() => { restoreGitCommandLineConfig = isolateGitCommandLineConfig() })
|
||||
afterAll(() => { restoreGitCommandLineConfig() })
|
||||
|
||||
async function fixture(reload: 'live' | 'startup' = 'live', overlay = false, prepare?: (ctx: Context) => void, config: Config = {}, packageManager?: ProfileContext['packageManager'], prepareFiles?: (dir: string) => void) {
|
||||
const temporaryHome = mkdtempSync(join(tmpdir(), 'plugin-manager-'))
|
||||
|
||||
Reference in new issue
Block a user