Record P08 isolated acceptance and explicit MQ recovery evidence

This commit is contained in:
2026-09-30 21:05:58 +08:00
parent 75e489fff0
commit d85480c00c
5 changed files with 133 additions and 4 deletions
@@ -154,7 +154,7 @@
## 验收台账
P01–P07 的项目内隔离证据见上;P07 唯一当前入口、全仓残留/合法例外及来源/hash 的逐项结果见 [`saas-dispatcher-p07-audit.md`](saas-dispatcher-p07-audit.md)。A01–A12 和 K01–K16 的最终逐项对照、业务覆盖率及诊断门禁仍待 P08。不得用本地 Mock 冒充真实 SaaS、management、MQ、OSS、AI、Asterisk、SIP、ECS 或生产签收。
P01–P08 的项目内隔离证据见上及下文;P07 唯一当前入口、全仓残留/合法例外及来源/hash 的逐项结果见 [`saas-dispatcher-p07-audit.md`](saas-dispatcher-p07-audit.md)。A01–A12、K01–K16、业务覆盖率、故障/重启和诊断门禁的最终对照见 [`saas-dispatcher-p08-acceptance.md`](saas-dispatcher-p08-acceptance.md)。不得用本地 Mock 冒充真实 SaaS、management、MQ、OSS、AI、Asterisk、SIP、ECS 或生产签收。
## P08 本地验收进度
@@ -163,3 +163,7 @@ P01–P07 的项目内隔离证据见上;P07 唯一当前入口、全仓残留
- 非生产呼叫诊断**前置门禁**:审查发现原脚本允许绕过 Asia/Shanghai 真实窗口、将 Asterisk `enabled/active` 状态和已安装制品 SHA-256 错误用 `|| true` 吞掉,也未检查 ARI module/HTTP `/ari/` 与所选 PJSIP endpoint。新增 `internal/config/nonprod_call_gate_test.go`:先以纯本机假工具复现缺失门禁,再覆盖时段 `08:59`/`09:00`/`19:59`/`20:00`、错误时钟、无效环境、systemd 两种失败、ARI 模块/HTTP route、缺失 endpoint/哈希,12 个故障/边界均在**假 `runuser` 被调用之前**拒绝;时间门禁在准备证据前及实际执行前各查一次,时段左闭右开,状态和 SHA 失败均显式错误。`bash -n deploys/test/nonprod-call-evidence.sh` 与定向 `go test -race ./internal/config` 通过。这是隔离负例,既无真实主机状态,也没有发起 SIP 呼叫;拨号后证据收集的故障处置及部署/SSH/磁盘诊断仍待核验,不冒充完整 P08 签收。
- 非生产证据**结束收集门禁**:原脚本在 `EXIT` 收尾时使用 `|| true` 掩盖抓包 SHA-256 等必需事实缺失,隔离 `--preflight-only` 试验先复现“合成抓包 hash 失败却返回成功”;新增 `TestNonprodPreflightRejectsIncompleteCapturedEvidence`,使用本机假 Asterisk/tcpdump/systemctl/sha256sum 和临时目录,仅写合成抓包、**不调用假拨号命令**。收尾现在保存原调用失败码,并单独核验 PJSIP logger 关闭、结束时间、抓包及录音 hash、Asterisk journal、SIP 摘要和受限目录归属;任一失败留 `diagnostic-errors.txt` 的脱敏原因,原本成功的前置检查须返回失败。`bash -n` 和定向 `go test -race ./internal/config` 经预期红→绿;受限临时目录保留了失败时的合成 pcap 与 hash 错误事实。尚未有真实主机、真实 RTP、SSH/EIP 与供应商证据,绝不据此声称非生产现场验证已通过。
- 会话续期与录音事实切换:`internal/dispatcher/agent_inbound_session_test.go` 先复现缺少相邻代际可识别拒绝,再保持原 `store.ErrCommandConflict` 栅栏,仅在同 Agent/Cell/boot/epoch 且有效的相邻代际附加显式转场错误;`internal/rpc/recording_server_flow_test.go` 先确认三项原事实以前均误归类为永久 `PermissionDenied`,再要求 mTLS 指纹已验证时才返回专用 `Unauthenticated`,其余拒绝维持原义。Agent 的新测试先复现无重报和无时限,再覆盖原 `RequestRecordingUpload`、`ReportCallEnded`、`ReportCallResult` 的源事件、operation/idempotency、资产与结果内容不变;六秒/调用方更早期限、换 Agent/Cell/boot/epoch、错误码不符、二次报告结果未知均失败关闭,绝不重拨或重传未知 OSS PUT。实际本机 mTLS bufconn 连续三次切换均在 `internal/rpc/recording_transport_test.go` 完成,原结果 outbox 身份保持;`internal/session/transition.go` 统一跨模块错误标记和消息,`proto/ERRORS.md` 与 `proto/manifest.json` 更新且 Proto RPC 不变。会话切换耗尽仍明确报错,不冒充已交付或跨 boot 自动恢复。
- P08 隔离交付对照:`internal/mq/broker_integration_test.go` 在临时 RabbitMQ 中验证关闭连接拒绝宣称交付、显式重开后原待发事件进入共享队列,三项必跑集成测试均实际 PASS;`internal/agent/recording_retry_test.go` 补足 8/16 分钟测试,使 1/2/4/8/16/32/60 分钟与 48 小时待人工边界有明确证据。`docs/evidence/saas-dispatcher-p08-acceptance.md` 逐项列 P01–P08、A01–A12、K01–K16、五条录音路径和失败/重启对照;53 个命名测试与 21 个文件引用存在。`make check`、`make coverage`(手写业务语句覆盖率 **72.0%**)和 `make release-check-local` 本地通过;活动目录意外代次路径和退役符号复扫均为 0,发布批准仍为 false。以上只核验本地 Mock 和诊断假工具:未运行真实 SaaS/management/MQ 应用收讫、OSS/AI 供应商、Asterisk/SIP/ECS、非生产现场拨号前抓包或生产切换。
@@ -0,0 +1,99 @@
# P08 项目内验收对照
本记录按 `docs/plan-saas-dispatcher-v05-v0.1.md` §3.1、§6 核对 P01–P08、K01–K16 和 A01–A12。**范围仅限单节点、单 Dispatcher、单 Agent、单 Cell、单租户的隔离 Mock。** 本机证书、RabbitMQ 容器和 OSS/AI 模拟服务不代表 SaaS 应用收讫、供应商验收或真实拨号。原计划是来源事实,不是第二份当前运行合同;当前入口是 `docs/plan-saas-dispatcher.md`。
## 可复现的本地门禁与来源
| 命令或来源 | 本地结果和边界 |
| --- | --- |
| `PATH=/tmp/sip-go-agent-tools/bin:$PATH make check` | 通过:格式、当前合同正反例、Proto 重新生成及来源/hash、`go vet ./...`、`go test -race ./...`、构建、本地验收;三项必需隔离 RabbitMQ 测试均**实际 PASS**,不是零项筛选。 |
| `bash scripts/check-current-mq-mock.sh` | `TestBrokerSharedResultQueueAndNoConfigure`、`TestRuntimeIsolatedControlBacklogExecuteAndSharedResult`、`TestDispatcherCommandStartsWithIsolatedMQHTTPAndAgent` 三项实际 PASS;使用临时隔离 RabbitMQ,不连接真实 SaaS。 |
| `make coverage` | Go 1.27.1;手写业务代码语句覆盖率 **72.0%**,排除生成目录 `gen/`,未排除本轮修改的手写代码;超过 65% 门槛。 |
| `PATH=/tmp/sip-go-agent-tools/bin:$PATH make release-check-local` | 通过本地制品/契约门禁;`production_approval=false`,未启动生产服务。 |
| `git diff --check` | 通过;仅作差异格式检查,不代表真实通信通过。 |
| `docs/plan-saas-dispatcher-v05-v0.1.md` | 固定来源 SHA-256 `666f39e56ea9f4b55661efcac82edd6f9729848e2d60e5f24cdf5aa3ac97ee87`;`contracts/local/manifest.json` 固定其来源、34 个合同 JSON 与对应哈希,清单自身 SHA-256 `668aea5a742d2bf9bed4685c06f8dd95099d5d29a6bc2f32c2211bbc21197b40`。 |
| `proto/manifest.json` | Proto 服务和生成物一致;错误说明更新后 `proto/ERRORS.md` 为 5108 字节、SHA-256 `024f53a02c01151f7223ad49ad2d32c73d7701497dfd032bed0de820d72c7dc4`,未更改 RPC 方法或 SaaS Schema。 |
> 上述门禁记录的是本地运行结果;新增的验收文件和门禁记录提交前再次执行,最终结果以本文件末尾复核为准。目标分支为 `feat/saas-dispatcher-contract`。已有 SQLite、Agent 恢复文件、spool 和 outbox 均未自动迁移或清理;两份使用者修改的 `docs/thirds/` 文档不属于本批改动。
## P01–P08
| 阶段 | 本项目可核验证据 | 状态 |
| --- | --- | --- |
| P01 契约 | `contracts/local/` 当前 Schema、MQ 拓扑、正反例与 `manifest.json`;`contracts/current_contract_test.go`、`contracts/current_topology_test.go`。 | 本地通过;真实 SaaS 字段未签收。 |
| P02 单入口/Proto | `cmd/sip-go-agent/main.go` 唯一 Mock 入口;`proto/agent/agent.proto` 八个方法;`scripts/check-proto.sh` 重新生成和核验。 | 本地通过。 |
| P03 只读配置 | `internal/configread/snapshots_test.go`、`discovery_test.go`、`internal/ai/binding_test.go`:任务、发现、SIP、额度、供应商及失效边界。 | 本地通过;真实 HTTP 未联调。 |
| P04 MQ/SQLite | `internal/mq/broker_integration_test.go`、`internal/dispatcher/runtime_integration_test.go`、`internal/store/result_test.go`:队列归属、实际路由、持久状态与唯一结果。 | 本地通过;实际 SaaS 应用收讫未验证。 |
| P05 AI/媒体 | `internal/ai/binding_test.go`、`keyword_hangup_test.go`、`internal/rpc/approved_full_ai_integration_test.go`:ASR-only/full-AI 与失败关闭。 | 本地通过;真实 AI/SIP/RTP 未验。 |
| P06 录音/结果 | `internal/agent/recording_delivery_test.go`、`recording_retry_test.go`、`internal/rpc/recording_transport_test.go`,见下方五条路径。 | 本地通过;真实 OSS 未验。 |
| P07 唯一当前入口 | `docs/evidence/saas-dispatcher-p07-audit.md` 的旧→新映射、历史例外与复扫;旧路径已删或按原字节归档。 | 本地通过;不处置现存数据。 |
| P08 对照验收 | 本文件 A01–A12、K01–K16 与故障/重启对照;53 个命名测试引用和 21 个源文件引用均存在;本地构建、覆盖率和诊断门禁的隔离负例。 | 本地证据通过;真实现场门禁仍待授权核验。 |
## A01–A12 逐项对照
| 检查 | 本地依据及结论 | 不能据此宣称的事项 |
| --- | --- | --- |
| A01 契约一致性 | `contracts/local/config-read.schema.json`、`task-discovery.schema.json`、`mq.schema.json`、`mq-topology.json` 及 34 个示例/Schema 文件受清单约束;`TestCurrentContractExamples` 对合法/非法实例实际执行,`TestCurrentContractTopology` 固定通信形态。五类 HTTP、命令/回执、共享最终结果和 provider/agent 字段仅按本项目已确认版本核验。 | 真实 SaaS 的响应字段或审批来源已兼容。 |
| A02 无代次名称 | `docs/evidence/saas-dispatcher-p07-audit.md` 逐项记录旧→新、已删除/归档和合法固定 `v1`、业务 revision、历史事实;本轮新增 `internal/session/` 不引入自有运行代次。本轮复扫 `cmd internal gen proto contracts/local scripts deploys` 的活动路径,意外 `_v2/_v3`、`v0.x` 路径 **0 项**;扫描 `cmd internal scripts deploys` 的废弃入口符号 **0 项**,保留 P07 已列的固定协议与历史例外。 | 把历史文档里的代次事实误当成现行运行入口。 |
| A03 固定通信名称 | `TestCurrentContractTopology`、`TestBrokerSharedResultQueueAndNoConfigure` 和根命令隔离集成测试核对实际 exchange/queue/routing key、共享结果接收及 `/internal/v1/dispatcher/...`;没有旧拓扑回退。 | 不同真实 SaaS 部署的路由已联调。 |
| A04 引用及生成一致性 | `make check` 执行当前合同及 Proto 清单/来源 SHA-256、重新生成、构建;P07 审计核对当前/历史引用和归档。固定历史来源哈希与本地清单各自独立。 | 从源代码仓库测试推断真实 management 已发布相同配置。 |
| A05 配置读取与失效 | `TestCurrentContractExamples` 覆盖字符串 tenant_id、旧结构字段和新增未知字段反例;`TestReadTaskDoesNotReuseConfigAfterHTTPFailure`、`TestReadTaskRejectsMismatchedOwnerAndMalformedSIP`、`TestBindCurrentFullAIUsesApprovedSDKFields`、`TestBindCurrentRejectsUnauthorizedProviderBeforeCall` 等核对 0/false/null、授权绑定、无缓存回退和虚构 credential 的 SDK 参数。 | 真实凭据或供应商 SDK 已端到端联调。 |
| A06 队列所有权与路由 | 上述三项隔离 MQ 测试实际 PASS;无 `configure` 权限、被动检查、D1/D2 路由和共享 SaaS 队列实际收件。`TestBrokerSharedResultQueueAndNoConfigure` 覆盖缺/错绑定、mandatory return、关闭连接拒发后**显式**重开并收取原待发消息;`TestExecuteTimeoutAndMQFailureDoNotRedial` 注入发布确认丢失,保持原状态、不重拨。 | 不能把 confirm 当 SaaS 处理回执,不能把显式重开写成自动热重连;没有注入真实 RabbitMQ 确认帧丢失。D1/D2 仅夹具。 |
| A07 控制与发现竞态 | `TestReadAllTasksContinuesAfterShortPageUntilEmptyPage`、`TestDiscoveryFollowerFailedPageKeepsCursorAndClosesAdmission`、`TestControlPauseDefaultHangupAckAfterAgentDispatch`、`TestControlStopCannotResumeOrDispatchPending`、`TestSIPNotificationPersistsBarrierAndWaitsForLoadedFullSnapshot`:游标、持久屏障、旧 running 及停用不可恢复。 | Agent/Asterisk 实际加载的 revision 已在真实环境核验。 |
| A08 执行与 AI 行为 | `TestExecuteRejectsInvalidCalleeWithoutStoppingTask`、`TestExecuteWaitsForRulesThenDispatchesOriginalIdentity`、`TestExecuteTimeoutAndMQFailureDoNotRedial`、`TestSelectTrunkWhitelistScheduleAndCaller`、`TestBindCurrentASROnlyDoesNotBindOtherProviders`、`TestKeywordHangupOnlyFinalUserLiteralAndOnlyOnce`;重复与未知执行不重拨,只有用户最终 ASR 命中关键词。 | Mock 时段/门禁不授权任何真实号码拨号;真实 SIP/AI 仍未验证。 |
| A09 结果与录音 | 下方五路径和额外的恢复/窗口测试;`TestFinalResultNeedsConfirmedEndAndRemainsExactlyOne`、`TestFinalResultPayloadNoAnswerHasNoFabricatedTranscriptOrAsset`。48 小时到期保留待人工、不发假结果。 | 模拟 HTTPS 对象接收不等于供应商 OSS 成功。 |
| A10 数据保护与恢复 | `TestRecordingRecoveryDiscoversAndResumesAfterProcessRestart`、`TestRecordingRecoveryUnknownPUTNeverRetriesAfterRestart`、`TestFinalResultOutboxFailureRollsBackAndCanResume`、`TestFinalResultRestartResumesSameOutboxBody`;会话代际竞态测试见下表。旧 SQLite 与旧 Agent 状态只读失败关闭,未自行清理。 | 未保存恢复双文件时进程崩溃不保证零丢失;真实主机重启未验。 |
| A11 测试与构建 | TDD:新增会话/录音测试先红后绿;上述 `make check`、72.0% 覆盖率及本地发布检查通过。`TestNonprodCallEvidenceFailsBeforeDialWithoutRequiredGates`、`TestNonprodPreflightRejectsIncompleteCapturedEvidence` 以临时假工具验证缺资源、缺诊断/抓包事实即失败关闭。 | 假工具不能替代实际 ECS、Asterisk/systemd、ARI/PJSIP、拨号前 SIP/RTP 抓包及收尾哈希。未经另行授权不执行这些现场动作。 |
| A12 交付边界 | 唯一当前入口 `docs/plan-saas-dispatcher.md`、`docs/thirds/saas-dispatcher.md`;本表分开本地证据和外部缺口。`production_approval=false`;已有业务数据未自动处置。 | 不宣称第二 Dispatcher、第二 Cell/租户、多机、容量、真实线路或生产通过。 |
### A09 五条录音路径及错误边界
| 路径 | 隔离测试与可观察结果 |
| --- | --- |
| 录音生成并上传成功 | `TestRecordingDeliveryDirectPUTThenOneResultWithoutBusinessFiles`:一次模拟 HTTPS PUT,仅录音字节;不先写再删业务录音/结果文件;确认通话结束和上传后才给唯一结果。 |
| OSS 明确失败 | `TestRecordingDeliveryFailedPUTPersistsOriginalAndNeverRetriesImplicitly`、`TestRecordingDeliveryRestartRetriesOriginalAndReportsConfirmedPUTOnly`:原录音/结果双文件、原对象与消息身份保留,重启后显式恢复,不自动重传不确定 PUT。 |
| 无录音/无应答 | `TestRecordingDeliveryNoRecordingReportsOnlyAfterConfirmedEnd`、`TestFinalResultPayloadNoAnswerHasNoFabricatedTranscriptOrAsset`:确认结束后直接回报,空录音,不编造转写/OSS 路径。 |
| 预期录音生成失败 | `TestRecordingDeliveryCaptureFailureDescribesFailureWithoutInventingUpload`、`TestApprovedRecordedMockCallGenerationFailureReportsEmptyRecording`:真实通话结果、空录音并说明失败;不当作 OSS 失败重试。 |
| 失败且无法保存双文件 | `TestRecordingDeliveryRecoveryDiskFailureNeverReportsUploaded`:明确报错,无完整副本不发最终结果,不伪称已开始可靠重试。 |
补充:`TestRecordingRecoveryBackoffAndResultRetryDoNotRepeatSuccessfulPUT` 核对 1/2/4/8/16/32/60 分钟及上限;`TestRecordingRecoveryExpiresAfterExactly48HoursWithoutResult` 以双文件保存后时刻为起点,到期留原文件待人工;`TestRecordingDeliveryConfirmedPUTReportRetryDoesNotRepeatPUT` 确认 MQ 失败不触发第二次成功 PUT。未知 PUT 由 `TestRecordingDeliveryUnknownPUTKeepsManualBarrierAcrossRestart` 和 `TestRecordingRecoveryPersistsInFlightBeforeUnknownPUT` 禁止盲重试。
## K01–K16 对照
| 决定 | 对应实现/测试及本地结论 |
| --- | --- |
| K01 | `contracts/local/mq-topology.json`、`TestCurrentContractTopology`、`TestBrokerSharedResultQueueAndNoConfigure`:任务/控制队列 SaaS 预建,D 无 configure,共享结果队列实际接收。 |
| K02 | `TestCurrentContractExamples`:tenant_id 数字,字符串及旧 `schema_version` 反例拒绝;内部事件/幂等身份保留。 |
| K03 | `TestReadAllTasksContinuesAfterShortPageUntilEmptyPage`、`TestDiscoveryFollowerPersistsPageBeforeAdvancingToTerminalEmpty`:非空短页继续、含游标空页结束,逐页持久化后前进;出错关准入。 |
| K04 | `TestControlStopCannotResumeOrDispatchPending`、`TestStoppedTaskSilentlyAcknowledgesOldExecuteWithoutRecreatingInbox`:同 task_id 的 stop 不可恢复;pause/resume 另有新鲜核验,不做任务删除或跨 D 迁移。 |
| K05 | `TestControlPauseDefaultHangupAckAfterAgentDispatch`、`TestControlAgentFailureKeepsBarrierWithoutSuccessAck`:缺省挂断,成功回执仅表示已交 Agent;未调度不回成功。 |
| K06 | `TestExecuteWaitsForRulesThenDispatchesOriginalIdentity`、`TestFinalResultNeedsConfirmedEndAndRemainsExactlyOne`:dispatched 与已接通分离;外部 task_id/callee 关联,内部仍保存每次事实身份。 |
| K07 | `TestSIPNotificationPersistsBarrierAndWaitsForLoadedFullSnapshot`、`TestReadSIPWithoutTasks`:全量 SIP revision 与通知对应,变更期间关准入;实际 Asterisk 加载仍待现场证明。 |
| K08 | `TestBindCurrentFullAIUsesApprovedSDKFields`、`TestBindCurrentRejectsUnauthorizedProviderBeforeCall`:虚构 credential 作为获批 provider 参数传入,不发明 ref,不记录真实密钥。 |
| K09 | `TestKeywordHangupOnlyFinalUserLiteralAndOnlyOnce`、`TestKeywordHangupConcurrentDuplicateFinalTriggersOnce`:仅用户侧最终 ASR 文本的字面关键词生效,重复通知一次。 |
| K10 | A09 五路径及 `TestRecordingRecoveryBackoffAndResultRetryDoNotRepeatSuccessfulPUT`、`TestRecordingRecoveryExpiresAfterExactly48HoursWithoutResult`:明确失败才持久化双文件,48 小时原起点/间隔/待人工边界。 |
| K11 | `TestFinalResultPayloadRejectsMissingOrInventedCallFacts`、`TestFinalResultPayloadNoAnswerHasNoFabricatedTranscriptOrAsset`:无真实 SIP 码保持 null 和文字原因,不伪造本地数字码。 |
| K12 | `TestExecuteWaitsForRulesThenDispatchesOriginalIdentity`、`TestSelectTrunkWhitelistScheduleAndCaller`:规则不满足等待并重新校验,不自动取消人工 pause/stop,不转为伪失败。 |
| K13 | `TestExecuteRejectsInvalidCalleeWithoutStoppingTask`:单号不合规只拒绝该执行,不出 dispatched/最终结果,不暂停整个任务。 |
| K14 | `TestFinalResultPayloadNoAnswerHasNoFabricatedTranscriptOrAsset`、`TestFinalResultNeedsConfirmedEndAndRemainsExactlyOne`:已确认 answered/no_answer/failed 与未知占用分离,不按 OSS 失败改写通话事实。 |
| K15 | `TestRecordingDeliveryCaptureFailureDescribesFailureWithoutInventingUpload`:录音生成失败仍给真实通话结果、recording={}、文字说明。 |
| K16 | `TestRecordingDeliveryRecoveryDiskFailureNeverReportsUploaded`:双文件保存失败不发结果、不编造可恢复状态。 |
## 故障、重启与会话竞态
| 注入/重启边界 | 观测证据 | 剩余限制 |
| --- | --- | --- |
| HTTP 页/配置网络失败或过期 | `TestReadTaskDoesNotReuseConfigAfterHTTPFailure`、`TestReadTasksFailsOnUnavailableRefreshWithoutStaleFallback`、`TestDiscoveryFollowerFailedPageKeepsCursorAndClosesAdmission`。 | 本地假 HTTP 服务。 |
| MQ 无绑定、不可路由、关闭后重开、确认结果不明 | `TestBrokerSharedResultQueueAndNoConfigure` 实际隔离收件;`TestExecuteTimeoutAndMQFailureDoNotRedial` 的发布确认丢失注入;`TestFinalResultRestartResumesSameOutboxBody` 保留原 outbox 消息身份。 | 未注入真实 SaaS/RabbitMQ 生产网络的确认帧丢失;confirm 不是应用收讫。 |
| Agent 执行/会话代际切换 | `TestInboundAgentFactRequiresCurrentUnexpiredActivatedSession`、`TestRecordingServerClassifiesOnlyVerifiedSessionCutover`、`TestRecordingFactsCrossRealMutualTLSUnaryTransportWithoutExternalOSS`、`TestRecordingClientDoesNotRetryUnverifiedOrUnknownOutcomes`、`TestRecordingClientStopsWhenSecondOutcomeIsUnknown`、`TestRecordingClientSessionCutoverReReportIsBounded`。只在同 Agent/Cell/boot/epoch、相邻有效代际且**明确拒绝**时,在六秒或调用方更早期限内重报原三类事实;未知结果立即停止。 | 窗口耗尽明确失败,不称已交付;换 boot/身份或真正过期不能凭此自动恢复。未做真实多主机重启。 |
| OSS 明确失败、未知 PUT、写盘错误、48 小时耗尽 | A09 测试分别验证原对象/进度、重启、未知不重传、失败不伪造结果和待人工。 | 成功上传不写业务恢复文件;在未完成失败落盘前崩溃,不保证恢复。 |
| 非生产诊断/现场证据缺失 | `TestNonprodCallEvidenceFailsBeforeDialWithoutRequiredGates`、`TestNonprodPreflightRejectsIncompleteCapturedEvidence` 假工具负例在缺时段、systemd、ARI/PJSIP、安装包哈希或抓包哈希时阻止验收。 | **未运行**真实主机 capture-first 诊断、SIP/RTP/录音/ASR 现场采集。 |
**待真实环境完成,不构成本地失败的伪证:**真实 SaaS/management 配置签收与审批版本同源;真实 RabbitMQ ACL/TLS、队列所有权、应用收讫和结果消费;Agent/Asterisk 实际加载、ECS/systemd/ARI/PJSIP 状态、媒体/抓包;真实 OSS/AI/SIP 供应商;多节点/容量和生产切换。没有云资源操作、真实号码拨打或现存数据清理的授权与执行。任何缺少现场抓包、状态快照或权限的非生产验证均须失败关闭,不能把隔离 Mock、制品 hash 或三项本地 MQ PASS 写作真实验收。
## 最终复核
- `PATH=/tmp/sip-go-agent-tools/bin:$PATH make check`:**PASS**,包含重新生成/核验 Proto、当前合同正反例和来源 hash、格式、`go vet ./...`、`go test -race ./...`、构建及三项**实际 PASS** 的隔离 MQ 用例;新增显式重连及全部会话/录音测试已计入。
- `make coverage`:**PASS**;Go 1.27.1,排除生成文件后的手写代码语句覆盖率 **72.0%**。
- `PATH=/tmp/sip-go-agent-tools/bin:$PATH make release-check-local`:**PASS**;本地制品 SHA-256 `00b283d422a27766f1d4763ee193d705857274a5ec9f3c4dba699594536ff30a`;清单报告 `production_approval=false`、`source_dirty=true`。这不是生产包签收,也未执行部署或拨号。
- 活动路径/退役符号复扫均为 **0**;53 个测试符号、21 个源文件引用及当前/索引/证据文档 54 条本地链接均存在。来源计划、当前合同与 Proto 错误说明的哈希见首表;两份使用者修改的第三方文档保持原状且未被纳入本轮变更。
+2 -2
View File
@@ -20,9 +20,9 @@
| P05 AI/媒体 | 不可变获批 AI 快照,ASR-only、ASR+LLM+TTS、关键词、16 kHz 媒体及失败边界仅经本地 Mock 验证。 |
| P06 录音/OSS/结果 | 隔离双向 TLS 录音、一次 HTTPS PUT、失败与未知结果恢复、48 小时边界和唯一最终 MQ 结果经本机测试。 |
| P07 唯一当前入口 | **项目内已核查**:旧执行/配置/MQ/AI/Proto/Schema/脚本路径已分批清除或逐字节归档,当前文档/引用/hash 及合法历史例外逐项核对,实际数据保留;详见 [`evidence/saas-dispatcher-p07-audit.md`](evidence/saas-dispatcher-p07-audit.md)。真实外部兼容与主机诊断不在此项签收内。 |
| P08 验收交付 | A01–A12、K01–K16、业务覆盖率 ≥65%、重启/故障矩阵及诊断门禁的最终对照**尚未完成**。不得将前述分批通过写为最终签收。 |
| P08 验收交付 | **项目内隔离验收已核查**:P01–P08、A01–A12、K01–K16 的证据、故障/重启与会话切换、72.0% 手写业务覆盖率及诊断门禁假工具负例见 [`evidence/saas-dispatcher-p08-acceptance.md`](evidence/saas-dispatcher-p08-acceptance.md)。真实现场诊断、SaaS/management/供应商及生产均未验。 |
每批变更、测试命令、正反例和未验证项详见 [`evidence/saas-dispatcher-implementation.md`](evidence/saas-dispatcher-implementation.md)。发布包 `production_approval=false`;`make check`、`make release-check-local` 仅检验本机隔离制品与行为,不运行真实服务。
每批变更、测试命令、正反例和未验证项详见 [`evidence/saas-dispatcher-implementation.md`](evidence/saas-dispatcher-implementation.md);最终项目内对照见 [`evidence/saas-dispatcher-p08-acceptance.md`](evidence/saas-dispatcher-p08-acceptance.md)。发布包 `production_approval=false`;`make check`、`make release-check-local` 仅检验本机隔离制品与行为,不运行真实服务。
## 交付门禁
+1 -1
View File
@@ -231,7 +231,7 @@ func TestRecordingRetryBackoffStopsAtOneHour(t *testing.T) {
for _, tc := range []struct {
attempts int
want time.Duration
}{{1, time.Minute}, {2, 2 * time.Minute}, {3, 4 * time.Minute}, {6, 32 * time.Minute}, {7, time.Hour}, {30, time.Hour}} {
}{{1, time.Minute}, {2, 2 * time.Minute}, {3, 4 * time.Minute}, {4, 8 * time.Minute}, {5, 16 * time.Minute}, {6, 32 * time.Minute}, {7, time.Hour}, {30, time.Hour}} {
if got := recordingRetryDelay(tc.attempts); got != tc.want {
t.Fatalf("attempt %d has retry delay %s, want %s", tc.attempts, got, tc.want)
}
+26
View File
@@ -176,6 +176,32 @@ func TestBrokerSharedResultQueueAndNoConfigure(t *testing.T) {
t.Fatalf("missing D-specific output binding for %s", id)
}
}
// A dead connection must not claim delivery. An explicit new session can
// publish the unchanged pending identity to the SaaS-owned shared queue.
route, _ := tenant.ResultRoute(ids[0])
pending := []byte(strings.Replace(string(resultBody), `"event_id":"call-result-example"`, `"event_id":"call-result-reopened"`, 1))
if err := brokers[0].Close(); err != nil {
t.Fatal(err)
}
if err := brokers[0].Publish(context.Background(), route.Exchange, route.BindingKey, pending); err == nil || !strings.Contains(err.Error(), "connection is closed") {
t.Fatalf("closed connection reported a successful publish: %v", err)
}
reopened, err := Open(url, ids[0], 1)
if err != nil {
t.Fatalf("explicit reconnect failed: %v", err)
}
defer reopened.Close()
brokers[0] = reopened
if err := reopened.Publish(context.Background(), route.Exchange, route.BindingKey, pending); err != nil {
t.Fatalf("original pending identity was not delivered after reconnect: %v", err)
}
delivery, ok, err := admin.Get(shared, false)
if err != nil || !ok || delivery.MessageId != "call-result-reopened" || string(delivery.Body) != string(pending) || delivery.DeliveryMode != amqp.Persistent {
t.Fatalf("reconnected result was not received by the SaaS-owned queue with its original identity: ok=%t err=%v", ok, err)
}
if err := delivery.Ack(false); err != nil {
t.Fatal(err)
}
for _, id := range ids {
route, _ := tenant.ResultRoute(id)
if err := admin.QueueUnbind(shared, route.BindingKey, route.Exchange, nil); err != nil {