mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
31d98634c3
* feat(web): add composer context sources * fix(web): require resolved context paths * fix(web): keep local context removal authoritative * fix(web): preserve project reference fallbacks * fix(web): clear linked dirs with context chips * fix(web): preserve context dirs when changing workdir * fix(web): promote matching context workdir * fix(web): preserve shared context dir ownership * fix(web): retain home context dirs in composer * fix(web): preserve promoted context dirs on clear * fix(web): keep linked context chips after send * fix(web): hydrate home workspace context chips * fix(web): stabilize workspace context submissions * fix(web): drop stale home workspace context dirs * fix(web): fail missing workspace context dirs * fix(web): keep home workspace context visible * feat(web): implement project directory materialization for references - Added `ensureReferencedProjectDir` function to create a managed project's folder if it doesn't exist, ensuring that references to new projects resolve correctly. - Updated `getProjectDetail` to include an option for directory materialization, preventing errors when referencing uninitialized projects. - Enhanced `ProjectReferenceModal` to filter out external projects with missing directories, improving user experience. - Introduced tests for the new functionality to ensure proper behavior when referencing projects. This change stabilizes project referencing and enhances the overall workflow for users dealing with managed projects. * fix reference context fail-fast paths
76 lines
3.2 KiB
TypeScript
76 lines
3.2 KiB
TypeScript
// Red-spec for referencing a brand-new (never-generated) project.
|
|
//
|
|
// A managed project is only a DB row until its first file write — its
|
|
// `PROJECTS_DIR/<id>` folder does not exist yet. When such a project is picked
|
|
// via "Reference another project", the daemon resolves it to that computed
|
|
// path; the composer's existence probe (`/api/dir-exists`) then reports the
|
|
// folder missing and the reference is dropped (previously it even blocked the
|
|
// whole submission). GET /api/projects/:id?ensureDir=1 fixes this by calling
|
|
// ensureReferencedProjectDir, which materializes the managed folder so the
|
|
// reference resolves to a real directory. External / imported roots (absolute
|
|
// baseDir) are the user's own folders and must never be created here.
|
|
|
|
import { mkdtempSync, rmSync, existsSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
|
|
import { ensureReferencedProjectDir } from '../src/routes/project/index.js';
|
|
import { ensureProject } from '../src/projects.js';
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
function makeProjectsRoot(): string {
|
|
const dir = mkdtempSync(path.join(tmpdir(), 'od-ref-ensure-'));
|
|
tempDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const dir of tempDirs.splice(0)) {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe('ensureReferencedProjectDir', () => {
|
|
it('materializes a managed project folder that has never been written to', async () => {
|
|
const projectsRoot = makeProjectsRoot();
|
|
const project = { id: 'reference-managed', metadata: { kind: 'prototype' } };
|
|
const target = path.join(projectsRoot, project.id);
|
|
|
|
expect(existsSync(target)).toBe(false); // brand-new project: no folder yet
|
|
await ensureReferencedProjectDir(projectsRoot, project, ensureProject);
|
|
expect(existsSync(target)).toBe(true); // now referencing resolves to a real dir
|
|
});
|
|
|
|
it('materializes a managed project folder even with no metadata', async () => {
|
|
const projectsRoot = makeProjectsRoot();
|
|
const project = { id: 'reference-bare' };
|
|
await ensureReferencedProjectDir(projectsRoot, project, ensureProject);
|
|
expect(existsSync(path.join(projectsRoot, project.id))).toBe(true);
|
|
});
|
|
|
|
it('never creates a folder for an imported/external project (absolute baseDir)', async () => {
|
|
const projectsRoot = makeProjectsRoot();
|
|
const externalBase = makeProjectsRoot(); // stand-in for the user's own folder
|
|
const project = { id: 'reference-imported', metadata: { kind: 'prototype', baseDir: externalBase } };
|
|
|
|
await ensureReferencedProjectDir(projectsRoot, project, ensureProject);
|
|
// The managed path under projectsRoot must not be conjured up for an
|
|
// external project — its files live in the user's own baseDir.
|
|
expect(existsSync(path.join(projectsRoot, project.id))).toBe(false);
|
|
});
|
|
|
|
it('propagates managed folder materialization failures', async () => {
|
|
const projectsRoot = makeProjectsRoot();
|
|
const project = { id: 'reference-managed', metadata: { kind: 'prototype' } };
|
|
const err = new Error('disk full');
|
|
|
|
await expect(
|
|
ensureReferencedProjectDir(projectsRoot, project, async () => {
|
|
throw err;
|
|
}),
|
|
).rejects.toThrow('disk full');
|
|
});
|
|
});
|