Files
HomeAgent/csrc/test/test_ha_codec.c
JianFeeeee 7351c6ca2e feat(c-core): 内核编解码层 C 化第一刀 —— L1 纯函数层落地并打通构建链
第一刀只做三个纯函数(窗口推断 / token 估算 / token 截断),
价值不在功能(Go 版没问题),而在打通「Go → cgo → C」全链路并
建立可复现的对照范式,后面每扩一个函数都复用它。

## 为什么是这三个

按「无状态 → 有状态」分层,L1 协议编解码最安全:纯 string in → struct out,
不碰网络、不碰 Lua、不碰 goroutine。三个函数更是同一组纯算术,最小可验证切片。

## 关键设计:包内符号链接,不链接静态库、不 include 包外源

三种做法都实测过,只有一种同时满足「可构建 + 可交叉编译 + 缓存可跟踪」:

1. ❌ 链接 `csrc/build/libha_codec.a`(原方案)
   - .a 是构建产物、不入库(.gitignore 的 build/ 命中 csrc/build/),
     而发布脚本原先并不产出它 ⇒「不入库 + 不生成」两头空,
     实测报 `cannot find .../libha_codec.a`
   - 交叉编译 linux/arm64(homed 真实发布目标)时,宿主 x86-64 的 .a
     被链进目标产物,实测报 `file in wrong format`

2. ❌ `#include "../../../csrc/src/ha_codec.c"`(包外相对包含)
   ★ Go 构建缓存**不跟踪包外被 #include 的 C 文件**。实测:包外源把返回值
   7→8,`go test` 依然通过(缓存命中、静默沿用旧代码);同样改动落在包内
   文件时立即判红。对「逐步推进 C 化」这是致命的——改 C 源码不生效且无报错。
   (包内 shim `#include` 包外源同样漏跟踪,已实测排除。)

3. ✅ 包内符号链接 `internal/agent/api/ha_codec.{c,h}` → `csrc/`
   文件在包目录内 ⇒ 缓存按内容正确跟踪;只有一份权威源 ⇒ 无副本漂移,
   也不需要「同步 C 源」的 make 目标。

## 不需要额外 build tag

ha_codec 是零依赖纯 C99 源码内联编译,不需要外部库或工具链前提;
而 homed 本就强制 cgo(sqlite3 + gojieba),故 C 路径自然生效。
只用 `cgo` / `!cgo` 一组约束(对比 onnxruntime:那个需运行期 .so,故必须显式 tag)。

## 顺带修掉的既有缺陷(非 C 化引入,但一直缺覆盖)

- Makefile 的 arm64 目标缺 CC/CXX:cgo 回退到宿主 g++,报
  `gcc_arm64.S: no such instruction: 'stp x29,x30,[sp,'`
  (deploy/packaging/build.sh:49 一直是对的,Makefile 漏了)
- Makefile 的 arm64 目标缺 .syso 隔离:cmd/{homed,waiter}/*.syso 是 Windows
  COFF 资源对象,Go 会把同目录 .syso 无条件链进任何目标,交叉到非 Windows
  平台报 `file format not recognized`(build.sh 有 hide_syso_for_target)

## 验证(每条可复现)

- `make build-linux-arm64` → ELF 64-bit LSB executable, ARM aarch64(82MB)
- `bash deploy/packaging/build.sh linux/arm64 homed` → ELF aarch64(79MB)
- 移走 csrc/build/ 后 homed(cgo)与 waiter(CGO=0)均能构建
- 变异 C 源(131072→777)后**同一缓存**下 go test 立即 FAIL(改前:仍报 ok)
- `make check-codec-paths` 两条路径 OK;`make csrc-test` C 契约测试 100%
- 黄金对照:手写用例 + 2000 次随机对拍,C 与纯 Go 逐值相等
- 全量 `go test -count=1 ./...` → 57 包:38 ok + 19 无测试 + 0 FAIL

新增 `make check-codec-paths` 防回归:只测一条路径时,另一条的破坏不会被发现。

## 文档

- `docs/zh/c-core/llm-orchestration-c.md` 同步为「已落地」,并更正因
  「Windows 原生已放弃」而过时的 §2.3(回退路径的理由需重述)
- plan.md 的 P0-1 标记为已修复,附实测证据
2026-09-25 14:45:08 +08:00

132 lines
4.7 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* test_ha_codec.c — ha_codec C 侧契约测试
*
* 编译运行(无 cmake 亦可):
* gcc -std=c99 -I../include ../src/ha_codec.c test_ha_codec.c -o test_ha_codec && ./test_ha_codec
*
* 这一层钉死 C 实现的语义;与 Go 的逐值一致由黄金对照测试负责(双保险)。
*/
#include "ha_codec.h"
#include <stdio.h>
#include <string.h>
static int g_fail = 0;
static int g_pass = 0;
static void check_int(const char *what, int got, int want) {
if (got != want) {
printf(" [FAIL] %s: got %d, want %d\n", what, got, want);
g_fail++;
} else {
g_pass++;
}
}
static void check_str(const char *what, const char *got, const char *want) {
if (strcmp(got, want) != 0) {
printf(" [FAIL] %s: got \"%s\", want \"%s\"\n", what, got, want);
g_fail++;
} else {
g_pass++;
}
}
static void check_size(const char *what, size_t got, size_t want) {
if (got != want) {
printf(" [FAIL] %s: got %zu, want %zu\n", what, got, want);
g_fail++;
} else {
g_pass++;
}
}
static void test_context_window(void) {
printf("model_context_window:\n");
check_int("deepseek-v4.1-flash",
ha_codec_model_context_window("deepseek/deepseek-v4.1-flash"), 1048576);
check_int("deepseek-v4-flash",
ha_codec_model_context_window("deepseek-v4-flash"), 1048576);
check_int("deepseek-chat",
ha_codec_model_context_window("deepseek-chat"), 65536);
check_int("claude-opus-5",
ha_codec_model_context_window("claude-opus-5"), 100000);
check_int("gpt-4-turbo",
ha_codec_model_context_window("gpt-4-turbo"), 128000);
check_int("llama-3-70b",
ha_codec_model_context_window("llama-3-70b"), 8192);
check_int("AUTO (unknown)",
ha_codec_model_context_window("AUTO"), HA_CODEC_CONTEXT_WINDOW_UNKNOWN);
check_int("NULL (unknown)",
ha_codec_model_context_window(NULL), HA_CODEC_CONTEXT_WINDOW_UNKNOWN);
check_int("case-insensitive",
ha_codec_model_context_window("QWEN-MAX"), 131072);
check_int("moonshot",
ha_codec_model_context_window("moonshot-v1-128k"), 131072);
/* 分支顺序:gpt-4-turbo 必须先于裸 gpt-4 命中 */
check_int("gpt-4-mini (branch order)",
ha_codec_model_context_window("gpt-4-mini"), 128000);
check_int("gpt-4 (bare)",
ha_codec_model_context_window("gpt-4"), 8192);
/* claude-3 必须先于裸 claude */
check_int("claude-3-opus (branch order)",
ha_codec_model_context_window("claude-3-opus"), 200000);
}
static void test_estimate_tokens(void) {
printf("estimate_tokens:\n");
check_int("empty", ha_codec_estimate_tokens(""), 0);
check_int("NULL", ha_codec_estimate_tokens(NULL), 0);
/* "abc" = 3 rune * 2 = 6 */
check_int("ascii abc", ha_codec_estimate_tokens("abc"), 6);
/* "你好" = 2 rune * 2 = 4(注意:不是字节数 6) */
check_int("chinese 2 chars", ha_codec_estimate_tokens("你好"), 4);
/* 混合 "a你" = 2 rune * 2 = 4 */
check_int("mixed", ha_codec_estimate_tokens("a你"), 4);
/* 4 字节 emoji:1 rune * 2 = 2 */
check_int("emoji", ha_codec_estimate_tokens("\xF0\x9F\x98\x80"), 2);
}
static void test_truncate(void) {
printf("truncate_by_tokens:\n");
char buf[64];
/* max_tokens<=0 → 空 */
ha_codec_truncate_by_tokens("hello", 0, buf, sizeof(buf));
check_str("max_tokens=0", buf, "");
/* 未超限 → 原样返回 */
size_t n = ha_codec_truncate_by_tokens("abc", 100, buf, sizeof(buf));
check_str("no truncation", buf, "abc");
check_size("no truncation len", n, 3);
/* "abcdefghij" = 10 rune → 20 tokens;max=8 → keep=4 → "abcd" */
n = ha_codec_truncate_by_tokens("abcdefghij", 8, buf, sizeof(buf));
check_str("keep 4", buf, "abcd");
check_size("keep 4 len", n, 4);
/* 中文按 rune 截断,不切碎 UTF-8:"你好世界" 4 rune,max=4 → keep=2 → "你好" */
n = ha_codec_truncate_by_tokens("你好世界", 4, buf, sizeof(buf));
check_str("chinese keep 2", buf, "你好");
check_size("chinese keep 2 len (bytes)", n, 6);
/* 缓冲区不足:必须 NUL 结尾且不越界 */
char tiny[4];
n = ha_codec_truncate_by_tokens("abcdefghij", 100, tiny, sizeof(tiny));
check_size("tiny buf len", n, 3);
check_str("tiny buf NUL-terminated", tiny, "abc");
/* out_cap=0 不写 */
check_size("zero cap", ha_codec_truncate_by_tokens("abc", 100, tiny, 0), 0);
}
int main(void) {
printf("=== ha_codec 契约测试 ===\n\n");
test_context_window();
test_estimate_tokens();
test_truncate();
printf("\n=== 结果: %d passed, %d failed ===\n", g_pass, g_fail);
return g_fail == 0 ? 0 : 1;
}