mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 06:18:24 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a88186909 | ||
|
|
493605756f |
@@ -1,6 +1,9 @@
|
||||
import { promises as fs, constants as fsConstants, realpathSync } from 'fs';
|
||||
import * as nodeFs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const fs = nodeFs.promises;
|
||||
const { constants: fsConstants } = nodeFs;
|
||||
|
||||
function isMarkerOnOwnLine(content: string, markerIndex: number, markerLength: number): boolean {
|
||||
let leftIndex = markerIndex - 1;
|
||||
while (leftIndex >= 0 && content[leftIndex] !== '\n') {
|
||||
@@ -56,9 +59,14 @@ export class FileSystemUtils {
|
||||
*/
|
||||
static canonicalizeExistingPath(targetPath: string): string {
|
||||
try {
|
||||
return realpathSync(targetPath);
|
||||
// Prefer the native resolver so Windows short-path aliases are expanded.
|
||||
return nodeFs.realpathSync.native(targetPath);
|
||||
} catch {
|
||||
return path.resolve(targetPath);
|
||||
try {
|
||||
return nodeFs.realpathSync(targetPath);
|
||||
} catch {
|
||||
return path.resolve(targetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,14 @@ import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { runCLI } from '../helpers/run-cli.js';
|
||||
import { FileSystemUtils } from '../../src/utils/file-system.js';
|
||||
|
||||
describe('artifact-workflow CLI commands', () => {
|
||||
let tempDir: string;
|
||||
let changesDir: string;
|
||||
|
||||
const canonical = (targetPath: string): string => FileSystemUtils.canonicalizeExistingPath(targetPath);
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'openspec-artifact-workflow-'));
|
||||
changesDir = path.join(tempDir, 'openspec', 'changes');
|
||||
@@ -411,10 +414,8 @@ describe('artifact-workflow CLI commands', () => {
|
||||
expect(result.stderr).toBe('');
|
||||
|
||||
const json = JSON.parse(result.stdout);
|
||||
const expectedProposalPath = await fs.realpath(path.join(changesDir, 'json-apply', 'proposal.md'));
|
||||
const expectedSpecPath = await fs.realpath(
|
||||
path.join(changesDir, 'json-apply', 'specs', 'test-spec.md')
|
||||
);
|
||||
const expectedProposalPath = canonical(path.join(changesDir, 'json-apply', 'proposal.md'));
|
||||
const expectedSpecPath = canonical(path.join(changesDir, 'json-apply', 'specs', 'test-spec.md'));
|
||||
expect(json.changeName).toBe('json-apply');
|
||||
expect(json.schemaName).toBe('spec-driven');
|
||||
expect(json.state).toBe('ready');
|
||||
@@ -472,7 +473,7 @@ apply:
|
||||
);
|
||||
expect(applyResult.exitCode).toBe(0);
|
||||
const applyJson = JSON.parse(applyResult.stdout);
|
||||
const resolvedSpecPath = await fs.realpath(specPath);
|
||||
const resolvedSpecPath = canonical(specPath);
|
||||
expect(applyJson.state).toBe('ready');
|
||||
expect(applyJson.missingArtifacts).toBeUndefined();
|
||||
expect(applyJson.contextFiles).toEqual({
|
||||
|
||||
@@ -2,11 +2,14 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { FileSystemUtils } from '../../../src/utils/file-system.js';
|
||||
import { artifactOutputExists, resolveArtifactOutputs } from '../../../src/core/artifact-graph/outputs.js';
|
||||
|
||||
describe('artifact-graph/outputs', () => {
|
||||
let tempDir: string;
|
||||
|
||||
const canonical = (targetPath: string): string => FileSystemUtils.canonicalizeExistingPath(targetPath);
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = path.join(os.tmpdir(), `openspec-outputs-test-${Date.now()}`);
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
@@ -20,7 +23,7 @@ describe('artifact-graph/outputs', () => {
|
||||
const filePath = path.join(tempDir, 'proposal.md');
|
||||
fs.writeFileSync(filePath, 'content');
|
||||
|
||||
expect(resolveArtifactOutputs(tempDir, 'proposal.md')).toEqual([fs.realpathSync(filePath)]);
|
||||
expect(resolveArtifactOutputs(tempDir, 'proposal.md')).toEqual([canonical(filePath)]);
|
||||
expect(artifactOutputExists(tempDir, 'proposal.md')).toBe(true);
|
||||
});
|
||||
|
||||
@@ -38,7 +41,7 @@ describe('artifact-graph/outputs', () => {
|
||||
fs.mkdirSync(nestedDir, { recursive: true });
|
||||
fs.writeFileSync(filePath, 'content');
|
||||
|
||||
expect(resolveArtifactOutputs(tempDir, 'specs/*/spec.md')).toEqual([fs.realpathSync(filePath)]);
|
||||
expect(resolveArtifactOutputs(tempDir, 'specs/*/spec.md')).toEqual([canonical(filePath)]);
|
||||
expect(artifactOutputExists(tempDir, 'specs/*/spec.md')).toBe(true);
|
||||
});
|
||||
|
||||
@@ -50,7 +53,7 @@ describe('artifact-graph/outputs', () => {
|
||||
fs.writeFileSync(matching, 'content');
|
||||
fs.writeFileSync(nonMatching, 'content');
|
||||
|
||||
expect(resolveArtifactOutputs(tempDir, 'specs/foo*.md')).toEqual([fs.realpathSync(matching)]);
|
||||
expect(resolveArtifactOutputs(tempDir, 'specs/foo*.md')).toEqual([canonical(matching)]);
|
||||
});
|
||||
|
||||
it('supports question-mark glob patterns', () => {
|
||||
@@ -60,7 +63,7 @@ describe('artifact-graph/outputs', () => {
|
||||
fs.writeFileSync(matching, 'content');
|
||||
fs.writeFileSync(path.join(specsDir, 'a10.md'), 'content');
|
||||
|
||||
expect(resolveArtifactOutputs(tempDir, 'specs/a?.md')).toEqual([fs.realpathSync(matching)]);
|
||||
expect(resolveArtifactOutputs(tempDir, 'specs/a?.md')).toEqual([canonical(matching)]);
|
||||
});
|
||||
|
||||
it('supports character class glob patterns', () => {
|
||||
@@ -73,8 +76,8 @@ describe('artifact-graph/outputs', () => {
|
||||
fs.writeFileSync(path.join(specsDir, 'c.md'), 'content');
|
||||
|
||||
expect(resolveArtifactOutputs(tempDir, 'specs/[ab].md')).toEqual([
|
||||
fs.realpathSync(aPath),
|
||||
fs.realpathSync(bPath),
|
||||
canonical(aPath),
|
||||
canonical(bPath),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -92,10 +95,10 @@ describe('artifact-graph/outputs', () => {
|
||||
fs.symlinkSync(realChangeDir, aliasChangeDir, process.platform === 'win32' ? 'junction' : 'dir');
|
||||
|
||||
expect(resolveArtifactOutputs(aliasChangeDir, 'proposal.md')).toEqual([
|
||||
fs.realpathSync(proposalPath),
|
||||
canonical(proposalPath),
|
||||
]);
|
||||
expect(resolveArtifactOutputs(aliasChangeDir, 'specs/*/spec.md')).toEqual([
|
||||
fs.realpathSync(specPath),
|
||||
canonical(specPath),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import * as nodeFs from 'fs';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
@@ -92,6 +93,22 @@ describe('FileSystemUtils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('canonicalizeExistingPath', () => {
|
||||
it('should prefer the native realpath resolver when available', async () => {
|
||||
const filePath = path.join(testDir, 'canonical.txt');
|
||||
await fs.writeFile(filePath, 'content');
|
||||
|
||||
const nativeSpy = vi.spyOn(nodeFs.realpathSync, 'native');
|
||||
|
||||
const resolved = FileSystemUtils.canonicalizeExistingPath(filePath);
|
||||
|
||||
expect(nativeSpy).toHaveBeenCalledWith(filePath);
|
||||
expect(resolved).toBe(nodeFs.realpathSync.native(filePath));
|
||||
|
||||
nativeSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('writeFile', () => {
|
||||
it('should write content to file', async () => {
|
||||
const filePath = path.join(testDir, 'output.txt');
|
||||
|
||||
Reference in New Issue
Block a user