Skip to content

Commit 981a4f9

Browse files
committed
test: TDD backfill — agent-registry-mongo + agentos CRUD + displaySource
Adds the test coverage that was missing from Phases 1 / 2b / 2c shipping without it. Total: 67 new tests across 4 files, 1163 LOC. - packages/agent-registry-mongo/src/registry.test.ts (9 tests): upsert shape, idempotent registeredAt, list ordering, get null, unregister idempotency, close idempotency, shared MongoClient. - packages/agent-registry-mongo/src/audit-log.test.ts (8 tests): append shape, QUERY_MAX/REPLY_MAX truncation with … suffix, newest-first + limit clamping [1, 500], filter combos (source/ok/before), count() vs list() equivalence. - packages/agent-registry-mongo/src/telemetry.test.ts (12 tests): onAgentConstructed upserts via SDK info; falls back to ctor-supplied agent fields; onChatStart returns ctx; onChatEnd appends success + failure rows with usage breakouts; durationMs derives from ctx when SDK omits it; configurable source tag; onError fires on Mongo failure without throwing; shared client lifecycle. - examples/agentos-api.test.ts (18 tests): drives createAgentOSApp via app.fetch(Request). POST /register (400 on missing name, upsert shape, idempotent registeredAt), PATCH (409 on in-memory, 404 on unknown, field update), DELETE (409/404/200), GET /by-source (400/in-memory match/registry match/dual match/empty 200), GET /agents (union, origin tagging, in-memory wins). - agentos/src/api.test.ts (20 tests): pure-function tests for displaySource() — null/undefined sentinels, local + inline structured, git URL parsing (https, ssh git@, with ref, ref URL-encoded), recognized hosts (github/gitlab/bitbucket), scheme-less + bare owner/repo, legacy string source, fallbacks. Live-Mongo paths use the same `describeMongo = url ? describe : describe.skip` gate the session-store-mongo tests already use; unique DB per run keeps parallel runs isolated. Offline (no MONGO_URL): 4 always-on tests pass, 57 env-gated skip cleanly. Workspace-wide pnpm -r test stays green. Wires vitest into examples/ and agentos/ (test script + ^2.0.0 devDep) so the suite is invokable per-package.
1 parent 2545e9e commit 981a4f9

8 files changed

Lines changed: 1163 additions & 4 deletions

File tree

agentos/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
"scripts": {
77
"dev": "vite",
88
"build": "tsc -b && vite build",
9-
"preview": "vite preview"
9+
"preview": "vite preview",
10+
"test": "vitest run --passWithNoTests",
11+
"typecheck": "tsc -b --noEmit"
1012
},
1113
"dependencies": {
1214
"react": "^18.3.1",
@@ -20,6 +22,7 @@
2022
"postcss": "^8.4.49",
2123
"tailwindcss": "^3.4.17",
2224
"typescript": "^5.7.2",
23-
"vite": "^6.0.5"
25+
"vite": "^6.0.5",
26+
"vitest": "^2.0.0"
2427
}
2528
}

agentos/src/api.test.ts

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/**
2+
* Pure-function unit tests for `displaySource`. No DOM, no fetch, no React.
3+
*
4+
* displaySource is the load-bearing piece behind <SourceBadge> in the agent
5+
* rail: it turns the variable-shaped `agent.source` (git/local/inline object
6+
* OR a legacy bare string) into a render-ready `{kind, primary, secondary,
7+
* href?}` triple. Wrong output here = the dashboard renders the wrong title
8+
* or links to the wrong repo, so we exercise every recognized shape + every
9+
* fallback branch.
10+
*/
11+
import { describe, expect, it } from "vitest";
12+
import { displaySource } from "./api.js";
13+
14+
describe("displaySource", () => {
15+
describe("null / undefined input", () => {
16+
it("returns the (no source) unknown sentinel for undefined", () => {
17+
expect(displaySource(undefined)).toEqual({
18+
kind: "unknown",
19+
primary: "(no source)",
20+
secondary: "",
21+
});
22+
});
23+
it("returns the (no source) unknown sentinel for null", () => {
24+
expect(displaySource(null)).toEqual({
25+
kind: "unknown",
26+
primary: "(no source)",
27+
secondary: "",
28+
});
29+
});
30+
it("returns the (no source) unknown sentinel for empty string", () => {
31+
expect(displaySource("")).toEqual({
32+
kind: "unknown",
33+
primary: "(no source)",
34+
secondary: "",
35+
});
36+
});
37+
});
38+
39+
describe("structured local source", () => {
40+
it("uses the last two path segments as the primary label", () => {
41+
expect(displaySource({ type: "local", path: "/Users/zeus/repos/devsupport-agent" })).toEqual({
42+
kind: "local",
43+
primary: "repos/devsupport-agent",
44+
secondary: "/Users/zeus/repos/devsupport-agent",
45+
});
46+
});
47+
it("falls back to the full path when there's only one segment", () => {
48+
expect(displaySource({ type: "local", path: "/agent" })).toEqual({
49+
kind: "local",
50+
primary: "agent",
51+
secondary: "/agent",
52+
});
53+
});
54+
it("handles a trailing slash without producing an empty primary", () => {
55+
const out = displaySource({ type: "local", path: "/Users/zeus/x/" });
56+
expect(out.kind).toBe("local");
57+
expect(out.primary.length).toBeGreaterThan(0);
58+
});
59+
});
60+
61+
describe("structured inline source", () => {
62+
it("uses manifest.name when present", () => {
63+
expect(
64+
displaySource({ type: "inline", manifest: { name: "ad-hoc-bot" } }),
65+
).toEqual({ kind: "inline", primary: "ad-hoc-bot", secondary: "inline manifest" });
66+
});
67+
it("falls back to 'inline' when manifest has no name", () => {
68+
expect(displaySource({ type: "inline", manifest: { spec_version: "0.1.0" } })).toEqual({
69+
kind: "inline",
70+
primary: "inline",
71+
secondary: "inline manifest",
72+
});
73+
});
74+
it("falls back to 'inline' when manifest.name is not a string", () => {
75+
expect(
76+
displaySource({
77+
type: "inline",
78+
manifest: { name: 42 as unknown as string },
79+
}),
80+
).toEqual({ kind: "inline", primary: "inline", secondary: "inline manifest" });
81+
});
82+
});
83+
84+
describe("structured git source", () => {
85+
it("parses an https URL with .git suffix into owner/repo + host + href", () => {
86+
expect(
87+
displaySource({
88+
type: "git",
89+
url: "https://github.com/open-gitagent/ComputerAgent.git",
90+
}),
91+
).toEqual({
92+
kind: "git",
93+
primary: "open-gitagent/ComputerAgent",
94+
secondary: "github.com",
95+
href: "https://github.com/open-gitagent/ComputerAgent",
96+
});
97+
});
98+
it("parses an ssh git@ URL into owner/repo + host + https href", () => {
99+
expect(
100+
displaySource({ type: "git", url: "git@github.com:open-gitagent/opengap.git" }),
101+
).toEqual({
102+
kind: "git",
103+
primary: "open-gitagent/opengap",
104+
secondary: "github.com",
105+
href: "https://github.com/open-gitagent/opengap",
106+
});
107+
});
108+
it("appends /tree/<ref> to the href when ref is provided", () => {
109+
expect(
110+
displaySource({
111+
type: "git",
112+
url: "https://github.com/open-gitagent/ComputerAgent",
113+
ref: "main",
114+
}),
115+
).toEqual({
116+
kind: "git",
117+
primary: "open-gitagent/ComputerAgent",
118+
secondary: "github.com",
119+
href: "https://github.com/open-gitagent/ComputerAgent/tree/main",
120+
});
121+
});
122+
it("url-encodes a ref containing a slash", () => {
123+
const out = displaySource({
124+
type: "git",
125+
url: "https://github.com/o/r",
126+
ref: "feat/abc",
127+
});
128+
expect(out.href).toBe("https://github.com/o/r/tree/feat%2Fabc");
129+
});
130+
it("recognizes gitlab.com hosts", () => {
131+
expect(
132+
displaySource({ type: "git", url: "https://gitlab.com/my-org/my-repo" }),
133+
).toEqual({
134+
kind: "git",
135+
primary: "my-org/my-repo",
136+
secondary: "gitlab.com",
137+
href: "https://gitlab.com/my-org/my-repo",
138+
});
139+
});
140+
it("recognizes bitbucket.org hosts", () => {
141+
expect(
142+
displaySource({ type: "git", url: "https://bitbucket.org/team/proj.git" }),
143+
).toEqual({
144+
kind: "git",
145+
primary: "team/proj",
146+
secondary: "bitbucket.org",
147+
href: "https://bitbucket.org/team/proj",
148+
});
149+
});
150+
it("parses a scheme-less host/owner/repo", () => {
151+
expect(displaySource({ type: "git", url: "github.com/o/r" })).toEqual({
152+
kind: "git",
153+
primary: "o/r",
154+
secondary: "github.com",
155+
href: "https://github.com/o/r",
156+
});
157+
});
158+
it("treats a bare owner/repo (no host) as github by default", () => {
159+
expect(displaySource({ type: "git", url: "open-gitagent/opengap" })).toEqual({
160+
kind: "git",
161+
primary: "open-gitagent/opengap",
162+
secondary: "github.com",
163+
href: "https://github.com/open-gitagent/opengap",
164+
});
165+
});
166+
});
167+
168+
describe("legacy string source", () => {
169+
it("treats a full https URL the same as the structured form", () => {
170+
const expected = {
171+
kind: "git" as const,
172+
primary: "open-gitagent/ComputerAgent",
173+
secondary: "github.com",
174+
href: "https://github.com/open-gitagent/ComputerAgent",
175+
};
176+
expect(displaySource("https://github.com/open-gitagent/ComputerAgent")).toEqual(expected);
177+
});
178+
it("treats a bare owner/repo string as github", () => {
179+
expect(displaySource("open-gitagent/opengap")).toEqual({
180+
kind: "git",
181+
primary: "open-gitagent/opengap",
182+
secondary: "github.com",
183+
href: "https://github.com/open-gitagent/opengap",
184+
});
185+
});
186+
});
187+
188+
describe("unrecognized shapes fall back cleanly", () => {
189+
it("returns kind=unknown with raw primary when there's only one path segment", () => {
190+
expect(displaySource("just-a-name")).toEqual({
191+
kind: "unknown",
192+
primary: "just-a-name",
193+
secondary: "",
194+
});
195+
});
196+
});
197+
});

0 commit comments

Comments
 (0)