From 5aad7e2f7192133b90c9cd0c679f30715e94ea31 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Thu, 24 Sep 2026 20:51:52 -0400 Subject: [PATCH 1/7] fix(media-use): tests never write into the person's media library --- packages/cli/src/media-use/lib/cache.mjs | 14 +++---- .../cli/src/media-use/lib/candidates.test.mjs | 4 +- .../cli/src/media-use/lib/manifest.test.mjs | 2 + packages/cli/src/media-use/lib/media-home.mjs | 20 ++++++++++ .../cli/src/media-use/lib/media-home.test.mjs | 40 +++++++++++++++++++ packages/cli/src/media-use/lib/misses.mjs | 6 +-- .../cli/src/media-use/lib/misses.test.mjs | 2 +- .../cli/src/media-use/lib/prefs-store.mjs | 4 +- .../src/media-use/lib/prefs-store.test.mjs | 2 +- .../cli/src/media-use/lib/recipe-store.mjs | 4 +- .../src/media-use/lib/recipe-store.test.mjs | 2 +- packages/cli/src/media-use/lib/stats.test.mjs | 2 +- packages/cli/src/media-use/lib/telemetry.mjs | 3 +- .../cli/src/media-use/lib/telemetry.test.mjs | 1 + packages/cli/src/media-use/resolve.test.mjs | 4 ++ scripts/check-media-use-copy-parity.test.mjs | 1 + .../media-use/scripts/compatibility.test.mjs | 2 + skills/media-use/scripts/lib/media-home.mjs | 20 ++++++++++ skills/media-use/scripts/lib/prefs-store.mjs | 4 +- skills/media-use/scripts/lib/recipe-store.mjs | 4 +- skills/media-use/scripts/lib/telemetry.mjs | 3 +- 21 files changed, 117 insertions(+), 27 deletions(-) create mode 100644 packages/cli/src/media-use/lib/media-home.mjs create mode 100644 packages/cli/src/media-use/lib/media-home.test.mjs create mode 100644 skills/media-use/scripts/lib/media-home.mjs diff --git a/packages/cli/src/media-use/lib/cache.mjs b/packages/cli/src/media-use/lib/cache.mjs index c7c7d6f7146..adcc895d900 100644 --- a/packages/cli/src/media-use/lib/cache.mjs +++ b/packages/cli/src/media-use/lib/cache.mjs @@ -1,17 +1,15 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from "node:fs"; import { join, basename } from "node:path"; import { createHash } from "node:crypto"; -import { homedir } from "node:os"; import { readManifest, appendRecord, normalizePrompt } from "./manifest.mjs"; +import { globalMediaDir, mediaHome } from "./media-home.mjs"; + +export { globalMediaDir }; const SCHEMA_PREFIX = "mu-v1-"; const KEY_HEX_CHARS = 16; const COMPLETE_SENTINEL = ".hf-complete"; -export function globalMediaDir() { - return join(homedir(), ".media"); -} - export function contentHash(filePath) { const bytes = readFileSync(filePath); return createHash("sha256").update(bytes).digest("hex"); @@ -30,11 +28,11 @@ function markComplete(entryDir) { } // The manifest helpers append their own ".media" to the dir they get, so the -// global manifest must be addressed by HOME, not by globalMediaDir() — passing +// global manifest must be addressed by mediaHome(), not by globalMediaDir() — passing // the latter nested it at ~/.media/.media/manifest.jsonl, invisible to the // Studio /api/assets/global route (which reads the documented flat path). export function readGlobalManifest() { - return readManifest(homedir()); + return readManifest(mediaHome()); } // Resolve a content-sha (full or unambiguous prefix) to a reusable global-cache @@ -105,7 +103,7 @@ export function cachePut(filePath, record) { reusable: true, cached_path: dest, }; - appendRecord(homedir(), globalRecord); + appendRecord(mediaHome(), globalRecord); return { sha, cached_path: dest }; } diff --git a/packages/cli/src/media-use/lib/candidates.test.mjs b/packages/cli/src/media-use/lib/candidates.test.mjs index 972577b3c00..72dabb587fa 100644 --- a/packages/cli/src/media-use/lib/candidates.test.mjs +++ b/packages/cli/src/media-use/lib/candidates.test.mjs @@ -6,13 +6,13 @@ import { tmpdir } from "node:os"; import { listCandidates, formatCandidates, CANDIDATE_CAP } from "./candidates.mjs"; import { findGlobalBySha } from "./cache.mjs"; -// candidates + findGlobalBySha are offline (no heygen), so we can override HOME +// candidates + findGlobalBySha are offline (no heygen), so we can point HYPERFRAMES_MEDIA_HOME // to a temp dir and seed a fake global ~/.media manifest deterministically. function sandbox() { const root = mkdtempSync(join(tmpdir(), "mu-cand-")); const project = join(root, "proj"); const home = join(root, "home"); - process.env.HOME = home; + process.env.HYPERFRAMES_MEDIA_HOME = home; return { root, project, home }; } function seedManifest(dir, records) { diff --git a/packages/cli/src/media-use/lib/manifest.test.mjs b/packages/cli/src/media-use/lib/manifest.test.mjs index 08e078ff27a..7422ec36b09 100644 --- a/packages/cli/src/media-use/lib/manifest.test.mjs +++ b/packages/cli/src/media-use/lib/manifest.test.mjs @@ -34,6 +34,8 @@ import { promote, } from "./cache.mjs"; +process.env.HYPERFRAMES_MEDIA_HOME = mkdtempSync(join(tmpdir(), "mu-media-home-")); + let tmp; function setup() { diff --git a/packages/cli/src/media-use/lib/media-home.mjs b/packages/cli/src/media-use/lib/media-home.mjs new file mode 100644 index 00000000000..046a04f917a --- /dev/null +++ b/packages/cli/src/media-use/lib/media-home.mjs @@ -0,0 +1,20 @@ +import { homedir } from "node:os"; +import { join } from "node:path"; + +// The one place that decides where the person's global media library lives: `/.media`. +// HYPERFRAMES_MEDIA_HOME points it elsewhere; under `node --test` it must, so no test can write +// fixtures into the real library that Desktop and Studio list as the person's own files. +export function mediaHome() { + const override = process.env.HYPERFRAMES_MEDIA_HOME; + if (override) return override; + if (process.env.NODE_TEST_CONTEXT) { + throw new Error( + "media-use: a test reached the real ~/.media; set HYPERFRAMES_MEDIA_HOME to a temp dir", + ); + } + return homedir(); +} + +export function globalMediaDir() { + return join(mediaHome(), ".media"); +} diff --git a/packages/cli/src/media-use/lib/media-home.test.mjs b/packages/cli/src/media-use/lib/media-home.test.mjs new file mode 100644 index 00000000000..41ecfc28dad --- /dev/null +++ b/packages/cli/src/media-use/lib/media-home.test.mjs @@ -0,0 +1,40 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; + +import { cachePut } from "./cache.mjs"; +import { globalMediaDir } from "./media-home.mjs"; + +const HOME = mkdtempSync(join(tmpdir(), "media-home-")); +process.env.HYPERFRAMES_MEDIA_HOME = HOME; + +test("the global cache writes into the media home a test points it at", () => { + const work = mkdtempSync(join(tmpdir(), "media-home-src-")); + const file = join(work, "clip.wav"); + writeFileSync(file, "media-home regression bytes"); + cachePut(file, { id: "bgm_001", type: "bgm", provenance: { prompt: "media home probe" } }); + const manifest = join(HOME, ".media", "manifest.jsonl"); + assert.ok(existsSync(manifest), "the temp media home holds the global manifest"); + assert.match(readFileSync(manifest, "utf8"), /media home probe/); + assert.equal(globalMediaDir(), join(HOME, ".media")); +}); + +test("a test that never points the media home anywhere fails instead of writing", () => { + const dir = mkdtempSync(join(tmpdir(), "media-home-guard-")); + const probe = join(dir, "probe.test.mjs"); + const lib = fileURLToPath(new URL("./media-home.mjs", import.meta.url)); + writeFileSync( + probe, + `import { test } from "node:test";\nimport { globalMediaDir } from ${JSON.stringify(lib)};\ntest("reach", () => globalMediaDir());\n`, + ); + const env = { ...process.env }; + delete env.HYPERFRAMES_MEDIA_HOME; + delete env.NODE_TEST_CONTEXT; + const run = spawnSync(process.execPath, ["--test", probe], { encoding: "utf8", env }); + assert.notEqual(run.status, 0, run.stdout); + assert.match(`${run.stdout}${run.stderr}`, /set HYPERFRAMES_MEDIA_HOME to a temp dir/); +}); diff --git a/packages/cli/src/media-use/lib/misses.mjs b/packages/cli/src/media-use/lib/misses.mjs index 585bb963849..b12f4695931 100644 --- a/packages/cli/src/media-use/lib/misses.mjs +++ b/packages/cli/src/media-use/lib/misses.mjs @@ -1,16 +1,16 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs"; -import { homedir } from "node:os"; import { join } from "node:path"; +import { globalMediaDir } from "./media-home.mjs"; const MISSES_FILE = "misses.jsonl"; function missesPath() { - return join(homedir(), ".media", MISSES_FILE); + return join(globalMediaDir(), MISSES_FILE); } export function recordMiss({ type, intent, provider_override, local_only }) { try { - const dir = join(homedir(), ".media"); + const dir = globalMediaDir(); mkdirSync(dir, { recursive: true }); appendFileSync( join(dir, MISSES_FILE), diff --git a/packages/cli/src/media-use/lib/misses.test.mjs b/packages/cli/src/media-use/lib/misses.test.mjs index d9b25856622..bc96111a9e7 100644 --- a/packages/cli/src/media-use/lib/misses.test.mjs +++ b/packages/cli/src/media-use/lib/misses.test.mjs @@ -9,7 +9,7 @@ function sandbox() { const root = mkdtempSync(join(tmpdir(), "mu-misses-")); const home = join(root, "home"); mkdirSync(home, { recursive: true }); - process.env.HOME = home; + process.env.HYPERFRAMES_MEDIA_HOME = home; return { root, home }; } diff --git a/packages/cli/src/media-use/lib/prefs-store.mjs b/packages/cli/src/media-use/lib/prefs-store.mjs index 6bc3f7345f1..a8f84896979 100644 --- a/packages/cli/src/media-use/lib/prefs-store.mjs +++ b/packages/cli/src/media-use/lib/prefs-store.mjs @@ -1,6 +1,6 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; -import { homedir } from "node:os"; import { basename, dirname, join, resolve } from "node:path"; +import { globalMediaDir } from "./media-home.mjs"; /** * Remembered defaults — the lightweight tier of HyperFrames user memory. @@ -41,7 +41,7 @@ export function projectPrefsPath(projectDir) { } export function userPrefsPath() { - return join(homedir(), ".media", PREFS_FILE); + return join(globalMediaDir(), PREFS_FILE); } function emptyFile() { diff --git a/packages/cli/src/media-use/lib/prefs-store.test.mjs b/packages/cli/src/media-use/lib/prefs-store.test.mjs index e0bc5ca814f..4c13e5fa24c 100644 --- a/packages/cli/src/media-use/lib/prefs-store.test.mjs +++ b/packages/cli/src/media-use/lib/prefs-store.test.mjs @@ -19,7 +19,7 @@ function sandbox() { mkdirSync(home, { recursive: true }); mkdirSync(projectA, { recursive: true }); mkdirSync(projectB, { recursive: true }); - process.env.HOME = home; + process.env.HYPERFRAMES_MEDIA_HOME = home; return { root, home, projectA, projectB }; } diff --git a/packages/cli/src/media-use/lib/recipe-store.mjs b/packages/cli/src/media-use/lib/recipe-store.mjs index e11ec96a31b..2b710e049eb 100644 --- a/packages/cli/src/media-use/lib/recipe-store.mjs +++ b/packages/cli/src/media-use/lib/recipe-store.mjs @@ -8,8 +8,8 @@ import { rmSync, writeFileSync, } from "node:fs"; -import { homedir } from "node:os"; import { basename, join, resolve } from "node:path"; +import { globalMediaDir } from "./media-home.mjs"; import { appendRecord, mediaDir, nextId } from "./manifest.mjs"; import { regenerateIndex } from "./index-gen.mjs"; import { mergedPreferences } from "./prefs-store.mjs"; @@ -58,7 +58,7 @@ export function projectRecipesDir(projectDir) { } export function userRecipesDir() { - return join(homedir(), ".media", "recipes"); + return join(globalMediaDir(), "recipes"); } export function slugifyRecipeName(name) { diff --git a/packages/cli/src/media-use/lib/recipe-store.test.mjs b/packages/cli/src/media-use/lib/recipe-store.test.mjs index 954291293d2..2bf751dca8e 100644 --- a/packages/cli/src/media-use/lib/recipe-store.test.mjs +++ b/packages/cli/src/media-use/lib/recipe-store.test.mjs @@ -83,7 +83,7 @@ function sandbox() { mkdirSync(project, { recursive: true }); writeFileSync(join(project, "frame.md"), "# Frame spec\nbackground: #101014\n"); writeFileSync(join(project, "STORYBOARD.md"), STORYBOARD); - process.env.HOME = home; + process.env.HYPERFRAMES_MEDIA_HOME = home; return { root, home, project }; } diff --git a/packages/cli/src/media-use/lib/stats.test.mjs b/packages/cli/src/media-use/lib/stats.test.mjs index ff0b6b466e8..3e5c32cfeb5 100644 --- a/packages/cli/src/media-use/lib/stats.test.mjs +++ b/packages/cli/src/media-use/lib/stats.test.mjs @@ -11,7 +11,7 @@ function sandbox() { const projectDir = join(root, "project"); mkdirSync(home, { recursive: true }); mkdirSync(projectDir, { recursive: true }); - process.env.HOME = home; + process.env.HYPERFRAMES_MEDIA_HOME = home; return { root, home, projectDir }; } diff --git a/packages/cli/src/media-use/lib/telemetry.mjs b/packages/cli/src/media-use/lib/telemetry.mjs index cd2911f529d..373a0d26fd0 100644 --- a/packages/cli/src/media-use/lib/telemetry.mjs +++ b/packages/cli/src/media-use/lib/telemetry.mjs @@ -5,6 +5,7 @@ import { randomUUID } from "node:crypto"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; +import { globalMediaDir } from "./media-home.mjs"; const POSTHOG_API_KEY = "phc_zjjbX0PnWxERXrMHhkEJWj9A9BhGVLRReICgsfTMmpx"; const POSTHOG_HOST = "https://us.i.posthog.com"; @@ -72,7 +73,7 @@ function writeSharedConfig(config) { // one — otherwise cross-surface continuity would start over on upgrade. function legacyMediaAnonId() { try { - const file = join(homedir(), ".media", "anon-id"); + const file = join(globalMediaDir(), "anon-id"); if (existsSync(file)) { const id = readFileSync(file, "utf8").trim(); if (id) return id; diff --git a/packages/cli/src/media-use/lib/telemetry.test.mjs b/packages/cli/src/media-use/lib/telemetry.test.mjs index 1a66d60f78e..54b39243364 100644 --- a/packages/cli/src/media-use/lib/telemetry.test.mjs +++ b/packages/cli/src/media-use/lib/telemetry.test.mjs @@ -18,6 +18,7 @@ function sandbox() { const home = join(root, "home"); mkdirSync(home, { recursive: true }); process.env.HOME = home; + process.env.HYPERFRAMES_MEDIA_HOME = home; return { root, home }; } diff --git a/packages/cli/src/media-use/resolve.test.mjs b/packages/cli/src/media-use/resolve.test.mjs index e9a617e82ef..c06007a32eb 100644 --- a/packages/cli/src/media-use/resolve.test.mjs +++ b/packages/cli/src/media-use/resolve.test.mjs @@ -33,10 +33,14 @@ const CAN_TSX = spawnSync(process.execPath, ["--import", "tsx", "--input-type=module", "-e", "0"], { stdio: "ignore", }).status === 0; +process.env.HYPERFRAMES_MEDIA_HOME = mkdtempSync(join(tmpdir(), "mu-resolve-home-")); + let tmp; function setup() { tmp = mkdtempSync(join(tmpdir(), "mu-resolve-test-")); + // A fresh global cache per test, apart from the project so the two manifests never coincide. + process.env.HYPERFRAMES_MEDIA_HOME = mkdtempSync(join(tmpdir(), "mu-resolve-home-")); } function cleanup() { diff --git a/scripts/check-media-use-copy-parity.test.mjs b/scripts/check-media-use-copy-parity.test.mjs index b9a045ae060..a2704f6beec 100644 --- a/scripts/check-media-use-copy-parity.test.mjs +++ b/scripts/check-media-use-copy-parity.test.mjs @@ -15,6 +15,7 @@ export const MEDIA_USE_COPY_NAMES = [ "index-gen.mjs", "manifest.mjs", "media-fetch.mjs", + "media-home.mjs", "npx-sync.mjs", "parakeet-words.mjs", "prefs-store.mjs", diff --git a/skills/media-use/scripts/compatibility.test.mjs b/skills/media-use/scripts/compatibility.test.mjs index 9b638a074a9..7e5b2ac4780 100644 --- a/skills/media-use/scripts/compatibility.test.mjs +++ b/skills/media-use/scripts/compatibility.test.mjs @@ -6,6 +6,8 @@ import { execFileSync } from "node:child_process"; import test from "node:test"; import { pathToFileURL } from "node:url"; +process.env.HYPERFRAMES_MEDIA_HOME = mkdtempSync(join(tmpdir(), "media-use-compat-home-")); + function run(scriptsDir, script, args) { return execFileSync(process.execPath, [join(scriptsDir, script), ...args], { encoding: "utf8", diff --git a/skills/media-use/scripts/lib/media-home.mjs b/skills/media-use/scripts/lib/media-home.mjs new file mode 100644 index 00000000000..046a04f917a --- /dev/null +++ b/skills/media-use/scripts/lib/media-home.mjs @@ -0,0 +1,20 @@ +import { homedir } from "node:os"; +import { join } from "node:path"; + +// The one place that decides where the person's global media library lives: `/.media`. +// HYPERFRAMES_MEDIA_HOME points it elsewhere; under `node --test` it must, so no test can write +// fixtures into the real library that Desktop and Studio list as the person's own files. +export function mediaHome() { + const override = process.env.HYPERFRAMES_MEDIA_HOME; + if (override) return override; + if (process.env.NODE_TEST_CONTEXT) { + throw new Error( + "media-use: a test reached the real ~/.media; set HYPERFRAMES_MEDIA_HOME to a temp dir", + ); + } + return homedir(); +} + +export function globalMediaDir() { + return join(mediaHome(), ".media"); +} diff --git a/skills/media-use/scripts/lib/prefs-store.mjs b/skills/media-use/scripts/lib/prefs-store.mjs index 6bc3f7345f1..a8f84896979 100644 --- a/skills/media-use/scripts/lib/prefs-store.mjs +++ b/skills/media-use/scripts/lib/prefs-store.mjs @@ -1,6 +1,6 @@ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; -import { homedir } from "node:os"; import { basename, dirname, join, resolve } from "node:path"; +import { globalMediaDir } from "./media-home.mjs"; /** * Remembered defaults — the lightweight tier of HyperFrames user memory. @@ -41,7 +41,7 @@ export function projectPrefsPath(projectDir) { } export function userPrefsPath() { - return join(homedir(), ".media", PREFS_FILE); + return join(globalMediaDir(), PREFS_FILE); } function emptyFile() { diff --git a/skills/media-use/scripts/lib/recipe-store.mjs b/skills/media-use/scripts/lib/recipe-store.mjs index e11ec96a31b..2b710e049eb 100644 --- a/skills/media-use/scripts/lib/recipe-store.mjs +++ b/skills/media-use/scripts/lib/recipe-store.mjs @@ -8,8 +8,8 @@ import { rmSync, writeFileSync, } from "node:fs"; -import { homedir } from "node:os"; import { basename, join, resolve } from "node:path"; +import { globalMediaDir } from "./media-home.mjs"; import { appendRecord, mediaDir, nextId } from "./manifest.mjs"; import { regenerateIndex } from "./index-gen.mjs"; import { mergedPreferences } from "./prefs-store.mjs"; @@ -58,7 +58,7 @@ export function projectRecipesDir(projectDir) { } export function userRecipesDir() { - return join(homedir(), ".media", "recipes"); + return join(globalMediaDir(), "recipes"); } export function slugifyRecipeName(name) { diff --git a/skills/media-use/scripts/lib/telemetry.mjs b/skills/media-use/scripts/lib/telemetry.mjs index cd2911f529d..373a0d26fd0 100644 --- a/skills/media-use/scripts/lib/telemetry.mjs +++ b/skills/media-use/scripts/lib/telemetry.mjs @@ -5,6 +5,7 @@ import { randomUUID } from "node:crypto"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; +import { globalMediaDir } from "./media-home.mjs"; const POSTHOG_API_KEY = "phc_zjjbX0PnWxERXrMHhkEJWj9A9BhGVLRReICgsfTMmpx"; const POSTHOG_HOST = "https://us.i.posthog.com"; @@ -72,7 +73,7 @@ function writeSharedConfig(config) { // one — otherwise cross-surface continuity would start over on upgrade. function legacyMediaAnonId() { try { - const file = join(homedir(), ".media", "anon-id"); + const file = join(globalMediaDir(), "anon-id"); if (existsSync(file)) { const id = readFileSync(file, "utf8").trim(); if (id) return id; From 2506124d449413b3f7005a5803250a0e7159d1bf Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Thu, 24 Sep 2026 21:31:09 -0400 Subject: [PATCH 2/7] fix(media-use): guard the media home only in this repo's test runs --- .github/workflows/ci.yml | 2 + package.json | 2 +- packages/cli/src/media-use/lib/media-home.mjs | 7 ++-- .../cli/src/media-use/lib/media-home.test.mjs | 38 ++++++++++++++----- packages/cli/src/media-use/lib/misses.mjs | 9 +++-- skills/media-use/scripts/lib/media-home.mjs | 7 ++-- 6 files changed, 45 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aee92463569..03e54a061ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -526,6 +526,8 @@ jobs: - name: Verify standalone skill imports run: bun test scripts/check-skill-import-boundaries.test.mjs - name: Discover and run skills tests + env: + HYPERFRAMES_MEDIA_HOME_REQUIRED: "1" # We expand the test list via bash so the job fails loudly when the # matcher comes back empty, rather than silently no-op'ing (which # would defeat the whole point of this job). diff --git a/package.json b/package.json index a658844d94f..b5a5cdcabc3 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "knip": "knip", "test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-registry-set-delta.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-pr-captures.test.mjs scripts/check-comment-citations.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-media-use-copy-parity.test.mjs scripts/check-svg-sanitize-parity.test.mjs scripts/check-media-use-svg-sanitize-generated.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-large-files.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/publish-workflow.test.mjs scripts/pr-edit-concurrency.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/host-registry-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/lint-skills.test.mjs scripts/creator-editing-recipes.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs packages/core/scripts/writeGeneratedFile.test.ts scripts/catalog-publication.test.mjs scripts/ci/resolve-workflow-pr.test.mjs scripts/check-catalog-source-pr.test.mjs scripts/generate-registry-items.test.ts scripts/catalog-drift.test.ts scripts/catalog-fetch-mirror.test.ts scripts/catalog-script-inlining.test.ts scripts/catalog-detail.test.ts scripts/generate-catalog-pages.test.ts scripts/verify-catalog-payloads.test.ts scripts/registry-skill-files.test.ts scripts/creator-editing-capabilities.test.mjs scripts/generate-catalog-previews.test.ts scripts/registry-primitive-payloads.test.ts registry/components/pan-stations/pan-stations.test.mjs && vitest run scripts/catalog/ scripts/contrastRatchet.test.ts scripts/generate-catalog-payloads.test.ts", "typecheck:scripts": "tsc --noEmit -p scripts/tsconfig.json", - "test:skills": "node --test 'skills/**/*.test.mjs' 'packages/cli/src/media-use/**/*.test.mjs'", + "test:skills": "HYPERFRAMES_MEDIA_HOME_REQUIRED=1 node --test 'skills/**/*.test.mjs' 'packages/cli/src/media-use/**/*.test.mjs'", "generate:previews": "tsx scripts/generate-template-previews.ts", "generate:catalog-previews": "tsx scripts/generate-catalog-previews.ts", "package:codex-plugin": "node scripts/package-codex-plugin.mjs", diff --git a/packages/cli/src/media-use/lib/media-home.mjs b/packages/cli/src/media-use/lib/media-home.mjs index 046a04f917a..bddd36ac9d6 100644 --- a/packages/cli/src/media-use/lib/media-home.mjs +++ b/packages/cli/src/media-use/lib/media-home.mjs @@ -2,12 +2,13 @@ import { homedir } from "node:os"; import { join } from "node:path"; // The one place that decides where the person's global media library lives: `/.media`. -// HYPERFRAMES_MEDIA_HOME points it elsewhere; under `node --test` it must, so no test can write -// fixtures into the real library that Desktop and Studio list as the person's own files. +// HYPERFRAMES_MEDIA_HOME points it elsewhere. This repo's media-use test runs set +// HYPERFRAMES_MEDIA_HOME_REQUIRED, so a test that forgot to point it fails instead of writing +// fixtures into the library Desktop and Studio list as the person's own files. export function mediaHome() { const override = process.env.HYPERFRAMES_MEDIA_HOME; if (override) return override; - if (process.env.NODE_TEST_CONTEXT) { + if (process.env.HYPERFRAMES_MEDIA_HOME_REQUIRED) { throw new Error( "media-use: a test reached the real ~/.media; set HYPERFRAMES_MEDIA_HOME to a temp dir", ); diff --git a/packages/cli/src/media-use/lib/media-home.test.mjs b/packages/cli/src/media-use/lib/media-home.test.mjs index 41ecfc28dad..d9312f53137 100644 --- a/packages/cli/src/media-use/lib/media-home.test.mjs +++ b/packages/cli/src/media-use/lib/media-home.test.mjs @@ -23,18 +23,38 @@ test("the global cache writes into the media home a test points it at", () => { assert.equal(globalMediaDir(), join(HOME, ".media")); }); -test("a test that never points the media home anywhere fails instead of writing", () => { - const dir = mkdtempSync(join(tmpdir(), "media-home-guard-")); +// A child `node --test` run over one probe file, with the media-home variables set as given. +function runProbe(env) { + const dir = mkdtempSync(join(tmpdir(), "media-home-probe-")); const probe = join(dir, "probe.test.mjs"); const lib = fileURLToPath(new URL("./media-home.mjs", import.meta.url)); writeFileSync( probe, - `import { test } from "node:test";\nimport { globalMediaDir } from ${JSON.stringify(lib)};\ntest("reach", () => globalMediaDir());\n`, + `import { test } from "node:test";\nimport { globalMediaDir } from ${JSON.stringify(lib)};\ntest("reach", () => console.log("dir=" + globalMediaDir()));\n`, ); - const env = { ...process.env }; - delete env.HYPERFRAMES_MEDIA_HOME; - delete env.NODE_TEST_CONTEXT; - const run = spawnSync(process.execPath, ["--test", probe], { encoding: "utf8", env }); - assert.notEqual(run.status, 0, run.stdout); - assert.match(`${run.stdout}${run.stderr}`, /set HYPERFRAMES_MEDIA_HOME to a temp dir/); + const childEnv = { ...process.env }; + delete childEnv.HYPERFRAMES_MEDIA_HOME; + delete childEnv.HYPERFRAMES_MEDIA_HOME_REQUIRED; + Object.assign(childEnv, env); + const run = spawnSync(process.execPath, ["--test", probe], { encoding: "utf8", env: childEnv }); + return { status: run.status, output: `${run.stdout}${run.stderr}` }; +} + +test("a repo test run that never points the media home anywhere fails instead of writing", () => { + const { status, output } = runProbe({ HYPERFRAMES_MEDIA_HOME_REQUIRED: "1" }); + assert.notEqual(status, 0, output); + assert.match(output, /set HYPERFRAMES_MEDIA_HOME to a temp dir/); +}); + +// Node sets NODE_TEST_CONTEXT for every project's test runs, so another project whose own tests +// spawn this CLI must get the person's real library, not the guard. +test("another project's node --test run still resolves the real media home", () => { + const home = mkdtempSync(join(tmpdir(), "media-home-user-")); + const { status, output } = runProbe({ + HOME: home, + USERPROFILE: home, + NODE_TEST_CONTEXT: "child-v8", + }); + assert.equal(status, 0, output); + assert.ok(output.includes(`dir=${join(home, ".media")}`), output); }); diff --git a/packages/cli/src/media-use/lib/misses.mjs b/packages/cli/src/media-use/lib/misses.mjs index b12f4695931..8b48462f973 100644 --- a/packages/cli/src/media-use/lib/misses.mjs +++ b/packages/cli/src/media-use/lib/misses.mjs @@ -1,5 +1,5 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs"; -import { join } from "node:path"; +import { dirname, join } from "node:path"; import { globalMediaDir } from "./media-home.mjs"; const MISSES_FILE = "misses.jsonl"; @@ -9,11 +9,12 @@ function missesPath() { } export function recordMiss({ type, intent, provider_override, local_only }) { + // Outside the try: the media-home guard must fail a test run, not be swallowed here. + const path = missesPath(); try { - const dir = globalMediaDir(); - mkdirSync(dir, { recursive: true }); + mkdirSync(dirname(path), { recursive: true }); appendFileSync( - join(dir, MISSES_FILE), + path, JSON.stringify({ ts: new Date().toISOString(), type, diff --git a/skills/media-use/scripts/lib/media-home.mjs b/skills/media-use/scripts/lib/media-home.mjs index 046a04f917a..bddd36ac9d6 100644 --- a/skills/media-use/scripts/lib/media-home.mjs +++ b/skills/media-use/scripts/lib/media-home.mjs @@ -2,12 +2,13 @@ import { homedir } from "node:os"; import { join } from "node:path"; // The one place that decides where the person's global media library lives: `/.media`. -// HYPERFRAMES_MEDIA_HOME points it elsewhere; under `node --test` it must, so no test can write -// fixtures into the real library that Desktop and Studio list as the person's own files. +// HYPERFRAMES_MEDIA_HOME points it elsewhere. This repo's media-use test runs set +// HYPERFRAMES_MEDIA_HOME_REQUIRED, so a test that forgot to point it fails instead of writing +// fixtures into the library Desktop and Studio list as the person's own files. export function mediaHome() { const override = process.env.HYPERFRAMES_MEDIA_HOME; if (override) return override; - if (process.env.NODE_TEST_CONTEXT) { + if (process.env.HYPERFRAMES_MEDIA_HOME_REQUIRED) { throw new Error( "media-use: a test reached the real ~/.media; set HYPERFRAMES_MEDIA_HOME to a temp dir", ); From 6c8c60a1ea99dd259b7bef47bbd9559eb5844803 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Thu, 24 Sep 2026 21:32:44 -0400 Subject: [PATCH 3/7] test(media-use): probe the media home from a plain child process --- .../cli/src/media-use/lib/media-home.test.mjs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/cli/src/media-use/lib/media-home.test.mjs b/packages/cli/src/media-use/lib/media-home.test.mjs index d9312f53137..16169f118f4 100644 --- a/packages/cli/src/media-use/lib/media-home.test.mjs +++ b/packages/cli/src/media-use/lib/media-home.test.mjs @@ -4,7 +4,6 @@ import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "node:test"; -import { fileURLToPath } from "node:url"; import { cachePut } from "./cache.mjs"; import { globalMediaDir } from "./media-home.mjs"; @@ -23,20 +22,22 @@ test("the global cache writes into the media home a test points it at", () => { assert.equal(globalMediaDir(), join(HOME, ".media")); }); -// A child `node --test` run over one probe file, with the media-home variables set as given. +// A child process that resolves the media dir, with only the given media-home variables set. function runProbe(env) { - const dir = mkdtempSync(join(tmpdir(), "media-home-probe-")); - const probe = join(dir, "probe.test.mjs"); - const lib = fileURLToPath(new URL("./media-home.mjs", import.meta.url)); - writeFileSync( - probe, - `import { test } from "node:test";\nimport { globalMediaDir } from ${JSON.stringify(lib)};\ntest("reach", () => console.log("dir=" + globalMediaDir()));\n`, - ); + const lib = new URL("./media-home.mjs", import.meta.url).href; const childEnv = { ...process.env }; delete childEnv.HYPERFRAMES_MEDIA_HOME; delete childEnv.HYPERFRAMES_MEDIA_HOME_REQUIRED; - Object.assign(childEnv, env); - const run = spawnSync(process.execPath, ["--test", probe], { encoding: "utf8", env: childEnv }); + delete childEnv.NODE_TEST_CONTEXT; + const run = spawnSync( + process.execPath, + [ + "--input-type=module", + "-e", + `const m = await import(${JSON.stringify(lib)}); console.log("dir=" + m.globalMediaDir());`, + ], + { encoding: "utf8", env: { ...childEnv, ...env } }, + ); return { status: run.status, output: `${run.stdout}${run.stderr}` }; } From 995d90d9ce540c1705fef0eee77c980a7985ac24 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Thu, 24 Sep 2026 21:38:24 -0400 Subject: [PATCH 4/7] fix(media-use): keep miss logging best-effort when home is unresolvable --- packages/cli/src/media-use/lib/media-home.mjs | 7 +++---- packages/cli/src/media-use/lib/misses.mjs | 3 +-- skills/media-use/scripts/lib/media-home.mjs | 7 +++---- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/media-use/lib/media-home.mjs b/packages/cli/src/media-use/lib/media-home.mjs index bddd36ac9d6..2ee2a79b053 100644 --- a/packages/cli/src/media-use/lib/media-home.mjs +++ b/packages/cli/src/media-use/lib/media-home.mjs @@ -1,10 +1,9 @@ import { homedir } from "node:os"; import { join } from "node:path"; -// The one place that decides where the person's global media library lives: `/.media`. -// HYPERFRAMES_MEDIA_HOME points it elsewhere. This repo's media-use test runs set -// HYPERFRAMES_MEDIA_HOME_REQUIRED, so a test that forgot to point it fails instead of writing -// fixtures into the library Desktop and Studio list as the person's own files. +// The one owner of where the person's media library lives. This repo's test runs set +// HYPERFRAMES_MEDIA_HOME_REQUIRED, so a test that forgot HYPERFRAMES_MEDIA_HOME fails instead +// of writing fixtures into the library Desktop and Studio list as the person's own files. export function mediaHome() { const override = process.env.HYPERFRAMES_MEDIA_HOME; if (override) return override; diff --git a/packages/cli/src/media-use/lib/misses.mjs b/packages/cli/src/media-use/lib/misses.mjs index 8b48462f973..ff5873c0aa6 100644 --- a/packages/cli/src/media-use/lib/misses.mjs +++ b/packages/cli/src/media-use/lib/misses.mjs @@ -9,9 +9,8 @@ function missesPath() { } export function recordMiss({ type, intent, provider_override, local_only }) { - // Outside the try: the media-home guard must fail a test run, not be swallowed here. - const path = missesPath(); try { + const path = missesPath(); mkdirSync(dirname(path), { recursive: true }); appendFileSync( path, diff --git a/skills/media-use/scripts/lib/media-home.mjs b/skills/media-use/scripts/lib/media-home.mjs index bddd36ac9d6..2ee2a79b053 100644 --- a/skills/media-use/scripts/lib/media-home.mjs +++ b/skills/media-use/scripts/lib/media-home.mjs @@ -1,10 +1,9 @@ import { homedir } from "node:os"; import { join } from "node:path"; -// The one place that decides where the person's global media library lives: `/.media`. -// HYPERFRAMES_MEDIA_HOME points it elsewhere. This repo's media-use test runs set -// HYPERFRAMES_MEDIA_HOME_REQUIRED, so a test that forgot to point it fails instead of writing -// fixtures into the library Desktop and Studio list as the person's own files. +// The one owner of where the person's media library lives. This repo's test runs set +// HYPERFRAMES_MEDIA_HOME_REQUIRED, so a test that forgot HYPERFRAMES_MEDIA_HOME fails instead +// of writing fixtures into the library Desktop and Studio list as the person's own files. export function mediaHome() { const override = process.env.HYPERFRAMES_MEDIA_HOME; if (override) return override; From 2aa67fd41bce112e90296197e4bc53a3163cedad Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Thu, 24 Sep 2026 21:39:00 -0400 Subject: [PATCH 5/7] test(media-use): miss logging survives an unresolvable media home --- packages/cli/src/media-use/lib/misses.test.mjs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/cli/src/media-use/lib/misses.test.mjs b/packages/cli/src/media-use/lib/misses.test.mjs index bc96111a9e7..e5286c069d1 100644 --- a/packages/cli/src/media-use/lib/misses.test.mjs +++ b/packages/cli/src/media-use/lib/misses.test.mjs @@ -54,6 +54,17 @@ test("recordMiss swallows filesystem failures", () => { } }); +test("recordMiss stays best-effort when the media home cannot be resolved", () => { + const savedEnv = { ...process.env }; + delete process.env.HYPERFRAMES_MEDIA_HOME; + process.env.HYPERFRAMES_MEDIA_HOME_REQUIRED = "1"; + try { + assert.doesNotThrow(() => recordMiss({ type: "sfx", intent: "no home", local_only: true })); + } finally { + restoreEnv(savedEnv); + } +}); + test("readMisses skips corrupt lines", () => { const savedEnv = { ...process.env }; const { root, home } = sandbox(); From 312f5c0c5522b29a794683b084319b15fea814af Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Thu, 24 Sep 2026 21:46:43 -0400 Subject: [PATCH 6/7] docs(media-use): describe the media library without naming an app --- packages/cli/src/media-use/lib/media-home.mjs | 2 +- skills/media-use/scripts/lib/media-home.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/media-use/lib/media-home.mjs b/packages/cli/src/media-use/lib/media-home.mjs index 2ee2a79b053..8a9128fd3bf 100644 --- a/packages/cli/src/media-use/lib/media-home.mjs +++ b/packages/cli/src/media-use/lib/media-home.mjs @@ -3,7 +3,7 @@ import { join } from "node:path"; // The one owner of where the person's media library lives. This repo's test runs set // HYPERFRAMES_MEDIA_HOME_REQUIRED, so a test that forgot HYPERFRAMES_MEDIA_HOME fails instead -// of writing fixtures into the library Desktop and Studio list as the person's own files. +// of writing fixtures into the media library apps list as the person's own files. export function mediaHome() { const override = process.env.HYPERFRAMES_MEDIA_HOME; if (override) return override; diff --git a/skills/media-use/scripts/lib/media-home.mjs b/skills/media-use/scripts/lib/media-home.mjs index 2ee2a79b053..8a9128fd3bf 100644 --- a/skills/media-use/scripts/lib/media-home.mjs +++ b/skills/media-use/scripts/lib/media-home.mjs @@ -3,7 +3,7 @@ import { join } from "node:path"; // The one owner of where the person's media library lives. This repo's test runs set // HYPERFRAMES_MEDIA_HOME_REQUIRED, so a test that forgot HYPERFRAMES_MEDIA_HOME fails instead -// of writing fixtures into the library Desktop and Studio list as the person's own files. +// of writing fixtures into the media library apps list as the person's own files. export function mediaHome() { const override = process.env.HYPERFRAMES_MEDIA_HOME; if (override) return override; From a6d9aeae336e69d46c4dd24f2c6aa589114726bc Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Thu, 24 Sep 2026 22:31:31 -0400 Subject: [PATCH 7/7] chore(media-use): refresh the skills manifest hash --- skills-manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skills-manifest.json b/skills-manifest.json index b87d31b3978..5cb80e233d9 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -54,8 +54,8 @@ "files": 1 }, "media-use": { - "hash": "b35de041b87e763d", - "files": 98 + "hash": "7915441c22070c00", + "files": 99 }, "motion-graphics": { "hash": "ab9d2e9c205ce1b1",