Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ccf7978
Add start command: new user entry point with state detection (#387)
JustAGhosT Mar 11, 2026
03b8e36
Add configurable prefix to kits commands (#388)
JustAGhosT Mar 11, 2026
c6bf794
fix(ci): CI remediation — package manager, review findings, test stab…
JustAGhosT Mar 11, 2026
5ffc812
fix(spec): correct githubSlug to phoenixvc/agentkit-forge (#391)
JustAGhosT Mar 11, 2026
f495b75
Add entry point for new framework users (#389)
JustAGhosT Mar 12, 2026
4ee4003
fix: caldues heuristics (#398)
JustAGhosT Mar 12, 2026
bf2d162
feat: complete revisit of agents (#399) (#400)
JustAGhosT Mar 12, 2026
c4ea1f4
fix(quality): resolve all lint and format errors
JustAGhosT Mar 13, 2026
ff53bba
I'll update the last_updated field in all the files from '2026-03-12'…
JustAGhosT Mar 14, 2026
c64fde0
docs: add AgentKit Forge sync feedback
JustAGhosT Mar 14, 2026
8c3343d
docs: update CLAUDE.md with repository-specific editing guidelines
JustAGhosT Mar 15, 2026
d3e7d8e
docs(claude): allow .agentkit edits in this repo (NB for framework dev)
JustAGhosT Mar 15, 2026
3437184
chore(sync): regenerate outputs after agentkit:sync
JustAGhosT Mar 15, 2026
6f6198d
Merge main into dev
JustAGhosT Mar 15, 2026
34b67cf
Fix/generated files and conflict markers (#427)
JustAGhosT Mar 17, 2026
567330a
chore(ci): reduce CodeQL to weekly + manual only (#430)
JustAGhosT Mar 17, 2026
0e8136f
docs(architecture): add tool-neutral agent hub findings, ADR-10, and …
JustAGhosT Mar 17, 2026
0b0218f
docs(history): add Linear PhoenixVC workspace setup implementation re…
JustAGhosT Mar 17, 2026
8794fc1
feat(engine): kit-based domain selection, init wizard, stop hook perf…
JustAGhosT Mar 21, 2026
88d2418
chore(merge): merge main into dev (#433)
JustAGhosT Mar 21, 2026
36113f7
chore(sync): merge main into dev — resolve conflicts and sync outputs
JustAGhosT Mar 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: explicit-domains-test
githubSlug: test-org/explicit-domains-test
description: Explicit domain override fixture for domain filtering tests.
phase: active
stack:
languages: [typescript, csharp, rust]
packageManager: pnpm
nodeVersion: lts/*
frameworks:
frontend: []
backend: [node.js]
css: []
orm: none
database: [none]
search: none
messaging: [none]
domains:
rules: [typescript, security]
automation:
languageProfile:
mode: configured
diagnostics: off
inferFrom:
frameworks: true
tests: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: fullstack-test
githubSlug: test-org/fullstack-test
description: Full-stack fixture for domain filtering tests.
phase: active
stack:
languages: [typescript, csharp, rust, solidity]
packageManager: pnpm
nodeVersion: lts/*
frameworks:
frontend: []
backend: [node.js, asp.net-core, axum]
css: []
orm: none
database: [none]
search: none
messaging: [none]
automation:
languageProfile:
mode: configured
diagnostics: off
inferFrom:
frameworks: true
tests: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: heuristic-test
githubSlug: test-org/heuristic-test
description: Heuristic mode fixture — all domains should be included (backward compat).
phase: active
stack:
languages: [javascript]
packageManager: pnpm
nodeVersion: lts/*
frameworks:
frontend: []
backend: [node.js]
css: []
orm: none
database: [none]
search: none
messaging: [none]
automation:
languageProfile:
mode: heuristic
diagnostics: off
inferFrom:
frameworks: true
tests: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: js-only-test
githubSlug: test-org/js-only-test
description: JS-only fixture for domain filtering tests.
phase: active
stack:
languages: [javascript]
packageManager: pnpm
nodeVersion: lts/*
frameworks:
frontend: []
backend: [node.js]
css: []
orm: none
database: [none]
search: none
messaging: [none]
automation:
languageProfile:
mode: configured
diagnostics: off
inferFrom:
frameworks: true
tests: true
297 changes: 297 additions & 0 deletions .agentkit/engines/node/src/__tests__/generation.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
/**
* Phase 6 — Domain filtering tests
*
* Tests that filterDomainsByStack correctly filters rule domains based on:
* 1. js-only project (mode: configured, languages: [javascript])
* 2. fullstack project (typescript + dotnet + rust + solidity)
* 3. explicit domains.rules override (only listed domains regardless of stack)
* 4. heuristic mode (all domains — backward compat)
*/
import { readFileSync } from 'fs';
import yaml from 'js-yaml';
import { dirname, resolve } from 'path';
import { describe, expect, it } from 'vitest';
import { filterDomainsByStack, filterTechStacks } from '../template-utils.mjs';

const FIXTURES_DIR = resolve(import.meta.dirname, '__fixtures__');
const AGENTKIT_ROOT = resolve(import.meta.dirname, '..', '..', '..', '..');

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function loadFixture(name) {
return yaml.load(readFileSync(resolve(FIXTURES_DIR, name), 'utf-8'));
}

function loadRulesSpec() {
return yaml.load(readFileSync(resolve(AGENTKIT_ROOT, 'spec', 'rules.yaml'), 'utf-8'));
}

/**
* Build a minimal vars object matching what flattenProjectYaml + language inference
* produces for a given project.yaml fixture.
* Only the fields used by filterDomainsByStack are populated.
*/
function buildVarsFromFixture(project) {
const langs = (project?.stack?.languages || []).map((l) => l.trim().toLowerCase());
const mode = project?.automation?.languageProfile?.mode || 'hybrid';

const hasLanguageTypeScript = langs.some((l) => l === 'typescript' || l === 'ts');
const hasLanguageJavaScript = langs.some((l) => l === 'javascript' || l === 'js');
const hasLanguageJsLike = hasLanguageTypeScript || hasLanguageJavaScript;
const hasLanguageRust = langs.includes('rust');
const hasLanguagePython = langs.includes('python');
const hasLanguageDotnet = langs.some((l) => ['csharp', 'c#', 'dotnet', '.net'].includes(l));
const hasLanguageBlockchain = langs.some((l) => ['solidity', 'blockchain'].includes(l));

const hasConfiguredLanguages = langs.length > 0;

let hasLanguageJsLikeEffective;
let hasLanguagePythonEffective;
let hasLanguageDotnetEffective;
let hasLanguageRustEffective;

if (mode === 'configured') {
hasLanguageJsLikeEffective = hasConfiguredLanguages && hasLanguageJsLike;
hasLanguagePythonEffective = hasConfiguredLanguages && hasLanguagePython;
hasLanguageDotnetEffective = hasConfiguredLanguages && hasLanguageDotnet;
hasLanguageRustEffective = hasConfiguredLanguages && hasLanguageRust;
} else if (mode === 'heuristic') {
// heuristic: inferred from frameworks/tests (we set to false for these unit tests
// since we have no framework/test signals in fixtures — real engine would infer)
hasLanguageJsLikeEffective = hasLanguageJsLike;
hasLanguagePythonEffective = hasLanguagePython;
hasLanguageDotnetEffective = hasLanguageDotnet;
hasLanguageRustEffective = hasLanguageRust;
} else {
// hybrid: use configured if present, else inferred
hasLanguageJsLikeEffective = hasConfiguredLanguages ? hasLanguageJsLike : false;
hasLanguagePythonEffective = hasConfiguredLanguages ? hasLanguagePython : false;
hasLanguageDotnetEffective = hasConfiguredLanguages ? hasLanguageDotnet : false;
hasLanguageRustEffective = hasConfiguredLanguages ? hasLanguageRust : false;
}

return {
languageProfileMode: mode,
hasLanguageJsLikeEffective,
hasLanguagePythonEffective,
hasLanguageDotnetEffective,
hasLanguageRustEffective,
hasLanguageBlockchain,
hasInfra: false,
};
}

function domainNames(rules) {
return new Set(rules.map((r) => r.domain));
}

// Universal domains that must always be present
const UNIVERSAL_DOMAINS = [
'security',
'testing',
'git-workflow',
'documentation',
'ci-cd',
'dependency-management',
'agent-conduct',
'template-protection',
];

// ---------------------------------------------------------------------------
// Scenario 1 — JS-only project (mode: configured, languages: [javascript])
// ---------------------------------------------------------------------------
describe('filterDomainsByStack — js-only project', () => {
const project = loadFixture('js-only-project.yaml');
const rulesSpec = loadRulesSpec();
const vars = buildVarsFromFixture(project);
const filtered = filterDomainsByStack(rulesSpec.rules, vars, project);
const names = domainNames(filtered);

it('includes the typescript domain for a javascript project', () => {
expect(names.has('typescript')).toBe(true);
});

it('includes all universal domains', () => {
for (const d of UNIVERSAL_DOMAINS) {
expect(names.has(d), `expected universal domain "${d}" to be present`).toBe(true);
}
});

it('excludes dotnet — not in stack', () => {
expect(names.has('dotnet')).toBe(false);
});

it('excludes rust — not in stack', () => {
expect(names.has('rust')).toBe(false);
});

it('excludes python — not in stack', () => {
expect(names.has('python')).toBe(false);
});

it('excludes blockchain — not in stack', () => {
expect(names.has('blockchain')).toBe(false);
});
});

// ---------------------------------------------------------------------------
// Scenario 2 — Fullstack project (typescript + dotnet + rust + solidity)
// ---------------------------------------------------------------------------
describe('filterDomainsByStack — fullstack project', () => {
const project = loadFixture('fullstack-project.yaml');
const rulesSpec = loadRulesSpec();
const vars = buildVarsFromFixture(project);
const filtered = filterDomainsByStack(rulesSpec.rules, vars, project);
const names = domainNames(filtered);

it('includes typescript', () => {
expect(names.has('typescript')).toBe(true);
});

it('includes dotnet', () => {
expect(names.has('dotnet')).toBe(true);
});

it('includes rust', () => {
expect(names.has('rust')).toBe(true);
});

it('includes blockchain', () => {
expect(names.has('blockchain')).toBe(true);
});

it('includes all universal domains', () => {
for (const d of UNIVERSAL_DOMAINS) {
expect(names.has(d), `expected universal domain "${d}" to be present`).toBe(true);
}
});

it('excludes python — not in stack', () => {
expect(names.has('python')).toBe(false);
});
});

// ---------------------------------------------------------------------------
// Scenario 3 — Explicit domains.rules override (only [typescript, security])
// ---------------------------------------------------------------------------
describe('filterDomainsByStack — explicit domains.rules override', () => {
const project = loadFixture('explicit-domains-project.yaml');
const rulesSpec = loadRulesSpec();
const vars = buildVarsFromFixture(project);
const filtered = filterDomainsByStack(rulesSpec.rules, vars, project);
const names = domainNames(filtered);

it('includes typescript — in explicit list', () => {
expect(names.has('typescript')).toBe(true);
});

it('includes security — in explicit list', () => {
expect(names.has('security')).toBe(true);
});

it('excludes testing — not in explicit list even though universal', () => {
expect(names.has('testing')).toBe(false);
});

it('excludes dotnet — not in explicit list despite being in stack', () => {
expect(names.has('dotnet')).toBe(false);
});

it('excludes rust — not in explicit list despite being in stack', () => {
expect(names.has('rust')).toBe(false);
});

it('has exactly 2 domains', () => {
expect(filtered.length).toBe(2);
});
});

// ---------------------------------------------------------------------------
// Scenario 4 — Heuristic mode (backward compat: all domains included)
// ---------------------------------------------------------------------------
describe('filterDomainsByStack — heuristic mode (backward compat)', () => {
const project = loadFixture('heuristic-project.yaml');
const rulesSpec = loadRulesSpec();
const vars = buildVarsFromFixture(project);
const filtered = filterDomainsByStack(rulesSpec.rules, vars, project);
const names = domainNames(filtered);

it('returns all domains from rules.yaml (no filtering)', () => {
expect(filtered.length).toBe(rulesSpec.rules.length);
});

it('includes typescript', () => {
expect(names.has('typescript')).toBe(true);
});

it('includes rust (even though not in stack)', () => {
expect(names.has('rust')).toBe(true);
});

it('includes python (even though not in stack)', () => {
expect(names.has('python')).toBe(true);
});

it('includes dotnet (even though not in stack)', () => {
expect(names.has('dotnet')).toBe(true);
});
});

// ---------------------------------------------------------------------------
// filterTechStacks — basic coverage
// ---------------------------------------------------------------------------
describe('filterTechStacks', () => {
const stacks = [
{ name: 'node' },
{ name: 'dotnet' },
{ name: 'rust' },
{ name: 'python' },
{ name: 'custom-tool' }, // unknown — always kept
];

it('keeps only node stack for a JS-only project', () => {
const vars = {
languageProfileMode: 'configured',
hasLanguageJsLikeEffective: true,
hasLanguageDotnetEffective: false,
hasLanguageRustEffective: false,
hasLanguagePythonEffective: false,
};
const result = filterTechStacks(stacks, vars);
const names = result.map((s) => s.name);
expect(names).toContain('node');
expect(names).not.toContain('dotnet');
expect(names).not.toContain('rust');
expect(names).not.toContain('python');
expect(names).toContain('custom-tool'); // unknown always kept
});

it('keeps all stacks in heuristic mode', () => {
const vars = { languageProfileMode: 'heuristic' };
const result = filterTechStacks(stacks, vars);
expect(result.length).toBe(stacks.length);
});

it('keeps dotnet and node for a fullstack JS+dotnet project', () => {
const vars = {
languageProfileMode: 'configured',
hasLanguageJsLikeEffective: true,
hasLanguageDotnetEffective: true,
hasLanguageRustEffective: false,
hasLanguagePythonEffective: false,
};
const result = filterTechStacks(stacks, vars);
const names = result.map((s) => s.name);
expect(names).toContain('node');
expect(names).toContain('dotnet');
expect(names).not.toContain('rust');
expect(names).not.toContain('python');
});

it('returns empty array when stacks is null', () => {
const vars = { languageProfileMode: 'configured' };
expect(filterTechStacks(null, vars)).toEqual([]);
});
});
Loading
Loading