add pql subsystem — daemon-side client + IPC handlers
PqlClient wraps pql CLI per D-003 (shell-out, never duplicate). IPC verbs: pql.files, pql.meta, pql.backlinks, pql.outlinks, pql.tags, pql.schema, pql.query, pql.doctor, pql.decisions.*, pql.tickets.*, pql.plan.status. 15 new core tests. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -20,8 +20,10 @@ import 'package:clide/src/daemon/editor_commands.dart';
|
|||||||
import 'package:clide/src/daemon/files_commands.dart';
|
import 'package:clide/src/daemon/files_commands.dart';
|
||||||
import 'package:clide/src/daemon/git_commands.dart';
|
import 'package:clide/src/daemon/git_commands.dart';
|
||||||
import 'package:clide/src/daemon/pane_commands.dart';
|
import 'package:clide/src/daemon/pane_commands.dart';
|
||||||
|
import 'package:clide/src/daemon/pql_commands.dart';
|
||||||
import 'package:clide/src/editor/registry.dart' show EditorRegistry;
|
import 'package:clide/src/editor/registry.dart' show EditorRegistry;
|
||||||
import 'package:clide/src/panes/registry.dart';
|
import 'package:clide/src/panes/registry.dart';
|
||||||
|
import 'package:clide/src/pql/client.dart';
|
||||||
|
|
||||||
Future<void> main(List<String> argv) async {
|
Future<void> main(List<String> argv) async {
|
||||||
if (argv.isEmpty) {
|
if (argv.isEmpty) {
|
||||||
@@ -152,6 +154,9 @@ Future<void> _runDaemon(List<String> args) async {
|
|||||||
|
|
||||||
registerGitCommands(dispatcher, files.root, events);
|
registerGitCommands(dispatcher, files.root, events);
|
||||||
|
|
||||||
|
final pql = PqlClient(workDir: files.root);
|
||||||
|
registerPqlCommands(dispatcher, pql);
|
||||||
|
|
||||||
final stopping = Completer<void>();
|
final stopping = Completer<void>();
|
||||||
void shutdown(ProcessSignal sig) {
|
void shutdown(ProcessSignal sig) {
|
||||||
if (!stopping.isCompleted) {
|
if (!stopping.isCompleted) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export 'src/git/diff.dart' show GitDiff, GitHunk, DiffLine, DiffLineKind;
|
|||||||
export 'src/git/operations.dart' show GitLogEntry, GitException;
|
export 'src/git/operations.dart' show GitLogEntry, GitException;
|
||||||
export 'src/git/status.dart'
|
export 'src/git/status.dart'
|
||||||
show GitStatus, GitFileStatus, GitFileState, GitConflictType;
|
show GitStatus, GitFileStatus, GitFileState, GitConflictType;
|
||||||
|
export 'src/pql/client.dart' show PqlClient, PqlException;
|
||||||
export 'src/ipc/envelope.dart';
|
export 'src/ipc/envelope.dart';
|
||||||
export 'src/ipc/paths.dart';
|
export 'src/ipc/paths.dart';
|
||||||
export 'src/ipc/schema_v1.dart';
|
export 'src/ipc/schema_v1.dart';
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
/// Registers `pql.*` command handlers on the daemon dispatcher.
|
||||||
|
///
|
||||||
|
/// Thin pass-through to [PqlClient] — the daemon is a JSON relay
|
||||||
|
/// between the IPC socket and the pql CLI. Per D-003, clide wraps
|
||||||
|
/// pql; it never re-implements.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import '../ipc/envelope.dart';
|
||||||
|
import '../ipc/schema_v1.dart';
|
||||||
|
import '../pql/client.dart';
|
||||||
|
import 'dispatcher.dart';
|
||||||
|
|
||||||
|
void registerPqlCommands(DaemonDispatcher d, PqlClient pql) {
|
||||||
|
d.register('pql.files', (req) async {
|
||||||
|
try {
|
||||||
|
final glob = req.args['glob'] as String?;
|
||||||
|
final limit = (req.args['limit'] as num?)?.toInt();
|
||||||
|
final files = await pql.files(glob: glob, limit: limit);
|
||||||
|
return IpcResponse.ok(id: req.id, data: {'files': files});
|
||||||
|
} on PqlException catch (e) {
|
||||||
|
return _pqlError(req.id, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
d.register('pql.meta', (req) async {
|
||||||
|
final path = req.args['path'] as String?;
|
||||||
|
if (path == null || path.isEmpty) {
|
||||||
|
return _userError(req.id, 'pql.meta requires a path');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final meta = await pql.meta(path);
|
||||||
|
return IpcResponse.ok(id: req.id, data: meta);
|
||||||
|
} on PqlException catch (e) {
|
||||||
|
return _pqlError(req.id, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
d.register('pql.backlinks', (req) async {
|
||||||
|
final path = req.args['path'] as String?;
|
||||||
|
if (path == null || path.isEmpty) {
|
||||||
|
return _userError(req.id, 'pql.backlinks requires a path');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final links = await pql.backlinks(path);
|
||||||
|
return IpcResponse.ok(id: req.id, data: {'links': links});
|
||||||
|
} on PqlException catch (e) {
|
||||||
|
return _pqlError(req.id, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
d.register('pql.outlinks', (req) async {
|
||||||
|
final path = req.args['path'] as String?;
|
||||||
|
if (path == null || path.isEmpty) {
|
||||||
|
return _userError(req.id, 'pql.outlinks requires a path');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final links = await pql.outlinks(path);
|
||||||
|
return IpcResponse.ok(id: req.id, data: {'links': links});
|
||||||
|
} on PqlException catch (e) {
|
||||||
|
return _pqlError(req.id, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
d.register('pql.tags', (req) async {
|
||||||
|
try {
|
||||||
|
final limit = (req.args['limit'] as num?)?.toInt();
|
||||||
|
final tags = await pql.tags(limit: limit);
|
||||||
|
return IpcResponse.ok(id: req.id, data: {'tags': tags});
|
||||||
|
} on PqlException catch (e) {
|
||||||
|
return _pqlError(req.id, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
d.register('pql.schema', (req) async {
|
||||||
|
try {
|
||||||
|
final schema = await pql.schema();
|
||||||
|
return IpcResponse.ok(id: req.id, data: {'schema': schema});
|
||||||
|
} on PqlException catch (e) {
|
||||||
|
return _pqlError(req.id, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
d.register('pql.query', (req) async {
|
||||||
|
final dsl = req.args['query'] as String?;
|
||||||
|
if (dsl == null || dsl.isEmpty) {
|
||||||
|
return _userError(req.id, 'pql.query requires a query string');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final limit = (req.args['limit'] as num?)?.toInt();
|
||||||
|
final results = await pql.query(dsl, limit: limit);
|
||||||
|
return IpcResponse.ok(id: req.id, data: {'results': results});
|
||||||
|
} on PqlException catch (e) {
|
||||||
|
return _pqlError(req.id, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
d.register('pql.doctor', (req) async {
|
||||||
|
try {
|
||||||
|
final report = await pql.doctor();
|
||||||
|
return IpcResponse.ok(id: req.id, data: report);
|
||||||
|
} on PqlException catch (e) {
|
||||||
|
return _pqlError(req.id, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
d.register('pql.decisions.sync', (req) async {
|
||||||
|
try {
|
||||||
|
final result = await pql.decisionSync();
|
||||||
|
return IpcResponse.ok(id: req.id, data: result);
|
||||||
|
} on PqlException catch (e) {
|
||||||
|
return _pqlError(req.id, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
d.register('pql.decisions.list', (req) async {
|
||||||
|
try {
|
||||||
|
final results = await pql.decisionList(
|
||||||
|
type: req.args['type'] as String?,
|
||||||
|
domain: req.args['domain'] as String?,
|
||||||
|
status: req.args['status'] as String?,
|
||||||
|
);
|
||||||
|
return IpcResponse.ok(id: req.id, data: {'decisions': results});
|
||||||
|
} on PqlException catch (e) {
|
||||||
|
return _pqlError(req.id, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
d.register('pql.decisions.show', (req) async {
|
||||||
|
final id = req.args['id'] as String?;
|
||||||
|
if (id == null || id.isEmpty) {
|
||||||
|
return _userError(req.id, 'pql.decisions.show requires an id');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final result = await pql.decisionShow(
|
||||||
|
id,
|
||||||
|
withRefs: req.args['withRefs'] as bool? ?? false,
|
||||||
|
withTickets: req.args['withTickets'] as bool? ?? false,
|
||||||
|
);
|
||||||
|
return IpcResponse.ok(id: req.id, data: result);
|
||||||
|
} on PqlException catch (e) {
|
||||||
|
return _pqlError(req.id, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
d.register('pql.decisions.coverage', (req) async {
|
||||||
|
try {
|
||||||
|
final gaps = await pql.decisionCoverage();
|
||||||
|
return IpcResponse.ok(id: req.id, data: {'gaps': gaps});
|
||||||
|
} on PqlException catch (e) {
|
||||||
|
return _pqlError(req.id, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
d.register('pql.tickets.list', (req) async {
|
||||||
|
try {
|
||||||
|
final results = await pql.ticketList(
|
||||||
|
status: req.args['status'] as String?,
|
||||||
|
team: req.args['team'] as String?,
|
||||||
|
assigned: req.args['assigned'] as String?,
|
||||||
|
decision: req.args['decision'] as String?,
|
||||||
|
);
|
||||||
|
return IpcResponse.ok(id: req.id, data: {'tickets': results});
|
||||||
|
} on PqlException catch (e) {
|
||||||
|
return _pqlError(req.id, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
d.register('pql.tickets.show', (req) async {
|
||||||
|
final id = req.args['id'] as String?;
|
||||||
|
if (id == null || id.isEmpty) {
|
||||||
|
return _userError(req.id, 'pql.tickets.show requires an id');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final result = await pql.ticketShow(
|
||||||
|
id,
|
||||||
|
withDecision: req.args['withDecision'] as bool? ?? false,
|
||||||
|
withBlockers: req.args['withBlockers'] as bool? ?? false,
|
||||||
|
);
|
||||||
|
return IpcResponse.ok(id: req.id, data: result);
|
||||||
|
} on PqlException catch (e) {
|
||||||
|
return _pqlError(req.id, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
d.register('pql.tickets.board', (req) async {
|
||||||
|
try {
|
||||||
|
final board = await pql.ticketBoard(
|
||||||
|
team: req.args['team'] as String?,
|
||||||
|
);
|
||||||
|
return IpcResponse.ok(id: req.id, data: {'columns': board});
|
||||||
|
} on PqlException catch (e) {
|
||||||
|
return _pqlError(req.id, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
d.register('pql.plan.status', (req) async {
|
||||||
|
try {
|
||||||
|
final status = await pql.planStatus();
|
||||||
|
return IpcResponse.ok(id: req.id, data: status);
|
||||||
|
} on PqlException catch (e) {
|
||||||
|
return _pqlError(req.id, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcResponse _userError(String id, String message) {
|
||||||
|
return IpcResponse.err(
|
||||||
|
id: id,
|
||||||
|
error: IpcError(
|
||||||
|
code: IpcExitCode.userError,
|
||||||
|
kind: IpcErrorKind.userError,
|
||||||
|
message: message,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcResponse _pqlError(String id, PqlException e) {
|
||||||
|
return IpcResponse.err(
|
||||||
|
id: id,
|
||||||
|
error: IpcError(
|
||||||
|
code: IpcExitCode.toolError,
|
||||||
|
kind: IpcErrorKind.toolError,
|
||||||
|
message: e.message,
|
||||||
|
hint: e.stderr.isNotEmpty ? e.stderr : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
/// pql CLI wrapper — shells out per D-003 (wrap, don't duplicate).
|
||||||
|
///
|
||||||
|
/// Every function runs `pql <subcommand>` as a subprocess, parses
|
||||||
|
/// the JSON stdout, and returns typed Dart maps. Errors surface as
|
||||||
|
/// [PqlException] with the stderr diagnostics attached.
|
||||||
|
library;
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
class PqlException implements Exception {
|
||||||
|
const PqlException(this.message, {this.exitCode = 1, this.stderr = ''});
|
||||||
|
final String message;
|
||||||
|
final int exitCode;
|
||||||
|
final String stderr;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'PqlException($exitCode): $message';
|
||||||
|
}
|
||||||
|
|
||||||
|
class PqlClient {
|
||||||
|
PqlClient({required this.workDir, this.pqlBinary = 'pql'});
|
||||||
|
|
||||||
|
final Directory workDir;
|
||||||
|
final String pqlBinary;
|
||||||
|
|
||||||
|
Future<List<Map<String, Object?>>> files({String? glob, int? limit}) async {
|
||||||
|
final args = ['files'];
|
||||||
|
if (glob != null) args.add(glob);
|
||||||
|
if (limit != null) args.addAll(['--limit', '$limit']);
|
||||||
|
return _runList(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, Object?>> meta(String path) async {
|
||||||
|
return _runObject(['meta', path]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Map<String, Object?>>> backlinks(String path) async {
|
||||||
|
return _runList(['backlinks', path]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Map<String, Object?>>> outlinks(String path) async {
|
||||||
|
return _runList(['outlinks', path]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Map<String, Object?>>> tags({int? limit}) async {
|
||||||
|
final args = ['tags'];
|
||||||
|
if (limit != null) args.addAll(['--limit', '$limit']);
|
||||||
|
return _runList(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Map<String, Object?>>> schema() async {
|
||||||
|
return _runList(['schema']);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Map<String, Object?>>> query(String dsl, {int? limit}) async {
|
||||||
|
final args = ['query', dsl];
|
||||||
|
if (limit != null) args.addAll(['--limit', '$limit']);
|
||||||
|
return _runList(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, Object?>> doctor() async {
|
||||||
|
return _runObject(['doctor']);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, Object?>> decisionSync() async {
|
||||||
|
return _runObject(['decisions', 'sync']);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Object?> decisionValidate() async {
|
||||||
|
return _runObject(['decisions', 'validate']);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Map<String, Object?>>> decisionList({
|
||||||
|
String? type,
|
||||||
|
String? domain,
|
||||||
|
String? status,
|
||||||
|
}) async {
|
||||||
|
final args = ['decisions', 'list'];
|
||||||
|
if (type != null) args.addAll(['--type', type]);
|
||||||
|
if (domain != null) args.addAll(['--domain', domain]);
|
||||||
|
if (status != null) args.addAll(['--status', status]);
|
||||||
|
return _runList(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, Object?>> decisionShow(
|
||||||
|
String id, {
|
||||||
|
bool withRefs = false,
|
||||||
|
bool withTickets = false,
|
||||||
|
}) async {
|
||||||
|
final args = ['decisions', 'show', id];
|
||||||
|
if (withRefs) args.add('--with-refs');
|
||||||
|
if (withTickets) args.add('--with-tickets');
|
||||||
|
return _runObject(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Map<String, Object?>>> decisionCoverage() async {
|
||||||
|
return _runList(['decisions', 'coverage']);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Map<String, Object?>>> ticketList({
|
||||||
|
String? status,
|
||||||
|
String? team,
|
||||||
|
String? assigned,
|
||||||
|
String? decision,
|
||||||
|
}) async {
|
||||||
|
final args = ['ticket', 'list'];
|
||||||
|
if (status != null) args.addAll(['--status', status]);
|
||||||
|
if (team != null) args.addAll(['--team', team]);
|
||||||
|
if (assigned != null) args.addAll(['--assigned', assigned]);
|
||||||
|
if (decision != null) args.addAll(['--decision', decision]);
|
||||||
|
return _runList(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, Object?>> ticketShow(
|
||||||
|
String id, {
|
||||||
|
bool withDecision = false,
|
||||||
|
bool withBlockers = false,
|
||||||
|
}) async {
|
||||||
|
final args = ['ticket', 'show', id];
|
||||||
|
if (withDecision) args.add('--with-decision');
|
||||||
|
if (withBlockers) args.add('--with-blockers');
|
||||||
|
return _runObject(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Map<String, Object?>>> ticketBoard({String? team}) async {
|
||||||
|
final args = ['ticket', 'board'];
|
||||||
|
if (team != null) args.addAll(['--team', team]);
|
||||||
|
return _runList(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, Object?>> planStatus() async {
|
||||||
|
return _runObject(['plan', 'status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
Future<List<Map<String, Object?>>> _runList(List<String> args) async {
|
||||||
|
final result = await _run(args);
|
||||||
|
if (result == null) return const [];
|
||||||
|
if (result is List) {
|
||||||
|
return [for (final e in result) (e as Map).cast<String, Object?>()];
|
||||||
|
}
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, Object?>> _runObject(List<String> args) async {
|
||||||
|
final result = await _run(args);
|
||||||
|
if (result is Map) return result.cast<String, Object?>();
|
||||||
|
return const {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Object?> _run(List<String> args) async {
|
||||||
|
final r = await Process.run(
|
||||||
|
pqlBinary,
|
||||||
|
args,
|
||||||
|
workingDirectory: workDir.path,
|
||||||
|
);
|
||||||
|
final stderr = (r.stderr as String).trim();
|
||||||
|
// Exit 2 = zero matches — valid empty result, not an error.
|
||||||
|
if (r.exitCode != 0 && r.exitCode != 2) {
|
||||||
|
throw PqlException(
|
||||||
|
'pql ${args.first} failed',
|
||||||
|
exitCode: r.exitCode,
|
||||||
|
stderr: stderr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final stdout = (r.stdout as String).trim();
|
||||||
|
if (stdout.isEmpty) return null;
|
||||||
|
return jsonDecode(stdout);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:clide/clide.dart';
|
||||||
|
import 'package:clide/src/daemon/pql_commands.dart';
|
||||||
|
import 'package:clide/src/pql/client.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late DaemonDispatcher dispatcher;
|
||||||
|
late PqlClient pql;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
pql = PqlClient(workDir: Directory.current);
|
||||||
|
dispatcher = DaemonDispatcher();
|
||||||
|
registerPqlCommands(dispatcher, pql);
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<IpcResponse> call(String cmd,
|
||||||
|
[Map<String, Object?> args = const {}]) {
|
||||||
|
return dispatcher.dispatch(IpcRequest(id: '1', cmd: cmd, args: args));
|
||||||
|
}
|
||||||
|
|
||||||
|
test('pql.files returns a list of files', () async {
|
||||||
|
final r = await call('pql.files', {'limit': 3});
|
||||||
|
expect(r.ok, isTrue);
|
||||||
|
final files = r.data['files'] as List;
|
||||||
|
expect(files, isNotEmpty);
|
||||||
|
expect(files.length, lessThanOrEqualTo(3));
|
||||||
|
final first = (files.first as Map).cast<String, Object?>();
|
||||||
|
expect(first.containsKey('path'), isTrue);
|
||||||
|
expect(first.containsKey('name'), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pql.meta returns file metadata', () async {
|
||||||
|
final r = await call('pql.meta', {'path': 'CLAUDE.md'});
|
||||||
|
expect(r.ok, isTrue);
|
||||||
|
expect(r.data['path'], 'CLAUDE.md');
|
||||||
|
expect(r.data.containsKey('outlinks'), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pql.meta without path returns error', () async {
|
||||||
|
final r = await call('pql.meta');
|
||||||
|
expect(r.ok, isFalse);
|
||||||
|
expect(r.error!.kind, 'user_error');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pql.outlinks returns links from a file', () async {
|
||||||
|
final r = await call('pql.outlinks', {'path': 'CLAUDE.md'});
|
||||||
|
expect(r.ok, isTrue);
|
||||||
|
final links = r.data['links'] as List;
|
||||||
|
expect(links, isNotEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pql.schema returns the frontmatter schema', () async {
|
||||||
|
final r = await call('pql.schema');
|
||||||
|
expect(r.ok, isTrue);
|
||||||
|
expect(r.data.containsKey('schema'), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pql.doctor returns diagnostic report', () async {
|
||||||
|
final r = await call('pql.doctor');
|
||||||
|
expect(r.ok, isTrue);
|
||||||
|
expect(r.data.containsKey('vault'), isTrue);
|
||||||
|
expect(r.data.containsKey('config'), isTrue);
|
||||||
|
expect(r.data.containsKey('version'), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pql.decisions.sync parses decisions', () async {
|
||||||
|
final r = await call('pql.decisions.sync');
|
||||||
|
expect(r.ok, isTrue);
|
||||||
|
expect(r.data.containsKey('synced'), isTrue);
|
||||||
|
expect((r.data['synced'] as num).toInt(), greaterThan(0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pql.decisions.list returns confirmed decisions', () async {
|
||||||
|
await call('pql.decisions.sync');
|
||||||
|
final r = await call('pql.decisions.list', {'type': 'confirmed'});
|
||||||
|
expect(r.ok, isTrue);
|
||||||
|
final decisions = r.data['decisions'] as List;
|
||||||
|
expect(decisions, isNotEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pql.decisions.show returns a single decision', () async {
|
||||||
|
await call('pql.decisions.sync');
|
||||||
|
final r = await call('pql.decisions.show', {'id': 'D-001'});
|
||||||
|
expect(r.ok, isTrue);
|
||||||
|
expect(r.data['id'], 'D-001');
|
||||||
|
expect(r.data['title'], isNotEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pql.decisions.show without id returns error', () async {
|
||||||
|
final r = await call('pql.decisions.show');
|
||||||
|
expect(r.ok, isFalse);
|
||||||
|
expect(r.error!.kind, 'user_error');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pql.decisions.coverage returns gaps', () async {
|
||||||
|
await call('pql.decisions.sync');
|
||||||
|
final r = await call('pql.decisions.coverage');
|
||||||
|
expect(r.ok, isTrue);
|
||||||
|
expect(r.data.containsKey('gaps'), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pql.tickets.board returns columns', () async {
|
||||||
|
final r = await call('pql.tickets.board');
|
||||||
|
expect(r.ok, isTrue);
|
||||||
|
expect(r.data.containsKey('columns'), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pql.plan.status returns dashboard', () async {
|
||||||
|
await call('pql.decisions.sync');
|
||||||
|
final r = await call('pql.plan.status');
|
||||||
|
expect(r.ok, isTrue);
|
||||||
|
expect(r.data.containsKey('decisions'), isTrue);
|
||||||
|
expect(r.data.containsKey('tickets'), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pql.query without query returns error', () async {
|
||||||
|
final r = await call('pql.query');
|
||||||
|
expect(r.ok, isFalse);
|
||||||
|
expect(r.error!.kind, 'user_error');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pql.backlinks without path returns error', () async {
|
||||||
|
final r = await call('pql.backlinks');
|
||||||
|
expect(r.ok, isFalse);
|
||||||
|
expect(r.error!.kind, 'user_error');
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user