mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-02 05:24:34 +08:00
Add workspace path expectation guardrails
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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(
|
||||
|
||||
@@ -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')
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user