@korso/shepherd-ui 0.2.0 → 0.2.1
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/README.md +47 -47
- package/dist/ShepherdRoot.js +15 -57
- package/dist/components/Dashboard.d.ts +16 -2
- package/dist/components/Dashboard.js +76 -5
- package/dist/config/EmptyState.js +5 -1
- package/dist/lib/{Dashboard-CG6KeFQ3.js → Dashboard-B7nioe5N.js} +529 -457
- package/dist/lib/index.d.ts +15 -1
- package/dist/lib/index.js +196 -279
- package/dist/lib/selfhost.js +1 -1
- package/dist/lib/styles.css +217 -209
- package/dist/selfhost/assets/index-D6rTsbVM.js +40 -0
- package/dist/selfhost/assets/{index-BZmImfYZ.css → index-DnhlP_lc.css} +1 -1
- package/dist/selfhost/index.html +13 -13
- package/dist/useLandscapePolling.d.ts +7 -0
- package/dist/useLandscapePolling.js +6 -1
- package/package.json +67 -67
- package/dist/ShepherdProvider.d.ts +0 -12
- package/dist/ShepherdProvider.js +0 -33
- package/dist/ShepherdProvider.test.d.ts +0 -1
- package/dist/ShepherdProvider.test.js +0 -47
- package/dist/ShepherdRoot.routing.test.d.ts +0 -1
- package/dist/ShepherdRoot.routing.test.js +0 -61
- package/dist/ShepherdRoot.test.d.ts +0 -1
- package/dist/ShepherdRoot.test.js +0 -37
- package/dist/app/assets/index-CDOCIg6s.js +0 -11
- package/dist/app/index.html +0 -12
- package/dist/client.test.d.ts +0 -1
- package/dist/client.test.js +0 -298
- package/dist/config/ConnectAgent.test.d.ts +0 -1
- package/dist/config/ConnectAgent.test.js +0 -92
- package/dist/config/EmptyState.test.d.ts +0 -1
- package/dist/config/EmptyState.test.js +0 -32
- package/dist/config/Members.test.d.ts +0 -1
- package/dist/config/Members.test.js +0 -66
- package/dist/config/Workspaces.test.d.ts +0 -1
- package/dist/config/Workspaces.test.js +0 -90
- package/dist/index.cjs +0 -5289
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -149
- package/dist/index.js.map +0 -1
- package/dist/selfhost/assets/index-CS268KSw.js +0 -40
- package/dist/test/setup.d.ts +0 -1
- package/dist/test/setup.js +0 -2
- package/dist/views/Chat.d.ts +0 -5
- package/dist/views/Chat.js +0 -60
- package/dist/views/Chat.test.d.ts +0 -1
- package/dist/views/Chat.test.js +0 -92
- package/dist/views/Config.d.ts +0 -12
- package/dist/views/Config.js +0 -6
- package/dist/views/Tasks.d.ts +0 -5
- package/dist/views/Tasks.js +0 -75
- package/dist/views/Tasks.test.d.ts +0 -1
- package/dist/views/Tasks.test.js +0 -115
- package/dist/views/useLandscape.d.ts +0 -14
- package/dist/views/useLandscape.js +0 -74
- package/dist/views/useLandscape.test.d.ts +0 -1
- package/dist/views/useLandscape.test.js +0 -101
- package/dist/views/wallboard.d.ts +0 -46
- package/dist/views/wallboard.js +0 -209
- package/dist/views/wallboard.test.d.ts +0 -1
- package/dist/views/wallboard.test.js +0 -143
package/dist/views/wallboard.js
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// wallboard — pure helpers ported from packages/hub/public/app.js, the vanilla
|
|
3
|
-
// wallboard the React Tasks/Chat views replace. These mirror that module's
|
|
4
|
-
// behavior exactly so the board reads the same data the same way (functional
|
|
5
|
-
// parity). No DOM, no clock: every helper takes `nowMs` so countdowns/relative
|
|
6
|
-
// times are computed against the server's clock (landscape.serverTime), not the
|
|
7
|
-
// browser's, keeping them correct under skew — same as the old page did.
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
/** Human "N ago" for a past ISO timestamp, relative to nowMs (epoch ms). */
|
|
10
|
-
export function formatRelative(iso, nowMs) {
|
|
11
|
-
const secs = Math.floor((nowMs - Date.parse(iso)) / 1000);
|
|
12
|
-
if (secs < 5)
|
|
13
|
-
return "just now";
|
|
14
|
-
if (secs < 60)
|
|
15
|
-
return `${secs}s ago`;
|
|
16
|
-
const mins = Math.floor(secs / 60);
|
|
17
|
-
if (mins < 60)
|
|
18
|
-
return `${mins}m ago`;
|
|
19
|
-
const hrs = Math.floor(mins / 60);
|
|
20
|
-
if (hrs < 24)
|
|
21
|
-
return `${hrs}h ago`;
|
|
22
|
-
return `${Math.floor(hrs / 24)}d ago`;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Deterministic chat color for an agent name — same name always maps to the
|
|
26
|
-
* same hue, so each speaker reads consistently across the announcement thread.
|
|
27
|
-
*/
|
|
28
|
-
export function colorForName(name) {
|
|
29
|
-
let h = 0;
|
|
30
|
-
for (let i = 0; i < name.length; i++)
|
|
31
|
-
h = (h * 31 + name.charCodeAt(i)) % 360;
|
|
32
|
-
return `hsl(${h}, 38%, 42%)`;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Up to two initials for an avatar. Prefers the capital letters of a CamelCase
|
|
36
|
-
* agent name (RedDragon → RD); otherwise the first two letters (alice → AL).
|
|
37
|
-
*/
|
|
38
|
-
export function initialsFor(name) {
|
|
39
|
-
if (!name)
|
|
40
|
-
return "?";
|
|
41
|
-
const caps = name.match(/[A-Z]/g);
|
|
42
|
-
if (caps && caps.length >= 2)
|
|
43
|
-
return caps[0] + caps[1];
|
|
44
|
-
return name.slice(0, 2).toUpperCase();
|
|
45
|
-
}
|
|
46
|
-
/** Human label for a history task's status. */
|
|
47
|
-
export function statusLabel(status) {
|
|
48
|
-
return status === "dropped" ? "dropped" : "done";
|
|
49
|
-
}
|
|
50
|
-
/** "active Nm/Nh" for the created->ended span; "" when the task hasn't ended. */
|
|
51
|
-
export function formatActiveDuration(createdIso, endedIso) {
|
|
52
|
-
if (!endedIso)
|
|
53
|
-
return "";
|
|
54
|
-
const secs = Math.floor((Date.parse(endedIso) - Date.parse(createdIso)) / 1000);
|
|
55
|
-
const mins = Math.floor(secs / 60);
|
|
56
|
-
if (mins < 60)
|
|
57
|
-
return `active ${mins}m`;
|
|
58
|
-
return `active ${Math.floor(mins / 60)}h`;
|
|
59
|
-
}
|
|
60
|
-
/** Local-day bucket label: "Today" / "Yesterday" / "Mon D". */
|
|
61
|
-
export function dayBucket(iso, nowMs) {
|
|
62
|
-
const d = new Date(Date.parse(iso));
|
|
63
|
-
const now = new Date(nowMs);
|
|
64
|
-
const startOf = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
|
|
65
|
-
const dayDiff = Math.round((startOf(now) - startOf(d)) / 86400000);
|
|
66
|
-
if (dayDiff <= 0)
|
|
67
|
-
return "Today";
|
|
68
|
-
if (dayDiff === 1)
|
|
69
|
-
return "Yesterday";
|
|
70
|
-
return d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
71
|
-
}
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
// Glob containment + active-claim grouping (verbatim from app.js, retyped)
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
/**
|
|
76
|
-
* Normalize a glob into a segment list. Mirrors the server's normalize
|
|
77
|
-
* (packages/hub/src/globs.ts) so the two agree on path shape.
|
|
78
|
-
*/
|
|
79
|
-
function normalizeGlob(pattern) {
|
|
80
|
-
let p = pattern.toLowerCase().replace(/\\/g, "/");
|
|
81
|
-
if (p.endsWith("/"))
|
|
82
|
-
p = p.replace(/\/+$/, "") + "/**";
|
|
83
|
-
const out = [];
|
|
84
|
-
for (const seg of p.split("/")) {
|
|
85
|
-
if (seg === "" || seg === ".")
|
|
86
|
-
continue;
|
|
87
|
-
if (seg === "..") {
|
|
88
|
-
if (out.length)
|
|
89
|
-
out.pop();
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
out.push(seg);
|
|
93
|
-
}
|
|
94
|
-
return out;
|
|
95
|
-
}
|
|
96
|
-
const SEG_WILDCARD = /[*?{}[\]]/;
|
|
97
|
-
/** Does single-segment pattern `a` cover single-segment pattern `b`? */
|
|
98
|
-
function segmentCovers(a, b) {
|
|
99
|
-
if (a === "*")
|
|
100
|
-
return true;
|
|
101
|
-
if (SEG_WILDCARD.test(a))
|
|
102
|
-
return a === b;
|
|
103
|
-
return !SEG_WILDCARD.test(b) && a === b;
|
|
104
|
-
}
|
|
105
|
-
/** Does pattern A (segments) cover pattern B (segments)? '**' consumes 0+ segments. */
|
|
106
|
-
function patternCovers(a, b) {
|
|
107
|
-
const memo = new Map();
|
|
108
|
-
function go(i, j) {
|
|
109
|
-
if (b.length - j === 0) {
|
|
110
|
-
for (let k = i; k < a.length; k++)
|
|
111
|
-
if (a[k] !== "**")
|
|
112
|
-
return false;
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
|
-
if (a.length - i === 0)
|
|
116
|
-
return false;
|
|
117
|
-
const key = i * (b.length + 1) + j;
|
|
118
|
-
const cached = memo.get(key);
|
|
119
|
-
if (cached !== undefined)
|
|
120
|
-
return cached;
|
|
121
|
-
const ah = a[i], bh = b[j];
|
|
122
|
-
let res;
|
|
123
|
-
if (ah === "**") {
|
|
124
|
-
res = go(i + 1, j) || go(i, j + 1);
|
|
125
|
-
}
|
|
126
|
-
else if (bh === "**") {
|
|
127
|
-
res = false;
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
res = segmentCovers(ah, bh) && go(i + 1, j + 1);
|
|
131
|
-
}
|
|
132
|
-
memo.set(key, res);
|
|
133
|
-
return res;
|
|
134
|
-
}
|
|
135
|
-
return go(0, 0);
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Does glob set `outer` fully cover glob set `inner`? Conservative: unsure → false,
|
|
139
|
-
* so a claim only folds when it is certainly contained. Empty `inner` is covered.
|
|
140
|
-
*/
|
|
141
|
-
export function globsCover(outer, inner) {
|
|
142
|
-
if (inner.length === 0)
|
|
143
|
-
return true;
|
|
144
|
-
const o = outer.map(normalizeGlob);
|
|
145
|
-
return inner.every((g) => {
|
|
146
|
-
const ng = normalizeGlob(g);
|
|
147
|
-
return o.some((op) => patternCovers(op, ng));
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Group active claims by agent for the board. Within an agent's claims, one that
|
|
152
|
-
* is STRICTLY covered by a broader sibling folds into `narrower`; everything else
|
|
153
|
-
* stays a visible `primary`. Groups + claims are newest-first; header fields come
|
|
154
|
-
* from the group's newest claim. Pure — no DOM, no clock.
|
|
155
|
-
*/
|
|
156
|
-
export function groupActiveClaims(tasks) {
|
|
157
|
-
const byAgent = new Map();
|
|
158
|
-
for (const t of tasks) {
|
|
159
|
-
if (!byAgent.has(t.agentName))
|
|
160
|
-
byAgent.set(t.agentName, []);
|
|
161
|
-
byAgent.get(t.agentName).push(t);
|
|
162
|
-
}
|
|
163
|
-
const newestFirst = (a, b) => b.createdAt.localeCompare(a.createdAt);
|
|
164
|
-
const groups = [];
|
|
165
|
-
for (const [agentName, claims] of byAgent) {
|
|
166
|
-
const sorted = [...claims].sort(newestFirst);
|
|
167
|
-
const isNarrower = sorted.map((c) => sorted.some((o) => o !== c &&
|
|
168
|
-
globsCover(o.pathGlobs, c.pathGlobs) &&
|
|
169
|
-
!globsCover(c.pathGlobs, o.pathGlobs)));
|
|
170
|
-
const head = sorted[0];
|
|
171
|
-
groups.push({
|
|
172
|
-
agentName,
|
|
173
|
-
model: head.model,
|
|
174
|
-
program: head.program,
|
|
175
|
-
repo: head.repo,
|
|
176
|
-
primaries: sorted.filter((_, i) => !isNarrower[i]),
|
|
177
|
-
narrower: sorted.filter((_, i) => isNarrower[i]),
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
groups.sort((g1, g2) => {
|
|
181
|
-
const t1 = g1.primaries.concat(g1.narrower)[0].createdAt;
|
|
182
|
-
const t2 = g2.primaries.concat(g2.narrower)[0].createdAt;
|
|
183
|
-
return t2.localeCompare(t1);
|
|
184
|
-
});
|
|
185
|
-
return groups;
|
|
186
|
-
}
|
|
187
|
-
// ---------------------------------------------------------------------------
|
|
188
|
-
// @mention target extraction (composer → DM direction). Ported from app.js so a
|
|
189
|
-
// typed @name still directs the message, matching the old send behavior. The
|
|
190
|
-
// autocomplete POPUP UI is intentionally not ported (visual polish, deferred).
|
|
191
|
-
// ---------------------------------------------------------------------------
|
|
192
|
-
/** Characters allowed in an agent-name mention token. */
|
|
193
|
-
const MENTION_CHARS = "A-Za-z0-9_-";
|
|
194
|
-
/**
|
|
195
|
-
* The first `@mention` in `text` matching a known agent name, returned in the
|
|
196
|
-
* name's canonical casing (`@reddragon` → `RedDragon`), or null when none match.
|
|
197
|
-
* An unmatched `@foo` is plain text and the message broadcasts.
|
|
198
|
-
*/
|
|
199
|
-
export function extractTarget(text, knownNames) {
|
|
200
|
-
const re = new RegExp(`(?:^|\\s)@([${MENTION_CHARS}]+)`, "g");
|
|
201
|
-
let m;
|
|
202
|
-
while ((m = re.exec(text)) !== null) {
|
|
203
|
-
const typed = m[1].toLowerCase();
|
|
204
|
-
const hit = knownNames.find((n) => n.toLowerCase() === typed);
|
|
205
|
-
if (hit)
|
|
206
|
-
return hit;
|
|
207
|
-
}
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { formatRelative, statusLabel, formatActiveDuration, dayBucket, initialsFor, colorForName, globsCover, groupActiveClaims, extractTarget, } from "./wallboard.js";
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// wallboard helpers — the pure logic ported from packages/hub/public/app.js,
|
|
5
|
-
// driving the React Tasks/Chat views. Mirrors that module's behavior so the
|
|
6
|
-
// ported board reads the same data the same way (functional parity).
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
function task(over = {}) {
|
|
9
|
-
return {
|
|
10
|
-
agentName: "alice",
|
|
11
|
-
program: "claude",
|
|
12
|
-
model: "opus",
|
|
13
|
-
repo: "acme/web",
|
|
14
|
-
intent: "do a thing",
|
|
15
|
-
pathGlobs: ["src/**"],
|
|
16
|
-
status: "active",
|
|
17
|
-
createdAt: "2026-06-29T00:00:00.000Z",
|
|
18
|
-
endedAt: null,
|
|
19
|
-
...over,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
describe("formatRelative", () => {
|
|
23
|
-
const now = Date.parse("2026-06-29T01:00:00.000Z");
|
|
24
|
-
it("says 'just now' under 5s", () => {
|
|
25
|
-
expect(formatRelative("2026-06-29T00:59:58.000Z", now)).toBe("just now");
|
|
26
|
-
});
|
|
27
|
-
it("renders seconds / minutes / hours / days", () => {
|
|
28
|
-
expect(formatRelative("2026-06-29T00:59:30.000Z", now)).toBe("30s ago");
|
|
29
|
-
expect(formatRelative("2026-06-29T00:30:00.000Z", now)).toBe("30m ago");
|
|
30
|
-
expect(formatRelative("2026-06-28T22:00:00.000Z", now)).toBe("3h ago");
|
|
31
|
-
expect(formatRelative("2026-06-26T01:00:00.000Z", now)).toBe("3d ago");
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
describe("statusLabel", () => {
|
|
35
|
-
it("maps dropped → dropped, everything else → done", () => {
|
|
36
|
-
expect(statusLabel("dropped")).toBe("dropped");
|
|
37
|
-
expect(statusLabel("done")).toBe("done");
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
describe("formatActiveDuration", () => {
|
|
41
|
-
it("is empty when not ended", () => {
|
|
42
|
-
expect(formatActiveDuration("2026-06-29T00:00:00.000Z", null)).toBe("");
|
|
43
|
-
});
|
|
44
|
-
it("renders minutes then hours", () => {
|
|
45
|
-
expect(formatActiveDuration("2026-06-29T00:00:00.000Z", "2026-06-29T00:30:00.000Z")).toBe("active 30m");
|
|
46
|
-
expect(formatActiveDuration("2026-06-29T00:00:00.000Z", "2026-06-29T03:00:00.000Z")).toBe("active 3h");
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
describe("dayBucket", () => {
|
|
50
|
-
const now = Date.parse("2026-06-29T12:00:00.000Z");
|
|
51
|
-
it("buckets today / yesterday", () => {
|
|
52
|
-
expect(dayBucket("2026-06-29T08:00:00.000Z", now)).toBe("Today");
|
|
53
|
-
expect(dayBucket("2026-06-28T08:00:00.000Z", now)).toBe("Yesterday");
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
describe("initialsFor", () => {
|
|
57
|
-
it("prefers two capitals of a CamelCase name", () => {
|
|
58
|
-
expect(initialsFor("RedDragon")).toBe("RD");
|
|
59
|
-
});
|
|
60
|
-
it("falls back to the first two letters", () => {
|
|
61
|
-
expect(initialsFor("alice")).toBe("AL");
|
|
62
|
-
});
|
|
63
|
-
it("handles empty", () => {
|
|
64
|
-
expect(initialsFor("")).toBe("?");
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
describe("groupActiveClaims", () => {
|
|
68
|
-
it("groups claims by agent", () => {
|
|
69
|
-
const groups = groupActiveClaims([
|
|
70
|
-
task({ agentName: "alice", pathGlobs: ["a/**"] }),
|
|
71
|
-
task({ agentName: "bob", pathGlobs: ["b/**"] }),
|
|
72
|
-
]);
|
|
73
|
-
expect(groups.map((g) => g.agentName).sort()).toEqual(["alice", "bob"]);
|
|
74
|
-
});
|
|
75
|
-
it("folds a strictly-covered claim into narrower", () => {
|
|
76
|
-
const groups = groupActiveClaims([
|
|
77
|
-
task({
|
|
78
|
-
agentName: "alice",
|
|
79
|
-
pathGlobs: ["src/**"],
|
|
80
|
-
createdAt: "2026-06-29T00:00:00.000Z",
|
|
81
|
-
}),
|
|
82
|
-
task({
|
|
83
|
-
agentName: "alice",
|
|
84
|
-
pathGlobs: ["src/a.ts"],
|
|
85
|
-
createdAt: "2026-06-29T00:01:00.000Z",
|
|
86
|
-
}),
|
|
87
|
-
]);
|
|
88
|
-
expect(groups).toHaveLength(1);
|
|
89
|
-
const g = groups[0];
|
|
90
|
-
expect(g.primaries.map((p) => p.pathGlobs)).toEqual([["src/**"]]);
|
|
91
|
-
expect(g.narrower.map((p) => p.pathGlobs)).toEqual([["src/a.ts"]]);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
describe("colorForName", () => {
|
|
95
|
-
it("is deterministic — the same name always maps to the same hue", () => {
|
|
96
|
-
expect(colorForName("RedDragon")).toBe(colorForName("RedDragon"));
|
|
97
|
-
});
|
|
98
|
-
it("distinguishes different names", () => {
|
|
99
|
-
expect(colorForName("alice")).not.toBe(colorForName("bob"));
|
|
100
|
-
});
|
|
101
|
-
it("returns a well-formed hsl() string", () => {
|
|
102
|
-
expect(colorForName("alice")).toMatch(/^hsl\(\d+, 38%, 42%\)$/);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
describe("globsCover", () => {
|
|
106
|
-
it("treats an empty inner set as covered", () => {
|
|
107
|
-
expect(globsCover(["src/**"], [])).toBe(true);
|
|
108
|
-
});
|
|
109
|
-
it("covers a literal under a '**' wildcard", () => {
|
|
110
|
-
expect(globsCover(["src/**"], ["src/a.ts"])).toBe(true);
|
|
111
|
-
});
|
|
112
|
-
it("does not cover across a path segment with a single '*'", () => {
|
|
113
|
-
// '*' is a single-segment wildcard: it must not span the '/'.
|
|
114
|
-
expect(globsCover(["src/*"], ["src/a/b.ts"])).toBe(false);
|
|
115
|
-
});
|
|
116
|
-
it("lets '**' span multiple path segments", () => {
|
|
117
|
-
expect(globsCover(["**"], ["src/deep/nested/file.ts"])).toBe(true);
|
|
118
|
-
});
|
|
119
|
-
it("matches a literal segment exactly, not a wildcard sibling", () => {
|
|
120
|
-
expect(globsCover(["src/a.ts"], ["src/a.ts"])).toBe(true);
|
|
121
|
-
expect(globsCover(["src/a.ts"], ["src/b.ts"])).toBe(false);
|
|
122
|
-
});
|
|
123
|
-
it("does not let a narrower literal cover a wildcard", () => {
|
|
124
|
-
expect(globsCover(["src/a.ts"], ["src/*"])).toBe(false);
|
|
125
|
-
});
|
|
126
|
-
it("normalizes '.' and '..' segments before comparing", () => {
|
|
127
|
-
// "src/foo/../**" normalizes to "src/**", which covers "src/a.ts".
|
|
128
|
-
expect(globsCover(["src/foo/../**"], ["src/a.ts"])).toBe(true);
|
|
129
|
-
});
|
|
130
|
-
it("requires every inner glob to be covered by some outer glob", () => {
|
|
131
|
-
expect(globsCover(["src/**"], ["src/a.ts", "lib/b.ts"])).toBe(false);
|
|
132
|
-
expect(globsCover(["src/**", "lib/**"], ["src/a.ts", "lib/b.ts"])).toBe(true);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
describe("extractTarget", () => {
|
|
136
|
-
it("resolves the first @mention matching a known name, canonical-cased", () => {
|
|
137
|
-
expect(extractTarget("hey @reddragon look", ["RedDragon", "alice"])).toBe("RedDragon");
|
|
138
|
-
});
|
|
139
|
-
it("returns null when no mention matches", () => {
|
|
140
|
-
expect(extractTarget("no mention here", ["alice"])).toBeNull();
|
|
141
|
-
expect(extractTarget("hi @nobody", ["alice"])).toBeNull();
|
|
142
|
-
});
|
|
143
|
-
});
|