Files
xiaobailing-turn-detection/labels/eot-studio/tests/annotations.test.ts
T

108 lines
6.6 KiB
TypeScript
Raw Normal View History

// @ts-nocheck -- Node's native TypeScript runner resolves explicit .ts imports.
import assert from "node:assert/strict";
import test from "node:test";
import {
createAnnotation,
exportAnnotations,
exportTranscripts,
formatTime,
getHistory,
parseAnnotations,
parseTranscripts,
reasonOptions,
validateAnnotation,
} from "../lib/annotations.ts";
const transcriptHeader = "call_id,role,start_ms,end_ms,transcript";
const annotationHeader = "call_id,decision_ms,user_text_at_decision,can_reply_now,confidence,reason,annotator";
const withoutId = (row) => Object.fromEntries(Object.entries(row).filter(([key]) => key !== "id"));
test("zero is a labeled negative example, and empty remains undecided", () => {
const rows = parseAnnotations(`\uFEFF${annotationHeader}\r\ndemo,5200,我明天下午,0,high,mid_turn_pause,A\r\ndemo,9400,听不清,,,audio_issue,A\r\n`);
assert.equal(rows[0].can_reply_now, "0");
assert.equal(rows[1].can_reply_now, "");
assert.equal(rows[1].confidence, "");
assert.notEqual(rows[0].id, rows[1].id);
assert.deepEqual(parseAnnotations(exportAnnotations(rows)).map(withoutId), rows.map(withoutId));
assert.equal(exportAnnotations(rows).split("\r\n")[0], annotationHeader);
assert.ok(!exportAnnotations(rows).includes(rows[0].id));
});
test("commas, doubled quotes and multiline UTF-8 text survive CSV round trips", () => {
const csv = `${transcriptHeader}\r\ndemo,user,100,800,"明天,\"\"下午\"\"\r\n都可以"\r\n`;
const rows = parseTranscripts(csv);
assert.equal(rows[0].transcript, '明天,"下午"\r\n都可以');
assert.deepEqual(parseTranscripts(exportTranscripts(rows)), rows);
const annotation = { ...createAnnotation("demo", 500), user_text_at_decision: '明天,"下午"\n先看看', annotator: 'A,"B"' };
assert.deepEqual(parseAnnotations(exportAnnotations([annotation])).map(withoutId), [withoutId(annotation)]);
});
test("required headers are validated while reordered columns remain usable", () => {
assert.equal(parseTranscripts("transcript,end_ms,role,call_id,start_ms\n可以,40,user,demo,20")[0].transcript, "可以");
assert.throws(() => parseTranscripts("call_id,role,start_ms,end_ms\ndemo,user,0,10"), /缺少字段.*transcript/);
assert.throws(() => parseTranscripts(`${transcriptHeader},extra\ndemo,user,0,10,可以,x`), /未知字段.*extra/);
assert.throws(() => parseTranscripts("call_id,role,start_ms,end_ms,transcript,role"), /重复字段/);
assert.throws(() => parseAnnotations(""), /为空/);
assert.deepEqual(parseAnnotations(annotationHeader), []);
});
test("malformed CSV and incorrect column counts give useful line diagnostics", () => {
assert.throws(() => parseTranscripts(`${transcriptHeader}\ndemo,user,0,100,"未闭合`), /第 2 行.*没有闭合/);
assert.throws(() => parseTranscripts(`${transcriptHeader}\ndemo,user,0,100,"可以"额外`), /结束引号后/);
assert.throws(() => parseTranscripts(`${transcriptHeader}\ndemo,user,0,100,可以"`), /双引号.*转义/);
assert.throws(() => parseTranscripts(`${transcriptHeader}\ndemo,user,0,100,多,一列`), /第 2 行.*实际有 6 列/);
});
test("invalid times and roles are rejected instead of being silently coerced", () => {
for (const start of ["", "-1", "1.5", "1e3", "9007199254740992"]) {
assert.throws(() => parseTranscripts(`${transcriptHeader}\ndemo,user,${start},2000,可以`), /非负整数毫秒/);
}
assert.throws(() => parseTranscripts(`${transcriptHeader}\ndemo,assistant,0,100,可以`), /agent 或 user/);
assert.throws(() => parseTranscripts(`${transcriptHeader}\ndemo,user,100,100,可以`), /小于 end_ms/);
assert.throws(() => parseTranscripts(`${transcriptHeader}\n,user,0,100,可以`), /call_id 不能为空/);
assert.throws(() => parseAnnotations(`${annotationHeader}\ndemo,-1,可以,1,high,completed_answer,A`), /非负整数毫秒/);
});
test("labels, confidence, known reasons and reason-label consistency are checked", () => {
assert.throws(() => parseAnnotations(`${annotationHeader}\ndemo,100,可以,2,high,completed_answer,A`), /can_reply_now 只能/);
assert.throws(() => parseAnnotations(`${annotationHeader}\ndemo,100,可以,1,strong,completed_answer,A`), /confidence 只能/);
assert.throws(() => parseAnnotations(`${annotationHeader}\ndemo,100,可以,1,high,freeform,A`), /未知的 reason/);
assert.throws(() => parseAnnotations(`${annotationHeader}\ndemo,100,可以,0,high,completed_answer,A`), /不一致/);
assert.throws(() => parseAnnotations(`${annotationHeader}\ndemo,100,可以,1,high,uncertain,A`), /不一致/);
for (const option of reasonOptions) {
const row = { ...createAnnotation("demo", 100), can_reply_now: option.can_reply_now, reason: option.value };
assert.equal(validateAnnotation(row), null);
}
});
test("duplicate decision positions are rejected within a call and allowed across calls", () => {
assert.throws(() => parseAnnotations(`${annotationHeader}\ndemo,100,可以,1,high,completed_answer,A\ndemo,100,等等,0,high,hesitation,B`), /已有标注/);
const first = createAnnotation("demo", 100);
const duplicate = createAnnotation("demo", 100);
assert.match(validateAnnotation(duplicate, [first]), /已有标注/);
assert.equal(validateAnnotation(first, [first]), null);
assert.throws(() => exportAnnotations([first, first]), /重复/);
assert.equal(parseAnnotations(`${annotationHeader}\ndemo,100,,,,,\nother,100,,,,,`).length, 2);
});
test("history uses all earlier completed records, excludes crossing and future records, and preserves tied order", () => {
const rows = parseTranscripts(`${transcriptHeader}\ndemo,user,300,440,我明天下午\ndemo,agent,100,240,什么时候方便\ndemo,user,400,700,不行后天可以\nother,user,0,5,其他通话\ndemo,user,300,400,同时出现的第二行\ndemo,agent,800,900,后天上午可以吗`);
const before = rows.slice();
assert.deepEqual(getHistory(rows, "demo", 520).map((row) => row.transcript), ["什么时候方便", "我明天下午", "同时出现的第二行"]);
assert.deepEqual(getHistory(rows, "demo", 440).map((row) => row.transcript), ["什么时候方便", "我明天下午", "同时出现的第二行"]);
assert.deepEqual(getHistory(rows, "demo", 100), []);
assert.deepEqual(rows, before);
});
test("new samples start undecided and time formatting preserves millisecond precision", () => {
const row = createAnnotation("demo", 0);
assert.equal(row.can_reply_now, "");
assert.equal(row.reason, "");
assert.equal(validateAnnotation(row), null);
assert.equal(formatTime(0), "00:00.000");
assert.equal(formatTime(5_200), "00:05.200");
assert.equal(formatTime(62_345), "01:02.345");
assert.equal(formatTime(3_600_000), "60:00.000");
assert.throws(() => createAnnotation("demo", -1), /非负整数/);
});