Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions frontend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,10 +288,12 @@ class AppController {
text: p.text || p.message,
source: p.source_employee || 'system',
});
// Route to terminal — 1-on-1 or EA chat path
} else if (this._currentConvId === p.conv_id && this._ceoTerm && (this._currentConvType === 'oneonone' || this._currentConvType === 'ea_chat')) {
// Route to terminal — 1-on-1, EA chat, or product planning path
} else if (this._currentConvId === p.conv_id && this._ceoTerm && (this._currentConvType === 'oneonone' || this._currentConvType === 'ea_chat' || this._currentConvType === 'product')) {
if (p.sender !== 'ceo' && p.text != null) {
const source = this._currentConvType === 'ea_chat'
// Product planning conversations are with the EA — label them
// the same as ea_chat. 1-on-1s show the employee's nickname.
const source = (this._currentConvType === 'ea_chat' || this._currentConvType === 'product')
? '玲珑阁 (EA)'
: this._resolveEmployeeNickname(p.employee_id || this._currentConvEmployeeId || '');
this._ceoTerm.appendMessage({
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@1mancompany/onemancompany",
"version": "0.7.85",
"version": "0.7.86",
"description": "The AI Operating System for One-Person Companies",
"bin": {
"onemancompany": "bin/cli.js"
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "onemancompany"
version = "0.7.85"
version = "0.7.86"
description = "A one-man company simulation with pixel art visualization and LangChain AI agents"
requires-python = ">=3.12"
dependencies = [
Expand Down
83 changes: 83 additions & 0 deletions tests/frontend/test_conversation_routing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env node
/* Regression test for the frontend's conversation_message router.
*
* Bug: PRODUCT planning conversations are opened in the CEO terminal
* (frontend/app.js:_openProductPlanningConversation sets
* _currentConvType = 'product'), but the conversation_message handler's
* terminal-routing branch only matched _currentConvType === 'oneonone' ||
* 'ea_chat'. EA replies for product planning thus arrived via WebSocket
* but were never rendered — they only showed up in server logs because
* debug logging captured the LLM output text.
*
* Two assertions:
* 1. Source-level: the routing filter in app.js includes 'product'.
* 2. Behavioral: a small reimplementation of the routing predicate
* returns true for {convType: 'product', conv_id matches}.
*/

const fs = require("fs");
const path = require("path");

const appJsPath = path.resolve(__dirname, "..", "..", "frontend", "app.js");
const appJsSrc = fs.readFileSync(appJsPath, "utf-8");

let failures = 0;
function assert(cond, msg) {
if (cond) console.log(` ok ${msg}`);
else {
failures += 1;
console.log(` FAIL ${msg}`);
}
}

// ── Source-level invariant ────────────────────────────────────────────────
// The terminal-routing branch of the conversation_message handler must
// include 'product' alongside 'oneonone' and 'ea_chat'. We grep the
// production source to ensure the fix isn't accidentally reverted.
const filterPattern =
/this\._currentConvType\s*===\s*'oneonone'\s*\|\|\s*this\._currentConvType\s*===\s*'ea_chat'\s*\|\|\s*this\._currentConvType\s*===\s*'product'/;
assert(
filterPattern.test(appJsSrc),
"conversation_message router includes 'product' in the terminal-routing filter",
);

// ── Behavioral mirror ─────────────────────────────────────────────────────
// Re-implement the predicate so a unit test would have caught the bug
// before it shipped. If the production filter ever drifts, the
// source-level assertion above will fail; if our predicate drifts, the
// behavioral cases below will fail.
function shouldRouteToTerminal(currentConvId, currentConvType, messageConvId) {
if (currentConvId !== messageConvId) return false;
return (
currentConvType === "oneonone" ||
currentConvType === "ea_chat" ||
currentConvType === "product"
);
}

assert(
shouldRouteToTerminal("c-1", "product", "c-1") === true,
"product planning replies route to the terminal when the conv is open",
);
assert(
shouldRouteToTerminal("c-1", "oneonone", "c-1") === true,
"1-on-1 replies still route to the terminal",
);
assert(
shouldRouteToTerminal("c-1", "ea_chat", "c-1") === true,
"EA chat replies still route to the terminal",
);
assert(
shouldRouteToTerminal("c-1", "product", "c-2") === false,
"messages for a different conv_id do NOT route to the terminal",
);
assert(
shouldRouteToTerminal("c-1", "ceo_inbox", "c-1") === true ? false : true,
"ceo_inbox conversations do NOT route to terminal (they use chatPanel)",
);

if (failures) {
console.log(`\n${failures} failed`);
process.exit(1);
}
console.log("\nall tests passed");
32 changes: 32 additions & 0 deletions tests/integration/test_frontend_conversation_routing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Pytest wrapper for the node-based frontend conversation-routing test.

Regression guard: PRODUCT planning conversations were not routed to the
CEO terminal because the conversation_message handler only matched
'oneonone' / 'ea_chat'. EA replies arrived via WebSocket but never
rendered. The accompanying node test asserts both the source-level
filter and a behavioral mirror of the routing predicate.
"""

from __future__ import annotations

import shutil
import subprocess
from pathlib import Path

import pytest

REPO_ROOT = Path(__file__).resolve().parents[2]
TEST_SCRIPT = REPO_ROOT / "tests" / "frontend" / "test_conversation_routing.js"


@pytest.mark.skipif(shutil.which("node") is None, reason="node not installed")
def test_frontend_conversation_routing() -> None:
result = subprocess.run(
["node", str(TEST_SCRIPT)],
cwd=REPO_ROOT,
capture_output=True,
text=True,
)
assert result.returncode == 0, (
f"frontend routing test failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
)
Loading