@@ -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 错误说明的哈希见首表;两份使用者修改的第三方文档保持原状且未被纳入本轮变更。