| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590 |
- /**
- * Registry emission — `registry.json` is GENERATED from `content/agents/**`, not hand-edited.
- *
- * ─── What is actually at stake ──────────────────────────────────────────────────────────
- *
- * `install.sh` reads `registry.json` and is the only shipping install path. Every assertion
- * here is really a statement about whether a real user can install this repo, which is why
- * this suite leans on the LIVE corpus rather than fixtures alone: a fixture tree cannot tell
- * us that the thing we ship is intact.
- *
- * Three properties matter, in this order:
- *
- * 1. Nothing that install.sh reads is lost (carry-through, aliases, eval-runner).
- * 2. The output is byte-stable and a fixed point over itself, or the subtask-11
- * `oac build && git diff --exit-code` gate is a coin flip.
- * 3. The generated entries actually come from the canonical files.
- *
- * ─── Two snapshots of known defects live here ───────────────────────────────────────────
- *
- * {@link MISSING_FROM_REGISTRY} and {@link REGISTRY_ONLY_DEPENDENCIES} pin bugs, not
- * invariants — same pattern as `KNOWN_DEAD` in `reference-resolution.test.ts`. When a subtask
- * repairs one, the test turns red and forces a deliberate edit here. That is the point: the
- * drift these record is exactly the kind that accumulated silently for months because the only
- * validator in the repo could not see it.
- */
- import { describe, it, expect } from "vitest";
- import { mkdtempSync, readFileSync, writeFileSync, rmSync } from "node:fs";
- import { tmpdir } from "node:os";
- import { join } from "node:path";
- import {
- RegistryEmitter,
- emitRegistry,
- serializeRegistry,
- entryForAgent,
- type RegistryDocument,
- type RegistryEntry,
- } from "../../../src/core/RegistryEmitter.js";
- import { generatedPaths } from "../../../src/core/BuildManifest.js";
- import { CanonicalAgentLoader } from "../../../src/core/AgentLoader.js";
- import { repoPath } from "../../support/pending.js";
- const COMMITTED_JSON = readFileSync(repoPath("registry.json"), "utf-8");
- const COMMITTED = JSON.parse(COMMITTED_JSON) as RegistryDocument;
- /** Categories phase 1 generates. Everything else is carried through verbatim. */
- const GENERATED_CATEGORIES = ["agents", "subagents"] as const;
- /** Anything that would make two runs differ. */
- const NONDETERMINISM = [
- { name: "an ISO timestamp", pattern: /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/ },
- { name: "a 'generated at' stamp", pattern: /generated (at|on)[:\s]/i },
- { name: "an absolute home path", pattern: /\/(Users|home)\/[^/\s"]+/ },
- ];
- /**
- * Agents that ship on disk, are referenced, and were ABSENT from the registry until subtask 10
- * generated it.
- *
- * Verified on disk 2026-07-15. `batch-executor` is the load-bearing one: both `openagent` and
- * `opencoder` declare `subagent:batch-executor`, so the registry advertised a dependency on a
- * component it did not contain. The other six were in neither the registry nor
- * `.opencode/config/agent-metadata.json` — they were added to `.opencode/agent/` and nothing
- * ever noticed. `auto-detect-components.sh --dry-run` reported all 7 as "New Components".
- *
- * ─── Updated by subtask 10, deliberately ────────────────────────────────────────────────
- *
- * This list used to assert the committed registry OMITS these 7 — it pinned the bug. Subtask
- * 10 ran `oac build`, which emits `registry.json` in place, so all 7 are now registered and
- * that assertion inverted. Per this file's own contract ("when a subtask repairs one, the test
- * turns red and forces a deliberate edit here"), the edit is made rather than the test
- * relaxed: the list now guards that generation KEEPS registering them. It stops being a
- * snapshot of a loss and becomes a regression guard, exactly as
- * {@link REGISTRY_ONLY_DEPENDENCIES} did when subtask 09b backfilled the 13 edges.
- */
- const RECOVERED_BY_GENERATION = [
- "adr-manager",
- "architecture-analyzer",
- "batch-executor",
- "contract-manager",
- "prioritization-engine",
- "stage-orchestrator",
- "story-mapper",
- ] as const;
- /**
- * Dependency edges that were once authored ONLY in `registry.json` — no agent file and no
- * sidecar entry declared them, so generating from the canonical tree used to DROP them.
- *
- * That was a real regression, not a cleanup: `install.sh` resolves these transitively
- * (`resolve_dependencies`, install.sh:420), so installing `subagent:externalscout` pulls in
- * `skill:context7` and would have stopped doing so. They were hand-authored straight into
- * `registry.json` and never made it into any file, which is precisely the drift this refactor
- * exists to end.
- *
- * Subtask 09b backfilled all 13 into `oac.dependencies` in `content/agents/**`, in the exact
- * order `registry.json` declares them, so the emitter now reproduces every one. This list is
- * therefore no longer a snapshot of a loss — it is the guard that the backfill stays put. Each
- * edge was verified to resolve against the registry before being carried over; the sibling dead
- * ref `context:context-system/*` (profiles-only, expands to 0) was deliberately NOT introduced
- * here — see `tests/unit/build/reference-resolution.test.ts`.
- */
- const REGISTRY_ONLY_DEPENDENCIES: Readonly<Record<string, readonly string[]>> = {
- contextscout: [
- "command:check-context-deps",
- "context:registry-dependencies",
- "context:context-system",
- "context:mvi",
- "context:structure",
- "context:workflows",
- "subagent:externalscout",
- "context:root-navigation",
- "context:context-paths-config",
- ],
- externalscout: ["skill:context7", "context:context-system"],
- "image-specialist": ["tool:gemini"],
- "context-organizer": ["context:core/context-system/*"],
- };
- function entries(document: RegistryDocument, category: string): RegistryEntry[] {
- return document.components[category] ?? [];
- }
- function byId(document: RegistryDocument, category: string): Map<string, RegistryEntry> {
- return new Map(entries(document, category).map((entry) => [entry.id, entry]));
- }
- /** A throwaway repo root containing only a `registry.json`, for fixed-point testing. */
- function scratchRoot(registryJson: string): string {
- const root = mkdtempSync(join(tmpdir(), "oac-registry-"));
- writeFileSync(join(root, "registry.json"), registryJson);
- return root;
- }
- // ============================================================================
- // SERIALISATION — the diff must be semantic, never formatting
- // ============================================================================
- describe("serializeRegistry", () => {
- it("reproduces the committed registry.json byte-for-byte from its own parse", () => {
- // If this drifts, every generated-vs-committed diff drowns in whitespace and escaping
- // noise and the subtask-11 gate stops meaning anything. It also pins the non-ASCII
- // escaping: the committed file writes `—` and two emoji as escapes, which bare
- // `JSON.stringify` would emit as raw UTF-8.
- expect(serializeRegistry(COMMITTED)).toBe(COMMITTED_JSON);
- });
- it("escapes non-ASCII rather than emitting raw UTF-8", () => {
- const json = serializeRegistry({ components: {}, note: "em—dash ⛔" } as RegistryDocument);
- expect(json).toContain("em\\u2014dash \\u26d4");
- expect(json).not.toContain("—");
- });
- it("ends with exactly one trailing newline", () => {
- expect(serializeRegistry(COMMITTED).endsWith("}\n")).toBe(true);
- expect(serializeRegistry(COMMITTED).endsWith("}\n\n")).toBe(false);
- });
- });
- // ============================================================================
- // DETERMINISM
- // ============================================================================
- describe("determinism", () => {
- it("emits byte-identical output across two runs", async () => {
- expect(await emitRegistry(repoPath())).toBe(await emitRegistry(repoPath()));
- });
- it("emits nothing that varies between runs", async () => {
- const json = await emitRegistry(repoPath());
- for (const { name, pattern } of NONDETERMINISM) {
- expect(json, `generated registry.json contains ${name}`).not.toMatch(pattern);
- }
- });
- it("carries metadata.lastUpdated from the base instead of stamping the clock", async () => {
- // auto-detect-components.sh:872 did `now | strftime("%Y-%m-%d")`, which alone would make
- // every rebuild a diff and every `git diff --exit-code` gate fail on an unchanged tree.
- const document = await new RegistryEmitter(repoPath()).emit();
- expect(document.metadata).toEqual(COMMITTED.metadata);
- });
- it("sorts generated entries by id, not by filesystem or insertion order", async () => {
- const document = await new RegistryEmitter(repoPath()).emit();
- for (const category of GENERATED_CATEGORIES) {
- const ids = entries(document, category).map((entry) => entry.id);
- expect(ids, `${category} is not id-sorted`).toEqual([...ids].sort());
- }
- });
- it("is a fixed point over its own output", async () => {
- // Subtask 10 writes this output back to registry.json, and the emitter then reads that as
- // its own base. If emitting were not idempotent the tree would oscillate and the diff gate
- // would fail forever on a tree nobody touched.
- const first = await emitRegistry(repoPath());
- const root = scratchRoot(first);
- try {
- const second = await new RegistryEmitter(root, {
- contentRoot: repoPath("content/agents"),
- }).emitJson();
- expect(second).toBe(first);
- } finally {
- rmSync(root, { recursive: true, force: true });
- }
- });
- });
- // ============================================================================
- // CARRY-THROUGH — phase 1 owns agents only
- // ============================================================================
- describe("carry-through of everything the canonical tree does not own", () => {
- it("leaves non-agent component categories byte-identical", async () => {
- const document = await new RegistryEmitter(repoPath()).emit();
- const carried = Object.keys(COMMITTED.components).filter(
- (category) => !GENERATED_CATEGORIES.includes(category as (typeof GENERATED_CATEGORIES)[number])
- );
- expect(carried).toContain("contexts");
- for (const category of carried) {
- expect(entries(document, category), `${category} was modified`).toEqual(
- entries(COMMITTED, category)
- );
- }
- });
- it("leaves every non-components top-level key untouched, in order", async () => {
- const document = await new RegistryEmitter(repoPath()).emit();
- expect(Object.keys(document)).toEqual(Object.keys(COMMITTED));
- for (const key of Object.keys(COMMITTED).filter((k) => k !== "components")) {
- expect(document[key], `top-level "${key}" was modified`).toEqual(COMMITTED[key]);
- }
- });
- it("preserves the component category order", async () => {
- const document = await new RegistryEmitter(repoPath()).emit();
- expect(Object.keys(document.components)).toEqual(Object.keys(COMMITTED.components));
- });
- it("preserves context aliases", async () => {
- // install.sh resolve_dependencies matches `.id == $id or (.aliases | index($id))`
- // (install.sh:420). Dropping an alias silently uninstalls a component for anyone naming
- // it the aliased way.
- const document = await new RegistryEmitter(repoPath()).emit();
- const aliased = entries(document, "contexts").filter((entry) => entry.aliases !== undefined);
- expect(aliased.map((entry) => entry.id).sort()).toEqual([
- "component-planning",
- "feature-breakdown",
- "session-management",
- ]);
- });
- it("keeps registry profiles verbatim and never consults profile.json", async () => {
- // `.profiles` is what install.sh reads (get_profile_components, install.sh:292).
- // `.opencode/profiles/<name>/profile.json` has drifted from it on all 5 profiles and is
- // read only by check-dependencies.ts, which no install path invokes. Reconciling them is
- // a content decision; generating from the wrong one silently changes what users install.
- const document = await new RegistryEmitter(repoPath()).emit();
- expect(document.profiles).toEqual(COMMITTED.profiles);
- });
- it("leaves the dead advanced-profile wildcard exactly as authored", async () => {
- // `context:context-system/*` expands to zero registry paths. Repairing it to
- // `context:core/context-system/*` would change what `--profile advanced` installs, so it
- // stays a known-dead ref that ReferenceResolver reports rather than an emitter rewrite.
- const document = await new RegistryEmitter(repoPath()).emit();
- const advanced = document.profiles as Record<string, { components: string[] }>;
- expect(advanced.advanced?.components).toContain("context:context-system/*");
- });
- it("carries agents that have no canonical counterpart", async () => {
- // `agent:eval-runner` ships in `.opencode/agent/eval-runner.md` and is in the committed
- // registry, but `content/agents/` does not carry it yet. Generating agents purely from the
- // canonical tree would delete it and uninstall the eval harness.
- const document = await new RegistryEmitter(repoPath()).emit();
- expect(byId(document, "agents").get("eval-runner")).toEqual(
- byId(COMMITTED, "agents").get("eval-runner")
- );
- });
- it("never drops an id the committed registry publishes", async () => {
- const document = await new RegistryEmitter(repoPath()).emit();
- for (const category of Object.keys(COMMITTED.components)) {
- const generated = byId(document, category);
- const dropped = entries(COMMITTED, category)
- .map((entry) => entry.id)
- .filter((id) => !generated.has(id));
- expect(dropped, `${category} ids dropped by the emitter`).toEqual([]);
- }
- });
- });
- // ============================================================================
- // WITHDRAWAL — an entry we generated must not outlive its canonical source
- // ============================================================================
- /**
- * The other half of carry-through, and the harder half.
- *
- * Carrying every unclaimed base entry is what keeps `eval-runner` alive, but applied blindly it
- * recreates the defect this emitter exists to cure: delete `content/agents/x.md` and the build
- * removes `.opencode/agent/x.md` while leaving `x` in the registry forever. The output is a
- * stable fixed point, so `oac build && git diff --exit-code` stays green while being wrong —
- * the drift gate cannot see it.
- *
- * By id alone "never had a source" and "source was deleted" are identical. The discriminator is
- * `.oac/build-manifest.json`: an entry whose `path` the ledger claims was written by us.
- */
- describe("withdrawal of entries the build itself generated", () => {
- /** A path the ledger claims — i.e. one the build wrote on a previous run. */
- const GENERATED_PATH = ".opencode/agent/subagents/utils/demo-agent.md";
- /** `eval-runner`'s file: shipped, hand-authored, and never generated by us. */
- const EVAL_RUNNER_PATH = ".opencode/agent/eval-runner.md";
- /**
- * The committed registry plus an entry for an agent the build generated and whose canonical
- * source has since been deleted — the exact state a deletion build reads as its base.
- */
- function baseWithDeletedSource(): RegistryDocument {
- const document = JSON.parse(COMMITTED_JSON) as RegistryDocument;
- document.components.subagents = [
- ...entries(document, "subagents"),
- {
- id: "demo-agent",
- name: "Demo Agent",
- type: "subagent",
- path: GENERATED_PATH,
- version: "1.0.0",
- description: "Generated by a previous build; its canonical source is now gone.",
- tags: ["demo"],
- dependencies: [],
- category: "subagents/utils",
- },
- ];
- return document;
- }
- /** Emit against a scratch base and an explicit ledger, over the live canonical tree. */
- async function emitWith(
- base: RegistryDocument,
- ledger: ReadonlySet<string>
- ): Promise<RegistryDocument> {
- const root = scratchRoot(serializeRegistry(base));
- try {
- return await new RegistryEmitter(root, {
- contentRoot: repoPath("content/agents"),
- generatedPaths: ledger,
- }).emit();
- } finally {
- rmSync(root, { recursive: true, force: true });
- }
- }
- it("drops an entry whose path the ledger claims once its canonical source is gone", async () => {
- // The bug: `merge` carried this forever because `demo-agent` is simply "not in generated",
- // which is also true of eval-runner. The ledger is what tells them apart.
- const document = await emitWith(baseWithDeletedSource(), new Set([GENERATED_PATH]));
- expect(byId(document, "subagents").has("demo-agent")).toBe(false);
- });
- it("keeps eval-runner while withdrawing a generated entry in the same merge", async () => {
- // The regression that matters most: a fix that drops the deleted agent by also dropping
- // every sourceless entry uninstalls the eval harness. Both must hold at once.
- const document = await emitWith(baseWithDeletedSource(), new Set([GENERATED_PATH]));
- expect(byId(document, "subagents").has("demo-agent")).toBe(false);
- expect(byId(document, "agents").get("eval-runner")).toEqual(
- byId(COMMITTED, "agents").get("eval-runner")
- );
- });
- it("carries every entry when there is no ledger, so a first build deletes nothing", async () => {
- // Matches BuildPipeline's rule: no record of having written a file is not evidence that we
- // wrote it. A fresh clone has no manifest and must not withdraw anything.
- const document = await emitWith(baseWithDeletedSource(), new Set());
- expect(byId(document, "subagents").get("demo-agent")?.path).toBe(GENERATED_PATH);
- expect(byId(document, "agents").has("eval-runner")).toBe(true);
- });
- it("carries an entry the ledger does not claim, however the ledger is populated", async () => {
- // eval-runner survives against the REAL ledger, not just a hand-made one.
- const document = await emitWith(baseWithDeletedSource(), generatedPaths(repoPath()));
- expect(byId(document, "agents").get("eval-runner")).toEqual(
- byId(COMMITTED, "agents").get("eval-runner")
- );
- });
- it("has a ledger that claims every generated agent file and not eval-runner", async () => {
- // The discriminator is only real if the live ledger actually separates the two populations.
- // If a later subtask canonicalises eval-runner this turns red and forces a deliberate edit:
- // the entry would then be generated by id and no longer need carrying at all.
- const ledger = generatedPaths(repoPath());
- const claimed = (id: string): boolean =>
- ledger.has(byId(COMMITTED, "agents").get(id)?.path ?? "");
- expect(ledger.has(EVAL_RUNNER_PATH), "eval-runner is in the ledger").toBe(false);
- expect(claimed("openagent"), "a generated agent is missing from the ledger").toBe(true);
- for (const category of GENERATED_CATEGORIES) {
- const unclaimed = entries(COMMITTED, category)
- .filter((entry) => !ledger.has(entry.path ?? ""))
- .map((entry) => entry.id);
- expect(unclaimed, `${category} entries the ledger does not claim`).toEqual(
- category === "agents" ? ["eval-runner"] : []
- );
- }
- });
- });
- // ============================================================================
- // GENERATION — entries come from the canonical files
- // ============================================================================
- describe("generation from the canonical tree", () => {
- it("derives every field of an entry from the canonical file", async () => {
- const document = await new RegistryEmitter(repoPath()).emit();
- const agents = await new CanonicalAgentLoader(repoPath("content/agents")).loadFromDirectory();
- const tester = agents.find((agent) => agent.oac.id === "tester");
- expect(tester).toBeDefined();
- expect(byId(document, "subagents").get("tester")).toEqual({
- id: "tester",
- name: "TestEngineer",
- type: "subagent",
- // Identity is oac.id, never the filename: the file is `test-engineer.md` but the
- // registry, the profiles and every context doc reference `tester`.
- path: ".opencode/agent/subagents/code/test-engineer.md",
- version: "1.0.0",
- description: tester!.frontmatter.description,
- tags: ["testing", "tdd", "quality"],
- dependencies: ["context:standards-tests"],
- category: "subagents/code",
- });
- });
- it("emits entry keys in the committed field order", async () => {
- const document = await new RegistryEmitter(repoPath()).emit();
- for (const category of GENERATED_CATEGORIES) {
- for (const entry of entries(document, category)) {
- if (entry.id === "eval-runner") continue; // carried through, keeps its own shape
- expect(Object.keys(entry), `${entry.id} field order`).toEqual([
- "id",
- "name",
- "type",
- "path",
- "version",
- "description",
- "tags",
- "dependencies",
- "category",
- ]);
- }
- }
- });
- it("points path at the built install tree, not the canonical source", async () => {
- // install.sh copies `.path` verbatim. If it named `content/agents/**` the installer would
- // fetch files that do not exist in the published layout.
- const document = await new RegistryEmitter(repoPath()).emit();
- for (const category of GENERATED_CATEGORIES) {
- for (const entry of entries(document, category)) {
- expect(entry.path, `${entry.id} path`).toMatch(/^\.opencode\/agent\//);
- }
- }
- });
- it("registers every canonical agent", async () => {
- const document = await new RegistryEmitter(repoPath()).emit();
- const agents = await new CanonicalAgentLoader(repoPath("content/agents")).loadFromDirectory();
- const registered = new Set([
- ...byId(document, "agents").keys(),
- ...byId(document, "subagents").keys(),
- ]);
- const unregistered = agents.map((a) => a.oac.id).filter((id) => !registered.has(id));
- expect(unregistered, "canonical agents the emitter failed to register").toEqual([]);
- });
- it("files an agent under the components key its oac.type names", async () => {
- const document = await new RegistryEmitter(repoPath()).emit();
- for (const entry of entries(document, "agents")) expect(entry.type).toBe("agent");
- for (const entry of entries(document, "subagents")) expect(entry.type).toBe("subagent");
- });
- it("resolves a fixture tree independently of the live corpus", async () => {
- const agents = await new CanonicalAgentLoader(repoPath("content/agents")).loadFromDirectory();
- const openagent = agents.find((agent) => agent.oac.id === "openagent");
- expect(entryForAgent(openagent!, ".opencode/agent")).toMatchObject({
- id: "openagent",
- type: "agent",
- path: ".opencode/agent/core/openagent.md",
- category: "core",
- });
- expect(entryForAgent(openagent!, "custom/root").path).toBe("custom/root/core/openagent.md");
- });
- });
- // ============================================================================
- // DEFECTS THE EMITTER FIXES, AND ONE IT WOULD CAUSE
- // ============================================================================
- describe("registry defects", () => {
- it.each(RECOVERED_BY_GENERATION)("keeps %s registered, which only generation ever added", async (id) => {
- // Both sides are asserted on purpose. The generated document proves the emitter still
- // derives the entry from the canonical file; the committed one proves the emit actually
- // reached disk. Checking only the emitter would let `registry.json` silently regress to a
- // hand-edited copy that drops these 7 again — the exact failure this refactor ends.
- const document = await new RegistryEmitter(repoPath()).emit();
- expect(byId(document, "subagents").get(id)?.path).toContain(id);
- expect(byId(COMMITTED, "subagents").has(id), `${id} is missing from the committed registry`).toBe(
- true
- );
- });
- it("makes subagent:batch-executor resolvable for openagent and opencoder", async () => {
- // Both declare `subagent:batch-executor`. The hand-maintained registry contained no such
- // component, so install.sh resolved the dependency to nothing and silently installed an
- // orchestrator whose parallel executor was missing. Generation is what closed that.
- const document = await new RegistryEmitter(repoPath()).emit();
- const agents = byId(document, "agents");
- expect(agents.get("openagent")?.dependencies).toContain("subagent:batch-executor");
- expect(agents.get("opencoder")?.dependencies).toContain("subagent:batch-executor");
- expect(byId(document, "subagents").has("batch-executor")).toBe(true);
- });
- it("preserves the dependency edges that only registry.json used to declare", async () => {
- // These edges were hand-authored in registry.json and absent from both the agent files and
- // agent-metadata.json, so generating from the canonical tree used to lose them. Subtask 09b
- // backfilled them into `oac.dependencies`. This asserts the round trip: every edge the
- // committed registry declares is still declared by the generated one, so `install.sh`
- // resolves exactly what it resolves today.
- const document = await new RegistryEmitter(repoPath()).emit();
- const subagents = byId(document, "subagents");
- for (const [id, backfilled] of Object.entries(REGISTRY_ONLY_DEPENDENCIES)) {
- const before = new Set(byId(COMMITTED, "subagents").get(id)?.dependencies ?? []);
- const after = new Set(subagents.get(id)?.dependencies ?? []);
- for (const dependency of backfilled) {
- expect(before.has(dependency), `${id} -> ${dependency} vanished from registry.json`).toBe(
- true
- );
- expect(
- after.has(dependency),
- `${id} -> ${dependency} was dropped — backfill it into content/agents/, do not weaken this test`
- ).toBe(true);
- }
- }
- });
- it("emits the backfilled edges in the order registry.json declares them", async () => {
- // Order is not cosmetic: the emitter maps `oac.dependencies` positionally, and the
- // subtask-11 `oac build && git diff --exit-code` gate compares bytes. A reordered list is a
- // spurious diff, so the canonical files must carry registry.json's own order.
- const subagents = byId(await new RegistryEmitter(repoPath()).emit(), "subagents");
- for (const id of Object.keys(REGISTRY_ONLY_DEPENDENCIES)) {
- expect(subagents.get(id)?.dependencies, `${id} dependency order drifted`).toEqual(
- byId(COMMITTED, "subagents").get(id)?.dependencies
- );
- }
- });
- });
|