Files
open-design/e2e/specs/namespace/main.spec.ts
0688faaec3 reland(sidecar): converge product clients on package boundary (#7636)
* reland(sidecar): converge product clients after weekly release

Restore the sidecar convergence from #7244 after the release-scheduling revert in #7634. This tree matches the previously reviewed and cross-platform QA-accepted implementation at d83d4e032f.

* fix(sidecar): clarify identity and cache witnesses

Generated-By: looper 0.11.2 (runner=fixer, agent=codex)

* fix(packaged): probe cross-source desktop owners

Generated-By: looper 0.11.2 (runner=fixer, agent=codex)

* fix(sidecar): preserve cross-source launch ownership

Generated-By: looper 0.11.2 (runner=fixer, agent=codex)

* test(pack): assert cross-source Windows launch ownership

Generated-By: looper 0.11.2 (runner=fixer, agent=codex)

* fix(sidecar): align HyperFrames with client boundary

Generated-By: looper 0.11.2 (runner=fixer, agent=codex)

* fix(e2e): keep HyperFrames fixture in helper lane

Generated-By: looper 0.11.2 (runner=fixer, agent=codex)

* docs(sidecar): restore review and handoff contracts

Generated-By: looper 0.11.2 (runner=fixer, agent=codex)

* fix(sidecar): restore cache and daemon boundaries

Generated-By: looper 0.11.2 (runner=fixer, agent=codex)

* fix(pack): restore sourcemaps and Linux smoke discovery

Generated-By: looper 0.11.2 (runner=fixer, agent=codex)

* fix(sidecar): make payload handoff generation-atomic

* fix(sidecar): preserve packaged action and mode parity

Generated-By: looper 0.11.2 (runner=fixer, agent=codex)

* test(pack): align Linux cleanup resource count

Generated-By: looper 0.11.2 (runner=fixer, agent=codex)

* fix(packaged): preserve headless sidecar mode

* test: normalize source line endings

* fix(daemon): launch Codex cmd shims on Windows

---------

Co-authored-by: Looper <looper@noreply.github.com>
2026-09-01 13:22:04 +00:00

238 lines
7.9 KiB
TypeScript

// @vitest-environment node
import { execFile } from 'node:child_process';
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { promisify } from 'node:util';
import { describe, expect, test } from 'vitest';
import { requestJson } from '@/vitest/http';
import { createSmokeSuite, e2eWorkspaceRoot } from '@/vitest/suite';
const execFileAsync = promisify(execFile);
type DaemonStatusResponse = {
dataDir?: string;
installedPlugins?: unknown;
mediaConfigDir?: string | null;
namespace?: unknown;
pid?: number;
port?: number;
};
type McpInstallInfoResponse = {
args: string[];
env: Record<string, string>;
};
type InstalledPlugin = {
fsPath?: string;
id: string;
source?: string;
sourceKind?: string;
version?: string;
};
type SseEvent = {
data: unknown;
event: string;
};
describe('namespace isolation spec', () => {
test('keeps namespace in lifecycle infrastructure while daemon clients use URL or an opaque sidecar capability', async () => {
const suite = await createSmokeSuite('namespace-isolation');
await suite.with.toolsDev(async ({ runtime, status, webUrl }) => {
expect(status.namespace).toBe(suite.namespace);
const daemonStatus = await requestJson<DaemonStatusResponse>(webUrl, '/api/daemon/status');
expect(daemonStatus.port).toBe(runtime.daemonPort);
expect(daemonStatus.dataDir).toBe(suite.dataDir);
expect(daemonStatus).not.toHaveProperty('namespace');
const installInfo = await requestJson<McpInstallInfoResponse>(webUrl, '/api/mcp/install-info');
const clientEndpoint = installInfo.env.OD_SIDECAR_CLIENT_ENDPOINT;
if (typeof clientEndpoint !== 'string' || clientEndpoint.length === 0) {
throw new Error('MCP install-info did not include the opaque sidecar client capability');
}
expect(Object.keys(installInfo.env).sort()).toEqual([
'OD_DATA_DIR',
'OD_SIDECAR_CLIENT_ENDPOINT',
]);
expect(installInfo.args).not.toContain('--daemon-url');
expect(installInfo.env.OD_DATA_DIR).toBe(suite.dataDir);
expect(installInfo.env).not.toHaveProperty('OD_NAMESPACE');
expect(installInfo.env).not.toHaveProperty('OD_SIDECAR_NAMESPACE');
expect(installInfo.env).not.toHaveProperty('OD_SIDECAR_IPC_BASE');
const cliStatus = await runDaemonCliJson<DaemonStatusResponse>(
['daemon', 'status', '--json'],
{
OD_DATA_DIR: suite.dataDir,
OD_NAMESPACE: 'wrong-daemon-namespace',
OD_SIDECAR_IPC_BASE: path.join(suite.scratchDir, 'wrong-ipc-base'),
OD_SIDECAR_CLIENT_ENDPOINT: clientEndpoint,
OD_SIDECAR_NAMESPACE: 'wrong-sidecar-namespace',
},
);
expect(cliStatus.port).toBe(runtime.daemonPort);
expect(cliStatus.dataDir).toBe(suite.dataDir);
expect(cliStatus).not.toHaveProperty('namespace');
const rejected = await runDaemonCliExpectFailure(
['daemon', 'status', '--json', '--namespace', 'should-not-parse'],
{ OD_SIDECAR_CLIENT_ENDPOINT: clientEndpoint },
);
expect(`${rejected.stdout}\n${rejected.stderr}`).toContain('unknown flag: --namespace');
const pluginSource = await writeLocalPluginFixture(suite.scratchDir);
const sourceForDaemon = `./${path.relative(e2eWorkspaceRoot(), pluginSource).replace(/\\/g, '/')}`;
const installResponse = await fetch(new URL('/api/plugins/install', ensureTrailingSlash(webUrl)), {
body: JSON.stringify({ source: sourceForDaemon }),
headers: { 'content-type': 'application/json' },
method: 'POST',
});
expect(installResponse.ok).toBe(true);
const installEvents = parseSseEvents(await installResponse.text());
const success = installEvents.find((entry) => entry.event === 'success')?.data as
| { plugin?: InstalledPlugin }
| undefined;
const expectedPluginRoot = path.join(suite.dataDir, 'plugins', 'e2e-namespace-plugin');
expect(success?.plugin?.id).toBe('e2e-namespace-plugin');
expect(success?.plugin?.fsPath).toBe(expectedPluginRoot);
const pluginInfo = await requestJson<InstalledPlugin>(webUrl, '/api/plugins/e2e-namespace-plugin');
expect(pluginInfo.fsPath).toBe(expectedPluginRoot);
await access(path.join(expectedPluginRoot, 'open-design.json'));
expect(await readFile(path.join(expectedPluginRoot, 'SKILL.md'), 'utf8')).toContain('E2E namespace plugin');
await suite.report.json('summary.json', {
cliStatus,
daemonStatus,
installInfo: {
args: installInfo.args,
envKeys: Object.keys(installInfo.env).sort(),
},
plugin: pluginInfo,
runtime,
toolsDevNamespace: status.namespace,
});
});
}, 180_000);
});
async function writeLocalPluginFixture(root: string): Promise<string> {
const pluginRoot = path.join(root, 'plugin-source');
await mkdir(pluginRoot, { recursive: true });
await writeFile(
path.join(pluginRoot, 'open-design.json'),
JSON.stringify(
{
name: 'e2e-namespace-plugin',
od: {
inputs: [{ name: 'topic', required: true, type: 'string' }],
kind: 'skill',
taskKind: 'new-generation',
useCase: { query: 'Make a {{topic}} brief.' },
},
title: 'E2E Namespace Plugin',
version: '1.0.0',
},
null,
2,
),
);
await writeFile(
path.join(pluginRoot, 'SKILL.md'),
[
'# E2E namespace plugin',
'',
'Create a deterministic fixture artifact for namespace isolation tests.',
'',
].join('\n'),
);
return pluginRoot;
}
async function runDaemonCliJson<T>(args: string[], env: Record<string, string>): Promise<T> {
const result = await runDaemonCli(args, env);
return parseJsonOutput<T>(result.stdout);
}
async function runDaemonCliExpectFailure(
args: string[],
env: Record<string, string>,
): Promise<{ stderr: string; stdout: string }> {
try {
await runDaemonCli(args, env);
} catch (error) {
const failure = error as { stderr?: string; stdout?: string };
return {
stderr: failure.stderr ?? '',
stdout: failure.stdout ?? '',
};
}
throw new Error(`expected daemon CLI to fail for args: ${args.join(' ')}`);
}
async function runDaemonCli(
args: string[],
env: Record<string, string>,
): Promise<{ stderr: string; stdout: string }> {
const mergedEnv: NodeJS.ProcessEnv = {
...process.env,
...env,
};
delete mergedEnv.OD_DAEMON_URL;
delete mergedEnv.OD_PORT;
const { stderr, stdout } = await execFileAsync(
process.execPath,
['--import', 'tsx', 'apps/daemon/src/cli.ts', ...args],
{
cwd: e2eWorkspaceRoot(),
env: mergedEnv,
maxBuffer: 20 * 1024 * 1024,
},
);
return { stderr, stdout };
}
function parseJsonOutput<T>(stdout: string): T {
const trimmed = stdout.trim();
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
return JSON.parse(trimmed) as T;
}
const objectStart = stdout.lastIndexOf('\n{');
const arrayStart = stdout.lastIndexOf('\n[');
const jsonStart = Math.max(objectStart, arrayStart);
if (jsonStart < 0) {
throw new Error(`Expected JSON output from daemon CLI, got: ${stdout}`);
}
return JSON.parse(stdout.slice(jsonStart + 1)) as T;
}
function parseSseEvents(text: string): SseEvent[] {
return text
.split(/\r?\n\r?\n/)
.map((block) => block.trim())
.filter(Boolean)
.map((block) => {
let event = 'message';
const dataLines: string[] = [];
for (const line of block.split(/\r?\n/)) {
if (line.startsWith('event:')) event = line.slice('event:'.length).trim();
if (line.startsWith('data:')) dataLines.push(line.slice('data:'.length).trim());
}
return {
data: dataLines.length > 0 ? JSON.parse(dataLines.join('\n')) : null,
event,
};
});
}
function ensureTrailingSlash(value: string): string {
return value.endsWith('/') ? value : `${value}/`;
}