Add workspace path expectation guardrails

This commit is contained in:
TabishB
2026-05-06 14:35:57 +10:00
parent b642398bf3
commit 344b87a18f
5 changed files with 141 additions and 50 deletions
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest';
import * as fs from 'node:fs';
function readWorkspaceCommandTestSource(): string {
return fs.readFileSync(new URL('./workspace.test.ts', import.meta.url), 'utf-8');
}
describe('workspace command path expectation guardrails', () => {
it('keeps generated workspace folder expectations behind canonical path helpers', () => {
const source = readWorkspaceCommandTestSource();
expect(source).not.toMatch(/\.folders\)\.toEqual\(\s*\[/u);
expect(source).not.toMatch(/expect\(workspaceFolders\)\.toEqual\(\s*\[/u);
});
it('keeps opener launch expectations behind canonical path helpers', () => {
const source = readWorkspaceCommandTestSource();
expect(source).not.toMatch(/expect\([^)]*Launch\.args\)\.toEqual\(/u);
expect(source).not.toMatch(/expect\(fs\.realpathSync\.native\([^)]*Launch\.cwd\)\)\.toBe/u);
expect(source).not.toMatch(/getWorkspaceCodeWorkspacePath\(expectedExistingPath\(/u);
});
});
+1 -4
View File
@@ -9,6 +9,7 @@ import {
getWorkspaceLocalStatePath,
parseWorkspaceLocalState,
} from '../../src/core/workspace/index.js';
import { expectedExistingPath } from '../helpers/workspace-paths.js';
vi.mock('@inquirer/prompts', () => ({
input: vi.fn(),
@@ -88,10 +89,6 @@ describe('workspace command interactive flows', () => {
return dir;
}
function expectedExistingPath(existingPath: string): string {
return process.platform === 'win32' ? fs.realpathSync.native(existingPath) : existingPath;
}
function readLocalState(workspaceName: string) {
const workspaceRoot = getManagedWorkspaceRoot(workspaceName);
return parseWorkspaceLocalState(
+41 -42
View File
@@ -25,6 +25,12 @@ import {
} from '../../src/core/workspace/index.js';
import { FileSystemUtils } from '../../src/utils/file-system.js';
import { runCLI, type RunCLIResult } from '../helpers/run-cli.js';
import {
expectedExistingPath,
expectedWorkspaceCodeWorkspacePath,
expectedWorkspaceFolders,
expectWorkspaceLaunchLog,
} from '../helpers/workspace-paths.js';
describe('workspace command', () => {
let tempDir: string;
@@ -51,10 +57,6 @@ describe('workspace command', () => {
return dir;
}
function expectedExistingPath(existingPath: string): string {
return process.platform === 'win32' ? fs.realpathSync.native(existingPath) : existingPath;
}
function parseJson(result: RunCLIResult): any {
try {
return JSON.parse(result.stdout);
@@ -197,19 +199,21 @@ describe('workspace command', () => {
expect(fs.readFileSync(path.join(workspaceRoot, 'AGENTS.md'), 'utf-8')).toContain(
'OpenSpec Workspace Guidance'
);
expect(JSON.parse(fs.readFileSync(getWorkspaceCodeWorkspacePath(workspaceRoot, 'platform'), 'utf-8')).folders).toEqual([
{
path: '.',
},
{
name: 'api',
path: expectedApi,
},
{
name: 'checkout',
path: expectedCheckout,
},
]);
expect(JSON.parse(fs.readFileSync(getWorkspaceCodeWorkspacePath(workspaceRoot, 'platform'), 'utf-8')).folders).toEqual(
expectedWorkspaceFolders([
{
path: '.',
},
{
name: 'api',
path: api,
},
{
name: 'checkout',
path: checkout,
},
])
);
const list = await runCLI(['workspace', 'ls', '--json'], { cwd: tempDir, env });
expect(list.exitCode).toBe(0);
@@ -959,7 +963,6 @@ paths:
it('opens a workspace through VS Code editor and agent overrides without changing stored preference', async () => {
const api = mkdir('repos/api');
const expectedApi = expectedExistingPath(api);
const web = mkdir('repos/web');
const setup = await setupWorkspace('platform', [`api=${api}`, `web=${web}`], ['--opener', 'editor']);
fs.rmSync(web, { recursive: true, force: true });
@@ -977,22 +980,22 @@ paths:
const workspaceFolders = JSON.parse(
fs.readFileSync(getWorkspaceCodeWorkspacePath(setup.workspace.root, 'platform'), 'utf-8')
).folders;
expect(workspaceFolders).toEqual([
{
path: '.',
},
{
name: 'api',
path: expectedApi,
},
]);
const editorLaunch = readLaunchLog(code.logPath);
expect(fs.realpathSync.native(editorLaunch.cwd)).toBe(
fs.realpathSync.native(setup.workspace.root)
expect(workspaceFolders).toEqual(
expectedWorkspaceFolders([
{
path: '.',
},
{
name: 'api',
path: api,
},
])
);
expect(editorLaunch.args).toEqual([
getWorkspaceCodeWorkspacePath(expectedExistingPath(setup.workspace.root), 'platform'),
]);
const editorLaunch = readLaunchLog(code.logPath);
expectWorkspaceLaunchLog(editorLaunch, {
cwd: setup.workspace.root,
args: [{ workspaceFile: { root: setup.workspace.root, name: 'platform' } }],
});
const currentWorkspaceOpen = await runCLI(['workspace', 'open', '--editor', '--no-interactive'], {
cwd: path.join(setup.workspace.root, WORKSPACE_CHANGES_DIR_NAME),
@@ -1011,14 +1014,10 @@ paths:
expect(codexOpen.exitCode).toBe(0);
const codexLaunch = readLaunchLog(codex.logPath);
expect(fs.realpathSync.native(codexLaunch.cwd)).toBe(
fs.realpathSync.native(setup.workspace.root)
);
expect(codexLaunch.args).toEqual([
'--add-dir',
expectedApi,
'Open this OpenSpec workspace.',
]);
expectWorkspaceLaunchLog(codexLaunch, {
cwd: setup.workspace.root,
args: ['--add-dir', { existingPath: api }, 'Open this OpenSpec workspace.'],
});
expect(readLocalState(setup.workspace.root).preferred_opener).toEqual({
kind: 'editor',
id: 'vscode',
@@ -1116,7 +1115,7 @@ preferred_opener:
expect(unavailable.exitCode).toBe(1);
expect(unavailable.stderr).toContain("'code' was not found on PATH");
expect(unavailable.stderr).toContain(
getWorkspaceCodeWorkspacePath(expectedExistingPath(platform.workspace.root), 'platform')
expectedWorkspaceCodeWorkspacePath(platform.workspace.root, 'platform')
);
});
+1 -4
View File
@@ -48,6 +48,7 @@ import {
writeWorkspaceLocalState,
writeWorkspaceRegistryState,
} from '../../../src/core/workspace/index.js';
import { expectedExistingPath } from '../../helpers/workspace-paths.js';
describe('workspace foundation', () => {
let tempDir: string;
@@ -84,10 +85,6 @@ paths: {}
return workspaceRoot;
}
function expectedExistingPath(existingPath: string): string {
return process.platform === 'win32' ? fs.realpathSync.native(existingPath) : existingPath;
}
describe('path helpers', () => {
it('exposes the workspace constants', () => {
expect(WORKSPACE_METADATA_DIR_NAME).toBe('.openspec-workspace');
+75
View File
@@ -0,0 +1,75 @@
import { expect } from 'vitest';
import * as fs from 'node:fs';
import { getWorkspaceCodeWorkspacePath } from '../../src/core/workspace/index.js';
/**
* Workspace commands canonicalize existing filesystem paths before storing,
* reporting, or passing them to openers. On Windows, GitHub runners can expose
* temp paths through short aliases such as RUNNER~1 while Node's native
* realpath expands them to the long user path. Use these helpers for expected
* workspace command paths instead of comparing raw mkdtemp/os.tmpdir strings.
*/
export function expectedExistingPath(existingPath: string): string {
return process.platform === 'win32' ? fs.realpathSync.native(existingPath) : existingPath;
}
function equivalentExistingPath(existingPath: string): string {
return fs.realpathSync.native(existingPath);
}
export function expectedWorkspaceCodeWorkspacePath(
workspaceRoot: string,
workspaceName: string
): string {
return getWorkspaceCodeWorkspacePath(expectedExistingPath(workspaceRoot), workspaceName);
}
export function expectedWorkspaceFolders<T extends { path: string }>(folders: T[]): T[] {
return folders.map((folder) =>
folder.path === '.'
? folder
: {
...folder,
path: expectedExistingPath(folder.path),
}
);
}
type WorkspaceLaunchArgExpectation =
| string
| {
existingPath: string;
}
| {
workspaceFile: {
root: string;
name: string;
};
};
function expectedWorkspaceLaunchArg(arg: WorkspaceLaunchArgExpectation): string {
if (typeof arg === 'string') {
return arg;
}
if ('existingPath' in arg) {
return expectedExistingPath(arg.existingPath);
}
return expectedWorkspaceCodeWorkspacePath(arg.workspaceFile.root, arg.workspaceFile.name);
}
export function expectedWorkspaceLaunchArgs(
args: WorkspaceLaunchArgExpectation[]
): string[] {
return args.map(expectedWorkspaceLaunchArg);
}
export function expectWorkspaceLaunchLog(
actual: { cwd: string; args: string[] },
expected: { cwd: string; args: WorkspaceLaunchArgExpectation[] }
): void {
expect(equivalentExistingPath(actual.cwd)).toBe(equivalentExistingPath(expected.cwd));
expect(actual.args).toEqual(expectedWorkspaceLaunchArgs(expected.args));
}