profile-completeness.test.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. /**
  2. * Profile completeness — every profile's transitive closure must be installable.
  3. *
  4. * A profile is a promise: "install `developer` and you get a working set". That promise is
  5. * only kept if every component the profile names resolves, AND every dependency of those
  6. * components resolves, recursively. A profile that names a live component which depends on a
  7. * dead one is just as broken as a profile that names the dead one directly — the install
  8. * fails at the same point, one level deeper.
  9. *
  10. * `validate-registry.sh` checks NEITHER half of this: it never reads `.profiles.*` at all
  11. * (see reference-resolution.test.ts), which is how `context:context-system/*` has sat in the
  12. * `advanced` profile expanding to zero components while the validator prints 244/244 green.
  13. */
  14. import { describe, it, expect } from "vitest";
  15. import {
  16. format,
  17. loadRegistry,
  18. profileReferences,
  19. resolveAll,
  20. type Reference,
  21. type Registry,
  22. } from "../../support/references.js";
  23. import { importPendingSymbols, repoPath } from "../../support/pending.js";
  24. const PROFILES = ["advanced", "business", "developer", "essential", "full"] as const;
  25. /** The one profile ref that does not resolve today. See reference-resolution.test.ts. */
  26. const KNOWN_DEAD_PROFILE_REF = "context:context-system/*";
  27. function refsFor(profile: string, registry: Registry): Reference[] {
  28. return profileReferences(registry).filter((reference) =>
  29. reference.source.endsWith(`profiles.${profile}.components`)
  30. );
  31. }
  32. /**
  33. * Expand a profile to its transitive closure of registry ids, following each component's
  34. * declared `dependencies`. Cycles terminate on the `seen` set.
  35. */
  36. function closure(profile: string, registry: Registry): { ids: Set<string>; refs: Reference[] } {
  37. const byRef = new Map<string, { dependencies?: string[] }>();
  38. for (const [category, components] of Object.entries(registry.components)) {
  39. // agents -> agent, contexts -> context, ... and `config` (already singular) stays put.
  40. const type = category.replace(/s$/, "");
  41. for (const component of components) byRef.set(`${type}:${component.id}`, component);
  42. }
  43. const ids = new Set<string>();
  44. const refs: Reference[] = [];
  45. const queue = refsFor(profile, registry);
  46. while (queue.length > 0) {
  47. const reference = queue.shift()!;
  48. if (ids.has(reference.ref)) continue;
  49. ids.add(reference.ref);
  50. refs.push(reference);
  51. for (const dependency of byRef.get(reference.ref)?.dependencies ?? []) {
  52. queue.push({
  53. ref: dependency,
  54. source: `${reference.source} -> ${reference.ref} depends on`,
  55. });
  56. }
  57. }
  58. return { ids, refs };
  59. }
  60. // ============================================================================
  61. // Green today — the profile corpus, and the closure minus the one known hole
  62. // ============================================================================
  63. describe("profiles", () => {
  64. it("registry carries exactly the 5 profiles on disk", () => {
  65. expect(Object.keys(loadRegistry().profiles).sort()).toEqual([...PROFILES].sort());
  66. });
  67. it.each(PROFILES)("%s names at least one component", (profile) => {
  68. expect(refsFor(profile, loadRegistry()).length).toBeGreaterThan(0);
  69. });
  70. it.each(PROFILES)("%s resolves every component it names, except the known dead ref", (profile) => {
  71. const registry = loadRegistry();
  72. const dead = resolveAll(refsFor(profile, registry), registry)
  73. .filter((resolution) => resolution.status !== "ok")
  74. .filter((resolution) => resolution.ref !== KNOWN_DEAD_PROFILE_REF);
  75. expect(dead, `${profile} names components that do not exist:\n${format(dead)}`).toEqual([]);
  76. });
  77. it.each(PROFILES)("%s has an installable transitive closure, except the known dead ref", (profile) => {
  78. const registry = loadRegistry();
  79. const { refs } = closure(profile, registry);
  80. const dead = resolveAll(refs, registry)
  81. .filter((resolution) => resolution.status !== "ok")
  82. .filter((resolution) => resolution.ref !== KNOWN_DEAD_PROFILE_REF);
  83. expect(
  84. dead,
  85. `${profile}'s closure reaches components that do not exist — installing it would fail:\n${format(dead)}`
  86. ).toEqual([]);
  87. });
  88. it("advanced is the profile carrying the known dead wildcard", () => {
  89. const registry = loadRegistry();
  90. const carriers = PROFILES.filter((profile) =>
  91. refsFor(profile, registry).some((r) => r.ref === KNOWN_DEAD_PROFILE_REF)
  92. );
  93. expect(carriers).toEqual(["advanced"]);
  94. });
  95. it("the closure is strictly larger than the named set for at least one profile", () => {
  96. // Guards the test itself: if dependency-following silently did nothing, every closure
  97. // would equal its profile's own list and these tests would prove much less than they read.
  98. const registry = loadRegistry();
  99. const grew = PROFILES.some(
  100. (profile) => closure(profile, registry).ids.size > refsFor(profile, registry).length
  101. );
  102. expect(grew, "closure() followed no dependencies — it is not testing transitivity").toBe(true);
  103. });
  104. });
  105. // ============================================================================
  106. // RED — the shipped profile loader (subtask 05)
  107. // ============================================================================
  108. const OWED_BY = "subtask 05 (src/core/ProfileLoader.ts)";
  109. describe("ProfileLoader (shipped)", () => {
  110. it("loads all 5 profiles", async () => {
  111. const { ProfileLoader } = await importPendingSymbols<{
  112. ProfileLoader: new (root: string) => { list(): Promise<string[]> };
  113. }>(
  114. "src/core/ProfileLoader.ts",
  115. ["ProfileLoader"],
  116. OWED_BY,
  117. "the build can enumerate the 5 profiles rather than hard-coding them"
  118. );
  119. expect((await new ProfileLoader(repoPath()).list()).sort()).toEqual([...PROFILES].sort());
  120. });
  121. it.each(PROFILES)("reports %s's closure as installable", async (profile) => {
  122. const { ProfileLoader } = await importPendingSymbols<{
  123. ProfileLoader: new (root: string) => {
  124. resolveClosure(profile: string): Promise<{ missing: { ref: string }[] }>;
  125. };
  126. }>(
  127. "src/core/ProfileLoader.ts",
  128. ["ProfileLoader"],
  129. OWED_BY,
  130. `every component in ${profile}'s transitive closure resolves, so installing it works`
  131. );
  132. const { missing } = await new ProfileLoader(repoPath()).resolveClosure(profile);
  133. const unexpected = missing.filter((entry) => entry.ref !== KNOWN_DEAD_PROFILE_REF);
  134. expect(unexpected).toEqual([]);
  135. });
  136. });