Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68670b2819 | ||
|
|
435c8e19f9 | ||
|
|
537beb2934 |
@@ -1,5 +1,17 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-29 — 咨询首字前少等最多 3 秒
|
||||
|
||||
- 判断「是不是寒暄」改为和扣点、记下问题同时进行,不再排在它们后面;真问题首字前最多快约 3 秒。
|
||||
- 计费不变:寒暄照旧先扣后退、免费;扣点失败时照旧提示,不会多扣。
|
||||
- Skill 版本不变(BUG-1499,issue #162)。
|
||||
|
||||
## 2026-10-29 — 咨询回答快约三分之一
|
||||
|
||||
- 咨询里写回答那一步,模型的思考量默认改为「低」:对比测试里思考量少约一半,一轮回答从约 34 秒降到约 22 秒,回答质量逐份并排读过没有变差(issue #134,产品选 A)。
|
||||
- 只影响写回答那一步;排盘、查资料那几步不变。
|
||||
- 需要退回时,在服务器设 `CONSULT_ANSWER_REASONING_EFFORT=off` 即恢复原来的请求。
|
||||
|
||||
## 2026-10-28 — 切换人物时看得出是谁的盘
|
||||
|
||||
- 「当前星盘」按钮上写名字、出生日期时间和城市;名单每行写关系和出生时间、城市。
|
||||
|
||||
+29
-14
@@ -20463,18 +20463,33 @@
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/rectification-recap-date-dup-20261028`。
|
||||
|
||||
## BUG-1498 | staging 门禁 Python 快速门偶发失败:`test_daily_starlanguage_does_not_eat_yesterday_cache` 网关调用 0 次
|
||||
## BUG-1498 | staging 门禁 Python 快速门失败:`test_daily_starlanguage_does_not_eat_yesterday_cache` 网关调用 0 次
|
||||
|
||||
- 状态:investigating(只记录已确认事实与假设;未改代码)
|
||||
- 首次发现 / 最近更新:2026-10-28(BUG-1481 验证段首次提到)/ 2026-10-28(staging 门禁 run 2017)
|
||||
- 影响面:只影响 CI 门禁,不影响线上。`tests/test_vedastro_snapshot_cache.py::test_daily_starlanguage_does_not_eat_yesterday_cache`。
|
||||
- 现象:`assert calls["gateway"] == 1` 得到 `0 == 1`;同一轮其余 1094 项通过。run 2016(同一份 Python 代码)这一项通过;本机单独连跑 5 次全部通过。
|
||||
- 触发条件:整套快速门一起跑时偶发,单独跑不出现,跟测试先后顺序或机器负载有关。
|
||||
- 已排除:BUG-1481 当时认为是测试之间共用负缓存目录;`tests/conftest.py::_isolate_vedastro_snapshot_cache` 现在每个测试都另建快照目录和负缓存目录,这次复发不是那个原因。
|
||||
- 假设(未证实):`scripts/vedastro_foreground.py` 的 `_FOREGROUND_VEDASTRO_EXECUTOR` 是模块级线程池,整个 pytest 进程共用,conftest 不清空它。前面的测试如果留下还在跑的任务占满线程,本测试的任务排在后面,前台 1.5 秒等待到期后被取消,网关一次都没调用。
|
||||
- 修复:未修。下一步:在 conftest 里每个测试后排空或重建这个线程池,或者本测试把等待时间设大,再在全套顺序下复跑确认。
|
||||
- 验证:未完成。
|
||||
- 防复发:未完成。
|
||||
- 关联:BUG-1481(第一次记录这个失败)。
|
||||
- 复发自:BUG-1481 记下的同一项失败。当时的处理(隔离负缓存目录)没拦住,因为根因很可能不在目录上。
|
||||
- 修复版本:无。
|
||||
- 状态:resolved(测试隔离 + 回归测试;以修复后第一轮 staging 门禁通过为准)
|
||||
- 首次发现 / 最近更新:2026-10-28(BUG-1481 验证段首次提到)/ 2026-10-28(staging 门禁 run 2017、2018 连续两次失败)
|
||||
- 影响面:只影响 CI 门禁,不影响线上。失败的是 `tests/test_vedastro_snapshot_cache.py::test_daily_starlanguage_does_not_eat_yesterday_cache`;同样的机制可能打到任何用前台 VedAstro 线程池的测试。
|
||||
- 现象:`assert calls["gateway"] == 1` 得到 `0 == 1`;同一轮其余 1094 项通过。run 2016 这一项通过,run 2017、2018 失败,三次 Python 代码相同;本机单独跑或整套跑都通过。
|
||||
- 触发条件:整套测试一起跑,前面某个测试留在前台线程池里的任务,在下一个测试开始之后才跑完。
|
||||
- 根因:`scripts/vedastro_foreground.py::_FOREGROUND_VEDASTRO_EXECUTOR` 是模块级线程池,整个 pytest 进程共用(`jyotish_api_server` 还按名字另外导入了一份)。任务跑完后,`_remember_gateway_outcome` 把结果写进「写的那一刻」环境变量指向的缓存目录。上一个测试留下的任务跑完时,环境变量已经指向下一个测试的隔离目录,于是一次失败被记成下一个测试「今天已经失败过」,`_failed_today` 命中,网关一次都没调用。线程池被占满时,任务排队到超时被取消,也会得到同样的 0 次。#130 给每个测试单独建目录,挡不住这种跨测试写入,所以 BUG-1481 那次的判断(共用目录)不是根因。
|
||||
- 修复:`tests/conftest.py::_isolate_foreground_vedastro_executor`。每个测试换一个新的前台线程池(两个模块里的名字都换);测试结束时丢掉排队的任务,正在跑的最多等 10 秒,所以剩下的任务一定在本测试的目录还有效时写完。生产代码没有改。
|
||||
- 验证:新增 `tests/test_vedastro_executor_isolation_20261028.py`:第一个测试留下一个 0.3 秒的任务,第二个测试断言它已经在上一个测试收尾时跑完、而且没有写进自己的目录。没有 fixture 时红(`assert 0 == 1`),加上后绿。`test_vedastro_snapshot_cache.py`、`test_vedastro_negative_day_20261028.py`、`test_vedastro_consultation_snapshot.py` 一起 19 项通过;本机完整快速门(`run_quality_gate.py --profile quick --skip-yoga-logic --skip-frontend-runtime`)通过。
|
||||
- 防复发:上面那组顺序测试固定了「任务不跨测试」。以后新增模块级线程池的,要放进同一个 fixture。
|
||||
- 关联:BUG-1481(第一次记下这个失败)、BUG-1479(#130,负缓存与每测试目录)。
|
||||
- 复发自:BUG-1481 记下的同一项失败。当时的处理(每测试单独目录)没拦住,因为根因是后台任务跨测试写入,不在目录共用上。
|
||||
- 修复版本:分支 `codex/bug-1498-executor-isolation`。
|
||||
|
||||
|
||||
## BUG-1499 | 咨询首字前要白等分类:判断寒暄要等扣点、写入问题都做完才开始
|
||||
|
||||
- 状态:resolved(代码 + 回归测试;待部署)
|
||||
- 首次发现 / 最近更新:2026-10-28 / 2026-10-29(issue #118 代码体检第 7 条 → #135 → #162)
|
||||
- 影响面:`frontend/src/app/api/consult/route.ts::runTurn`、`frontend/src/lib/consultation-route-service.ts`(`beforeReserve` 上下文多带 `subject`)。
|
||||
- 现象:每个咨询问题首字前,「是不是寒暄」的分类(最多 3 秒)排在扣点、写入问题之后才开始,三步串行。
|
||||
- 触发条件:任何未带 entrypoint 的咨询轮次。
|
||||
- 根因:分类调用写在 `appendQuestion()` 之后;它只需要用户原话、历史、称呼和会话模型,不依赖扣点或写入的结果。
|
||||
- 修复:审核放行后,在 `beforeReserve` 里开始分类,与扣点、写入同时进行;写入完成后再等分类结果,按原来的寒暄 / 正常两条路走。用的模型是扣点选中的同一个 `sessionModel` 对象。计费顺序、寒暄先扣后退(`complete_consultation_free`)与金额都不变;扣点失败时分类结果丢弃,最多白调一次分类模型(有 3 秒上限,失败默认按正常问题)。新增计时 `prep.classify_wait`(写入后还要等分类多久)。
|
||||
- 验证:前端相关 71 条测试过;全量 5339 条里 25 条失败,与改动前同一批(Docker / 数据库 / rsync 环境),无新增;tsc 0 错、lint 0 错。
|
||||
- 防复发:`consultation-smalltalk.test.ts` 锁住分类在审核之后、扣点之前开始,且用扣点选中的同一个模型对象;寒暄分支仍走 `completeSmalltalk`。
|
||||
- 关联:BUG-976(寒暄先扣后退)、BUG-1074(开流前的串行链)、#134(同批提速)。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/consult-classify-parallel-20261029`。
|
||||
|
||||
@@ -546,3 +546,5 @@
|
||||
| `TASK-home-crash-saved-people-20261027.md` | `PROGRESS-home-crash-saved-people-20261027.md` | **#115 首页崩溃**:存过别人的账户一进首页就「页面出错了」——人物列表的资料缺两个字段,启程引导一读就报错;入口补齐默认值(BUG-1464) | 待验收 | `codex/home-crash-saved-people-20261027` |
|
||||
| `TASK-upstream-sync9-20261028.md` | `PROGRESS-upstream-sync9-20261028.md` | **第 1 次打回(快速门 1 败:Darakaraka 旧测试锁错值,见 `TASK-upstream-sync9-fix-20261028.md`)** · **#141 上游同步第九轮**:甲批九项已做完并推 `codex/upstream-sync9-20261028`;校正 20 条和 900 条与上一份回放逐条相同;乙批仍等 #137 | 待验收 | `codex/upstream-sync9-20261028` |
|
||||
| `TASK-upstream-sync9b-20261028.md` | `PROGRESS-upstream-sync9b-20261028.md` | **第 1 次打回(冻结契约排除清单删了第八轮两条,见 `TASK-upstream-sync9b-fix-20261028.md`)** · **#159 上游同步第九轮乙批(产品选 A)**:Sarpa Drekkana 补成五段、第 22 号 Drekkana 改为 D3 第 8 宫;改冻结文件 `varga.py`,校正 20 + 900 与上一份逐条相同后按 ERR-110 重新冻结。Narayana 小运强度口径(B2)不做,另问产品 | 待验收 | `codex/upstream-sync9b-20261028` |
|
||||
| — | — | **#134 咨询答题默认低思考量(产品选 A)**:`CONSULT_ANSWER_REASONING_EFFORT` 不设时默认 `low`,设 `off` 恢复原请求体(Claude 直接执行) | 已推 staging | `codex/answer-effort-default-20261029` |
|
||||
| — | — | **#162 咨询分类与扣点同时进行(产品同意)**:审核放行后分类与扣点、写入并行;计费与寒暄退点不变(BUG-1499,Claude 直接执行) | 已推 staging | `codex/consult-classify-parallel-20261029` |
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# 真机走查 · 咨询回答默认低思考量(issue #134)
|
||||
|
||||
| # | 操作 | 应该看到 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 用一张星盘问一个具体问题(如「今年事业怎么样」) | 回答正常出来,比以前快一截(以前常要半分钟以上) |
|
||||
| 2 | 连问两三个追问 | 每轮都正常回答,引用的宫位、大运和以前一样具体 |
|
||||
| 3 | 问一个时间段(如「明年上半年」) | 照常出时段分析,没有空答或中途断掉 |
|
||||
@@ -0,0 +1,7 @@
|
||||
# 真机走查 · 咨询分类与扣点同时进行(issue #162)
|
||||
|
||||
| # | 操作 | 应该看到 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 用一张星盘问一个具体问题 | 正常回答;首字出来比以前快一点 |
|
||||
| 2 | 只发「你好」或「谢谢」 | 回一句寒暄;点数余额不变(先扣后退) |
|
||||
| 3 | 点数不足时发一个问题 | 照旧提示「咨询点数不足」,不会多扣 |
|
||||
@@ -537,6 +537,49 @@ export async function POST(request: Request) {
|
||||
},
|
||||
});
|
||||
let prepared: PreparedConsultationRoute<ModelSelection, ConsultationPlan>;
|
||||
// Issue #162: classification starts once moderation passes and runs beside
|
||||
// reserve + append instead of after them. Billing order is unchanged: a
|
||||
// smalltalk turn is still reserved, then refunded by completeSmalltalk.
|
||||
// The classifier fails open and is capped at 3 s, so an unused early call
|
||||
// (reserve refused, append failed) costs at most that one call.
|
||||
const earlyClassification: { turn: ReturnType<typeof classifyConsultationTurn> | null } = { turn: null };
|
||||
let classifyStarted = 0;
|
||||
function startClassification(name: string) {
|
||||
classifyStarted = Date.now();
|
||||
return classifyConsultationTurn({
|
||||
model: sessionModel,
|
||||
question: visibleQuestion,
|
||||
history: chatSession.messages,
|
||||
name,
|
||||
signal: request.signal,
|
||||
onObservation(observation) {
|
||||
if (!observation.late) {
|
||||
classificationUsage = observation.usage ?? {};
|
||||
classificationOutcome = observation.outcome;
|
||||
classificationDurationMs = observation.durationMs;
|
||||
}
|
||||
const inputTokens = observation.usage?.inputTokens ?? 0;
|
||||
const outputTokens = observation.usage?.outputTokens ?? 0;
|
||||
logAgentObservability({
|
||||
requestId, sessionId,
|
||||
agentVersion: "consultation-smalltalk-v1",
|
||||
modelVersion: String(sessionModel.configVersion),
|
||||
policyVersion: "consultation-smalltalk-v1",
|
||||
toolCalls: [],
|
||||
classification: { durationMs: observation.durationMs },
|
||||
contractPhases: [{
|
||||
phase: observation.late ? "classification.late_usage" : `classification.${observation.outcome}`,
|
||||
durationMs: observation.durationMs,
|
||||
status: observation.outcome === "smalltalk" || observation.outcome === "consult" ? "completed" : "failed",
|
||||
}],
|
||||
...(observation.usage ? { inputTokens, outputTokens,
|
||||
costMicrousd: Math.round((inputTokens * (sessionModel.inputCostMicrousdPerMillion ?? 0)
|
||||
+ outputTokens * (sessionModel.outputCostMicrousdPerMillion ?? 0)) / 1_000_000) } : {}),
|
||||
billingSettlementResult: "not_applicable",
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
const prepareStarted = Date.now();
|
||||
try {
|
||||
prepared = await prepareConsultationRoute<ModelSelection, ConsultationPlan>({
|
||||
@@ -570,7 +613,7 @@ export async function POST(request: Request) {
|
||||
return data;
|
||||
},
|
||||
loadCandidateRange: (profileUserId) => loadReportCandidateRange(accounting, { userId: profileUserId }),
|
||||
beforeReserve: async ({ consultationMode, serverChart }) => {
|
||||
beforeReserve: async ({ consultationMode, serverChart, declaredWindow, subject }) => {
|
||||
if (serverChart && consultationMode !== "declared_birth_window") prewarmConsultPacketForTurn(serverChart.toolInput);
|
||||
prepPhases.push({
|
||||
phase: "prep.subject",
|
||||
@@ -579,6 +622,9 @@ export async function POST(request: Request) {
|
||||
});
|
||||
const inputModeration = await settleInputModeration();
|
||||
if (inputModeration?.verdict === "block") throw new ConsultationInputBlockedError(inputModeration);
|
||||
if (parsed.data.entrypoint === undefined) {
|
||||
earlyClassification.turn = startClassification(subject?.name || serverChart?.name || declaredWindow?.name || "");
|
||||
}
|
||||
return createConsultationPlan({
|
||||
userIntent: parsed.data.question,
|
||||
theme: consultationTheme,
|
||||
@@ -718,42 +764,14 @@ export async function POST(request: Request) {
|
||||
.eq("id", sessionId)
|
||||
.eq("user_id", userId)
|
||||
.maybeSingle();
|
||||
const classifyStarted = Date.now();
|
||||
const turn = parsed.data.entrypoint === undefined
|
||||
? await classifyConsultationTurn({
|
||||
model: selectedModel,
|
||||
question: visibleQuestion,
|
||||
history: chatSession.messages,
|
||||
name: subjectName,
|
||||
signal: request.signal,
|
||||
onObservation(observation) {
|
||||
if (!observation.late) {
|
||||
classificationUsage = observation.usage ?? {};
|
||||
classificationOutcome = observation.outcome;
|
||||
classificationDurationMs = observation.durationMs;
|
||||
}
|
||||
const inputTokens = observation.usage?.inputTokens ?? 0;
|
||||
const outputTokens = observation.usage?.outputTokens ?? 0;
|
||||
logAgentObservability({
|
||||
requestId, sessionId,
|
||||
agentVersion: "consultation-smalltalk-v1",
|
||||
modelVersion: String(selectedModel.configVersion),
|
||||
policyVersion: "consultation-smalltalk-v1",
|
||||
toolCalls: [],
|
||||
classification: { durationMs: observation.durationMs },
|
||||
contractPhases: [{
|
||||
phase: observation.late ? "classification.late_usage" : `classification.${observation.outcome}`,
|
||||
durationMs: observation.durationMs,
|
||||
status: observation.outcome === "smalltalk" || observation.outcome === "consult" ? "completed" : "failed",
|
||||
}],
|
||||
...(observation.usage ? { inputTokens, outputTokens,
|
||||
costMicrousd: Math.round((inputTokens * (selectedModel.inputCostMicrousdPerMillion ?? 0)
|
||||
+ outputTokens * (selectedModel.outputCostMicrousdPerMillion ?? 0)) / 1_000_000) } : {}),
|
||||
billingSettlementResult: "not_applicable",
|
||||
});
|
||||
},
|
||||
})
|
||||
: { kind: "consult" as const };
|
||||
// Started in beforeReserve, beside reserve + append (issue #162).
|
||||
const classifyWaitStarted = Date.now();
|
||||
const turn = earlyClassification.turn ? await earlyClassification.turn : { kind: "consult" as const };
|
||||
prepPhases.push({
|
||||
phase: "prep.classify_wait",
|
||||
durationMs: Math.max(0, Date.now() - classifyWaitStarted),
|
||||
status: "completed",
|
||||
});
|
||||
prepPhases.push({
|
||||
phase: "prep.classify",
|
||||
durationMs: classificationDurationMs ?? Math.max(0, Date.now() - classifyStarted),
|
||||
|
||||
@@ -138,6 +138,8 @@ type ConsultationPreReserveContext = Readonly<{
|
||||
serverChart: ServerChartConsultation | null;
|
||||
declaredWindow: DeclaredBirthWindowConsultation | null;
|
||||
generalDailyReference: GeneralDailyReference | null;
|
||||
/** The bound subject, so pre-reserve work can address it by name (issue #162). */
|
||||
subject: PreparedConsultationSubject | null;
|
||||
}>;
|
||||
|
||||
type PrepareConsultationRouteInput<Reservation> = Readonly<{
|
||||
@@ -615,6 +617,7 @@ export async function prepareConsultationRoute<Reservation, GuardResult>(
|
||||
serverChart,
|
||||
declaredWindow,
|
||||
generalDailyReference,
|
||||
subject,
|
||||
}) as Awaited<GuardResult>
|
||||
: undefined;
|
||||
const reservation = await input.reserve();
|
||||
|
||||
@@ -177,22 +177,26 @@ export const MAX_EVIDENCE_LOOKUPS_PER_TURN = 3;
|
||||
export const CONSULTATION_WINDOW_CALC_TOOL_ID = "run-jyotish-window-consultation";
|
||||
|
||||
/**
|
||||
* Answer-step reasoning effort. Unset or blank keeps today's request body.
|
||||
* An illegal value warns once and is treated as unset.
|
||||
* Answer-step reasoning effort. Unset or blank means "low" (issue #134: the
|
||||
* A/B halved reasoning tokens and cut answer time ~37% with no quality loss).
|
||||
* "off" sends no reasoning_effort, i.e. the pre-#134 request body.
|
||||
* An illegal value warns once and is treated as "off".
|
||||
*/
|
||||
const CONSULT_ANSWER_REASONING_EFFORTS = ["low", "medium", "high", "max"] as const;
|
||||
export const CONSULT_ANSWER_REASONING_EFFORT_DEFAULT: ProviderReasoningEffort = "low";
|
||||
let consultAnswerReasoningEffortWarned = false;
|
||||
|
||||
export function consultAnswerReasoningEffort(): ProviderReasoningEffort | undefined {
|
||||
const raw = process.env.CONSULT_ANSWER_REASONING_EFFORT;
|
||||
if (raw === undefined || raw.trim() === "") return undefined;
|
||||
if (raw === undefined || raw.trim() === "") return CONSULT_ANSWER_REASONING_EFFORT_DEFAULT;
|
||||
const value = raw.trim();
|
||||
if (value === "off") return undefined;
|
||||
if ((CONSULT_ANSWER_REASONING_EFFORTS as readonly string[]).includes(value)) {
|
||||
return value as ProviderReasoningEffort;
|
||||
}
|
||||
if (!consultAnswerReasoningEffortWarned) {
|
||||
consultAnswerReasoningEffortWarned = true;
|
||||
console.warn(`CONSULT_ANSWER_REASONING_EFFORT=${value} is not low, medium, high, or max; leaving reasoning effort unset.`);
|
||||
console.warn(`CONSULT_ANSWER_REASONING_EFFORT=${value} is not off, low, medium, high, or max; leaving reasoning effort unset.`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -51,9 +51,12 @@ test("an illegal effort warns once and stays unset", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("unset effort keeps the pre-change request bodies", async () => {
|
||||
// 原值: 不设环境变量时请求体与改动前逐字节相同
|
||||
// 新值: 设成 "off" 时请求体与改动前逐字节相同;不设时默认 "low"(见下一条)
|
||||
// 原因: issue #134 产品选 A,答题步默认低思考量;"off" 是一行回退
|
||||
test("off keeps the pre-change request bodies", async () => {
|
||||
const previous = process.env.CONSULT_ANSWER_REASONING_EFFORT;
|
||||
delete process.env.CONSULT_ANSWER_REASONING_EFFORT;
|
||||
process.env.CONSULT_ANSWER_REASONING_EFFORT = "off";
|
||||
try {
|
||||
const natal = await captureStreamBodies({
|
||||
settings: consultationGenerationSettings(MODEL),
|
||||
@@ -76,6 +79,43 @@ test("unset effort keeps the pre-change request bodies", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("unset or blank effort defaults to low", () => {
|
||||
const previous = process.env.CONSULT_ANSWER_REASONING_EFFORT;
|
||||
try {
|
||||
delete process.env.CONSULT_ANSWER_REASONING_EFFORT;
|
||||
assert.equal(consultAnswerReasoningEffort(), "low");
|
||||
process.env.CONSULT_ANSWER_REASONING_EFFORT = " ";
|
||||
assert.equal(consultAnswerReasoningEffort(), "low");
|
||||
process.env.CONSULT_ANSWER_REASONING_EFFORT = "off";
|
||||
assert.equal(consultAnswerReasoningEffort(), undefined);
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.CONSULT_ANSWER_REASONING_EFFORT;
|
||||
else process.env.CONSULT_ANSWER_REASONING_EFFORT = previous;
|
||||
}
|
||||
});
|
||||
|
||||
test("unset effort puts low on the answer step only", async () => {
|
||||
const previous = process.env.CONSULT_ANSWER_REASONING_EFFORT;
|
||||
delete process.env.CONSULT_ANSWER_REASONING_EFFORT;
|
||||
try {
|
||||
const natal = await captureStreamBodies({
|
||||
settings: consultationGenerationSettings(MODEL),
|
||||
prepareStep: (input) => consultationNatalPrepareStep({
|
||||
stepNumber: input.stepNumber,
|
||||
answerReady: input.stepNumber >= 1,
|
||||
model: MODEL,
|
||||
}),
|
||||
steps: 2,
|
||||
}) as Record<string, unknown>[];
|
||||
assert.deepEqual(natal[0], fixture.natal[0]);
|
||||
assert.equal(natal[1]?.reasoning_effort, "low");
|
||||
assert.deepEqual(withoutEffort(natal[1]), fixture.natal[1]);
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.CONSULT_ANSWER_REASONING_EFFORT;
|
||||
else process.env.CONSULT_ANSWER_REASONING_EFFORT = previous;
|
||||
}
|
||||
});
|
||||
|
||||
test("low effort is only on the answer step", async () => {
|
||||
const previous = process.env.CONSULT_ANSWER_REASONING_EFFORT;
|
||||
process.env.CONSULT_ANSWER_REASONING_EFFORT = "low";
|
||||
|
||||
@@ -279,7 +279,11 @@ test("the consult route opens the stream before subject reads, reservation, ques
|
||||
"prepareConsultationRoute<ModelSelection, ConsultationPlan>({",
|
||||
"consultSettlement.reserveCredit",
|
||||
"consultSettlement.appendQuestion",
|
||||
"await classifyConsultationTurn({",
|
||||
// 原值: "await classifyConsultationTurn({"
|
||||
// 新值: 分类调用与等待结果两处都在流内
|
||||
// 原因: issue #162 分类提前到 beforeReserve 开始,结果在写入问题之后等待
|
||||
"return classifyConsultationTurn({",
|
||||
"await earlyClassification.turn",
|
||||
"insufficient ? 402 : 503",
|
||||
'error: "暂时无法生成解读"',
|
||||
]) {
|
||||
|
||||
@@ -77,5 +77,8 @@ test("a consult turn prewarms the card packet with the same identity the card re
|
||||
{ apiBase: "http://engine.test", fetch: (async () => { throw new Error("down"); }) as unknown as typeof fetch },
|
||||
);
|
||||
const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8");
|
||||
assert.match(route, /beforeReserve: async \(\{ consultationMode, serverChart \}\) => \{\n\s+if \(serverChart && consultationMode !== "declared_birth_window"\) prewarmConsultPacketForTurn\(serverChart\.toolInput\);/);
|
||||
// 原值: beforeReserve 只解构 consultationMode、serverChart
|
||||
// 新值: 同时解构 declaredWindow、subject;预热仍是第一句
|
||||
// 原因: issue #162 beforeReserve 多取 declaredWindow、subject,用来在审核后提前开始分类
|
||||
assert.match(route, /beforeReserve: async \(\{ consultationMode, serverChart, declaredWindow, subject \}\) => \{\n\s+if \(serverChart && consultationMode !== "declared_birth_window"\) prewarmConsultPacketForTurn\(serverChart\.toolInput\);/);
|
||||
});
|
||||
|
||||
@@ -163,7 +163,10 @@ test("the title read starts after the question is saved and is awaited at the ol
|
||||
const append = route.indexOf("consultSettlement.appendQuestion()");
|
||||
assert.match(settlement, /accounting\.rpc\("append_consultation_question"/);
|
||||
const title = route.indexOf('.select("title")');
|
||||
const classify = route.indexOf("await classifyConsultationTurn({");
|
||||
// 原值: 分类以 "await classifyConsultationTurn({" 在标题读取之后开始
|
||||
// 新值: 分类在 beforeReserve 里提前开始,这里在标题读取之后等它的结果
|
||||
// 原因: issue #162 分类与扣点、写入同时进行;标题读取仍在保存成功之后,等待次序不变
|
||||
const classify = route.indexOf("await earlyClassification.turn");
|
||||
const awaited = route.indexOf("await titleRead");
|
||||
assert.ok(append > 0 && title > append && classify > title && awaited > classify);
|
||||
});
|
||||
|
||||
@@ -222,8 +222,19 @@ test("smalltalk views never reconstruct an invented settled execution timeline",
|
||||
});
|
||||
test("route gates all special entries before a single selected-model classifier and keeps tool contracts", () => {
|
||||
const route = source("../src/app/api/consult/route.ts");
|
||||
assert.match(route, /parsed.data.entrypoint === undefined\s*\? await classifyConsultationTurn/);
|
||||
assert.match(route, /classifyConsultationTurn\(\{\s*model: selectedModel,\s*question: visibleQuestion/);
|
||||
// 原值: 分类在扣点、写入之后以 `entrypoint === undefined ? await classifyConsultationTurn` 串行开始,用 selectedModel
|
||||
// 新值: 审核放行后在 beforeReserve 里开始分类,与扣点、写入同时进行;用的 sessionModel 就是扣点选中的同一个对象
|
||||
// 原因: issue #162 去掉首字前最多 3 秒的串行等待;计费顺序与寒暄退点不变
|
||||
assert.match(route, /if \(parsed\.data\.entrypoint === undefined\) \{\s*earlyClassification\.turn = startClassification\(/);
|
||||
assert.match(route, /classifyConsultationTurn\(\{\s*model: sessionModel,\s*question: visibleQuestion/);
|
||||
assert.match(route, /\(modelId\) => sessionModel\?\.id === modelId \? sessionModel : null/);
|
||||
const beforeReserve = route.slice(route.indexOf("beforeReserve: async"), route.indexOf("reserve: async () =>"));
|
||||
assert.ok(
|
||||
beforeReserve.indexOf('if (inputModeration?.verdict === "block") throw') < beforeReserve.indexOf("earlyClassification.turn = startClassification("),
|
||||
"classification starts only after moderation lets the input through",
|
||||
);
|
||||
assert.ok(route.indexOf("earlyClassification.turn = startClassification(") < route.indexOf("consultSettlement.reserveCredit"));
|
||||
assert.match(route, /await earlyClassification\.turn/);
|
||||
assert.equal((route.match(/requireTool: true/g) ?? []).length, 2);
|
||||
const smalltalk = route.slice(route.indexOf('if (turn.kind === "smalltalk")'), route.indexOf("const expectedTitle"));
|
||||
const settlement = source("../src/lib/consult/settlement.ts");
|
||||
|
||||
@@ -167,7 +167,10 @@ test("consultation plans are server-owned and bounded", () => {
|
||||
prepareCall.slice(beforeReserveAt, reserveAt),
|
||||
// 原值:参数只解构 consultationMode;新值:还解构 serverChart,用来在扣点前不等待地预热数据卡
|
||||
// 原因:issue #129 每天首问不在回答路径上现算整包;createConsultationPlan 仍在这个回调里、仍早于 reserve
|
||||
/beforeReserve:\s*async\s*\(\{\s*consultationMode,\s*serverChart\s*\}\)[\s\S]*createConsultationPlan\(\{/,
|
||||
// 原值: beforeReserve 只解构 consultationMode、serverChart
|
||||
// 新值: 同时解构 declaredWindow、subject;计划仍在 beforeReserve 里生成
|
||||
// 原因: issue #162 beforeReserve 多取 declaredWindow、subject,用来在审核后提前开始分类
|
||||
/beforeReserve:\s*async\s*\(\{\s*consultationMode,\s*serverChart,\s*declaredWindow,\s*subject\s*\}\)[\s\S]*createConsultationPlan\(\{/,
|
||||
);
|
||||
assert.match(
|
||||
prepareCall.slice(reserveAt),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -58,6 +59,41 @@ def _isolate_vedastro_snapshot_cache(tmp_path_factory, monkeypatch) -> None:
|
||||
clear_validate_memory()
|
||||
|
||||
|
||||
_FOREGROUND_EXECUTOR_MODULES = (
|
||||
"scripts.vedastro_foreground",
|
||||
"vedastro_foreground",
|
||||
"scripts.jyotish_api_server",
|
||||
"jyotish_api_server",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolate_foreground_vedastro_executor(monkeypatch):
|
||||
"""A fresh foreground pool per test, drained at teardown (BUG-1498).
|
||||
|
||||
A task left running stores its outcome under whatever cache directory the
|
||||
environment names when it finishes, which would be the next test's.
|
||||
"""
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from scripts import vedastro_foreground
|
||||
|
||||
executor = ThreadPoolExecutor(
|
||||
max_workers=vedastro_foreground._FOREGROUND_VEDASTRO_WORKERS,
|
||||
thread_name_prefix="jyotish-vedastro-fg-test",
|
||||
)
|
||||
for name in _FOREGROUND_EXECUTOR_MODULES:
|
||||
module = sys.modules.get(name)
|
||||
if module is not None and hasattr(module, "_FOREGROUND_VEDASTRO_EXECUTOR"):
|
||||
monkeypatch.setattr(module, "_FOREGROUND_VEDASTRO_EXECUTOR", executor)
|
||||
yield
|
||||
# Queued work is dropped; running work gets a bounded wait so a stuck stub cannot hang the run.
|
||||
executor.shutdown(wait=False, cancel_futures=True)
|
||||
deadline = time.monotonic() + 10
|
||||
for thread in list(getattr(executor, "_threads", ())):
|
||||
thread.join(max(deadline - time.monotonic(), 0))
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(items) -> None:
|
||||
for item in items:
|
||||
if item.fspath.basename == "test_api_server_security.py" and item.name.startswith(SLOW_API_SECURITY_PREFIXES):
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""BUG-1498: a foreground VedAstro task never outlives the test that started it.
|
||||
|
||||
The two tests run in file order. The first leaves a slow task on the foreground
|
||||
executor; the second checks that the task finished before it began, so it could
|
||||
not write into the second test's cache directory.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
from scripts import vedastro_foreground as foreground
|
||||
|
||||
_SEEN: list[str] = []
|
||||
|
||||
|
||||
def _slow_task() -> None:
|
||||
time.sleep(0.3)
|
||||
_SEEN.append(os.environ.get("JYOTISH_VEDASTRO_SNAPSHOT_NEGATIVE_CACHE_DIR", ""))
|
||||
|
||||
|
||||
def test_a_leaves_slow_work_on_the_foreground_executor() -> None:
|
||||
_SEEN.clear()
|
||||
foreground._FOREGROUND_VEDASTRO_EXECUTOR.submit(_slow_task)
|
||||
|
||||
|
||||
def test_b_does_not_see_the_previous_tests_work() -> None:
|
||||
mine = os.environ["JYOTISH_VEDASTRO_SNAPSHOT_NEGATIVE_CACHE_DIR"]
|
||||
assert len(_SEEN) == 1, "the earlier test's task finished during its own teardown"
|
||||
time.sleep(0.5)
|
||||
assert mine not in _SEEN, "a leftover task wrote into this test's cache directory"
|
||||
Reference in New Issue
Block a user