Phase 1 — Fragment infrastructure:
- Add {{include:fragment-name}} support to prompt-loader.js
- fragmentsDir registered alongside promptsDir/templatesDir
- warmCache() now reads prompts/fragments/*.md with 'frg:' prefix
- Pre-resolution pass in loadPrompt() resolves {{include:}} before
the {{var}} validator (colon is outside validator regex [a-zA-Z0-9_],
so unresolved includes are caught as parse errors)
- Lazy-load fallback for fragments mirrors existing prompt lazy-load
- Create prompts/fragments/working-directory.md (Variant A: full
contract including 'Do NOT cd to any other directory')
- Create prompts/fragments/working-directory-ops.md (Variant B:
ops prompts, no cd restriction)
- Replace duplicated 3-line Working Directory boilerplate in 17 prompts
with {{include:working-directory}} (12 files) or
{{include:working-directory-ops}} (5 ops files)
- One fix to Working Directory wording now propagates to all 17 prompts
Phase 2 — RFC #4782 stub manifests:
- Add deploy, smoke-production, release, rollback, challenge to
KNOWN_UNIT_TYPES and UNIT_MANIFESTS in unit-context-manifest.js
- All 5 builders already called composeInlinedContext() but returned ""
because resolveManifest() found no entry; now they return live content
- All 26 unit types now have manifests (resolveManifest returns non-null
for every type in KNOWN_UNIT_TYPES)
Tests:
- 5 new tests in prompt-loader-fragments.test.mjs (include resolution,
lazy-load fallback, unknown fragment error, nested var inheritance,
variant-B fragment)
- Full unit suite: 427 files passed, 4476 tests passed, 0 regressions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
108 lines
3.3 KiB
JavaScript
108 lines
3.3 KiB
JavaScript
/**
|
|
* prompt-loader-fragments.test.mjs — fragment include resolution contracts.
|
|
*
|
|
* Purpose: verify that {{include:name}} directives in prompt templates are
|
|
* resolved before variable validation so fragment-provided placeholder text
|
|
* participates in normal {{var}} substitution.
|
|
*
|
|
* Consumer: prompt-loader.js warmCache + loadPrompt.
|
|
*/
|
|
import { rmSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { afterEach, describe, expect, test } from "vitest";
|
|
import { loadPrompt } from "../prompt-loader.js";
|
|
|
|
const promptsDir = join(import.meta.dirname, "..", "prompts");
|
|
const fragmentsDir = join(promptsDir, "fragments");
|
|
|
|
// Temp fixture files written during tests — cleaned up in afterEach.
|
|
const tempFiles = [];
|
|
|
|
function writeFixturePrompt(name, content) {
|
|
const path = join(promptsDir, `${name}.md`);
|
|
writeFileSync(path, content);
|
|
tempFiles.push(path);
|
|
return name;
|
|
}
|
|
|
|
function writeFixtureFragment(name, content) {
|
|
const path = join(fragmentsDir, `${name}.md`);
|
|
writeFileSync(path, content);
|
|
tempFiles.push(path);
|
|
return name;
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const p of tempFiles.splice(0)) rmSync(p, { force: true });
|
|
});
|
|
|
|
describe("fragment include resolution", () => {
|
|
test("fragment_resolved_and_var_substituted_in_one_pass", () => {
|
|
// Write a minimal fragment and a minimal prompt that includes it.
|
|
writeFixtureFragment("testfrag-wd-simple", "## WD\n\nPath: `{{workingDirectory}}`.");
|
|
writeFixturePrompt("testprompt-include-simple", "{{include:testfrag-wd-simple}}\n");
|
|
|
|
const result = loadPrompt("testprompt-include-simple", {
|
|
workingDirectory: "/my/project",
|
|
});
|
|
|
|
expect(result).toContain("## WD");
|
|
expect(result).toContain("`/my/project`");
|
|
});
|
|
|
|
test("working_directory_fragment_resolves_in_real_prompt", () => {
|
|
// Use a real migrated prompt that includes {{include:working-directory}}.
|
|
// Write a minimal wrapper prompt so we control the exact var set.
|
|
writeFixturePrompt(
|
|
"testprompt-wd-real",
|
|
"# Header\n\n{{include:working-directory}}\n",
|
|
);
|
|
|
|
const result = loadPrompt("testprompt-wd-real", {
|
|
workingDirectory: "/real/path",
|
|
});
|
|
|
|
expect(result).toContain("## Working Directory");
|
|
expect(result).toContain("`/real/path`");
|
|
expect(result).toContain("Do NOT `cd` to any other directory");
|
|
});
|
|
|
|
test("ops_fragment_omits_no_cd_restriction", () => {
|
|
writeFixturePrompt(
|
|
"testprompt-wd-ops",
|
|
"# Header\n\n{{include:working-directory-ops}}\n",
|
|
);
|
|
|
|
const result = loadPrompt("testprompt-wd-ops", {
|
|
workingDirectory: "/ops/path",
|
|
});
|
|
|
|
expect(result).toContain("## Working Directory");
|
|
expect(result).toContain("`/ops/path`");
|
|
expect(result).not.toContain("Do NOT `cd`");
|
|
});
|
|
|
|
test("unknown_fragment_throws_parse_error", () => {
|
|
writeFixturePrompt(
|
|
"testprompt-unknown-frag",
|
|
"{{include:this-fragment-does-not-exist-xyzzy}}\n",
|
|
);
|
|
|
|
expect(() => loadPrompt("testprompt-unknown-frag", {})).toThrow(
|
|
/unknown fragment.*this-fragment-does-not-exist-xyzzy/,
|
|
);
|
|
});
|
|
|
|
test("prompts_without_includes_unaffected", () => {
|
|
// A plain template with no {{include:}} must still load and substitute cleanly.
|
|
writeFixturePrompt(
|
|
"testprompt-no-include",
|
|
"# Plain\n\nValue: {{someVar}}.\n",
|
|
);
|
|
|
|
const result = loadPrompt("testprompt-no-include", { someVar: "hello" });
|
|
|
|
expect(result).toBe("# Plain\n\nValue: hello.");
|
|
expect(result).not.toContain("{{include:");
|
|
});
|
|
});
|