agent-nuvira 2.7.0 → 2.7.2

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 (114) hide show
  1. package/dist/agents/orchestrator.d.ts.map +1 -1
  2. package/dist/agents/orchestrator.js +17 -3
  3. package/dist/agents/orchestrator.js.map +1 -1
  4. package/dist/app.js +26 -0
  5. package/dist/cli/chat.d.ts.map +1 -1
  6. package/dist/cli/chat.js +108 -2
  7. package/dist/cli/chat.js.map +1 -1
  8. package/dist/config/auth.js +18 -0
  9. package/dist/config/jwt.js +24 -0
  10. package/dist/config/keys.js +14 -0
  11. package/dist/gateway/chat-store.d.ts +32 -5
  12. package/dist/gateway/chat-store.d.ts.map +1 -1
  13. package/dist/gateway/chat-store.js +52 -6
  14. package/dist/gateway/chat-store.js.map +1 -1
  15. package/dist/gateway/registry.d.ts.map +1 -1
  16. package/dist/gateway/registry.js +54 -10
  17. package/dist/gateway/registry.js.map +1 -1
  18. package/dist/inference/model-validator.d.ts.map +1 -1
  19. package/dist/inference/model-validator.js +11 -1
  20. package/dist/inference/model-validator.js.map +1 -1
  21. package/dist/inference/tool-call-utils.d.ts +31 -0
  22. package/dist/inference/tool-call-utils.d.ts.map +1 -1
  23. package/dist/inference/tool-call-utils.js +41 -0
  24. package/dist/inference/tool-call-utils.js.map +1 -1
  25. package/dist/learning/reasoning-trace.d.ts +5 -3
  26. package/dist/learning/reasoning-trace.d.ts.map +1 -1
  27. package/dist/learning/reasoning-trace.js +2 -2
  28. package/dist/learning/reasoning-trace.js.map +1 -1
  29. package/dist/middleware/auth.js +30 -0
  30. package/dist/nlu/conversation-gate.js +1 -1
  31. package/dist/nlu/conversation-gate.js.map +1 -1
  32. package/dist/nlu/entities.d.ts.map +1 -1
  33. package/dist/nlu/entities.js +12 -0
  34. package/dist/nlu/entities.js.map +1 -1
  35. package/dist/nlu/intent.d.ts.map +1 -1
  36. package/dist/nlu/intent.js +83 -15
  37. package/dist/nlu/intent.js.map +1 -1
  38. package/dist/passport.js +34 -0
  39. package/dist/routes/auth.js +42 -0
  40. package/dist/routes/user.js +31 -0
  41. package/dist/server.js +17 -0
  42. package/dist/tools/pipeline-tool.d.ts.map +1 -1
  43. package/dist/tools/pipeline-tool.js +14 -0
  44. package/dist/tools/pipeline-tool.js.map +1 -1
  45. package/dist/tools/release-sync.d.ts +52 -16
  46. package/dist/tools/release-sync.d.ts.map +1 -1
  47. package/dist/tools/release-sync.js +143 -79
  48. package/dist/tools/release-sync.js.map +1 -1
  49. package/dist/web-dashboard/src/App.js +85 -0
  50. package/dist/web-dashboard/src/admin-auth.test.js +186 -0
  51. package/dist/web-dashboard/src/ansi.js +23 -0
  52. package/dist/web-dashboard/src/ansi.test.js +31 -0
  53. package/dist/web-dashboard/src/api-admin-auth.test.js +172 -0
  54. package/dist/web-dashboard/src/api-admin.test.js +65 -0
  55. package/dist/web-dashboard/src/api-hub.test.js +117 -0
  56. package/dist/web-dashboard/src/api.js +1421 -0
  57. package/dist/web-dashboard/src/api.test.js +51 -0
  58. package/dist/web-dashboard/src/artifacts.js +128 -0
  59. package/dist/web-dashboard/src/artifacts.test.js +139 -0
  60. package/dist/web-dashboard/src/components/AdminPanel.js +567 -0
  61. package/dist/web-dashboard/src/components/AdminPanel.test.js +288 -0
  62. package/dist/web-dashboard/src/components/AgentHub.js +1580 -0
  63. package/dist/web-dashboard/src/components/AgentHub.test.js +343 -0
  64. package/dist/web-dashboard/src/components/BedrockOnboarding.js +320 -0
  65. package/dist/web-dashboard/src/components/BenchmarkCharts.js +228 -0
  66. package/dist/web-dashboard/src/components/ChatPage.js +1586 -0
  67. package/dist/web-dashboard/src/components/ChatPage.test.js +899 -0
  68. package/dist/web-dashboard/src/components/ContactsPage.js +141 -0
  69. package/dist/web-dashboard/src/components/CostDashboard.js +105 -0
  70. package/dist/web-dashboard/src/components/DAGView.js +477 -0
  71. package/dist/web-dashboard/src/components/DAGView.test.js +147 -0
  72. package/dist/web-dashboard/src/components/EnvVarEditor.js +138 -0
  73. package/dist/web-dashboard/src/components/EvalsPage.js +73 -0
  74. package/dist/web-dashboard/src/components/EvalsPage.test.js +120 -0
  75. package/dist/web-dashboard/src/components/ExecutionHistory.js +201 -0
  76. package/dist/web-dashboard/src/components/GatewayPage.js +40 -0
  77. package/dist/web-dashboard/src/components/GatewayPage.test.js +74 -0
  78. package/dist/web-dashboard/src/components/HealthPanel.js +68 -0
  79. package/dist/web-dashboard/src/components/HistoryBrowser.js +34 -0
  80. package/dist/web-dashboard/src/components/Layout.js +50 -0
  81. package/dist/web-dashboard/src/components/Markdown.js +152 -0
  82. package/dist/web-dashboard/src/components/Markdown.test.js +126 -0
  83. package/dist/web-dashboard/src/components/MarkdownZeroDep.js +237 -0
  84. package/dist/web-dashboard/src/components/MemoryPanel.js +81 -0
  85. package/dist/web-dashboard/src/components/ModelTimeline.js +298 -0
  86. package/dist/web-dashboard/src/components/ModelsPanel.js +1484 -0
  87. package/dist/web-dashboard/src/components/ModelsPanel.test.js +460 -0
  88. package/dist/web-dashboard/src/components/Overview.js +123 -0
  89. package/dist/web-dashboard/src/components/PhaseTimeline.js +359 -0
  90. package/dist/web-dashboard/src/components/PhaseTimeline.test.js +234 -0
  91. package/dist/web-dashboard/src/components/PlatformConfigSection.js +384 -0
  92. package/dist/web-dashboard/src/components/PlatformConfigSection.test.js +106 -0
  93. package/dist/web-dashboard/src/components/PlatformsPage.js +250 -0
  94. package/dist/web-dashboard/src/components/QuotaPanel.js +159 -0
  95. package/dist/web-dashboard/src/components/QuotaPanel.test.js +85 -0
  96. package/dist/web-dashboard/src/components/RequestsPanel.js +229 -0
  97. package/dist/web-dashboard/src/components/RequestsPanel.test.js +103 -0
  98. package/dist/web-dashboard/src/components/RoutingInsightsPanel.js +938 -0
  99. package/dist/web-dashboard/src/components/RoutingInsightsPanel.test.js +339 -0
  100. package/dist/web-dashboard/src/components/RoutingWalkthrough.js +408 -0
  101. package/dist/web-dashboard/src/components/RoutingWalkthrough.test.js +209 -0
  102. package/dist/web-dashboard/src/components/TaskConsole.js +119 -0
  103. package/dist/web-dashboard/src/components/TaskConsole.test.js +123 -0
  104. package/dist/web-dashboard/src/components/TasksPage.js +256 -0
  105. package/dist/web-dashboard/src/components/TasksPage.test.js +103 -0
  106. package/dist/web-dashboard/src/components/TracePanel.js +306 -0
  107. package/dist/web-dashboard/src/components/WhatsAppPanel.js +195 -0
  108. package/dist/web-dashboard/src/components/WhatsAppPanel.test.js +122 -0
  109. package/dist/web-dashboard/src/jsonOrNull.js +37 -0
  110. package/dist/web-dashboard/src/main.js +15 -0
  111. package/dist/web-dashboard/src/mask.js +11 -0
  112. package/dist/web-dashboard/vite.config.js +23 -0
  113. package/dist/web-dashboard/vitest.config.js +15 -0
  114. package/package.json +5 -3
@@ -0,0 +1,288 @@
1
+ "use strict";
2
+ /**
3
+ * Admin panel tests (Sessions 17 + 18).
4
+ *
5
+ * Session 17: the command-runner (doctor checks + masked provider table).
6
+ * Session 18: the user-id + password control layer — the panel now gates the
7
+ * write surface behind setup/login, and the provider table is editable
8
+ * (save/test/remove via the same ConfigManager the CLI writes through).
9
+ * These tests cover the auth flows, the editor actions, and the stale-server
10
+ * degradation (null → friendly error, never a crash).
11
+ */
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ const vitest_1 = require("vitest");
17
+ const react_1 = require("@testing-library/react");
18
+ const AdminPanel_1 = __importDefault(require("./AdminPanel"));
19
+ const api_1 = require("../api");
20
+ const VALID_PAYLOAD = {
21
+ system: [
22
+ { name: 'Config Directory', status: 'pass', message: '~/.buff/ exists', detail: '/Users/tester/.buff', fix: undefined },
23
+ { name: 'Connectivity', status: 'warn', message: 'Slow probe', detail: '312ms to groq', fix: 'Check network' },
24
+ { name: 'Docker', status: 'fail', message: 'not running', detail: undefined, fix: 'Start Docker Desktop' },
25
+ ],
26
+ enterprise: [
27
+ { name: 'Secrets Backend', status: 'pass', message: 'keyring available' },
28
+ { name: 'Audit Chain', status: 'warn', message: '2 files, 0 gaps', fix: undefined },
29
+ ],
30
+ providers: [
31
+ { type: 'groq', configured: true, keySource: 'env', keyMasked: 'gsk_…abcd', model: 'llama-3.3-70b', baseUrl: 'https://api.groq.com/openai/v1' },
32
+ { type: 'nim', configured: false, keySource: 'none', keyMasked: null, model: undefined, baseUrl: undefined },
33
+ ],
34
+ serverTime: 123,
35
+ };
36
+ const CATALOG = [
37
+ { id: 'groq', label: 'Groq', icon: '🟢', envVar: 'GROQ_API_KEY', keyless: false },
38
+ { id: 'gemini', label: 'Google Gemini', icon: '🔷', envVar: 'GEMINI_API_KEY', keyless: false },
39
+ ];
40
+ /** Authed status — the panel reaches the editor. */
41
+ function mockAuthedStatus(role = 'admin') {
42
+ return vitest_1.vi.spyOn(api_1.dashboardAPI, 'fetchAdminAuthStatus').mockResolvedValue({ configured: true, authenticated: true, user: 'admin', role });
43
+ }
44
+ /** The editor's data endpoints (checks + catalog) — NEVER touches the status mock. */
45
+ function mockServerData(payload = VALID_PAYLOAD) {
46
+ const checks = vitest_1.vi.spyOn(api_1.dashboardAPI, 'fetchAdminChecks').mockResolvedValue(payload);
47
+ const catalog = vitest_1.vi.spyOn(api_1.dashboardAPI, 'fetchAdminCatalog').mockResolvedValue(CATALOG);
48
+ return { checks, catalog };
49
+ }
50
+ /** Full authed server: status + data. */
51
+ function mockAuthedServer(payload = VALID_PAYLOAD) {
52
+ const status = mockAuthedStatus();
53
+ const { checks, catalog } = mockServerData(payload);
54
+ return { status, checks, catalog };
55
+ }
56
+ (0, vitest_1.describe)('AdminPanel', () => {
57
+ (0, vitest_1.afterEach)(() => {
58
+ (0, react_1.cleanup)();
59
+ vitest_1.vi.restoreAllMocks();
60
+ (0, api_1.setAdminToken)(null);
61
+ });
62
+ // ─── Auth gate (Session 18) ─────────────────────────────────────────────
63
+ (0, vitest_1.it)('shows the SETUP form when no admin credential exists', async () => {
64
+ vitest_1.vi.spyOn(api_1.dashboardAPI, 'fetchAdminAuthStatus').mockResolvedValue({ configured: false, authenticated: false, user: null });
65
+ (0, react_1.render)(<AdminPanel_1.default />);
66
+ (0, vitest_1.expect)(await react_1.screen.findByText(/Set up access/)).toBeTruthy();
67
+ (0, vitest_1.expect)(react_1.screen.getByPlaceholderText('admin')).toBeTruthy();
68
+ (0, vitest_1.expect)(react_1.screen.getAllByPlaceholderText('••••••••')).toHaveLength(2); // password + confirm
69
+ });
70
+ (0, vitest_1.it)('setup submits the user-id + password (and validates the confirm match)', async () => {
71
+ vitest_1.vi.spyOn(api_1.dashboardAPI, 'fetchAdminAuthStatus').mockResolvedValue({ configured: false, authenticated: false, user: null });
72
+ const setup = vitest_1.vi.spyOn(api_1.dashboardAPI, 'adminSetup').mockResolvedValue({ ok: true, user: 'admin', token: 'tok' });
73
+ mockServerData(); // post-setup fetchAdminChecks/catalog
74
+ (0, react_1.render)(<AdminPanel_1.default />);
75
+ await react_1.screen.findByText(/Set up access/);
76
+ react_1.fireEvent.change(react_1.screen.getByPlaceholderText('admin'), { target: { value: 'admin' } });
77
+ const [pw, confirm] = react_1.screen.getAllByPlaceholderText('••••••••');
78
+ react_1.fireEvent.change(pw, { target: { value: 'long-pass-1' } });
79
+ react_1.fireEvent.change(confirm, { target: { value: 'long-pass-1' } });
80
+ react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /Create admin/ }));
81
+ await (0, react_1.waitFor)(() => (0, vitest_1.expect)(setup).toHaveBeenCalledWith('admin', 'long-pass-1'));
82
+ // Authed → editor renders.
83
+ await react_1.screen.findByText(/System Checks/);
84
+ });
85
+ (0, vitest_1.it)('rejects a mismatched confirm password without calling setup', async () => {
86
+ vitest_1.vi.spyOn(api_1.dashboardAPI, 'fetchAdminAuthStatus').mockResolvedValue({ configured: false, authenticated: false, user: null });
87
+ const setup = vitest_1.vi.spyOn(api_1.dashboardAPI, 'adminSetup');
88
+ (0, react_1.render)(<AdminPanel_1.default />);
89
+ await react_1.screen.findByText(/Set up access/);
90
+ react_1.fireEvent.change(react_1.screen.getByPlaceholderText('admin'), { target: { value: 'admin' } });
91
+ const [pw, confirm] = react_1.screen.getAllByPlaceholderText('••••••••');
92
+ react_1.fireEvent.change(pw, { target: { value: 'long-pass-1' } });
93
+ react_1.fireEvent.change(confirm, { target: { value: 'different-pass' } });
94
+ react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /Create admin/ }));
95
+ (0, vitest_1.expect)(await react_1.screen.findByText(/Passwords do not match/)).toBeTruthy();
96
+ (0, vitest_1.expect)(setup).not.toHaveBeenCalled();
97
+ });
98
+ (0, vitest_1.it)('shows the LOGIN form when configured but not authenticated', async () => {
99
+ vitest_1.vi.spyOn(api_1.dashboardAPI, 'fetchAdminAuthStatus').mockResolvedValue({ configured: true, authenticated: false, user: null });
100
+ (0, react_1.render)(<AdminPanel_1.default />);
101
+ (0, vitest_1.expect)(await react_1.screen.findByRole('button', { name: /Log in/ })).toBeTruthy();
102
+ (0, vitest_1.expect)(react_1.screen.getAllByPlaceholderText('••••••••')).toHaveLength(1);
103
+ });
104
+ (0, vitest_1.it)('login succeeds and reaches the editor', async () => {
105
+ vitest_1.vi.spyOn(api_1.dashboardAPI, 'fetchAdminAuthStatus').mockResolvedValue({ configured: true, authenticated: false, user: null });
106
+ const login = vitest_1.vi.spyOn(api_1.dashboardAPI, 'adminLogin').mockResolvedValue({ ok: true, user: 'admin', token: 'tok' });
107
+ mockServerData();
108
+ (0, react_1.render)(<AdminPanel_1.default />);
109
+ await react_1.screen.findByRole('button', { name: /Log in/ });
110
+ react_1.fireEvent.change(react_1.screen.getByPlaceholderText('admin'), { target: { value: 'admin' } });
111
+ react_1.fireEvent.change(react_1.screen.getByPlaceholderText('••••••••'), { target: { value: 'secret-pass' } });
112
+ react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /Log in/ }));
113
+ await (0, react_1.waitFor)(() => (0, vitest_1.expect)(login).toHaveBeenCalledWith('admin', 'secret-pass'));
114
+ await react_1.screen.findByText(/System Checks/);
115
+ });
116
+ (0, vitest_1.it)('login shows the server error on a bad password', async () => {
117
+ vitest_1.vi.spyOn(api_1.dashboardAPI, 'fetchAdminAuthStatus').mockResolvedValue({ configured: true, authenticated: false, user: null });
118
+ vitest_1.vi.spyOn(api_1.dashboardAPI, 'adminLogin').mockResolvedValue({ ok: false, error: 'Invalid username or password.', unauthorized: true });
119
+ (0, react_1.render)(<AdminPanel_1.default />);
120
+ await react_1.screen.findByRole('button', { name: /Log in/ });
121
+ react_1.fireEvent.change(react_1.screen.getByPlaceholderText('admin'), { target: { value: 'admin' } });
122
+ react_1.fireEvent.change(react_1.screen.getByPlaceholderText('••••••••'), { target: { value: 'wrong' } });
123
+ react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /Log in/ }));
124
+ (0, vitest_1.expect)(await react_1.screen.findByText(/Invalid username or password/)).toBeTruthy();
125
+ });
126
+ (0, vitest_1.it)('degrades to a friendly error when the auth-status fetch fails (stale server) — no crash', async () => {
127
+ vitest_1.vi.spyOn(api_1.dashboardAPI, 'fetchAdminAuthStatus').mockResolvedValue(null);
128
+ (0, react_1.render)(<AdminPanel_1.default />);
129
+ (0, vitest_1.expect)(await react_1.screen.findByText(/Could not reach the dashboard server/i)).toBeTruthy();
130
+ });
131
+ // ─── Command-runner (Session 17, authed) ────────────────────────────────
132
+ (0, vitest_1.it)('renders check rows with pass/warn/fail badges and the provider table (keys masked)', async () => {
133
+ mockAuthedServer();
134
+ (0, react_1.render)(<AdminPanel_1.default />);
135
+ (0, vitest_1.expect)(await react_1.screen.findAllByText('2')).toHaveLength(3); // passing + warnings + providers
136
+ (0, vitest_1.expect)(react_1.screen.getByText('1')).toBeTruthy(); // failing count
137
+ (0, vitest_1.expect)(react_1.screen.getByText(/System Checks/)).toBeTruthy();
138
+ (0, vitest_1.expect)(react_1.screen.getByText(/Enterprise Self-Check/)).toBeTruthy();
139
+ (0, vitest_1.expect)(react_1.screen.getByText(/Provider Configuration/)).toBeTruthy();
140
+ (0, vitest_1.expect)(react_1.screen.getAllByText(/✅ PASS/)).toHaveLength(2);
141
+ (0, vitest_1.expect)(react_1.screen.getAllByText(/⚠️ WARN/)).toHaveLength(2);
142
+ (0, vitest_1.expect)(react_1.screen.getAllByText(/❌ FAIL/)).toHaveLength(1);
143
+ (0, vitest_1.expect)(react_1.screen.getByText(/💡 Start Docker Desktop/)).toBeTruthy();
144
+ // Provider table: masked key (input placeholder) + source label — the real
145
+ // key never appears anywhere.
146
+ (0, vitest_1.expect)(react_1.screen.getByPlaceholderText('gsk_…abcd')).toBeTruthy();
147
+ (0, vitest_1.expect)(react_1.screen.getByText(/Environment/)).toBeTruthy(); // 'groq · Environment'
148
+ (0, vitest_1.expect)(react_1.screen.getAllByText(/Not configured/)).toHaveLength(2); // nim badge + table cell
149
+ (0, vitest_1.expect)(react_1.screen.queryByText(/gsk_[A-Za-z0-9]{10,}/)).toBeNull();
150
+ });
151
+ (0, vitest_1.it)('re-runs all checks when Refresh is clicked', async () => {
152
+ const { checks } = mockAuthedServer();
153
+ (0, react_1.render)(<AdminPanel_1.default />);
154
+ await react_1.screen.findByText(/System Checks/);
155
+ react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /Refresh \(run all commands\)/ }));
156
+ await (0, react_1.waitFor)(() => (0, vitest_1.expect)(checks).toHaveBeenCalledTimes(2));
157
+ await react_1.screen.findByRole('button', { name: /Refresh \(run all commands\)/ });
158
+ });
159
+ (0, vitest_1.it)('shows empty states when no providers / no enterprise checks', async () => {
160
+ mockAuthedServer({ ...VALID_PAYLOAD, providers: [], enterprise: [] });
161
+ (0, react_1.render)(<AdminPanel_1.default />);
162
+ (0, vitest_1.expect)(await react_1.screen.findByText(/No providers configured yet/)).toBeTruthy();
163
+ (0, vitest_1.expect)(react_1.screen.getByText(/No enterprise checks returned/)).toBeTruthy();
164
+ });
165
+ // ─── Provider editor (Session 18, authed) ───────────────────────────────
166
+ (0, vitest_1.it)('saves an edited provider (key/baseUrl/model) and shows the refreshed row', async () => {
167
+ mockAuthedServer();
168
+ const save = vitest_1.vi.spyOn(api_1.dashboardAPI, 'saveProvider').mockResolvedValue({
169
+ ok: true,
170
+ provider: { ...VALID_PAYLOAD.providers[0], keyMasked: 'gsk_…wxyz' },
171
+ });
172
+ (0, react_1.render)(<AdminPanel_1.default />);
173
+ await react_1.screen.findByText(/System Checks/);
174
+ // Edit groq's row via its stable aria-labels: new key + model (baseUrl untouched).
175
+ react_1.fireEvent.change(react_1.screen.getByLabelText('groq API key'), { target: { value: 'gsk_new-secret-key' } });
176
+ react_1.fireEvent.change(react_1.screen.getByLabelText('groq model'), { target: { value: 'llama-4' } });
177
+ react_1.fireEvent.click(react_1.screen.getAllByRole('button', { name: /💾 Save/ })[0]);
178
+ await (0, react_1.waitFor)(() => (0, vitest_1.expect)(save).toHaveBeenCalledWith('groq', { apiKey: 'gsk_new-secret-key', model: 'llama-4' }));
179
+ (0, vitest_1.expect)(await react_1.screen.findByText('✅ Saved')).toBeTruthy();
180
+ (0, vitest_1.expect)(react_1.screen.getByPlaceholderText('gsk_…wxyz')).toBeTruthy(); // refreshed masked row
181
+ });
182
+ (0, vitest_1.it)('tests a provider and shows the model count', async () => {
183
+ mockAuthedServer();
184
+ vitest_1.vi.spyOn(api_1.dashboardAPI, 'testProvider').mockResolvedValue({ ok: true, models: ['llama-3.3-70b', 'mixtral'] });
185
+ (0, react_1.render)(<AdminPanel_1.default />);
186
+ await react_1.screen.findByText(/System Checks/);
187
+ react_1.fireEvent.click(react_1.screen.getAllByRole('button', { name: /🔌 Test/ })[0]);
188
+ (0, vitest_1.expect)(await react_1.screen.findByText(/✅ Connected — 2 model/)).toBeTruthy();
189
+ });
190
+ (0, vitest_1.it)('removes a provider after confirm', async () => {
191
+ mockAuthedServer();
192
+ const del = vitest_1.vi.spyOn(api_1.dashboardAPI, 'deleteProvider').mockResolvedValue({
193
+ ok: true,
194
+ cleared: true,
195
+ provider: { ...VALID_PAYLOAD.providers[0], configured: false, keySource: 'none', keyMasked: null },
196
+ });
197
+ vitest_1.vi.spyOn(window, 'confirm').mockReturnValue(true);
198
+ (0, react_1.render)(<AdminPanel_1.default />);
199
+ await react_1.screen.findByText(/System Checks/);
200
+ react_1.fireEvent.click(react_1.screen.getAllByRole('button', { name: /🗑 Remove/ })[0]);
201
+ await (0, react_1.waitFor)(() => (0, vitest_1.expect)(del).toHaveBeenCalledWith('groq'));
202
+ // The removed row (with its masked-key placeholder) disappears.
203
+ await (0, react_1.waitFor)(() => (0, vitest_1.expect)(react_1.screen.queryByPlaceholderText('gsk_…abcd')).toBeNull());
204
+ });
205
+ (0, vitest_1.it)('shows the session-expired gate when a write returns 401', async () => {
206
+ mockAuthedServer();
207
+ vitest_1.vi.spyOn(api_1.dashboardAPI, 'saveProvider').mockResolvedValue({ ok: false, error: 'Not authenticated — log in first.', unauthorized: true });
208
+ (0, react_1.render)(<AdminPanel_1.default />);
209
+ await react_1.screen.findByText(/System Checks/);
210
+ react_1.fireEvent.click(react_1.screen.getAllByRole('button', { name: /💾 Save/ })[0]);
211
+ (0, vitest_1.expect)(await react_1.screen.findByText(/Session expired — log in again/)).toBeTruthy();
212
+ });
213
+ // ─── RBAC roles (Session 19) ────────────────────────────────────────────
214
+ (0, vitest_1.it)('shows the role badge and hides the editor for a VIEWER session (read-only note)', async () => {
215
+ mockAuthedServer();
216
+ // Override the status with a viewer role (authed but read-only).
217
+ vitest_1.vi.spyOn(api_1.dashboardAPI, 'fetchAdminAuthStatus').mockResolvedValue({ configured: true, authenticated: true, user: 'view', role: 'viewer' });
218
+ (0, react_1.render)(<AdminPanel_1.default />);
219
+ await react_1.screen.findByText(/System Checks/);
220
+ // Role badge + read-only note.
221
+ (0, vitest_1.expect)(react_1.screen.getByText(/view · viewer/)).toBeTruthy();
222
+ (0, vitest_1.expect)(react_1.screen.getByText(/read-only here/)).toBeTruthy();
223
+ // No editor controls, no user management.
224
+ (0, vitest_1.expect)(react_1.screen.queryByRole('button', { name: /💾 Save/ })).toBeNull();
225
+ (0, vitest_1.expect)(react_1.screen.queryByRole('button', { name: /🗑 Remove/ })).toBeNull();
226
+ (0, vitest_1.expect)(react_1.screen.queryByRole('button', { name: /➕ Add/ })).toBeNull();
227
+ (0, vitest_1.expect)(react_1.screen.queryByText(/Dashboard Users/)).toBeNull();
228
+ // Read-only masked value still renders.
229
+ (0, vitest_1.expect)(react_1.screen.getByText('gsk_…abcd')).toBeTruthy();
230
+ });
231
+ (0, vitest_1.it)('admin can add a dashboard user with a role', async () => {
232
+ mockAuthedServer();
233
+ vitest_1.vi.spyOn(api_1.dashboardAPI, 'fetchAdminUsers').mockResolvedValue({ ok: true, users: [{ user: 'admin', role: 'admin', createdAt: 1 }] });
234
+ const add = vitest_1.vi.spyOn(api_1.dashboardAPI, 'addAdminUser').mockResolvedValue({ ok: true });
235
+ (0, react_1.render)(<AdminPanel_1.default />);
236
+ await react_1.screen.findByText(/Dashboard Users/);
237
+ react_1.fireEvent.change(react_1.screen.getByLabelText('Username'), { target: { value: 'ops' } });
238
+ react_1.fireEvent.change(react_1.screen.getByLabelText(/Password \(min 8 chars\)/), { target: { value: 'ops-pass-123' } });
239
+ react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /Add user/ }));
240
+ await (0, react_1.waitFor)(() => (0, vitest_1.expect)(add).toHaveBeenCalledWith('ops', 'ops-pass-123', 'viewer'));
241
+ (0, vitest_1.expect)(await react_1.screen.findByText('✅ User added')).toBeTruthy();
242
+ });
243
+ (0, vitest_1.it)('admin can remove another dashboard user (not self)', async () => {
244
+ mockAuthedServer();
245
+ vitest_1.vi.spyOn(api_1.dashboardAPI, 'fetchAdminUsers').mockResolvedValue({
246
+ ok: true,
247
+ users: [
248
+ { user: 'admin', role: 'admin', createdAt: 1 },
249
+ { user: 'ops', role: 'operator', createdAt: 2 },
250
+ ],
251
+ });
252
+ const remove = vitest_1.vi.spyOn(api_1.dashboardAPI, 'removeAdminUser').mockResolvedValue({ ok: true, removed: true });
253
+ vitest_1.vi.spyOn(window, 'confirm').mockReturnValue(true);
254
+ (0, react_1.render)(<AdminPanel_1.default />);
255
+ await react_1.screen.findByText(/Dashboard Users/);
256
+ // 'you' marker on the own user; remove button only on the other.
257
+ (0, vitest_1.expect)(react_1.screen.getByText('you')).toBeTruthy();
258
+ react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /🗑 Remove user/ }));
259
+ await (0, react_1.waitFor)(() => (0, vitest_1.expect)(remove).toHaveBeenCalledWith('ops'));
260
+ (0, vitest_1.expect)(await react_1.screen.findByText(/✅ Removed ops/)).toBeTruthy();
261
+ });
262
+ (0, vitest_1.it)('warns instead of hiding the row when removing an ENV-sourced key (env re-injects)', async () => {
263
+ mockAuthedServer();
264
+ vitest_1.vi.spyOn(api_1.dashboardAPI, 'deleteProvider').mockResolvedValue({
265
+ ok: true,
266
+ cleared: false,
267
+ envSourced: true,
268
+ envVar: 'GROQ_API_KEY',
269
+ provider: { ...VALID_PAYLOAD.providers[0] }, // row stays (still configured via env)
270
+ });
271
+ vitest_1.vi.spyOn(window, 'confirm').mockReturnValue(true);
272
+ (0, react_1.render)(<AdminPanel_1.default />);
273
+ await react_1.screen.findByText(/System Checks/);
274
+ react_1.fireEvent.click(react_1.screen.getAllByRole('button', { name: /🗑 Remove/ })[0]);
275
+ // The row is NOT removed — the env note explains how to actually remove it.
276
+ (0, vitest_1.expect)(await react_1.screen.findByText(/\$GROQ_API_KEY — unset it there/)).toBeTruthy();
277
+ (0, vitest_1.expect)(react_1.screen.getByPlaceholderText('gsk_…abcd')).toBeTruthy();
278
+ });
279
+ (0, vitest_1.it)('logs out back to the login form', async () => {
280
+ mockAuthedServer();
281
+ const logout = vitest_1.vi.spyOn(api_1.dashboardAPI, 'adminLogout').mockResolvedValue(undefined);
282
+ (0, react_1.render)(<AdminPanel_1.default />);
283
+ await react_1.screen.findByText(/System Checks/);
284
+ react_1.fireEvent.click(react_1.screen.getByRole('button', { name: /🚪 Log out/ }));
285
+ await (0, react_1.waitFor)(() => (0, vitest_1.expect)(logout).toHaveBeenCalled());
286
+ (0, vitest_1.expect)(await react_1.screen.findByRole('button', { name: /Log in/ })).toBeTruthy();
287
+ });
288
+ });