Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions cli/tests/e2e/framework-model-inheritance.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { mkdir, readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { frameworkBuildTargetModes } from "../../src/contexts/translate/domain/build-target.js";
import "../../src/runtime/wiring/translate.js";
import { REPOSITORY_ROOT } from "../helpers/repository-root.js";
import { createTestEnv, runCli } from "./helpers.js";

// Every target/mode the CLI announces, built from the shipped plugins rather than a fixture.
const CELLS = frameworkBuildTargetModes();

// A recipe may show a model in an example block; only a declaration the host reads counts.
function overridesModel(path: string, content: string): boolean {
if (path.endsWith(".toml")) return /^model\s*=/m.test(content);
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
return frontmatter !== null && /^model\s*:/m.test(frontmatter[1]);
}

describe.concurrent("E2E: the shipped framework leaves model selection to the host", () => {
it.each(CELLS)(
"$target $mode emits no model override",
async ({ target, mode }) => {
const { tempDir, projectDir, fakeHome, cleanup } = await createTestEnv(
`model-${target}-${mode}`
);
try {
const outDir = join(tempDir, "dist");
await mkdir(outDir, { recursive: true });
const args = ["translate", REPOSITORY_ROOT, "--to", target, "--out", outDir, "--as", mode];
const build = await runCli(args, projectDir, fakeHome);
expect(build.exitCode, build.stderr).toBe(0);

const files = await readdir(outDir, { recursive: true, withFileTypes: true });
const emitted = files.filter((entry) => entry.isFile());
expect(emitted.length).toBeGreaterThan(0);
const overrides: string[] = [];
for (const entry of emitted) {
const path = join(entry.parentPath, entry.name);
if (overridesModel(path, await readFile(path, "utf-8"))) {
overrides.push(path.slice(outDir.length));
}
}
expect(overrides).toEqual([]);
} finally {
await cleanup();
}
},
120_000
);
});
1 change: 1 addition & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ Dependency-free Node scripts declared in `hooks/hooks.json`; `node` must be on `
| Skills | Links stay inside the skill directory. Flat distribution renames skills `<plugin>-<skill>`; marketplace installation preserves the tree. | [Portability guard](../scripts/__tests__/a-skill-links-only-inside-itself.test.js) |
| Bundled scripts | Named plugin-relative in backticks, never linked. | [Portability guard](../scripts/__tests__/a-skill-links-only-inside-itself.test.js) |
| Hook adapters | CLI owns OpenCode's shared host protocol; plugins own payload mapping. | [Delivery and compatibility](../cli/ARCHITECTURE.md#hook-adaptation) |
| Model selection | The user and host own the concrete model and reasoning settings; no shipped skill or agent pins one, and a target build carries over a model the user wrote only where the target supports one. | [Source guard](../scripts/__tests__/model-selection.test.js) · [Build guard](../cli/tests/e2e/framework-model-inheritance.e2e.test.ts) |

## Capability discovery and addressing

Expand Down
Loading