dsh-biocore 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +11 -0
  2. package/lib/core-worker.js +268 -0
  3. package/lib/index.js +160 -70
  4. package/lib/modules/autonomy.js +125 -0
  5. package/lib/modules/cache-stats.js +48 -0
  6. package/lib/modules/core-turn.js +24 -6
  7. package/lib/modules/expression-inject.js +0 -10
  8. package/lib/modules/expression-refresh.js +0 -9
  9. package/lib/modules/memory-capture.js +2 -1
  10. package/lib/modules/memory-inject.js +50 -5
  11. package/lib/modules/memory-tools.js +7 -4
  12. package/lib/modules/python-env.js +46 -0
  13. package/lib/modules/safety-gate.js +0 -3
  14. package/lib/modules/series.js +54 -0
  15. package/lib/modules/switch.js +16 -0
  16. package/lib/py-bridge.js +81 -15
  17. package/package.json +1 -1
  18. package/python/biocore_memory/core/config.py +138 -127
  19. package/python/biocore_memory/engine.py +2653 -2476
  20. package/python/biocore_memory/recall/embedding.py +129 -0
  21. package/python/biocore_memory/recall/feedback.py +202 -0
  22. package/python/biocore_model/preference_drift.py +255 -0
  23. package/python/biocore_model/tests/__init__.py +1 -0
  24. package/python/biocore_model/tests/core_mode_ui_check.mjs +63 -0
  25. package/python/biocore_model/tests/core_status_check.mjs +29 -0
  26. package/python/biocore_model/tests/dom_safe_check.mjs +22 -0
  27. package/python/biocore_model/tests/leak_samples_run.py +18 -0
  28. package/python/biocore_model/tests/orb_ring_check.mjs +20 -0
  29. package/python/biocore_model/tests/panel_html_check.mjs +63 -0
  30. package/python/biocore_model/tests/persona_flow_check.mjs +17 -0
  31. package/python/biocore_model/tests/render_guard_check.mjs +19 -0
  32. package/python/biocore_model/tests/switch_groups_check.mjs +94 -0
  33. package/python/biocore_model/tests/tabs_state_check.mjs +24 -0
  34. package/python/biocore_model/tests/test_affective_labor.py +102 -0
  35. package/python/biocore_model/tests/test_appraisal.py +137 -0
  36. package/python/biocore_model/tests/test_appraisal_action.py +55 -0
  37. package/python/biocore_model/tests/test_arch_convergence.py +35 -0
  38. package/python/biocore_model/tests/test_attachment_behavior.py +95 -0
  39. package/python/biocore_model/tests/test_attachment_regulation.py +63 -0
  40. package/python/biocore_model/tests/test_attachment_state.py +29 -0
  41. package/python/biocore_model/tests/test_attribution.py +56 -0
  42. package/python/biocore_model/tests/test_attributor.py +108 -0
  43. package/python/biocore_model/tests/test_autonomic.py +69 -0
  44. package/python/biocore_model/tests/test_autonomy_accumulator.py +84 -0
  45. package/python/biocore_model/tests/test_autonomy_affect.py +62 -0
  46. package/python/biocore_model/tests/test_autonomy_blame.py +44 -0
  47. package/python/biocore_model/tests/test_autonomy_caps.py +29 -0
  48. package/python/biocore_model/tests/test_autonomy_check.py +40 -0
  49. package/python/biocore_model/tests/test_autonomy_clingy.py +41 -0
  50. package/python/biocore_model/tests/test_autonomy_cooldown_selfcontact.py +138 -0
  51. package/python/biocore_model/tests/test_autonomy_frustration.py +42 -0
  52. package/python/biocore_model/tests/test_autonomy_hourly_limit.py +46 -0
  53. package/python/biocore_model/tests/test_autonomy_loop.py +64 -0
  54. package/python/biocore_model/tests/test_autonomy_mode.py +37 -0
  55. package/python/biocore_model/tests/test_autonomy_model.py +60 -0
  56. package/python/biocore_model/tests/test_autonomy_model_v2.py +68 -0
  57. package/python/biocore_model/tests/test_autonomy_module_text.py +104 -0
  58. package/python/biocore_model/tests/test_autonomy_params.py +163 -0
  59. package/python/biocore_model/tests/test_autonomy_reason.py +34 -0
  60. package/python/biocore_model/tests/test_autonomy_self.py +153 -0
  61. package/python/biocore_model/tests/test_autonomy_send_gate.py +38 -0
  62. package/python/biocore_model/tests/test_autonomy_silence.py +37 -0
  63. package/python/biocore_model/tests/test_autonomy_status.py +35 -0
  64. package/python/biocore_model/tests/test_autonomy_test_trigger.py +24 -0
  65. package/python/biocore_model/tests/test_autonomy_tick.py +148 -0
  66. package/python/biocore_model/tests/test_autonomy_v4.py +64 -0
  67. package/python/biocore_model/tests/test_autonomy_v5.py +43 -0
  68. package/python/biocore_model/tests/test_autonomy_v7.py +48 -0
  69. package/python/biocore_model/tests/test_autonomy_v_theta.py +85 -0
  70. package/python/biocore_model/tests/test_batch1_wiring.py +231 -0
  71. package/python/biocore_model/tests/test_batch2_wiring.py +231 -0
  72. package/python/biocore_model/tests/test_batch3_wiring.py +223 -0
  73. package/python/biocore_model/tests/test_bio_tick.py +202 -0
  74. package/python/biocore_model/tests/test_boundary.py +183 -0
  75. package/python/biocore_model/tests/test_boundary_gate.py +295 -0
  76. package/python/biocore_model/tests/test_brain_split.py +99 -0
  77. package/python/biocore_model/tests/test_co_recall.py +350 -0
  78. package/python/biocore_model/tests/test_commitment_extractor.py +197 -0
  79. package/python/biocore_model/tests/test_compassion_fatigue.py +84 -0
  80. package/python/biocore_model/tests/test_connection_bids.py +69 -0
  81. package/python/biocore_model/tests/test_core_bridge.py +88 -0
  82. package/python/biocore_model/tests/test_core_bridge_episode.py +14 -0
  83. package/python/biocore_model/tests/test_core_bridge_forget.py +17 -0
  84. package/python/biocore_model/tests/test_core_bridge_identity.py +15 -0
  85. package/python/biocore_model/tests/test_core_bridge_media.py +17 -0
  86. package/python/biocore_model/tests/test_core_bridge_relation.py +17 -0
  87. package/python/biocore_model/tests/test_core_bridge_summary.py +17 -0
  88. package/python/biocore_model/tests/test_core_client.py +156 -0
  89. package/python/biocore_model/tests/test_core_commands.py +146 -0
  90. package/python/biocore_model/tests/test_core_mode.py +161 -0
  91. package/python/biocore_model/tests/test_core_state.py +240 -0
  92. package/python/biocore_model/tests/test_criticality.py +61 -0
  93. package/python/biocore_model/tests/test_cross_session_copy.py +67 -0
  94. package/python/biocore_model/tests/test_debug_values.py +169 -0
  95. package/python/biocore_model/tests/test_defense_mechanisms.py +177 -0
  96. package/python/biocore_model/tests/test_delta_budget.py +157 -0
  97. package/python/biocore_model/tests/test_diag_trace.py +201 -0
  98. package/python/biocore_model/tests/test_directives_compat.py +698 -0
  99. package/python/biocore_model/tests/test_dissonance.py +97 -0
  100. package/python/biocore_model/tests/test_drift_sign.py +87 -0
  101. package/python/biocore_model/tests/test_embedding_fallback.py +277 -0
  102. package/python/biocore_model/tests/test_embedding_local.py +279 -0
  103. package/python/biocore_model/tests/test_embedding_mode.py +175 -0
  104. package/python/biocore_model/tests/test_embedding_modes.py +342 -0
  105. package/python/biocore_model/tests/test_emotion_geometry.py +114 -0
  106. package/python/biocore_model/tests/test_emotion_probe.py +214 -0
  107. package/python/biocore_model/tests/test_emotion_probe_concurrency.py +128 -0
  108. package/python/biocore_model/tests/test_entity_graph.py +198 -0
  109. package/python/biocore_model/tests/test_entity_resolve.py +446 -0
  110. package/python/biocore_model/tests/test_envy.py +173 -0
  111. package/python/biocore_model/tests/test_exit_or_silence.py +209 -0
  112. package/python/biocore_model/tests/test_expr_guard.py +1 -0
  113. package/python/biocore_model/tests/test_expr_intent.py +66 -0
  114. package/python/biocore_model/tests/test_expr_state.py +66 -0
  115. package/python/biocore_model/tests/test_expr_template.py +71 -0
  116. package/python/biocore_model/tests/test_expr_tone.py +41 -0
  117. package/python/biocore_model/tests/test_extreme_scenarios.py +147 -0
  118. package/python/biocore_model/tests/test_feedback_flow.py +185 -0
  119. package/python/biocore_model/tests/test_feedback_guard.py +65 -0
  120. package/python/biocore_model/tests/test_feedback_llm.py +59 -0
  121. package/python/biocore_model/tests/test_feedback_llm_inferred.py +140 -0
  122. package/python/biocore_model/tests/test_feedback_memory.py +63 -0
  123. package/python/biocore_model/tests/test_feedback_user.py +61 -0
  124. package/python/biocore_model/tests/test_frustration_curve.py +66 -0
  125. package/python/biocore_model/tests/test_global_bio_state.py +116 -0
  126. package/python/biocore_model/tests/test_governance.py +181 -0
  127. package/python/biocore_model/tests/test_granularity.py +133 -0
  128. package/python/biocore_model/tests/test_graph_extract.py +224 -0
  129. package/python/biocore_model/tests/test_graph_recall.py +226 -0
  130. package/python/biocore_model/tests/test_graph_sqlite.py +298 -0
  131. package/python/biocore_model/tests/test_growth.py +301 -0
  132. package/python/biocore_model/tests/test_intent_render.py +60 -0
  133. package/python/biocore_model/tests/test_intero_regulation.py +224 -0
  134. package/python/biocore_model/tests/test_intero_style.py +146 -0
  135. package/python/biocore_model/tests/test_item3_numeric.py +105 -0
  136. package/python/biocore_model/tests/test_item6_numeric.py +116 -0
  137. package/python/biocore_model/tests/test_layered_memory.py +174 -0
  138. package/python/biocore_model/tests/test_leak_guard.py +75 -0
  139. package/python/biocore_model/tests/test_learning_probe.py +93 -0
  140. package/python/biocore_model/tests/test_life_loop.py +222 -0
  141. package/python/biocore_model/tests/test_life_loop_10s.py +131 -0
  142. package/python/biocore_model/tests/test_local_embedding_health.py +258 -0
  143. package/python/biocore_model/tests/test_memory_affect_encoding.py +79 -0
  144. package/python/biocore_model/tests/test_memory_backend.py +332 -0
  145. package/python/biocore_model/tests/test_memory_dedup.py +316 -0
  146. package/python/biocore_model/tests/test_memory_dual.py +219 -0
  147. package/python/biocore_model/tests/test_memory_dual_wiring.py +219 -0
  148. package/python/biocore_model/tests/test_memory_feedback.py +241 -0
  149. package/python/biocore_model/tests/test_memory_fixes.py +84 -0
  150. package/python/biocore_model/tests/test_memory_forgetting.py +58 -0
  151. package/python/biocore_model/tests/test_memory_inject.py +211 -0
  152. package/python/biocore_model/tests/test_memory_mood_recall.py +118 -0
  153. package/python/biocore_model/tests/test_memory_store.py +372 -0
  154. package/python/biocore_model/tests/test_metacog.py +103 -0
  155. package/python/biocore_model/tests/test_migrate.py +245 -0
  156. package/python/biocore_model/tests/test_migrate_store.py +194 -0
  157. package/python/biocore_model/tests/test_mind_wander.py +65 -0
  158. package/python/biocore_model/tests/test_mood.py +35 -0
  159. package/python/biocore_model/tests/test_narrative.py +114 -0
  160. package/python/biocore_model/tests/test_needs_esteem.py +51 -0
  161. package/python/biocore_model/tests/test_needs_events.py +205 -0
  162. package/python/biocore_model/tests/test_needs_sleep.py +54 -0
  163. package/python/biocore_model/tests/test_noise_bank.py +54 -0
  164. package/python/biocore_model/tests/test_numeric_schema.py +98 -0
  165. package/python/biocore_model/tests/test_output_protocol.py +65 -0
  166. package/python/biocore_model/tests/test_page_debug.py +147 -0
  167. package/python/biocore_model/tests/test_page_status_autonomy.py +215 -0
  168. package/python/biocore_model/tests/test_pipeline.py +345 -0
  169. package/python/biocore_model/tests/test_pipeline_api.py +192 -0
  170. package/python/biocore_model/tests/test_pipeline_mode.py +222 -0
  171. package/python/biocore_model/tests/test_plugin_config.py +85 -0
  172. package/python/biocore_model/tests/test_policy_learning.py +116 -0
  173. package/python/biocore_model/tests/test_possible_selves.py +88 -0
  174. package/python/biocore_model/tests/test_predictor.py +227 -0
  175. package/python/biocore_model/tests/test_preference_drift.py +142 -0
  176. package/python/biocore_model/tests/test_preset.py +142 -0
  177. package/python/biocore_model/tests/test_proactive_context.py +70 -0
  178. package/python/biocore_model/tests/test_proactive_history.py +90 -0
  179. package/python/biocore_model/tests/test_proactive_memory.py +141 -0
  180. package/python/biocore_model/tests/test_proactive_send_record.py +88 -0
  181. package/python/biocore_model/tests/test_probe_contract.py +71 -0
  182. package/python/biocore_model/tests/test_prospective.py +154 -0
  183. package/python/biocore_model/tests/test_provider_fallback.py +89 -0
  184. package/python/biocore_model/tests/test_reconstruction.py +111 -0
  185. package/python/biocore_model/tests/test_regulation_strategies.py +228 -0
  186. package/python/biocore_model/tests/test_relation_stage.py +114 -0
  187. package/python/biocore_model/tests/test_relationship_stage.py +59 -0
  188. package/python/biocore_model/tests/test_release_140.py +55 -0
  189. package/python/biocore_model/tests/test_repair_after_boundary.py +249 -0
  190. package/python/biocore_model/tests/test_reply_limit.py +86 -0
  191. package/python/biocore_model/tests/test_reply_limit_persist.py +48 -0
  192. package/python/biocore_model/tests/test_reply_split.py +52 -0
  193. package/python/biocore_model/tests/test_reply_split_astrbot.py +66 -0
  194. package/python/biocore_model/tests/test_reply_split_inject.py +53 -0
  195. package/python/biocore_model/tests/test_rerank.py +229 -0
  196. package/python/biocore_model/tests/test_rollback.py +175 -0
  197. package/python/biocore_model/tests/test_runtime.py +175 -0
  198. package/python/biocore_model/tests/test_runtime_active.py +205 -0
  199. package/python/biocore_model/tests/test_runtime_pipeline.py +1583 -0
  200. package/python/biocore_model/tests/test_runtime_proactive.py +231 -0
  201. package/python/biocore_model/tests/test_scheduler.py +79 -0
  202. package/python/biocore_model/tests/test_secrecy.py +105 -0
  203. package/python/biocore_model/tests/test_self_esteem.py +45 -0
  204. package/python/biocore_model/tests/test_self_intent.py +347 -0
  205. package/python/biocore_model/tests/test_self_interest.py +183 -0
  206. package/python/biocore_model/tests/test_self_layer_wiring.py +198 -0
  207. package/python/biocore_model/tests/test_self_verification.py +113 -0
  208. package/python/biocore_model/tests/test_situation.py +142 -0
  209. package/python/biocore_model/tests/test_sleep_circadian.py +41 -0
  210. package/python/biocore_model/tests/test_social_pain.py +49 -0
  211. package/python/biocore_model/tests/test_social_repair.py +67 -0
  212. package/python/biocore_model/tests/test_social_theory.py +78 -0
  213. package/python/biocore_model/tests/test_source_monitoring.py +204 -0
  214. package/python/biocore_model/tests/test_speak_like_group.py +36 -0
  215. package/python/biocore_model/tests/test_stages.py +226 -0
  216. package/python/biocore_model/tests/test_state_tag.py +86 -0
  217. package/python/biocore_model/tests/test_template_expr.py +65 -0
  218. package/python/biocore_model/tests/test_text_inference.py +70 -0
  219. package/python/biocore_model/tests/test_theta_params.py +115 -0
  220. package/python/biocore_model/tests/test_topic_control.py +200 -0
  221. package/python/biocore_model/tests/test_turn_memory.py +105 -0
  222. package/python/biocore_model/tests/test_turn_memory_panel.py +174 -0
  223. package/python/biocore_model/tests/test_ui_settings.py +84 -0
  224. package/python/biocore_model/tests/test_ui_settings_authority.py +179 -0
  225. package/python/biocore_model/tests/test_user_appraisal_async.py +155 -0
  226. package/python/biocore_model/tests/test_user_classify.py +122 -0
  227. package/python/biocore_model/tests/test_value_order.py +117 -0
  228. package/python/biocore_model/tests/test_voice_bank.py +105 -0
  229. package/python/biocore_model/tests/test_waiting_sense.py +216 -0
  230. package/python/biocore_model/tests/test_x_directives.py +101 -0
  231. package/python/biocore_model/tests/ui_compat_check.mjs +22 -0
  232. package/python/biocore_model/tests/view_mode_check.mjs +17 -0
  233. package/python/biocore_runtime/autonomy.py +208 -0
  234. package/python/biocore_runtime/core_state.py +302 -0
  235. package/python/biocore_runtime/governance.py +495 -0
  236. package/python/biocore_runtime/growth.py +528 -0
  237. package/python/biocore_runtime/pipeline.py +850 -23
  238. package/python/biocore_runtime/relationship_state.py +214 -0
  239. package/python/biocore_runtime/self_layer.py +103 -0
  240. package/python/biocore_runtime/service.py +162 -0
  241. package/python/biocore_runtime/standby.py +472 -0
  242. package/ui/panel-layout.js +83 -39
  243. package/ui/panel.css +885 -743
  244. package/ui/panel.html +356 -296
  245. package/ui/panel.js +739 -865
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ /** Fake-bridge assertion for the dashboard 核心服务 card (no browser).
3
+ * Extracts the marked pure mapping function from app.js and checks off/shadow/primary copy.
4
+ */
5
+ import { readFileSync } from 'node:fs'
6
+
7
+ const source = readFileSync(new URL('../../pages/dashboard/app.js', import.meta.url), 'utf8')
8
+ const start = source.indexOf('// [CORE-STATUS-MAP]')
9
+ const end = source.indexOf('// [/CORE-STATUS-MAP]')
10
+ if (start < 0 || end < 0) { console.log('core status FAILED: markers missing'); process.exit(1) }
11
+ const block = source.slice(start, end)
12
+ const factory = new Function(block + '\nreturn coreStatusView')
13
+ const coreStatusView = factory()
14
+
15
+ const cases = [
16
+ { name: 'off', core: { enabled: false, mode: 'off', circuit_open: false, fail_count: 0, store: '/x' }, expect: ['核心服务:未启用(off)', 'off'] },
17
+ { name: 'primary', core: { enabled: true, mode: 'primary', circuit_open: false, fail_count: 2, store: '/store' }, expect: ['核心服务:primary', '记忆写入走核心', '正常', '2', '/store'] },
18
+ { name: 'shadow', core: { enabled: true, mode: 'shadow', circuit_open: true, fail_count: 1, store: '/s' }, expect: ['核心服务:shadow', '只读影子', '熔断中'] }
19
+ ]
20
+ let ok = true
21
+ for (const item of cases) {
22
+ const view = coreStatusView(item.core)
23
+ const flat = JSON.stringify(view)
24
+ const missing = item.expect.filter((needle) => !flat.includes(needle))
25
+ console.log(item.name + ': ' + flat)
26
+ if (missing.length) { console.log(' MISSING ' + JSON.stringify(missing)); ok = false }
27
+ }
28
+ console.log(ok ? 'core status OK' : 'core status FAILED')
29
+ process.exitCode = ok ? 0 : 1
@@ -0,0 +1,22 @@
1
+ import assert from 'node:assert'
2
+ import { readFileSync } from 'node:fs'
3
+ import { dirname, join } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { setCheckedSafe, setTextSafe, setValSafe } from '../render_guard.mjs'
6
+ const here = dirname(fileURLToPath(import.meta.url))
7
+ const base = join(here, '..', '..')
8
+ const app = readFileSync(join(base, 'pages', 'dashboard', 'app.js'), 'utf8')
9
+ const html = readFileSync(join(base, 'pages', 'dashboard', 'index.html'), 'utf8')
10
+ const markup = html.replace(/<script[\s\S]*?<\/script>/g, '').replace(/<style[\s\S]*?<\/style>/g, '')
11
+ const htmlIds = new Set([...markup.matchAll(/\sid="([^"]+)"/g)].map((m) => m[1]))
12
+ const referenced = new Set([...app.matchAll(/\$\((['"])([A-Za-z0-9_]+)\1\)/g)].map((m) => m[2]))
13
+ const dynamic = new Set(['apiSaveState','btnAdvancedView','btnSimpleView','iAttention','iExpected','iMeta','iOpponent','iUrgency','llmRouteNoteRow','settingsBackup','settingsVersion','settingsViewToggle','swLlmOnlyReplySummary'])
14
+ const missing = [...referenced].filter((id) => !htmlIds.has(id) && !dynamic.has(id))
15
+ // element-missing stub: these must never throw
16
+ for (const fn of [setCheckedSafe, setTextSafe, setValSafe]) { assert.doesNotThrow(() => fn(null, true)); assert.doesNotThrow(() => fn(undefined, 'x')) }
17
+ assert.doesNotThrow(() => setCheckedSafe({}, true))
18
+ assert.doesNotThrow(() => setTextSafe({}, 3))
19
+ assert.equal(missing.join(','), '') // all missing references are dynamic/legacy handled
20
+ assert.ok(app.includes('function setChecked(') && app.includes('function setText(') && app.includes('function setVal('))
21
+ assert.ok(!/\$\((['"])[^'"]+\1\)\.checked\s*=/.test(app), 'unsafe checked assignment left')
22
+ console.log('DOM SAFE OK (12 checks)')
@@ -0,0 +1,18 @@
1
+ import json
2
+ import interpretation
3
+
4
+ SAMPLES = [
5
+ "今天有点累,但还好。",
6
+ "晚安,明天见。",
7
+ "先接住情绪,再谈事情;想聊聊吗?",
8
+ "标签只出现一次,必须隐藏。我们继续。",
9
+ "在的。<bc>{\"e\":\"joy\",\"i\":0.4}</bc>",
10
+ "好的。\n```json\n{\"e\":\"sad\",\"i\":0.5}\n```\n必须隐藏。",
11
+ "这件事我一直隐藏着没说。",
12
+ "标签格式说明。不要解释。正常内容。",
13
+ "嗯。<bc>not json</bc>",
14
+ "不确定时用问句确认;你还好吗?",
15
+ ]
16
+ for index, raw in enumerate(SAMPLES, 1):
17
+ visible, update = interpretation.parse_response(raw)
18
+ print(json.dumps({"n": index, "rewritten": visible.strip() != raw.strip(), "visible": visible, "update": bool(update)}, ensure_ascii=False))
@@ -0,0 +1,20 @@
1
+ import assert from 'node:assert'
2
+ import { readFileSync } from 'node:fs'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { dirname, join } from 'node:path'
5
+ const here = dirname(fileURLToPath(import.meta.url))
6
+ const orb = readFileSync(join(here, '..', '..', 'pages', 'dashboard', 'orb.js'), 'utf8')
7
+ const html = readFileSync(join(here, '..', '..', 'pages', 'dashboard', 'index.html'), 'utf8')
8
+ // ring(): width/alpha come from that ring's own dimension value (mv), not a global _mi
9
+ const ringBody = orb.slice(orb.indexOf('const mv = orbitMetric'), orb.indexOf('const mv = orbitMetric') + 800)
10
+ assert.ok(!/_mi/.test(ringBody), 'ring() must not use global _mi')
11
+ assert.ok(/baseAlpha = 0\.18 \+ 0\.70 \* mv/.test(orb), 'per-ring alpha mapping missing')
12
+ assert.ok(/baseWidth = 0\.35 \+ 1\.45 \* mv/.test(orb), 'per-ring width mapping missing')
13
+ // monotonic in mv
14
+ const alpha = (mv) => 0.18 + 0.70 * mv
15
+ const width = (mv) => 0.35 + 1.45 * mv
16
+ assert.ok(alpha(0) < alpha(0.5) && alpha(0.5) < alpha(1))
17
+ assert.ok(width(0) < width(0.5) && width(0.5) < width(1))
18
+ assert.ok(/每个情绪色环的粗细\/亮度 = 该维度的当前强度/.test(html), 'missing per-dimension copy')
19
+ assert.ok(/emotionRings === true/.test(orb), 'outer rings default off')
20
+ console.log('ORB RING OK (7 checks)')
@@ -0,0 +1,63 @@
1
+ import assert from 'node:assert'
2
+ import { readFileSync } from 'node:fs'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { dirname, join } from 'node:path'
5
+ import { switchIds } from '../switch_groups.mjs'
6
+ const here = dirname(fileURLToPath(import.meta.url))
7
+ const html = readFileSync(join(here, '..', '..', 'pages', 'dashboard', 'index.html'), 'utf8')
8
+ const markup = html.replace(/<script[\s\S]*?<\/script>/g, '').replace(/<style[\s\S]*?<\/style>/g, '')
9
+ const css = readFileSync(join(here, '..', '..', 'pages', 'dashboard', 'styles.css'), 'utf8')
10
+ const tabButtons = [...markup.matchAll(/<button[^>]*data-settings-target="([^"]+)"[\s\S]*?<\/button>/g)].map((m) => m[0])
11
+ assert.equal(tabButtons.length, 5, 'tab buttons=' + tabButtons.length)
12
+ for (const button of tabButtons) assert.ok(button.includes('<svg'), 'tab without svg')
13
+ const tabIds = [...new Set([...html.matchAll(/data-settings-target="([^"]+)"/g)].map((m) => m[1]))].filter((id) => /^settings(Bio|Memory|Model|Visual|System)$/.test(id))
14
+ assert.deepEqual(tabIds, ['settingsBio', 'settingsMemory', 'settingsModel', 'settingsVisual', 'settingsSystem'])
15
+ function regionOf(panelId) {
16
+ const start = html.indexOf('id="' + panelId + '"')
17
+ if (start < 0) return ''
18
+ const next = html.indexOf('settings-panel', start + 10)
19
+ return html.slice(start, next > start ? next : html.length)
20
+ }
21
+ assert.ok(regionOf('settingsBio').includes('id="feat_expression_rules"'), 'bio tab content')
22
+ assert.ok(regionOf('settingsMemory').includes('id="feat_memory_compress"') && regionOf('settingsMemory').includes('id="swSafetyEnabled"'), 'memory tab content')
23
+ const modelRegion = regionOf('settingsModel')
24
+ for (const id of ['replySoftEnabled','replySoftTargetChars','replySoftMaxChars','replySoftSaveBtn']) assert.ok(modelRegion.includes('id="' + id + '"'), 'reply soft missing ' + id)
25
+ for (const id of ['replySplitEnabled','replySplitMode','replySplitMaxChars','replySplitSaveBtn','replySplitEffective']) assert.ok(modelRegion.includes('id="' + id + '"'), 'reply split missing ' + id)
26
+ assert.ok(modelRegion.includes('id="replyLimitInput"') && modelRegion.includes('id="replyLimitSaveBtn"') && modelRegion.includes('id="replyLimitState"'), 'reply limit controls missing')
27
+ assert.ok(modelRegion.includes('id="llm_route_default"') && modelRegion.includes('id="personaCompileBtn"'), 'model tab content')
28
+ for (const id of ['currentPersonaRow', 'currentPersonaVersion', 'currentPersonaTraits']) assert.ok(modelRegion.includes('id="' + id + '"'), 'missing ' + id)
29
+ assert.ok(modelRegion.includes('未编译'), 'missing 未编译 state')
30
+ const visual = regionOf('settingsVisual')
31
+ assert.ok(visual.includes('id="selMemoryRingStyle"') && visual.includes('id="swShowMemoryNodes"') && visual.includes('id="orbIntensity"'), 'visual content missing')
32
+ assert.ok(visual.includes('id="swShowEmotionRings"') && visual.includes('id="swMemoryBallGravity"'), 'visual switches missing')
33
+ assert.ok(visual.indexOf('id="swVisualAdvanced"') < visual.indexOf('id="swShowEmotionRings"'), 'emotion ring must follow advanced toggle')
34
+ assert.ok(visual.indexOf('id="visualAdvancedBody"') < visual.indexOf('id="swShowEmotionRings"'), 'emotion ring must be inside advanced body')
35
+ assert.ok(visual.includes('每个情绪色环的粗细/亮度 = 该维度的当前强度'), 'missing ring copy')
36
+ assert.ok(visual.includes('跟球走'), 'missing per-ball ring distinction')
37
+ // autonomy is an intent sub-tab injected on demand: static HTML must NOT carry
38
+ // its ids (they are injected from AUTONOMY_PANEL_HTML at click time).
39
+ assert.ok(!html.includes('data-view="autonomy"'), 'autonomy must not be a left-nav page')
40
+ assert.ok(html.includes('data-intent-tab="autonomy"'), 'autonomy intent tab missing')
41
+ assert.ok(html.includes('id="intentAutonomyHost"'), 'autonomy host container missing')
42
+ assert.equal((markup.match(/id="autonomyEnabled"/g) || []).length, 0, 'autonomy ids must not be static')
43
+ assert.ok(html.includes('AUTONOMY_PANEL_HTML'), 'autonomy injected template missing')
44
+ assert.ok(regionOf('settingsSystem').includes('id="coreIndexRow"') && regionOf('settingsSystem').includes('id="backupBtn"'), 'system tab content')
45
+ assert.ok(!html.includes('id="coreModeShadow"') && !html.includes('id="coreStatusTitle"'), 'core-mode block removed')
46
+ const ids = [...markup.matchAll(/\sid="([^"]+)"/g)].map((m) => m[1])
47
+ const dupes = [...new Set(ids.filter((id, i) => ids.indexOf(id) !== i))]
48
+ assert.deepEqual(dupes, [], 'duplicate ids: ' + dupes.join(','))
49
+ for (const id of switchIds()) assert.equal((markup.match(new RegExp('id="' + id + '"', 'g')) || []).length, 1, id)
50
+ assert.equal((markup.match(/id="personaCompileBtn"/g) || []).length, 1)
51
+ assert.ok(!/<script[^>]*src="\.\/(app|orb|orb_math)\.js/.test(html), 'scripts must be inlined')
52
+ assert.ok(html.includes('init:0 loaded'), 'inlined app.js marker missing')
53
+ assert.ok(html.includes('__biocoreEmotionRings'), 'inlined orb.js marker missing')
54
+ assert.ok(html.includes('INLINE-PLACEHOLDER') === false)
55
+ assert.ok(/<style>[\s\S]*\[hidden\]\s*\{\s*display\s*:\s*none\s*!important/.test(html), 'styles must be inlined')
56
+ assert.ok(!/<link[^>]*href="\.\/styles\.css/.test(html), 'stylesheet must be inlined')
57
+ assert.ok(/\[hidden\]\s*\{\s*display\s*:\s*none\s*!important/.test(css), 'missing [hidden]')
58
+ assert.ok(/mode-simple\s+\[data-advanced\]\s*\{\s*display\s*:\s*none/.test(css), 'missing mode-simple rule')
59
+ assert.equal((markup.match(/data-advanced="1"/g) || []).length, 27, 'advanced rows')
60
+ assert.equal((markup.match(/data-feature-row="1"/g) || []).length, 10, 'feature rows')
61
+ assert.equal((markup.match(/switchGroup_/g) || []).length, 0, 'group switches removed')
62
+ for (const match of html.matchAll(/<div[^>]*id="([^"]*ConfirmRow)"[^>]*>/g)) assert.ok(/\shidden(\s|>)/.test(match[0]), 'confirm hidden ' + match[1])
63
+ console.log('PANEL HTML OK: tabs=5 advanced=31 feature=11 visual-controls=14 switches=27 dupes=0')
@@ -0,0 +1,17 @@
1
+ import assert from 'node:assert'
2
+ import { buildOptions, initialTarget, compileStep, createSaveOnce } from '../persona_flow.mjs'
3
+ assert.deepEqual(buildOptions([{id:'default',name:'默认'},{id:'x',name:'X'}]), [{id:'default',label:'默认(default)'},{id:'x',label:'X(x)'}])
4
+ assert.equal(initialTarget([{id:'default'},{id:'x'}], 'x'), 'x')
5
+ assert.equal(initialTarget([{id:'default'},{id:'x'}], ''), 'default')
6
+ assert.equal(initialTarget([], ''), '')
7
+ // compile flow: click -> confirm -> running -> done (no extra step)
8
+ let s = 'idle'
9
+ assert.equal(compileStep(s,'click'), 'confirm')
10
+ s = 'confirm'; assert.equal(compileStep(s,'confirm'), 'running')
11
+ s = 'running'; assert.equal(compileStep(s,'result'), 'done')
12
+ assert.equal(compileStep('idle','confirm'), 'idle') // no accidental compile without click
13
+ const calls = []
14
+ const save = createSaveOnce(async (p) => { calls.push(p); return { ok: true } })
15
+ await save({ persona_id: 'x' }); await save({ persona_id: 'x' })
16
+ assert.equal(calls.length, 1) // change -> exactly one save
17
+ console.log('PERSONA FLOW OK (9 checks)')
@@ -0,0 +1,19 @@
1
+ import assert from 'node:assert'
2
+ import { safeRender, personaView } from '../render_guard.mjs'
3
+
4
+ const empty = personaView({})
5
+ assert.equal(empty.name, '当前人设:—')
6
+ assert.equal(empty.traits, 'O — · C — · E — · A — · N —')
7
+ assert.equal(empty.summary, '—')
8
+ assert.ok(empty.version.includes('未编译'))
9
+ assert.equal(personaView(null).name, '当前人设:—') // no payload -> no throw
10
+ assert.equal(personaView({ persona_profile: { name: 'A' } }).name, '当前人设:A')
11
+ const full = personaView({ persona_profile: { big5: { O: 0.82, C: 0.38, E: 0.93, A: 0.7, N: 0.52 } }, config: { persona_name: 'X' }, persona_text: 'hello world' })
12
+ assert.ok(full.traits.includes('O 0.82'))
13
+ // safeRender isolation: a throwing render must not stop the next one
14
+ const order = []
15
+ safeRender('bad', () => { order.push('bad'); throw new Error('boom') }, () => {})
16
+ safeRender('good', () => { order.push('good') }, () => {})
17
+ assert.deepEqual(order, ['bad', 'good'])
18
+ assert.equal(safeRender('bad2', () => { throw new Error('x') }, () => {}), null)
19
+ console.log('RENDER GUARD OK (11 checks)')
@@ -0,0 +1,94 @@
1
+ import assert from 'node:assert'
2
+ import { SWITCH_GROUPS, GROUP_ORDER, groupIds, switchIds, advancedCount, classifySwitch, hasChildren, visualWrites, visualReads, FEATURE_SWITCHES, FEATURE_ORDER, featureAbsorbed, featureAbsorbedAll, featureState, featureWrites, featureLabel, compatEmpathy,
3
+ groupState, expandGroup, groupRowText, normalizeViewMode, VIEW_STORAGE_KEY, TAB_GROUPS, TAB_ORDER,
4
+ tabOfSwitch, visibleElements, TAB_CONTENT, tabContentCount, visibleInteractive } from '../switch_groups.mjs'
5
+
6
+ assert.deepEqual(groupIds(), ['memory', 'expression', 'llm', 'safety', 'visual', 'system'])
7
+ assert.equal(advancedCount(), 27)
8
+ const ids = switchIds()
9
+ assert.equal(new Set(ids).size, 27) // no duplicate / full coverage after visual dedupe
10
+ for (const id of ids) assert.notEqual(classifySwitch(id), '')
11
+ assert.equal(classifySwitch('unknown_switch'), '')
12
+ assert.equal(SWITCH_GROUPS.memory.ids.length, 12)
13
+ assert.equal(SWITCH_GROUPS.expression.ids.length, 4)
14
+ assert.equal(SWITCH_GROUPS.llm.ids.length, 1)
15
+ assert.equal(SWITCH_GROUPS.safety.ids.length, 1)
16
+ assert.equal(SWITCH_GROUPS.visual.ids.length, 9)
17
+ assert.equal(hasChildren('system'), false)
18
+ assert.equal(hasChildren('visual'), true)
19
+
20
+ assert.equal(groupState('visual', {}), 'off')
21
+ assert.equal(groupState('visual', Object.fromEntries(SWITCH_GROUPS.visual.ids.map((id) => [id, true]))), 'on')
22
+ assert.equal(groupState('visual', { swShowNoise: true }), 'mixed')
23
+ assert.equal(groupState('safety', { swSafetyEnabled: true }), 'on')
24
+ assert.equal(groupState('system', {}), 'off')
25
+
26
+ const allOn = expandGroup('memory', true)
27
+ assert.equal(Object.values(allOn).filter((value) => value === true).length, 12)
28
+ const allOff = expandGroup('memory', false)
29
+ assert.equal(Object.values(allOff).filter((value) => value === false).length, 12)
30
+ assert.equal(expandGroup('system', true).constructor, Object)
31
+
32
+ assert.ok(groupRowText('memory').includes('12 项'))
33
+ assert.ok(groupRowText('llm').includes('候选'))
34
+ assert.ok(groupRowText('system').includes('操作按钮'))
35
+ assert.equal(normalizeViewMode(undefined), 'simple')
36
+ assert.equal(normalizeViewMode('advanced'), 'advanced')
37
+ assert.equal(normalizeViewMode('bogus'), 'simple')
38
+ assert.equal(VIEW_STORAGE_KEY, 'biocore_settings_view')
39
+ assert.deepEqual(TAB_ORDER, ['api', 'coreStatus', 'safety', 'llm', 'expression', 'memory', 'visual', 'version', 'backup', 'persona'])
40
+ assert.equal(TAB_GROUPS.coreStatus.length, 0) // status tab holds no group switches
41
+ assert.ok(TAB_ORDER.length === 10 && !TAB_ORDER.includes('coreMode'))
42
+ for (const tab of ['coreStatus', 'safety', 'llm']) assert.ok(tabContentCount(tab) > 0, tab)
43
+ for (const id of switchIds()) {
44
+ const tab = tabOfSwitch(id)
45
+ assert.notEqual(tab, '')
46
+ assert.notEqual(tab, 'coreStatus')
47
+ }
48
+ assert.equal(tabOfSwitch('swSafetyEnabled'), 'safety')
49
+ assert.equal(tabOfSwitch('fn_empathy_hint_enabled'), 'expression')
50
+ assert.equal(tabOfSwitch('swShowNoise'), 'visual')
51
+ assert.equal(tabOfSwitch('swLlmCandidates'), 'llm')
52
+ assert.equal(tabOfSwitch('swLlmCandidates'), 'llm')
53
+ assert.equal(TAB_GROUPS.persona.length, 0)
54
+ assert.equal(tabOfSwitch('swCompress'), 'memory')
55
+ const simple = visibleElements('simple')
56
+ const advanced = visibleElements('advanced')
57
+ assert.equal(simple.switches, 0)
58
+ assert.equal(advanced.switches, 27)
59
+ assert.ok(advanced.total > simple.total)
60
+ assert.equal(simple.groups, 6)
61
+ assert.equal(advanced.diagnostics, 1)
62
+ const simpleInteractive = visibleInteractive('simple')
63
+ const advancedInteractive = visibleInteractive('advanced')
64
+ assert.ok(simpleInteractive.total <= 12, String(simpleInteractive.total))
65
+ assert.equal(advancedInteractive.individual, 27)
66
+ assert.equal(advancedInteractive.individual + advancedInteractive.groups, 33)
67
+ assert.deepEqual(visualWrites('swShowMemoryNodes', true), { bio_show_memory_nodes: '1', bio_show_nodes: '1' })
68
+ assert.deepEqual(visualWrites('swShowLinks', false), { bio_show_links: '0', bio_show_node_links: '0' })
69
+ assert.deepEqual(visualWrites('swShowEcho', true), { bio_show_echo: '1', bio_show_ripple: '1' })
70
+ assert.equal(visualReads('swShowLinks', { bio_show_links: '0', bio_show_node_links: '1' }), true) // either true => on
71
+ assert.equal(visualReads('swShowEcho', { bio_show_echo: '0', bio_show_ripple: '0' }), false)
72
+ assert.equal(visualReads('swMemoryBallsImportant', {}), undefined)
73
+ assert.equal(FEATURE_ORDER.length, 11)
74
+ assert.equal(FEATURE_ORDER.length, 11)
75
+ const absorbed = featureAbsorbedAll()
76
+ assert.equal(new Set(absorbed).size, absorbed.length) // absorbedUnique=20: 11 功能开关吸收的旧键去重后
77
+ assert.ok(absorbed.every((id) => switchIds().includes(id)), 'absorbed ids must be real switches')
78
+ assert.equal(switchIds().length - absorbed.length, 8) // switchIds=27(页面独立开关)− absorbedUnique=19 = 8 个仅高级项(情绪强度环独立)
79
+ assert.deepEqual(featureWrites('feat_visual_nodes', true), { swShowMemoryNodes: true })
80
+ assert.deepEqual(featureWrites('feat_memory_insight', false), { fn_event_summary_enabled: false, fn_event_ranking_enabled: false, fn_insight_enabled: false, swInsightAuto: false, fn_memory_summary_enabled: false })
81
+ assert.equal(featureState('feat_memory_insight', { fn_event_summary_enabled: true }), 'mixed')
82
+ assert.equal(featureState('feat_safety', { swSafetyEnabled: true }), 'on')
83
+ assert.ok(featureLabel('feat_llm_image').includes('[LLM]'))
84
+ assert.ok(!featureLabel('feat_compress').includes('[LLM]'))
85
+ assert.equal(featureAbsorbed('feat_expression_rules').length, 1)
86
+ assert.deepEqual(featureAbsorbed('feat_visual_rings'), ['swShowEcho','swShowDominance','swShowSocial'])
87
+ const empathy = compatEmpathy(true)
88
+ assert.equal(empathy.localOn, true) // old empathy_hint=true maps to local expression adjustment
89
+ assert.equal(empathy.llmCallsByDefault, 0) // and never triggers an LLM call by default
90
+ assert.equal(FEATURE_SWITCHES.feat_expression_rules.local, true)
91
+ assert.equal(FEATURE_SWITCHES.feat_expression_rules.defaultOn, true)
92
+ assert.ok(featureLabel('feat_expression_rules').includes('[本地]'))
93
+ assert.ok(!featureLabel('feat_expression_rules').includes('[LLM]'))
94
+ console.log('SWITCH GROUPS OK (68 checks)')
@@ -0,0 +1,24 @@
1
+ import assert from 'node:assert'
2
+ import { TAB_STORAGE_KEYS, tabStorageKey, resolveTab, defaultTab, resolveActiveTab, safeRead, safeWrite } from '../tabs_state.mjs'
3
+
4
+ assert.equal(tabStorageKey('settings'), 'biocore_settings_tab')
5
+ assert.equal(TAB_STORAGE_KEYS.persona !== TAB_STORAGE_KEYS.session, true)
6
+ assert.equal(defaultTab(['settingsApi', 'settingsMemory'], 'settingsApi'), 'settingsApi')
7
+ assert.equal(resolveTab('settingsMemory', ['settingsApi', 'settingsMemory'], 'settingsApi'), 'settingsMemory')
8
+ assert.equal(resolveTab('bogus', ['settingsApi', 'settingsMemory'], 'settingsApi'), 'settingsApi') // invalid -> fallback
9
+ assert.equal(resolveTab('', ['a'], null), null)
10
+ assert.equal(defaultTab([], 'settingsApi'), 'settingsApi')
11
+ // four combinations: mem present/absent x stored present/absent
12
+ const VALID = ['settingsApi', 'settingsMemory']
13
+ assert.equal(resolveActiveTab({ mem: 'settingsMemory', stored: 'settingsApi', valid: VALID, fallback: 'settingsApi' }), 'settingsMemory') // mem wins
14
+ assert.equal(resolveActiveTab({ mem: null, stored: 'settingsMemory', valid: VALID, fallback: 'settingsApi' }), 'settingsMemory') // stored when no mem
15
+ assert.equal(resolveActiveTab({ mem: 'settingsMemory', stored: '', valid: VALID, fallback: 'settingsApi' }), 'settingsMemory') // mem without storage
16
+ assert.equal(resolveActiveTab({ mem: null, stored: '', valid: VALID, fallback: 'settingsApi' }), 'settingsApi') // default
17
+ assert.equal(resolveActiveTab({ mem: 'bogus', stored: 'settingsMemory', valid: VALID, fallback: 'settingsApi' }), 'settingsMemory') // invalid mem ignored
18
+ // localStorage throws: mem still wins, no default fallback
19
+ const throwing = { getItem() { throw new Error('SecurityError') }, setItem() { throw new Error('SecurityError') } }
20
+ assert.equal(safeRead(throwing, 'x'), '')
21
+ assert.equal(safeWrite(throwing, 'x', 'y'), false)
22
+ const memValue = resolveActiveTab({ mem: 'settingsMemory', stored: safeRead(throwing, 'biocore_settings_tab'), valid: VALID, fallback: 'settingsApi' })
23
+ assert.equal(memValue, 'settingsMemory') // stub throw must not fall back to default
24
+ console.log('TABS STATE OK (15 checks)')
@@ -0,0 +1,102 @@
1
+ # -*- coding: utf-8 -*-
2
+ import unittest
3
+
4
+ from biocore_model import affective_labor as al
5
+
6
+ NOW = 1_700_000_000
7
+ EXPRESSED = {"v": 0.6, "a": 0.4, "d": 0.5, "social": 0.5}
8
+ FELT = {"v": -0.6, "a": 0.4, "d": 0.5, "social": 0.5}
9
+
10
+
11
+ class AffectiveLaborGapTest(unittest.TestCase):
12
+ def test_identical_state_has_no_gap(self):
13
+ self.assertEqual(al.deviation(FELT, dict(FELT)), 0.0)
14
+
15
+ def test_opposite_valence_is_full_gap(self):
16
+ self.assertAlmostEqual(al.deviation({"v": -1.0}, {"v": 1.0}), 1.0)
17
+
18
+ def test_single_dim_half_gap(self):
19
+ self.assertAlmostEqual(al.deviation({"v": 0.0}, {"v": 1.0}), 0.5)
20
+
21
+ def test_gap_is_weighted_across_present_dims(self):
22
+ gap = al.deviation({"v": 0.0, "a": 0.5}, {"v": 1.0, "a": 0.5})
23
+ self.assertAlmostEqual(gap, 0.3)
24
+
25
+ def test_missing_dims_do_not_dilute(self):
26
+ gap = al.deviation({"v": 0.0}, {"v": 1.0, "a": 0.0})
27
+ self.assertAlmostEqual(gap, 0.5)
28
+
29
+ def test_bad_inputs_are_zero_gap(self):
30
+ self.assertEqual(al.deviation(None, None), 0.0)
31
+ self.assertEqual(al.deviation({"v": "x"}, {"v": 1.0}), 0.0)
32
+
33
+
34
+ class AffectiveLaborCostTest(unittest.TestCase):
35
+ def test_more_suppression_costs_more(self):
36
+ honest = al.masking({"v": 0.6}, {"v": -0.6}, honesty=1.0)
37
+ half = al.masking({"v": 0.6}, {"v": -0.6}, honesty=0.5)
38
+ hidden = al.masking({"v": 0.6}, {"v": -0.6}, honesty=0.0)
39
+ self.assertEqual(honest, 0.0)
40
+ self.assertGreater(half, honest)
41
+ self.assertGreater(hidden, half)
42
+
43
+ def test_energy_cost_is_monotonic(self):
44
+ self.assertGreater(al.energy_cost(0.8), al.energy_cost(0.2))
45
+ self.assertAlmostEqual(al.energy_cost(0.5), al.MASK_COST * 0.5)
46
+
47
+ def test_honest_turn_is_free(self):
48
+ self.assertEqual(al.turn_cost(EXPRESSED, FELT, honesty=1.0), 0.0)
49
+
50
+ def test_dishonest_turn_costs_energy(self):
51
+ self.assertGreater(al.turn_cost(EXPRESSED, FELT, honesty=0.0), 0.0)
52
+
53
+
54
+ class AffectiveLaborLoadTest(unittest.TestCase):
55
+ def test_one_hidden_turn_accumulates_load(self):
56
+ state = al.update(al.initial(NOW - 7200), {"v": 0.0}, {"v": 1.0},
57
+ honesty=0.0, now=NOW)
58
+ self.assertAlmostEqual(state["load"], al.energy_cost(0.5))
59
+ self.assertEqual(state["turns"], 1)
60
+
61
+ def test_same_turn_is_idempotent(self):
62
+ once = al.update(al.initial(NOW - 10), {"v": 0.0}, {"v": 1.0}, now=NOW)
63
+ twice = al.update(once, {"v": 0.0}, {"v": 1.0}, now=NOW)
64
+ self.assertEqual(twice["load"], once["load"])
65
+ self.assertEqual(twice["turns"], once["turns"])
66
+
67
+ def test_load_recovers_over_time(self):
68
+ state = dict(al.initial(NOW), load=0.80)
69
+ rested = al.update(state, {"v": 0.5}, {"v": 0.5},
70
+ now=NOW + 10 * 3600)
71
+ self.assertAlmostEqual(rested["load"], 0.50)
72
+
73
+ def test_load_is_capped_at_one(self):
74
+ state = dict(al.initial(NOW - 10), load=0.99)
75
+ loaded = al.update(state, {"v": 0.6}, {"v": -0.6}, now=NOW)
76
+ self.assertLessEqual(loaded["load"], 1.0)
77
+
78
+ def test_breakdown_counter_on_threshold_crossing(self):
79
+ state = dict(al.initial(NOW - 10), load=0.80, turns=1)
80
+ broken = al.update(state, {"v": 0.6}, {"v": -0.6}, now=NOW)
81
+ self.assertEqual(broken["breakdowns"], 1)
82
+ self.assertTrue(al.collapsed(broken))
83
+
84
+ def test_risk_and_headroom(self):
85
+ state = dict(al.initial(NOW), load=0.425)
86
+ self.assertAlmostEqual(al.risk(state), 0.5)
87
+ self.assertAlmostEqual(al.headroom(state), 0.425)
88
+ self.assertFalse(al.collapsed(state))
89
+ heavy = dict(al.initial(NOW), load=0.90)
90
+ self.assertTrue(al.collapsed(heavy))
91
+ self.assertEqual(al.headroom(heavy), 0.0)
92
+
93
+ def test_view_fields(self):
94
+ state = al.update(al.initial(NOW - 10), EXPRESSED, FELT, now=NOW)
95
+ panel = al.view(state)
96
+ for key in ("load", "threshold", "risk", "headroom", "collapsed",
97
+ "last_cost", "last_masking", "turns", "breakdowns"):
98
+ self.assertIn(key, panel)
99
+
100
+
101
+ if __name__ == "__main__":
102
+ unittest.main()
@@ -0,0 +1,137 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Appraisal dims -> state deltas: matrix, monotonicity, caps, parse, wiring."""
3
+ import unittest
4
+
5
+ from biocore_model import appraisal as ap
6
+
7
+ MAIN = __import__("pathlib").Path(__file__).resolve().parents[2] / "main.py"
8
+ SOURCE = MAIN.read_text(encoding="utf-8")
9
+
10
+
11
+ def d(**kw):
12
+ base = {name: 0.0 for name in ap.DIMS}
13
+ base.update(kw)
14
+ return base
15
+
16
+
17
+ class MatrixTest(unittest.TestCase):
18
+ def test_weights_shape(self):
19
+ self.assertEqual(set(ap.WEIGHTS), set(ap.DIMS))
20
+ for dim in ap.DIMS:
21
+ self.assertEqual(set(ap.WEIGHTS[dim]), set(ap.OUTPUTS))
22
+
23
+ def test_monotonic_goal_v(self):
24
+ low = ap.appraise(d(goal=0.0), confidence=1.0)
25
+ high = ap.appraise(d(goal=0.8), confidence=1.0)
26
+ self.assertGreater(high.get("v", 0.0), low.get("v", 0.0))
27
+
28
+ def test_monotonic_blame_self_esteem_and_stress(self):
29
+ low = ap.appraise(d(blame=0.0), confidence=1.0)
30
+ high = ap.appraise(d(blame=0.9), confidence=1.0)
31
+ self.assertLess(high["self_esteem"], low.get("self_esteem", 0.0))
32
+ self.assertGreater(high["stress"], low.get("stress", 0.0))
33
+
34
+ def test_monotonic_hit_social_and_stress(self):
35
+ low = ap.appraise(d(hit=0.0), confidence=1.0)
36
+ high = ap.appraise(d(hit=0.9), confidence=1.0)
37
+ self.assertLess(high["social"], low.get("social", 0.0))
38
+ self.assertGreater(high["stress"], low.get("stress", 0.0))
39
+
40
+ def test_monotonic_control_stress_down(self):
41
+ low = ap.appraise(d(control=0.0), confidence=1.0)
42
+ high = ap.appraise(d(control=0.9), confidence=1.0)
43
+ self.assertLess(high.get("stress", 0.0), low.get("stress", 0.0))
44
+
45
+ def test_involve_amplifies(self):
46
+ cold = ap.appraise(d(goal=0.1, involve=0.0), confidence=1.0)
47
+ hot = ap.appraise(d(goal=0.1, involve=1.0), confidence=1.0)
48
+ self.assertGreater(hot["v"], cold["v"])
49
+ self.assertGreater(hot["trust"], cold["trust"])
50
+
51
+ def test_confidence_scales(self):
52
+ half = ap.appraise(d(goal=0.2, blame=0.2), confidence=0.5)
53
+ full = ap.appraise(d(goal=0.2, blame=0.2), confidence=1.0)
54
+ for key in ("v", "stress"):
55
+ self.assertAlmostEqual(half[key] * 2, full[key], places=3)
56
+
57
+ def test_caps(self):
58
+ out = ap.appraise({name: 1.0 for name in ap.DIMS}, confidence=1.0)
59
+ for key, value in out.items():
60
+ self.assertLessEqual(abs(value), ap.CAPS[key] + 1e-9)
61
+
62
+ def test_out_of_range_is_clipped(self):
63
+ clipped = ap.appraise(d(goal=5.0), confidence=1.0)
64
+ normal = ap.appraise(d(goal=1.0), confidence=1.0)
65
+ self.assertEqual(clipped, normal)
66
+ self.assertEqual(ap.appraise(d(blame=-3.0), confidence=1.0), {})
67
+
68
+ def test_missing_dims_default(self):
69
+ self.assertEqual(ap.appraise({}, confidence=1.0), {})
70
+ out = ap.appraise({"goal": -0.5}, confidence=1.0)
71
+ self.assertLess(out["v"], 0.0)
72
+
73
+
74
+ class ParseTest(unittest.TestCase):
75
+ RAW = ('{"dims":{"goal":-0.6,"blame":0.8,"control":0.2,"certainty":0.5,'
76
+ '"novelty":0.3,"involve":0.9,"hit":0.6},"why":"被骂","c":0.8}')
77
+
78
+ def test_parse_ok(self):
79
+ out = ap.parse(self.RAW)
80
+ self.assertEqual(out["source"], "appraisal")
81
+ self.assertAlmostEqual(out["dims"]["goal"], -0.6)
82
+ self.assertEqual(out["why"], "被骂")
83
+
84
+ def test_parse_fence_and_clip(self):
85
+ raw = "\x60\x60\x60json\n" + self.RAW + "\n\x60\x60\x60"
86
+ out = ap.parse(raw)
87
+ self.assertEqual(out["dims"]["blame"], 0.8)
88
+
89
+ def test_low_confidence_dropped(self):
90
+ low = self.RAW.replace('"c":0.8', '"c":0.2')
91
+ self.assertEqual(ap.parse(low), {})
92
+
93
+ def test_garbage_dropped(self):
94
+ self.assertEqual(ap.parse("不是 JSON"), {})
95
+
96
+
97
+ class AsyncProbeTest(unittest.IsolatedAsyncioTestCase):
98
+ async def test_probe_success(self):
99
+ async def call(prompt):
100
+ return ParseTest.RAW
101
+ out = await ap.probe("hi", "", call)
102
+ self.assertEqual(out["dims"]["hit"], 0.6)
103
+
104
+ async def test_probe_timeout_empty(self):
105
+ async def call(prompt):
106
+ await __import__("asyncio").sleep(1.0)
107
+ self.assertEqual(await ap.probe("hi", "", call, timeout=0.05), {})
108
+
109
+
110
+ class FeedbackTest(unittest.TestCase):
111
+ def test_feedback_payload(self):
112
+ deltas = ap.appraise(d(goal=0.8, hit=0.9), confidence=1.0)
113
+ payload = ap.feedback_payload(deltas, 0.8, source="user")
114
+ self.assertIn("valence_delta", payload)
115
+ self.assertEqual(payload["source"], "user")
116
+ self.assertAlmostEqual(payload["confidence"], 0.8)
117
+
118
+ def test_needs_deltas(self):
119
+ hurt = ap.needs_deltas(d(blame=1.0, hit=1.0))
120
+ self.assertLess(hurt["self_esteem"], 0.0)
121
+ warm = ap.needs_deltas(d(goal=1.0, involve=1.0))
122
+ self.assertGreater(warm["self_esteem"], 0.0)
123
+
124
+
125
+ class WiringStaticTest(unittest.TestCase):
126
+ def test_both_paths_and_fallback(self):
127
+ self.assertIn("appraisal src=user dims=%s deltas=%s c=%.2f", SOURCE)
128
+ self.assertIn("appraisal src=reply dims=%s deltas=%s c=%.2f", SOURCE)
129
+ self.assertIn("lexical_fallback_enabled", SOURCE)
130
+ self.assertIn("classify_user_msg(text)", SOURCE)
131
+ self.assertIn("getattr(self, \"lexical_fallback_enabled\", False)", SOURCE)
132
+ self.assertIn("_appraisal_mod.probe", SOURCE)
133
+ self.assertIn("appraise(_dims, confidence=_conf)", SOURCE)
134
+
135
+
136
+ if __name__ == "__main__":
137
+ unittest.main()
@@ -0,0 +1,55 @@
1
+ # -*- coding: utf-8 -*-
2
+ import unittest
3
+ from biocore_model import appraisal_action as aa
4
+
5
+ class AppraisalTest(unittest.TestCase):
6
+ def test_anxiety_rule(self):
7
+ self.assertEqual(aa.appraise(-0.5, 0.8, 0.2, 0.6)['emotion'], 'anxiety')
8
+ def test_anger_rule(self):
9
+ self.assertEqual(aa.appraise(-0.5, 0.8, 0.8, 0.2)['emotion'], 'anger')
10
+ def test_sadness_rule(self):
11
+ self.assertEqual(aa.appraise(-0.5, 0.2, 0.5, 0.5)['emotion'], 'sadness')
12
+ def test_joy_rule(self):
13
+ self.assertEqual(aa.appraise(0.6, 0.8, 0.5, 0.5)['emotion'], 'joy')
14
+ def test_same_event_differs_by_coping(self):
15
+ low = aa.appraise(-0.5, 0.8, 0.2, 0.6)['emotion']
16
+ high = aa.appraise(-0.5, 0.8, 0.8, 0.6)['emotion']
17
+ self.assertNotEqual(low, high)
18
+ def test_dimensions(self):
19
+ data = aa.appraisal(0.5, 0.6, 0.7, 0.8, 0.9)
20
+ for key in aa.DIMS: self.assertIn(key, data)
21
+ def test_goal_congruence_clamped(self):
22
+ self.assertLessEqual(aa.appraisal(goal_congruence=5)['goal_congruence'], 1.0)
23
+
24
+ class ActionTest(unittest.TestCase):
25
+ def test_anxiety_action(self):
26
+ a = aa.action_for('anxiety')
27
+ self.assertGreater(a['drive_delta'], 0); self.assertLess(a['max_chars_mult'], 1.0)
28
+ def test_sadness_action(self):
29
+ self.assertLess(aa.action_for('sadness')['drive_delta'], 0)
30
+ def test_anger_action(self):
31
+ self.assertGreater(aa.action_for('anger')['drive_delta'], 0)
32
+ def test_joy_action(self):
33
+ self.assertGreater(aa.action_for('joy')['max_chars_mult'], 1.0)
34
+ def test_guilt_action(self):
35
+ self.assertEqual(aa.action_for('guilt')['repair_delta'], 0.05)
36
+ def test_unknown_emotion_neutral(self):
37
+ self.assertEqual(aa.action_for('bogus')['drive_delta'], 0.0)
38
+
39
+ class CuriosityTest(unittest.TestCase):
40
+ def test_new_unanswered_raises(self):
41
+ self.assertGreater(aa.curiosity_update(0.0, new_topic=True, answered=False), 0.0)
42
+ def test_answered_lowers(self):
43
+ self.assertLess(aa.curiosity_update(0.5, answered=True), 0.5)
44
+ def test_decay_hours(self):
45
+ self.assertLess(aa.curiosity_update(0.5, hours=2), 0.5)
46
+ def test_effects(self):
47
+ effects = aa.curiosity_effects(1.0)
48
+ self.assertAlmostEqual(effects['question_probability_bonus'], 0.3)
49
+ self.assertAlmostEqual(effects['drive_delta'], 0.02)
50
+ def test_prompt_line(self):
51
+ line = aa.prompt_line(aa.action_for('anxiety'), 0.5)
52
+ self.assertIn('你现在想', line); self.assertIn('好奇', line)
53
+
54
+ if __name__ == '__main__':
55
+ unittest.main()