agentic-orchestrator 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/control-plane/src/core/path-layout.ts +23 -5
- package/apps/control-plane/test/path-layout.spec.ts +11 -0
- package/dist/apps/control-plane/core/path-layout.js +20 -1
- package/dist/apps/control-plane/core/path-layout.js.map +1 -1
- package/package.json +1 -1
- package/packages/web-dashboard/test/api/launch/submit.route.spec.ts +30 -0
- package/packages/web-dashboard/test/lib/launch-contracts.spec.ts +65 -0
|
@@ -1,7 +1,28 @@
|
|
|
1
|
+
import fsSync from 'node:fs';
|
|
1
2
|
import fs from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
3
5
|
import { ensureDir, pathExists } from './fs.js';
|
|
4
6
|
|
|
7
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
function resolveBundledOrchestratorRoot(startDir: string): string | null {
|
|
10
|
+
let current = path.resolve(startDir);
|
|
11
|
+
while (true) {
|
|
12
|
+
const candidate = path.join(current, 'agentic', 'orchestrator', 'schemas');
|
|
13
|
+
if (fsSync.existsSync(candidate)) {
|
|
14
|
+
return path.join(current, 'agentic', 'orchestrator');
|
|
15
|
+
}
|
|
16
|
+
const parent = path.dirname(current);
|
|
17
|
+
if (parent === current) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
current = parent;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const BUNDLED_ORCHESTRATOR_ROOT = resolveBundledOrchestratorRoot(MODULE_DIR);
|
|
25
|
+
|
|
5
26
|
export class AopPathLayout {
|
|
6
27
|
readonly repoRoot: string;
|
|
7
28
|
|
|
@@ -54,11 +75,8 @@ export class AopPathLayout {
|
|
|
54
75
|
}
|
|
55
76
|
|
|
56
77
|
organizerOrderingSchemaPath(): string {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
'schemas',
|
|
60
|
-
'organizer-ordering-artifact.schema.json',
|
|
61
|
-
);
|
|
78
|
+
const root = BUNDLED_ORCHESTRATOR_ROOT ?? this.legacyOrchestratorRoot;
|
|
79
|
+
return path.join(root, 'schemas', 'organizer-ordering-artifact.schema.json');
|
|
62
80
|
}
|
|
63
81
|
|
|
64
82
|
get legacyFeaturesRoot(): string {
|
|
@@ -13,6 +13,17 @@ async function writeMarker(filePath: string, content = 'marker'): Promise<void>
|
|
|
13
13
|
await fs.writeFile(filePath, content, 'utf8');
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
describe('AopPathLayout', () => {
|
|
17
|
+
it('resolves organizerOrderingSchemaPath from the bundled package tree', () => {
|
|
18
|
+
const layout = new AopPathLayout('/some/external/project');
|
|
19
|
+
const schemaPath = layout.organizerOrderingSchemaPath();
|
|
20
|
+
|
|
21
|
+
expect(schemaPath).toContain('organizer-ordering-artifact.schema.json');
|
|
22
|
+
// Must NOT resolve from the arbitrary repoRoot — it must find the bundled schema
|
|
23
|
+
expect(schemaPath).not.toContain('/some/external/project/');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
16
27
|
describe('ensureAopRuntimeLayout', () => {
|
|
17
28
|
it('migrates legacy features when .aop is missing', async () => {
|
|
18
29
|
const repoRoot = await makeTempDir();
|
|
@@ -1,6 +1,24 @@
|
|
|
1
|
+
import fsSync from 'node:fs';
|
|
1
2
|
import fs from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
3
5
|
import { ensureDir, pathExists } from './fs.js';
|
|
6
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
function resolveBundledOrchestratorRoot(startDir) {
|
|
8
|
+
let current = path.resolve(startDir);
|
|
9
|
+
while (true) {
|
|
10
|
+
const candidate = path.join(current, 'agentic', 'orchestrator', 'schemas');
|
|
11
|
+
if (fsSync.existsSync(candidate)) {
|
|
12
|
+
return path.join(current, 'agentic', 'orchestrator');
|
|
13
|
+
}
|
|
14
|
+
const parent = path.dirname(current);
|
|
15
|
+
if (parent === current) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
current = parent;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const BUNDLED_ORCHESTRATOR_ROOT = resolveBundledOrchestratorRoot(MODULE_DIR);
|
|
4
22
|
export class AopPathLayout {
|
|
5
23
|
repoRoot;
|
|
6
24
|
constructor(repoRoot) {
|
|
@@ -40,7 +58,8 @@ export class AopPathLayout {
|
|
|
40
58
|
return path.join(this.instanceRuntimeRoot(instanceId), 'organizer-ordering.json');
|
|
41
59
|
}
|
|
42
60
|
organizerOrderingSchemaPath() {
|
|
43
|
-
|
|
61
|
+
const root = BUNDLED_ORCHESTRATOR_ROOT ?? this.legacyOrchestratorRoot;
|
|
62
|
+
return path.join(root, 'schemas', 'organizer-ordering-artifact.schema.json');
|
|
44
63
|
}
|
|
45
64
|
get legacyFeaturesRoot() {
|
|
46
65
|
return path.join(this.repoRoot, 'agentic', 'features');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path-layout.js","sourceRoot":"","sources":["../../../../apps/control-plane/src/core/path-layout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEhD,MAAM,OAAO,aAAa;IACf,QAAQ,CAAS;IAE1B,YAAY,QAAgB;QAC1B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED,IAAI,gBAAgB;QAClB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;IACvE,CAAC;IAED,IAAI,sBAAsB;QACxB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;IAC7D,CAAC;IAED,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC1C,CAAC;IAED,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IACjD,CAAC;IAED,IAAI,eAAe;QACjB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IAChD,CAAC;IAED,IAAI,mBAAmB;QACrB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,kBAAkB,CAAC,CAAC;IAC7D,CAAC;IAED,mBAAmB,CAAC,UAAkB;QACpC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;IACrD,CAAC;IAED,gBAAgB,CAAC,UAAkB;QACjC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC,CAAC;IAC3E,CAAC;IAED,gBAAgB,CAAC,UAAkB;QACjC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,EAAE,iBAAiB,CAAC,CAAC;IAC5E,CAAC;IAED,oBAAoB,CAAC,UAAkB;QACrC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,EAAE,wBAAwB,CAAC,CAAC;IACnF,CAAC;IAED,qBAAqB,CAAC,UAAkB;QACtC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,EAAE,yBAAyB,CAAC,CAAC;IACpF,CAAC;IAED,2BAA2B;QACzB,
|
|
1
|
+
{"version":3,"file":"path-layout.js","sourceRoot":"","sources":["../../../../apps/control-plane/src/core/path-layout.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEhD,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAEhE,SAAS,8BAA8B,CAAC,QAAgB;IACtD,IAAI,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACrC,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,SAAS,CAAC,CAAC;QAC3E,IAAI,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YACjC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;QACvD,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,GAAG,MAAM,CAAC;IACnB,CAAC;AACH,CAAC;AAED,MAAM,yBAAyB,GAAG,8BAA8B,CAAC,UAAU,CAAC,CAAC;AAE7E,MAAM,OAAO,aAAa;IACf,QAAQ,CAAS;IAE1B,YAAY,QAAgB;QAC1B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED,IAAI,gBAAgB;QAClB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;IACvE,CAAC;IAED,IAAI,sBAAsB;QACxB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;IAC7D,CAAC;IAED,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC1C,CAAC;IAED,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IACjD,CAAC;IAED,IAAI,eAAe;QACjB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;IAChD,CAAC;IAED,IAAI,mBAAmB;QACrB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,kBAAkB,CAAC,CAAC;IAC7D,CAAC;IAED,mBAAmB,CAAC,UAAkB;QACpC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;IACrD,CAAC;IAED,gBAAgB,CAAC,UAAkB;QACjC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC,CAAC;IAC3E,CAAC;IAED,gBAAgB,CAAC,UAAkB;QACjC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,EAAE,iBAAiB,CAAC,CAAC;IAC5E,CAAC;IAED,oBAAoB,CAAC,UAAkB;QACrC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,EAAE,wBAAwB,CAAC,CAAC;IACnF,CAAC;IAED,qBAAqB,CAAC,UAAkB;QACtC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,UAAU,CAAC,EAAE,yBAAyB,CAAC,CAAC;IACpF,CAAC;IAED,2BAA2B;QACzB,MAAM,IAAI,GAAG,yBAAyB,IAAI,IAAI,CAAC,sBAAsB,CAAC;QACtE,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,yCAAyC,CAAC,CAAC;IAC/E,CAAC;IAED,IAAI,kBAAkB;QACpB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;IACzD,CAAC;IAED,IAAI,iBAAiB;QACnB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;IACxD,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;IACpD,CAAC;IAED,IAAI,aAAa;QACf,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,aAAa,CAAC,CAAC;IACrD,CAAC;IAED,WAAW,CAAC,SAAiB;QAC3B,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC;IACjD,CAAC;IAED,eAAe,CAAC,SAAiB;QAC/B,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,eAAe,CAAC,CAAC;IACjE,CAAC;IAED,SAAS,CAAC,SAAiB;QACzB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,UAAU,CAAC,CAAC;IAC5D,CAAC;IAED,QAAQ,CAAC,SAAiB;QACxB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,WAAW,CAAC,CAAC;IAC7D,CAAC;IAED,gBAAgB,CAAC,SAAiB;QAChC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,CAAC;IAC3D,CAAC;IAED,eAAe,CAAC,SAAiB;QAC/B,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,WAAW,CAAC,CAAC;IAC7D,CAAC;IAED,WAAW,CAAC,SAAiB;QAC3B,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,oBAAoB,CAAC,CAAC;IACtE,CAAC;IAED,aAAa,CAAC,SAAiB;QAC7B,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,cAAc,CAAC,CAAC;IAChE,CAAC;IAED,aAAa,CAAC,SAAiB;QAC7B,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,gBAAgB,CAAC,CAAC;IAClE,CAAC;IAED,QAAQ,CAAC,SAAiB;QACxB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,CAAC;IACxD,CAAC;IAED,YAAY,CAAC,SAAiB;QAC5B,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,UAAU,CAAC,CAAC;IAC5D,CAAC;IAED,QAAQ,CAAC,SAAiB;QACxB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,CAAC;IAC3D,CAAC;IAED,SAAS;QACP,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAC,CAAC;IACnD,CAAC;IAED,kBAAkB,CAAC,KAAa;QAC9B,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,GAAG,KAAK,iBAAiB,CAAC,CAAC;IAChE,CAAC;IAED,cAAc,CAAC,KAAa;QAC1B,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,GAAG,KAAK,YAAY,CAAC,CAAC;IAC3D,CAAC;IAED,cAAc,CAAC,KAAa;QAC1B,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC;IACtD,CAAC;CACF;AAED,KAAK,UAAU,qBAAqB,CAAC,UAAkB,EAAE,UAAkB;IACzE,MAAM,EAAE,CAAC,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE;QAClC,SAAS,EAAE,IAAI;QACf,KAAK,EAAE,KAAK;QACZ,YAAY,EAAE,KAAK;KACpB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,MAAqB;IAChE,MAAM,iBAAiB,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAC/D,MAAM,oBAAoB,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC;IACzE,MAAM,mBAAmB,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;IAEvE,sCAAsC;IACtC,wEAAwE;IACxE,IAAI,CAAC,iBAAiB,IAAI,CAAC,oBAAoB,IAAI,mBAAmB,CAAC,EAAE,CAAC;QACxE,MAAM,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QACpC,IAAI,oBAAoB,EAAE,CAAC;YACzB,MAAM,qBAAqB,CAAC,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;QAC9E,CAAC;QACD,IAAI,mBAAmB,EAAE,CAAC;YACxB,MAAM,qBAAqB,CAAC,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAC,eAAe,CAAC,CAAC;QAChF,CAAC;IACH,CAAC;IAED,MAAM,SAAS,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IACrC,MAAM,SAAS,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IACxC,MAAM,SAAS,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;AAC9C,CAAC"}
|
package/package.json
CHANGED
|
@@ -261,6 +261,36 @@ describe('POST /api/launch/submit', () => {
|
|
|
261
261
|
).resolves.toContain('Ship it.');
|
|
262
262
|
});
|
|
263
263
|
|
|
264
|
+
it('accepts uploaded drafts where name is stripped but source_path retains .md', async () => {
|
|
265
|
+
const response = await POST(
|
|
266
|
+
new Request('http://localhost/api/launch/submit', {
|
|
267
|
+
method: 'POST',
|
|
268
|
+
body: JSON.stringify({
|
|
269
|
+
drafts: [
|
|
270
|
+
{
|
|
271
|
+
draft_id: 'upload-stripped',
|
|
272
|
+
draft_order: 0,
|
|
273
|
+
source_type: 'upload',
|
|
274
|
+
name: 'my-feature',
|
|
275
|
+
source_path: 'my-feature.md',
|
|
276
|
+
markdown: '# My Feature\n\nDescription.',
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
}),
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
const body = (await response.json()) as {
|
|
283
|
+
ok: boolean;
|
|
284
|
+
data?: { outcomes: Array<{ draft_id: string; status: string; errors: string[] }> };
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
expect(body.ok).toBe(true);
|
|
288
|
+
expect(body.data?.outcomes[0]).toMatchObject({
|
|
289
|
+
draft_id: 'upload-stripped',
|
|
290
|
+
status: 'submitted',
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
264
294
|
it('rejects duplicate slugs before writing specs or submitting enrollment requests', async () => {
|
|
265
295
|
prepareLaunchDraftSpecsMock.mockResolvedValue([
|
|
266
296
|
{
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { validateLaunchDraftInput } from '@/lib/launch-contracts.js';
|
|
3
|
+
import type { LaunchDraftInput } from '@/lib/launch-contracts.js';
|
|
4
|
+
|
|
5
|
+
function baseDraft(overrides: Partial<LaunchDraftInput> = {}): LaunchDraftInput {
|
|
6
|
+
return {
|
|
7
|
+
draft_id: 'draft-1',
|
|
8
|
+
draft_order: 0,
|
|
9
|
+
source_type: 'paste',
|
|
10
|
+
name: 'Feature Draft',
|
|
11
|
+
markdown: '# Feature',
|
|
12
|
+
...overrides,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('validateLaunchDraftInput', () => {
|
|
17
|
+
it('returns no errors for a valid paste draft', () => {
|
|
18
|
+
expect(validateLaunchDraftInput(baseDraft())).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('returns no errors for a valid upload draft with .md extension', () => {
|
|
22
|
+
expect(validateLaunchDraftInput(baseDraft({ source_type: 'upload', name: 'spec.md' }))).toEqual(
|
|
23
|
+
[],
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('rejects uploaded drafts with unsupported extensions', () => {
|
|
28
|
+
const errors = validateLaunchDraftInput(
|
|
29
|
+
baseDraft({ source_type: 'upload', name: 'notes.txt' }),
|
|
30
|
+
);
|
|
31
|
+
expect(errors).toContain('Only .md draft uploads are supported.');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('accepts uploaded drafts when name is stripped but source_path retains .md', () => {
|
|
35
|
+
const errors = validateLaunchDraftInput(
|
|
36
|
+
baseDraft({ source_type: 'upload', name: 'my-feature', source_path: 'my-feature.md' }),
|
|
37
|
+
);
|
|
38
|
+
expect(errors).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('rejects uploaded drafts when both name and source_path lack .md extension', () => {
|
|
42
|
+
const errors = validateLaunchDraftInput(
|
|
43
|
+
baseDraft({ source_type: 'upload', name: 'my-feature', source_path: 'my-feature.txt' }),
|
|
44
|
+
);
|
|
45
|
+
expect(errors).toContain('Only .md draft uploads are supported.');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('rejects drafts with empty name', () => {
|
|
49
|
+
expect(validateLaunchDraftInput(baseDraft({ name: ' ' }))).toContain(
|
|
50
|
+
'Draft name is required.',
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('rejects drafts with empty markdown', () => {
|
|
55
|
+
expect(validateLaunchDraftInput(baseDraft({ markdown: '' }))).toContain(
|
|
56
|
+
'Draft markdown is required.',
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('rejects drafts with empty draft_id', () => {
|
|
61
|
+
expect(validateLaunchDraftInput(baseDraft({ draft_id: '' }))).toContain(
|
|
62
|
+
'draft_id is required.',
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
});
|