@forgeax/engine-vite-plugin-rhi-debug 0.0.0-dev.8d955ade1c79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,170 @@
1
+ import { createHash, randomUUID } from 'crypto';
2
+ import { writeFile, mkdir, rename, rm, access } from 'fs/promises';
3
+ import { resolve, join } from 'path';
4
+ import { decodeTape } from '@forgeax/engine-rhi-debug';
5
+
6
+ // src/index.ts
7
+ var RAW_TAPE_ROUTE = "/__forgeax-debug/tape";
8
+ var RHITAPE_MIME = "application/x-forgeax-rhitape";
9
+ var DEFINE_KEY = "import.meta.env.FORGEAX_ENGINE_RHI_DEBUG";
10
+ var RUN_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
11
+ function selectCaptureProvider(providers) {
12
+ const providerIds = providers.map((provider2) => provider2.id);
13
+ if (providers.length === 0) {
14
+ return { ok: false, error: { code: "capture-target-unavailable", providerIds } };
15
+ }
16
+ if (providers.length > 1) {
17
+ return { ok: false, error: { code: "capture-target-ambiguous", providerIds } };
18
+ }
19
+ const provider = providers[0];
20
+ if (provider === void 0) {
21
+ return { ok: false, error: { code: "capture-target-unavailable", providerIds } };
22
+ }
23
+ return { ok: true, value: provider };
24
+ }
25
+ function createRawTapeProvider(options) {
26
+ const rootDir = resolve(options.rootDir);
27
+ const write = options.writeFile ?? (async (path, bytes) => {
28
+ await writeFile(path, bytes);
29
+ });
30
+ return {
31
+ async accept(upload) {
32
+ if (!RUN_ID_PATTERN.test(upload.runId)) {
33
+ return {
34
+ ok: false,
35
+ error: {
36
+ code: "capture-run-id-invalid",
37
+ hint: "runId must contain only ASCII letters, numbers, underscore, or hyphen"
38
+ }
39
+ };
40
+ }
41
+ if (upload.contentType !== RHITAPE_MIME) {
42
+ return {
43
+ ok: false,
44
+ error: {
45
+ code: "capture-mime-invalid",
46
+ hint: `content-type must be exactly ${RHITAPE_MIME}`
47
+ }
48
+ };
49
+ }
50
+ const decoded = decodeTape(upload.bytes);
51
+ if (!decoded.ok) {
52
+ return {
53
+ ok: false,
54
+ error: {
55
+ code: "capture-tape-invalid",
56
+ hint: decoded.error.hint
57
+ }
58
+ };
59
+ }
60
+ const digest = `sha256:${createHash("sha256").update(upload.bytes).digest("hex")}`;
61
+ const debugDir = join(rootDir, ".forgeax-debug");
62
+ const outDir = join(debugDir, upload.runId);
63
+ const finalPath = join(outDir, "frame.rhitape");
64
+ const tempDir = join(debugDir, `.rhitape-${upload.runId}-${randomUUID()}`);
65
+ const debugDirExisted = await pathExists(debugDir);
66
+ const outDirExisted = await pathExists(outDir);
67
+ try {
68
+ await mkdir(tempDir, { recursive: true });
69
+ await write(join(tempDir, "frame.rhitape"), upload.bytes);
70
+ await mkdir(outDir, { recursive: true });
71
+ await rename(join(tempDir, "frame.rhitape"), finalPath);
72
+ await rm(tempDir, { recursive: true, force: true });
73
+ } catch {
74
+ await rm(tempDir, { recursive: true, force: true });
75
+ if (!outDirExisted) await rm(outDir, { recursive: true, force: true });
76
+ if (!debugDirExisted) await rm(debugDir, { recursive: true, force: true });
77
+ return {
78
+ ok: false,
79
+ error: {
80
+ code: "capture-artifact-write-failed",
81
+ hint: "the raw tape could not be written atomically; inspect the dev-server filesystem and retry"
82
+ }
83
+ };
84
+ }
85
+ return { ok: true, value: { kind: "rhi-tape", digest, path: finalPath } };
86
+ }
87
+ };
88
+ }
89
+ function sendJson(res, status, payload) {
90
+ res.statusCode = status;
91
+ res.setHeader("Content-Type", "application/json");
92
+ res.end(JSON.stringify(payload));
93
+ }
94
+ async function readRawBody(req) {
95
+ const chunks = [];
96
+ let size = 0;
97
+ for await (const chunk of req) {
98
+ const bytes = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
99
+ chunks.push(bytes);
100
+ size += bytes.byteLength;
101
+ }
102
+ const output = new Uint8Array(size);
103
+ let offset = 0;
104
+ for (const chunk of chunks) {
105
+ output.set(chunk, offset);
106
+ offset += chunk.byteLength;
107
+ }
108
+ return output;
109
+ }
110
+ function contentType(req) {
111
+ const value = req.headers?.["content-type"];
112
+ return Array.isArray(value) ? value[0] ?? "" : value ?? "";
113
+ }
114
+ async function pathExists(path) {
115
+ try {
116
+ await access(path);
117
+ return true;
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+ function vitePluginRhiDebug(options = {}) {
123
+ return {
124
+ name: "forgeax:rhi-debug",
125
+ config(_config, env) {
126
+ return {
127
+ define: { [DEFINE_KEY]: JSON.stringify(env.command === "serve" ? "1" : "0") }
128
+ };
129
+ },
130
+ configureServer(server) {
131
+ const provider = createRawTapeProvider({ rootDir: options.rootDir ?? process.cwd() });
132
+ server.middlewares.use(async (request, response, next) => {
133
+ const req = request;
134
+ const res = response;
135
+ const url = new URL(req.url ?? "", "http://localhost");
136
+ if (url.pathname !== RAW_TAPE_ROUTE) {
137
+ next();
138
+ return;
139
+ }
140
+ if (req.method !== "POST") {
141
+ res.setHeader("Allow", "POST");
142
+ sendJson(res, 405, {
143
+ error: "method-not-allowed",
144
+ hint: `use POST ${RAW_TAPE_ROUTE}?runId=<id> with raw ${RHITAPE_MIME} bytes`
145
+ });
146
+ return;
147
+ }
148
+ const runId = url.searchParams.get("runId") ?? "";
149
+ const bytes = await readRawBody(req);
150
+ const result = await provider.accept({ runId, contentType: contentType(req), bytes });
151
+ if (!result.ok) {
152
+ sendJson(
153
+ res,
154
+ result.error.code === "capture-artifact-write-failed" ? 500 : 400,
155
+ result.error
156
+ );
157
+ return;
158
+ }
159
+ res.statusCode = 200;
160
+ res.setHeader("Content-Type", "application/json");
161
+ res.end(JSON.stringify(result.value));
162
+ });
163
+ }
164
+ };
165
+ }
166
+ var index_default = vitePluginRhiDebug;
167
+
168
+ export { RAW_TAPE_ROUTE, RHITAPE_MIME, createRawTapeProvider, index_default as default, selectCaptureProvider, vitePluginRhiDebug };
169
+ //# sourceMappingURL=index.mjs.map
170
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":["provider"],"mappings":"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","file":"index.mjs","sourcesContent":["// @forgeax/engine-vite-plugin-rhi-debug -- serve-only raw tape transport.\n//\n// The plugin is a leaf: it validates one encoded v7 tape with the core decoder,\n// persists one `.rhitape` file, and exposes no replay or report owner.\n\nimport { createHash, randomUUID } from 'node:crypto';\nimport { access, mkdir, rename, rm, writeFile } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport { decodeTape } from '@forgeax/engine-rhi-debug';\nimport type { Plugin, ViteDevServer } from 'vite';\n\nexport const RAW_TAPE_ROUTE = '/__forgeax-debug/tape' as const;\nexport const RHITAPE_MIME = 'application/x-forgeax-rhitape' as const;\n\nconst DEFINE_KEY = 'import.meta.env.FORGEAX_ENGINE_RHI_DEBUG';\nconst RUN_ID_PATTERN = /^[A-Za-z0-9_-]+$/;\n\nexport interface CaptureProvider {\n readonly id: string;\n}\n\nexport type CaptureProviderError =\n | { readonly code: 'capture-target-unavailable'; readonly providerIds: readonly string[] }\n | { readonly code: 'capture-target-ambiguous'; readonly providerIds: readonly string[] };\n\nexport type ViteProviderError =\n | CaptureProviderError\n | {\n readonly code:\n | 'capture-run-id-invalid'\n | 'capture-mime-invalid'\n | 'capture-tape-invalid'\n | 'capture-artifact-write-failed';\n readonly hint: string;\n };\n\nexport interface RawTapeUpload {\n readonly runId: string;\n readonly contentType: string;\n readonly bytes: Uint8Array;\n}\n\nexport interface RawTapeArtifactRef {\n readonly kind: 'rhi-tape';\n readonly digest: string;\n readonly path: string;\n}\n\nexport interface RawTapeProviderOptions {\n readonly rootDir: string;\n readonly writeFile?: (path: string, bytes: Uint8Array) => Promise<void>;\n}\n\nexport interface RawTapeProvider {\n accept(upload: RawTapeUpload): Promise<ProviderResult<RawTapeArtifactRef, ViteProviderError>>;\n}\n\ntype ProviderResult<T, E> =\n | { readonly ok: true; readonly value: T }\n | { readonly ok: false; readonly error: E };\n\nexport function selectCaptureProvider(\n providers: readonly CaptureProvider[],\n): ProviderResult<CaptureProvider, CaptureProviderError> {\n const providerIds = providers.map((provider) => provider.id);\n if (providers.length === 0) {\n return { ok: false, error: { code: 'capture-target-unavailable', providerIds } };\n }\n if (providers.length > 1) {\n return { ok: false, error: { code: 'capture-target-ambiguous', providerIds } };\n }\n const provider = providers[0];\n if (provider === undefined) {\n return { ok: false, error: { code: 'capture-target-unavailable', providerIds } };\n }\n return { ok: true, value: provider };\n}\n\nexport function createRawTapeProvider(options: RawTapeProviderOptions): RawTapeProvider {\n const rootDir = resolve(options.rootDir);\n const write =\n options.writeFile ??\n (async (path: string, bytes: Uint8Array) => {\n await writeFile(path, bytes);\n });\n\n return {\n async accept(upload) {\n if (!RUN_ID_PATTERN.test(upload.runId)) {\n return {\n ok: false,\n error: {\n code: 'capture-run-id-invalid',\n hint: 'runId must contain only ASCII letters, numbers, underscore, or hyphen',\n },\n };\n }\n if (upload.contentType !== RHITAPE_MIME) {\n return {\n ok: false,\n error: {\n code: 'capture-mime-invalid',\n hint: `content-type must be exactly ${RHITAPE_MIME}`,\n },\n };\n }\n\n const decoded = decodeTape(upload.bytes);\n if (!decoded.ok) {\n return {\n ok: false,\n error: {\n code: 'capture-tape-invalid',\n hint: decoded.error.hint,\n },\n };\n }\n\n const digest = `sha256:${createHash('sha256').update(upload.bytes).digest('hex')}`;\n const debugDir = join(rootDir, '.forgeax-debug');\n const outDir = join(debugDir, upload.runId);\n const finalPath = join(outDir, 'frame.rhitape');\n const tempDir = join(debugDir, `.rhitape-${upload.runId}-${randomUUID()}`);\n const debugDirExisted = await pathExists(debugDir);\n const outDirExisted = await pathExists(outDir);\n\n try {\n await mkdir(tempDir, { recursive: true });\n await write(join(tempDir, 'frame.rhitape'), upload.bytes);\n await mkdir(outDir, { recursive: true });\n await rename(join(tempDir, 'frame.rhitape'), finalPath);\n await rm(tempDir, { recursive: true, force: true });\n } catch {\n await rm(tempDir, { recursive: true, force: true });\n if (!outDirExisted) await rm(outDir, { recursive: true, force: true });\n if (!debugDirExisted) await rm(debugDir, { recursive: true, force: true });\n return {\n ok: false,\n error: {\n code: 'capture-artifact-write-failed',\n hint: 'the raw tape could not be written atomically; inspect the dev-server filesystem and retry',\n },\n };\n }\n\n return { ok: true, value: { kind: 'rhi-tape', digest, path: finalPath } };\n },\n };\n}\n\nexport interface RhiDebugPluginOptions {\n readonly rootDir?: string;\n}\n\ninterface MiddlewareRequest extends AsyncIterable<Uint8Array> {\n readonly method?: string;\n readonly url?: string;\n readonly headers?: Readonly<Record<string, string | string[] | undefined>>;\n}\n\ninterface MiddlewareResponse {\n statusCode: number;\n setHeader(name: string, value: string): void;\n end(chunk?: string | Uint8Array): void;\n}\n\nfunction sendJson(res: MiddlewareResponse, status: number, payload: unknown): void {\n res.statusCode = status;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify(payload));\n}\n\nasync function readRawBody(req: AsyncIterable<Uint8Array>): Promise<Uint8Array> {\n const chunks: Uint8Array[] = [];\n let size = 0;\n for await (const chunk of req) {\n const bytes = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);\n chunks.push(bytes);\n size += bytes.byteLength;\n }\n const output = new Uint8Array(size);\n let offset = 0;\n for (const chunk of chunks) {\n output.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return output;\n}\n\nfunction contentType(req: MiddlewareRequest): string {\n const value = req.headers?.['content-type'];\n return Array.isArray(value) ? (value[0] ?? '') : (value ?? '');\n}\n\nasync function pathExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\nexport function vitePluginRhiDebug(options: RhiDebugPluginOptions = {}): Plugin {\n return {\n name: 'forgeax:rhi-debug',\n\n config(_config, env) {\n return {\n define: { [DEFINE_KEY]: JSON.stringify(env.command === 'serve' ? '1' : '0') },\n };\n },\n\n configureServer(server: ViteDevServer) {\n const provider = createRawTapeProvider({ rootDir: options.rootDir ?? process.cwd() });\n server.middlewares.use(async (request, response, next) => {\n const req = request as MiddlewareRequest;\n const res = response as unknown as MiddlewareResponse;\n const url = new URL(req.url ?? '', 'http://localhost');\n if (url.pathname !== RAW_TAPE_ROUTE) {\n next();\n return;\n }\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST');\n sendJson(res, 405, {\n error: 'method-not-allowed',\n hint: `use POST ${RAW_TAPE_ROUTE}?runId=<id> with raw ${RHITAPE_MIME} bytes`,\n });\n return;\n }\n\n const runId = url.searchParams.get('runId') ?? '';\n const bytes = await readRawBody(req);\n const result = await provider.accept({ runId, contentType: contentType(req), bytes });\n if (!result.ok) {\n sendJson(\n res,\n result.error.code === 'capture-artifact-write-failed' ? 500 : 400,\n result.error,\n );\n return;\n }\n res.statusCode = 200;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify(result.value));\n });\n },\n };\n}\n\nexport default vitePluginRhiDebug;\n"]}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@forgeax/engine-vite-plugin-rhi-debug",
3
+ "version": "0.0.0-dev.8d955ade1c79",
4
+ "private": false,
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "sideEffects": false,
8
+ "description": "Vite serve-only raw .rhitape capture provider with strict validation and atomic persistence.",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "main": "./dist/index.mjs",
17
+ "types": "./dist/index.d.ts",
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "peerDependencies": {
25
+ "@forgeax/engine-rhi-debug": "0.0.0-dev.8d955ade1c79",
26
+ "vite": "^7 || ^8"
27
+ },
28
+ "devDependencies": {
29
+ "@forgeax/engine-rhi-debug": "0.0.0-dev.8d955ade1c79",
30
+ "@forgeax/engine-types": "0.0.0-dev.8d955ade1c79",
31
+ "@types/node": "^20.14.0",
32
+ "vite": "8.0.10"
33
+ },
34
+ "forgeax": {
35
+ "metrics": {
36
+ "bundle-size": {
37
+ "enabled": true,
38
+ "path": "dist/index.mjs",
39
+ "compression": "gzip"
40
+ },
41
+ "fps": {
42
+ "enabled": false,
43
+ "reason": "dev-server-only vite plugin; no runtime canvas or frame loop"
44
+ },
45
+ "bench": {
46
+ "enabled": false,
47
+ "reason": "thin middleware shell validates and persists raw tape bytes; no perf-critical hot path"
48
+ },
49
+ "gate": {
50
+ "enabled": false,
51
+ "reason": "no standalone binary gate for this package; CI gates are repo-root scripts"
52
+ },
53
+ "spike-report": {
54
+ "enabled": false,
55
+ "reason": "not a spike package"
56
+ }
57
+ }
58
+ },
59
+ "scripts": {
60
+ "build": "tsup",
61
+ "typecheck": "tsc -b",
62
+ "test": "vitest run"
63
+ }
64
+ }
@@ -0,0 +1,57 @@
1
+ import { mkdtemp, readdir, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { encodeTape } from '@forgeax/engine-rhi-debug';
5
+ import { describe, expect, it } from 'vitest';
6
+ import { createRawTapeProvider, RHITAPE_MIME } from '../index';
7
+
8
+ async function validTapeBytes(): Promise<Uint8Array> {
9
+ const encoded = encodeTape({
10
+ header: { formatVersion: 7, rhiCaps: {}, eventCount: 0, blobCount: 0 },
11
+ bootstrap: [],
12
+ events: [],
13
+ blobs: [],
14
+ });
15
+ if (!encoded.ok) throw new Error(encoded.error.hint);
16
+ return encoded.value;
17
+ }
18
+
19
+ describe('atomic .rhitape persistence', () => {
20
+ it('leaves exactly one final artifact and no temporary sibling', async () => {
21
+ const rootDir = await mkdtemp(join(tmpdir(), 'forgeax-rhitape-atomic-'));
22
+ try {
23
+ const provider = createRawTapeProvider({ rootDir });
24
+ const result = await provider.accept({
25
+ runId: 'atomic',
26
+ contentType: RHITAPE_MIME,
27
+ bytes: await validTapeBytes(),
28
+ });
29
+ expect(result.ok).toBe(true);
30
+ const files = await readdir(join(rootDir, '.forgeax-debug', 'atomic'));
31
+ expect(files).toEqual(['frame.rhitape']);
32
+ } finally {
33
+ await rm(rootDir, { recursive: true, force: true });
34
+ }
35
+ });
36
+
37
+ it('reports disk failure without leaving final or temporary output', async () => {
38
+ const rootDir = await mkdtemp(join(tmpdir(), 'forgeax-rhitape-disk-'));
39
+ try {
40
+ const provider = createRawTapeProvider({
41
+ rootDir,
42
+ writeFile: async () => {
43
+ throw new Error('injected disk failure');
44
+ },
45
+ });
46
+ const result = await provider.accept({
47
+ runId: 'disk-failure',
48
+ contentType: RHITAPE_MIME,
49
+ bytes: await validTapeBytes(),
50
+ });
51
+ expect(result.ok).toBe(false);
52
+ await expect(readdir(join(rootDir, '.forgeax-debug'))).rejects.toThrow();
53
+ } finally {
54
+ await rm(rootDir, { recursive: true, force: true });
55
+ }
56
+ });
57
+ });
@@ -0,0 +1,107 @@
1
+ // config.define injection + prod constant-fold tests (w12).
2
+ //
3
+ // AC-07 / C6: config(cfg) injects "1" for dev serve and "0" for production
4
+ // build. The production literal lets Vite tree-shake the dev-only endpoint and
5
+ // avoids leaving a bare @forgeax/engine-rhi-debug import in preview bundles.
6
+
7
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
8
+ import { tmpdir } from 'node:os';
9
+ import { join } from 'node:path';
10
+
11
+ import { build, type Plugin } from 'vite';
12
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
13
+
14
+ import { vitePluginRhiDebug } from '../index';
15
+
16
+ // The factory returns a Vite plugin; view it through the Plugin type so the
17
+ // optional config / configureServer hooks are statically known to the test.
18
+ const asPlugin = (): Plugin => vitePluginRhiDebug() as unknown as Plugin;
19
+
20
+ // ─── config(cfg) hook unit assertion ─────────────────────────────────────────
21
+
22
+ describe('config(cfg) define injection', () => {
23
+ it('injects import.meta.env.FORGEAX_ENGINE_RHI_DEBUG = "1" for serve', () => {
24
+ const plugin = asPlugin();
25
+ const cfg: { define?: Record<string, string> } = {};
26
+ // Vite calls config(userConfig, env); the hook mutates/returns define.
27
+ const hook = typeof plugin.config === 'function' ? plugin.config : plugin.config?.handler;
28
+ const ret = hook?.call(
29
+ undefined as never,
30
+ cfg as never,
31
+ {
32
+ command: 'serve',
33
+ mode: 'development',
34
+ } as never,
35
+ );
36
+ const define = (ret as { define?: Record<string, string> } | undefined)?.define ?? cfg.define;
37
+ expect(define).toBeDefined();
38
+ expect(define?.['import.meta.env.FORGEAX_ENGINE_RHI_DEBUG']).toBe(JSON.stringify('1'));
39
+ });
40
+
41
+ it('injects import.meta.env.FORGEAX_ENGINE_RHI_DEBUG = "0" for build', () => {
42
+ const plugin = asPlugin();
43
+ const hook = typeof plugin.config === 'function' ? plugin.config : plugin.config?.handler;
44
+ const ret = hook?.call(
45
+ undefined as never,
46
+ {} as never,
47
+ {
48
+ command: 'build',
49
+ mode: 'production',
50
+ } as never,
51
+ );
52
+ const define = (ret as { define?: Record<string, string> } | undefined)?.define;
53
+ expect(define?.['import.meta.env.FORGEAX_ENGINE_RHI_DEBUG']).toBe(JSON.stringify('0'));
54
+ });
55
+ });
56
+
57
+ // ─── vite build constant-fold (enabled vs not) ───────────────────────────────
58
+
59
+ describe('vite build constant-fold (w12)', () => {
60
+ let tmpRoot: string;
61
+
62
+ beforeEach(async () => {
63
+ tmpRoot = await mkdtemp(join(tmpdir(), 'forgeax-vprd-define-'));
64
+ // Entry reads the guard flag and exports it, so the bundler cannot dead-strip
65
+ // the reference; whether it folds to "1" depends on define injection.
66
+ await writeFile(
67
+ join(tmpRoot, 'entry.js'),
68
+ 'export const flag = import.meta.env.FORGEAX_ENGINE_RHI_DEBUG;\n',
69
+ );
70
+ });
71
+
72
+ afterEach(async () => {
73
+ await rm(tmpRoot, { recursive: true, force: true });
74
+ });
75
+
76
+ async function buildCode(withPlugin: boolean): Promise<string> {
77
+ const output = await build({
78
+ root: tmpRoot,
79
+ logLevel: 'silent',
80
+ plugins: withPlugin ? [asPlugin()] : [],
81
+ build: {
82
+ write: false,
83
+ minify: false,
84
+ lib: {
85
+ entry: join(tmpRoot, 'entry.js'),
86
+ formats: ['es'],
87
+ fileName: 'out',
88
+ },
89
+ },
90
+ });
91
+ const arr = Array.isArray(output) ? output : [output];
92
+ const chunks = arr.flatMap((o) => ('output' in o ? o.output : []));
93
+ return chunks.map((c) => ('code' in c ? c.code : '')).join('\n');
94
+ }
95
+
96
+ it('with plugin: production bundle folds the flag to the literal "0"', async () => {
97
+ const code = await buildCode(true);
98
+ // define replaces `import.meta.env.FORGEAX_ENGINE_RHI_DEBUG` with "0".
99
+ expect(code).toContain('"0"');
100
+ expect(code).not.toContain('FORGEAX_ENGINE_RHI_DEBUG');
101
+ });
102
+
103
+ it('without plugin: bundle retains no FORGEAX_ENGINE_RHI_DEBUG literal residue', async () => {
104
+ const code = await buildCode(false);
105
+ expect(code).not.toContain('FORGEAX_ENGINE_RHI_DEBUG');
106
+ });
107
+ });
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { type CaptureProvider, selectCaptureProvider } from '../index';
3
+
4
+ const provider = (id: string): CaptureProvider => ({ id });
5
+
6
+ describe('capture provider cardinality', () => {
7
+ it('fails closed when no provider is available', () => {
8
+ const result = selectCaptureProvider([]);
9
+ expect(result).toEqual({
10
+ ok: false,
11
+ error: { code: 'capture-target-unavailable', providerIds: [] },
12
+ });
13
+ });
14
+
15
+ it('returns the only provider without creating a second route', () => {
16
+ const only = provider('tab-a');
17
+ expect(selectCaptureProvider([only])).toEqual({ ok: true, value: only });
18
+ });
19
+
20
+ it('fails closed when multiple providers race for one artifact', () => {
21
+ const result = selectCaptureProvider([provider('tab-a'), provider('tab-b')]);
22
+ expect(result).toEqual({
23
+ ok: false,
24
+ error: { code: 'capture-target-ambiguous', providerIds: ['tab-a', 'tab-b'] },
25
+ });
26
+ });
27
+ });
@@ -0,0 +1,86 @@
1
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { decodeTape, encodeTape } from '@forgeax/engine-rhi-debug';
5
+ import { describe, expect, it } from 'vitest';
6
+ import { createRawTapeProvider, RHITAPE_MIME, vitePluginRhiDebug } from '../index';
7
+
8
+ async function validTapeBytes(): Promise<Uint8Array> {
9
+ const encoded = encodeTape({
10
+ header: { formatVersion: 7, rhiCaps: {}, eventCount: 0, blobCount: 0 },
11
+ bootstrap: [],
12
+ events: [],
13
+ blobs: [],
14
+ });
15
+ if (!encoded.ok) throw new Error(encoded.error.hint);
16
+ return encoded.value;
17
+ }
18
+
19
+ describe('raw .rhitape provider', () => {
20
+ it('accepts one raw v7 body, validates it, and returns one digest-bearing artifact', async () => {
21
+ const rootDir = await mkdtemp(join(tmpdir(), 'forgeax-rhitape-provider-'));
22
+ try {
23
+ const bytes = await validTapeBytes();
24
+ const provider = createRawTapeProvider({ rootDir });
25
+ const result = await provider.accept({
26
+ runId: 'raw-provider',
27
+ contentType: RHITAPE_MIME,
28
+ bytes,
29
+ });
30
+
31
+ expect(result.ok).toBe(true);
32
+ if (!result.ok) return;
33
+ expect(result.value).toMatchObject({
34
+ kind: 'rhi-tape',
35
+ path: join(rootDir, '.forgeax-debug', 'raw-provider', 'frame.rhitape'),
36
+ });
37
+ expect(decodeTape(await readFile(result.value.path))).toMatchObject({ ok: true });
38
+ } finally {
39
+ await rm(rootDir, { recursive: true, force: true });
40
+ }
41
+ });
42
+
43
+ it('rejects non-raw MIME and invalid bytes before writing', async () => {
44
+ const rootDir = await mkdtemp(join(tmpdir(), 'forgeax-rhitape-invalid-'));
45
+ try {
46
+ const provider = createRawTapeProvider({ rootDir });
47
+ const wrongMime = await provider.accept({
48
+ runId: 'wrong-mime',
49
+ contentType: 'application/json',
50
+ bytes: await validTapeBytes(),
51
+ });
52
+ const invalidBytes = await provider.accept({
53
+ runId: 'invalid-bytes',
54
+ contentType: RHITAPE_MIME,
55
+ bytes: new Uint8Array([1, 2, 3]),
56
+ });
57
+
58
+ expect(wrongMime.ok).toBe(false);
59
+ expect(invalidBytes.ok).toBe(false);
60
+ await expect(
61
+ readFile(join(rootDir, '.forgeax-debug', 'wrong-mime', 'frame.rhitape')),
62
+ ).rejects.toThrow();
63
+ await expect(
64
+ readFile(join(rootDir, '.forgeax-debug', 'invalid-bytes', 'frame.rhitape')),
65
+ ).rejects.toThrow();
66
+ } finally {
67
+ await rm(rootDir, { recursive: true, force: true });
68
+ }
69
+ });
70
+
71
+ it('keeps debug flag enabled only for serve', () => {
72
+ const plugin = vitePluginRhiDebug();
73
+ expect(plugin.config).toBeTypeOf('function');
74
+ if (typeof plugin.config !== 'function') return;
75
+ expect(
76
+ plugin.config?.call(undefined as never, {}, { command: 'serve', mode: 'development' }),
77
+ ).toMatchObject({
78
+ define: { 'import.meta.env.FORGEAX_ENGINE_RHI_DEBUG': '"1"' },
79
+ });
80
+ expect(
81
+ plugin.config?.call(undefined as never, {}, { command: 'build', mode: 'production' }),
82
+ ).toMatchObject({
83
+ define: { 'import.meta.env.FORGEAX_ENGINE_RHI_DEBUG': '"0"' },
84
+ });
85
+ });
86
+ });