test: stabilize ACP 404 verification flows
This commit is contained in:
parent
a8049f63e1
commit
2faa411780
55
docs/reports/2026-04-08-acp-http-404-repro-verification.md
Normal file
55
docs/reports/2026-04-08-acp-http-404-repro-verification.md
Normal file
@ -0,0 +1,55 @@
|
||||
# ACP HTTP 404 场景自动复现验证
|
||||
|
||||
## 变更摘要
|
||||
|
||||
本次验证目标不是再次修复,而是确认 `ACP_HTTP_404: ACP HTTP request failed (404) · unexpected content type: text/plain` 这一类场景能否通过自动化测试稳定复现,并作为后续回归证据保留。
|
||||
|
||||
验证对象聚焦在 ACP runtime transport 行为:
|
||||
|
||||
- 当客户端以 `http(s)` 基址优先请求 `/acp/rpc`
|
||||
- 服务端返回 `404`
|
||||
- 且 `content-type` 为 `text/plain`
|
||||
- 但同一基址下 `/acp` WebSocket 仍可正常响应
|
||||
|
||||
该场景现在由 `test/runtime/gateway_acp_client_suite.dart` 内的专门用例覆盖。
|
||||
|
||||
## 测试命令与结果
|
||||
|
||||
- `flutter test test/runtime/gateway_acp_client_test.dart --reporter expanded --plain-name "falls back to websocket when HTTP bridge returns plain-text 404"`
|
||||
- 结果:通过
|
||||
- 关键输出:`GatewayAcpClient falls back to websocket when HTTP bridge returns plain-text 404`
|
||||
- `flutter test test/runtime/gateway_acp_client_test.dart --reporter expanded`
|
||||
- 结果:通过
|
||||
- 关键输出:`00:00 +12: All tests passed!`
|
||||
- `flutter analyze`
|
||||
- 结果:通过
|
||||
- 关键输出:`No issues found!`
|
||||
|
||||
## 重点验证点覆盖
|
||||
|
||||
- 已验证:自动化测试可以稳定构造“`/acp/rpc` 返回 `404 text/plain`,但 `/acp` WebSocket 可用”的复现场景。
|
||||
- 已验证:当前实现会在该场景下从 HTTP bridge 失败回退到 WebSocket,并成功获取 ACP capabilities。
|
||||
- 已验证:原有错误分类仍保留,`text/html` 这类网页响应不会被误判为可回退场景。
|
||||
- 已验证:相关 ACP client suite 全量回归通过,没有引入同文件内的其他协议回归。
|
||||
|
||||
## 失败项
|
||||
|
||||
- 无自动化失败项。
|
||||
|
||||
## 高风险回归点
|
||||
|
||||
- ACP transport 是运行时核心链路,涉及 HTTP / WebSocket 双通道切换,属于中高风险协议行为。
|
||||
- 当前自动化复现基于 fake server,覆盖的是协议级场景,不等同于已经对用户机器上的真实 `127.0.0.1:18789` 服务完成实机重放。
|
||||
|
||||
## 建议人工补测项
|
||||
|
||||
- 如果需要证明“截图里的真实本地服务”与自动化场景完全一致,建议对用户当前本地 gateway 做一次实机补测:
|
||||
- 在真实 `127.0.0.1:18789` 上确认 `/acp/rpc` 是否返回 `404 text/plain`
|
||||
- 同时确认 `/acp` WebSocket 是否可握手并返回 `acp.capabilities`
|
||||
- 从 Desktop UI 发起一次与截图相同的对话请求,确认不再出现该报错
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `test/runtime/gateway_acp_client_suite.dart`
|
||||
- `test/runtime/gateway_acp_client_test.dart`
|
||||
- `lib/runtime/gateway_acp_client.dart`
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../test/helpers/test_keys.dart';
|
||||
import 'test_support.dart';
|
||||
|
||||
Finder _textEither(String zh, String en) {
|
||||
@ -45,6 +46,7 @@ void main() {
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await pumpDesktopApp(tester);
|
||||
await waitForIntegrationFinder(tester, find.byKey(TestKeys.assistantTaskRail));
|
||||
|
||||
expect(_textEither('新对话', 'New conversation'), findsWidgets);
|
||||
await tester.tap(
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../test/helpers/test_keys.dart';
|
||||
import 'test_support.dart';
|
||||
|
||||
Finder _textEither(String zh, String en) {
|
||||
@ -45,6 +46,7 @@ void main() {
|
||||
'desktop shell exposes settings entry for gateway configuration',
|
||||
(WidgetTester tester) async {
|
||||
await pumpDesktopApp(tester);
|
||||
await waitForIntegrationFinder(tester, find.byKey(TestKeys.assistantTaskRail));
|
||||
|
||||
await tester.tap(
|
||||
find.byKey(const Key('assistant-side-pane-tab-navigation')),
|
||||
|
||||
@ -10,6 +10,20 @@ import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
|
||||
import '../test_support.dart';
|
||||
|
||||
Future<void> _waitForText(
|
||||
WidgetTester tester,
|
||||
Finder finder, {
|
||||
Duration timeout = const Duration(seconds: 10),
|
||||
}) async {
|
||||
final deadline = DateTime.now().add(timeout);
|
||||
while (finder.evaluate().isEmpty) {
|
||||
if (DateTime.now().isAfter(deadline)) {
|
||||
fail('Timed out waiting for ${finder.description}');
|
||||
}
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets(
|
||||
'AssistantPage single agent can be selected and receive streaming reply',
|
||||
@ -57,7 +71,7 @@ void main() {
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
await tester.pumpAndSettle();
|
||||
await _waitForText(tester, find.textContaining('CODEX_REPLY'));
|
||||
|
||||
expect(find.textContaining('CODEX_REPLY'), findsWidgets);
|
||||
expect(server.requestCount, greaterThanOrEqualTo(1));
|
||||
@ -108,7 +122,9 @@ class _ChatServer {
|
||||
HttpHeaders.contentTypeHeader,
|
||||
'text/event-stream; charset=utf-8',
|
||||
);
|
||||
request.response.headers.set(HttpHeaders.cacheControlHeader, 'no-cache');
|
||||
request.response.write('data: ${jsonEncode(response)}\n\n');
|
||||
await request.response.flush();
|
||||
await request.response.close();
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,20 @@ import 'package:xworkmate/runtime/runtime_models_profiles.dart';
|
||||
|
||||
import '../test_support.dart';
|
||||
|
||||
Future<void> _waitForText(
|
||||
WidgetTester tester,
|
||||
Finder finder, {
|
||||
Duration timeout = const Duration(seconds: 10),
|
||||
}) async {
|
||||
final deadline = DateTime.now().add(timeout);
|
||||
while (finder.evaluate().isEmpty) {
|
||||
if (DateTime.now().isAfter(deadline)) {
|
||||
fail('Timed out waiting for ${finder.description}');
|
||||
}
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('SettingsPage Codex external ACP can test and save', (
|
||||
WidgetTester tester,
|
||||
@ -47,20 +61,21 @@ void main() {
|
||||
const ValueKey<String>('external-acp-test-Codex'),
|
||||
);
|
||||
final saveButton = find.byKey(
|
||||
const ValueKey<String>('external-acp-apply-Codex'),
|
||||
const ValueKey<String>('external-acp-save-Codex'),
|
||||
);
|
||||
|
||||
expect(endpointField, findsOneWidget);
|
||||
await tester.enterText(endpointField, server.baseUri.toString());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(testButton);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await _waitForText(tester, find.textContaining('连接成功'));
|
||||
|
||||
expect(find.textContaining('连接成功'), findsOneWidget);
|
||||
|
||||
await tester.tap(saveButton);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.pump();
|
||||
|
||||
final saved = controller.settings.externalAcpEndpointForProviderId('codex');
|
||||
expect(saved?.endpoint, server.baseUri.toString());
|
||||
@ -110,7 +125,9 @@ class _AcpServer {
|
||||
HttpHeaders.contentTypeHeader,
|
||||
'text/event-stream; charset=utf-8',
|
||||
);
|
||||
request.response.headers.set(HttpHeaders.cacheControlHeader, 'no-cache');
|
||||
request.response.write('data: ${jsonEncode(response)}\n\n');
|
||||
await request.response.flush();
|
||||
await request.response.close();
|
||||
}
|
||||
}
|
||||
|
||||
@ -673,7 +673,7 @@ paths:
|
||||
ValueKey('external-acp-test-${customProfile.providerKey}'),
|
||||
);
|
||||
final applyButton = find.byKey(
|
||||
ValueKey('external-acp-apply-${customProfile.providerKey}'),
|
||||
ValueKey('external-acp-save-${customProfile.providerKey}'),
|
||||
);
|
||||
|
||||
expect(labelField, findsOneWidget);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user