@app-vitals/agent-lib 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin-types.ts +4097 -0
- package/claude-timeout.ts +10 -0
- package/claude-timeout.unit.test.ts +8 -0
- package/cli-flags.ts +83 -0
- package/cli-flags.unit.test.ts +137 -0
- package/clone-plan.ts +73 -0
- package/clone-plan.unit.test.ts +69 -0
- package/default-agent-env.ts +15 -0
- package/default-agent-env.unit.test.ts +19 -0
- package/github-login.ts +20 -0
- package/github-login.unit.test.ts +78 -0
- package/graceful-shutdown.ts +143 -0
- package/graceful-shutdown.unit.test.ts +236 -0
- package/org-repo.ts +9 -0
- package/org-repo.unit.test.ts +42 -0
- package/package.json +37 -4
- package/pg-sslmode.ts +66 -0
- package/pg-sslmode.unit.test.ts +74 -0
- package/pool-observability.ts +209 -0
- package/pool-observability.unit.test.ts +233 -0
- package/pricing.ts +98 -0
- package/pricing.unit.test.ts +214 -0
- package/progress-phases.ts +58 -0
- package/progress-phases.unit.test.ts +50 -0
- package/request-context.ts +23 -0
- package/request-context.unit.test.ts +51 -0
- package/secret-env-vars.ts +41 -0
- package/secret-env-vars.unit.test.ts +62 -0
- package/sentry.ts +201 -0
- package/sentry.unit.test.ts +294 -0
- package/task-store-types.ts +3256 -0
- package/task-store-types.unit.test.ts +24 -0
- package/web/toolbar.ts +244 -0
- package/web/toolbar.unit.test.ts +243 -0
- package/README.md +0 -3
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
describe("task-store-types", () => {
|
|
6
|
+
it("should have generated the task-store-types.ts file", () => {
|
|
7
|
+
const filePath = resolve(__dirname, "./task-store-types.ts");
|
|
8
|
+
const content = readFileSync(filePath, "utf-8");
|
|
9
|
+
expect(content).toBeDefined();
|
|
10
|
+
expect(content.length).toBeGreaterThan(0);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should export the paths interface", () => {
|
|
14
|
+
const filePath = resolve(__dirname, "./task-store-types.ts");
|
|
15
|
+
const content = readFileSync(filePath, "utf-8");
|
|
16
|
+
expect(content).toContain("export interface paths");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should include task endpoints from the OpenAPI spec", () => {
|
|
20
|
+
const filePath = resolve(__dirname, "./task-store-types.ts");
|
|
21
|
+
const content = readFileSync(filePath, "utf-8");
|
|
22
|
+
expect(content).toContain('"/tasks"');
|
|
23
|
+
});
|
|
24
|
+
});
|
package/web/toolbar.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/web/toolbar.ts
|
|
3
|
+
* Shared Shipwright toolbar — used by both the metrics dashboard and the admin UI.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ShipwrightToolbarOptions {
|
|
7
|
+
userName: string;
|
|
8
|
+
/** Current request path, used to highlight the active nav link. */
|
|
9
|
+
activePath: string;
|
|
10
|
+
/** Form action for the sign-out button (differs per service). */
|
|
11
|
+
logoutAction: string;
|
|
12
|
+
/** Full URL of the metrics dashboard. Defaults to "/dashboard". */
|
|
13
|
+
metricsUrl?: string;
|
|
14
|
+
/** Base URL of the admin service. Defaults to empty string (same origin). */
|
|
15
|
+
adminBaseUrl?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Public, unauthenticated variant. When true, render only the read-only
|
|
18
|
+
* surfaces (Metrics dashboard + public task board) and no sign-out — the
|
|
19
|
+
* authenticated /admin/* links and logout are omitted, since they 404 (and
|
|
20
|
+
* aren't reachable) on the public proof host.
|
|
21
|
+
*/
|
|
22
|
+
readOnly?: boolean;
|
|
23
|
+
/** Public task board URL, used only in readOnly mode. Defaults to "/public/tasks". */
|
|
24
|
+
tasksUrl?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Render the "Account" nav entry (self-serve accounts, admin service only).
|
|
27
|
+
* Defaults to false so other services sharing this toolbar never show it.
|
|
28
|
+
*/
|
|
29
|
+
showAccount?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function baseStyles(): string {
|
|
33
|
+
return `
|
|
34
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
35
|
+
html { font-size: 16px; -webkit-font-smoothing: antialiased; }
|
|
36
|
+
body {
|
|
37
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
38
|
+
background: #f7f7fb;
|
|
39
|
+
color: #1a1a2e;
|
|
40
|
+
min-height: 100vh;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* ─── Toolbar ───────────────────────────────────────── */
|
|
44
|
+
.vos-toolbar {
|
|
45
|
+
background: #fff;
|
|
46
|
+
border-bottom: 1px solid #e8e8ee;
|
|
47
|
+
height: 52px;
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
padding: 0 24px;
|
|
51
|
+
gap: 24px;
|
|
52
|
+
position: sticky;
|
|
53
|
+
top: 0;
|
|
54
|
+
z-index: 100;
|
|
55
|
+
}
|
|
56
|
+
.vos-wordmark {
|
|
57
|
+
font-size: 15px;
|
|
58
|
+
font-weight: 700;
|
|
59
|
+
color: #1a1a2e;
|
|
60
|
+
text-decoration: none;
|
|
61
|
+
letter-spacing: -0.3px;
|
|
62
|
+
flex-shrink: 0;
|
|
63
|
+
}
|
|
64
|
+
.vos-wordmark:hover { color: #4f46e5; }
|
|
65
|
+
.vos-nav {
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: 4px;
|
|
69
|
+
flex: 1;
|
|
70
|
+
}
|
|
71
|
+
.vos-nav-link {
|
|
72
|
+
padding: 5px 12px;
|
|
73
|
+
border-radius: 6px;
|
|
74
|
+
font-size: 13px;
|
|
75
|
+
font-weight: 500;
|
|
76
|
+
color: #6b7280;
|
|
77
|
+
text-decoration: none;
|
|
78
|
+
transition: background 0.1s, color 0.1s;
|
|
79
|
+
white-space: nowrap;
|
|
80
|
+
}
|
|
81
|
+
.vos-nav-link:hover { background: #f3f4f6; color: #1a1a2e; }
|
|
82
|
+
.vos-nav-link.active {
|
|
83
|
+
background: #eef2ff;
|
|
84
|
+
color: #4f46e5;
|
|
85
|
+
font-weight: 600;
|
|
86
|
+
}
|
|
87
|
+
.vos-user {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: 12px;
|
|
91
|
+
flex-shrink: 0;
|
|
92
|
+
margin-left: auto;
|
|
93
|
+
}
|
|
94
|
+
.vos-username {
|
|
95
|
+
font-size: 13px;
|
|
96
|
+
color: #6b7280;
|
|
97
|
+
white-space: nowrap;
|
|
98
|
+
overflow: hidden;
|
|
99
|
+
text-overflow: ellipsis;
|
|
100
|
+
max-width: 160px;
|
|
101
|
+
}
|
|
102
|
+
.vos-signout-btn {
|
|
103
|
+
font-size: 13px;
|
|
104
|
+
font-weight: 500;
|
|
105
|
+
color: #6b7280;
|
|
106
|
+
background: none;
|
|
107
|
+
border: 1px solid #e8e8ee;
|
|
108
|
+
border-radius: 6px;
|
|
109
|
+
padding: 5px 12px;
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
font-family: inherit;
|
|
112
|
+
transition: background 0.1s, color 0.1s, border-color 0.1s;
|
|
113
|
+
white-space: nowrap;
|
|
114
|
+
}
|
|
115
|
+
.vos-signout-btn:hover { background: #f7f7fb; color: #1a1a2e; border-color: #d1d5db; }
|
|
116
|
+
|
|
117
|
+
/* ─── Page body ─────────────────────────────────────── */
|
|
118
|
+
.vos-page {
|
|
119
|
+
max-width: 960px;
|
|
120
|
+
margin: 0 auto;
|
|
121
|
+
padding: 32px 24px 64px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* ─── Hamburger toggle (CSS-only, no JS) ───────────── */
|
|
125
|
+
.vos-nav-toggle { display: none; }
|
|
126
|
+
.vos-hamburger {
|
|
127
|
+
display: none;
|
|
128
|
+
background: none;
|
|
129
|
+
border: none;
|
|
130
|
+
font-size: 20px;
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
color: #1a1a2e;
|
|
133
|
+
padding: 4px 8px;
|
|
134
|
+
line-height: 1;
|
|
135
|
+
font-family: inherit;
|
|
136
|
+
}
|
|
137
|
+
.vos-hamburger:hover { color: #4f46e5; }
|
|
138
|
+
|
|
139
|
+
@media (max-width: 640px) {
|
|
140
|
+
.vos-toolbar { padding: 0 16px; gap: 12px; flex-wrap: wrap; height: auto; min-height: 52px; align-items: center; }
|
|
141
|
+
.vos-username { display: none; }
|
|
142
|
+
.vos-nav-link { padding: 5px 8px; }
|
|
143
|
+
.vos-page { padding: 20px 16px 48px; }
|
|
144
|
+
|
|
145
|
+
/* Hide nav links by default on mobile */
|
|
146
|
+
.vos-nav {
|
|
147
|
+
display: none;
|
|
148
|
+
flex-direction: column;
|
|
149
|
+
width: 100%;
|
|
150
|
+
gap: 2px;
|
|
151
|
+
padding: 8px 0;
|
|
152
|
+
order: 3;
|
|
153
|
+
}
|
|
154
|
+
/* Show nav links when hamburger checkbox is checked */
|
|
155
|
+
.vos-nav-toggle:checked + nav .vos-nav {
|
|
156
|
+
display: flex;
|
|
157
|
+
}
|
|
158
|
+
.vos-nav-link {
|
|
159
|
+
display: block;
|
|
160
|
+
padding: 10px 12px;
|
|
161
|
+
border-radius: 6px;
|
|
162
|
+
font-size: 14px;
|
|
163
|
+
}
|
|
164
|
+
.vos-nav-link.active {
|
|
165
|
+
background: #eef2ff;
|
|
166
|
+
color: #4f46e5;
|
|
167
|
+
font-weight: 600;
|
|
168
|
+
}
|
|
169
|
+
/* Show hamburger on mobile */
|
|
170
|
+
.vos-hamburger {
|
|
171
|
+
display: block;
|
|
172
|
+
}
|
|
173
|
+
/* Hide user section sign-out on mobile when nav is closed */
|
|
174
|
+
.vos-user {
|
|
175
|
+
order: 2;
|
|
176
|
+
margin-left: 0;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function escapeHtml(str: string): string {
|
|
184
|
+
return str
|
|
185
|
+
.replace(/&/g, "&")
|
|
186
|
+
.replace(/</g, "<")
|
|
187
|
+
.replace(/>/g, ">")
|
|
188
|
+
.replace(/"/g, """)
|
|
189
|
+
.replace(/'/g, "'");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function renderShipwrightToolbar(
|
|
193
|
+
opts: ShipwrightToolbarOptions,
|
|
194
|
+
): string {
|
|
195
|
+
const { userName, activePath, logoutAction } = opts;
|
|
196
|
+
const metricsUrl = opts.metricsUrl ?? "/dashboard";
|
|
197
|
+
const adminBase = opts.adminBaseUrl ?? "";
|
|
198
|
+
const active = (prefix: string) =>
|
|
199
|
+
activePath.startsWith(prefix) ? " active" : "";
|
|
200
|
+
// /admin/agents/:id/queue-activity starts with /admin/agents, so without
|
|
201
|
+
// this special case both "Agents" and "Queue & Activity" would highlight
|
|
202
|
+
// simultaneously on that page. Any path containing "/queue-activity"
|
|
203
|
+
// belongs to the new tab instead.
|
|
204
|
+
const isQueueActivity = activePath.includes("/queue-activity");
|
|
205
|
+
|
|
206
|
+
// Public proof surface: only the read-only Metrics + Tasks views are routed and
|
|
207
|
+
// reachable, so omit every /admin/* link and the sign-out form (they 404 on the
|
|
208
|
+
// public host). The wordmark points at the dashboard instead of /admin/agents.
|
|
209
|
+
if (opts.readOnly) {
|
|
210
|
+
const tasksUrl = opts.tasksUrl ?? "/public/tasks";
|
|
211
|
+
return `<input type="checkbox" id="vos-nav-toggle" class="vos-nav-toggle" aria-hidden="true">
|
|
212
|
+
<nav class="vos-toolbar" aria-label="Site navigation">
|
|
213
|
+
<a href="${metricsUrl}" class="vos-wordmark">Shipwright</a>
|
|
214
|
+
<div id="vos-nav-content" class="vos-nav">
|
|
215
|
+
<a href="${metricsUrl}" class="vos-nav-link${active(metricsUrl)}">Metrics</a>
|
|
216
|
+
<a href="${tasksUrl}" class="vos-nav-link${active(tasksUrl)}">Tasks</a>
|
|
217
|
+
</div>
|
|
218
|
+
<label for="vos-nav-toggle" class="vos-hamburger" aria-label="Toggle navigation" aria-expanded="false" aria-controls="vos-nav-content">☰</label>
|
|
219
|
+
</nav>
|
|
220
|
+
<script>(function(){var t=document.getElementById('vos-nav-toggle');if(t)t.addEventListener('change',function(){var l=document.querySelector('label[for="vos-nav-toggle"]');if(l)l.setAttribute('aria-expanded',t.checked?'true':'false');});})();</script>`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return `<input type="checkbox" id="vos-nav-toggle" class="vos-nav-toggle" aria-hidden="true">
|
|
224
|
+
<nav class="vos-toolbar" aria-label="Site navigation">
|
|
225
|
+
<a href="${adminBase}/admin/agents" class="vos-wordmark">Shipwright</a>
|
|
226
|
+
<div id="vos-nav-content" class="vos-nav">
|
|
227
|
+
<a href="${adminBase}/admin/agents" class="vos-nav-link${isQueueActivity ? "" : active("/admin/agents")}">Agents</a>
|
|
228
|
+
<a href="${adminBase}/admin/tasks" class="vos-nav-link${active("/admin/tasks")}">Tasks</a>
|
|
229
|
+
<a href="${adminBase}/admin/sessions" class="vos-nav-link${active("/admin/sessions")}">Sessions</a>
|
|
230
|
+
<a href="${adminBase}/admin/prs" class="vos-nav-link${active("/admin/prs")}">PRs</a>
|
|
231
|
+
<a href="${adminBase}/admin/chat" class="vos-nav-link${active("/admin/chat")}">Chat</a>
|
|
232
|
+
<a href="${adminBase}/admin/queue-activity" class="vos-nav-link${isQueueActivity ? " active" : ""}">Queue & Activity</a>
|
|
233
|
+
${opts.showAccount ? `<a href="${adminBase}/admin/account" class="vos-nav-link${active("/admin/account")}">Account</a>\n ` : ""}<a href="${metricsUrl}" class="vos-nav-link${active(metricsUrl)}">Metrics</a>
|
|
234
|
+
</div>
|
|
235
|
+
<div class="vos-user">
|
|
236
|
+
<span class="vos-username">${escapeHtml(userName)}</span>
|
|
237
|
+
<form method="POST" action="${logoutAction}" style="margin:0">
|
|
238
|
+
<button type="submit" class="vos-signout-btn">Sign out</button>
|
|
239
|
+
</form>
|
|
240
|
+
</div>
|
|
241
|
+
<label for="vos-nav-toggle" class="vos-hamburger" aria-label="Toggle navigation" aria-expanded="false" aria-controls="vos-nav-content">☰</label>
|
|
242
|
+
</nav>
|
|
243
|
+
<script>(function(){var t=document.getElementById('vos-nav-toggle');if(t)t.addEventListener('change',function(){var l=document.querySelector('label[for="vos-nav-toggle"]');if(l)l.setAttribute('aria-expanded',t.checked?'true':'false');});})();</script>`;
|
|
244
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/web/toolbar.unit.test.ts
|
|
3
|
+
* Pure unit tests for renderShipwrightToolbar().
|
|
4
|
+
*
|
|
5
|
+
* Strategy: call the render function directly, assert on returned HTML strings.
|
|
6
|
+
* No I/O, no Hono, no HTTP — pure string → string.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
import { renderShipwrightToolbar } from "./toolbar.ts";
|
|
11
|
+
|
|
12
|
+
describe("renderShipwrightToolbar", () => {
|
|
13
|
+
describe("authenticated mode (readOnly=false)", () => {
|
|
14
|
+
const html = renderShipwrightToolbar({
|
|
15
|
+
userName: "Alice",
|
|
16
|
+
activePath: "/admin/agents",
|
|
17
|
+
logoutAction: "/auth/logout",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("contains hamburger button with class vos-hamburger", () => {
|
|
21
|
+
expect(html).toContain("vos-hamburger");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("hamburger button shows ☰ icon", () => {
|
|
25
|
+
expect(html).toContain("☰");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("contains nav links for authenticated mode", () => {
|
|
29
|
+
expect(html).toContain("Agents");
|
|
30
|
+
expect(html).toContain("Metrics");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// TBF-1.1: the navbar Tasks link must not hardcode a state filter — doing
|
|
34
|
+
// so silently pre-filtered every navbar-driven visit to ready-only tasks
|
|
35
|
+
// and (post-AXR-1.3) bounced the user off the default board view. The
|
|
36
|
+
// link must land on bare /admin/tasks.
|
|
37
|
+
test("Tasks link has no hardcoded ?state=ready filter", () => {
|
|
38
|
+
expect(html).not.toContain("/admin/tasks?state=ready");
|
|
39
|
+
expect(html).toContain('href="/admin/tasks" class="vos-nav-link');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("does not render an /admin/provision link (removed dead nav shortcut)", () => {
|
|
43
|
+
expect(html).not.toContain('href="/admin/provision"');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("active link has active class", () => {
|
|
47
|
+
expect(html).toContain('class="vos-nav-link active"');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("checkbox input appears before nav element in DOM", () => {
|
|
51
|
+
expect(html.indexOf("<input")).toBeLessThan(html.indexOf("<nav"));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("hamburger label has aria-expanded attribute for screen reader feedback", () => {
|
|
55
|
+
expect(html).toContain('aria-expanded="false"');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("hamburger label has aria-controls pointing to nav content", () => {
|
|
59
|
+
expect(html).toContain('aria-controls="vos-nav-content"');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("nav content div has id for aria-controls target", () => {
|
|
63
|
+
expect(html).toContain('id="vos-nav-content"');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("includes JS shim to toggle aria-expanded on checkbox change", () => {
|
|
67
|
+
expect(html).toContain("aria-expanded");
|
|
68
|
+
expect(html).toContain("<script>");
|
|
69
|
+
expect(html).toContain("change");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("contains a Queue & Activity link pointing at /admin/queue-activity", () => {
|
|
73
|
+
expect(html).toContain('href="/admin/queue-activity"');
|
|
74
|
+
expect(html).toContain("Queue & Activity");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// SESH-4.2: the Sessions link lives between Tasks and PRs.
|
|
78
|
+
test("contains a Sessions link pointing at /admin/sessions, between Tasks and PRs", () => {
|
|
79
|
+
expect(html).toContain('href="/admin/sessions"');
|
|
80
|
+
expect(html).toContain(">Sessions<");
|
|
81
|
+
const tasksIdx = html.indexOf('href="/admin/tasks"');
|
|
82
|
+
const sessionsIdx = html.indexOf('href="/admin/sessions"');
|
|
83
|
+
const prsIdx = html.indexOf('href="/admin/prs"');
|
|
84
|
+
expect(tasksIdx).toBeGreaterThan(-1);
|
|
85
|
+
expect(sessionsIdx).toBeGreaterThan(tasksIdx);
|
|
86
|
+
expect(prsIdx).toBeGreaterThan(sessionsIdx);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("active-tab highlighting: Agents vs Queue & Activity (AXR-3.3)", () => {
|
|
91
|
+
function activeHref(html: string): string | undefined {
|
|
92
|
+
const match = html.match(
|
|
93
|
+
/<a href="([^"]+)" class="vos-nav-link active">/,
|
|
94
|
+
);
|
|
95
|
+
return match?.[1];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
test("/admin/agents highlights the Agents tab, not Queue & Activity", () => {
|
|
99
|
+
const html = renderShipwrightToolbar({
|
|
100
|
+
userName: "Alice",
|
|
101
|
+
activePath: "/admin/agents",
|
|
102
|
+
logoutAction: "/auth/logout",
|
|
103
|
+
});
|
|
104
|
+
expect(activeHref(html)).toBe("/admin/agents");
|
|
105
|
+
expect(html).toContain(
|
|
106
|
+
'<a href="/admin/queue-activity" class="vos-nav-link">',
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("/admin/agents/:id highlights the Agents tab, not Queue & Activity", () => {
|
|
111
|
+
const html = renderShipwrightToolbar({
|
|
112
|
+
userName: "Alice",
|
|
113
|
+
activePath: "/admin/agents/agent-123",
|
|
114
|
+
logoutAction: "/auth/logout",
|
|
115
|
+
});
|
|
116
|
+
expect(activeHref(html)).toBe("/admin/agents");
|
|
117
|
+
expect(html).toContain(
|
|
118
|
+
'<a href="/admin/queue-activity" class="vos-nav-link">',
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("/admin/agents/:id/queue-activity highlights Queue & Activity, not Agents", () => {
|
|
123
|
+
const html = renderShipwrightToolbar({
|
|
124
|
+
userName: "Alice",
|
|
125
|
+
activePath: "/admin/agents/agent-123/queue-activity",
|
|
126
|
+
logoutAction: "/auth/logout",
|
|
127
|
+
});
|
|
128
|
+
expect(activeHref(html)).toBe("/admin/queue-activity");
|
|
129
|
+
expect(html).toContain('<a href="/admin/agents" class="vos-nav-link">');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("active-tab highlighting: Sessions (SESH-4.2)", () => {
|
|
134
|
+
function activeHref(html: string): string | undefined {
|
|
135
|
+
const match = html.match(
|
|
136
|
+
/<a href="([^"]+)" class="vos-nav-link active">/,
|
|
137
|
+
);
|
|
138
|
+
return match?.[1];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
test("/admin/sessions highlights the Sessions tab", () => {
|
|
142
|
+
const html = renderShipwrightToolbar({
|
|
143
|
+
userName: "Alice",
|
|
144
|
+
activePath: "/admin/sessions",
|
|
145
|
+
logoutAction: "/auth/logout",
|
|
146
|
+
});
|
|
147
|
+
expect(activeHref(html)).toBe("/admin/sessions");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("/admin/sessions/some-slug (session detail) also highlights the Sessions tab", () => {
|
|
151
|
+
const html = renderShipwrightToolbar({
|
|
152
|
+
userName: "Alice",
|
|
153
|
+
activePath: "/admin/sessions/some-slug",
|
|
154
|
+
logoutAction: "/auth/logout",
|
|
155
|
+
});
|
|
156
|
+
expect(activeHref(html)).toBe("/admin/sessions");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("read-only mode (readOnly=true)", () => {
|
|
161
|
+
const html = renderShipwrightToolbar({
|
|
162
|
+
userName: "",
|
|
163
|
+
activePath: "/dashboard",
|
|
164
|
+
logoutAction: "",
|
|
165
|
+
readOnly: true,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("contains hamburger button with class vos-hamburger", () => {
|
|
169
|
+
expect(html).toContain("vos-hamburger");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("hamburger button shows ☰ icon", () => {
|
|
173
|
+
expect(html).toContain("☰");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("contains only read-only nav links", () => {
|
|
177
|
+
expect(html).toContain("Metrics");
|
|
178
|
+
expect(html).toContain("Tasks");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("does not contain authenticated-only links", () => {
|
|
182
|
+
expect(html).not.toContain("Agents");
|
|
183
|
+
expect(html).not.toContain("Sign out");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("checkbox input appears before nav element in DOM", () => {
|
|
187
|
+
expect(html.indexOf("<input")).toBeLessThan(html.indexOf("<nav"));
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("hamburger label has aria-expanded attribute for screen reader feedback", () => {
|
|
191
|
+
expect(html).toContain('aria-expanded="false"');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("hamburger label has aria-controls pointing to nav content", () => {
|
|
195
|
+
expect(html).toContain('aria-controls="vos-nav-content"');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("nav content div has id for aria-controls target", () => {
|
|
199
|
+
expect(html).toContain('id="vos-nav-content"');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("includes JS shim to toggle aria-expanded on checkbox change", () => {
|
|
203
|
+
expect(html).toContain("aria-expanded");
|
|
204
|
+
expect(html).toContain("<script>");
|
|
205
|
+
expect(html).toContain("change");
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("renderShipwrightToolbar Account entry (SSP-3.2)", () => {
|
|
211
|
+
const base = {
|
|
212
|
+
userName: "Alice",
|
|
213
|
+
activePath: "/admin/agents",
|
|
214
|
+
logoutAction: "/auth/logout",
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
test("omits the Account link by default so other services never render it", () => {
|
|
218
|
+
expect(renderShipwrightToolbar(base)).not.toContain("/admin/account");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("renders the Account link when showAccount is true", () => {
|
|
222
|
+
const html = renderShipwrightToolbar({ ...base, showAccount: true });
|
|
223
|
+
expect(html).toContain('href="/admin/account" class="vos-nav-link');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("highlights Account when it is the active path", () => {
|
|
227
|
+
const html = renderShipwrightToolbar({
|
|
228
|
+
...base,
|
|
229
|
+
activePath: "/admin/account",
|
|
230
|
+
showAccount: true,
|
|
231
|
+
});
|
|
232
|
+
expect(html).toContain('href="/admin/account" class="vos-nav-link active"');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("readOnly mode never renders Account even when showAccount is true", () => {
|
|
236
|
+
const html = renderShipwrightToolbar({
|
|
237
|
+
...base,
|
|
238
|
+
readOnly: true,
|
|
239
|
+
showAccount: true,
|
|
240
|
+
});
|
|
241
|
+
expect(html).not.toContain("/admin/account");
|
|
242
|
+
});
|
|
243
|
+
});
|
package/README.md
DELETED