summaryrefslogtreecommitdiffhomepage
path: root/packages/system-prompt/src/service.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/system-prompt/src/service.test.ts')
-rw-r--r--packages/system-prompt/src/service.test.ts514
1 files changed, 307 insertions, 207 deletions
diff --git a/packages/system-prompt/src/service.test.ts b/packages/system-prompt/src/service.test.ts
index 37c1c0d..16591ec 100644
--- a/packages/system-prompt/src/service.test.ts
+++ b/packages/system-prompt/src/service.test.ts
@@ -5,223 +5,323 @@ import { createSystemPromptService, DEFAULT_TEMPLATE } from "./service.js";
/** In-memory StorageNamespace for tests. */
function memoryStorage(): StorageNamespace {
- const store = new Map<string, string>();
- return {
- get: async (key: string) => store.get(key) ?? null,
- set: async (key: string, value: string) => {
- store.set(key, value);
- },
- delete: async (key: string) => {
- store.delete(key);
- },
- has: async (key: string) => store.has(key),
- keys: async (prefix?: string) =>
- [...store.keys()].filter((k) => (prefix === undefined ? true : k.startsWith(prefix))),
- };
+ const store = new Map<string, string>();
+ return {
+ get: async (key: string) => store.get(key) ?? null,
+ set: async (key: string, value: string) => {
+ store.set(key, value);
+ },
+ delete: async (key: string) => {
+ store.delete(key);
+ },
+ has: async (key: string) => store.has(key),
+ keys: async (prefix?: string) =>
+ [...store.keys()].filter((k) => (prefix === undefined ? true : k.startsWith(prefix))),
+ };
}
function fakeFs(files: ReadonlyMap<string, string>): ResolverFs {
- return {
- readText: async (path: string) => files.get(path) ?? "",
- exists: async (path: string) => files.has(path),
- };
+ return {
+ readText: async (path: string) => files.get(path) ?? "",
+ exists: async (path: string) => files.has(path),
+ };
}
const failSpawn = async (): Promise<GitSpawnResult> => ({
- stdout: "",
- stderr: "",
- exitCode: 128,
+ stdout: "",
+ stderr: "",
+ exitCode: 128,
});
function adapters(files: ReadonlyMap<string, string>): ResolverAdapters {
- return {
- spawn: failSpawn,
- fs: fakeFs(files),
- now: () => new Date("2024-06-15T12:30:00.000Z"),
- platform: () => "linux",
- hostname: () => "myhost",
- };
+ return {
+ spawn: failSpawn,
+ fs: fakeFs(files),
+ now: () => new Date("2024-06-15T12:30:00.000Z"),
+ platform: () => "linux",
+ hostname: () => "myhost",
+ };
}
describe("system-prompt service", () => {
- it("construct persists and returns the resolved string", async () => {
- // 14. construct writes to storage and returns the resolved string.
- const storage = memoryStorage();
- const service = createSystemPromptService({
- storage,
- adapters: adapters(new Map([["/proj/AGENTS.md", "RULES"]])),
- });
-
- const result = await service.construct("conv-1", "/proj", { model: "gpt-4" });
-
- expect(result).toContain("You are a helpful coding assistant.");
- expect(result).toContain("RULES");
- expect(result).toContain("/proj");
- // persisted under resolved:<conversationId>
- expect(await storage.get("resolved:conv-1")).toBe(result);
- });
-
- it("get returns persisted value after construct", async () => {
- // 15. after construct, get returns the same string.
- const service = createSystemPromptService({
- storage: memoryStorage(),
- adapters: adapters(new Map()),
- });
-
- // before construct → null
- expect(await service.get("conv-2")).toBeNull();
-
- const result = await service.construct("conv-2", "/proj");
- expect(await service.get("conv-2")).toBe(result);
- });
-
- it("get returns null before construct", async () => {
- const service = createSystemPromptService({
- storage: memoryStorage(),
- adapters: adapters(new Map()),
- });
-
- expect(await service.get("never-constructed")).toBeNull();
- });
-
- it("empty/no template stored → default template → non-empty", async () => {
- // 16. no template stored → default template used → resolves to non-empty.
- const service = createSystemPromptService({
- storage: memoryStorage(),
- adapters: adapters(new Map()), // no AGENTS.md
- });
-
- const result = await service.construct("conv-3", "/proj");
-
- expect(result.length).toBeGreaterThan(0);
- expect(result).toContain("You are a helpful coding assistant.");
- expect(result).toContain("/proj");
- // no AGENTS.md file → the [if file:AGENTS.md] block is omitted
- expect(result).not.toContain("AGENTS.md");
- });
-
- it("stored template is used instead of default", async () => {
- const storage = memoryStorage();
- await storage.set("template", "cwd=[prompt:cwd] os=[system:os]");
- const service = createSystemPromptService({
- storage,
- adapters: adapters(new Map()),
- });
-
- const result = await service.construct("conv-4", "/work");
- expect(result).toBe("cwd=/work os=linux");
- });
-
- it("empty stored template → empty string", async () => {
- const storage = memoryStorage();
- await storage.set("template", "");
- const service = createSystemPromptService({
- storage,
- adapters: adapters(new Map()),
- });
-
- const result = await service.construct("conv-5", "/proj");
- expect(result).toBe("");
- expect(await service.get("conv-5")).toBe("");
- });
-
- it("construct is independent per conversation", async () => {
- const storage = memoryStorage();
- await storage.set("template", "[prompt:cwd]");
- const service = createSystemPromptService({
- storage,
- adapters: adapters(new Map()),
- });
-
- const a = await service.construct("conv-a", "/dir-a");
- const b = await service.construct("conv-b", "/dir-b");
-
- expect(a).toBe("/dir-a");
- expect(b).toBe("/dir-b");
- expect(await service.get("conv-a")).toBe("/dir-a");
- expect(await service.get("conv-b")).toBe("/dir-b");
- });
-
- it("DEFAULT_TEMPLATE contains the expected structure", () => {
- expect(DEFAULT_TEMPLATE).toContain("You are a helpful coding assistant.");
- expect(DEFAULT_TEMPLATE).toContain("[if file:AGENTS.md]");
- expect(DEFAULT_TEMPLATE).toContain("[file:AGENTS.md]");
- expect(DEFAULT_TEMPLATE).toContain("[prompt:cwd]");
- });
-
- it("getWithMeta on a never-constructed conversation returns { prompt: null, cwd: null }", async () => {
- // 1. never constructed → both fields null.
- const service = createSystemPromptService({
- storage: memoryStorage(),
- adapters: adapters(new Map()),
- });
-
- const meta = await service.getWithMeta("never-constructed");
- expect(meta).toEqual({ prompt: null, cwd: null, computerId: null });
- });
-
- it("getWithMeta after construct returns the resolved prompt and the exact cwd", async () => {
- // 2. after construct → prompt + exact cwd passed to construct.
- const service = createSystemPromptService({
- storage: memoryStorage(),
- adapters: adapters(new Map([["/proj/AGENTS.md", "RULES"]])),
- });
-
- const result = await service.construct("conv-meta", "/proj", { model: "gpt-4" });
- const meta = await service.getWithMeta("conv-meta");
-
- expect(meta.prompt).toBe(result);
- expect(meta.cwd).toBe("/proj");
- });
-
- it("get still returns the same value as before (backward compat)", async () => {
- // 3. get() behavior is unchanged by the additive getWithMeta.
- const service = createSystemPromptService({
- storage: memoryStorage(),
- adapters: adapters(new Map()),
- });
-
- // before construct → null
- expect(await service.get("conv-bc")).toBeNull();
-
- const result = await service.construct("conv-bc", "/proj");
- expect(await service.get("conv-bc")).toBe(result);
- });
-
- it("construct called twice with different cwds stores the latest cwd", async () => {
- // 4. second construct overwrites the cwd (not the first).
- const storage = memoryStorage();
- await storage.set("template", "[prompt:cwd]");
- const service = createSystemPromptService({
- storage,
- adapters: adapters(new Map()),
- });
-
- await service.construct("conv-twice", "/first");
- expect(await storage.get("resolved-cwd:conv-twice")).toBe("/first");
-
- const second = await service.construct("conv-twice", "/second");
- expect(second).toBe("/second");
- expect(await storage.get("resolved-cwd:conv-twice")).toBe("/second");
- expect(await storage.get("resolved-cwd:conv-twice")).not.toBe("/first");
- });
-
- it("getWithMeta after a second construct with a different cwd returns the new cwd and new prompt", async () => {
- // 5. getWithMeta reflects the latest construct, not the first.
- const storage = memoryStorage();
- await storage.set("template", "[prompt:cwd]");
- const service = createSystemPromptService({
- storage,
- adapters: adapters(new Map()),
- });
-
- const first = await service.construct("conv-second", "/dir-a");
- const firstMeta = await service.getWithMeta("conv-second");
- expect(firstMeta).toEqual({ prompt: first, cwd: "/dir-a", computerId: null });
-
- const second = await service.construct("conv-second", "/dir-b");
- const secondMeta = await service.getWithMeta("conv-second");
- expect(secondMeta).toEqual({ prompt: second, cwd: "/dir-b", computerId: null });
- expect(secondMeta.cwd).not.toBe("/dir-a");
- });
+ it("construct persists and returns the resolved string", async () => {
+ // 14. construct writes to storage and returns the resolved string.
+ const storage = memoryStorage();
+ const service = createSystemPromptService({
+ storage,
+ adapters: adapters(new Map([["/proj/AGENTS.md", "RULES"]])),
+ });
+
+ const result = await service.construct("conv-1", "/proj", { model: "gpt-4" });
+
+ expect(result).toContain("You are a helpful coding assistant.");
+ expect(result).toContain("RULES");
+ expect(result).toContain("/proj");
+ // persisted under resolved:<conversationId>
+ expect(await storage.get("resolved:conv-1")).toBe(result);
+ });
+
+ it("get returns persisted value after construct", async () => {
+ // 15. after construct, get returns the same string.
+ const service = createSystemPromptService({
+ storage: memoryStorage(),
+ adapters: adapters(new Map()),
+ });
+
+ // before construct → null
+ expect(await service.get("conv-2")).toBeNull();
+
+ const result = await service.construct("conv-2", "/proj");
+ expect(await service.get("conv-2")).toBe(result);
+ });
+
+ it("get returns null before construct", async () => {
+ const service = createSystemPromptService({
+ storage: memoryStorage(),
+ adapters: adapters(new Map()),
+ });
+
+ expect(await service.get("never-constructed")).toBeNull();
+ });
+
+ it("empty/no template stored → default template → non-empty", async () => {
+ // 16. no template stored → default template used → resolves to non-empty.
+ const service = createSystemPromptService({
+ storage: memoryStorage(),
+ adapters: adapters(new Map()), // no AGENTS.md
+ });
+
+ const result = await service.construct("conv-3", "/proj");
+
+ expect(result.length).toBeGreaterThan(0);
+ expect(result).toContain("You are a helpful coding assistant.");
+ expect(result).toContain("/proj");
+ // no AGENTS.md file → the [if file:AGENTS.md] block is omitted
+ expect(result).not.toContain("AGENTS.md");
+ });
+
+ it("stored template is used instead of default", async () => {
+ const storage = memoryStorage();
+ await storage.set("template", "cwd=[prompt:cwd] os=[system:os]");
+ const service = createSystemPromptService({
+ storage,
+ adapters: adapters(new Map()),
+ });
+
+ const result = await service.construct("conv-4", "/work");
+ expect(result).toBe("cwd=/work os=linux");
+ });
+
+ it("empty stored template → empty string", async () => {
+ const storage = memoryStorage();
+ await storage.set("template", "");
+ const service = createSystemPromptService({
+ storage,
+ adapters: adapters(new Map()),
+ });
+
+ const result = await service.construct("conv-5", "/proj");
+ expect(result).toBe("");
+ expect(await service.get("conv-5")).toBe("");
+ });
+
+ it("construct is independent per conversation", async () => {
+ const storage = memoryStorage();
+ await storage.set("template", "[prompt:cwd]");
+ const service = createSystemPromptService({
+ storage,
+ adapters: adapters(new Map()),
+ });
+
+ const a = await service.construct("conv-a", "/dir-a");
+ const b = await service.construct("conv-b", "/dir-b");
+
+ expect(a).toBe("/dir-a");
+ expect(b).toBe("/dir-b");
+ expect(await service.get("conv-a")).toBe("/dir-a");
+ expect(await service.get("conv-b")).toBe("/dir-b");
+ });
+
+ it("DEFAULT_TEMPLATE contains the expected structure", () => {
+ expect(DEFAULT_TEMPLATE).toContain("You are a helpful coding assistant.");
+ expect(DEFAULT_TEMPLATE).toContain("[if file:AGENTS.md]");
+ expect(DEFAULT_TEMPLATE).toContain("[file:AGENTS.md]");
+ expect(DEFAULT_TEMPLATE).toContain("[prompt:cwd]");
+ });
+
+ it("getWithMeta on a never-constructed conversation returns { prompt: null, cwd: null }", async () => {
+ // 1. never constructed → both fields null.
+ const service = createSystemPromptService({
+ storage: memoryStorage(),
+ adapters: adapters(new Map()),
+ });
+
+ const meta = await service.getWithMeta("never-constructed");
+ expect(meta).toEqual({ prompt: null, cwd: null, computerId: null });
+ });
+
+ it("getWithMeta after construct returns the resolved prompt and the exact cwd", async () => {
+ // 2. after construct → prompt + exact cwd passed to construct.
+ const service = createSystemPromptService({
+ storage: memoryStorage(),
+ adapters: adapters(new Map([["/proj/AGENTS.md", "RULES"]])),
+ });
+
+ const result = await service.construct("conv-meta", "/proj", { model: "gpt-4" });
+ const meta = await service.getWithMeta("conv-meta");
+
+ expect(meta.prompt).toBe(result);
+ expect(meta.cwd).toBe("/proj");
+ });
+
+ it("get still returns the same value as before (backward compat)", async () => {
+ // 3. get() behavior is unchanged by the additive getWithMeta.
+ const service = createSystemPromptService({
+ storage: memoryStorage(),
+ adapters: adapters(new Map()),
+ });
+
+ // before construct → null
+ expect(await service.get("conv-bc")).toBeNull();
+
+ const result = await service.construct("conv-bc", "/proj");
+ expect(await service.get("conv-bc")).toBe(result);
+ });
+
+ it("construct called twice with different cwds stores the latest cwd", async () => {
+ // 4. second construct overwrites the cwd (not the first).
+ const storage = memoryStorage();
+ await storage.set("template", "[prompt:cwd]");
+ const service = createSystemPromptService({
+ storage,
+ adapters: adapters(new Map()),
+ });
+
+ await service.construct("conv-twice", "/first");
+ expect(await storage.get("resolved-cwd:conv-twice")).toBe("/first");
+
+ const second = await service.construct("conv-twice", "/second");
+ expect(second).toBe("/second");
+ expect(await storage.get("resolved-cwd:conv-twice")).toBe("/second");
+ expect(await storage.get("resolved-cwd:conv-twice")).not.toBe("/first");
+ });
+
+ it("getWithMeta after a second construct with a different cwd returns the new cwd and new prompt", async () => {
+ // 5. getWithMeta reflects the latest construct, not the first.
+ const storage = memoryStorage();
+ await storage.set("template", "[prompt:cwd]");
+ const service = createSystemPromptService({
+ storage,
+ adapters: adapters(new Map()),
+ });
+
+ const first = await service.construct("conv-second", "/dir-a");
+ const firstMeta = await service.getWithMeta("conv-second");
+ expect(firstMeta).toEqual({ prompt: first, cwd: "/dir-a", computerId: null });
+
+ const second = await service.construct("conv-second", "/dir-b");
+ const secondMeta = await service.getWithMeta("conv-second");
+ expect(secondMeta).toEqual({ prompt: second, cwd: "/dir-b", computerId: null });
+ expect(secondMeta.cwd).not.toBe("/dir-a");
+ });
+
+ // ── resolveText: resolve an arbitrary template (no persistence) ────────────
+
+ it("resolveText substitutes [type:name] variables in an arbitrary template", async () => {
+ const service = createSystemPromptService({
+ storage: memoryStorage(),
+ adapters: adapters(new Map()),
+ });
+
+ const result = await service.resolveText(
+ "os=[system:os] cwd=[prompt:cwd] ws=[prompt:workspace_id] conv=[prompt:conversation_id] model=[prompt:model]",
+ "/work",
+ { model: "gpt-4", conversationId: "c1", workspaceId: "ws-1" },
+ );
+
+ expect(result).toBe("os=linux cwd=/work ws=ws-1 conv=c1 model=gpt-4");
+ });
+
+ it("resolveText uses the SAME resolution as construct (same resolver + variables)", async () => {
+ // A template resolved via resolveText must equal the same template
+ // resolved via construct (both go through the shared resolveTemplate).
+ const storage = memoryStorage();
+ const template = "os=[system:os] cwd=[prompt:cwd] date=[system:date]";
+ await storage.set("template", template);
+ const service = createSystemPromptService({
+ storage,
+ adapters: adapters(new Map()),
+ });
+
+ const viaConstruct = await service.construct("conv-rt", "/work");
+ const viaResolveText = await service.resolveText(template, "/work");
+ expect(viaResolveText).toBe(viaConstruct);
+ });
+
+ it("resolveText does NOT persist (unlike construct)", async () => {
+ const storage = memoryStorage();
+ const service = createSystemPromptService({
+ storage,
+ adapters: adapters(new Map()),
+ });
+
+ await service.resolveText("[prompt:cwd]", "/work", { conversationId: "no-persist" });
+
+ // No resolved:* / resolved-cwd:* / resolved-computer:* keys written.
+ expect(await storage.get("resolved:no-persist")).toBeNull();
+ expect(await storage.get("resolved-cwd:no-persist")).toBeNull();
+ expect(await storage.get("resolved-computer:no-persist")).toBeNull();
+ });
+
+ it("resolveText resolves dynamic file:<path> variables referenced by the template", async () => {
+ const service = createSystemPromptService({
+ storage: memoryStorage(),
+ adapters: adapters(new Map([["/work/AGENTS.md", "RULES"]])),
+ });
+
+ const result = await service.resolveText("[file:AGENTS.md]", "/work");
+ expect(result).toBe("RULES");
+ });
+
+ it("resolveText handles conditional blocks ([if]/[else]/[endif])", async () => {
+ const service = createSystemPromptService({
+ storage: memoryStorage(),
+ adapters: adapters(new Map()),
+ });
+
+ // [prompt:model] exists (string) → then-branch renders.
+ const withModel = await service.resolveText(
+ "[if prompt:model]has model[else]no model[endif]",
+ "/work",
+ { model: "gpt-4" },
+ );
+ expect(withModel).toBe("has model");
+
+ // model absent → else-branch renders.
+ const noModel = await service.resolveText(
+ "[if prompt:model]has model[else]no model[endif]",
+ "/work",
+ );
+ expect(noModel).toBe("no model");
+ });
+
+ it("resolveText leaves unknown [type:name] tags substituted as empty (mirrors parser)", async () => {
+ const service = createSystemPromptService({
+ storage: memoryStorage(),
+ adapters: adapters(new Map()),
+ });
+
+ // An unknown variable (not in the catalog) → empty string, matching
+ // parseTemplate's "key absent from the map → empty string" rule.
+ const result = await service.resolveText("x=[unknown:thing]", "/work");
+ expect(result).toBe("x=");
+ });
+
+ it("resolveText on an empty template returns an empty string", async () => {
+ const service = createSystemPromptService({
+ storage: memoryStorage(),
+ adapters: adapters(new Map()),
+ });
+ expect(await service.resolveText("", "/work")).toBe("");
+ });
});