singularity-forge/src/resources/extensions/sf/tests/prompt-loader-fragments.test.mjs
Mikael Hugo ca5d869e34 feat(prompts): fragment infrastructure + RFC #4782 stub manifests
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>
2026-05-13 00:30:19 +02:00

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:");
});
});