@assistkick/create 1.0.0
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/bin/create.d.ts +2 -0
- package/dist/bin/create.js +25 -0
- package/dist/bin/create.js.map +1 -0
- package/dist/src/scaffolder.d.ts +22 -0
- package/dist/src/scaffolder.js +120 -0
- package/dist/src/scaffolder.js.map +1 -0
- package/package.json +24 -0
- package/templates/product-system/.env.example +8 -0
- package/templates/product-system/CLAUDE.md +45 -0
- package/templates/product-system/package.json +32 -0
- package/templates/product-system/packages/backend/package.json +37 -0
- package/templates/product-system/packages/backend/src/middleware/auth_middleware.test.ts +86 -0
- package/templates/product-system/packages/backend/src/middleware/auth_middleware.ts +35 -0
- package/templates/product-system/packages/backend/src/routes/auth.ts +463 -0
- package/templates/product-system/packages/backend/src/routes/coherence.ts +187 -0
- package/templates/product-system/packages/backend/src/routes/graph.ts +67 -0
- package/templates/product-system/packages/backend/src/routes/kanban.ts +201 -0
- package/templates/product-system/packages/backend/src/routes/pipeline.ts +41 -0
- package/templates/product-system/packages/backend/src/routes/projects.ts +122 -0
- package/templates/product-system/packages/backend/src/routes/users.ts +97 -0
- package/templates/product-system/packages/backend/src/server.ts +159 -0
- package/templates/product-system/packages/backend/src/services/auth_service.test.ts +115 -0
- package/templates/product-system/packages/backend/src/services/auth_service.ts +82 -0
- package/templates/product-system/packages/backend/src/services/coherence-review.ts +339 -0
- package/templates/product-system/packages/backend/src/services/email_service.ts +75 -0
- package/templates/product-system/packages/backend/src/services/init.ts +80 -0
- package/templates/product-system/packages/backend/src/services/invitation_service.test.ts +235 -0
- package/templates/product-system/packages/backend/src/services/invitation_service.ts +193 -0
- package/templates/product-system/packages/backend/src/services/password_reset_service.test.ts +151 -0
- package/templates/product-system/packages/backend/src/services/password_reset_service.ts +135 -0
- package/templates/product-system/packages/backend/src/services/project_service.test.ts +215 -0
- package/templates/product-system/packages/backend/src/services/project_service.ts +171 -0
- package/templates/product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -0
- package/templates/product-system/packages/backend/src/services/pty_session_manager.ts +279 -0
- package/templates/product-system/packages/backend/src/services/terminal_ws_handler.ts +133 -0
- package/templates/product-system/packages/backend/src/services/user_management_service.test.ts +158 -0
- package/templates/product-system/packages/backend/src/services/user_management_service.ts +128 -0
- package/templates/product-system/packages/backend/tsconfig.json +22 -0
- package/templates/product-system/packages/frontend/index.html +13 -0
- package/templates/product-system/packages/frontend/package-lock.json +2666 -0
- package/templates/product-system/packages/frontend/package.json +30 -0
- package/templates/product-system/packages/frontend/public/favicon.svg +16 -0
- package/templates/product-system/packages/frontend/src/App.tsx +29 -0
- package/templates/product-system/packages/frontend/src/api/client.ts +386 -0
- package/templates/product-system/packages/frontend/src/api/client_projects.test.ts +104 -0
- package/templates/product-system/packages/frontend/src/api/client_refresh.test.ts +145 -0
- package/templates/product-system/packages/frontend/src/components/CoherenceView.tsx +414 -0
- package/templates/product-system/packages/frontend/src/components/GraphLegend.tsx +124 -0
- package/templates/product-system/packages/frontend/src/components/GraphSettings.tsx +112 -0
- package/templates/product-system/packages/frontend/src/components/GraphView.tsx +370 -0
- package/templates/product-system/packages/frontend/src/components/InviteUserDialog.tsx +85 -0
- package/templates/product-system/packages/frontend/src/components/KanbanView.tsx +470 -0
- package/templates/product-system/packages/frontend/src/components/LoginPage.tsx +116 -0
- package/templates/product-system/packages/frontend/src/components/ProjectSelector.tsx +187 -0
- package/templates/product-system/packages/frontend/src/components/QaIssueSheet.tsx +192 -0
- package/templates/product-system/packages/frontend/src/components/SidePanel.tsx +231 -0
- package/templates/product-system/packages/frontend/src/components/TerminalView.tsx +200 -0
- package/templates/product-system/packages/frontend/src/components/Toolbar.tsx +84 -0
- package/templates/product-system/packages/frontend/src/components/UsersView.tsx +249 -0
- package/templates/product-system/packages/frontend/src/constants/graph.ts +191 -0
- package/templates/product-system/packages/frontend/src/hooks/useAuth.tsx +54 -0
- package/templates/product-system/packages/frontend/src/hooks/useGraph.ts +27 -0
- package/templates/product-system/packages/frontend/src/hooks/useKanban.ts +21 -0
- package/templates/product-system/packages/frontend/src/hooks/useProjects.ts +86 -0
- package/templates/product-system/packages/frontend/src/hooks/useTheme.ts +26 -0
- package/templates/product-system/packages/frontend/src/hooks/useToast.tsx +62 -0
- package/templates/product-system/packages/frontend/src/hooks/use_projects_logic.test.ts +61 -0
- package/templates/product-system/packages/frontend/src/main.tsx +12 -0
- package/templates/product-system/packages/frontend/src/pages/accept_invitation_page.tsx +167 -0
- package/templates/product-system/packages/frontend/src/pages/forgot_password_page.tsx +100 -0
- package/templates/product-system/packages/frontend/src/pages/register_page.tsx +137 -0
- package/templates/product-system/packages/frontend/src/pages/reset_password_page.tsx +146 -0
- package/templates/product-system/packages/frontend/src/routes/ProtectedRoute.tsx +12 -0
- package/templates/product-system/packages/frontend/src/routes/accept_invitation.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/dashboard.tsx +221 -0
- package/templates/product-system/packages/frontend/src/routes/forgot_password.tsx +13 -0
- package/templates/product-system/packages/frontend/src/routes/login.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/register.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/reset_password.tsx +13 -0
- package/templates/product-system/packages/frontend/src/styles/index.css +3358 -0
- package/templates/product-system/packages/frontend/src/utils/auth_validation.test.ts +51 -0
- package/templates/product-system/packages/frontend/src/utils/auth_validation.ts +19 -0
- package/templates/product-system/packages/frontend/src/utils/login_validation.test.ts +61 -0
- package/templates/product-system/packages/frontend/src/utils/login_validation.ts +24 -0
- package/templates/product-system/packages/frontend/src/utils/logout.test.ts +63 -0
- package/templates/product-system/packages/frontend/src/utils/node_sizing.test.ts +62 -0
- package/templates/product-system/packages/frontend/src/utils/node_sizing.ts +24 -0
- package/templates/product-system/packages/frontend/src/utils/task_status.test.ts +53 -0
- package/templates/product-system/packages/frontend/src/utils/task_status.ts +14 -0
- package/templates/product-system/packages/frontend/tsconfig.json +21 -0
- package/templates/product-system/packages/frontend/vite.config.ts +20 -0
- package/templates/product-system/packages/shared/.env.example +3 -0
- package/templates/product-system/packages/shared/README.md +1 -0
- package/templates/product-system/packages/shared/db/migrate.ts +32 -0
- package/templates/product-system/packages/shared/db/migrations/0000_dashing_gorgon.sql +128 -0
- package/templates/product-system/packages/shared/db/migrations/meta/0000_snapshot.json +819 -0
- package/templates/product-system/packages/shared/db/migrations/meta/_journal.json +13 -0
- package/templates/product-system/packages/shared/db/schema.ts +137 -0
- package/templates/product-system/packages/shared/drizzle.config.js +14 -0
- package/templates/product-system/packages/shared/lib/claude-service.ts +215 -0
- package/templates/product-system/packages/shared/lib/coherence.ts +278 -0
- package/templates/product-system/packages/shared/lib/completeness.ts +30 -0
- package/templates/product-system/packages/shared/lib/constants.ts +327 -0
- package/templates/product-system/packages/shared/lib/db.ts +81 -0
- package/templates/product-system/packages/shared/lib/git_workflow.ts +110 -0
- package/templates/product-system/packages/shared/lib/graph.ts +186 -0
- package/templates/product-system/packages/shared/lib/kanban.ts +161 -0
- package/templates/product-system/packages/shared/lib/markdown.ts +205 -0
- package/templates/product-system/packages/shared/lib/pipeline-state-store.ts +124 -0
- package/templates/product-system/packages/shared/lib/pipeline.ts +489 -0
- package/templates/product-system/packages/shared/lib/prompt_builder.ts +170 -0
- package/templates/product-system/packages/shared/lib/relevance_search.ts +159 -0
- package/templates/product-system/packages/shared/lib/session.ts +152 -0
- package/templates/product-system/packages/shared/lib/validator.ts +117 -0
- package/templates/product-system/packages/shared/lib/work_summary_parser.ts +130 -0
- package/templates/product-system/packages/shared/package.json +30 -0
- package/templates/product-system/packages/shared/scripts/assign-project.ts +52 -0
- package/templates/product-system/packages/shared/tools/add_edge.ts +61 -0
- package/templates/product-system/packages/shared/tools/add_node.ts +101 -0
- package/templates/product-system/packages/shared/tools/end_session.ts +87 -0
- package/templates/product-system/packages/shared/tools/get_gaps.ts +87 -0
- package/templates/product-system/packages/shared/tools/get_kanban.ts +125 -0
- package/templates/product-system/packages/shared/tools/get_node.ts +78 -0
- package/templates/product-system/packages/shared/tools/get_status.ts +98 -0
- package/templates/product-system/packages/shared/tools/migrate_to_turso.ts +385 -0
- package/templates/product-system/packages/shared/tools/move_card.ts +143 -0
- package/templates/product-system/packages/shared/tools/rebuild_index.ts +77 -0
- package/templates/product-system/packages/shared/tools/remove_edge.ts +59 -0
- package/templates/product-system/packages/shared/tools/remove_node.ts +96 -0
- package/templates/product-system/packages/shared/tools/resolve_question.ts +75 -0
- package/templates/product-system/packages/shared/tools/search_nodes.ts +106 -0
- package/templates/product-system/packages/shared/tools/start_session.ts +144 -0
- package/templates/product-system/packages/shared/tools/update_node.ts +133 -0
- package/templates/product-system/packages/shared/tsconfig.json +24 -0
- package/templates/product-system/pnpm-workspace.yaml +2 -0
- package/templates/product-system/smoke_test.ts +219 -0
- package/templates/product-system/tests/coherence_review.test.ts +562 -0
- package/templates/product-system/tests/db_sqlite_fallback.test.ts +75 -0
- package/templates/product-system/tests/edge_type_color_coding.test.ts +147 -0
- package/templates/product-system/tests/emit-tool-use-events.test.ts +85 -0
- package/templates/product-system/tests/feature_kind.test.ts +139 -0
- package/templates/product-system/tests/gap_indicators.test.ts +199 -0
- package/templates/product-system/tests/graceful_init.test.ts +142 -0
- package/templates/product-system/tests/graph_legend.test.ts +314 -0
- package/templates/product-system/tests/graph_settings_sheet.test.ts +804 -0
- package/templates/product-system/tests/hide_defined_filter.test.ts +205 -0
- package/templates/product-system/tests/kanban.test.ts +529 -0
- package/templates/product-system/tests/neighborhood_focus.test.ts +132 -0
- package/templates/product-system/tests/node_search.test.ts +340 -0
- package/templates/product-system/tests/node_sizing.test.ts +170 -0
- package/templates/product-system/tests/node_type_toggle_filters.test.ts +285 -0
- package/templates/product-system/tests/node_type_visual_encoding.test.ts +103 -0
- package/templates/product-system/tests/pipeline-state-store.test.ts +268 -0
- package/templates/product-system/tests/pipeline-unit.test.ts +593 -0
- package/templates/product-system/tests/pipeline.test.ts +195 -0
- package/templates/product-system/tests/pipeline_stats_all_cards.test.ts +193 -0
- package/templates/product-system/tests/play_all.test.ts +296 -0
- package/templates/product-system/tests/qa_issue_sheet.test.ts +464 -0
- package/templates/product-system/tests/relevance_search.test.ts +186 -0
- package/templates/product-system/tests/search_reorder.test.ts +88 -0
- package/templates/product-system/tests/serve_ui.test.ts +281 -0
- package/templates/product-system/tests/serve_ui_drizzle.test.ts +114 -0
- package/templates/product-system/tests/session_context_recall.test.ts +135 -0
- package/templates/product-system/tests/side_panel.test.ts +345 -0
- package/templates/product-system/tests/spec_completeness_label.test.ts +69 -0
- package/templates/product-system/tests/url_routing_test.ts +122 -0
- package/templates/product-system/tests/user_login.test.ts +150 -0
- package/templates/product-system/tests/user_registration.test.ts +205 -0
- package/templates/product-system/tests/web_terminal.test.ts +572 -0
- package/templates/product-system/tests/work_summary.test.ts +211 -0
- package/templates/product-system/tests/zoom_pan.test.ts +43 -0
- package/templates/product-system/tsconfig.json +24 -0
- package/templates/skills/product-bootstrap/SKILL.md +312 -0
- package/templates/skills/product-code-reviewer/SKILL.md +147 -0
- package/templates/skills/product-debugger/SKILL.md +206 -0
- package/templates/skills/product-debugger/references/agent-browser.md +1156 -0
- package/templates/skills/product-developer/SKILL.md +182 -0
- package/templates/skills/product-interview/SKILL.md +220 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Reorder Search Above Type Filter (feat_028).
|
|
3
|
+
* Verifies that the search input is rendered inside the legend card,
|
|
4
|
+
* above the legend sections, and that existing search functionality is preserved.
|
|
5
|
+
* Updated for React frontend architecture.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it } from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
10
|
+
import { join, dirname } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const SOURCE_ROOT = existsSync(join(__dirname, '..', 'package.json')) ? join(__dirname, '..') : join(__dirname, '..', '..');
|
|
15
|
+
const htmlPath = join(SOURCE_ROOT, 'packages', 'frontend', 'index.html');
|
|
16
|
+
const cssPath = join(SOURCE_ROOT, 'packages', 'frontend', 'src', 'styles', 'index.css');
|
|
17
|
+
const legendPath = join(SOURCE_ROOT, 'packages', 'frontend', 'src', 'components', 'GraphLegend.tsx');
|
|
18
|
+
|
|
19
|
+
const html = readFileSync(htmlPath, 'utf-8');
|
|
20
|
+
const css = readFileSync(cssPath, 'utf-8');
|
|
21
|
+
const legendSrc = readFileSync(legendPath, 'utf-8');
|
|
22
|
+
|
|
23
|
+
describe('feat_028: Reorder Search Above Type Filter', () => {
|
|
24
|
+
|
|
25
|
+
describe('HTML structure', () => {
|
|
26
|
+
it('search-container is NOT in index.html (rendered by React component)', () => {
|
|
27
|
+
assert.ok(!html.includes('id="search-container"'),
|
|
28
|
+
'search-container should not be in index.html — it is rendered by GraphLegend component');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('filter-bar is removed from index.html', () => {
|
|
32
|
+
assert.ok(!html.includes('class="filter-bar"'),
|
|
33
|
+
'filter-bar should be removed from index.html');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('GraphLegend renders search', () => {
|
|
38
|
+
it('legend component includes search-container with id', () => {
|
|
39
|
+
assert.ok(legendSrc.includes('id="search-container"'),
|
|
40
|
+
'GraphLegend should render an element with id="search-container"');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('legend renders search input with search-input class', () => {
|
|
44
|
+
assert.ok(legendSrc.includes('className="search-input"'),
|
|
45
|
+
'GraphLegend should render search input');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('legend does NOT render type select dropdown (removed per rejection)', () => {
|
|
49
|
+
assert.ok(!legendSrc.includes('search-type-select'),
|
|
50
|
+
'GraphLegend should not render a type select dropdown');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('legend does NOT render search-results dropdown (removed per feat_035)', () => {
|
|
54
|
+
assert.ok(!legendSrc.includes('search-results'),
|
|
55
|
+
'GraphLegend should not render search results dropdown — search dims nodes directly');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('search container appears before the legend-header in JSX', () => {
|
|
59
|
+
const searchIdx = legendSrc.indexOf('legend-search-container');
|
|
60
|
+
const headerIdx = legendSrc.indexOf('legend-header');
|
|
61
|
+
assert.ok(searchIdx > -1, 'legend-search-container should exist in component');
|
|
62
|
+
assert.ok(headerIdx > -1, 'legend-header should exist in component');
|
|
63
|
+
assert.ok(searchIdx < headerIdx,
|
|
64
|
+
'search container should appear before the legend header (above legend sections)');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('CSS layout', () => {
|
|
69
|
+
it('legend-search-container has display flex', () => {
|
|
70
|
+
const rule = css.match(/\.legend-search-container\s*\{([^}]+)\}/);
|
|
71
|
+
assert.ok(rule, 'legend-search-container CSS rule should exist');
|
|
72
|
+
assert.ok(rule[1].includes('display: flex'),
|
|
73
|
+
'legend-search-container should use display: flex');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('search input uses flex: 1 to span full width', () => {
|
|
77
|
+
assert.ok(css.includes('.legend-search-container .search-input'),
|
|
78
|
+
'search-input inside legend should have specific styling');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('side panel top is 44px without filter bar', () => {
|
|
82
|
+
const panelRule = css.match(/\.side-panel\s*\{([^}]+)\}/);
|
|
83
|
+
assert.ok(panelRule, 'side-panel CSS block should exist');
|
|
84
|
+
assert.ok(panelRule[1].includes('top: 44px'),
|
|
85
|
+
'side panel top should be 44px');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for serve_ui.ts server endpoints and UI classes.
|
|
3
|
+
* Uses node:test built-in runner.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { createServer } from 'node:http';
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { join, dirname } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const SKILL_DIR = join(__dirname, '..');
|
|
14
|
+
|
|
15
|
+
// In-memory test fixtures (data lives in DB now, not files)
|
|
16
|
+
const TEST_GRAPH = {
|
|
17
|
+
project: { name: 'Test Project' },
|
|
18
|
+
nodes: [
|
|
19
|
+
{ id: 'comp_001', type: 'component', name: 'Test Component', completeness: 0.5, open_questions_count: 1 },
|
|
20
|
+
{ id: 'feat_001', type: 'feature', name: 'Test Feature', completeness: 1, open_questions_count: 0 },
|
|
21
|
+
],
|
|
22
|
+
edges: [
|
|
23
|
+
{ from: 'feat_001', to: 'comp_001', relation: 'contains' },
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const TEST_NODES = {
|
|
28
|
+
comp_001: '---\nid: comp_001\ntype: component\nname: Test Component\n---\nBody content here.',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const TEST_KANBAN = {
|
|
32
|
+
feat_001: { column: 'done', rejection_count: 0, notes: [] },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Helper: start the server for integration tests
|
|
36
|
+
const startTestServer = async () => {
|
|
37
|
+
const { createServer: createHttp } = await import('node:http');
|
|
38
|
+
const { readFile: rf, stat: st } = await import('node:fs/promises');
|
|
39
|
+
|
|
40
|
+
const UI_DIR = join(SKILL_DIR, 'packages', 'frontend');
|
|
41
|
+
|
|
42
|
+
const sendJson = (res, statusCode, data) => {
|
|
43
|
+
const body = JSON.stringify(data);
|
|
44
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
45
|
+
res.end(body);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const routeRequest = async (req, res) => {
|
|
49
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
50
|
+
const path = url.pathname;
|
|
51
|
+
|
|
52
|
+
if (path === '/api/graph') {
|
|
53
|
+
sendJson(res, 200, TEST_GRAPH);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const nodeMatch = path.match(/^\/api\/node\/([a-z_]+\d+)$/);
|
|
58
|
+
if (nodeMatch) {
|
|
59
|
+
const nodeId = nodeMatch[1];
|
|
60
|
+
const content = TEST_NODES[nodeId];
|
|
61
|
+
if (content) {
|
|
62
|
+
sendJson(res, 200, { id: nodeId, content });
|
|
63
|
+
} else {
|
|
64
|
+
sendJson(res, 404, { error: `Node not found: ${nodeId}` });
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (path === '/api/kanban') {
|
|
70
|
+
sendJson(res, 200, TEST_KANBAN);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const filePath = path === '/' ? join(UI_DIR, 'index.html') : join(UI_DIR, path);
|
|
75
|
+
if (!filePath.startsWith(UI_DIR)) {
|
|
76
|
+
res.writeHead(403);
|
|
77
|
+
res.end('Forbidden');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const info = await st(filePath);
|
|
83
|
+
if (!info.isFile()) { res.writeHead(404); res.end('Not found'); return; }
|
|
84
|
+
const content = await rf(filePath);
|
|
85
|
+
const ext = filePath.split('.').pop();
|
|
86
|
+
const mimeMap = { html: 'text/html', js: 'application/javascript', css: 'text/css' };
|
|
87
|
+
res.writeHead(200, { 'Content-Type': mimeMap[ext] || 'application/octet-stream' });
|
|
88
|
+
res.end(content);
|
|
89
|
+
} catch {
|
|
90
|
+
res.writeHead(404);
|
|
91
|
+
res.end('Not found');
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const server = createHttp(routeRequest);
|
|
96
|
+
await new Promise(resolve => server.listen(0, resolve));
|
|
97
|
+
const port = server.address().port;
|
|
98
|
+
return { server, port, baseUrl: `http://localhost:${port}` };
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
describe('Server API', () => {
|
|
102
|
+
let server, port, baseUrl;
|
|
103
|
+
|
|
104
|
+
beforeEach(async () => {
|
|
105
|
+
const s = await startTestServer();
|
|
106
|
+
server = s.server;
|
|
107
|
+
port = s.port;
|
|
108
|
+
baseUrl = s.baseUrl;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
server.close();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('GET /api/graph returns valid graph JSON', async () => {
|
|
116
|
+
const resp = await fetch(`${baseUrl}/api/graph`);
|
|
117
|
+
assert.equal(resp.status, 200);
|
|
118
|
+
const data = await resp.json();
|
|
119
|
+
assert.ok(data.nodes, 'graph should have nodes array');
|
|
120
|
+
assert.ok(data.edges, 'graph should have edges array');
|
|
121
|
+
assert.ok(data.project, 'graph should have project object');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('GET /api/node/:id returns node content for existing node', async () => {
|
|
125
|
+
const resp = await fetch(`${baseUrl}/api/node/comp_001`);
|
|
126
|
+
assert.equal(resp.status, 200);
|
|
127
|
+
const data = await resp.json();
|
|
128
|
+
assert.equal(data.id, 'comp_001');
|
|
129
|
+
assert.ok(data.content, 'should have content field');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('GET /api/node/:id returns 404 for non-existent node', async () => {
|
|
133
|
+
const resp = await fetch(`${baseUrl}/api/node/nonexistent_999`);
|
|
134
|
+
assert.equal(resp.status, 404);
|
|
135
|
+
const data = await resp.json();
|
|
136
|
+
assert.ok(data.error);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('GET /api/kanban returns valid kanban JSON', async () => {
|
|
140
|
+
const resp = await fetch(`${baseUrl}/api/kanban`);
|
|
141
|
+
assert.equal(resp.status, 200);
|
|
142
|
+
const data = await resp.json();
|
|
143
|
+
assert.ok(typeof data === 'object', 'kanban should be an object');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('GET / serves index.html', async () => {
|
|
147
|
+
const resp = await fetch(`${baseUrl}/`);
|
|
148
|
+
assert.equal(resp.status, 200);
|
|
149
|
+
const ct = resp.headers.get('content-type');
|
|
150
|
+
assert.ok(ct.includes('text/html'));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('GET /src/styles/index.css serves CSS file', async () => {
|
|
154
|
+
const resp = await fetch(`${baseUrl}/src/styles/index.css`);
|
|
155
|
+
assert.equal(resp.status, 200);
|
|
156
|
+
const ct = resp.headers.get('content-type');
|
|
157
|
+
assert.ok(ct.includes('text/css'));
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('GET /nonexistent returns 404', async () => {
|
|
161
|
+
const resp = await fetch(`${baseUrl}/does_not_exist.xyz`);
|
|
162
|
+
assert.equal(resp.status, 404);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('rejects directory traversal attempts', async () => {
|
|
166
|
+
const resp = await fetch(`${baseUrl}/../graph.json`);
|
|
167
|
+
// URL normalization may resolve this differently, but we should not serve graph.json
|
|
168
|
+
assert.notEqual(resp.headers.get('content-type'), 'application/json');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('ThemeManager', () => {
|
|
173
|
+
it('defaults to dark theme when storage is empty', () => {
|
|
174
|
+
const storage = { getItem: () => null, setItem: () => {} };
|
|
175
|
+
const root = { setAttribute: () => {} };
|
|
176
|
+
const toggle = null;
|
|
177
|
+
|
|
178
|
+
// Inline the logic for testing
|
|
179
|
+
const theme = storage.getItem('graph-ui-theme') || 'dark';
|
|
180
|
+
assert.equal(theme, 'dark');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('reads theme from storage', () => {
|
|
184
|
+
const storage = { getItem: (key) => key === 'graph-ui-theme' ? 'light' : null, setItem: () => {} };
|
|
185
|
+
const theme = storage.getItem('graph-ui-theme') || 'dark';
|
|
186
|
+
assert.equal(theme, 'light');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('toggles between dark and light', () => {
|
|
190
|
+
let current = 'dark';
|
|
191
|
+
const toggle = () => { current = current === 'dark' ? 'light' : 'dark'; };
|
|
192
|
+
toggle();
|
|
193
|
+
assert.equal(current, 'light');
|
|
194
|
+
toggle();
|
|
195
|
+
assert.equal(current, 'dark');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('GraphRenderer data transformations', () => {
|
|
200
|
+
const sampleGraph = {
|
|
201
|
+
nodes: [
|
|
202
|
+
{ id: 'feat_001', type: 'feature', name: 'Node A', completeness: 1, open_questions_count: 0 },
|
|
203
|
+
{ id: 'comp_001', type: 'component', name: 'Node B', completeness: 0.5, open_questions_count: 2 },
|
|
204
|
+
{ id: 'dec_001', type: 'decision', name: 'Node C', completeness: 0.8, open_questions_count: 0 },
|
|
205
|
+
],
|
|
206
|
+
edges: [
|
|
207
|
+
{ from: 'feat_001', to: 'comp_001', relation: 'contains' },
|
|
208
|
+
{ from: 'feat_001', to: 'dec_001', relation: 'governed_by' },
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
it('computes edge counts for nodes', () => {
|
|
213
|
+
const edgeCounts = new Map();
|
|
214
|
+
sampleGraph.edges.forEach(e => {
|
|
215
|
+
edgeCounts.set(e.from, (edgeCounts.get(e.from) || 0) + 1);
|
|
216
|
+
edgeCounts.set(e.to, (edgeCounts.get(e.to) || 0) + 1);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
assert.equal(edgeCounts.get('feat_001'), 2);
|
|
220
|
+
assert.equal(edgeCounts.get('comp_001'), 1);
|
|
221
|
+
assert.equal(edgeCounts.get('dec_001'), 1);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('filters links to only include existing nodes', () => {
|
|
225
|
+
const nodeIds = new Set(sampleGraph.nodes.map(n => n.id));
|
|
226
|
+
const extraEdges = [
|
|
227
|
+
...sampleGraph.edges,
|
|
228
|
+
{ from: 'feat_001', to: 'nonexistent_001', relation: 'contains' },
|
|
229
|
+
];
|
|
230
|
+
const validLinks = extraEdges.filter(e => nodeIds.has(e.from) && nodeIds.has(e.to));
|
|
231
|
+
assert.equal(validLinks.length, 2);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('computes node radius based on edge count', () => {
|
|
235
|
+
const nodeRadius = (edgeCount) => {
|
|
236
|
+
const base = 8;
|
|
237
|
+
const scale = Math.min(edgeCount * 1.5, 20);
|
|
238
|
+
return base + scale;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
assert.equal(nodeRadius(0), 8);
|
|
242
|
+
assert.equal(nodeRadius(2), 11);
|
|
243
|
+
assert.equal(nodeRadius(20), 28); // capped at 20
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('computes node stroke for gaps', () => {
|
|
247
|
+
const nodeStroke = (d) => {
|
|
248
|
+
if (d.completeness < 1 && d.open_questions_count > 0) return '#ff6b6b';
|
|
249
|
+
if (d.completeness < 1) return '#ffd43b';
|
|
250
|
+
return 'transparent';
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
assert.equal(nodeStroke({ completeness: 1, open_questions_count: 0 }), 'transparent');
|
|
254
|
+
assert.equal(nodeStroke({ completeness: 0.5, open_questions_count: 2 }), '#ff6b6b');
|
|
255
|
+
assert.equal(nodeStroke({ completeness: 0.8, open_questions_count: 0 }), '#ffd43b');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('computes overall completeness percentage', () => {
|
|
259
|
+
const nodes = sampleGraph.nodes;
|
|
260
|
+
const total = nodes.length;
|
|
261
|
+
const sum = nodes.reduce((acc, n) => acc + (n.completeness || 0), 0);
|
|
262
|
+
const pct = Math.round((sum / total) * 100);
|
|
263
|
+
assert.equal(pct, 77); // (1 + 0.5 + 0.8) / 3 = 0.7666...
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('ApiClient', () => {
|
|
268
|
+
it('constructs URLs correctly with base URL', () => {
|
|
269
|
+
const baseUrl = 'http://localhost:3000';
|
|
270
|
+
const graphUrl = `${baseUrl}/api/graph`;
|
|
271
|
+
const nodeUrl = `${baseUrl}/api/node/feat_001`;
|
|
272
|
+
assert.equal(graphUrl, 'http://localhost:3000/api/graph');
|
|
273
|
+
assert.equal(nodeUrl, 'http://localhost:3000/api/node/feat_001');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('constructs URLs correctly with empty base URL', () => {
|
|
277
|
+
const baseUrl = '';
|
|
278
|
+
const graphUrl = `${baseUrl}/api/graph`;
|
|
279
|
+
assert.equal(graphUrl, '/api/graph');
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for backend Drizzle integration (originally feat_047).
|
|
3
|
+
* Verifies that the backend API routes use the Drizzle data access layer.
|
|
4
|
+
* Uses node:test built-in runner.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it } from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { join, dirname } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const SOURCE_ROOT = join(__dirname, '..');
|
|
14
|
+
const BACKEND_SRC = join(SOURCE_ROOT, 'packages', 'backend', 'src');
|
|
15
|
+
const SHARED_LIB = join(SOURCE_ROOT, 'packages', 'shared', 'lib');
|
|
16
|
+
|
|
17
|
+
describe('Backend Drizzle integration', () => {
|
|
18
|
+
let graphRouteContent: string;
|
|
19
|
+
let kanbanRouteContent: string;
|
|
20
|
+
let coherenceRouteContent: string;
|
|
21
|
+
let serverContent: string;
|
|
22
|
+
|
|
23
|
+
it('loads backend source files', async () => {
|
|
24
|
+
graphRouteContent = await readFile(join(BACKEND_SRC, 'routes', 'graph.ts'), 'utf-8');
|
|
25
|
+
kanbanRouteContent = await readFile(join(BACKEND_SRC, 'routes', 'kanban.ts'), 'utf-8');
|
|
26
|
+
coherenceRouteContent = await readFile(join(BACKEND_SRC, 'routes', 'coherence.ts'), 'utf-8');
|
|
27
|
+
serverContent = await readFile(join(BACKEND_SRC, 'server.ts'), 'utf-8');
|
|
28
|
+
assert.ok(graphRouteContent.length > 0);
|
|
29
|
+
assert.ok(kanbanRouteContent.length > 0);
|
|
30
|
+
assert.ok(coherenceRouteContent.length > 0);
|
|
31
|
+
assert.ok(serverContent.length > 0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('graph route uses readGraph()', () => {
|
|
35
|
+
assert.ok(graphRouteContent.includes('readGraph'), 'graph route should use readGraph');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('graph route uses getDb() for node content', () => {
|
|
39
|
+
assert.ok(graphRouteContent.includes('getDb'), 'graph route should use getDb');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('kanban route uses loadKanban()', () => {
|
|
43
|
+
assert.ok(kanbanRouteContent.includes('loadKanban'), 'kanban route should use loadKanban');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('kanban route uses saveKanbanEntry()', () => {
|
|
47
|
+
assert.ok(kanbanRouteContent.includes('saveKanbanEntry'), 'kanban route should use saveKanbanEntry');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('coherence route uses loadCoherence()', () => {
|
|
51
|
+
assert.ok(
|
|
52
|
+
coherenceRouteContent.includes('readCoherenceData') || coherenceRouteContent.includes('loadCoherence'),
|
|
53
|
+
'coherence route should load coherence data from DB'
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('server uses Express', () => {
|
|
58
|
+
assert.ok(serverContent.includes('express'), 'server should use Express');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('backend does NOT use writeFile for data persistence', () => {
|
|
62
|
+
assert.ok(!graphRouteContent.includes('writeFile'), 'graph route should not use writeFile');
|
|
63
|
+
assert.ok(!kanbanRouteContent.includes('writeFile'), 'kanban route should not use writeFile');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('coherence.ts module structure', () => {
|
|
68
|
+
let coherenceContent: string;
|
|
69
|
+
|
|
70
|
+
it('loads coherence.ts source', async () => {
|
|
71
|
+
const coherencePath = join(SHARED_LIB, 'coherence.ts');
|
|
72
|
+
coherenceContent = await readFile(coherencePath, 'utf-8');
|
|
73
|
+
assert.ok(coherenceContent.length > 0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('exports loadCoherence function', () => {
|
|
77
|
+
assert.ok(coherenceContent.includes('export const loadCoherence'), 'should export loadCoherence');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('exports saveCoherence function', () => {
|
|
81
|
+
assert.ok(coherenceContent.includes('export const saveCoherence'), 'should export saveCoherence');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('exports getProposal function', () => {
|
|
85
|
+
assert.ok(coherenceContent.includes('export const getProposal'), 'should export getProposal');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('exports updateProposalStatus function', () => {
|
|
89
|
+
assert.ok(coherenceContent.includes('export const updateProposalStatus'), 'should export updateProposalStatus');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('exports updateCoherenceState function', () => {
|
|
93
|
+
assert.ok(coherenceContent.includes('export const updateCoherenceState'), 'should export updateCoherenceState');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('exports addProposals function', () => {
|
|
97
|
+
assert.ok(coherenceContent.includes('export const addProposals'), 'should export addProposals');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('uses coherenceReviews table from schema', () => {
|
|
101
|
+
assert.ok(
|
|
102
|
+
coherenceContent.includes("import { coherenceReviews, reviewMeta }"),
|
|
103
|
+
'should import coherenceReviews and reviewMeta from schema'
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('uses getDb from lib/db.ts', () => {
|
|
108
|
+
assert.ok(coherenceContent.includes("import { getDb }"), 'should import getDb from db.ts');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('uses review_meta for coherence state', () => {
|
|
112
|
+
assert.ok(coherenceContent.includes("coherence:state"), 'should use coherence:state key in review_meta');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for session context recall (feat_058).
|
|
3
|
+
* Tests data transformations and formatting without requiring Drizzle/Turso.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it } from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Helper: extractDescriptionExcerpt — mirrors the function in start_session.js.
|
|
10
|
+
*/
|
|
11
|
+
const extractDescriptionExcerpt = (body, maxLen = 120) => {
|
|
12
|
+
if (!body) return '';
|
|
13
|
+
const match = body.match(/## Description\s*\n+(.+)/);
|
|
14
|
+
if (!match) return '';
|
|
15
|
+
const line = match[1].trim();
|
|
16
|
+
return line.length > maxLen ? line.slice(0, maxLen) + '…' : line;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe('session context recall', () => {
|
|
20
|
+
describe('extractDescriptionExcerpt', () => {
|
|
21
|
+
it('extracts first line after ## Description', () => {
|
|
22
|
+
const body = '## Description\nThis is the description line.\n\n## Other Section\nstuff';
|
|
23
|
+
assert.equal(extractDescriptionExcerpt(body), 'This is the description line.');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('truncates long descriptions with ellipsis', () => {
|
|
27
|
+
const longLine = 'A'.repeat(200);
|
|
28
|
+
const body = `## Description\n${longLine}\n`;
|
|
29
|
+
const result = extractDescriptionExcerpt(body, 120);
|
|
30
|
+
assert.equal(result.length, 121); // 120 chars + ellipsis
|
|
31
|
+
assert.ok(result.endsWith('…'));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns empty string for null body', () => {
|
|
35
|
+
assert.equal(extractDescriptionExcerpt(null), '');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns empty string when no Description section exists', () => {
|
|
39
|
+
assert.equal(extractDescriptionExcerpt('## Other\nContent'), '');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('handles blank lines between header and content', () => {
|
|
43
|
+
const body = '## Description\n\nActual content here.';
|
|
44
|
+
assert.equal(extractDescriptionExcerpt(body), 'Actual content here.');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('recent sessions data transformation', () => {
|
|
49
|
+
it('parses nodesTouched JSON from session records', () => {
|
|
50
|
+
const session = {
|
|
51
|
+
sessionNumber: 5,
|
|
52
|
+
startedAt: '2026-03-01T10:00:00.000Z',
|
|
53
|
+
endedAt: '2026-03-01T11:00:00.000Z',
|
|
54
|
+
summary: 'Worked on auth features',
|
|
55
|
+
nodesTouched: '["feat_001","feat_002","dec_003"]',
|
|
56
|
+
questionsResolved: 2,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const nodesTouched = JSON.parse(session.nodesTouched);
|
|
60
|
+
assert.deepEqual(nodesTouched, ['feat_001', 'feat_002', 'dec_003']);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('handles null nodesTouched gracefully', () => {
|
|
64
|
+
const session = { nodesTouched: null };
|
|
65
|
+
const nodesTouched = session.nodesTouched ? JSON.parse(session.nodesTouched) : [];
|
|
66
|
+
assert.deepEqual(nodesTouched, []);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('handles empty array nodesTouched', () => {
|
|
70
|
+
const session = { nodesTouched: '[]' };
|
|
71
|
+
const nodesTouched = JSON.parse(session.nodesTouched);
|
|
72
|
+
assert.deepEqual(nodesTouched, []);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('recent decisions filtering', () => {
|
|
77
|
+
it('filters decisions updated after earliest session start', () => {
|
|
78
|
+
const recentSessions = [
|
|
79
|
+
{ startedAt: '2026-03-03T10:00:00.000Z', endedAt: '2026-03-03T11:00:00.000Z' },
|
|
80
|
+
{ startedAt: '2026-03-02T10:00:00.000Z', endedAt: '2026-03-02T11:00:00.000Z' },
|
|
81
|
+
{ startedAt: '2026-03-01T10:00:00.000Z', endedAt: '2026-03-01T11:00:00.000Z' },
|
|
82
|
+
];
|
|
83
|
+
const earliest = recentSessions[recentSessions.length - 1].startedAt;
|
|
84
|
+
|
|
85
|
+
const allDecisions = [
|
|
86
|
+
{ id: 'dec_001', name: 'Recent Decision', updatedAt: '2026-03-02T12:00:00.000Z' },
|
|
87
|
+
{ id: 'dec_002', name: 'Old Decision', updatedAt: '2026-02-15T12:00:00.000Z' },
|
|
88
|
+
{ id: 'dec_003', name: 'Another Recent', updatedAt: '2026-03-01T10:30:00.000Z' },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const filtered = allDecisions.filter(d => d.updatedAt >= earliest);
|
|
92
|
+
assert.equal(filtered.length, 2);
|
|
93
|
+
assert.equal(filtered[0].id, 'dec_001');
|
|
94
|
+
assert.equal(filtered[1].id, 'dec_003');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns empty when no sessions exist', () => {
|
|
98
|
+
const recentSessions = [];
|
|
99
|
+
assert.equal(recentSessions.length, 0);
|
|
100
|
+
// getRecentDecisions returns [] when recentSessions is empty
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('recently modified nodes formatting', () => {
|
|
105
|
+
it('formats node data for concise display', () => {
|
|
106
|
+
const node = {
|
|
107
|
+
id: 'feat_042',
|
|
108
|
+
name: 'Search Feature',
|
|
109
|
+
type: 'feature',
|
|
110
|
+
status: 'defined',
|
|
111
|
+
completeness: 0.85,
|
|
112
|
+
updatedAt: '2026-03-03T14:30:00.000Z',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const formatted = `${node.id} ${node.name} [${node.type}] ${node.status} — ${Math.round(node.completeness * 100)}% (updated ${node.updatedAt})`;
|
|
116
|
+
assert.equal(formatted, 'feat_042 Search Feature [feature] defined — 85% (updated 2026-03-03T14:30:00.000Z)');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('context recall omission on first session', () => {
|
|
121
|
+
it('skips recall section when no ended sessions exist', () => {
|
|
122
|
+
const recentSessions = [];
|
|
123
|
+
const shouldShowRecall = recentSessions.length > 0;
|
|
124
|
+
assert.equal(shouldShowRecall, false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('shows recall section when ended sessions exist', () => {
|
|
128
|
+
const recentSessions = [
|
|
129
|
+
{ sessionNumber: 1, startedAt: '2026-03-01T10:00:00.000Z', endedAt: '2026-03-01T11:00:00.000Z' },
|
|
130
|
+
];
|
|
131
|
+
const shouldShowRecall = recentSessions.length > 0;
|
|
132
|
+
assert.equal(shouldShowRecall, true);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|