[BUG]: sf package uninstall hides the real uninstall error behind MALFORMED_QUERY
还没有人认领这个 Issue。
评估
- 难度
- 2/5
- 预计耗时
- 1-3 小时
- 新手友好度
- 56/100
- Issue 类型
- 缺陷
- 描述清晰度
- 描述清楚
- 活跃度
- 活跃
- 技术栈
- node.js, typescript
调研方向
Read src/package/packageUninstall.ts around getUninstallErrors and its callers, pollUninstall and SubscriberPackageVersion.uninstallStatus. Run the two named tests, test/package/uninstallPackage.test.ts and test/package/subscriberPackageVersion.test.ts, to check the malformed SOQL and error-handling paths. Done means both callers query with valid SOQL and their tests verify the resulting uninstall error.
由索引模型根据 Issue 内容生成。
描述
TL;DR: the SOQL in getUninstallErrors (src/package/packageUninstall.ts L30) is wrapped in
literal ". The patch below removes them and adds a test for each caller. I'd like to open it as
a PR on forcedotcom/packaging. If that's OK, assign this issue to me and I'll open it.
Summary
I'm a Salesforce employee (product management for ISV extensibility). I found this while testing
managed-package installs and uninstalls the way our ISV partners run them.
When an uninstall fails after the org accepts the request, sf package uninstall --wait and
sf package uninstall report hide the failure: no "uninstall failed", no request id, no reasons
(see Actual). I've hit it four times in three months in my own ISV test pipelines, once mistaking
it for a platform error and once losing a real failure behind a passing retry.
Cause
src/package/packageUninstall.ts L28-33:
const errorQueryResult = await conn.tooling.query<{ Message: string }>(
`"SELECT Message FROM PackageVersionUninstallRequestError WHERE ParentRequest.Id = '${id}' ORDER BY Message"`
);
The query throws before UNINSTALL_ERROR is built. Its only two callers:
pollUninstall,defaultbranch (L57-64):sf package uninstall --waitSubscriberPackageVersion.uninstallStatus,Status === 'Error'(subscriberPackageVersion.ts L215-222):sf package uninstall report
No test caught it: the pollUninstall error test
(uninstallPackage.test.ts L85)
stubs conn.tooling.query without checking its argument, and uninstallStatus's Error branch
has no test in
subscriberPackageVersion.test.ts.
Steps to reproduce
- Unit tests, under Node 22. Save the patch below as
tool01.patchin an empty directory, then
from that directory:
Result: 13 passing, 2 failing ("should send the uninstall request, and handle errorsgit clone https://github.com/forcedotcom/packaging && cd packaging git checkout 51fc61b # the commit the patch is cut against yarn install git apply ../tool01.patch # fix + tests git checkout -- src/package/packageUninstall.ts # undo only the fix npx mocha test/package/uninstallPackage.test.ts test/package/subscriberPackageVersion.test.ts
appropriately" and "uninstallStatus reports a failed request with its errors, using valid
SOQL"), each withAssertionError: expected '"SELECT Message FROM PackageVersionUn…' to equal 'SELECT Message FROM PackageVersionUni…' - Against a scratch org, the query string from L30:
Result:sf data query --use-tooling-api -o <org> -q "\"SELECT Message FROM PackageVersionUninstallRequestError WHERE ParentRequest.Id = '06y000000000000AAA' ORDER BY Message\""MALFORMED_QUERY: unexpected token: '"'. Without the outer quotes it runs
(totalSize: 0).
End to end, with a real uninstall that fails late (optional; needs a Dev Hub with a namespace)
Only an uninstall the org accepts and that fails later reaches this code. One the org refuses up
front (for example, a subscriber class references the package) is reported correctly. So this
recipe adds the blocking reference while a slow uninstall script holds the job open.
- Build a managed beta with one custom object (
UninstProbe__c) and an uninstall script that
waits 40 seconds:// sfdx-project.json, package directory { "path": "force-app", "package": "UninstLateFailProbe", "versionNumber": "0.1.0.NEXT", "uninstallScript": "UninstProbeHandler", "default": true }global without sharing class UninstProbeHandler implements UninstallHandler { global void onUninstall(UninstallContext ctx) { Long start = System.currentTimeMillis(); while (System.currentTimeMillis() - start < 40000) { } } }sf package version create -p UninstLateFailProbe -x --skip-validation -w 30 - Install it in a scratch org and start
sf package uninstall -p <04t> -o <org> -w 20. - Once the uninstall prints
Status = InProgress, deploy this class from a second terminal, finishing
within the script's 40 seconds (<ns>is the package's namespace):
Result: the uninstall command ends with the Actual output; the request'spublic with sharing class SubRefProbe { public static Integer countRows() { return [SELECT COUNT() FROM <ns>__UninstProbe__c]; } }StatusisErrorand the package
is still installed.sf package uninstall report -i <06y>prints the same. - Control: delete
SubRefProbeand uninstall again (the package is still installed):Successfully uninstalled package.
Measured 2026-10-04, CLI 2.152.14, Winter '27 scratch org.
Expected
UNINSTALL_ERROR: Can't uninstall the package <06y> during uninstall request <06y>., plus the
=== Errors list when PackageVersionUninstallRequestError has rows. (Both ids are the request id
today, at packageUninstall.ts L58 and subscriberPackageVersion.ts L219; a separate, minor issue.)
Actual
The CLI prints Error (1): unexpected token: '"' (exit 1). With --json, the error code is
MALFORMED_QUERY. This is the uninstall command's output, from the end-to-end steps; the quick
repro (step 2) shows the underlying query error.
Fix and tests
Checked on main at 51fc61b (5.0.14): with the patch, the two files pass 15 tests.
- Fix: drop the outer
"at L30. uninstallPackage.test.ts, "should send the uninstall request, and handle errors
appropriately": now also checks the SOQLpollUninstallsends. The id it expects,
04t4p000002BaHYAA0, is the request id this file'screate()stub returns, not the package id.subscriberPackageVersion.test.ts, new "uninstallStatus reports a failed request with its
errors, using valid SOQL": reuses thequeryStubfrombeforeEach, and checks the error after
thetry, so a call that doesn't throw fails.
diff --git a/src/package/packageUninstall.ts b/src/package/packageUninstall.ts
index 927a1c4..7680743 100644
--- a/src/package/packageUninstall.ts
+++ b/src/package/packageUninstall.ts
@@ -27,7 +27,7 @@ type UninstallResult = PackagingSObjects.SubscriberPackageVersionUninstallReques
export async function getUninstallErrors(conn: Connection, id: string): Promise<Array<{ Message: string }>> {
const errorQueryResult = await conn.tooling.query<{ Message: string }>(
- `"SELECT Message FROM PackageVersionUninstallRequestError WHERE ParentRequest.Id = '${id}' ORDER BY Message"`
+ `SELECT Message FROM PackageVersionUninstallRequestError WHERE ParentRequest.Id = '${id}' ORDER BY Message`
);
return errorQueryResult?.records ?? [];
}
diff --git a/test/package/subscriberPackageVersion.test.ts b/test/package/subscriberPackageVersion.test.ts
index 0f191f4..44e5ccf 100644
--- a/test/package/subscriberPackageVersion.test.ts
+++ b/test/package/subscriberPackageVersion.test.ts
@@ -170,6 +170,24 @@ describe('subscriberPackageVersion', () => {
expect(queryStub.called).to.be.true;
}
});
+ it('uninstallStatus reports a failed request with its errors, using valid SOQL', async () => {
+ const id = '06y000000000001AAA';
+ // @ts-ignore
+ $$.SANDBOX.stub(connection.tooling, 'retrieve').resolves({ Id: id, Status: 'Error' });
+ queryStub.resolves({ records: [{ Message: 'this is a server-side error message' }], done: true, totalSize: 1 });
+
+ let error: Error | undefined;
+ try {
+ await SubscriberPackageVersion.uninstallStatus(id, connection);
+ } catch (e) {
+ error = e as Error;
+ }
+ expect(error?.name).to.equal('UNINSTALL_ERROR');
+ expect(error?.message).to.include('(1) this is a server-side error message');
+ expect(queryStub.firstCall.args[0]).to.equal(
+ `SELECT Message FROM PackageVersionUninstallRequestError WHERE ParentRequest.Id = '${id}' ORDER BY Message`
+ );
+ });
it('should propagate the same error from the SPV query', async () => {
connection = await testOrg.getConnection();
diff --git a/test/package/uninstallPackage.test.ts b/test/package/uninstallPackage.test.ts
index 3ebf4cc..89c5ca0 100644
--- a/test/package/uninstallPackage.test.ts
+++ b/test/package/uninstallPackage.test.ts
@@ -91,7 +91,7 @@ describe('Package Uninstall', () => {
}),
});
// @ts-ignore
- $$.SANDBOX.stub(conn.tooling, 'query').resolves({
+ const queryStub = $$.SANDBOX.stub(conn.tooling, 'query').resolves({
records: [{ Message: 'this is a server-side error message' }, { Message: 'this is a second error message' }],
});
@@ -108,6 +108,9 @@ describe('Package Uninstall', () => {
expect(error.message).to.include('(2) this is a second error message');
expect(error.actions).to.deep.equal(['Verify installed package ID and resolve errors, then try again.']);
}
+ expect(queryStub.firstCall.args[0]).to.equal(
+ "SELECT Message FROM PackageVersionUninstallRequestError WHERE ParentRequest.Id = '04t4p000002BaHYAA0' ORDER BY Message"
+ );
});
it('should send the uninstall request, and handle errors appropriately (0 error messages)', async () => {
Environment
- Affected:
@salesforce/packaging5.0.12 (L30 at its commit 7ec944d) through 5.0.14 (main, 51fc61b). - CLI runs (step 2 and the end-to-end recipe):
@salesforce/cli/2.152.14 darwin-arm64 node-v24.20.0, which bundles the@salesforce/packaginglibrary 5.0.12 (via plugin-packaging 3.0.7). - Unit tests (step 1): Node 22, on 51fc61b. The repo's mocha doesn't start on Node 26.
sf doctor (version detail, trimmed)
cliVersion: @salesforce/cli/2.152.14
architecture: darwin-arm64 (Darwin 25.6.0), shell zsh
nodeVersion: node-v24.20.0
plugin-packaging: 3.0.7 (core); it loads the @salesforce/packaging library 5.0.12, where this bug is
other plugins: deploy-retrieve 4.2.2, auth 5.0.7, org 6.0.13, data 5.1.8 (core); code-analyzer 5.16.0 (user)
diagnostics: all pass except "sourceApiVersion matches apiVersion" (warn: run outside a project)
- 主要语言
- 没有语言数据
- 星标
- 571
- 派生
- 80
- 平均合并
- 14 小时 43 分钟
- 30 天内合并 PR
- 5
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 没有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
forcedotcom/cli 的其他 Issue
-
investigating validated
难度 2/5 1-3 小时 新手友好度 72/100
forcedotcom/cli#3671 · 1 条评论 ·
-
sf agent mcp asset replace --assets null throws raw TypeError instead of InvalidShape可能已有人在做 @konkonrong-lgtm 于 74 天前认领。 未关闭area:afdx bug investigating owned by another team validated
难度 2/5 1-3 小时 新手友好度 78/100
forcedotcom/cli#3625 · 4 条评论 ·
-
area:afdx bug owned by another team
难度 2/5 1-3 小时 新手友好度 78/100
forcedotcom/cli#3608 · 2 条评论 ·
-
难度 3/5 1-2 天 新手友好度 68/100
forcedotcom/cli#3670 ·
-
announcement
难度 1/5 1 小时以内 新手友好度 1/100
forcedotcom/cli#3668 ·
相似的 Issue
-
bug
难度 2/5 1-3 小时 新手友好度 84/100
GoogleCloudPlatform/gce-rescue#195 ·
维护者通常 2 天内回复
-
package-request
难度 2/5 1-3 小时 新手友好度 72/100
ScoopInstaller/Extras#19155 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
unoplatform/uno.check#591 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 73/100
prime-radiant-inc/evener#5028 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 70/100
NanmiCoder/dsh-agent-teams#288 ·
维护者通常 1 天内回复