Pending: 0 Building: 1 Running: 8 Failed: 181
Created Date Type Name Commit Description State Age Up Time Life Time Action
merged [ADD] P-OPS-4 Ops Cockpit + AI Operations Manager governance (opens M3) [FIX] viin_ai_base: add base.user_admin to demo company B (multi-company invariant) The viin_ai_base DEMO creates a second res.company ('AI Ops Demo Co B') for the cross-tenant isolation demo, but a static demo <record> does NOT add the acting user to the new company the way a real res.company.create does - so it stranded base.user_admin (Mitchell Admin) outside that company. On the shared Runbot post-install DB this broke an UNRELATED Viindoo-Standard test with zero static code coupling: viin_helpdesk auto-creates one 'General' team per company; 'AI Ops Demo Co B' sorts before 'YourCompany' (res.company _order='sequence,name', both seq=0) so its team is created first and gets the lowest id; viin_helpdesk_project's TestAccessTicket.test_user_who_following_task_or_project_read_ticket does team.search([], limit=1) AS base.user_admin and the GLOBAL helpdesk_team_company_rule (no groups -> applies to admin) denies read of a team in a company admin is not a member of -> AccessError at helpdesk_ticket.py:336 (team.stage_ids). Fix: add base.user_admin to company_ai_ops_demo_b.company_ids, restoring the invariant that the admin/superuser setup account is a member of every company present in the install. The ops-manager demo user stays main-company-only, so the cross-tenant isolation demo is unaffected. Proven by confirm-by-toggle on a live DB (adding admin -> the helpdesk test runs green; removing the demo company -> green) and by an A/B install (helpdesk_project alone passes; helpdesk_project + viin_ai_base fails). Guarded by a new red-before- green regression test TestDemoCompanyMembership (RED without the fix: '1 failed'). Verified: helpdesk test 0 failed/0 error of 1; full viin_ai_base suite 0 failed/0 error of 122 tests (166 total). Succeed
merged [ADD] P-OPS-4 Ops Cockpit + AI Operations Manager governance (opens M3) [FIX] viin_ai_approval,viin_ai_ops: suppress force-sent approver mail in routing tests Runbot marks a build FAILED when the captured test log contains any ERROR record (odoo lower_logging -> result.had_error_log), even with 0 assertion failures. Three new test classes drive the real approval-routing path (action_route -> _submit_to_approval -> request.action_confirm), which message_post()s an approver notification that is force-sent synchronously. The tests create fresh res.company / users with no email and run without a mail.catchall.domain ICP, so the send has no sender address and mail_mail logs 'failed sending mail ... mail.catchall.domain' at ERROR (20 records), tripping the Runbot gate although every assertion passes. These tests assert routing / state / counts / multi-company isolation, NOT email delivery, so set mail_notify_force_send=False on the class env in setUpClass: the notification mail stays queued (never synchronously sent in the test transaction) so no spurious error is logged. Inherited by every sub-env (all build from self.env.context), so it reaches the message_post inside action_route(). Affected: TestCockpitPendingApprovalsProvider, TestMultiCompanyApprovalRouting (viin_ai_approval), TestOpsCockpitAggregation (viin_ai_ops). Verified locally (fresh DB, no catchall): 20 -> 0 'failed sending mail' ERROR records, 0 ERROR-level logs, full viin_ai_approval+viin_ai_ops suite 0 failed / 0 error of 85 tests. Failed
merged [ADD] P-OPS-4 Ops Cockpit + AI Operations Manager governance (opens M3) [FIX] viin_ai_ops: prettier-format cockpit JS for eslint web/tooling check Runbot test_pylint/test_eslint (eslint --no-eslintrc -c web/tooling/_eslintrc.json, prettier 2.8.x, trailingComma es5) flagged 8 prettier/prettier violations on the new cockpit JS: - ops_cockpit.js: collapse a single-string _t() call to one line; drop two es5 trailing commas in doAction() argument lists. - cockpit_tour.js: reformat an assignment ternary (break after '=', deepen indent). Formatting only, no logic change. Verified clean with prettier 2.8.8 (--tab-width 4 --print-width 100 --trailing-comma es5). Failed
merged [ADD] P-OPS-4 Ops Cockpit + AI Operations Manager governance (opens M3) [FIX] viin_ai_ops_brain: sudo Brain reads in approval-rationale evidence write The 'Generate Approval Rationale' button is callable by any user with write on the proposal (no group on the view) and writes an internal audit page under sudo. But the Brain-config reads on that path ran as the calling user: the rationale template content_html, the evidence vault auto_approve flag, and the originating viin.ai.trace business id. A non-Brain (or non-author) user hit AccessError on the vault/template, and the T5 trace attribution silently degraded to the no-trace sentinel under the trace own-records rule. Read all three under sudo (consistent with the already-sudo'd page + link create) so evidence is written with full-fidelity attribution regardless of who triggers it. Adds a red-before- green test: a non-author non-manager trigger records the REAL trace_id. Failed
merged [ADD] P-OPS-4 Ops Cockpit + AI Operations Manager governance (opens M3) [ADD] viin_ai_ops: P-OPS-4 Ops Cockpit dashboard + ops-manager governance gate OWL client action 'Ops Cockpit' under AI > Reporting: a COO/CFO governance pane-of-glass with 6 ACL-respecting metric tiles (pending_approvals, stuck_workflow, fail_rate, cost_per_goal, stale_evidence, ai_action_backlog) via retrieve_ops_cockpit() running as the user (read_group/search_count, no sudo, no raw SQL). Cross-module metrics flow through a decoupling provider hook so viin_ai_ops gains zero dependency edges. Company-wide view gated on group_ai_ops_manager (not base.group_system); tiles drill into the same records within the user's scope. Semantic v17 design tokens (no hardcoded color), AA contrast, responsive. Failed
merged [ADD] viin_ai_workflow: P-OPS-3 Operating Layer workflow adapter + heartbeat routines [FIX] viin_ai_workflow,viin_ai_workflow_automation: capture expected error-path logs in tests Runbot marks a build FAILED when the captured test log contains any WARNING/ERROR record (odoo lower_logging -> result.had_error_log), even with 0 assertion failures. Two tests exercise deliberate error-paths whose operator-facing logs leaked into the captured log: - test_cron_savepoint_isolates_a_failing_routine (WF-8): the poisoned routine raises, _cron_run_routines logs its traceback at ERROR (fault-isolation signal). - test_execute_approved_no_published_version_stays_approved: the unpublished workflow logs two WARNINGs (graceful no-fake-execute signal). Wrap each triggering call in self.assertLogs() so the expected records are captured (propagate=False -> no leak to Runbot) AND asserted to have fired - stronger than mute_logger: the test now proves the except-branch / degradation path actually ran. Production logging is unchanged (the logs are correct and valuable in operation; only the tests own their expected output now). Verified locally (no API key): 0 failed / 18 tests, 0 leaked WARNING/ERROR. Killed
merged [ADD] viin_ai_workflow: P-OPS-3 Operating Layer workflow adapter + heartbeat routines [DOC] docs,decisions: P-OPS-3 shipped + ADR-017 two-module split - roadmap.md: P-OPS-3 marked done (PR #49), module inventory 38 -> 39 (base viin_ai_workflow + bridge viin_ai_workflow_automation), M3 status (P-OPS-1/2/3 done; remaining P-OPS-4 cockpit + 3 heartbeat), ADR table. - operating-model.md 4.5: split tiering (Community base + EE bridge) refines the 4.3 single-module sketch. - ADR-017 (Accepted): two-module split rationale + super() parity invariant + no-exact-pin + Reports menu root owned by viin_ai_base. Runtime-verified Community 11 + Enterprise 18 tests, no API key. Failed
merged [ADD] viin_ai_workflow: P-OPS-3 Operating Layer workflow adapter + heartbeat routines [FIX] viin_ai_workflow,viin_ai_workflow_automation: runtime install + test fixes Found by a full Community + Enterprise install/test run (static review missed these): - work_routine_data.xml: drop name_vi_VN from viin.ai.agent / viin.ai.goal records - those models have no name_vi_VN field (ValueError at load). Routine names (viin.ai.work.routine.name is translate=True) are translated via i18n/vi.po instead; agent/goal names stay English per cluster precedent. - work_routine.py: the scan-domain safe_eval was passed the raw 'datetime' module, which safe_eval forbids. Use the wrapped datetime from odoo.tools.safe_eval (the core ir_filters domain-eval idiom). - viin_ai_workflow_automation form views: add <field workflow_instance_id invisible='1'/> so the stat-button 'invisible' modifier can resolve it (Odoo 17 requires modifier fields present in the view). - test_work_routine WF-3: the base-no-override check used a prefix that also matched the bridge package (viin_ai_workflow vs viin_ai_workflow_automation); anchor it with a trailing dot so the bridge's legitimate override is allowed. Verified: Community 0 failed/11 tests; Enterprise 0 failed/18 tests (no API key). Killed Not finished
merged [ADD] viin_ai_workflow: P-OPS-3 Operating Layer workflow adapter + heartbeat routines [ADD] viin_ai_workflow,viin_ai_workflow_automation: P-OPS-3 workflow adapter Operating Layer P-OPS-3 - wires decide -> orchestrate. viin_ai_workflow (Community base): - viin.ai.work.routine model + heartbeat cron (ar_overdue_review_daily, stock_anomaly_daily, cashflow_summary_weekly): scan -> work_item -> agent analysis -> Brain evidence hook -> optional governed proposal. - Adds routine_id on viin.ai.work.item. Multi-company (with_company, record rule); cron uses savepoint-per-routine, no manual commit. - Work Routines config menu + Routine Activity report; demo data with time-relative dates; vi_VN translations. viin_ai_workflow_automation (Enterprise auto_install bridge): - Adds workflow_instance_id (work_item/proposal) + control_policy.workflow_id. - Overrides viin.ai.action.proposal._execute_approved: route to a workflow.instance when a policy targets one, else super() to the P-OPS-2 single-step path (Community parity preserved). - workflow_id constrained to workflows targeting viin.ai.action.proposal. - depends viin_workflow_automation without an exact version pin. Killed Not finished
merged [ADD] viin_ai_workflow: P-OPS-3 Operating Layer workflow adapter + heartbeat routines [ADD] viin_ai_workflow,viin_ai_workflow_automation: P-OPS-3 workflow adapter Operating Layer P-OPS-3 - wires decide -> orchestrate. viin_ai_workflow (Community base): - viin.ai.work.routine model + heartbeat cron (ar_overdue_review_daily, stock_anomaly_daily, cashflow_summary_weekly): scan -> work_item -> agent analysis -> Brain evidence hook -> optional governed proposal. - Adds routine_id on viin.ai.work.item. Multi-company (with_company, record rule); cron uses savepoint-per-routine, no manual commit. - Work Routines config menu + Routine Activity report; demo data with time-relative dates; vi_VN translations. viin_ai_workflow_automation (Enterprise auto_install bridge): - Adds workflow_instance_id (work_item/proposal) + control_policy.workflow_id. - Overrides viin.ai.action.proposal._execute_approved: route to a workflow.instance when a policy targets one, else super() to the P-OPS-2 single-step path (Community parity preserved). - workflow_id constrained to workflows targeting viin.ai.action.proposal. - depends viin_workflow_automation without an exact version pin. Killed Not finished
merged [ADD] P-OPS-2 approval governance + no-key AI test framework [FIX] viin_ai_approval: mute expected ai_prompt warning in adapter test test_executes_when_action_runs creates an ir.actions.server with state='ai_prompt' but no ai_model_id/ai_prompt/ai_target_field_id (only to pass the executable-action filter); the spy calls through to run(), which logs the expected missing-fields warning. Mute that logger for the test, as test_server_action_ai_prompt already does. Killed
merged [ADD] P-OPS-2 approval governance + no-key AI test framework [FIX] viin_ai_approval_sale: pin QUOTE type to order company in e2e test test_end_to_end_confirm_cron_advisory_posted raised UserError 'Incompatible companies' on a multi-company DB: _make_quote_request searched the QUOTE approval type with limit=1 unfiltered, so it could pick another company's seeded type and fail _check_company at request create. Filter by the order's company (allowing company-less) for FIRST-deterministic behavior regardless of how many companies exist. Verified: 0 failed, 0 error(s) on a full demo DB. Failed
merged [ADD] P-OPS-2 approval governance + no-key AI test framework [FIX] viin_ai_approval: P-OPS-2 review fixes (XSS/PII/multi-company/hasattr) Post-review fixes for the P-OPS-2 approval governance + no-key test PR. CRITICAL - viin_ai_approval: stored XSS in _format_advisory_html - LLM content was interpolated into HTML then wrapped in Markup() at the end (no escaping). Rebuilt all 3 paths with Markup(template) %% arg so every LLM-sourced value (summary/recommendation/signal label+value, fallback raw) is auto-escaped. HIGH - viin_ai_approval: advisory cron processed requests cross-company; sudo() bypasses the company ir.rule. Run each request via with_company(company_id) so its own agent/provider/BYOK key resolves. - viin_ai_approval: ir_cron_advisory.xml missing noupdate=1 - upgrades reset the cron and silently re-enable LLM calls. Added noupdate=1. - viin_ai_approval_sale: payment_term_id.name (free-text PII) was sent to the LLM. Replaced with a PII-free payment_term_category derived from term lines. - viin_ai_ops_brain: evidence page landed ai_review_status='n_a' instead of honouring vault.auto_approve_ai_content; and page/link create lacked sudo() (AccessError for non-Brain-editor users). Set status explicitly + sudo(). hasattr existence-probe cleanup (ref Viindoo/odoo-mcp-client#63) - viin_ai_approval_sale: commercial_partner_id does not exist on sale.order - hasattr was always False, so credit-headroom always used the wrong partner. Fixed to partner_id.commercial_partner_id (real hidden bug). - account credit fields are guaranteed in the closure -> direct access. - margin/margin_percent (sale_margin soft dep, not in closure) -> use 'f' in source._fields. Base generic probes -> 'f' in source._fields too. MED/LOW - Dedup _has_executable_code/_has_real_code into viin_ai_approval/hooks.py. - _stub_provider patches the registry class (was import-path) like _stub_llm. - account: date.today()->fields.Date.today(); fix invalid payment_state key in topic prompt. ops_brain: goal fallback model + inheriting view id/name. Tests - Added stored-XSS regression tests, real PII-absent assertions (account+sale), ai_review_status-follows-vault-policy test. - Replaced duck-typed-fake sale signal tests with real-record ORM tests + two helpers (FIRST-deterministic skipTest when sale_margin/account absent). Killed Not finished
merged [ADD] viin_ai_approval(+account,+sale,+ops_brain): P-OPS-2 approval governance + AI advisory on human approvals [ADD] viin_ai_approval,viin_ai_ops_brain,test_viin_ai: P-OPS-2 approval governance + no-key AI test framework Phase 3.8 P-OPS-2 on top of viin_ai_ops, wired to the existing OmniApproval engine (viin_approval, tvtmaaddons17), workflow-independent (behaviour + tests identical with or without viin_workflow_automation), plus a cross-cutting no-key AI/LLM test framework. Status docs (roadmap + AGENTS) reconciled. SSOT: docs/ai/operating-model.md, ADR-015/016. == P-OPS-2 approval governance == Concern A - "Approval OF AI" (viin_ai_approval + viin_ai_ops_brain): a side-effecting viin.ai.action.proposal (medium/high risk, or a runs_as_sudo tool) is routed to a human viin.approval.request; on the human decision the proposal state syncs (approved/rejected/ draft) and the approved action is handed to execution. State sync is DATA-DRIVEN via the approval type's code_* hooks (code_validate_post / code_refuse_post / code_cancel_post) calling the proposal's public callbacks - NOT action_* method overrides (the idiomatic OmniApproval way, which is why post-approval execution is environment-independent). Captures trace_id / prompt_snapshot / iteration_index into approval evidence. viin_ai_ops_brain writes an attributed "Approval Rationale" Brain page (T5: is_ai_authored + agent + trace_id + confidence; html_escape on untrusted text). Concern B - "AI IN approval" (viin_ai_approval infra + per-app pilots): posts an AI risk- advisory card (risk score + summary + recommendation + signals) to a human approval request's chatter, for the approver - ADVISORY-ONLY (no auto-approve). Data-driven trigger: code_confirm_post enqueues (none -> pending) and wakes a cron; the cron runs the LLM out-of-band, using cr.savepoint() per record + the ir.cron row-lock for non-concurrency (no manual cr.commit). Graceful 'skipped' when no BYOK key is configured; silent 'error' on failure. Per-app pilots (ADR-015 convention: auto_install bridge, data-XML agent/topic seed, non-clobbering post_init_hook, PII-minimised signal builder): viin_ai_approval_account (CUSTOMER_INVOICE / VENDOR_PAYMENTS / CUSTOMER_REFUNDS) + viin_ai_approval_sale (QUOTE). == No-key AI/LLM test framework == L1 - shared unit-test helpers in viin_ai_base/tests/ (not on the runtime path): canonical FakeResponse, result builders (make_llm_call_result / make_completion_result / make_stream_chunks / make_fake_embed_fn), and AIStubMixin (_stub_llm / _stub_stream / _stub_provider patching the registry-class agent seam) + seed_pgvector_rows. 10 legacy test files converge onto these; 5 disconnected stub patterns (3 _FakeResponse copies, 2 identical fake_embed, 3 raw-SQL pgvector blocks) are deduplicated. The provider-inheritance test is hardened to assert the resolved MRO instead of the _inherit attribute. L2 - test_viin_ai: a separate TEST-ONLY module (Odoo test_* convention) the customer NEVER installs (installable, no auto_install, in no product dependency closure; dev/demo/CI only -> customer prod carries zero fabrication code). Two roles: a gated mock LLM provider (overrides one seam, make_request, for api_protocol='mock'; every protocol-keyed helper falls through to the openai_compat shape so cost-cap / PII redaction / normalize / usage.log / trace all run real; a fail-closed gate in models/gate.py - @api.constrains on the vendor + a refusal in make_request - so no fabrication is reachable when the gate is closed, preserving the BYOK invariant) AND the home for test cases that cannot live in a production module (cross-module / full-stack / mock-dependent). == Docs == ADR-015 (Concern B per-app advisory pattern), ADR-016 (shared AI test framework + gated test_viin_ai placement + L3 rejection), docs/ai/testing.md (no-key testing SSOT: decision tree, helper API, coverage fence, @tagged rule, Runbot lint/test commands). Pointers from AGENTS.md §6, docs/ai/README.md, docs/decisions/README.md. roadmap.md + AGENTS.md §3 reconciled: P-OPS-2 shipped (flagging that the original spec assumed action_* overrides while the shipped path is data-driven code_* hooks); gate M3 still needs P-OPS-3 (viin_ai_workflow) + P-OPS-4 (ops cockpit) + heartbeat routines. == Verification == No-key: Community 376/376 + EE 304/304 (with viin_workflow_automation present), 0 failed / 0 error. Code-quality (test_lint + test_pylint, exact Runbot config): flake8 + pylint_odoo clean, no lint suppressions; manifest/RST clean. Live demo: a real sale-quotation approval confirm -> cron -> agent.run -> mock provider yields a genuine advisory card (no network, no key, no canned message_post). Failed
merged [ADD] viin_ai_approval(+account,+sale,+ops_brain): P-OPS-2 approval governance + AI advisory on human approvals [ADD] viin_ai_*: no-key AI/LLM test framework (helpers + test_viin_ai module + docs) Give every viin_ai_* module a way to test LLM-dependent logic deterministically without an API key and without network, and unlock a live no-key demo - without weakening the BYOK invariant (no fabrication code reachable in a customer prod DB). A code-grounded audit found 5 disconnected stub patterns across 15 test files (3 _FakeResponse copies, 2 byte-identical fake_embed, 3 raw-SQL pgvector blocks, no canonical response shape). See docs/ai/testing.md + ADR-016 for the rationale. L1 - shared unit-test helpers in viin_ai_base/tests/ (not on the runtime path): - fake_response.py: one canonical FakeResponse. - builders.py: make_completion_result / make_llm_call_result / make_stream_chunks / make_fake_embed_fn - single-sourced result shapes (Appendix A). - common.py: AIStubMixin with _stub_llm / _stub_stream / _stub_provider context managers patching the registry-class agent seam (_do_llm_call / _do_llm_stream), plus seed_pgvector_rows. 10 legacy test files converge onto these; the duplicated _FakeResponse / fake_embed / inline result dicts and raw-SQL blocks are deleted. L2 - test_viin_ai: the cluster's TEST-ONLY module (Odoo test_* convention, cf. test_pylint) the customer NEVER installs (installable but no auto_install, in no product dependency closure, nothing depends on it; dev/demo/CI only -> customer prod carries zero fabrication code). Two roles: (1) a gated mock LLM provider; (2) the home for test cases that cannot live in a production module (cross-module / full-stack / mock-dependent). It overrides one seam (viin.ai.provider.make_request) for api_protocol='mock'; every protocol-keyed helper already falls through to the openai_compat shape, so cost-cap, PII redaction, normalize, usage.log and trace all run real - only the network call is faked (a local _MockResponse; models/ never imports the tests-only FakeResponse). Fail-closed gate (SSOT in models/gate.py): an @api.constrains on the vendor (a 'mock' vendor cannot persist outside test/demo) AND a refusal in make_request (never fabricates when the gate is closed). Gate open iff --test-enable OR registry test-mode OR ir.config_parameter test_viin_ai.enabled='1' (demo data sets it). 11 tests: 6 fail-closed gate proofs + 5 no-key full-stack proofs (real agent.run / call_embedding -> mock -> usage.log written). Also harden test_provider_inherits_mixin_and_thread: it asserted on viin.ai.provider._inherit (which reports only the last-loaded module's own declaration, so test_viin_ai legitimately flipped it) - rewritten to assert the resolved MRO + the capabilities the bases contribute; still fails if a base is genuinely removed. Docs: docs/ai/testing.md (SSOT decision tree + helper API + coverage fence + @tagged rule + Runbot lint/test commands with the W503/W504 trap called out), ADR-016 (decision + gated-placement rationale + L3 rejection), pointers from AGENTS.md §6, docs/ai/README.md, docs/decisions/README.md. Verified no-key: Community 376/376 + EE 304/304 (with viin_workflow_automation present), 0 failed 0 error; flake8 + pylint_odoo (exact Runbot config) clean, 10.00/10, no lint suppressions. Live demo: a real sale-quotation approval confirm -> cron -> agent.run -> mock provider yields a genuine advisory card (no network, no key, no canned message_post). Killed Not finished
merged [ADD] viin_ai_approval(+account,+sale,+ops_brain): P-OPS-2 approval governance + AI advisory on human approvals [ADD] viin_ai_*: no-key AI/LLM test framework (helpers + test_viin_ai module + docs) Give every viin_ai_* module a way to test LLM-dependent logic deterministically without an API key and without network, and unlock a live no-key demo - without weakening the BYOK invariant (no fabrication code reachable in a customer prod DB). A code-grounded audit found 5 disconnected stub patterns across 15 test files (3 _FakeResponse copies, 2 byte-identical fake_embed, 3 raw-SQL pgvector blocks, no canonical response shape). See docs/ai/testing.md + ADR-016 for the rationale. L1 - shared unit-test helpers in viin_ai_base/tests/ (not on the runtime path): - fake_response.py: one canonical FakeResponse. - builders.py: make_completion_result / make_llm_call_result / make_stream_chunks / make_fake_embed_fn - single-sourced result shapes (Appendix A). - common.py: AIStubMixin with _stub_llm / _stub_stream / _stub_provider context managers patching the registry-class agent seam (_do_llm_call / _do_llm_stream), plus seed_pgvector_rows. 10 legacy test files converge onto these; the duplicated _FakeResponse / fake_embed / inline result dicts and raw-SQL blocks are deleted. L2 - test_viin_ai: the cluster's TEST-ONLY module (Odoo test_* convention, cf. test_pylint) the customer NEVER installs (installable but no auto_install, in no product dependency closure, nothing depends on it; dev/demo/CI only -> customer prod carries zero fabrication code). Two roles: (1) a gated mock LLM provider; (2) the home for test cases that cannot live in a production module (cross-module / full-stack / mock-dependent). It overrides one seam (viin.ai.provider.make_request) for api_protocol='mock'; every protocol-keyed helper already falls through to the openai_compat shape, so cost-cap, PII redaction, normalize, usage.log and trace all run real - only the network call is faked (a local _MockResponse; models/ never imports the tests-only FakeResponse). Fail-closed gate (SSOT in models/gate.py): an @api.constrains on the vendor (a 'mock' vendor cannot persist outside test/demo) AND a refusal in make_request (never fabricates when the gate is closed). Gate open iff --test-enable OR registry test-mode OR ir.config_parameter test_viin_ai.enabled='1' (demo data sets it). 11 tests: 6 fail-closed gate proofs + 5 no-key full-stack proofs (real agent.run / call_embedding -> mock -> usage.log written). Also harden test_provider_inherits_mixin_and_thread: it asserted on viin.ai.provider._inherit (which reports only the last-loaded module's own declaration, so test_viin_ai legitimately flipped it) - rewritten to assert the resolved MRO + the capabilities the bases contribute; still fails if a base is genuinely removed. Docs: docs/ai/testing.md (SSOT decision tree + helper API + coverage fence + @tagged rule + Runbot lint/test commands with the W503/W504 trap called out), ADR-016 (decision + gated-placement rationale + L3 rejection), pointers from AGENTS.md §6, docs/ai/README.md, docs/decisions/README.md. Verified no-key: Community 376/376 + EE 304/304 (with viin_workflow_automation present), 0 failed 0 error; flake8 + pylint_odoo (exact Runbot config) clean, 10.00/10, no lint suppressions. Live demo: a real sale-quotation approval confirm -> cron -> agent.run -> mock provider yields a genuine advisory card (no network, no key, no canned message_post). Failed
merged [ADD] viin_ai_approval(+account,+sale,+ops_brain): P-OPS-2 approval governance + AI advisory on human approvals [ADD] viin_ai_*: no-key AI/LLM test framework (helpers + gated viin_ai_mock + docs) Give every viin_ai_* module a way to test LLM-dependent logic deterministically without an API key and without network, and unlock a live no-key demo - without weakening the BYOK invariant (no fabrication code reachable in a customer prod DB). A code-grounded audit found 5 disconnected stub patterns across 15 test files (3 _FakeResponse copies, 2 byte-identical fake_embed, 3 raw-SQL pgvector blocks, no canonical response shape). See docs/ai/testing.md + ADR-016 for the rationale. L1 - shared unit-test helpers in viin_ai_base/tests/ (not on the runtime path): - fake_response.py: one canonical FakeResponse. - builders.py: make_completion_result / make_llm_call_result / make_stream_chunks / make_fake_embed_fn - single-sourced result shapes (Appendix A). - common.py: AIStubMixin with _stub_llm / _stub_stream / _stub_provider context managers patching the registry-class agent seam (_do_llm_call / _do_llm_stream), plus seed_pgvector_rows. 10 legacy test files converge onto these; the duplicated _FakeResponse / fake_embed / inline result dicts and raw-SQL blocks are deleted. L2 - viin_ai_mock: a SEPARATE gated module the customer NEVER installs (auto_install False, in no product dependency closure; installed only on dev/demo/CI). Overrides one seam (viin.ai.provider.make_request) for api_protocol='mock'; every protocol-keyed helper already falls through to the openai_compat shape, so cost-cap, PII redaction, normalize, usage.log and trace all run real - only the network call is faked (a local _MockResponse; models/ never imports the tests-only FakeResponse). Fail-closed gate (SSOT in models/gate.py): an @api.constrains on the vendor (a 'mock' vendor cannot persist outside test/demo) AND a refusal in make_request (never fabricates when the gate is closed). Gate open iff --test-enable OR registry test-mode OR ir.config_parameter viin_ai_mock.enabled='1' (demo data sets it). 11 tests: 6 fail-closed gate proofs + 5 no-key full-stack proofs (real agent.run / call_embedding -> mock -> usage.log written). Also harden test_provider_inherits_mixin_and_thread: it asserted on viin.ai.provider._inherit (which reports only the last-loaded module's own declaration, so viin_ai_mock legitimately flipped it) - rewritten to assert the resolved MRO + the capabilities the bases contribute; still fails if a base is genuinely removed. Docs: docs/ai/testing.md (SSOT decision tree + helper API + coverage fence + @tagged rule + Runbot lint/test commands with the W503/W504 trap called out), ADR-016 (decision + gated-placement rationale + L3 rejection), pointers from AGENTS.md §6, docs/ai/README.md, docs/decisions/README.md. Verified no-key: Community 376/376 + EE 304/304 (with viin_workflow_automation present), 0 failed 0 error; flake8 + pylint_odoo (exact Runbot config) clean, 10.00/10, no lint suppressions. Live demo: a real sale-quotation approval confirm -> cron -> agent.run -> mock provider yields a genuine advisory card (no network, no key, no canned message_post). Killed Not finished
merged [ADD] viin_ai_approval(+account,+sale,+ops_brain): P-OPS-2 approval governance + AI advisory on human approvals [ADD] viin_ai_approval,viin_ai_approval_account,viin_ai_approval_sale,viin_ai_ops_brain: P-OPS-2 approval governance + AI advisory on human approvals Phase 3.8 P-OPS-2. Two cleanly separated concerns on top of viin_ai_ops, wired to the existing OmniApproval (viin_approval) engine. Behaviour + tests do NOT depend on viin_workflow_automation (identical with or without it installed). Concern A - "Approval OF AI" (viin_ai_approval, viin_ai_ops_brain): - Routes a side-effecting viin.ai.action.proposal (medium/high risk, or a runs_as_sudo tool) to a human viin.approval.request; on the human decision the proposal state syncs (approved/rejected/draft) and the approved action is handed to execution. State sync is data-driven via the approval type's code_* hooks (code_validate_post / code_refuse_post / code_cancel_post) calling the proposal's public callbacks - NOT via action_* overrides (the viin_approval idiom). - Post-approval execution is environment-independent (no workflow-engine gate; P-OPS-3 viin_ai_workflow overrides execution later). - viin_ai_ops_brain writes an attributed "Approval Rationale" Brain page (T5: is_ai_authored + agent + trace + confidence; html_escape on untrusted text). Concern B - "AI IN approval" (viin_ai_approval infra + per-app pilots): - Posts an AI risk-advisory card (risk score + summary + recommendation + signals) to the chatter of a human approval request, for the approver. Advisory only (no auto-approve). Data-driven trigger: code_confirm_post enqueues (none->pending) and wakes a cron; the cron runs the LLM out-of-band and posts the card. - The cron uses cr.savepoint() per record for failure isolation and relies on the ir.cron row-lock for non-concurrency - NO manual cr.commit(). Graceful 'skipped' when the BYOK key is not configured; silent 'error' on failure (no chatter noise). - Per-app pilots (ADR-015 convention; auto_install; data-XML agent/topic seed + a non-clobbering post_init_hook wiring the approval type by stable code; PII-minimised signal builder): viin_ai_approval_account (CUSTOMER_INVOICE / VENDOR_PAYMENTS / CUSTOMER_REFUNDS) and viin_ai_approval_sale (QUOTE). - UX: band-coloured chatter card (AA-contrast, "AI advisory - not a decision" disclaimer), a form status badge, and a list AI-risk column + "High risk" filter. Cluster decoupling kept: only viin_ai_approval depends viin_approval; the per-app modules reach it through the bridge. ADR-015 records the per-app advisory convention (decisions/README updated). Tests: Odoo native TransactionCase/@tagged, behaviour-protecting (no live LLM - the provider boundary is stubbed at agent.run / _do_llm_call). 36 tests, 0 failed and 0 error in BOTH a Community addons set AND a full Viindoo-EE set (with viin_workflow_automation). flake8 + the full test_pylint enable-list both pass with NO lint suppressions. Killed Not finished
merged [ADD] viin_ai_approval(+account,+sale,+ops_brain): P-OPS-2 approval governance + AI advisory on human approvals [REF] viin_ai_approval: advisory cron uses savepoint isolation, not manual commit Replaces the dangerous manual self.env.cr.commit() in the advisory cron (and the pylint disable that hid it) with the idiomatic Odoo pattern: - The ir.cron row lock (FOR NO KEY UPDATE SKIP LOCKED, ir_cron.py) already prevents the cron from running concurrently with itself, so the manual 'claim' commit was never needed. - Per-record isolation now uses 'with self.env.cr.savepoint()'; the ir.cron wrapper commits once on clean return (the pattern viin_account_auto_transfer / viin_zalo use). At-least-once retry on the next tick; no transaction breakage. - Tests no longer patch cr.commit (that patch was a symptom of the bad design). Also resolves R8180 without a disable: Concern A + Concern B are now one viin.approval.request model class (one class per model per module), the two concerns kept in clearly-marked sections; advisory_runner.py merged in + removed. Fixed the SALE-6 fixture to map partner/currency/user (the OmniApproval origin sync would otherwise null the sale.order partner_id). 36/36 tests pass; flake8 + full pylint_odoo enable-list both exit 0 with NO disables. Killed Not finished
merged [ADD] viin_ai_approval(+account,+sale,+ops_brain): P-OPS-2 approval governance + AI advisory on human approvals [FIX] viin_ai_approval,viin_ai_approval_account,viin_ai_approval_sale: pass Runbot test_pylint lint Runbot Code-Quality stage (test_pylint: pylint_odoo + flake8) was red while local --test-enable passed (the lint suite is not run by --test-enable). Resolved every reported violation: - E8102 invalid-commit (advisory_runner cron commits) -> documented + pylint disable - W8150 odoo-addons-relative-import (tests importing own module) -> relative imports - R8180 consider-merging-classes-inherited (Concern A/B separate _inherit) -> documented disable - E741 ambiguous 'l', F401 unused imports, E303, F841 unused var - W503 line break before binary operator -> restructured so no operator sits at a line boundary Verified: exact test_flake8 + full test_pylint enable-list both exit 0; 36/36 tests still pass. Killed Not finished
merged [DOC] docs,decisions: ADR-013 voice modality + ADR-014 dual deployment BYOK/managed [DOC] docs,decisions: ADR-013 voice modality + ADR-014 dual deployment BYOK/managed Tài liệu hoá voice agent modality cho cụm AI (doc-only, chưa code). - ADR-013 (Proposed): voice loop audio in -> STT -> agentic loop CŨ -> TTS -> audio out, BYOK-first. Stack tham chiếu license sạch thương mại + tiếng Việt (PhoWhisper/faster-whisper, Chatterbox, Pipecat/LiveKit), framing reference-default + swappable. Loại VibeVoice (vendor khuyến cáo không dùng thương mại + watermark + supply risk + voice cloning vướng Luật 91/2025). Ranh giới: user-initiated voice = execute theo ACL user (không proposal); AI-proactive = proposal-approve-workflow. Anti-misfire cho tool requires_confirmation (TTS readback + STT ack + nonce gate). - ADR-014 (Proposed, CẦN CEO ratify): phân định #11 BYOK - cấm proxy LLM vendor cloud giữ nguyên; managed compute cho model open-source self-host là phạm trù riêng (bán compute Viindoo). BYOK trước; managed Phase 9 (cuối roadmap, gap Phase 8). Invariant chặn managed proxy LLM cloud. - Cập nhật architecture (voice_interactive mode), data-models (call_tts, model_type=tts, usage_log audio billing, deployment_model), operating-model, security (§5.5 voice/biometric, PII forced-on external STT), roadmap (Phase 7.1 voice + Phase 9 managed), conventions (audio codec), integration (voice evidence -> Brain), AGENTS pointer #11 (CHƯA HIỆU LỰC tới khi ratify). Killed Not finished
merged [ADD] viin_ai_approval(+account,+sale,+ops_brain): P-OPS-2 approval governance + AI advisory on human approvals [DOC] docs,decisions: ADR-013 Concern-B per-app advisory pattern Records the reusable viin_ai_approval_<app> convention (auto_install bridge, data-XML agent/topic seed, non-clobbering post_init_hook by stable code, PII-minimised signal builder, 5-test checklist) extracted from the two shipped pilots (account + sale). Status: proposed, pending owner sign-off. Failed
merged [ADD] viin_ai_approval(+account,+sale,+ops_brain): P-OPS-2 approval governance + AI advisory on human approvals [REF] viin_ai_approval: data-driven state sync via approval-type code_* hooks Follow the viin_approval idiom: do NOT override action_validate/refuse/cancel in Python to trigger side-effects. Instead the seeded approval.request.type carries code_validate_post / code_refuse_post / code_cancel_post inline hooks (safe_eval) that call the proposal's public callbacks _on_approval_validated/refused/cancelled. - viin_approval_request.py: remove the action_* overrides + _propagate_to_work_item; keep only the evidence/link fields (ai_proposal_id, ai_trace_id, ai_prompt_snapshot, ai_iteration_index). - action_proposal.py: add the public callbacks (approved + work-item start + honest single-step execute; rejected; back-to-draft). - data/approval_request_type.xml: the three code_* hooks dispatch to the callbacks via ai_proposal_id. Verified identical green (0 failed of 10) in BOTH Community and full-EE addons sets; grep confirms no action_* override remains. Tests assert business outcomes (proposal approved/rejected/draft after the decision), mechanism-agnostic. Killed Not finished
merged [ADD] viin_ai_approval(+account,+sale,+ops_brain): P-OPS-2 approval governance + AI advisory on human approvals [ADD] viin_ai_approval,viin_ai_ops_brain: P-OPS-2 approval adapter + Brain evidence Wire the propose -> DECIDE -> remember steps of the AI operating model (Phase 3.8, opens gate M3). Two new Community-compatible modules + small reconciles: * viin_ai_approval (depends viin_ai_ops, viin_approval 0.2.2): maps viin.ai.action.proposal <-> viin.approval.request. action_route submits a pending proposal to an approval request; action_validate/refuse/cancel overrides sync state back to the proposal (+ work item). Sudo/high-risk proposals are forced through approval (never auto-execute). Community fallback executes an approved proposal single-step via its mapped ir.actions.server (state='ai_prompt') under sudo, and only marks 'executed' when a side effect actually ran - otherwise it stays 'approved' (execution deferred to P-OPS-3 / the Enterprise viin_workflow_automation when present). Evidence (trace, prompt snapshot, iteration index) captured on the request. * viin_ai_ops_brain (depends viin_ai_ops, viin_brain): generates a Brain "Approval Rationale" evidence page per proposal with mandatory T5 attribution (is_ai_authored + agent + trace + confidence), untrusted text html-escaped, linked back to proposal/work item/goal. Idempotent. * viin_ai_ops: base proposal form now provides one shared oe_button_box (first child of sheet) so adapter stat buttons hoist to a single control-panel box. * viin_ai_agent: deprecated stub anchors (viin.ai.schedule, viin.ai.approval.chain) docstrings point to the shipped viin_ai_approval adapter; field anchors retained for v17 schema compatibility (removal deferred). Tests: 54 Odoo-native tests pass (ACL, 2-way sync, sudo/high-risk enforce, Community fallback honesty + Enterprise-defer, T5 attribution, cross-module integration). The workflow-dependent fallback tests force _workflow_automation_installed deterministically (FIRST-compliant) so they pass whether or not the Enterprise module is installed. Visual UI/UX review PASS (single button box, AA contrast, theme-faithful, console clean). Killed Not finished
merged [ADD] viin_ai_approval(+account,+sale,+ops_brain): P-OPS-2 approval governance + AI advisory on human approvals [ADD] viin_ai_approval,viin_ai_ops_brain: P-OPS-2 approval adapter + Brain evidence Wire the propose -> DECIDE -> remember steps of the AI operating model (Phase 3.8, opens gate M3). Two new Community-compatible modules + small reconciles: * viin_ai_approval (depends viin_ai_ops, viin_approval 0.2.2): maps viin.ai.action.proposal <-> viin.approval.request. action_route submits a pending proposal to an approval request; action_validate/refuse/cancel overrides sync state back to the proposal (+ work item). Sudo/high-risk proposals are forced through approval (never auto-execute). Community fallback executes an approved proposal single-step via its mapped ir.actions.server (state='ai_prompt') under sudo, and only marks 'executed' when a side effect actually ran - otherwise it stays 'approved' (execution deferred to P-OPS-3). Evidence (trace, prompt snapshot, iteration index) captured on the request. * viin_ai_ops_brain (depends viin_ai_ops, viin_brain): generates a Brain "Approval Rationale" evidence page per proposal with mandatory T5 attribution (is_ai_authored + agent + trace + confidence), untrusted text html-escaped, linked back to proposal/work item/goal. Idempotent. * viin_ai_ops: base proposal form now provides one shared oe_button_box (first child of sheet) so adapter stat buttons hoist to a single control-panel box. * viin_ai_agent: deprecated stub anchors (viin.ai.schedule, viin.ai.approval.chain) docstrings point to the shipped viin_ai_approval adapter; field anchors retained for v17 schema compatibility (removal deferred). Tests: 53 Odoo-native tests pass (ACL, 2-way sync, sudo/high-risk enforce, Community fallback honesty, T5 attribution, cross-module integration). Visual UI/UX review: PASS (single button box, AA contrast, theme-faithful, console clean). Failed
merged [DOC] Vision/mission first-read + ADR-012 bridge re-purpose + reconcile drift [DOC] docs,AGENTS: vision/mission first-read + ADR-012 bridge re-purpose + reconcile drift Anti-drift: add a single SSOT "Vision & Mission" block at the top of AGENTS.md (auto-loaded; CLAUDE.md/GEMINI.md @import it) and make it the first mandatory read in section 0, plus a vision-first pointer in docs/README.md. When code/roadmap drift, vision is the anchor. ADR-012 (new): seed master data per application. Brain viin_brain_<app> modules are re-purposed as seed-master-data carriers per app (vault/page/template), and each viin_ai_<app> connector seeds AI master data (agent/topic/tool/SQL template) per app. This overrides ONLY the "bridges redundant -> removal at v1.2" clause of ADR-001; the mail.thread universal-sidebar decision stands. ADR-001 gets a partially-superseded-by pointer; decisions index updated. Reconcile drift with shipped state: - roadmap §2: add viin_ai_ops (P-OPS-1) + viin_ai_editor (ADR-010) + 5 connector stubs; fix viin_ai_agent version 0.2.1; ADR count 8->10. - roadmap §7: add viin_ai_editor row; bridges re-scoped to seed carriers; total 31->38. - roadmap §8: add ADR-010/012 rows; WI count 4->8; clarify ADR-009 (no file yet). - roadmap §10/§11 + Track B timeline + M2 gate: drop stale "remove v1.2" wording. - AGENTS.md §3 status: Track A stopped at P-OPS-1, Track B (P16/ADR-010/WI-007) DONE. Doc-only; no code changes. Reviving bridges + writing seed data is a later code phase. Killed Not started Not finished
merged [IMP] viin_brain: Brain editor UI/UX polish (#44) [IMP] viin_brain: Brain editor UI/UX polish - white canvas, AA contrast, inline icon, symmetric padding, mobile collapse, declutter chrome, a11y labels (#44) Addresses issue #44 (P16 follow-up): white-paper canvas, WCAG-AA breadcrumb + property labels, inline page icon, symmetric editor padding, mobile single-column collapse, decluttered context-pane chrome (overflow menu), deduped vault selector, inline record-mention chips, property input label/id association. Includes the code-review cleanup and tour updates for the new overflow navigation. Killed Not finished
merged [IMP] viin_brain: Brain editor UI/UX polish (#44) [IMP] viin_brain: Brain editor UI/UX polish - white canvas, AA contrast, inline icon, symmetric padding, mobile collapse, declutter chrome, a11y labels (#44) Addresses issue #44 (P16 follow-up). Includes code-review cleanup (shadow token, deduped chip styling, t-foreach overflow menu, centralised property a11y attrs, structural header layout, label-for only on input-bearing types), eslint/prettier formatting, and tour updates for the overflow tab navigation. Failed
merged [IMP] viin_brain: Brain editor UI/UX polish (#44) [IMP] viin_brain: Brain editor UI/UX polish - white canvas, AA contrast, inline icon, symmetric padding, mobile collapse, declutter chrome, a11y labels (#44) Addresses issue #44 (P16 follow-up). Includes code-review cleanup: shadow token, deduped chip styling, t-foreach overflow menu, centralised property a11y attrs, structural header layout, and label-for only on input-bearing property types. Failed
merged [IMP] viin_brain: Brain editor UI/UX polish (#44) [IMP] viin_brain: Brain editor UI/UX polish - white canvas, AA contrast, inline icon, symmetric padding, mobile collapse, declutter chrome, a11y labels (#44) Killed Not finished
merged [IMP] P16: Editor Extension Framework - modular Brain/AI editor (OdooEditor migration) [DOC] docs,AGENTS: ADR-010 editor framework + reconcile docs with shipped P16 ADR-010 Editor Extension Framework + WI-007/WI-008; reconcile brain/ai architecture, ai-bridge, ui-design, conventions, roadmap and decision index with the shipped code; AGENTS module map adds viin_ai_editor + engine desc. Killed
merged [IMP] P16: Editor Extension Framework - modular Brain/AI editor (OdooEditor migration) [IMP] P16 review supplement: fix review findings + Brain editor UX polish (#2) * [FIX] viin_ai_ops: null-guard ListRenderer.setDefaultColumnWidths against intermittent DOM race Intermittent TypeError "Cannot read properties of null (reading 'style')" in core Odoo 17 ListRenderer.setDefaultColumnWidths() surfaces on the Action Proposals list when the view re-renders with zero records (isEmpty toggle races thead commit to DOM, querySelector returns null). Adds a defensive monkey-patch in viin_ai_ops that overrides the method with an identical but null-safe implementation: if headerEl is null the column iteration step is skipped instead of crashing. This matches the upstream fix strategy applied in Odoo 18 via useMagicColumnWidths. BACKPORT FROM: Odoo 17 core list_renderer.js (upstream fixed in 18.0) REMOVE WHEN UPGRADING TO: 18.0 * [FIX] viin_ai_editor: surface UserError message in AI block error state Odoo 17 RPCError shape wraps the real UserError text in err.data.message while err.message is always the opaque "Odoo Server Error" wrapper. The catch in AiBlock._generate now reads err.data.message first so users see the actionable UserError (e.g. "No AI agent configured") instead of the unhelpful wrapper. - ai_block.js: prioritise err.data.message || err.message || fallback _t() - conventions.md: add JSON-RPC error handling convention (SSOT: chat_panel.js) - test_ai_block_state_machine.js: add 2 cases covering RPCError shape and plain-Error fallback * [IMP] viin_web_editor,viin_brain,viin_ai_brain: dedup powerbox native commands (WI-007) Introduce a generic suppress-native registry in viin_web_editor so downstream modules can declaratively remove native OdooEditor/Wysiwyg powerbox commands that they replace, without the engine hardcoding any consumer-specific names. Changes: - viin_web_editor/powerbox_registry.js: export new registry viin_web_editor.powerbox_suppress_native with suppressor shape {matcher, contextGate?} - viin_web_editor/wysiwyg_patch.js: filter upCommands (native) through all registered suppressors before concatenating extra commands; exceptions inside suppressors are caught per-entry (fail-safe) - viin_brain/slash_commands.js: register five suppressors (headings/quote/code/ bullet/numbered, all context-gated to .o_brain_editor_body_edit); fix brain_bullet_list and brain_numbered_list to use toggleList (identical to native, preserves toggle-off semantics) - viin_ai_brain/powerbox_commands.js: register suppressor for native ChatGPT (fa-magic) gated on bridge.available=true - docs/decisions/wi-007-powerbox-suppress-native.md: new append-only WI, Refines ADR-010 * [FIX] viin_ai_brain: multi-company filter agent lookup (B1) Extract the Knowledge Base Assistant agent lookup into a single SSOT helper `viin_ai_brain/controllers/common.py::find_kba_agent()` that enforces `company_id in [env.company.id, False]` on every search. Before this fix wysiwyg.py `_get_agent()` searched by name only with no company filter, leaking cross-tenant agent records. ai_block.py already had the filter but in a local function. Both controllers now import `find_kba_agent` from `controllers/common.py`, eliminating the duplicate logic and the security gap. * [IMP] viin_ai_brain: topic-based agent lookup + CTA (MED-2) Update `controllers/common.py::find_kba_agent` to resolve the KBA agent via the `viin_ai_brain.topic_brain_assistant` topic xmlid instead of the display name. This makes lookup name-independent: renaming the agent for i18n or branding purposes does not break any controller path. When no agent with the topic is found, raises UserError with an actionable CTA pointing to AI > Configuration > Agents (viin_ai_agent.action_viin_ai_agent). Frontend changes: - ai_bridge_service.js: pass topicXmlId instead of hard-coded topicName for ChatPanel so the chat panel can resolve the topic server-side. - ai_editor_backend_service.js: remove "Knowledge Base Assistant" hard-coded fallback for agent_name; server is the SSOT (empty string sentinel on RPC success avoids masking real lookup failures). Tests (Odoo native TransactionCase, @tagged post_install): - TestKbaAgentLookupMultiCompany: B1 isolation - company A env cannot resolve company B's KBA agent; global agents (company_id=False) are visible from any company; missing-company agent raises UserError+CTA. - TestKbaAgentTopicLookup: rename-safe lookup (MED-2); detaching topic raises UserError+CTA; wysiwyg endpoint surfaces CTA end-to-end. - Updated TestBrainBridgeCommon to attach the KBA topic to the fixture agent, and updated _ensure_brain_agent in tour tests likewise. - Updated test_commit_attribution_raises_when_no_kba_agent: detach topic (not rename) to simulate missing agent under topic-based lookup. Verified: 0 failed, 0 error of 57 tests (wave_wid_test ephemeral DB). * [IMP] viin_brain: fix AI badge wording to reflect installed-not-ready semantics Badge text changed from "AI ready" to "AI features available" and tooltip updated to "AI features are installed - configure a provider & agent to use them." to avoid implying the AI is operational before a provider and agent are configured (Option B wording-only fix, WI-E). * [DOC] docs: WI-008 PR #43 deferred follow-ups tracking + roadmap corrections Create docs/decisions/wi-008-pr43-deferred-followups.md (append-only WI): - WI-F (MED-3): Provider Test Connection button -> viin_ai_base Layer 0 backlog - WI-G: L2 active-default policy (needs ADR-011), L3 dashboard CTA (W12 P-OPS-4), L4 admin empty-state guidance (Phase 5/onboarding) - WI-007-onboarding: Phase 5 ADR + wizard (M4 gate); seed agent stub explicitly excluded (model_id required+restrict constraint); CTA error already done in WI-D2 - B2: CRDT pitch guidance - interim relay is NOT OT real-time collab (ADR-002 intact) - B3: SQL template count corrected to 15 (roadmap said 11 pre-viin_ai_crm baseline; review cited 13; correct per CEO 2026-06-06 = 15) Update docs/roadmap.md: - Fix SQL template count 11 -> 15 with WI-008 B3 reference - Add Layer 0 backlog note after M1 gate (WI-F, WI-G L2) - Add AI Onboarding wizard item to Phase 5 W16-W19 (gated M4) - Add WI-005, WI-006, WI-008 rows to ADR/WI cross-reference table - Mark ADR-010 DONE 2026-06-04; add ADR-011 candidate for active-default policy Update docs/decisions/README.md: - Add wi-008 row to WI index - Update date to 2026-06-06 * [FIX] viin_ai_brain,viin_ai_editor,viin_brain,viin_web_editor: eslint prettier formatting on WI changes * [REF] viin_ai_brain: Command.* in WI-D tests + drop dead chat prop (review LOW) * [FIX] viin_brain: powerbox suppressor context-gate timing (Phase V blocker) Root cause: _getPowerboxOptions() runs synchronously inside startEdition(). The DOM markers (.o_brain_editor_body_edit[data-page-id]) are applied in the startWysiwyg callback AFTER startEdition() resolves, so _isBrainContext() always returned false during powerbox build - all 5 suppress-native rules were no-ops, causing 7 native commands to appear twice (Heading 1/2/3, Text, Numbered list, Quote, Link). Fix: set wysiwyg.__brainPageContext = true on the wysiwyg instance BEFORE calling startEdition() in page_editor.js. Update _isBrainContext() to check this early-signal flag (Tier 1) before falling back to the DOM closest() check (Tier 2). Flag is Brain-PageEditor-only; mail/website editors are unaffected. Bonus: deduplicate powerbox "AI" category registrations. Three WI phases (WI-A1, WI-B3, WI-007) each registered a separate category key all named "AI", producing three UI separators. Guard each registration behind a check for all known AI category keys so only one "AI" category appears in the powerbox when both viin_ai_editor and viin_ai_brain are installed. * [FIX] viin_web_editor,viin_brain,viin_ai_brain: suppress native powerbox at merge point + Text/Link + AI category (Phase V) - wysiwyg_patch.js: add Tier 2 powerboxFilters injection to suppress OdooEditor-level commands (Heading 1/2/3, Bulleted list, Numbered list, Text) that bypass _getPowerboxOptions and cannot be caught by Tier 1 alone. Use _viinCustom sentinel to distinguish consumer commands from native ones so suppressors never accidentally remove Brain's own commands that share the same fontawesome icon. - slash_commands.js: add viin_brain.suppress_text (fa-paragraph) and viin_brain.suppress_link (fa-link) suppressors for the native Text and Link/Button commands that brain_paragraph and brain_external_link replace. - brain_editor_registry_commands.js: document that key "brain_ai" is intentional (viin_ai_editor uses .add("AI") unconditionally; using the same key here would throw on duplicate). Powerbox.open() deduplicates categories by name at runtime. - powerbox_commands.js (viin_ai_brain): same documentation for key "brain_ai_write". - wi-007-powerbox-suppress-native.md: update to describe two-tier architecture, add Text/Link suppressor table rows, add AI category safety analysis. * [FIX] viin_ai_brain: group AI powerbox commands under single category (Phase V) Root cause: _t("AI") at module-evaluation time returns LazyTranslatedString *objects* (extends String). Different _t() calls across modules produce different object instances never === each other. Powerbox._groupCommands() matches command.category === category.name with strict equality, so the match always fails for cross-module LazyTranslatedString instances. Each AI command fell into the remaining-categories path and got its own "AI" header, rendering 3 separate "AI" separators instead of one. Fix: use plain string literal "AI" (not _t("AI")) for both category name and command category across all 3 AI-contributing modules. Plain strings compare correctly with ===. "AI" is locale-invariant so no i18n value is lost. All 3 modules now register under canonical key "AI": - viin_ai_editor: add("AI", ..., {force:true}) - prevents DuplicatedKeyError when brain/ai_brain registers first - viin_ai_brain, viin_brain: guarded by !contains("AI") - avoid overwrite warning when viin_ai_editor loads first WI-007 decision doc updated to document the real root cause and the fix. * [FIX] viin_brain: vault page resequence no longer clears the tree (P16 review) Root cause: _reorderPage called _invalidateParent(null) after the ORM writes. _invalidateParent deletes childrenByParent.get(parentId) then re-fetches only when parentId is in expandedIds. The root parent (null) is never in expandedIds, so the root list was permanently deleted and the sidebar showed "No page in this vault" until the user navigated away or reloaded the page. Server data was saved correctly — only the UI was affected. Fix: optimistic reorder. Swap sequence values directly in the in-memory pageCache before awaiting the ORM writes, re-sort the affected parent's child-id array in childrenByParent using the updated sequences, then bump the flatRows cache. The list is never cleared between the drop event and the server response. A try/catch around the ORM writes restores consistency via _invalidateParent + an explicit _loadRoots call only in the error path. A new QUnit unit test (no-mount harness, same pattern as the WI-8 suite) pins the regression: after _reorderPage the root list must still exist, contain both page ids, and be re-sorted by the new sequences. * [IMP] viin_brain: Brain editor UX polish - editor border, powerbox default style, placeholder hints, clickable breadcrumb (P16 review) Issue 2 (editor border): fix specificity loss in .o_brain_editor_body_edit note-editable border override. Add compound selector &.note-editable + !important backstop so Brain wysiwyg canvas never shows the textarea-like border from web_editor.backend_assets_wysiwyg regardless of stylesheet load order. Issue 3 (powerbox style): remove Brain-specific powerbox SCSS overrides (_powerbox.scss). The custom grey background / washed-out description text diverged from Odoo default. Revert to Odoo default white-background crisp powerbox. Command injection via wysiwyg_patch.js / powerboxCommandsRegistry is completely unaffected - only visual overrides removed. Issue 4 (placeholder hint): wire OdooEditor native placeholder option in _buildWysiwygOptions() so empty pages show "Type / for commands, [[ to link a page, @@ to mention" hint via .oe-hint::before. Also add o_brain_editor_placeholder class to the editable in startWysiwyg() as CSS fallback for :empty:not(:focus)::before defined in _app_shell.scss. Issue 5 (breadcrumb + header lean): make vault breadcrumb segment clickable (role=button, tabindex=0, cursor pointer, hover highlight, onBreadcrumbVaultClick handler calls onVaultSelect prop bound from BrainApp.onVaultSelect). Tighten header layout: margin-bottom 16px->10px, breadcrumb mb 8px->4px, sep padding 0 6px->0 4px, title font 28px->26px, icon 24px->22px, padding 4px->2px 0. * [FIX] viin_brain: show editor placeholder hint (CSS cascade fix, P16 review) Drag-handle rule `> p::before { content: "⋮⋮" }` was overriding `.oe-hint::before { content: attr(placeholder) }` due to equal specificity and source order. Fix: narrow the drag-handle selector to `> p:not(.oe-hint)` so the hint paragraph is excluded from the drag-handle block entirely. Also add explicit `content: attr(placeholder)` to the `.oe-hint::before` rule to ensure it always wins regardless of cascade order, and bump opacity from 0.38 to 0.45 for better readability. * [FIX] viin_brain: eslint prettier formatting on P16 UI fixes * [FIX] viin_brain,viin_ai_brain: scope Brain powerbox commands to Brain editor context (P16 review) Brain-specific powerbox commands were registered globally and leaked into every editor that loads the powerbox patch (mail templates, CRM description, website builder, etc.), causing UI noise and potential runtime errors when Brain-specific callbacks (wikilink, embed, database, record mention) fire outside a Brain page. Fix: add isDisabled(wysiwyg) => !isBrainContext(wysiwyg) to all Brain-specific commands so they are hidden by the Powerbox engine outside a Brain editor. isBrainContext uses a two-tier check (wysiwyg.__brainPageContext early-signal flag + DOM .o_brain_editor_body_edit[data-page-id] fallback) identical to the existing WI-C suppress-native contextGate. SSOT: extract shared predicate to viin_brain/static/src/utils/brain_context.js (web.assets_backend eager bundle, no @web_editor dep). Three consumers: - slash_commands.js (wysiwyg lazy bundle) - all 14 Brain commands gated - brain_editor_registry_commands.js (eager) - brain_ai_block stub gated - viin_ai_brain/registries/powerbox_commands.js - brain_ai_write + brain_ai_chat now require both Brain context AND bridge.available brain_ai_write / brain_ai_chat: previous _isDisabledWhenNoBridge only checked bridge availability; these commands also leaked into non-Brain editors when bridge was active. New _isDisabledWhenNoBridgeOrOutsideBrain combines both. Not gated: viin_ai_editor ai_block command is a general AI editor feature (not Brain-specific); its isDisabled already gates on backend.available. ai_block_conflict_dedup.js WI-C dedup mechanism preserved unchanged. WI-007 suppress-native contextGate logic preserved unchanged. * [FIX] viin_ai_editor: eslint prettier trailing comma * [FIX] viin_ai_brain: address Codex review on PR #43 supplement Two P2 findings from the automated Codex review: 1. find_kba_agent preferred global over company-specific agents. order='company_id desc' sorts SQL NULLs (global, company_id=False) FIRST under PostgreSQL DESC, so a company with its own KBA agent was still routed through the shared global one. Replaced with a two-step preference (company-specific search first, global fallback) - DB-agnostic and provably correct. Added TestKbaAgentCompanyPreference regression tests (company-specific wins; global still used as fallback). 2. Native ChatGPT (fa-magic) powerbox command was suppressed in EVERY editor once viin_ai_brain was installed, because the suppressor gated only on bridge.available (globally true). In non-Brain editors (mail/CRM/website) the replacement brain_ai_write is itself disabled, leaving no AI write command. Added isBrainContext(wysiwyg) to the suppression contextGate so the native command is only replaced inside Brain editors. Failed
merged [IMP] P16: Editor Extension Framework - modular Brain/AI editor (OdooEditor migration) [FIX] viin_brain: restore pylint-odoo consider-merging-classes-inherited disable Reverts the removal in b93db89, which was based on a faulty reproduction against VANILLA Odoo (no pylint-odoo loaded -> the inline disable read as unknown-option-value W0012). The Viindoo Runbot lint is NOT Odoo-core test_lint: it runs the tvtmaaddons test_pylint module, which loads pylint_odoo and ENABLES consider-merging-classes-inherited (R8180). So the disable is required: viin.brain.page is intentionally re-opened in both viin_brain_page_tag.py and viin_brain_page_extension.py for readability, and R8180 asks to merge them. Verified against the period-correct Odoo 17 stack (pylint 2.15.10 / astroid 2.13.5 / pylint-odoo 8.0.22) using the Runbot module's exact ENABLED_CODES: with the disable restored, all 21 viin modules report 0 findings (R8180 suppressed). Runbot build 222101 failed on exactly this one message (1 failed of 24 tests). Killed
merged [IMP] P16: Editor Extension Framework - modular Brain/AI editor (OdooEditor migration) [FIX] viin_web_editor,viin_brain,viin_ai_editor,viin_ai_brain: Runbot test_lint/test_pylint green Reproduced the Runbot failures locally against Odoo 17 test_lint (period-correct pylint 3.3.7) instead of guessing, and fixed the real root causes: - test_pylint (Python): viin_brain/models/viin_brain_page_tag.py carried '# pylint: disable=consider-merging-classes-inherited'. That message belongs to the pylint-odoo plugin, which Odoo core test_pylint does NOT load, so the inline disable resolves to unknown-option-value (W0012) - not suppressed by '--disable=all,useless-option-value' - making pylint exit non-zero. The message is not even in test_pylint's enable list, so the disable was inert. Removed it. Verified pylint exit 0 across all 21 viin modules. - test_eslint (JS): 417 prettier/prettier errors across P16 JS under the repo .eslintrc.json (extends plugin:prettier/recommended, tabWidth 4, printWidth 100). The husky hook only formats staged files, so P16 files committed earlier were never formatted. Ran eslint --fix; verified 0 errors against both the repo prettier config and Odoo core eslintrc. - test_manifests: viin_ai_editor set 'auto_install': False, which equals the default manifest value (assertNotEqual hard-fail). Removed the key. No behavior change: JS edits are pure formatting; Python edits delete an inert key and an inert lint pragma. package-lock.json kept at baseline. Failed
merged [IMP] P16: Editor Extension Framework - modular Brain/AI editor (OdooEditor migration) [FIX] viin_web_editor,viin_brain,viin_ai_editor,viin_ai_brain: lint clean for Runbot (prettier/eslint format, unused vars, manifest, pot, flake8) Failed
merged [IMP] P16: Editor Extension Framework - modular Brain/AI editor (OdooEditor migration) [FIX] viin_brain: repair slash-focus tour (AI category priority outranks Brain) ROOT CAUSE (tour fragility, not product code regression): viin_ai_editor registers an "AI" powerbox category at priority 95 and an "AI block" command at priority 90. The "Brain" category is registered at priority 90. The OdooEditor Powerbox sorts categories by priority desc then name asc, so "AI" (p=95) renders before "Brain" (p=90). Pressing Enter after a bare "/" selected the "AI block" command instead of "Heading 1", and the assertion :has(h1,h2,h3) failed. Evidence: failure screenshot showed the AI block component rendered (the "AI block - Empty - type a prompt or pick a template" UI) instead of an h1. The Brain heading-via-slash command itself works correctly when explicitly selected; the tour was testing an incidental ordering assumption. FIX (tour fix, not product code): Changed the powerbox step to type "/heading" instead of bare "/" before pressing Enter. After insertText("/heading") + a keyup event, Powerbox. _onKeyup() runs patienceDiff and filters filteredCommands to only the three Brain heading commands (Heading 1 p=100, Heading 2 p=90, Heading 3 p=80). "Heading 1" is then the active/first command. Tightened the final assertion from :has(h1,h2,h3) to :has(h1) since the filter + priority guarantee h1. FAILURE 2 (TestBrainTimelineTour): already green in the worktree; 0 failed on isolation re-run. No code change needed. Wikilink tour: confirmed still green (3/3 pass, 0 errors). Failed
merged [IMP] P16: Editor Extension Framework - modular Brain/AI editor (OdooEditor migration) [FIX] viin_ai_editor: valid XML comments in demo data (double-hyphen) Failed
merged [DOC] ADR-010 Editor Extension Framework (modular Brain/AI editor) — Proposed [DOC] docs: ADR-010 Editor Extension Framework (modular Brain/AI editor) Refine ADR-003 with the structural HOW for the OdooEditor migration (P16): - Keep viin_web_editor as the neutral propagation base (patch HtmlField/ Wysiwyg once, driven by global registries) - it is the keystone that lets Brain and AI editor plugins depend on it without coupling to each other (AGENTS.md cam ky #2). Deleting it was considered and rejected. - Move the AI block out of viin_brain into a new AI-cluster module viin_ai_editor (generic, works in any html field); bridge viin_ai_brain wires AI output to Brain attribution + resolves Brain x AI conflicts. - Grounded in verified Odoo 17 web_editor seams (options.plugins, _getPowerboxOptions, HtmlField.wysiwygOptions, no native NodeView, setupCollaboration, '/'-only input rule). Tracker odooeditor-migration-progress.md re-organised from 23-features-in-Brain to module-split + a 4-phase wave plan (framework -> plugins -> bridge -> verify+visual) with odoo-ui-reviewer visual gates. Status Proposed - awaiting sign-off before execution. Killed Not started Not finished
merged [DOC] ADR-010 Editor Extension Framework (modular Brain/AI editor) — Proposed [DOC] docs: ADR-010 Editor Extension Framework (modular Brain/AI editor) Refine ADR-003 with the structural HOW for the OdooEditor migration (P16): - Keep viin_web_editor as the neutral propagation base (patch HtmlField/ Wysiwyg once, driven by global registries) - it is the keystone that lets Brain and AI editor plugins depend on it without coupling to each other (AGENTS.md cam ky #2). Deleting it was considered and rejected. - Move the AI block out of viin_brain into a new AI-cluster module viin_ai_editor (generic, works in any html field); bridge viin_ai_brain wires AI output to Brain attribution + resolves Brain x AI conflicts. - Grounded in verified Odoo 17 web_editor seams (options.plugins, _getPowerboxOptions, HtmlField.wysiwygOptions, no native NodeView, setupCollaboration, '/'-only input rule). Tracker odooeditor-migration-progress.md re-organised from 23-features-in-Brain to module-split + a 4-phase wave plan (framework -> plugins -> bridge -> verify+visual) with odoo-ui-reviewer visual gates. Status Proposed - awaiting sign-off before execution. Killed Not finished
open [DOC] Reconcile ADR-002 CRDT with shipped interim relay (OT-final after P16) [DOC] docs: reconcile ADR-002 CRDT with shipped interim relay Recon + Odoo native test (TestCollabStepBuffer/Controller + TestBrainRealtimeCollab -> 0 failed of 15) cho thấy CRDT thực tế đã ship ~90% qua launch-hardening (WI-9 snapshot-relay + WI-12 presence + WI-13 flag), KHÔNG phải 'sắp làm W7-W11' như roadmap/ADR-002 cũ. - ADR-002: thêm §Implementation Status — interim snapshot-relay đã ship + verified; Decision (Option 1 OdooEditor native OT) GIỮ NGUYÊN làm đích; OT-final re-sequenced SAU P16 (cần PageEditor wrap OdooEditor). Ghi rõ gap: relay step.html_snapshot, chưa gọi setupCollaboration, chưa set collaborative=true. - roadmap + brain/README: phản ánh interim DONE sớm, OT-final sau P16. Docs-only; không đụng code. Khớp quyết định: giữ mục tiêu OT thật. Killed