sqlew 3.5.3 → 3.6.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/CHANGELOG.md +247 -1772
- package/README.md +70 -304
- package/assets/config.example.toml +97 -0
- package/dist/adapters/index.d.ts +11 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +21 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/mysql-adapter.d.ts +31 -0
- package/dist/adapters/mysql-adapter.d.ts.map +1 -0
- package/dist/adapters/mysql-adapter.js +63 -0
- package/dist/adapters/mysql-adapter.js.map +1 -0
- package/dist/adapters/postgresql-adapter.d.ts +31 -0
- package/dist/adapters/postgresql-adapter.d.ts.map +1 -0
- package/dist/adapters/postgresql-adapter.js +63 -0
- package/dist/adapters/postgresql-adapter.js.map +1 -0
- package/dist/adapters/sqlite-adapter.d.ts +37 -0
- package/dist/adapters/sqlite-adapter.d.ts.map +1 -0
- package/dist/adapters/sqlite-adapter.js +129 -0
- package/dist/adapters/sqlite-adapter.js.map +1 -0
- package/dist/adapters/types.d.ts +33 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/cli.js +55 -54
- package/dist/cli.js.map +1 -1
- package/dist/config/example-generator.d.ts +11 -0
- package/dist/config/example-generator.d.ts.map +1 -0
- package/dist/config/example-generator.js +48 -0
- package/dist/config/example-generator.js.map +1 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +4 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/types.d.ts +9 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js.map +1 -1
- package/dist/database.d.ts +44 -122
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +145 -416
- package/dist/database.js.map +1 -1
- package/dist/index.js +215 -185
- package/dist/index.js.map +1 -1
- package/dist/knexfile.d.ts +6 -0
- package/dist/knexfile.d.ts.map +1 -0
- package/dist/knexfile.js +85 -0
- package/dist/knexfile.js.map +1 -0
- package/dist/migrations/add-help-system-tables.d.ts +35 -0
- package/dist/migrations/add-help-system-tables.d.ts.map +1 -0
- package/dist/migrations/add-help-system-tables.js +206 -0
- package/dist/migrations/add-help-system-tables.js.map +1 -0
- package/dist/migrations/add-token-tracking.d.ts +28 -0
- package/dist/migrations/add-token-tracking.d.ts.map +1 -0
- package/dist/migrations/add-token-tracking.js +108 -0
- package/dist/migrations/add-token-tracking.js.map +1 -0
- package/dist/migrations/index.d.ts +25 -12
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +147 -20
- package/dist/migrations/index.js.map +1 -1
- package/dist/migrations/knex/20251025020452_create_master_tables.d.ts +4 -0
- package/dist/migrations/knex/20251025020452_create_master_tables.d.ts.map +1 -0
- package/dist/migrations/knex/20251025020452_create_master_tables.js +65 -0
- package/dist/migrations/knex/20251025020452_create_master_tables.js.map +1 -0
- package/dist/migrations/knex/20251025021152_create_transaction_tables.d.ts +4 -0
- package/dist/migrations/knex/20251025021152_create_transaction_tables.d.ts.map +1 -0
- package/dist/migrations/knex/20251025021152_create_transaction_tables.js +235 -0
- package/dist/migrations/knex/20251025021152_create_transaction_tables.js.map +1 -0
- package/dist/migrations/knex/20251025021351_create_indexes.d.ts +4 -0
- package/dist/migrations/knex/20251025021351_create_indexes.d.ts.map +1 -0
- package/dist/migrations/knex/20251025021351_create_indexes.js +62 -0
- package/dist/migrations/knex/20251025021351_create_indexes.js.map +1 -0
- package/dist/migrations/knex/20251025021416_seed_master_data.d.ts +4 -0
- package/dist/migrations/knex/20251025021416_seed_master_data.d.ts.map +1 -0
- package/dist/migrations/knex/20251025021416_seed_master_data.js +58 -0
- package/dist/migrations/knex/20251025021416_seed_master_data.js.map +1 -0
- package/dist/migrations/knex/20251025070349_create_views.d.ts +4 -0
- package/dist/migrations/knex/20251025070349_create_views.d.ts.map +1 -0
- package/dist/migrations/knex/20251025070349_create_views.js +143 -0
- package/dist/migrations/knex/20251025070349_create_views.js.map +1 -0
- package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.d.ts +4 -0
- package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.d.ts.map +1 -0
- package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.js +15 -0
- package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.js.map +1 -0
- package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.d.ts +8 -0
- package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.d.ts.map +1 -0
- package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.js +12 -0
- package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.js.map +1 -0
- package/dist/migrations/knex/20251025090000_create_help_system_tables.d.ts +19 -0
- package/dist/migrations/knex/20251025090000_create_help_system_tables.d.ts.map +1 -0
- package/dist/migrations/knex/20251025090000_create_help_system_tables.js +115 -0
- package/dist/migrations/knex/20251025090000_create_help_system_tables.js.map +1 -0
- package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.d.ts +13 -0
- package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.d.ts.map +1 -0
- package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.js +377 -0
- package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.js.map +1 -0
- package/dist/migrations/knex/20251025100000_seed_help_metadata.d.ts +15 -0
- package/dist/migrations/knex/20251025100000_seed_help_metadata.d.ts.map +1 -0
- package/dist/migrations/knex/20251025100000_seed_help_metadata.js +253 -0
- package/dist/migrations/knex/20251025100000_seed_help_metadata.js.map +1 -0
- package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.d.ts +16 -0
- package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.d.ts.map +1 -0
- package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.js +276 -0
- package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.js.map +1 -0
- package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.d.ts +8 -0
- package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.d.ts.map +1 -0
- package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.js +64 -0
- package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.js.map +1 -0
- package/dist/migrations/seed-help-data.d.ts +48 -0
- package/dist/migrations/seed-help-data.d.ts.map +1 -0
- package/dist/migrations/seed-help-data.js +1466 -0
- package/dist/migrations/seed-help-data.js.map +1 -0
- package/dist/migrations/seed-tool-metadata.d.ts +24 -0
- package/dist/migrations/seed-tool-metadata.d.ts.map +1 -0
- package/dist/migrations/seed-tool-metadata.js +392 -0
- package/dist/migrations/seed-tool-metadata.js.map +1 -0
- package/dist/migrations/v3.6.0-help-system-refactor.d.ts +46 -0
- package/dist/migrations/v3.6.0-help-system-refactor.d.ts.map +1 -0
- package/dist/migrations/v3.6.0-help-system-refactor.js +223 -0
- package/dist/migrations/v3.6.0-help-system-refactor.js.map +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2 -0
- package/dist/schema.js.map +1 -1
- package/dist/tests/git-aware-completion.test.js +89 -70
- package/dist/tests/git-aware-completion.test.js.map +1 -1
- package/dist/tests/help-system.test.d.ts +23 -0
- package/dist/tests/help-system.test.d.ts.map +1 -0
- package/dist/tests/help-system.test.js +374 -0
- package/dist/tests/help-system.test.js.map +1 -0
- package/dist/tests/tasks.auto-pruning-decision-link.test.js +92 -78
- package/dist/tests/tasks.auto-pruning-decision-link.test.js.map +1 -1
- package/dist/tests/tasks.auto-pruning-partial.test.js +106 -95
- package/dist/tests/tasks.auto-pruning-partial.test.js.map +1 -1
- package/dist/tests/tasks.auto-pruning-persistence.test.js +115 -97
- package/dist/tests/tasks.auto-pruning-persistence.test.js.map +1 -1
- package/dist/tests/tasks.auto-pruning-safety.test.js +124 -103
- package/dist/tests/tasks.auto-pruning-safety.test.js.map +1 -1
- package/dist/tests/tasks.dependencies.test.js +338 -307
- package/dist/tests/tasks.dependencies.test.js.map +1 -1
- package/dist/tests/tasks.link-file-backward-compat.test.js +116 -104
- package/dist/tests/tasks.link-file-backward-compat.test.js.map +1 -1
- package/dist/tests/tasks.watch-files-action.test.js +122 -101
- package/dist/tests/tasks.watch-files-action.test.js.map +1 -1
- package/dist/tests/tasks.watch-files-parameter.test.js +105 -94
- package/dist/tests/tasks.watch-files-parameter.test.js.map +1 -1
- package/dist/tests/two-step-git-completion.test.js +176 -133
- package/dist/tests/two-step-git-completion.test.js.map +1 -1
- package/dist/tests/vcs-staging.test.js +1 -1
- package/dist/tests/vcs-staging.test.js.map +1 -1
- package/dist/tools/config.d.ts +9 -6
- package/dist/tools/config.d.ts.map +1 -1
- package/dist/tools/config.js +16 -14
- package/dist/tools/config.js.map +1 -1
- package/dist/tools/constraints.d.ts +10 -7
- package/dist/tools/constraints.d.ts.map +1 -1
- package/dist/tools/constraints.js +66 -48
- package/dist/tools/constraints.js.map +1 -1
- package/dist/tools/context.d.ts +36 -33
- package/dist/tools/context.d.ts.map +1 -1
- package/dist/tools/context.js +374 -330
- package/dist/tools/context.js.map +1 -1
- package/dist/tools/files.d.ts +12 -9
- package/dist/tools/files.d.ts.map +1 -1
- package/dist/tools/files.js +173 -95
- package/dist/tools/files.js.map +1 -1
- package/dist/tools/help-queries.d.ts +130 -0
- package/dist/tools/help-queries.d.ts.map +1 -0
- package/dist/tools/help-queries.js +393 -0
- package/dist/tools/help-queries.js.map +1 -0
- package/dist/tools/messaging.d.ts +14 -11
- package/dist/tools/messaging.d.ts.map +1 -1
- package/dist/tools/messaging.js +217 -133
- package/dist/tools/messaging.js.map +1 -1
- package/dist/tools/tasks.d.ts +18 -16
- package/dist/tools/tasks.d.ts.map +1 -1
- package/dist/tools/tasks.js +513 -439
- package/dist/tools/tasks.js.map +1 -1
- package/dist/tools/utils.d.ts +14 -11
- package/dist/tools/utils.d.ts.map +1 -1
- package/dist/tools/utils.js +86 -121
- package/dist/tools/utils.js.map +1 -1
- package/dist/utils/activity-logging.d.ts +114 -0
- package/dist/utils/activity-logging.d.ts.map +1 -0
- package/dist/utils/activity-logging.js +162 -0
- package/dist/utils/activity-logging.js.map +1 -0
- package/dist/utils/batch.d.ts +2 -2
- package/dist/utils/batch.d.ts.map +1 -1
- package/dist/utils/batch.js +8 -8
- package/dist/utils/batch.js.map +1 -1
- package/dist/utils/cleanup.d.ts +21 -13
- package/dist/utils/cleanup.d.ts.map +1 -1
- package/dist/utils/cleanup.js +31 -24
- package/dist/utils/cleanup.js.map +1 -1
- package/dist/utils/debug-logger.d.ts +44 -0
- package/dist/utils/debug-logger.d.ts.map +1 -0
- package/dist/utils/debug-logger.js +116 -0
- package/dist/utils/debug-logger.js.map +1 -0
- package/dist/utils/help-tracking.d.ts +55 -0
- package/dist/utils/help-tracking.d.ts.map +1 -0
- package/dist/utils/help-tracking.js +88 -0
- package/dist/utils/help-tracking.js.map +1 -0
- package/dist/utils/retention.d.ts +7 -7
- package/dist/utils/retention.d.ts.map +1 -1
- package/dist/utils/retention.js +12 -12
- package/dist/utils/retention.js.map +1 -1
- package/dist/utils/task-stale-detection.d.ts +15 -13
- package/dist/utils/task-stale-detection.d.ts.map +1 -1
- package/dist/utils/task-stale-detection.js +100 -302
- package/dist/utils/task-stale-detection.js.map +1 -1
- package/dist/utils/token-estimation.d.ts +72 -0
- package/dist/utils/token-estimation.d.ts.map +1 -0
- package/dist/utils/token-estimation.js +71 -0
- package/dist/utils/token-estimation.js.map +1 -0
- package/dist/utils/token-logging.d.ts +48 -0
- package/dist/utils/token-logging.d.ts.map +1 -0
- package/dist/utils/token-logging.js +112 -0
- package/dist/utils/token-logging.js.map +1 -0
- package/dist/utils/view-queries.d.ts +34 -0
- package/dist/utils/view-queries.d.ts.map +1 -0
- package/dist/utils/view-queries.js +192 -0
- package/dist/utils/view-queries.js.map +1 -0
- package/dist/watcher/file-watcher.d.ts.map +1 -1
- package/dist/watcher/file-watcher.js +25 -11
- package/dist/watcher/file-watcher.js.map +1 -1
- package/docs/BEST_PRACTICES.md +56 -448
- package/docs/MIGRATION_v3.6.0.md +170 -0
- package/docs/SHARED_CONCEPTS.md +63 -208
- package/docs/TASK_OVERVIEW.md +2 -2
- package/docs/TOOL_SELECTION.md +41 -248
- package/package.json +16 -4
|
@@ -3,36 +3,23 @@
|
|
|
3
3
|
* Tests add_dependency, remove_dependency, get_dependencies actions
|
|
4
4
|
* and enhanced list/get actions with dependency support
|
|
5
5
|
*/
|
|
6
|
-
import { describe, it, beforeEach } from 'node:test';
|
|
6
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
7
7
|
import assert from 'node:assert/strict';
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
8
|
+
import { initializeDatabase, getOrCreateAgent, closeDatabase } from '../database.js';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import os from 'os';
|
|
12
12
|
/**
|
|
13
13
|
* Test database instance
|
|
14
14
|
*/
|
|
15
15
|
let testDb;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*/
|
|
19
|
-
function createTestDatabase() {
|
|
20
|
-
const db = new Database(':memory:');
|
|
21
|
-
db.pragma('foreign_keys = ON');
|
|
22
|
-
// Initialize schema
|
|
23
|
-
initializeSchema(db);
|
|
24
|
-
// Run task dependencies migration
|
|
25
|
-
const migrationResult = migrateToTaskDependencies(db);
|
|
26
|
-
if (!migrationResult.success) {
|
|
27
|
-
throw new Error(`Migration failed: ${migrationResult.message}`);
|
|
28
|
-
}
|
|
29
|
-
return db;
|
|
30
|
-
}
|
|
16
|
+
let tempDir;
|
|
17
|
+
let tempDbPath;
|
|
31
18
|
/**
|
|
32
19
|
* Helper: Create a test task
|
|
33
20
|
*/
|
|
34
|
-
function createTestTask(db, title, status = 'todo') {
|
|
35
|
-
const agentId = getOrCreateAgent(db, 'test-agent');
|
|
21
|
+
async function createTestTask(db, title, status = 'todo') {
|
|
22
|
+
const agentId = await getOrCreateAgent(db, 'test-agent');
|
|
36
23
|
const statusIdMap = {
|
|
37
24
|
'todo': 1,
|
|
38
25
|
'in_progress': 2,
|
|
@@ -42,181 +29,206 @@ function createTestTask(db, title, status = 'todo') {
|
|
|
42
29
|
'archived': 6
|
|
43
30
|
};
|
|
44
31
|
const statusId = statusIdMap[status];
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
32
|
+
const knex = db.getKnex();
|
|
33
|
+
const now = Math.floor(Date.now() / 1000);
|
|
34
|
+
const [taskId] = await knex('t_tasks').insert({
|
|
35
|
+
title,
|
|
36
|
+
status_id: statusId,
|
|
37
|
+
priority: 2,
|
|
38
|
+
created_by_agent_id: agentId,
|
|
39
|
+
assigned_agent_id: agentId,
|
|
40
|
+
created_ts: now,
|
|
41
|
+
updated_ts: now
|
|
42
|
+
});
|
|
43
|
+
return taskId;
|
|
50
44
|
}
|
|
51
45
|
/**
|
|
52
46
|
* Inline implementation of addDependency for testing (avoiding module dependency)
|
|
53
47
|
*/
|
|
54
|
-
function addDependencyTest(db, params) {
|
|
55
|
-
if (!params.
|
|
56
|
-
throw new Error('Parameter "
|
|
48
|
+
async function addDependencyTest(db, params) {
|
|
49
|
+
if (!params.depends_on_task_id) {
|
|
50
|
+
throw new Error('Parameter "depends_on_task_id" is required');
|
|
57
51
|
}
|
|
58
|
-
if (!params.
|
|
59
|
-
throw new Error('Parameter "
|
|
52
|
+
if (!params.task_id) {
|
|
53
|
+
throw new Error('Parameter "task_id" is required');
|
|
60
54
|
}
|
|
61
|
-
|
|
55
|
+
const knex = db.getKnex();
|
|
56
|
+
return await knex.transaction(async (trx) => {
|
|
62
57
|
const TASK_STATUS_ARCHIVED = 6;
|
|
63
58
|
// Validation 1: No self-dependencies
|
|
64
|
-
if (params.
|
|
59
|
+
if (params.depends_on_task_id === params.task_id) {
|
|
65
60
|
throw new Error('Self-dependency not allowed');
|
|
66
61
|
}
|
|
67
62
|
// Validation 2: Both tasks must exist and check if archived
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
const dependsOnTask = await trx('t_tasks')
|
|
64
|
+
.where({ id: params.depends_on_task_id })
|
|
65
|
+
.select('id', 'status_id')
|
|
66
|
+
.first();
|
|
67
|
+
const task = await trx('t_tasks')
|
|
68
|
+
.where({ id: params.task_id })
|
|
69
|
+
.select('id', 'status_id')
|
|
70
|
+
.first();
|
|
71
|
+
if (!dependsOnTask) {
|
|
72
|
+
throw new Error(`Task #${params.depends_on_task_id} not found`);
|
|
72
73
|
}
|
|
73
|
-
if (!
|
|
74
|
-
throw new Error(`
|
|
74
|
+
if (!task) {
|
|
75
|
+
throw new Error(`Task #${params.task_id} not found`);
|
|
75
76
|
}
|
|
76
77
|
// Validation 3: Neither task is archived
|
|
77
|
-
if (
|
|
78
|
-
throw new Error(`Cannot add dependency: Task #${params.
|
|
78
|
+
if (dependsOnTask.status_id === TASK_STATUS_ARCHIVED) {
|
|
79
|
+
throw new Error(`Cannot add dependency: Task #${params.depends_on_task_id} is archived`);
|
|
79
80
|
}
|
|
80
|
-
if (
|
|
81
|
-
throw new Error(`Cannot add dependency: Task #${params.
|
|
81
|
+
if (task.status_id === TASK_STATUS_ARCHIVED) {
|
|
82
|
+
throw new Error(`Cannot add dependency: Task #${params.task_id} is archived`);
|
|
82
83
|
}
|
|
83
84
|
// Validation 4: No direct circular (reverse relationship)
|
|
84
|
-
const reverseExists =
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
const reverseExists = await trx('t_task_dependencies')
|
|
86
|
+
.where({
|
|
87
|
+
depends_on_task_id: params.task_id,
|
|
88
|
+
task_id: params.depends_on_task_id
|
|
89
|
+
})
|
|
90
|
+
.first();
|
|
88
91
|
if (reverseExists) {
|
|
89
|
-
throw new Error(`Circular dependency detected: Task #${params.
|
|
92
|
+
throw new Error(`Circular dependency detected: Task #${params.task_id} already depends on Task #${params.depends_on_task_id}`);
|
|
90
93
|
}
|
|
91
94
|
// Validation 5: No transitive circular (check if adding this would create a cycle)
|
|
92
|
-
const cycleCheck =
|
|
95
|
+
const cycleCheck = await trx.raw(`
|
|
93
96
|
WITH RECURSIVE dependency_chain AS (
|
|
94
|
-
-- Start from the task that would
|
|
95
|
-
SELECT
|
|
97
|
+
-- Start from the task that would have the dependency
|
|
98
|
+
SELECT task_id, 1 as depth
|
|
96
99
|
FROM t_task_dependencies
|
|
97
|
-
WHERE
|
|
100
|
+
WHERE depends_on_task_id = ?
|
|
98
101
|
|
|
99
102
|
UNION ALL
|
|
100
103
|
|
|
101
104
|
-- Follow the chain of dependencies
|
|
102
|
-
SELECT d.
|
|
105
|
+
SELECT d.task_id, dc.depth + 1
|
|
103
106
|
FROM t_task_dependencies d
|
|
104
|
-
JOIN dependency_chain dc ON d.
|
|
107
|
+
JOIN dependency_chain dc ON d.depends_on_task_id = dc.task_id
|
|
105
108
|
WHERE dc.depth < 100
|
|
106
109
|
)
|
|
107
110
|
SELECT task_id FROM dependency_chain WHERE task_id = ?
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
`, [params.task_id, params.depends_on_task_id]);
|
|
112
|
+
const cycleResult = Array.isArray(cycleCheck) ? cycleCheck[0] : cycleCheck;
|
|
113
|
+
if (cycleResult && cycleResult.task_id) {
|
|
110
114
|
// Build cycle path for error message
|
|
111
|
-
const cyclePathResult =
|
|
115
|
+
const cyclePathResult = await trx.raw(`
|
|
112
116
|
WITH RECURSIVE dependency_chain AS (
|
|
113
|
-
SELECT
|
|
114
|
-
CAST(
|
|
117
|
+
SELECT task_id, 1 as depth,
|
|
118
|
+
CAST(task_id AS TEXT) as path
|
|
115
119
|
FROM t_task_dependencies
|
|
116
|
-
WHERE
|
|
120
|
+
WHERE depends_on_task_id = ?
|
|
117
121
|
|
|
118
122
|
UNION ALL
|
|
119
123
|
|
|
120
|
-
SELECT d.
|
|
121
|
-
dc.path || ' → ' || d.
|
|
124
|
+
SELECT d.task_id, dc.depth + 1,
|
|
125
|
+
dc.path || ' → ' || d.task_id
|
|
122
126
|
FROM t_task_dependencies d
|
|
123
|
-
JOIN dependency_chain dc ON d.
|
|
127
|
+
JOIN dependency_chain dc ON d.depends_on_task_id = dc.task_id
|
|
124
128
|
WHERE dc.depth < 100
|
|
125
129
|
)
|
|
126
130
|
SELECT path FROM dependency_chain WHERE task_id = ? ORDER BY depth DESC LIMIT 1
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
131
|
+
`, [params.task_id, params.depends_on_task_id]);
|
|
132
|
+
const pathResult = Array.isArray(cyclePathResult) ? cyclePathResult[0] : cyclePathResult;
|
|
133
|
+
const cyclePath = pathResult?.path || `#${params.task_id} → ... → #${params.depends_on_task_id}`;
|
|
134
|
+
throw new Error(`Circular dependency detected: Task #${params.depends_on_task_id} → #${cyclePath} → #${params.depends_on_task_id}`);
|
|
130
135
|
}
|
|
131
136
|
// All validations passed - insert dependency
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
const now = Math.floor(Date.now() / 1000);
|
|
138
|
+
await trx('t_task_dependencies').insert({
|
|
139
|
+
depends_on_task_id: params.depends_on_task_id,
|
|
140
|
+
task_id: params.task_id,
|
|
141
|
+
created_ts: now
|
|
142
|
+
});
|
|
137
143
|
return {
|
|
138
144
|
success: true,
|
|
139
|
-
message: `Dependency added: Task #${params.
|
|
145
|
+
message: `Dependency added: Task #${params.task_id} depends on Task #${params.depends_on_task_id}`
|
|
140
146
|
};
|
|
141
147
|
});
|
|
142
148
|
}
|
|
143
149
|
/**
|
|
144
150
|
* Inline implementation of removeDependency for testing
|
|
145
151
|
*/
|
|
146
|
-
function removeDependencyTest(db, params) {
|
|
147
|
-
if (!params.
|
|
148
|
-
throw new Error('Parameter "
|
|
152
|
+
async function removeDependencyTest(db, params) {
|
|
153
|
+
if (!params.depends_on_task_id) {
|
|
154
|
+
throw new Error('Parameter "depends_on_task_id" is required');
|
|
149
155
|
}
|
|
150
|
-
if (!params.
|
|
151
|
-
throw new Error('Parameter "
|
|
156
|
+
if (!params.task_id) {
|
|
157
|
+
throw new Error('Parameter "task_id" is required');
|
|
152
158
|
}
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
159
|
+
const knex = db.getKnex();
|
|
160
|
+
await knex('t_task_dependencies')
|
|
161
|
+
.where({
|
|
162
|
+
depends_on_task_id: params.depends_on_task_id,
|
|
163
|
+
task_id: params.task_id
|
|
164
|
+
})
|
|
165
|
+
.delete();
|
|
158
166
|
return {
|
|
159
167
|
success: true,
|
|
160
|
-
message: `Dependency removed: Task #${params.
|
|
168
|
+
message: `Dependency removed: Task #${params.task_id} no longer depends on Task #${params.depends_on_task_id}`
|
|
161
169
|
};
|
|
162
170
|
}
|
|
163
171
|
/**
|
|
164
172
|
* Inline implementation of getDependencies for testing
|
|
165
173
|
*/
|
|
166
|
-
function getDependenciesTest(db, params) {
|
|
174
|
+
async function getDependenciesTest(db, params) {
|
|
167
175
|
if (!params.task_id) {
|
|
168
176
|
throw new Error('Parameter "task_id" is required');
|
|
169
177
|
}
|
|
170
178
|
const includeDetails = params.include_details || false;
|
|
179
|
+
const knex = db.getKnex();
|
|
171
180
|
// Check if task exists
|
|
172
|
-
const taskExists =
|
|
181
|
+
const taskExists = await knex('t_tasks')
|
|
182
|
+
.where({ id: params.task_id })
|
|
183
|
+
.select('id')
|
|
184
|
+
.first();
|
|
173
185
|
if (!taskExists) {
|
|
174
186
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
175
187
|
}
|
|
176
188
|
// Build query based on include_details flag
|
|
177
189
|
let selectFields;
|
|
178
190
|
if (includeDetails) {
|
|
179
|
-
selectFields =
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
191
|
+
selectFields = [
|
|
192
|
+
't.id',
|
|
193
|
+
't.title',
|
|
194
|
+
's.name as status',
|
|
195
|
+
't.priority',
|
|
196
|
+
'aa.name as assigned_to',
|
|
197
|
+
't.created_ts',
|
|
198
|
+
't.updated_ts',
|
|
199
|
+
'td.description'
|
|
200
|
+
];
|
|
189
201
|
}
|
|
190
202
|
else {
|
|
191
|
-
selectFields =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
203
|
+
selectFields = [
|
|
204
|
+
't.id',
|
|
205
|
+
't.title',
|
|
206
|
+
's.name as status',
|
|
207
|
+
't.priority'
|
|
208
|
+
];
|
|
197
209
|
}
|
|
198
|
-
// Get blockers (tasks that
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const blockers =
|
|
209
|
-
// Get blocking (tasks this task
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const blocking =
|
|
210
|
+
// Get blockers (tasks that this task depends on)
|
|
211
|
+
let blockersQuery = knex('t_tasks as t')
|
|
212
|
+
.join('t_task_dependencies as d', 't.id', 'd.depends_on_task_id')
|
|
213
|
+
.leftJoin('m_task_statuses as s', 't.status_id', 's.id')
|
|
214
|
+
.leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
|
|
215
|
+
.where('d.task_id', params.task_id)
|
|
216
|
+
.select(selectFields);
|
|
217
|
+
if (includeDetails) {
|
|
218
|
+
blockersQuery = blockersQuery.leftJoin('t_task_details as td', 't.id', 'td.task_id');
|
|
219
|
+
}
|
|
220
|
+
const blockers = await blockersQuery;
|
|
221
|
+
// Get blocking (tasks that depend on this task)
|
|
222
|
+
let blockingQuery = knex('t_tasks as t')
|
|
223
|
+
.join('t_task_dependencies as d', 't.id', 'd.task_id')
|
|
224
|
+
.leftJoin('m_task_statuses as s', 't.status_id', 's.id')
|
|
225
|
+
.leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
|
|
226
|
+
.where('d.depends_on_task_id', params.task_id)
|
|
227
|
+
.select(selectFields);
|
|
228
|
+
if (includeDetails) {
|
|
229
|
+
blockingQuery = blockingQuery.leftJoin('t_task_details as td', 't.id', 'td.task_id');
|
|
230
|
+
}
|
|
231
|
+
const blocking = await blockingQuery;
|
|
220
232
|
return {
|
|
221
233
|
task_id: params.task_id,
|
|
222
234
|
blockers,
|
|
@@ -226,43 +238,61 @@ function getDependenciesTest(db, params) {
|
|
|
226
238
|
/**
|
|
227
239
|
* Setup before each test
|
|
228
240
|
*/
|
|
229
|
-
beforeEach(() => {
|
|
230
|
-
// Create
|
|
231
|
-
|
|
241
|
+
beforeEach(async () => {
|
|
242
|
+
// Create temp directory for test files and database
|
|
243
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sqlew-test-'));
|
|
244
|
+
tempDbPath = path.join(tempDir, 'test.db');
|
|
245
|
+
// Initialize database with Knex adapter
|
|
246
|
+
testDb = await initializeDatabase({
|
|
247
|
+
databaseType: 'sqlite',
|
|
248
|
+
connection: {
|
|
249
|
+
filename: tempDbPath,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
afterEach(async () => {
|
|
254
|
+
await closeDatabase();
|
|
255
|
+
// Cleanup temp directory
|
|
256
|
+
if (fs.existsSync(tempDir)) {
|
|
257
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
258
|
+
}
|
|
232
259
|
});
|
|
233
260
|
// ============================================================================
|
|
234
261
|
// Task #66: Unit Tests for add_dependency Validation
|
|
235
262
|
// ============================================================================
|
|
236
263
|
describe('add_dependency - Success Cases', () => {
|
|
237
|
-
it('should add valid dependency', () => {
|
|
264
|
+
it('should add valid dependency', async () => {
|
|
238
265
|
// Arrange
|
|
239
|
-
const task1 = createTestTask(testDb, 'Task 1');
|
|
240
|
-
const task2 = createTestTask(testDb, 'Task 2');
|
|
266
|
+
const task1 = await createTestTask(testDb, 'Task 1');
|
|
267
|
+
const task2 = await createTestTask(testDb, 'Task 2');
|
|
241
268
|
// Act
|
|
242
|
-
const result = addDependencyTest(testDb, {
|
|
243
|
-
|
|
244
|
-
|
|
269
|
+
const result = await addDependencyTest(testDb, {
|
|
270
|
+
depends_on_task_id: task1,
|
|
271
|
+
task_id: task2
|
|
245
272
|
});
|
|
246
273
|
// Assert
|
|
247
274
|
assert.strictEqual(result.success, true);
|
|
248
275
|
assert.match(result.message, /Dependency added/);
|
|
249
276
|
// Verify in database
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
277
|
+
const knex = testDb.getKnex();
|
|
278
|
+
const deps = await knex('t_task_dependencies')
|
|
279
|
+
.where({
|
|
280
|
+
depends_on_task_id: task1,
|
|
281
|
+
task_id: task2
|
|
282
|
+
})
|
|
283
|
+
.first();
|
|
254
284
|
assert.ok(deps);
|
|
255
285
|
});
|
|
256
|
-
it('should add valid dependency and verify via get_dependencies', () => {
|
|
286
|
+
it('should add valid dependency and verify via get_dependencies', async () => {
|
|
257
287
|
// Arrange
|
|
258
|
-
const task1 = createTestTask(testDb, 'Task 1');
|
|
259
|
-
const task2 = createTestTask(testDb, 'Task 2');
|
|
288
|
+
const task1 = await createTestTask(testDb, 'Task 1');
|
|
289
|
+
const task2 = await createTestTask(testDb, 'Task 2');
|
|
260
290
|
// Act
|
|
261
|
-
addDependencyTest(testDb, {
|
|
262
|
-
|
|
263
|
-
|
|
291
|
+
await addDependencyTest(testDb, {
|
|
292
|
+
depends_on_task_id: task1,
|
|
293
|
+
task_id: task2
|
|
264
294
|
});
|
|
265
|
-
const result = getDependenciesTest(testDb, { task_id: task2 });
|
|
295
|
+
const result = await getDependenciesTest(testDb, { task_id: task2 });
|
|
266
296
|
// Assert
|
|
267
297
|
assert.strictEqual(result.task_id, task2);
|
|
268
298
|
assert.strictEqual(result.blockers.length, 1);
|
|
@@ -271,14 +301,14 @@ describe('add_dependency - Success Cases', () => {
|
|
|
271
301
|
});
|
|
272
302
|
});
|
|
273
303
|
describe('add_dependency - Validation: Self-Dependency', () => {
|
|
274
|
-
it('should reject self-dependency', () => {
|
|
304
|
+
it('should reject self-dependency', async () => {
|
|
275
305
|
// Arrange
|
|
276
|
-
const task1 = createTestTask(testDb, 'Task 1');
|
|
306
|
+
const task1 = await createTestTask(testDb, 'Task 1');
|
|
277
307
|
// Act & Assert
|
|
278
|
-
assert.
|
|
279
|
-
addDependencyTest(testDb, {
|
|
280
|
-
|
|
281
|
-
|
|
308
|
+
await assert.rejects(async () => {
|
|
309
|
+
await addDependencyTest(testDb, {
|
|
310
|
+
depends_on_task_id: task1,
|
|
311
|
+
task_id: task1
|
|
282
312
|
});
|
|
283
313
|
}, {
|
|
284
314
|
message: /Self-dependency not allowed/
|
|
@@ -286,20 +316,20 @@ describe('add_dependency - Validation: Self-Dependency', () => {
|
|
|
286
316
|
});
|
|
287
317
|
});
|
|
288
318
|
describe('add_dependency - Validation: Direct Circular', () => {
|
|
289
|
-
it('should reject direct circular dependency', () => {
|
|
319
|
+
it('should reject direct circular dependency', async () => {
|
|
290
320
|
// Arrange
|
|
291
|
-
const taskA = createTestTask(testDb, 'Task A');
|
|
292
|
-
const taskB = createTestTask(testDb, 'Task B');
|
|
293
|
-
// Add A blocks B
|
|
294
|
-
addDependencyTest(testDb, {
|
|
295
|
-
|
|
296
|
-
|
|
321
|
+
const taskA = await createTestTask(testDb, 'Task A');
|
|
322
|
+
const taskB = await createTestTask(testDb, 'Task B');
|
|
323
|
+
// Add A blocks B (B depends on A)
|
|
324
|
+
await addDependencyTest(testDb, {
|
|
325
|
+
depends_on_task_id: taskA,
|
|
326
|
+
task_id: taskB
|
|
297
327
|
});
|
|
298
|
-
// Act & Assert - Try to add B blocks A
|
|
299
|
-
assert.
|
|
300
|
-
addDependencyTest(testDb, {
|
|
301
|
-
|
|
302
|
-
|
|
328
|
+
// Act & Assert - Try to add B blocks A (A depends on B)
|
|
329
|
+
await assert.rejects(async () => {
|
|
330
|
+
await addDependencyTest(testDb, {
|
|
331
|
+
depends_on_task_id: taskB,
|
|
332
|
+
task_id: taskA
|
|
303
333
|
});
|
|
304
334
|
}, {
|
|
305
335
|
message: /Circular dependency detected/
|
|
@@ -307,51 +337,51 @@ describe('add_dependency - Validation: Direct Circular', () => {
|
|
|
307
337
|
});
|
|
308
338
|
});
|
|
309
339
|
describe('add_dependency - Validation: Transitive Circular', () => {
|
|
310
|
-
it('should reject transitive circular dependency (A→B→C→A)', () => {
|
|
340
|
+
it('should reject transitive circular dependency (A→B→C→A)', async () => {
|
|
311
341
|
// Arrange
|
|
312
|
-
const taskA = createTestTask(testDb, 'Task A');
|
|
313
|
-
const taskB = createTestTask(testDb, 'Task B');
|
|
314
|
-
const taskC = createTestTask(testDb, 'Task C');
|
|
315
|
-
// Add A blocks B
|
|
316
|
-
addDependencyTest(testDb, {
|
|
317
|
-
|
|
318
|
-
|
|
342
|
+
const taskA = await createTestTask(testDb, 'Task A');
|
|
343
|
+
const taskB = await createTestTask(testDb, 'Task B');
|
|
344
|
+
const taskC = await createTestTask(testDb, 'Task C');
|
|
345
|
+
// Add A blocks B (B depends on A)
|
|
346
|
+
await addDependencyTest(testDb, {
|
|
347
|
+
depends_on_task_id: taskA,
|
|
348
|
+
task_id: taskB
|
|
319
349
|
});
|
|
320
|
-
// Add B blocks C
|
|
321
|
-
addDependencyTest(testDb, {
|
|
322
|
-
|
|
323
|
-
|
|
350
|
+
// Add B blocks C (C depends on B)
|
|
351
|
+
await addDependencyTest(testDb, {
|
|
352
|
+
depends_on_task_id: taskB,
|
|
353
|
+
task_id: taskC
|
|
324
354
|
});
|
|
325
|
-
// Act & Assert - Try to add C blocks A (would create cycle)
|
|
326
|
-
assert.
|
|
327
|
-
addDependencyTest(testDb, {
|
|
328
|
-
|
|
329
|
-
|
|
355
|
+
// Act & Assert - Try to add C blocks A (A depends on C, would create cycle)
|
|
356
|
+
await assert.rejects(async () => {
|
|
357
|
+
await addDependencyTest(testDb, {
|
|
358
|
+
depends_on_task_id: taskC,
|
|
359
|
+
task_id: taskA
|
|
330
360
|
});
|
|
331
361
|
}, {
|
|
332
362
|
message: /Circular dependency detected/
|
|
333
363
|
});
|
|
334
364
|
});
|
|
335
|
-
it('should reject transitive circular with cycle path in error message', () => {
|
|
365
|
+
it('should reject transitive circular with cycle path in error message', async () => {
|
|
336
366
|
// Arrange
|
|
337
|
-
const task1 = createTestTask(testDb, 'Task 1');
|
|
338
|
-
const task2 = createTestTask(testDb, 'Task 2');
|
|
339
|
-
const task3 = createTestTask(testDb, 'Task 3');
|
|
340
|
-
// Create chain: 1 → 2 → 3
|
|
341
|
-
addDependencyTest(testDb, {
|
|
342
|
-
|
|
343
|
-
|
|
367
|
+
const task1 = await createTestTask(testDb, 'Task 1');
|
|
368
|
+
const task2 = await createTestTask(testDb, 'Task 2');
|
|
369
|
+
const task3 = await createTestTask(testDb, 'Task 3');
|
|
370
|
+
// Create chain: 1 → 2 → 3 (2 depends on 1, 3 depends on 2)
|
|
371
|
+
await addDependencyTest(testDb, {
|
|
372
|
+
depends_on_task_id: task1,
|
|
373
|
+
task_id: task2
|
|
344
374
|
});
|
|
345
|
-
addDependencyTest(testDb, {
|
|
346
|
-
|
|
347
|
-
|
|
375
|
+
await addDependencyTest(testDb, {
|
|
376
|
+
depends_on_task_id: task2,
|
|
377
|
+
task_id: task3
|
|
348
378
|
});
|
|
349
|
-
// Act & Assert - Try to add 3 → 1
|
|
379
|
+
// Act & Assert - Try to add 3 → 1 (1 depends on 3)
|
|
350
380
|
let errorMessage = '';
|
|
351
381
|
try {
|
|
352
|
-
addDependencyTest(testDb, {
|
|
353
|
-
|
|
354
|
-
|
|
382
|
+
await addDependencyTest(testDb, {
|
|
383
|
+
depends_on_task_id: task3,
|
|
384
|
+
task_id: task1
|
|
355
385
|
});
|
|
356
386
|
}
|
|
357
387
|
catch (error) {
|
|
@@ -366,57 +396,57 @@ describe('add_dependency - Validation: Transitive Circular', () => {
|
|
|
366
396
|
});
|
|
367
397
|
});
|
|
368
398
|
describe('add_dependency - Validation: Non-Existent Tasks', () => {
|
|
369
|
-
it('should reject dependency with non-existent blocker', () => {
|
|
399
|
+
it('should reject dependency with non-existent blocker', async () => {
|
|
370
400
|
// Arrange
|
|
371
|
-
const task1 = createTestTask(testDb, 'Task 1');
|
|
401
|
+
const task1 = await createTestTask(testDb, 'Task 1');
|
|
372
402
|
// Act & Assert
|
|
373
|
-
assert.
|
|
374
|
-
addDependencyTest(testDb, {
|
|
375
|
-
|
|
376
|
-
|
|
403
|
+
await assert.rejects(async () => {
|
|
404
|
+
await addDependencyTest(testDb, {
|
|
405
|
+
depends_on_task_id: 999,
|
|
406
|
+
task_id: task1
|
|
377
407
|
});
|
|
378
408
|
}, {
|
|
379
|
-
message: /
|
|
409
|
+
message: /Task #999 not found/
|
|
380
410
|
});
|
|
381
411
|
});
|
|
382
|
-
it('should reject dependency with non-existent blocked', () => {
|
|
412
|
+
it('should reject dependency with non-existent blocked', async () => {
|
|
383
413
|
// Arrange
|
|
384
|
-
const task1 = createTestTask(testDb, 'Task 1');
|
|
414
|
+
const task1 = await createTestTask(testDb, 'Task 1');
|
|
385
415
|
// Act & Assert
|
|
386
|
-
assert.
|
|
387
|
-
addDependencyTest(testDb, {
|
|
388
|
-
|
|
389
|
-
|
|
416
|
+
await assert.rejects(async () => {
|
|
417
|
+
await addDependencyTest(testDb, {
|
|
418
|
+
depends_on_task_id: task1,
|
|
419
|
+
task_id: 999
|
|
390
420
|
});
|
|
391
421
|
}, {
|
|
392
|
-
message: /
|
|
422
|
+
message: /Task #999 not found/
|
|
393
423
|
});
|
|
394
424
|
});
|
|
395
425
|
});
|
|
396
426
|
describe('add_dependency - Validation: Archived Tasks', () => {
|
|
397
|
-
it('should reject dependency with archived blocker', () => {
|
|
427
|
+
it('should reject dependency with archived blocker', async () => {
|
|
398
428
|
// Arrange
|
|
399
|
-
const task1 = createTestTask(testDb, 'Task 1', 'archived');
|
|
400
|
-
const task2 = createTestTask(testDb, 'Task 2');
|
|
429
|
+
const task1 = await createTestTask(testDb, 'Task 1', 'archived');
|
|
430
|
+
const task2 = await createTestTask(testDb, 'Task 2');
|
|
401
431
|
// Act & Assert
|
|
402
|
-
assert.
|
|
403
|
-
addDependencyTest(testDb, {
|
|
404
|
-
|
|
405
|
-
|
|
432
|
+
await assert.rejects(async () => {
|
|
433
|
+
await addDependencyTest(testDb, {
|
|
434
|
+
depends_on_task_id: task1,
|
|
435
|
+
task_id: task2
|
|
406
436
|
});
|
|
407
437
|
}, {
|
|
408
438
|
message: /Cannot add dependency: Task #\d+ is archived/
|
|
409
439
|
});
|
|
410
440
|
});
|
|
411
|
-
it('should reject dependency with archived blocked', () => {
|
|
441
|
+
it('should reject dependency with archived blocked', async () => {
|
|
412
442
|
// Arrange
|
|
413
|
-
const task1 = createTestTask(testDb, 'Task 1');
|
|
414
|
-
const task2 = createTestTask(testDb, 'Task 2', 'archived');
|
|
443
|
+
const task1 = await createTestTask(testDb, 'Task 1');
|
|
444
|
+
const task2 = await createTestTask(testDb, 'Task 2', 'archived');
|
|
415
445
|
// Act & Assert
|
|
416
|
-
assert.
|
|
417
|
-
addDependencyTest(testDb, {
|
|
418
|
-
|
|
419
|
-
|
|
446
|
+
await assert.rejects(async () => {
|
|
447
|
+
await addDependencyTest(testDb, {
|
|
448
|
+
depends_on_task_id: task1,
|
|
449
|
+
task_id: task2
|
|
420
450
|
});
|
|
421
451
|
}, {
|
|
422
452
|
message: /Cannot add dependency: Task #\d+ is archived/
|
|
@@ -427,37 +457,37 @@ describe('add_dependency - Validation: Archived Tasks', () => {
|
|
|
427
457
|
// Task #67: Unit Tests for remove_dependency and get_dependencies
|
|
428
458
|
// ============================================================================
|
|
429
459
|
describe('remove_dependency', () => {
|
|
430
|
-
it('should remove existing dependency', () => {
|
|
460
|
+
it('should remove existing dependency', async () => {
|
|
431
461
|
// Arrange
|
|
432
|
-
const task1 = createTestTask(testDb, 'Task 1');
|
|
433
|
-
const task2 = createTestTask(testDb, 'Task 2');
|
|
434
|
-
addDependencyTest(testDb, {
|
|
435
|
-
|
|
436
|
-
|
|
462
|
+
const task1 = await createTestTask(testDb, 'Task 1');
|
|
463
|
+
const task2 = await createTestTask(testDb, 'Task 2');
|
|
464
|
+
await addDependencyTest(testDb, {
|
|
465
|
+
depends_on_task_id: task1,
|
|
466
|
+
task_id: task2
|
|
437
467
|
});
|
|
438
468
|
// Verify it exists
|
|
439
|
-
const beforeDeps = getDependenciesTest(testDb, { task_id: task2 });
|
|
469
|
+
const beforeDeps = await getDependenciesTest(testDb, { task_id: task2 });
|
|
440
470
|
assert.strictEqual(beforeDeps.blockers.length, 1);
|
|
441
471
|
// Act
|
|
442
|
-
const result = removeDependencyTest(testDb, {
|
|
443
|
-
|
|
444
|
-
|
|
472
|
+
const result = await removeDependencyTest(testDb, {
|
|
473
|
+
depends_on_task_id: task1,
|
|
474
|
+
task_id: task2
|
|
445
475
|
});
|
|
446
476
|
// Assert
|
|
447
477
|
assert.strictEqual(result.success, true);
|
|
448
478
|
assert.match(result.message, /Dependency removed/);
|
|
449
479
|
// Verify it no longer exists
|
|
450
|
-
const afterDeps = getDependenciesTest(testDb, { task_id: task2 });
|
|
480
|
+
const afterDeps = await getDependenciesTest(testDb, { task_id: task2 });
|
|
451
481
|
assert.strictEqual(afterDeps.blockers.length, 0);
|
|
452
482
|
});
|
|
453
|
-
it('should succeed silently when removing non-existent dependency (idempotent)', () => {
|
|
483
|
+
it('should succeed silently when removing non-existent dependency (idempotent)', async () => {
|
|
454
484
|
// Arrange
|
|
455
|
-
const task1 = createTestTask(testDb, 'Task 1');
|
|
456
|
-
const task2 = createTestTask(testDb, 'Task 2');
|
|
485
|
+
const task1 = await createTestTask(testDb, 'Task 1');
|
|
486
|
+
const task2 = await createTestTask(testDb, 'Task 2');
|
|
457
487
|
// Act - Remove dependency that doesn't exist
|
|
458
|
-
const result = removeDependencyTest(testDb, {
|
|
459
|
-
|
|
460
|
-
|
|
488
|
+
const result = await removeDependencyTest(testDb, {
|
|
489
|
+
depends_on_task_id: task1,
|
|
490
|
+
task_id: task2
|
|
461
491
|
});
|
|
462
492
|
// Assert
|
|
463
493
|
assert.strictEqual(result.success, true);
|
|
@@ -465,22 +495,22 @@ describe('remove_dependency', () => {
|
|
|
465
495
|
});
|
|
466
496
|
});
|
|
467
497
|
describe('get_dependencies - Metadata Only', () => {
|
|
468
|
-
it('should return blockers and blocking (metadata-only)', () => {
|
|
498
|
+
it('should return blockers and blocking (metadata-only)', async () => {
|
|
469
499
|
// Arrange
|
|
470
|
-
const taskA = createTestTask(testDb, 'Task A');
|
|
471
|
-
const taskB = createTestTask(testDb, 'Task B');
|
|
472
|
-
const taskC = createTestTask(testDb, 'Task C');
|
|
473
|
-
// A blocks B, B blocks C
|
|
474
|
-
addDependencyTest(testDb, {
|
|
475
|
-
|
|
476
|
-
|
|
500
|
+
const taskA = await createTestTask(testDb, 'Task A');
|
|
501
|
+
const taskB = await createTestTask(testDb, 'Task B');
|
|
502
|
+
const taskC = await createTestTask(testDb, 'Task C');
|
|
503
|
+
// A blocks B, B blocks C (B depends on A, C depends on B)
|
|
504
|
+
await addDependencyTest(testDb, {
|
|
505
|
+
depends_on_task_id: taskA,
|
|
506
|
+
task_id: taskB
|
|
477
507
|
});
|
|
478
|
-
addDependencyTest(testDb, {
|
|
479
|
-
|
|
480
|
-
|
|
508
|
+
await addDependencyTest(testDb, {
|
|
509
|
+
depends_on_task_id: taskB,
|
|
510
|
+
task_id: taskC
|
|
481
511
|
});
|
|
482
512
|
// Act - Get dependencies for B
|
|
483
|
-
const result = getDependenciesTest(testDb, { task_id: taskB });
|
|
513
|
+
const result = await getDependenciesTest(testDb, { task_id: taskB });
|
|
484
514
|
// Assert
|
|
485
515
|
assert.strictEqual(result.task_id, taskB);
|
|
486
516
|
// B is blocked by A
|
|
@@ -495,11 +525,11 @@ describe('get_dependencies - Metadata Only', () => {
|
|
|
495
525
|
assert.strictEqual(result.blockers[0].description, undefined);
|
|
496
526
|
assert.strictEqual(result.blocking[0].description, undefined);
|
|
497
527
|
});
|
|
498
|
-
it('should return empty arrays for task with no dependencies', () => {
|
|
528
|
+
it('should return empty arrays for task with no dependencies', async () => {
|
|
499
529
|
// Arrange
|
|
500
|
-
const task1 = createTestTask(testDb, 'Task 1');
|
|
530
|
+
const task1 = await createTestTask(testDb, 'Task 1');
|
|
501
531
|
// Act
|
|
502
|
-
const result = getDependenciesTest(testDb, { task_id: task1 });
|
|
532
|
+
const result = await getDependenciesTest(testDb, { task_id: task1 });
|
|
503
533
|
// Assert
|
|
504
534
|
assert.strictEqual(result.task_id, task1);
|
|
505
535
|
assert.strictEqual(result.blockers.length, 0);
|
|
@@ -507,22 +537,23 @@ describe('get_dependencies - Metadata Only', () => {
|
|
|
507
537
|
});
|
|
508
538
|
});
|
|
509
539
|
describe('get_dependencies - With Details', () => {
|
|
510
|
-
it('should return full details when include_details=true', () => {
|
|
540
|
+
it('should return full details when include_details=true', async () => {
|
|
511
541
|
// Arrange
|
|
512
|
-
const task1 = createTestTask(testDb, 'Task 1');
|
|
513
|
-
const task2 = createTestTask(testDb, 'Task 2');
|
|
542
|
+
const task1 = await createTestTask(testDb, 'Task 1');
|
|
543
|
+
const task2 = await createTestTask(testDb, 'Task 2');
|
|
514
544
|
// Add description to task1
|
|
515
|
-
testDb.
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
545
|
+
const knex = testDb.getKnex();
|
|
546
|
+
await knex('t_task_details').insert({
|
|
547
|
+
task_id: task1,
|
|
548
|
+
description: 'This is task 1 description'
|
|
549
|
+
});
|
|
550
|
+
// Add dependency (task2 depends on task1)
|
|
551
|
+
await addDependencyTest(testDb, {
|
|
552
|
+
depends_on_task_id: task1,
|
|
553
|
+
task_id: task2
|
|
523
554
|
});
|
|
524
555
|
// Act
|
|
525
|
-
const result = getDependenciesTest(testDb, {
|
|
556
|
+
const result = await getDependenciesTest(testDb, {
|
|
526
557
|
task_id: task2,
|
|
527
558
|
include_details: true
|
|
528
559
|
});
|
|
@@ -531,81 +562,81 @@ describe('get_dependencies - With Details', () => {
|
|
|
531
562
|
assert.strictEqual(result.blockers[0].id, task1);
|
|
532
563
|
assert.strictEqual(result.blockers[0].description, 'This is task 1 description');
|
|
533
564
|
});
|
|
534
|
-
it('should not include description by default', () => {
|
|
565
|
+
it('should not include description by default', async () => {
|
|
535
566
|
// Arrange
|
|
536
|
-
const task1 = createTestTask(testDb, 'Task 1');
|
|
537
|
-
const task2 = createTestTask(testDb, 'Task 2');
|
|
567
|
+
const task1 = await createTestTask(testDb, 'Task 1');
|
|
568
|
+
const task2 = await createTestTask(testDb, 'Task 2');
|
|
538
569
|
// Add description
|
|
539
|
-
testDb.
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
570
|
+
const knex = testDb.getKnex();
|
|
571
|
+
await knex('t_task_details').insert({
|
|
572
|
+
task_id: task1,
|
|
573
|
+
description: 'This is task 1 description'
|
|
574
|
+
});
|
|
575
|
+
await addDependencyTest(testDb, {
|
|
576
|
+
depends_on_task_id: task1,
|
|
577
|
+
task_id: task2
|
|
546
578
|
});
|
|
547
579
|
// Act
|
|
548
|
-
const result = getDependenciesTest(testDb, { task_id: task2 });
|
|
580
|
+
const result = await getDependenciesTest(testDb, { task_id: task2 });
|
|
549
581
|
// Assert
|
|
550
582
|
assert.strictEqual(result.blockers[0].description, undefined);
|
|
551
583
|
});
|
|
552
584
|
});
|
|
553
585
|
describe('get_dependencies - Error Handling', () => {
|
|
554
|
-
it('should throw error for non-existent task', () => {
|
|
586
|
+
it('should throw error for non-existent task', async () => {
|
|
555
587
|
// Act & Assert
|
|
556
|
-
assert.
|
|
557
|
-
getDependenciesTest(testDb, { task_id: 999 });
|
|
588
|
+
await assert.rejects(async () => {
|
|
589
|
+
await getDependenciesTest(testDb, { task_id: 999 });
|
|
558
590
|
}, {
|
|
559
591
|
message: /Task with id 999 not found/
|
|
560
592
|
});
|
|
561
593
|
});
|
|
562
594
|
});
|
|
563
595
|
describe('CASCADE Deletion', () => {
|
|
564
|
-
it('should cascade delete dependencies when task deleted', () => {
|
|
596
|
+
it('should cascade delete dependencies when task deleted', async () => {
|
|
565
597
|
// Arrange
|
|
566
|
-
const taskA = createTestTask(testDb, 'Task A');
|
|
567
|
-
const taskB = createTestTask(testDb, 'Task B');
|
|
568
|
-
// Add A blocks B
|
|
569
|
-
addDependencyTest(testDb, {
|
|
570
|
-
|
|
571
|
-
|
|
598
|
+
const taskA = await createTestTask(testDb, 'Task A');
|
|
599
|
+
const taskB = await createTestTask(testDb, 'Task B');
|
|
600
|
+
// Add A blocks B (B depends on A)
|
|
601
|
+
await addDependencyTest(testDb, {
|
|
602
|
+
depends_on_task_id: taskA,
|
|
603
|
+
task_id: taskB
|
|
572
604
|
});
|
|
573
605
|
// Verify dependency exists
|
|
574
|
-
const beforeDeps = getDependenciesTest(testDb, { task_id: taskB });
|
|
606
|
+
const beforeDeps = await getDependenciesTest(testDb, { task_id: taskB });
|
|
575
607
|
assert.strictEqual(beforeDeps.blockers.length, 1);
|
|
576
608
|
// Act - Delete task A
|
|
577
|
-
|
|
609
|
+
const knex = testDb.getKnex();
|
|
610
|
+
await knex('t_tasks').where({ id: taskA }).delete();
|
|
578
611
|
// Assert - Dependency should be deleted
|
|
579
|
-
const depsInDb =
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
`).all(taskA, taskA);
|
|
612
|
+
const depsInDb = await knex('t_task_dependencies')
|
|
613
|
+
.where('depends_on_task_id', taskA)
|
|
614
|
+
.orWhere('task_id', taskA);
|
|
583
615
|
assert.strictEqual(depsInDb.length, 0);
|
|
584
616
|
// Verify B still exists
|
|
585
|
-
const taskBExists =
|
|
617
|
+
const taskBExists = await knex('t_tasks').where({ id: taskB }).first();
|
|
586
618
|
assert.ok(taskBExists);
|
|
587
619
|
// Verify B has no dependencies anymore
|
|
588
|
-
const afterDeps = getDependenciesTest(testDb, { task_id: taskB });
|
|
620
|
+
const afterDeps = await getDependenciesTest(testDb, { task_id: taskB });
|
|
589
621
|
assert.strictEqual(afterDeps.blockers.length, 0);
|
|
590
622
|
});
|
|
591
|
-
it('should cascade delete when blocked task is deleted', () => {
|
|
623
|
+
it('should cascade delete when blocked task is deleted', async () => {
|
|
592
624
|
// Arrange
|
|
593
|
-
const taskA = createTestTask(testDb, 'Task A');
|
|
594
|
-
const taskB = createTestTask(testDb, 'Task B');
|
|
595
|
-
addDependencyTest(testDb, {
|
|
596
|
-
|
|
597
|
-
|
|
625
|
+
const taskA = await createTestTask(testDb, 'Task A');
|
|
626
|
+
const taskB = await createTestTask(testDb, 'Task B');
|
|
627
|
+
await addDependencyTest(testDb, {
|
|
628
|
+
depends_on_task_id: taskA,
|
|
629
|
+
task_id: taskB
|
|
598
630
|
});
|
|
599
631
|
// Act - Delete task B
|
|
600
|
-
|
|
632
|
+
const knex = testDb.getKnex();
|
|
633
|
+
await knex('t_tasks').where({ id: taskB }).delete();
|
|
601
634
|
// Assert - Dependency should be deleted
|
|
602
|
-
const depsInDb =
|
|
603
|
-
|
|
604
|
-
WHERE blocked_task_id = ?
|
|
605
|
-
`).all(taskB);
|
|
635
|
+
const depsInDb = await knex('t_task_dependencies')
|
|
636
|
+
.where('task_id', taskB);
|
|
606
637
|
assert.strictEqual(depsInDb.length, 0);
|
|
607
638
|
// Verify A still exists with no dependencies
|
|
608
|
-
const afterDeps = getDependenciesTest(testDb, { task_id: taskA });
|
|
639
|
+
const afterDeps = await getDependenciesTest(testDb, { task_id: taskA });
|
|
609
640
|
assert.strictEqual(afterDeps.blocking.length, 0);
|
|
610
641
|
});
|
|
611
642
|
});
|