@renseiai/agentfactory 0.8.2 → 0.8.4
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/dist/src/config/index.d.ts +2 -2
- package/dist/src/config/index.d.ts.map +1 -1
- package/dist/src/config/index.js +1 -1
- package/dist/src/config/repository-config.d.ts +92 -1
- package/dist/src/config/repository-config.d.ts.map +1 -1
- package/dist/src/config/repository-config.js +80 -2
- package/dist/src/config/repository-config.test.js +303 -2
- package/dist/src/orchestrator/detect-work-type.test.d.ts +2 -0
- package/dist/src/orchestrator/detect-work-type.test.d.ts.map +1 -0
- package/dist/src/orchestrator/detect-work-type.test.js +62 -0
- package/dist/src/orchestrator/heartbeat-writer.test.d.ts +2 -0
- package/dist/src/orchestrator/heartbeat-writer.test.d.ts.map +1 -0
- package/dist/src/orchestrator/heartbeat-writer.test.js +139 -0
- package/dist/src/orchestrator/orchestrator-utils.test.d.ts +2 -0
- package/dist/src/orchestrator/orchestrator-utils.test.d.ts.map +1 -0
- package/dist/src/orchestrator/orchestrator-utils.test.js +41 -0
- package/dist/src/orchestrator/orchestrator.d.ts +26 -0
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.js +111 -51
- package/dist/src/orchestrator/state-recovery.test.d.ts +2 -0
- package/dist/src/orchestrator/state-recovery.test.d.ts.map +1 -0
- package/dist/src/orchestrator/state-recovery.test.js +425 -0
- package/dist/src/orchestrator/types.d.ts +11 -1
- package/dist/src/orchestrator/types.d.ts.map +1 -1
- package/dist/src/providers/claude-provider.d.ts.map +1 -1
- package/dist/src/providers/claude-provider.js +6 -0
- package/dist/src/providers/index.d.ts +71 -15
- package/dist/src/providers/index.d.ts.map +1 -1
- package/dist/src/providers/index.js +156 -28
- package/dist/src/providers/index.test.d.ts +2 -0
- package/dist/src/providers/index.test.d.ts.map +1 -0
- package/dist/src/providers/index.test.js +225 -0
- package/package.json +3 -3
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { loadRepositoryConfig, RepositoryConfigSchema, getEffectiveAllowedProjects } from './repository-config.js';
|
|
2
|
-
export type { RepositoryConfig } from './repository-config.js';
|
|
1
|
+
export { loadRepositoryConfig, RepositoryConfigSchema, ProjectConfigSchema, ProvidersConfigSchema, getEffectiveAllowedProjects, getProjectConfig, getProjectPath, getProvidersConfig } from './repository-config.js';
|
|
2
|
+
export type { RepositoryConfig, ProjectConfig } from './repository-config.js';
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,2BAA2B,EAAE,gBAAgB,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AACpN,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA"}
|
package/dist/src/config/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { loadRepositoryConfig, RepositoryConfigSchema, getEffectiveAllowedProjects } from './repository-config.js';
|
|
1
|
+
export { loadRepositoryConfig, RepositoryConfigSchema, ProjectConfigSchema, ProvidersConfigSchema, getEffectiveAllowedProjects, getProjectConfig, getProjectPath, getProvidersConfig } from './repository-config.js';
|
|
@@ -7,12 +7,64 @@
|
|
|
7
7
|
* - Project allowlisting for the orchestrator (allowedProjects field)
|
|
8
8
|
*/
|
|
9
9
|
import { z } from 'zod';
|
|
10
|
+
import type { ProvidersConfig } from '../providers/index.js';
|
|
11
|
+
/** Per-project configuration (object form of projectPaths values) */
|
|
12
|
+
export declare const ProjectConfigSchema: z.ZodObject<{
|
|
13
|
+
path: z.ZodString;
|
|
14
|
+
packageManager: z.ZodOptional<z.ZodEnum<{
|
|
15
|
+
pnpm: "pnpm";
|
|
16
|
+
npm: "npm";
|
|
17
|
+
yarn: "yarn";
|
|
18
|
+
bun: "bun";
|
|
19
|
+
none: "none";
|
|
20
|
+
}>>;
|
|
21
|
+
buildCommand: z.ZodOptional<z.ZodString>;
|
|
22
|
+
testCommand: z.ZodOptional<z.ZodString>;
|
|
23
|
+
validateCommand: z.ZodOptional<z.ZodString>;
|
|
24
|
+
}, z.core.$strip>;
|
|
25
|
+
export type ProjectConfig = z.infer<typeof ProjectConfigSchema>;
|
|
26
|
+
/** Provider selection configuration */
|
|
27
|
+
export declare const ProvidersConfigSchema: z.ZodObject<{
|
|
28
|
+
default: z.ZodOptional<z.ZodEnum<{
|
|
29
|
+
claude: "claude";
|
|
30
|
+
codex: "codex";
|
|
31
|
+
amp: "amp";
|
|
32
|
+
"spring-ai": "spring-ai";
|
|
33
|
+
a2a: "a2a";
|
|
34
|
+
}>>;
|
|
35
|
+
byWorkType: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
36
|
+
claude: "claude";
|
|
37
|
+
codex: "codex";
|
|
38
|
+
amp: "amp";
|
|
39
|
+
"spring-ai": "spring-ai";
|
|
40
|
+
a2a: "a2a";
|
|
41
|
+
}>>>;
|
|
42
|
+
byProject: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
43
|
+
claude: "claude";
|
|
44
|
+
codex: "codex";
|
|
45
|
+
amp: "amp";
|
|
46
|
+
"spring-ai": "spring-ai";
|
|
47
|
+
a2a: "a2a";
|
|
48
|
+
}>>>;
|
|
49
|
+
}, z.core.$strip>;
|
|
10
50
|
export declare const RepositoryConfigSchema: z.ZodObject<{
|
|
11
51
|
apiVersion: z.ZodString;
|
|
12
52
|
kind: z.ZodLiteral<"RepositoryConfig">;
|
|
13
53
|
repository: z.ZodOptional<z.ZodString>;
|
|
14
54
|
allowedProjects: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
15
|
-
projectPaths: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString
|
|
55
|
+
projectPaths: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
56
|
+
path: z.ZodString;
|
|
57
|
+
packageManager: z.ZodOptional<z.ZodEnum<{
|
|
58
|
+
pnpm: "pnpm";
|
|
59
|
+
npm: "npm";
|
|
60
|
+
yarn: "yarn";
|
|
61
|
+
bun: "bun";
|
|
62
|
+
none: "none";
|
|
63
|
+
}>>;
|
|
64
|
+
buildCommand: z.ZodOptional<z.ZodString>;
|
|
65
|
+
testCommand: z.ZodOptional<z.ZodString>;
|
|
66
|
+
validateCommand: z.ZodOptional<z.ZodString>;
|
|
67
|
+
}, z.core.$strip>]>>>;
|
|
16
68
|
sharedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
17
69
|
linearCli: z.ZodOptional<z.ZodString>;
|
|
18
70
|
packageManager: z.ZodOptional<z.ZodEnum<{
|
|
@@ -25,6 +77,29 @@ export declare const RepositoryConfigSchema: z.ZodObject<{
|
|
|
25
77
|
buildCommand: z.ZodOptional<z.ZodString>;
|
|
26
78
|
testCommand: z.ZodOptional<z.ZodString>;
|
|
27
79
|
validateCommand: z.ZodOptional<z.ZodString>;
|
|
80
|
+
providers: z.ZodOptional<z.ZodObject<{
|
|
81
|
+
default: z.ZodOptional<z.ZodEnum<{
|
|
82
|
+
claude: "claude";
|
|
83
|
+
codex: "codex";
|
|
84
|
+
amp: "amp";
|
|
85
|
+
"spring-ai": "spring-ai";
|
|
86
|
+
a2a: "a2a";
|
|
87
|
+
}>>;
|
|
88
|
+
byWorkType: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
89
|
+
claude: "claude";
|
|
90
|
+
codex: "codex";
|
|
91
|
+
amp: "amp";
|
|
92
|
+
"spring-ai": "spring-ai";
|
|
93
|
+
a2a: "a2a";
|
|
94
|
+
}>>>;
|
|
95
|
+
byProject: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
96
|
+
claude: "claude";
|
|
97
|
+
codex: "codex";
|
|
98
|
+
amp: "amp";
|
|
99
|
+
"spring-ai": "spring-ai";
|
|
100
|
+
a2a: "a2a";
|
|
101
|
+
}>>>;
|
|
102
|
+
}, z.core.$strip>>;
|
|
28
103
|
}, z.core.$strip>;
|
|
29
104
|
export type RepositoryConfig = z.infer<typeof RepositoryConfigSchema>;
|
|
30
105
|
/**
|
|
@@ -33,6 +108,22 @@ export type RepositoryConfig = z.infer<typeof RepositoryConfigSchema>;
|
|
|
33
108
|
* Otherwise falls back to `allowedProjects`.
|
|
34
109
|
*/
|
|
35
110
|
export declare function getEffectiveAllowedProjects(config: RepositoryConfig): string[] | undefined;
|
|
111
|
+
/**
|
|
112
|
+
* Returns the normalized ProjectConfig for a given project name.
|
|
113
|
+
* Handles both string shorthand and object form of projectPaths values.
|
|
114
|
+
* Per-project overrides take precedence over repo-wide defaults.
|
|
115
|
+
*/
|
|
116
|
+
export declare function getProjectConfig(config: RepositoryConfig, projectName: string): ProjectConfig | null;
|
|
117
|
+
/**
|
|
118
|
+
* Returns just the path string for a given project name.
|
|
119
|
+
* Handles both string shorthand and object form.
|
|
120
|
+
*/
|
|
121
|
+
export declare function getProjectPath(config: RepositoryConfig, projectName: string): string | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* Returns the providers config from a RepositoryConfig, if present.
|
|
124
|
+
* Convenience helper for passing to ProviderResolutionContext.configProviders.
|
|
125
|
+
*/
|
|
126
|
+
export declare function getProvidersConfig(config: RepositoryConfig): ProvidersConfig | undefined;
|
|
36
127
|
/**
|
|
37
128
|
* Load and validate .agentfactory/config.yaml from the given git root.
|
|
38
129
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repository-config.d.ts","sourceRoot":"","sources":["../../../src/config/repository-config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;
|
|
1
|
+
{"version":3,"file":"repository-config.d.ts","sourceRoot":"","sources":["../../../src/config/repository-config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAM5D,qEAAqE;AACrE,eAAO,MAAM,mBAAmB;;;;;;;;;;;;iBAW9B,CAAA;AAEF,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAW/D,uCAAuC;AACvC,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;iBAOhC,CAAA;AAEF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmDlC,CAAA;AAMD,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA;AAMrE;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,EAAE,GAAG,SAAS,CAK1F;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAepG;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKhG;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,gBAAgB,GAAG,eAAe,GAAG,SAAS,CAExF;AAMD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAQ7E"}
|
|
@@ -13,13 +13,46 @@ import YAML from 'yaml';
|
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
// Zod Schema
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
|
+
/** Per-project configuration (object form of projectPaths values) */
|
|
17
|
+
export const ProjectConfigSchema = z.object({
|
|
18
|
+
/** Root directory for this project within the repo */
|
|
19
|
+
path: z.string(),
|
|
20
|
+
/** Package manager override for this project */
|
|
21
|
+
packageManager: z.enum(['pnpm', 'npm', 'yarn', 'bun', 'none']).optional(),
|
|
22
|
+
/** Build command override for this project */
|
|
23
|
+
buildCommand: z.string().optional(),
|
|
24
|
+
/** Test command override for this project */
|
|
25
|
+
testCommand: z.string().optional(),
|
|
26
|
+
/** Validation command override for this project */
|
|
27
|
+
validateCommand: z.string().optional(),
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* projectPaths values can be a string (path shorthand) or a full ProjectConfig object.
|
|
31
|
+
* String values are normalized to { path: value } by getProjectConfig().
|
|
32
|
+
*/
|
|
33
|
+
const ProjectPathValueSchema = z.union([z.string(), ProjectConfigSchema]);
|
|
34
|
+
/** Valid agent provider names */
|
|
35
|
+
const AgentProviderNameSchema = z.enum(['claude', 'codex', 'amp', 'spring-ai', 'a2a']);
|
|
36
|
+
/** Provider selection configuration */
|
|
37
|
+
export const ProvidersConfigSchema = z.object({
|
|
38
|
+
/** Default provider for all agents */
|
|
39
|
+
default: AgentProviderNameSchema.optional(),
|
|
40
|
+
/** Provider overrides by work type (e.g., { qa: 'codex' }) */
|
|
41
|
+
byWorkType: z.record(z.string(), AgentProviderNameSchema).optional(),
|
|
42
|
+
/** Provider overrides by project name (e.g., { Social: 'codex' }) */
|
|
43
|
+
byProject: z.record(z.string(), AgentProviderNameSchema).optional(),
|
|
44
|
+
});
|
|
16
45
|
export const RepositoryConfigSchema = z.object({
|
|
17
46
|
apiVersion: z.string(),
|
|
18
47
|
kind: z.literal('RepositoryConfig'),
|
|
19
48
|
repository: z.string().optional(),
|
|
20
49
|
allowedProjects: z.array(z.string()).optional(),
|
|
21
|
-
/**
|
|
22
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Maps Linear project names to their root directory or full config.
|
|
52
|
+
* String shorthand: { Family: 'apps/family' }
|
|
53
|
+
* Object form: { 'Family iOS': { path: 'apps/family-ios', packageManager: 'none', buildCommand: 'make build' } }
|
|
54
|
+
*/
|
|
55
|
+
projectPaths: z.record(z.string(), ProjectPathValueSchema).optional(),
|
|
23
56
|
/** Shared directories that any project's agent may modify (e.g., ['packages/ui']) */
|
|
24
57
|
sharedPaths: z.array(z.string()).optional(),
|
|
25
58
|
/**
|
|
@@ -52,6 +85,11 @@ export const RepositoryConfigSchema = z.object({
|
|
|
52
85
|
* Injected into workflow templates as {{validateCommand}}.
|
|
53
86
|
*/
|
|
54
87
|
validateCommand: z.string().optional(),
|
|
88
|
+
/**
|
|
89
|
+
* Provider selection configuration.
|
|
90
|
+
* Allows routing agents to different providers by work type or project.
|
|
91
|
+
*/
|
|
92
|
+
providers: ProvidersConfigSchema.optional(),
|
|
55
93
|
}).refine((data) => !(data.allowedProjects && data.projectPaths), { message: 'allowedProjects and projectPaths are mutually exclusive — use one or the other' });
|
|
56
94
|
// ---------------------------------------------------------------------------
|
|
57
95
|
// Helpers
|
|
@@ -67,6 +105,46 @@ export function getEffectiveAllowedProjects(config) {
|
|
|
67
105
|
}
|
|
68
106
|
return config.allowedProjects;
|
|
69
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Returns the normalized ProjectConfig for a given project name.
|
|
110
|
+
* Handles both string shorthand and object form of projectPaths values.
|
|
111
|
+
* Per-project overrides take precedence over repo-wide defaults.
|
|
112
|
+
*/
|
|
113
|
+
export function getProjectConfig(config, projectName) {
|
|
114
|
+
if (!config.projectPaths)
|
|
115
|
+
return null;
|
|
116
|
+
const value = config.projectPaths[projectName];
|
|
117
|
+
if (value === undefined)
|
|
118
|
+
return null;
|
|
119
|
+
// Normalize string shorthand to object form
|
|
120
|
+
const projectConfig = typeof value === 'string' ? { path: value } : value;
|
|
121
|
+
return {
|
|
122
|
+
path: projectConfig.path,
|
|
123
|
+
packageManager: projectConfig.packageManager ?? config.packageManager,
|
|
124
|
+
buildCommand: projectConfig.buildCommand ?? config.buildCommand,
|
|
125
|
+
testCommand: projectConfig.testCommand ?? config.testCommand,
|
|
126
|
+
validateCommand: projectConfig.validateCommand ?? config.validateCommand,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Returns just the path string for a given project name.
|
|
131
|
+
* Handles both string shorthand and object form.
|
|
132
|
+
*/
|
|
133
|
+
export function getProjectPath(config, projectName) {
|
|
134
|
+
if (!config.projectPaths)
|
|
135
|
+
return undefined;
|
|
136
|
+
const value = config.projectPaths[projectName];
|
|
137
|
+
if (value === undefined)
|
|
138
|
+
return undefined;
|
|
139
|
+
return typeof value === 'string' ? value : value.path;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Returns the providers config from a RepositoryConfig, if present.
|
|
143
|
+
* Convenience helper for passing to ProviderResolutionContext.configProviders.
|
|
144
|
+
*/
|
|
145
|
+
export function getProvidersConfig(config) {
|
|
146
|
+
return config.providers;
|
|
147
|
+
}
|
|
70
148
|
// ---------------------------------------------------------------------------
|
|
71
149
|
// Loader
|
|
72
150
|
// ---------------------------------------------------------------------------
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { existsSync, readFileSync } from 'fs';
|
|
3
|
-
import { loadRepositoryConfig, RepositoryConfigSchema, getEffectiveAllowedProjects } from './repository-config.js';
|
|
3
|
+
import { loadRepositoryConfig, RepositoryConfigSchema, getEffectiveAllowedProjects, getProjectConfig, getProjectPath, getProvidersConfig, ProvidersConfigSchema } from './repository-config.js';
|
|
4
4
|
vi.mock('fs', () => ({
|
|
5
5
|
existsSync: vi.fn(),
|
|
6
6
|
readFileSync: vi.fn(),
|
|
@@ -84,6 +84,38 @@ sharedPaths:
|
|
|
84
84
|
sharedPaths: ['packages/ui', 'packages/lexical'],
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
|
+
it('parses valid YAML with mixed string and object projectPaths', () => {
|
|
88
|
+
mockExistsSync.mockReturnValue(true);
|
|
89
|
+
mockReadFileSync.mockReturnValue(`apiVersion: v1
|
|
90
|
+
kind: RepositoryConfig
|
|
91
|
+
repository: github.com/org/monorepo
|
|
92
|
+
projectPaths:
|
|
93
|
+
Social: apps/social
|
|
94
|
+
Family iOS:
|
|
95
|
+
path: apps/family-ios
|
|
96
|
+
packageManager: none
|
|
97
|
+
buildCommand: "make build"
|
|
98
|
+
testCommand: "make test"
|
|
99
|
+
sharedPaths:
|
|
100
|
+
- packages/ui
|
|
101
|
+
`);
|
|
102
|
+
const result = loadRepositoryConfig('/some/repo');
|
|
103
|
+
expect(result).toEqual({
|
|
104
|
+
apiVersion: 'v1',
|
|
105
|
+
kind: 'RepositoryConfig',
|
|
106
|
+
repository: 'github.com/org/monorepo',
|
|
107
|
+
projectPaths: {
|
|
108
|
+
Social: 'apps/social',
|
|
109
|
+
'Family iOS': {
|
|
110
|
+
path: 'apps/family-ios',
|
|
111
|
+
packageManager: 'none',
|
|
112
|
+
buildCommand: 'make build',
|
|
113
|
+
testCommand: 'make test',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
sharedPaths: ['packages/ui'],
|
|
117
|
+
});
|
|
118
|
+
});
|
|
87
119
|
it('throws on invalid schema — allowedProjects is not an array', () => {
|
|
88
120
|
mockExistsSync.mockReturnValue(true);
|
|
89
121
|
mockReadFileSync.mockReturnValue(`apiVersion: v1
|
|
@@ -122,7 +154,7 @@ describe('RepositoryConfigSchema', () => {
|
|
|
122
154
|
kind: 'InvalidKind',
|
|
123
155
|
})).toThrow();
|
|
124
156
|
});
|
|
125
|
-
it('validates config with projectPaths only', () => {
|
|
157
|
+
it('validates config with projectPaths only (string shorthand)', () => {
|
|
126
158
|
const result = RepositoryConfigSchema.parse({
|
|
127
159
|
apiVersion: 'v1',
|
|
128
160
|
kind: 'RepositoryConfig',
|
|
@@ -135,6 +167,44 @@ describe('RepositoryConfigSchema', () => {
|
|
|
135
167
|
expect(result.projectPaths).toEqual({ Social: 'apps/social', Family: 'apps/family' });
|
|
136
168
|
expect(result.allowedProjects).toBeUndefined();
|
|
137
169
|
});
|
|
170
|
+
it('validates config with projectPaths object form', () => {
|
|
171
|
+
const result = RepositoryConfigSchema.parse({
|
|
172
|
+
apiVersion: 'v1',
|
|
173
|
+
kind: 'RepositoryConfig',
|
|
174
|
+
projectPaths: {
|
|
175
|
+
Social: 'apps/social',
|
|
176
|
+
'Family iOS': {
|
|
177
|
+
path: 'apps/family-ios',
|
|
178
|
+
packageManager: 'none',
|
|
179
|
+
buildCommand: 'make build',
|
|
180
|
+
testCommand: 'make test',
|
|
181
|
+
validateCommand: 'make build',
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
expect(result.projectPaths).toEqual({
|
|
186
|
+
Social: 'apps/social',
|
|
187
|
+
'Family iOS': {
|
|
188
|
+
path: 'apps/family-ios',
|
|
189
|
+
packageManager: 'none',
|
|
190
|
+
buildCommand: 'make build',
|
|
191
|
+
testCommand: 'make test',
|
|
192
|
+
validateCommand: 'make build',
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
it('validates config with mixed string and object projectPaths', () => {
|
|
197
|
+
const result = RepositoryConfigSchema.parse({
|
|
198
|
+
apiVersion: 'v1',
|
|
199
|
+
kind: 'RepositoryConfig',
|
|
200
|
+
projectPaths: {
|
|
201
|
+
Social: 'apps/social',
|
|
202
|
+
Family: 'apps/family',
|
|
203
|
+
'Family iOS': { path: 'apps/family-ios', packageManager: 'none' },
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
expect(Object.keys(result.projectPaths)).toEqual(['Social', 'Family', 'Family iOS']);
|
|
207
|
+
});
|
|
138
208
|
it('validates config with projectPaths and sharedPaths', () => {
|
|
139
209
|
const result = RepositoryConfigSchema.parse({
|
|
140
210
|
apiVersion: 'v1',
|
|
@@ -246,4 +316,235 @@ describe('getEffectiveAllowedProjects', () => {
|
|
|
246
316
|
});
|
|
247
317
|
expect(getEffectiveAllowedProjects(config)).toBeUndefined();
|
|
248
318
|
});
|
|
319
|
+
it('returns keys from mixed string/object projectPaths', () => {
|
|
320
|
+
const config = RepositoryConfigSchema.parse({
|
|
321
|
+
apiVersion: 'v1',
|
|
322
|
+
kind: 'RepositoryConfig',
|
|
323
|
+
projectPaths: {
|
|
324
|
+
Social: 'apps/social',
|
|
325
|
+
'Family iOS': { path: 'apps/family-ios', packageManager: 'none' },
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
expect(getEffectiveAllowedProjects(config)).toEqual(['Social', 'Family iOS']);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
describe('getProjectConfig', () => {
|
|
332
|
+
it('returns null when projectPaths is not set', () => {
|
|
333
|
+
const config = RepositoryConfigSchema.parse({
|
|
334
|
+
apiVersion: 'v1',
|
|
335
|
+
kind: 'RepositoryConfig',
|
|
336
|
+
});
|
|
337
|
+
expect(getProjectConfig(config, 'Social')).toBeNull();
|
|
338
|
+
});
|
|
339
|
+
it('returns null for unknown project', () => {
|
|
340
|
+
const config = RepositoryConfigSchema.parse({
|
|
341
|
+
apiVersion: 'v1',
|
|
342
|
+
kind: 'RepositoryConfig',
|
|
343
|
+
projectPaths: { Social: 'apps/social' },
|
|
344
|
+
});
|
|
345
|
+
expect(getProjectConfig(config, 'Unknown')).toBeNull();
|
|
346
|
+
});
|
|
347
|
+
it('normalizes string shorthand to ProjectConfig', () => {
|
|
348
|
+
const config = RepositoryConfigSchema.parse({
|
|
349
|
+
apiVersion: 'v1',
|
|
350
|
+
kind: 'RepositoryConfig',
|
|
351
|
+
projectPaths: { Social: 'apps/social' },
|
|
352
|
+
});
|
|
353
|
+
const result = getProjectConfig(config, 'Social');
|
|
354
|
+
expect(result).toEqual({
|
|
355
|
+
path: 'apps/social',
|
|
356
|
+
packageManager: undefined,
|
|
357
|
+
buildCommand: undefined,
|
|
358
|
+
testCommand: undefined,
|
|
359
|
+
validateCommand: undefined,
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
it('returns per-project overrides from object form', () => {
|
|
363
|
+
const config = RepositoryConfigSchema.parse({
|
|
364
|
+
apiVersion: 'v1',
|
|
365
|
+
kind: 'RepositoryConfig',
|
|
366
|
+
projectPaths: {
|
|
367
|
+
'Family iOS': {
|
|
368
|
+
path: 'apps/family-ios',
|
|
369
|
+
packageManager: 'none',
|
|
370
|
+
buildCommand: 'make build',
|
|
371
|
+
testCommand: 'make test',
|
|
372
|
+
validateCommand: 'make build',
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
const result = getProjectConfig(config, 'Family iOS');
|
|
377
|
+
expect(result).toEqual({
|
|
378
|
+
path: 'apps/family-ios',
|
|
379
|
+
packageManager: 'none',
|
|
380
|
+
buildCommand: 'make build',
|
|
381
|
+
testCommand: 'make test',
|
|
382
|
+
validateCommand: 'make build',
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
it('falls back to repo-wide defaults when per-project overrides are not set', () => {
|
|
386
|
+
const config = RepositoryConfigSchema.parse({
|
|
387
|
+
apiVersion: 'v1',
|
|
388
|
+
kind: 'RepositoryConfig',
|
|
389
|
+
buildCommand: 'pnpm build',
|
|
390
|
+
testCommand: 'pnpm test',
|
|
391
|
+
projectPaths: { Social: 'apps/social' },
|
|
392
|
+
});
|
|
393
|
+
const result = getProjectConfig(config, 'Social');
|
|
394
|
+
expect(result).toEqual({
|
|
395
|
+
path: 'apps/social',
|
|
396
|
+
packageManager: undefined,
|
|
397
|
+
buildCommand: 'pnpm build',
|
|
398
|
+
testCommand: 'pnpm test',
|
|
399
|
+
validateCommand: undefined,
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
it('per-project overrides take precedence over repo-wide defaults', () => {
|
|
403
|
+
const config = RepositoryConfigSchema.parse({
|
|
404
|
+
apiVersion: 'v1',
|
|
405
|
+
kind: 'RepositoryConfig',
|
|
406
|
+
buildCommand: 'pnpm build',
|
|
407
|
+
testCommand: 'pnpm test',
|
|
408
|
+
packageManager: 'pnpm',
|
|
409
|
+
projectPaths: {
|
|
410
|
+
'Family iOS': {
|
|
411
|
+
path: 'apps/family-ios',
|
|
412
|
+
packageManager: 'none',
|
|
413
|
+
buildCommand: 'make build',
|
|
414
|
+
testCommand: 'make test',
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
const result = getProjectConfig(config, 'Family iOS');
|
|
419
|
+
expect(result).toEqual({
|
|
420
|
+
path: 'apps/family-ios',
|
|
421
|
+
packageManager: 'none',
|
|
422
|
+
buildCommand: 'make build',
|
|
423
|
+
testCommand: 'make test',
|
|
424
|
+
validateCommand: undefined,
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
describe('getProjectPath', () => {
|
|
429
|
+
it('returns path from string shorthand', () => {
|
|
430
|
+
const config = RepositoryConfigSchema.parse({
|
|
431
|
+
apiVersion: 'v1',
|
|
432
|
+
kind: 'RepositoryConfig',
|
|
433
|
+
projectPaths: { Social: 'apps/social' },
|
|
434
|
+
});
|
|
435
|
+
expect(getProjectPath(config, 'Social')).toBe('apps/social');
|
|
436
|
+
});
|
|
437
|
+
it('returns path from object form', () => {
|
|
438
|
+
const config = RepositoryConfigSchema.parse({
|
|
439
|
+
apiVersion: 'v1',
|
|
440
|
+
kind: 'RepositoryConfig',
|
|
441
|
+
projectPaths: { 'Family iOS': { path: 'apps/family-ios' } },
|
|
442
|
+
});
|
|
443
|
+
expect(getProjectPath(config, 'Family iOS')).toBe('apps/family-ios');
|
|
444
|
+
});
|
|
445
|
+
it('returns undefined for unknown project', () => {
|
|
446
|
+
const config = RepositoryConfigSchema.parse({
|
|
447
|
+
apiVersion: 'v1',
|
|
448
|
+
kind: 'RepositoryConfig',
|
|
449
|
+
projectPaths: { Social: 'apps/social' },
|
|
450
|
+
});
|
|
451
|
+
expect(getProjectPath(config, 'Unknown')).toBeUndefined();
|
|
452
|
+
});
|
|
453
|
+
it('returns undefined when projectPaths is not set', () => {
|
|
454
|
+
const config = RepositoryConfigSchema.parse({
|
|
455
|
+
apiVersion: 'v1',
|
|
456
|
+
kind: 'RepositoryConfig',
|
|
457
|
+
});
|
|
458
|
+
expect(getProjectPath(config, 'Social')).toBeUndefined();
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
describe('ProvidersConfigSchema', () => {
|
|
462
|
+
it('validates a complete providers config', () => {
|
|
463
|
+
const result = ProvidersConfigSchema.parse({
|
|
464
|
+
default: 'codex',
|
|
465
|
+
byWorkType: { qa: 'amp', development: 'claude' },
|
|
466
|
+
byProject: { Social: 'codex' },
|
|
467
|
+
});
|
|
468
|
+
expect(result.default).toBe('codex');
|
|
469
|
+
expect(result.byWorkType).toEqual({ qa: 'amp', development: 'claude' });
|
|
470
|
+
expect(result.byProject).toEqual({ Social: 'codex' });
|
|
471
|
+
});
|
|
472
|
+
it('validates an empty object', () => {
|
|
473
|
+
const result = ProvidersConfigSchema.parse({});
|
|
474
|
+
expect(result.default).toBeUndefined();
|
|
475
|
+
expect(result.byWorkType).toBeUndefined();
|
|
476
|
+
expect(result.byProject).toBeUndefined();
|
|
477
|
+
});
|
|
478
|
+
it('rejects invalid provider names', () => {
|
|
479
|
+
expect(() => ProvidersConfigSchema.parse({ default: 'invalid' })).toThrow();
|
|
480
|
+
});
|
|
481
|
+
it('rejects invalid provider names in byWorkType', () => {
|
|
482
|
+
expect(() => ProvidersConfigSchema.parse({ byWorkType: { qa: 'invalid' } })).toThrow();
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
describe('RepositoryConfigSchema with providers', () => {
|
|
486
|
+
it('validates config with providers field', () => {
|
|
487
|
+
const result = RepositoryConfigSchema.parse({
|
|
488
|
+
apiVersion: 'v1',
|
|
489
|
+
kind: 'RepositoryConfig',
|
|
490
|
+
providers: {
|
|
491
|
+
default: 'codex',
|
|
492
|
+
byWorkType: { qa: 'amp' },
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
expect(result.providers).toEqual({
|
|
496
|
+
default: 'codex',
|
|
497
|
+
byWorkType: { qa: 'amp' },
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
it('validates config without providers field', () => {
|
|
501
|
+
const result = RepositoryConfigSchema.parse({
|
|
502
|
+
apiVersion: 'v1',
|
|
503
|
+
kind: 'RepositoryConfig',
|
|
504
|
+
});
|
|
505
|
+
expect(result.providers).toBeUndefined();
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
describe('loadRepositoryConfig with providers', () => {
|
|
509
|
+
beforeEach(() => {
|
|
510
|
+
vi.clearAllMocks();
|
|
511
|
+
});
|
|
512
|
+
afterEach(() => {
|
|
513
|
+
vi.restoreAllMocks();
|
|
514
|
+
});
|
|
515
|
+
it('parses config with providers section', () => {
|
|
516
|
+
mockExistsSync.mockReturnValue(true);
|
|
517
|
+
mockReadFileSync.mockReturnValue(`apiVersion: v1
|
|
518
|
+
kind: RepositoryConfig
|
|
519
|
+
providers:
|
|
520
|
+
default: codex
|
|
521
|
+
byWorkType:
|
|
522
|
+
qa: amp
|
|
523
|
+
byProject:
|
|
524
|
+
Social: claude
|
|
525
|
+
`);
|
|
526
|
+
const result = loadRepositoryConfig('/some/repo');
|
|
527
|
+
expect(result?.providers).toEqual({
|
|
528
|
+
default: 'codex',
|
|
529
|
+
byWorkType: { qa: 'amp' },
|
|
530
|
+
byProject: { Social: 'claude' },
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
describe('getProvidersConfig', () => {
|
|
535
|
+
it('returns providers config when present', () => {
|
|
536
|
+
const config = RepositoryConfigSchema.parse({
|
|
537
|
+
apiVersion: 'v1',
|
|
538
|
+
kind: 'RepositoryConfig',
|
|
539
|
+
providers: { default: 'codex' },
|
|
540
|
+
});
|
|
541
|
+
expect(getProvidersConfig(config)).toEqual({ default: 'codex' });
|
|
542
|
+
});
|
|
543
|
+
it('returns undefined when providers not set', () => {
|
|
544
|
+
const config = RepositoryConfigSchema.parse({
|
|
545
|
+
apiVersion: 'v1',
|
|
546
|
+
kind: 'RepositoryConfig',
|
|
547
|
+
});
|
|
548
|
+
expect(getProvidersConfig(config)).toBeUndefined();
|
|
549
|
+
});
|
|
249
550
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detect-work-type.test.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/detect-work-type.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { detectWorkType } from './orchestrator.js';
|
|
3
|
+
describe('detectWorkType', () => {
|
|
4
|
+
describe('leaf issues (isParent=false)', () => {
|
|
5
|
+
it('maps Backlog status to development', () => {
|
|
6
|
+
expect(detectWorkType('Backlog', false)).toBe('development');
|
|
7
|
+
});
|
|
8
|
+
it('maps Finished status to qa', () => {
|
|
9
|
+
expect(detectWorkType('Finished', false)).toBe('qa');
|
|
10
|
+
});
|
|
11
|
+
it('maps Delivered status to acceptance', () => {
|
|
12
|
+
expect(detectWorkType('Delivered', false)).toBe('acceptance');
|
|
13
|
+
});
|
|
14
|
+
it('maps Rejected status to refinement', () => {
|
|
15
|
+
expect(detectWorkType('Rejected', false)).toBe('refinement');
|
|
16
|
+
});
|
|
17
|
+
it('maps Icebox status to research', () => {
|
|
18
|
+
expect(detectWorkType('Icebox', false)).toBe('research');
|
|
19
|
+
});
|
|
20
|
+
it('maps Started status to inflight', () => {
|
|
21
|
+
expect(detectWorkType('Started', false)).toBe('inflight');
|
|
22
|
+
});
|
|
23
|
+
it('defaults unknown status to development', () => {
|
|
24
|
+
expect(detectWorkType('SomeUnknownStatus', false)).toBe('development');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
describe('parent issues (isParent=true)', () => {
|
|
28
|
+
it('upgrades Backlog/development to coordination', () => {
|
|
29
|
+
expect(detectWorkType('Backlog', true)).toBe('coordination');
|
|
30
|
+
});
|
|
31
|
+
it('upgrades Finished/qa to qa-coordination', () => {
|
|
32
|
+
expect(detectWorkType('Finished', true)).toBe('qa-coordination');
|
|
33
|
+
});
|
|
34
|
+
it('upgrades Delivered/acceptance to acceptance-coordination', () => {
|
|
35
|
+
expect(detectWorkType('Delivered', true)).toBe('acceptance-coordination');
|
|
36
|
+
});
|
|
37
|
+
it('upgrades Rejected/refinement to refinement-coordination', () => {
|
|
38
|
+
expect(detectWorkType('Rejected', true)).toBe('refinement-coordination');
|
|
39
|
+
});
|
|
40
|
+
it('does not upgrade research (Icebox) — no coordination variant', () => {
|
|
41
|
+
expect(detectWorkType('Icebox', true)).toBe('research');
|
|
42
|
+
});
|
|
43
|
+
it('does not upgrade inflight (Started) — no coordination variant', () => {
|
|
44
|
+
expect(detectWorkType('Started', true)).toBe('inflight');
|
|
45
|
+
});
|
|
46
|
+
it('upgrades unknown status (defaults to development → coordination)', () => {
|
|
47
|
+
expect(detectWorkType('SomeUnknownStatus', true)).toBe('coordination');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('post-refinement rework scenario (SUP-1116 bug)', () => {
|
|
51
|
+
it('parent issue returning to Backlog after refinement gets coordination, not development', () => {
|
|
52
|
+
// This is the exact scenario that caused SUP-1116 to fail:
|
|
53
|
+
// After refinement-coordination completed, the parent moved to Backlog.
|
|
54
|
+
// The orchestrator's run() previously hardcoded 'development', which loaded
|
|
55
|
+
// the wrong template and the agent asked for human input instead of
|
|
56
|
+
// autonomously dispatching sub-agents.
|
|
57
|
+
const workType = detectWorkType('Backlog', true);
|
|
58
|
+
expect(workType).toBe('coordination');
|
|
59
|
+
expect(workType).not.toBe('development');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heartbeat-writer.test.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/heartbeat-writer.test.ts"],"names":[],"mappings":""}
|